Move ten completed plans from plans/ to plans/done/ and update the index:
- 2026-07-18 session-review-three-sessions, 2026-07-20 desktop-mascot,
2026-07-20 session-review-ten-sessions, 2026-07-21 chat-full-polish,
2026-07-29 health-check-reality-and-knowledge-graph,
2026-07-30 session-review-plan-drift, and the four 2026-08-03 chat plans
(changes-review, reliability-and-ux-audit, cyberspace-style-adoption,
working-visibility).
- Refresh two stale statuses: cyberspace-style-adoption ("Draft" -> shipped as
full replacement in v0.16.0/757ef2f) and health-check-reality ("ready for
implementation" -> shipped across the v0.14.x-0.16.x check commits).
- .gitignore: ignore local tooling artifacts (.playwright-mcp/, config-screen.png).
No code change. index.md Active/Done tables now match the filesystem (no orphans).
VERSION: 0.17.0 -> 0.17.1
17 KiB
2026-07-20 — Session review: past 10 sessions
Status: Implemented — all P0/P1/P2 items landed in v0.7.13.
Scope: Ten most-recently-active agent:nomos sessions by
last_active_at, pulled from http://localhost:8092/sessions on
2026-07-20. Method per .agents/skills/session-review/SKILL.md. Three
(1e9c7691, 55927f0a, 2926de4e) overlap with the 2026-07-18 review
and are summarized; the other seven are new.
Sessions reviewed
| # | sid | goal (short) | outcome | msgs | toolcalls | top tools |
|---|---|---|---|---|---|---|
| 1 | a51e2086 |
reset rclone-backup & re-run | partial | 12 | 20 | run:7, set_goal:2, propose_plan:2, get_execution_status:2 |
| 2 | fefa4fa3 |
fix rclone OOM | success | 9 | 84 | run:28, update_plan_step:15, get_entity:8, list_entities:5 |
| 3 | 95fdd322 |
quick fleet health check | success | 3 | 6 | get_health_summary/state_snapshot/list_lxcs/signal_history |
| 4 | 8c76bb3a |
greeting + title-sync test | success | 2 | 9 | update_plan_step:4, propose_plan, whoami, get_state_snapshot |
| 5 | 438ec8bd |
(no goal set) greeting | success | 2 | 2 | whoami, get_health_summary |
| 6 | 8acea2e3 |
inspect rclone timer (live) | partial | 4 | 19 | run:6, update_plan_step:5, propose_plan, get_entity_knowledge |
| 7 | 1e9c7691 |
debug chown hang on strong | success | 13 | 97 | run:60, update_plan_step:7, get_execution_status:7 |
| 8 | 55927f0a |
add NFS ludo-lvm → ZimaOS | success | 25 | 104 | run:49, update_plan_step:13, get_entity:10 |
| 9 | 2926de4e |
apt upgrade host:netbird-vps | success | 9 | 27 | update_plan_step:7, run:6, search_knowledge:2 |
| 10 | cb8c8a4a |
inspect rclone timer (live) | success | 2 | 14 | update_plan_step:4, run:4, get_entity_knowledge |
Score: 8 success / 2 partial / 0 blocked. No message exceeded 2.8 KB.
What worked
- Read-only DB Q&A is now clean.
95fdd322and438ec8bddid exactly what the 2026-07-18 review asked: pure-DB question →get_health_summary+get_state_snapshot+list_lxcs, norun. The agent even narrates "This is a pure-DB Q&A — noruncalls needed." - Knowledge writeback hygiene continues. Every long-running session
did
upsert_knowledge+update_entity_attributes+create_relationshipwhen applicable. The graph is current. - Plan lifecycle is followed everywhere —
set_goal→propose_plan→update_plan_step→complete_task. Even trivial sessions (greeting) follow it. - Poll-after-timeout pattern is now the default —
fefa4fa3after the rclone LXC reboot,2926de4eafter the apt upgrade. No more blind retry storms like the 2026-07-18 chown case. - The rclone saga ended well (
fefa4fa3): root cause (2 GiB LXC OOM) was diagnosed via DB + live check; fix (pct set 2→4 GiB) was applied; test backup verified 245 transfers / 4 min / no OOM.
What didn't
1. The rclone objective took three sessions to close (blocker)
Same operator goal — "rclone backup is broken" — spawned a51e2086
(partial), 8acea2e3 (partial), cb8c8a4a (success), and finally
fefa4fa3 (success). The first three were the agent trying to inspect
the live systemd state and bouncing off the classifier:
8acea2e3:pct exec 132 systemctl status rclone-backup.timerflaggedconfig_mutation— sat in approval limbo until the user moved on.a51e2086:curl http://192.168.8.214:5572/rc/...(read-only RC API) flaggedconfig_mutation. The agent kept reframing; user said "lets just close this session."cb8c8a4a: same goal, eventually succeeded — but only after the agent found a different path.fefa4fa3: only when the user escalated to "fix it so the backup works" did the agent pivot to the actual root cause (memory).
This is the single biggest friction point in the batch.
2. Classifier overreach on read-only pct exec / curl (blocker)
The preflight classifier in internal/policy matches command substrings
(pct exec, curl, dd, etc.) without parsing the actual command. A
read-only systemctl status becomes config_mutation. The agent has
no tool to ask "classify this command before I send it" — it just keeps
retrying with cosmetic changes until the user bails.
3. update_plan_step is the second-largest tool bucket (cosmetic → friction)
Across 10 sessions: run ~199, update_plan_step ~57. That's ~22% of
all tool calls spent on bookkeeping. For a 2-message greeting session
(8c76bb3a) the agent still called update_plan_step ×4 plus
propose_plan. The scaffolding is louder than the work.
4. pending_approvals doesn't match reality (cosmetic, but misleading)
a51e2086 summary literally says "Both commands are queued" — yet
pending_approvals=0. The field is hasPendingApprovals
(store.go:962) which only counts executions currently in
pending_approval state; once they're cancelled/expired it drops to 0
even though the session was blocked by approvals. As an audit signal
it lies. A session can be outcome=partial because of approval
friction without pending_approvals ever being non-zero at review time.
5. Title is still the first sentence of the first assistant message (cosmetic)
"Assent window is open — executing the plan\n\nMemory bumped: 4294967296..." is not a useful label. Same complaint applies to
8c76bb3a ("Hey! 👋 Nomos here, running on mac-mini:8092...") and
95fdd322 ("This is a pure-DB Q&A — no run calls needed..."). The
list view ends up being unreadable without opening each row.
6. Goal field empty on one session (438ec8bd) (cosmetic)
set_goal was never called for the bare greeting. Minor, but it means
the session is unsearchable by goal text.
Ease of getting session details
I had to write Python+curl to audit 10 sessions. The pain points:
- Two endpoints must be merged by hand.
/sessionsreturns metadata (title,goal,outcome,summary,status,pending_approvals, timestamps) but no message/tool counts./sessions/{id}returns onlysession_id+messages— no metadata at all.cmd/nomos/eval/main.go:302-303already carries a comment complaining about this ("only session_id + messages"). Any consumer has to do the same join I did. - No aggregates on the list endpoint.
message_count,tool_call_count,top_tools,duration— all require fetching every session's full transcript and walking the message tree. For 10 sessions that's 10 extra HTTP round trips and ~600 KB of JSON parsed client-side. For a fleet audit at scale it's quadratic. - No filtering or pagination on
/sessions. It returns every session in one shot. The skill's own script does.sessions[:5]and.sessions[:10]client-side. - Tool calls are nested two levels deep
(
messages[].content.tool_calls[].name) withcontentstored asjson.RawMessage. The jq path requires?.everywhere. A flat/sessions/{id}/tool_callsview would be far easier to analyze. - No
/sessions?outcome=partialor?entity_id=...filter. Finding "show me every session that touchedlxc:rcloneand didn't succeed" requires the full scan. titleis the raw first assistant text. Useless for skimming a list — you have to open each row to know what it was.- No
closed_at/outcome_set_at.last_active_atis the closest proxy but it conflates "agent is still working" with "operator just opened the transcript." Duration can only be computed aslast_active - created, which is wrong for reopened sessions (a51e2086shows "5647 min" = 4 days because the user re-opened it on 2026-07-19 to close it). - No "blocker reason" field. When
outcome=partial, the why is buried in the last assistant text. A structuredblocker: "approval_timeout"/blocker: "classifier_overreach"/blocker: "user_abandoned"would make trend analysis trivial.
Improvement plan
P0 — Blockers ✅
-
✅ Stop the classifier from flagging read-only
pct exec/curlasconfig_mutation. Ininternal/policy, parse the command (not just substring-match) before assigning risk class. Concretely:pct exec <id> -- <cmd>should be classified by the inner command, not the wrapper.curl <url>without-X POST/-d/--upload-fileis read-only. This single change would have collapsed sessions #1, #3, #6, #10 into a handful of tool calls each and avoided three duplicate rclone sessions.- Done:
internal/policy/command.gonow unwrapspct exec,qm guest exec,bash -c,sh -c,sudo, and env-var assignments before classification. Curl GET (the default) without POST/data/ upload/output flags is now read-only. Output redirection (>/>>) disqualifies the read-only path. Tests ininternal/policy/command_test.gocover the new behaviors.
- Done:
-
✅ Add a command-scoped
preflightMCP tool. The existingpreflightin AGENTS.md §3 is entity/service-scoped, not command-scoped. The agent today has to keep reframing and re-submitting to discover what the classifier will accept. A command preflight returns{risk_class, reason}synchronously so the agent can decide whether to submit, rephrase, or surface to the operator.- Done: new
classify_commandMCP tool ininternal/mcp/tools.gothat takescommand+ optionaldeclared_riskand returns the exact risk class thatrunwould assign. Documented innomos/SOUL.mdwith explicit guidance to pre-classify beforerunwhen the classification is uncertain — "Do NOT submit arun, get it queued for approval, and then retry with cosmetic variations."
- Done: new
P1 — Friction ✅
-
✅ De-dupe sessions for the same entity + problem. When a session is
outcome=partialagainst an entity and a new session is created within 24h with a similar goal, surface the prior session to the agent atset_goaltime. Three rclone sessions exist because each new session started from scratch.- Done:
cmd/nomos/store.gogainedrecentPartialSessions(ctx, excludeSessionID, since); theset_goalhandler incmd/nomos/tasks.gocalls it and includes up to 5 prior partial/ failed sessions (with goal + summary) in the response. The agent is told to search_knowledge or read the prior transcript before re-planning.
- Done:
-
✅ Quiet the
update_plan_stepscaffolding. Either (a) make the agent not call it for single-step sessions (greeting/health-check), or (b) stop persisting it as a message — keep it only in aplan_stepstable that the UI hydrates from/sessions/{id}/plan(which already exists). It currently inflates transcript size and tool-call counts.- Done:
completeTaskincmd/nomos/store.gonow auto-closes any in-flight plan steps (pending/running → done on success, → skipped on partial/failure). SOUL.md §6 documents the new pattern: "for one-step plans ... propose_plan → answer → complete_task, skipping the per-step running→done dance entirely."
- Done:
-
✅ Add
blockerandclosed_atto thesessionstruct. Setblockerautomatically whenoutcome=partial/failed: scan the last assistant message for signatures ("queued for approval", "cancel", "close this session"). Surface in/sessionslist so trends are queryable.- Done: migration
021_session_blocker_and_closed_at.up.sqladds the two columns + backfillsclosed_atfor existing terminal sessions- adds a partial-index on
closed_at DESC WHERE status IN ('done','failed').cmd/nomos/store.gocompleteTasksetsclosed_at = now()and derivesblockerfrom the last assistant message viaderiveBlocker. The blocker patterns table covers approval_timeout, user_abandoned, classifier_overreach, model_refusal, model_empty_response, missing_knowledge, missing_capability, tool_error.
- adds a partial-index on
- Done: migration
P2 — Cosmetic / API ergonomics ✅
-
✅ Add aggregates to
/sessionslist.message_count,tool_call_count,duration_seconds. Computed server-side at list time (single SQL pass with LEFT JOINs toagent_messagesandagent_activity). Eliminates the N+1 transcript fetch I had to do.- Done:
sessionstruct incmd/nomos/store.gocarries the three new fields;listSessionsFiltered,getSession, andrecentPartialSessionsall populate them.
- Done:
-
✅ Single endpoint that returns both metadata and messages. Either enrich
/sessions/{id}with the fullsessionstruct, or add?include=messageson the list endpoint. The split-persistence is a leaky abstraction called out ineval/main.go:302-303.- Done:
GET /sessions/{id}incmd/nomos/main.gonow returns{session_id, session, messages}— thesessionfield carries the full metadata (title, goal, outcome, summary, blocker, pending_approvals, message_count, tool_call_count, etc.). Themessagesfield is unchanged. Clients that only readmessageskeep working.
- Done:
-
✅ Filtering & pagination on
/sessions.?outcome=partial&entity_id=...&since=...&limit=20&cursor=.... Removes the "fetch everything, filter client-side" pattern in the skill's own script.- Done:
cmd/nomos/main.gohandleSessionsListparsesoutcome/status/entity_id/blocker/since/cursor/limitquery params.listFilter+listSessionsFilteredincmd/nomos/store.gobuild a dynamic WHERE + LIMIT.sinceaccepts both RFC3339 timestamps and Go durations ("24h", "7d" → parsed as hours). The response includesnext_cursorfor paging.
- Done:
-
✅ Auto-title from
goal(when set), not from the first assistant text. Fall back to the assistant text only if no goal. The greeting session438ec8bdhasgoal=""and a useless title;fefa4fa3has goal "Fix the rclone backup so it completes successfully instead of OOM-killing" — that's the right title.- Done:
setGoalincmd/nomos/store.gonow setstitle = goalon the same UPDATE that sets the goal. The title-from-first-assistant-text path incmd/nomos/main.gopreserves the goal title when one exists (falls back totruncate(finalText, 80)only when no goal is set). Truncates the goal title to 120 chars.
- Done:
-
✅ Add
/sessions/{id}/tool_callsflat view. Returns[{id, name, args, result, error, type, message_id, role, seq, created_at}]without the message-shell nesting. Makes jq one-liners and trend scripts trivial.- Done: new route in
cmd/nomos/main.gohandleSessionDetail;getSessionToolCallsincmd/nomos/store.gowalks messages and flattenstool_calls[]into a chronological flat list. Each tool_use/tool_result pair is emitted as two rows sharing an id (preserving the persisted shape) — clients that want the merged shape can group by ID.
- Done: new route in
Suggested order
If only two land: P0.1 (parse the inner command for pct exec /
curl classification) and P2.6 (aggregates on /sessions). The
first eliminates the most visible user-facing friction in this batch
(three duplicate rclone sessions); the second makes future audits like
this one a single curl | jq instead of a Python script.
Verification commands
# Re-pull any session for follow-up
curl -s http://localhost:8092/sessions | jq '.sessions[:10]'
curl -s http://localhost:8092/sessions/a51e2086-a816-4206-a556-dbca362cdda6 | jq .
curl -s http://localhost:8092/sessions/8acea2e3-fc4d-4953-b9df-8e58e59a549a | jq .
curl -s http://localhost:8092/sessions/cb8c8a4a-14a5-4dff-8393-6ed1e7ea7c30 | jq .
curl -s http://localhost:8092/sessions/fefa4fa3-5414-4633-8e5a-51aa4a76609c | jq .
# After P0.1 lands: confirm read-only commands classify as reversible_low
# (whatever the preflight surface becomes — TBC when the tool is added)
Related files
cmd/nomos/main.go—/sessionsand/sessions/{id}handlers (handleSessionsListline 363,handleSessionDetailline 383)cmd/nomos/store.go—sessionstruct (line 89),messagestruct (line 103),listSessions(line 317),getMessages(line 377),hasPendingApprovals(line 962)cmd/nomos/agent.go— agent loop, retry behavior, goal statecmd/nomos/eval/main.go:302— comment calling out the/sessions/{id}"only session_id + messages" gapinternal/policy/*— risk-class classifier (target of P0.1)internal/mcp/server.go—runtool,preflight(entity-scoped), all MCP tool implementationsnomos/SOUL.md— agent persona, tool-selection rules.agents/skills/session-review/SKILL.md— the audit protocolplans/2026-07-18-session-review-three-sessions.md— prior review; three sessions overlap with this one
Relationship to the 2026-07-18 review
That review's P0.1 (retry cap), P0.2 (investigate-before-retry SOUL
guidance), P1.3 (runbook capture), P1.5 (bulk inspection tool),
P1.6 (vm: target support), P1.8 (ask-before-migrate) all landed or
are tracked separately. This review does not re-open them. The
remaining open items from that review are P1.7 (approval window
auto-extends on execution timeout) and P2.9 (long-running command
PENDING detection), both deferred there with rationale; this review
found no new evidence that would change that deferral.