supervisor: replace Go package with anvillm-supervisor shell script
Remove the internal/supervisor Go package and cron-driven orphan check script. Replace with a single anvillm-supervisor script invoked by cron that handles both orphan bead recovery (--orphans, every 5m) and idle worker nudging (--nudge, every 1m). Nudges use send_message.sh as user, consistent with existing mail processing. anvilsrv no longer runs any supervisor goroutine. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
989558cf7f
commit
47a97e5cea
|
|
@ -9,7 +9,6 @@ import (
|
|||
"anvillm/internal/maildir"
|
||||
"anvillm/internal/p9"
|
||||
"anvillm/internal/session"
|
||||
"anvillm/internal/supervisor"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -229,11 +228,6 @@ func start(daemonize bool) {
|
|||
mdWriter := maildir.New(mailDir, srv.Events())
|
||||
defer mdWriter.Close()
|
||||
|
||||
// Start supervisor
|
||||
ctx := context.Background()
|
||||
sup := supervisor.New(mgr, p9.NewRolesFS())
|
||||
go sup.Run(ctx)
|
||||
|
||||
logging.Logger().Info("anvilsrv started successfully", zap.String("socket", srv.SocketPath()))
|
||||
|
||||
// Start background monitor for self-healing
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ type RolesFS struct {
|
|||
type RoleMeta struct {
|
||||
Name string
|
||||
Description string
|
||||
Worker bool // true if role should run the work polling loop
|
||||
Path string // file path
|
||||
}
|
||||
|
||||
|
|
@ -88,9 +87,6 @@ func parseRoleFrontMatter(rolePath string) (*RoleMeta, error) {
|
|||
meta.Description = strings.TrimSpace(desc)
|
||||
hasDescription = true
|
||||
}
|
||||
if worker, ok := strings.CutPrefix(line, "worker:"); ok {
|
||||
meta.Worker = strings.TrimSpace(worker) == "true"
|
||||
}
|
||||
}
|
||||
|
||||
// If missing description, exclude from list
|
||||
|
|
@ -185,20 +181,6 @@ func (r *RolesFS) Read(path string) ([]byte, error) {
|
|||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
// IsWorker reports whether the named role has worker: true in its front-matter.
|
||||
func (r *RolesFS) IsWorker(roleName string) bool {
|
||||
roles, err := r.listAllRoles()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, role := range roles {
|
||||
if role.Name == roleName {
|
||||
return role.Worker
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ReadRole reads a role by name, searching all roles directories.
|
||||
func (r *RolesFS) ReadRole(roleName string) (string, error) {
|
||||
roles, err := r.listAllRoles()
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
package supervisor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"anvillm/internal/backend/tmux"
|
||||
"anvillm/internal/logging"
|
||||
"anvillm/internal/p9"
|
||||
"anvillm/internal/session"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const idleThreshold = 15 * time.Second
|
||||
|
||||
type Supervisor struct {
|
||||
sessions *session.Manager
|
||||
roles *p9.RolesFS
|
||||
}
|
||||
|
||||
func New(s *session.Manager, r *p9.RolesFS) *Supervisor {
|
||||
return &Supervisor{sessions: s, roles: r}
|
||||
}
|
||||
|
||||
// nudgeIdleWorkers sends a prompt to any worker-role session that has been
|
||||
// idle for longer than idleThreshold, reminding it to run its polling loop.
|
||||
func (s *Supervisor) nudgeIdleWorkers() {
|
||||
for _, botID := range s.sessions.List() {
|
||||
sess := s.sessions.Get(botID)
|
||||
if sess == nil || sess.State() != "idle" {
|
||||
continue
|
||||
}
|
||||
|
||||
tmuxSess, ok := sess.(*tmux.Session)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if tmuxSess.IdleDuration() < idleThreshold {
|
||||
continue
|
||||
}
|
||||
|
||||
role := tmuxSess.GetRole()
|
||||
if role == "" || !s.roles.IsWorker(role) {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if _, err := sess.Send(ctx, "You are idle. Run your work polling loop to check for available tasks."); err != nil {
|
||||
logging.Logger().Warn("supervisor: failed to nudge idle worker",
|
||||
zap.String("session", botID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logging.Logger().Error("supervisor panic", zap.Any("panic", r))
|
||||
}
|
||||
}()
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.nudgeIdleWorkers()
|
||||
}
|
||||
}
|
||||
}
|
||||
14
mkfile
14
mkfile
|
|
@ -21,8 +21,8 @@ build:V:
|
|||
chmod 0755 $INSTALL_PATH/9p-read-inbox
|
||||
cp scripts/anvilspawn $INSTALL_PATH/anvilspawn
|
||||
chmod 0755 $INSTALL_PATH/anvilspawn
|
||||
cp scripts/anvillm-orphan-check $INSTALL_PATH/anvillm-orphan-check
|
||||
chmod 0755 $INSTALL_PATH/anvillm-orphan-check
|
||||
cp scripts/anvillm-supervisor $INSTALL_PATH/anvillm-supervisor
|
||||
chmod 0755 $INSTALL_PATH/anvillm-supervisor
|
||||
mkdir -p $INSTALL_PATH/Teams
|
||||
cp -f team-templates/* $INSTALL_PATH/Teams/
|
||||
chmod 0755 $INSTALL_PATH/Teams/*
|
||||
|
|
@ -58,12 +58,14 @@ install:V: build
|
|||
|
||||
cron-install:V:
|
||||
mkdir -p $HOME/.local/share/anvillm
|
||||
(crontab -l 2>/dev/null | grep -v 'anvillm-orphan-check'; echo "*/5 * * * * $INSTALL_PATH/anvillm-orphan-check >>$HOME/.local/share/anvillm/orphan-check.log 2>&1") | crontab -
|
||||
echo "cron installed: anvillm-orphan-check (every 5 minutes)"
|
||||
(crontab -l 2>/dev/null | grep -v 'anvillm-supervisor'; \
|
||||
echo "*/5 * * * * $INSTALL_PATH/anvillm-supervisor --orphans >>$HOME/.local/share/anvillm/worker-check.log 2>&1"; \
|
||||
echo "*/1 * * * * $INSTALL_PATH/anvillm-supervisor --nudge >>$HOME/.local/share/anvillm/worker-check.log 2>&1") | crontab -
|
||||
echo "cron installed: anvillm-supervisor (orphans: every 5m, nudge: every 1m)"
|
||||
|
||||
cron-remove:V:
|
||||
(crontab -l 2>/dev/null | grep -v 'anvillm-orphan-check') | crontab -
|
||||
echo "cron removed: anvillm-orphan-check"
|
||||
(crontab -l 2>/dev/null | grep -v 'anvillm-supervisor') | crontab -
|
||||
echo "cron removed: anvillm-supervisor"
|
||||
|
||||
clean:V:
|
||||
rm -f $INSTALL_PATH/anvilsrv
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Orphan bead recovery: find in_progress beads whose assignee session has died
|
||||
# and unclaim them. BeadReady fires automatically; waiting bots claim via event.
|
||||
# Intended to run from cron every 5 minutes.
|
||||
set -euo pipefail
|
||||
|
||||
LOG_PREFIX="[orphan-check $(date '+%Y-%m-%d %H:%M:%S')]"
|
||||
log() { echo "$LOG_PREFIX $*"; }
|
||||
|
||||
# Get live session IDs
|
||||
LIVE_SESSIONS=$(9p read anvillm/list 2>/dev/null | awk -F'\t' '{print $1}' || true)
|
||||
|
||||
# Get all mounts
|
||||
MTAB=$(9p read anvillm/beads/mtab 2>/dev/null || true)
|
||||
if [ -z "$MTAB" ]; then
|
||||
log "no mounts, nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while IFS=$'\t' read -r MOUNT CWD; do
|
||||
[ -z "$MOUNT" ] && continue
|
||||
|
||||
IN_PROGRESS=$(9p read "anvillm/beads/$MOUNT/list" 2>/dev/null \
|
||||
| jq -r '.[] | select(.status == "in_progress") | [.id, (.assignee // "")] | @tsv' \
|
||||
2>/dev/null || true)
|
||||
|
||||
[ -z "$IN_PROGRESS" ] && continue
|
||||
|
||||
while IFS=$'\t' read -r BEAD_ID ASSIGNEE; do
|
||||
[ -z "$BEAD_ID" ] && continue
|
||||
|
||||
# Skip if claimed by user (human in the loop — not an orphan)
|
||||
[ "$ASSIGNEE" = "user" ] && continue
|
||||
|
||||
# Skip if assignee session is still alive
|
||||
if [ -n "$ASSIGNEE" ] && echo "$LIVE_SESSIONS" | grep -qx "$ASSIGNEE"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
log "orphan: $BEAD_ID (was: ${ASSIGNEE:-unassigned}) mount=$MOUNT — unclaiming"
|
||||
echo "unclaim $BEAD_ID" | 9p write "anvillm/beads/$MOUNT/ctl" 2>/dev/null || \
|
||||
log "WARNING: unclaim failed for $BEAD_ID"
|
||||
|
||||
done <<< "$IN_PROGRESS"
|
||||
done <<< "$MTAB"
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
#!/bin/bash
|
||||
# Periodic worker health check: orphan bead recovery and idle worker nudging.
|
||||
# Usage: anvillm-worker-check [--orphans] [--nudge]
|
||||
set -euo pipefail
|
||||
|
||||
LOG_PREFIX="[worker-check $(date '+%Y-%m-%d %H:%M:%S')]"
|
||||
log() { echo "$LOG_PREFIX $*"; }
|
||||
|
||||
DO_ORPHANS=0
|
||||
DO_NUDGE=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--orphans) DO_ORPHANS=1; shift ;;
|
||||
--nudge) DO_NUDGE=1; shift ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$DO_ORPHANS" -eq 0 ] && [ "$DO_NUDGE" -eq 0 ]; then
|
||||
echo "usage: anvillm-worker-check [--orphans] [--nudge]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SESSIONS=$(9p read anvillm/list 2>/dev/null || true)
|
||||
|
||||
# --- Orphan recovery ---
|
||||
if [ "$DO_ORPHANS" -eq 1 ]; then
|
||||
LIVE_SESSIONS=$(echo "$SESSIONS" | awk -F'\t' '{print $1}' || true)
|
||||
|
||||
MTAB=$(9p read anvillm/beads/mtab 2>/dev/null || true)
|
||||
if [ -z "$MTAB" ]; then
|
||||
log "orphans: no mounts, nothing to do"
|
||||
else
|
||||
while IFS=$'\t' read -r MOUNT CWD; do
|
||||
[ -z "$MOUNT" ] && continue
|
||||
|
||||
IN_PROGRESS=$(9p read "anvillm/beads/$MOUNT/list" 2>/dev/null \
|
||||
| jq -r '.[] | select(.status == "in_progress") | [.id, (.assignee // "")] | @tsv' \
|
||||
2>/dev/null || true)
|
||||
|
||||
[ -z "$IN_PROGRESS" ] && continue
|
||||
|
||||
while IFS=$'\t' read -r BEAD_ID ASSIGNEE; do
|
||||
[ -z "$BEAD_ID" ] && continue
|
||||
[ "$ASSIGNEE" = "user" ] && continue
|
||||
if [ -n "$ASSIGNEE" ] && echo "$LIVE_SESSIONS" | grep -qx "$ASSIGNEE"; then
|
||||
continue
|
||||
fi
|
||||
log "orphan: $BEAD_ID (was: ${ASSIGNEE:-unassigned}) mount=$MOUNT — unclaiming"
|
||||
echo "unclaim $BEAD_ID" | 9p write "anvillm/beads/$MOUNT/ctl" 2>/dev/null || \
|
||||
log "WARNING: unclaim failed for $BEAD_ID"
|
||||
done <<< "$IN_PROGRESS"
|
||||
done <<< "$MTAB"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Nudge idle workers ---
|
||||
if [ "$DO_NUDGE" -eq 1 ]; then
|
||||
[ -z "$SESSIONS" ] && { log "nudge: no sessions"; exit 0; }
|
||||
|
||||
BODY='Use the execute_code tool to run wait_for_bead.sh and check for available work.'
|
||||
|
||||
while IFS=$'\t' read -r ID BACKEND STATE ALIAS ROLE CWD; do
|
||||
[ -z "$ID" ] && continue
|
||||
[ "$STATE" != "idle" ] && continue
|
||||
[ "$ROLE" = "-" ] || [ -z "$ROLE" ] && continue
|
||||
|
||||
ROLE_CONTENT=$(9p read "anvillm/roles/${ROLE}.md" 2>/dev/null || true)
|
||||
echo "$ROLE_CONTENT" | grep -q '^worker: true' || continue
|
||||
|
||||
log "nudge: $ID (role=$ROLE)"
|
||||
AGENT_ID=user send_message.sh \
|
||||
--to "$ID" \
|
||||
--type prompt_request \
|
||||
--subject work \
|
||||
--body "$BODY" || \
|
||||
log "WARNING: nudge failed for $ID"
|
||||
done <<< "$SESSIONS"
|
||||
fi
|
||||
Reference in New Issue