Skip to content

WS-AUTO-002 — AFK Work Loop : worker autonome + heartbeats dynamiques - #6

Merged
MKheru merged 6 commits into
acos-mainfrom
ws-auto-002-afk-work-loop
May 4, 2026
Merged

WS-AUTO-002 — AFK Work Loop : worker autonome + heartbeats dynamiques#6
MKheru merged 6 commits into
acos-mainfrom
ws-auto-002-afk-work-loop

Conversation

@MKheru

@MKheru MKheru commented May 4, 2026

Copy link
Copy Markdown
Owner

Résumé

Pendant que AH est en afk_manual ou afk_auto, un thread daemon tick toutes les 30 min, pick une tâche taguée dans les status files, route vers un provider/modèle adapté, délègue à un sub-agent leaf, et logue le cycle. Les heartbeats (J3/J7/J14) embarquent maintenant un récap déterministe des cycles exécutés au lieu du wording aspirationnel.

Spec complète : ~/Documents/Projects/ACOS-HERMES/.workspace/WS_AFK_WORK_LOOP_SPEC.md (~830 lignes, 15 sections).

Fichiers

Path Type Lignes
agent/afk_worker.py nouveau — module principal worker ~720
agent/afk_heartbeat.py modifié — _build_dynamic_recap() + refonte HB1/HB2/HB3 +120
agent/afk_scheduler.py modifié — purge picked-index quand AH sort d'AFK (OQ4) +9
tools/delegate_tool.py modifié — 4 params optionnels override (model/provider/base_url/api_key), non-breaking +24
gateway/run.py modifié — thread afk-worker au startup, transient parent_agent build +89
tests/agent/test_afk_worker.py nouveau — 40 tests (unit + integration + verbosity + sécurité) ~700

Décisions de design (Q1-Q10 + B1-B3 + OQ1-OQ7)

  • Q1 Tick 30 min (configurable)
  • Q2 Status files only en Phase 1 (~/SMCP_STATUS.md, etc. configurables)
  • Q3 Priorité explicite par tags [priority:Pn], fallback FIFO ; exclusion [afk:no] / [blocked:*]
  • Q4 Matrice YAML statique + tags [afk:type] ; decision_structurelle → block + post Discord
  • Q5 Role leaf strict
  • Q6 1 délégation séquentielle par tick
  • Q7 Skip si quota MiniMax ≥ 80 % sur 5h OU semaine
  • Q8 Worker stoppe à stand_by (silent skip)
  • Q9 Logs : Markdown (~/AFK_LOG.md) + JSONL (~/.hermes/afk_log.jsonl) en parallèle
  • Q10 Heartbeats : recap déterministe ; synthèse LLM optionnelle (default off)
  • B2 Thread daemon (pas asyncio task — delegate_task est synchrone via ThreadPoolExecutor)
  • OQ1 Worker partage l'agent de session si dispo, sinon spawn un transient AIAgent dédié (pattern cron/scheduler.py)
  • OQ4 Picked-index purgé via hook dans process_user_message quand mode flip → normal

Sécurité — §9.1, §9.3.2, §9.3.4, §2.4

  • Aucun paramètre hardcodé : tout via ~/.hermes/config.yaml afk_worker.*
  • Pas de subprocess.run/fork+exec : thread Python pur (SECCOMP-safe)
  • Sub-agent reste sous le tool layer publish blacklist
  • Tests sécurité : worker ne modifie pas les status files, OPENROUTER_API_KEY value never appears in any log file
  • Cooldown : 3 erreurs consécutives → cooldown_level += 1 (skip 2 / 4 ticks ; halt + Discord notif au niveau 3)

Test plan

  • 40 tests unitaires + integration + sécurité — tous green
  • Étape 1 (déployé désactivé) — service boot OK, worker thread exits propre
  • Étape 2 (mécanique E2E avec status_files vides) — 2 ticks skipped_no_task consécutifs, format MD+JSONL OK
  • Étape 3 (vraie backlog 12h overnight) — 136 cycles, 3 délégations productives complètes via MiniMax (audit memo, RU/JA/PT memo, README lab-smcp), 0 modification du code source hermes-agent
  • Bug 2 fix : status=completed avec API fail → promu à error pour retry
  • Compatible avec config nouvelle ère Codex (main GPT-5.5 + delegation MiniMax) — la matrice simplifiée hérite de delegation.*

Notes

  • Bug 1 OpenRouter (sub-agent fail systématique sur calls Sonnet/OpenRouter) devenu non-pertinent — la config n'utilise plus OpenRouter pour le worker. Le code des overrides reste utilisable pour any future per-call provider routing.
  • Workflow recommandé pour activer en prod : afk_worker.enabled: true + tagger les tâches du backlog. Le picked-index dedupe garantit qu'on ne re-pick pas une tâche complete tant qu'elle reste in_progress dans le status file.

Anu Kheru added 6 commits May 3, 2026 18:42
While AH is in afk_manual or afk_auto, a new background thread
(agent/afk_worker.py) ticks every 30 min, picks a tagged in_progress
task from configured status files, routes it to a per-type provider/
model (Sonnet via OpenRouter for code, MiniMax for research/doc/
summary), and delegates execution to a leaf sub-agent. Each cycle
is appended to ~/AFK_LOG.md (markdown) and ~/.hermes/afk_log.jsonl
(machine-readable). MAX_DEPTH=50 finally has a chain to consume.

Heartbeats (HB1/HB2/HB3) now embed a deterministic recap built from
afk_log.jsonl — counts per status, top 3 completed tasks, current
quota, cooldown level — replacing the aspirational "to be filled
by AH later" wording.

Safety:
- §9.1 §9.3.2 §9.3.4: all parameters via ~/.hermes/config.yaml
  afk_worker.* (interval, status_files, threshold, model_routing,
  toolsets, log paths, picked-index path)
- §2.4 SECCOMP: thread daemon in hermes-agent.service, no fork+exec
- §2.5 budget: MiniMax quota gate at 80% (5h OR weekly)
- Tags: [priority:P0|P1|P2], [afk:research|code_review|...],
  [afk:no], [blocked:*], [afk:decision_structurelle] (auto Discord
  block + skip)
- Picked-index dedupe (~/.hermes/afk_worker_picked.json) avoids
  re-picking completed tasks; auto-purged when AH exits AFK
- Cooldown: 3 errors -> cooldown_level++ (skip 2/4 ticks; halt at 3)
- delegate_task gains 4 optional params (model_override,
  provider_override, base_url_override, api_key_override) - non-
  breaking, lets the worker route per-call without mutating config

Tests: 33 new + 53 existing AFK tests all green (no regression).
Per Khéri review of Phase 4 Étape 2: the worker was silent during
delegation cycles (only AFK_LOG.md/jsonl tracked activity). Added
2 Discord posts per delegated cycle so Khéri can observe the worker
working in real-time:

  - Pre-delegation: "🔄 AFK cycle starting" with task title, source
    file/section/line, AFK type, priority, model, toolsets, hint
    that the 600s timeout applies.
  - Post-delegation: "✅/❌ AFK cycle done" with status, api_calls,
    duration, error if any, sub-agent summary (≤500 chars), current
    MiniMax quota, pointer to ~/AFK_LOG.md.

Skipped statuses (skipped_quota, skipped_no_task, skipped_cooldown)
remain silent to avoid spam — only successful/errored cycles post.
The existing decision_blocked + cooldown_halt posts are unchanged.

Behavior is gated by afk_worker.verbose in config.yaml. Default is
True (the new default — matches Khéri's preference). Set false for
silent operation if needed later.

6 new tests cover the verbose flag (default true, explicit on/off,
2 posts on completed cycle, 0 posts when verbose=false, 0 posts on
skipped_no_task regardless). Total: 39 tests, all green.
Étape 3 testing on VPS exposed an OQ1 design gap: the worker assumed
runner._running_agents would always have at least one entry to reuse
as parent_agent for delegate_task. In practice the gateway is idle
when the worker ticks (no Discord conversation underway), so the
registry is empty and every cycle errored:

  delegation: error, duration: 0s,
  error: "no parent_agent available at runtime"

Fix: _get_afk_parent_agent now follows the cron/scheduler.py pattern.
If no session agent exists, build a transient AIAgent dedicated to
the worker (quiet_mode, skip_context_files, skip_memory, platform=
"afk_worker") and cache it for subsequent ticks. Credentials come
from _resolve_runtime_agent_kwargs() — same chain the gateway uses.

The session-agent path stays as the cheap fast-path when an active
conversation is running (free reuse). Transient build only happens
when truly idle.
Overnight run produced 3 real completions via MiniMax + 3 false
"completed" cycles where Sonnet/OpenRouter exhausted retries:

  status=completed
  summary="API call failed after 3 retries: "

Bug 2 (fixed here): delegate_task hands back status=completed even
when the child's LLM call never returned. The worker took it at face
value and marked tasks as done in the picked-index, blocking retry.
Fix: in _run_delegation, detect summaries starting with "API call
failed" and promote status to error. The picked-index then schedules
a 3-tick cooldown and retries instead of locking the task as done.
+1 test simulating the prod failure pattern.

Bug 1 (still open — debug logs added): direct curl from VPS with
the same OPENROUTER_API_KEY → HTTP 200, Sonnet replies. But
delegate_task → child agent → openrouter call → 3 retries failed.
Override propagation likely loses the api_key somewhere between the
worker call and the child runtime. Added INFO logs at three points:

  1. afk_worker._run_delegation: provider/model/base_url/api_key_len
     before calling delegate_task
  2. delegate_task: same after applying the 4 overrides to creds
  3. _build_child_agent: effective creds vs parent creds for diff

All length-only on api_key — no value ever logged (§1).
Once we see the chain in journalctl, we'll know exactly where the
override falls back to parent_api_key (which is MiniMax for the
transient AFK parent, hence the 401-equivalent).
Suspect the conditional 'if any(...)' debug log isn't firing for some
reason (override seems to propagate based on auxiliary_client log, but
no INFO from tools.delegate_tool appears in journal). Add an entry log
without any condition to confirm code path is executed and override
kwargs are received.
These were added to trace where the override propagation lost the
api_key for OpenRouter calls. Bug 1 became moot once we switched
the AFK matrix away from openrouter to MiniMax (sub-agents) under
Codex (main). Keeping the override-application code (4 if-blocks)
since the wiring is correct and useful for any future per-call
provider override.

The afk_worker side log ('afk-worker delegation overrides: ...')
stays in place — it's useful in steady-state to see what the worker
picks per tick, length-only on api_key.

Tests: 40/40 still green.
@MKheru
MKheru merged commit 084f120 into acos-main May 4, 2026
@MKheru
MKheru deleted the ws-auto-002-afk-work-loop branch May 4, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant