16 KiB
2026-08-04 — Session audit: agent reliability, plan system, and learning loop gaps
Status: Done — all 12 tasks implemented, tested, deployed in v0.21.0. Verified in production
with live session tests (classifications, feedback, token tracking, execution linkage all confirmed).
Reviewed sessions: Past 5 completed plus ZimaOS continuation (268895a5)
Method: Direct Postgres read of agent_sessions/agent_messages/
agent_activity/session_plan_steps/executions/classifications/feedback/
patterns/skills/approvals/nomos_plan_executions/audit_log on the prod
mac-mini. Cross-referenced with internal/audit/, internal/mcp/,
internal/httpapi/, internal/policy/.
1. Sessions audited
| # | Session | Turns | Run calls | Failures | Plan steps (done/total) | Duration | Outcome |
|---|---|---|---|---|---|---|---|
| S1 | Pocket-pascal deploy (a433b386) | 8 | 111 | 1 | 10/11 | 1.5h | Success (truncated by turn limit) |
| S2 | SSH re-investigation (6f0ade08) | 4 | 42 | 0 | 0/4 | 11m | Success (all steps replaced) |
| S3 | Webhook HMAC (0f509508) | 2 | 40 | 0 | 5/5 | 2m | Success (clean; best session) |
| S4 | Check scripts (d458a5f8) | 23 | 96 | 4 | 0/14 | 2h | Success (3 plan gens; 0 steps done) |
| S5 | ZimaOS outage (268895a5) | 18+10 | 209 | 8 | 0/5 | 2h30m | Marked success; dashboard still broken |
Headline: 85% session success rate, but plan adherence is ~38% (15/39 steps
ever reached done). The ZimaOS session was the worst: marked success while
the dashboard was still down, then burned 81 more calls and 30 minutes chasing
irrelevant DHCP reservations.
2. Cross-cutting findings
F1 — MCP 30s client timeout kills every long-running command (P0)
All 9 run failures across the audit are Post "http://api:8090/mcp": context deadline exceeded at exactly 30s. Commands with sleep 30, qm shutdown+wait, or async poll loops always hit this. The agent retries with
longer sleeps and hits the same wall.
Root cause: cmd/nomos MCP client uses a 30s timeout; the run tool
blocks synchronously waiting for command completion. No async path exists for
long-running commands.
F2 — Premature success: complete_task fires before the goal is actually met (P0)
Session 268895a5 called complete_task(success) at 18:29 with summary "zimaos
returns 200." The endpoint was serving ttyd (terminal), not the ZimaOS
dashboard. The agent conflated "HTTPS 200" with "dashboard works." The user had
to resume the session.
Root cause: No pre-completion validation. The agent can mark success with a
summary that doesn't match reality. complete_task is a write-and-forget
operation with no state check.
F3 — Wrong target: run doesn't validate the target can execute the command (P0)
In the ZimaOS continuation, qm stop 100 --skiplock was executed on
lxc:dns. The command failed (qm: command not found) because the agent
copied the command from a previous run but forgot to change the target.
Similarly, cmd/nomos tried docker exec on host:hubris (no docker).
Root cause: run(target, command) in internal/mcp/server.go doesn't
validate that the target type can execute the given command. A simple
allowlist would catch qm/pct/pvesh on non-host targets.
F4 — Plan system is decorative: 62% of steps never reach done (P1)
Across 5 sessions: 39 plan steps. 24 (62%) were replaced, 15 (38%) reached
done. Session d458a5f8 had 3 complete plan regenerations with zero
completed steps. The agent replaces plans instead of completing or explicitly
skipping steps.
Root cause: Plan steps carry status (pending/running/done/failed/
skipped/blocked/replaced) but replaced has no replaced_reason
field. The model can silently replace every step and the system doesn't flag
it. No constraint ties propose_plan to existing plan state.
F5 — No plan on session resume: continuation sessions run ad-hoc (P1)
The ZimaOS continuation (18:29→19:01) had 81 calls with zero
propose_plan calls. The agent ran ad-hoc tool calls with no structure.
Root cause: When a session resumes, the must_have_plan guard is already
satisfied by the old (completed) plan. The agent doesn't re-plan on resume.
F6 — Scope expansion / rabbit holes: agent chases irrelevant sub-goals (P2)
In the ZimaOS continuation, the agent spent ~40 calls trying to fix Technitium DHCP reservations — a completely different subsystem from the goal ("make the dashboard reachable"). The ZimaOS dashboard hadn't started since July 19 (3-week-old issue), making the DHCP reservation effort moot. The agent never surfaced a question like "This is pre-existing — should I still fix DHCP?"
Root cause: No scope gate. When the agent pivots to a subsystem unrelated
to the stated goal, nothing stops it. The session_questions mechanism exists
(2 calls in 193 sessions) but the model never uses it.
F7 — Command generation errors: malformed bash from the LLM (P2)
In the ZimaOS continuation, the agent generated:
head - ninstead ofhead -n→ bash syntax errorecho "---" && \n curl ...→ literal\nin command → ambiguous redirect
Root cause: The LLM generates bash commands inline in content blocks. No
syntax validation, no escape-character handling. The run tool should reject
malformed commands before execution.
F8 — Learning pipeline completely empty (P4)
| Table | Rows |
|---|---|
classifications |
0 |
feedback |
0 |
patterns |
0 |
skills |
0 |
Despite 1,884 executions and 263 approvals, the system learns nothing from
outcomes. The ZimaOS session discovered that the dashboard hadn't started since
July 19 — this was never persisted. The HMAC trailing-newline discovery was
persisted manually; if the agent forgot upsert_knowledge, it would be lost.
F9 — Observability gaps (P3)
| Gap | Detail |
|---|---|
| Token tracking | agent_activity.token_count is NULL for every row |
| Execution linkage | nomos_plan_executions is empty; audit_log.session_id is null |
| Plan quality | No metric for step completion rate (currently 38%) |
| MCP timeout rate | No counter for run calls that hit the client timeout |
F10 — Execution success rate is 74% (P2)
1,884 executions: 1,400 completed (74%), 295 failed (15.6%), 152 cancelled (8%), 37 denied (2%). One in four execution attempts doesn't complete.
3. Improvement plan (ordered by impact/effort)
Task 1 — Prevent premature complete_task(success) (fixes F2)
internal/mcp/tools.go — In the complete_task handler, when
outcome=success: require the summary field to contain a verifiable state
assertion. Minimum: if the goal mentions a URL, check that the summary doesn't
contradict known state. Lightweight: log a warning if the summary says "returns
200" but the last ping_service or run result says otherwise.
Coach: nomos/SOUL.md — explicit rule: "Before calling
complete_task(success), restate the user's original goal in your own words and
verify each condition. If any condition is 'probably works' rather than
'verified,' ask the operator or set outcome=partial."
Task 2 — Target validation in run (fixes F3)
internal/mcp/server.go — In the run handler, before dispatching:
validate that the command prefix matches the target type.
pct/qm/pvesh/iptables → only host:* targets
systemctl/docker → host:* or lxc:* targets
curl/nmap/ss → any target
If mismatched, return a clear error: "Cannot run qm on lxc:dns — qm is a
Proxmox host command. Use target host:hubris or host:strong." Do not classify
or execute.
Test: TestClassifyCommand_WrongTarget → commands with host-only prefixes
on LXC targets return error without execution.
Task 3 — Raise MCP client timeout; add async path for long-running commands (fixes F1)
cmd/nomos — Raise the MCP client timeout from 30s to 120s.
internal/mcp/server.go — For run commands that the classifier
determines will exceed the client timeout (presence of sleep, wait,
timeout in the command), return immediately with an execution_id and status
running. The agent already has get_execution_status — use it:
- Classify the command; if it contains
sleep,wait, or shell constructs that imply polling, flag it asasync_potential. - Start the command, return the
execution_idimmediately. - Agent polls with
get_execution_status(execution_id). - If the client timeout is hit mid-poll, the execution continues on the server — it's not lost.
Test: run with sleep 60; echo done on host:hubris → returns
immediately (not 30s timeout), get_execution_status eventually returns
completed.
Task 4 — Plan step integrity: require replaced_reason on replacement (fixes F4)
session_plan_steps migration — Add replaced_reason TEXT column.
cmd/nomos — When the agent emits update_plan_step with
status=replaced, require a non-empty replaced_reason. Valid reasons:
wrong_diagnosis, scope_change, blocked, superseded, operator_override.
Coach: nomos/SOUL.md — explicit rule: "Complete (status=done) or
explicitly skip (status=skipped) steps. Use status=replaced only when the
entire plan generation is wrong; include the reason. Replacing all steps with
no reason is a session-quality violation."
Task 5 — Force propose_plan on session resume (fixes F5)
cmd/nomos — When a session with status done or failed receives a new
user message, reset the plan state: clear step status, require a new
propose_plan call before any run calls. The "must have plan" guard should
consider the resumed session as plan-less until a fresh propose_plan is
called.
Guard: set_goal + propose_plan must be called before any run in a
resumed session. Reuse the existing "No plan — call set_goal then propose_plan"
error from internal/mcp/server.go.
Task 6 — Scope gate: surface session_questions on context switch (fixes F6)
Coach: nomos/SOUL.md — explicit rule: "Before pivoting to a subsystem
not mentioned in the user's goal, ask via session_questions. Example: 'The
dashboard logs show it hasn't started since July 19. Do you want me to debug
the dashboard service itself [A], skip it and just stabilize the IP [B], or
stop here [C]?'"
cmd/nomos prompt — Add to the system prompt: "When the investigation
leads to a subsystem or root cause unrelated to the expressed goal, surface a
session_question before taking action."
Task 7 — Auto-upsert knowledge on session close (fixes F8)
cmd/nomos — On complete_task (any outcome: success, partial, failure),
auto-generate a knowledge entry:
title: "<date>: <goal summary>"
content: "## Outcome\n<outcome>\n## Root cause\n<extracted>\n## What was done\n<summary>\n## What was left\n<unresolved>"
tags: [session:<id>]
about: [entities involved]
This ensures every session leaves a trace regardless of whether the agent
remembered to call upsert_knowledge.
Task 8 — Token tracking (fixes F9)
cmd/nomos — After each LLM call, extract usage.prompt_tokens,
usage.completion_tokens, usage.total_tokens from the response and write to
agent_activity.token_count. Currently the field exists but is never populated
(NULL for all rows).
Task 9 — Execution linkage (fixes F9)
internal/mcp/server.go — When run creates an execution, write a row
into nomos_plan_executions linking session_id, plan_step_seq, and
execution_id.
internal/mcp/server.go — Pass session_id (from MCP request headers)
into audit_log writes. Currently audit_log.session_id is NULL — the
createAuditLog function in internal/httpapi/impl.go receives the
correlation_id but not the session_id from the MCP path.
Task 10 — Plan quality metric (fixes F9)
cmd/nomos — At session close, compute: completed_steps / total_steps_per_plan (currently ~38%). Log as a metric or write as a session
attribute. Track over time to measure plan-adherence improvements from Tasks
4+5.
Task 11 — Auto-classify every run call (fixes F8)
internal/mcp/server.go — The run handler already calls the classifier
(classifyCommand in internal/policy/command.go) to determine risk_class and
approval route. Write the result to the classifications table. Currently the
table is empty (0 rows) despite 1,884 executions being classified.
Task 12 — Auto-feedback on session close (fixes F8)
cmd/nomos — On complete_task, generate a feedback entry:
session_id: <id>
outcome: <outcome>
observation: <summary>
lesson: <extracted from complete_task.summary>
side_effects: <entities created/modified during session>
cmd/oikos — Add a daily cron or scheduler job that reads recent
feedback entries and extracts patterns (recurring root causes, same-fix
applied multiple times, known-broken services). Seed the pattern table.
Task 13 — Command syntax validation in run (fixes F7)
internal/mcp/server.go — Before executing a run command, do
lightweight bash syntax validation:
- Reject literal \n in commands (should be ; or &&)
- Reject commands where the last line ends with \ (backslash-continuation)
but no next line
- Warn on common typos: "head - n", "grep - i", spaces before flags
- Reject `&& \n` patterns (the LLM sometimes inserts literal \n between && chains)
Task 14 — Stuck-session reaping (from prior plan; re-confirmed)
This session exhibited the same idle zombie pattern (23da10db — no closed_at,
status failed but outcome failure). Task 5 from the 2026-08-03 plan is
still open. Copying here for completeness.
4. Recommended sequence
P0 (blocks operational waste):
1 → 2 → 3
P1 (fixes plan architecture):
4 → 5
P2 (cognitive guardrails):
6 → 7 → 13
P3 (observability):
8 → 9 → 10
P4 (learning loop):
11 → 12
Sequence rationale: Tasks 1-3 stop the worst outcomes (premature success, wrong-target execution, MCP timeouts). Tasks 4-5 make the plan system actually useful instead of decorative. Tasks 6-7 add guardrails that prevent the ZimaOS rabbit-hole class of failure. Tasks 8-10 give us visibility into whether any of the previous tasks are working. Tasks 11-12 close the learning loop.
5. Validation
| Task | Test |
|---|---|
| 1 | Session with goal "make X reachable" where last ping shows 502 → complete_task(success) is rejected or warns |
| 2 | run("lxc:dns", "qm stop 100") → error: "qm is a Proxmox host command" |
| 3 | run with sleep 45; echo done → returns execution_id immediately, get_execution_status shows final result |
| 4 | update_plan_step(status=replaced) with no reason → rejected; with reason → accepted |
| 5 | Resumed session calls run before propose_plan → blocked: "No plan — call propose_plan" |
| 6 | Agent pivots to unrelated subsystem → session_questions is called before action |
| 7 | complete_task → knowledge entry created automatically with session link |
| 8 | agent_activity.token_count is non-NULL after any LLM call |
| 9 | nomos_plan_executions has rows linking session + step + execution |
| 10 | Session close writes plan_adherence attribute (step-completion %) |
| 11 | classifications table has 1 row per run call with risk_class + route |
| 12 | complete_task → auto feedback entry; daily pattern job finds recurring issues |
| 13 | run with head - n /etc/hosts → rejected with clear error about malformed command |
6. Out of scope / open questions
- Whether to raise
auto_actfromoffforreversible_lowactions (separate policy decision; would reduce approval pileup without code changes). - Whether to add a
delete_entityMCP tool for lifecycle management (separate from this reliability plan). - The exact TTL for stuck-session reaping (30 min recommended, confirmed in 2026-08-03 plan).
- Whether
runasync mode should be opt-in (command contains sleep/wait) or universal (every run returns immediately, agent always polls). Recommend opt-in for now — most commands complete in <5s.