Backend:
- Add isThinking flag to agentEvent for text before tool calls
- Separate thinking from response text in runChatTurn and continue.go
- Persist thinking in a dedicated field in message content
Frontend:
- Add thinking field to MessageContent, ChatMessage, ChatTextEvent types
- Create ThinkingBlock.svelte — collapsible block with brain icon
- SSE handler moves text_delta content to thinking on isThinking flag
- Render thinking block between tools and response in ChatThread
- Fix chat window scroll reset on focus change (stable windowKeys order)
- Remove redundant #key id wrapper in WindowLayer
- Enlarge sidebar rail (24→32 default, 40→60 max)
- Remove glyph from sidebar, square graph at top
- Replace AgentTrace/ToolCallCard/UnifiedTimeline with TurnTrace/ToolLine
Make background/long/desynced turns visible and queueable, fixing the four
symptoms that survived the v0.15.0 chat reliability pass.
F1 - status-driven working signal (workspace.ts taskWorking/currentWorking =
streaming OR status in {planning,executing}). Drives the chat trace, indicator,
and activity spinner so a turn with no live stream (background resume, a dropped
SSE, an idle-close mid long turn) still looks alive.
F2 - operator messages sent during an in-flight turn are now QUEUED and
auto-run when the gate frees, replacing the "still finishing a previous step...
send it again" rejection. Per-session in-memory FIFO (messagequeue.go, capped at
20) drained one-at-a-time under the turn gate; a `queued` SSE event drives a
"Queued" hint. drainQueued releases via a per-iteration deferred closure so a
runChatTurn panic can't deadlock the session's gate.
F3 - SSE keepalive (12s `:keepalive` comment) in handleChat so 20-40s
inter-iteration gaps no longer trip a proxy/browser idle close (the desync root
cause). All SSE writes serialized through one mutex.
F4 - generation-aware activity timeline (only the last propose_plan renders;
superseded ones collapse to one "Earlier plan revised" marker; step-attribution
follows only the current generation) + debounced plan refetch on lifecycle
events so a missed plan.proposed self-heals.
Verified against the last session (23da10db: 6m33s turn, operator "status"
deferred at 19:48:05). go test ./cmd/nomos/ green (new messagequeue tests);
web vitest 72/72 (new F4 generation tests); vite build clean.
VERSION: 0.16.0 -> 0.17.0
The agent could run two turns for one session at once (a reconnect resumed
while the live turn was still going), and their interleaved tool calls
corrupted the activity panel, fabricated a confusing "parallel/nested"
sequence, and made tasks feel stuck/never-ending. Several UX gaps compounded it.
Turn serialization (F1):
- turnGate: at most one in-flight turn per session. Background resume paths
(continuation worker, idle sweep, answer-question, /resume, reconnect)
skip non-blocking when busy; the live chat path waits briefly then bails
cleanly instead of stacking a second turn.
- resumeSession returns whether it ran; continueSession marks an execution
"continued" only after a real run (review P0) so a busy-skip can't lose a
finished-execution result. Idle nudge bumps only after delivery (P1).
Connection state (F2/F3, web):
- humanize/bucket raw errors ("model connection dropped..."); one surface
per drop; a terminal task.status event clears stuck streaming/disconnected
state and dismisses the connection toast. Reconnect no longer spawns turns.
Streaming where you look (F4, web):
- live command output in the global activity timeline and in the inline
tool card (auto-opened, tail-pinned) -- not just the per-window rail.
Other (web): artifact/knowledge deep links (F5); step-first stable
"thinking" headline (F6); stable chat layout, no empty->content reflow (F7);
lazy event sync (P2.2); reconnect skips a terminal session (P2.1).
VERSION: 0.14.2 -> 0.15.0
The plan recorded false history after a re-plan and the activity panel
showed fabricated, churning timestamps. Two bugs compounding on one event
stream.
Plan drift (P0.1):
- proposePlan seq is now 1..N per generation; (session,generation,seq) is
the addressing key. The model's 1-based update_plan_step calls always map
to the CURRENT plan after a re-plan, instead of resurrecting a superseded
`replaced` row as done while the live work went unrecorded.
- updatePlanStep resolves against MAX(generation); a stale/out-of-range seq
returns errPlanStepNotFound (never touches a superseded generation).
- getPlanSteps returns only the current generation by default; ?all=true
keeps the audit/eval view (plan_generations assertion).
- completeTask auto-close scopes to the current gen, stamps started_at, and
emits one plan.step.finished per closed step so the panel converges
instead of freezing on "running" after completion (P1.1).
- propose_plan result enumerates step seqs; writeback detector matches
"write back"/"writeback"/"upsert_knowledge" so a natural-language final
step isn't doubled (P1.2).
- migration 029 renumbers existing seq per generation + unique index.
Activity panel (P0.2 / P1.1, web):
- computeActivityLog uses the real message created_at for tool calls; live
entries fall back to wall-clock frozen on first sight, killing the 3s
poll churn. Steps use real started_at.
- dropped plan-step events warn + count instead of a silent no-op.
Tests: TestProposePlan updated; + generation-relative-seq and auto-close
event-emission regression tests; + web activity purity/timestamp tests.
VERSION: 0.14.0 -> 0.14.1
Classifier now unwraps pct exec / qm guest exec / bash -c / sh -c / sudo
and env-var assignments before classification, so read-only inspection
wrapped in pct exec no longer escalates to config_mutation. curl GET
(default method, no -d/-F/-T/-o/>) is read-only. Eliminates the three
duplicate rclone sessions (a51e2086, 8acea2e3, cb8c8a4a) that bounced
off the classifier for the same goal.
New classify_command MCP tool: command-scoped preflight that returns the
exact risk class run would assign. Documented in SOUL.md with guidance
to pre-classify before run when the verdict is uncertain.
set_goal surfaces prior partial/failed sessions from the last 24h so the
agent picks up the thread instead of rediscovering it.
completeTask auto-closes in-flight plan steps (pending/running -> done
on success, skipped on partial/failure), so one-step plans no longer
need the per-step running->done dance right before completion.
Migration 021 adds blocker + closed_at to agent_sessions. completeTask
sets closed_at once and derives a structured blocker reason
(approval_timeout, user_abandoned, classifier_overreach, model_refusal,
tool_error, ...) from the last assistant message.
/sessions list now carries message_count, tool_call_count,
duration_seconds (server-side aggregates — no more N+1 transcript
fetches to audit a fleet). GET /sessions/{id} returns both metadata
and messages. New query params filter + paginate: outcome, status,
entity_id, blocker, since (RFC3339 or Go duration), cursor, limit.
Titles now prefer the goal when set; sessions without a goal fall back
to the first assistant text.
New GET /sessions/{id}/tool_calls flat view for audit scripts.
Plan: plans/2026-07-20-session-review-ten-sessions.md. VERSION 0.7.12 -> 0.7.13.
Replace the sidebar + hash-routed page shell with a desktop metaphor:
draggable app icons, apps opening as floating wmkit windows, a centered
"What should Nomos do?" task launcher, and a bottom taskbar showing all
open windows plus a system tray.
- New app registry ($lib/apps.ts) — adding an app is one entry, nothing
else to touch.
- New desktop shell components (Desktop, WindowLayer, DesktopIcon,
Taskbar, TaskLauncher) under $lib/components/desktop-shell/.
- Icon positions are a persisted, collision-avoiding grid ($lib/stores/icons.ts).
- Window layout persists across reloads (wmkit persist), with
drag-to-maximize, F6 window cycling, and now a right-click desktop menu
(cascade/tile/show desktop/reset icons) plus Cmd/Ctrl+Z undo/redo for
window moves, resizes, and closes.
- Taskbar buttons get a hover-close and self-correct their title once a
new task's real goal is known.
- New task windows (desktop launcher and the Tasks app's "New task"
button) open as a window, not a dialog, and hand off to the real
session window once the backend assigns an id.
- Fixed a real gap along the way: GET /sessions/{id} couldn't tell
"session deleted" from "session has no messages yet" (both returned
200 with an empty list) — cmd/nomos/main.go now checks existence and
404s, so a stale/persisted task window shows "Task not found" instead
of a misleadingly empty, live-looking chat.
- Test coverage for the new pure logic (icon placement/collision
avoidance, app registry id helpers) plus a vitest matchMedia polyfill
needed to import anything touching the theme store.
Deletes the now-superseded sidebar shell, MinimizedWindowsBar, and the
standalone Chat/EntityDetail pages (folded into the window layer).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Session-review implementation for the three sessions audited in
plans/2026-07-18-session-review-three-sessions.md. v0.7.11 → v0.7.12.
P0.1 — retry cap + investigate-before-retry (cmd/nomos/retrycap.go,
agent.go): after 3 identical failing run calls in a single turn, refuse
to dispatch the call again and return a directive to investigate *why*
(ps/strace/lsof) or surface the blocker. Per-turn scope so a fresh turn
after the operator responds can retry once more. Session 1e9c7691's 20+
identical chown retries (knfsd held a kernel lock on the exported NFS
dir) is the direct motivation.
P0.2 + P1.8 + P2.10 — SOUL.md guidance: hung command is not a failed
command (investigate before retry); ask before proposing a multi-step
migration; multi-goal sessions summarize the arc not just the last goal.
P1.3 — two new runbook entities in seeds/knowledge.yaml:
- nfs-exported-dir-mutation-hang (the knfsd fchownat lock procedure:
killall → exportfs -u → mutate → exportfs -a → verify)
- netbird-mgmt-oidc-race-after-upgrade (docker restart netbird-mgmt
after ~30s for the traefik/authentik OIDC race)
P1.4 — setGoal emits task.superseded event when prior goal is overwritten
by a different goal (store.go, TestSetGoal_SupersededEvent). Session
55927f0a had two set_goal calls with the first silently abandoned.
P1.5 — inspect_path MCP tool: runs mount/df/ls/stat for one path across
up to 8 targets in one parallel call, replacing the 15+ run-call
fact-gathering fan-out sessions 1 and 2 each spent on cross-target path
tracing (tools.go, server.go: inspectPathAcrossTargets, inspectOneTarget).
P1.6 — vm: target support in run via qm guest exec (no more SSH-hop
with nested quoting). Extracted shared resolveProxmoxHostSlug for
LXC + VM, with hosts-relationship fallback when attributes.host is
absent (server.go, tools.go). Session 55927f0a's SSH-hop workarounds
for vm:zimaos are the direct motivation.
Deferred (documented in plan): P1.7 (approval window auto-extend on
timeout) and P2.9 (long-running command PENDING detection) — both
addressed at lower cost by the retry cap. Session 3's poll-after-timeout
pattern already works; the cap protects against the failure mode.
The auto-complete fired when the agent hit the P5 approval gate — it
queued a config_mutation run for approval, the P5 gate blocked further
runs, the turn ended, and auto-complete closed the session as 'partial'.
The operator's approval would then land on a dead task.
Fix: hasPendingApprovals check — if the session has any executions in
pending_approval state, skip auto-complete. The session stays in
'executing' until the operator approves (or denies).
VERSION 0.7.5 → 0.7.6
The len(pending)>0 path still injected a brief note saying 'execution(s)
are now running' — the model saw this, thought work was being done for
it, and no-op'd (finish_reason=stop, content_len=0). Same confusion as
the len(pending)==0 case, just from the other branch.
Fix: both assent paths are now fully silent. No system note at all. The
model sees 'go ahead' in the replayed history and responds naturally.
Also removed chat_assent tool_use/tool_result emit events. These were
persisted in the transcript and confused the model on replay — it saw
its own 'tool calls' (chat_assent) and thought it had already acted.
VERSION 0.7.4 → 0.7.5
The auto-complete safety net required hadEntityWriteback to be true,
which meant sessions where the agent did the work but forgot to call
update_entity_attributes stayed stuck in 'executing' forever.
Relax: auto-complete fires if the agent did discovery (ran run),
regardless of writeback. If writeback happened → success; if not →
partial (honest: work was done but knowledge graph not updated).
VERSION 0.7.3 → 0.7.4
When the chat handler approves a pending execution via chat-assent, the
execution completes in ~2s. The continuation worker detects the completed
execution and calls resumeSession — while the chat handler is still
processing 'go ahead'. Two concurrent LLM calls for the same session cause
empty responses (finish_reason=stop) and race conditions.
Fix: mark the execution as continued immediately after chat-assent grants
it, so the continuation worker skips it. The chat handler will drive the
continuation itself (the model sees 'go ahead' and executes the plan).
VERSION 0.7.2 → 0.7.3
The assent pre-processing injected verbose system notes ('the operator
approved... they are now running... you MUST continue...') on top of the
replayed user message ('go ahead'). The model saw both, latched onto
'now running', concluded the work was being done for it, and no-op'd
(finish_reason=stop, content_len=0) — leaving the session stuck in
'executing'.
Root cause: the model already sees 'go ahead' in the replayed history
(the user message is saved to the DB before chat() is called, and
getRecentMessages replays it). The system note was redundant AND
confusing — it told the model work was 'running' when it wasn't.
Fix:
- len(pending)==0 (plan-proposal approval): open assent window silently.
No system note. The model sees 'go ahead' and responds naturally.
- len(pending)>0 (actual pending executions): brief note naming the
specific execution IDs that were approved ('don't re-request those').
No 'continue the plan' directive — the model knows to continue.
VERSION 0.7.1 → 0.7.2
P1: add docker compose (logs|ps|top|config|images|port|cp) to read-only
allowlist. docker compose logs was classified as config_mutation, causing
individual approval cards for read-only inspection commands.
P2: remove approval entries from activityLog. They were always status=running
and never transitioned to done (the derived store builds from tool-call
text, not execution status), causing AgentIndicator to latch onto a stale
'Approval: ...' entry and never clear — even after the session completed.
P3: remove InlineApproval from Chat.svelte. The green 'Completed in 1s on
lxc:...' boxes were noise in the chat stream. Approval UX belongs in the
Operations page (already has it via Ops.svelte), not inline in the chat.
P4: stale execution cleanup. Startup sweep (mark >1hr non-terminal as
cancelled) + 5-min periodic sweep (mark >10min non-terminal as cancelled).
98 orphaned executions accumulated from eval testing (39 running from
apt_upgrade:audit timeouts, 19 pending_approval, 3 approved).
P5: refuse second config_mutation run when an approval is already pending
for the session. Without this, the agent queues N individual approvals
before the operator can respond — confirmed in session 20757eb9 (two
approval cards for what should have been one plan-level approval).
VERSION 0.7.0 → 0.7.1
The agent often skips update_plan_step bookkeeping (leaving steps
pending/running) but still does the work + writeback. The strict
allPlanStepsTerminal check missed these cases.
Add path (b): if the agent did discovery (ran `run`) AND wrote back
(update_entity_attributes/create_relationship), auto-complete. D.1 already
enforces writeback before completion — if writeback happened, the work
is done.
The #1 remaining model reliability gap: the agent does the work (proposes
plan, executes all steps, writes back) but forgets to call complete_task,
leaving the session stuck in 'executing'. The eval showed 3/8 failures
with this pattern.
Fix: autoCompleteIfPlanDone — a structural safety net that fires at both
chat exit paths (normal completion + maxIterations). If the session has a
goal, the agent didn't call complete_task, and ALL plan steps are in a
terminal state (done/failed/replaced/skipped/blocked), auto-complete with
the agent's final text as the summary. Mirrors autoCompleteTrivialTask
but for structured tasks where the work is provably done.
Also: bump maxLLMRetries from 2 to 3 (complex multi-turn flows benefit
from one more retry on empty responses).
The assent system note said 'Do not re-request or call run again for
these' — the LLM interpreted this as 'don't call run at all' and produced
empty responses (finish_reason=stop, content_len=0) until retries were
exhausted, leaving the session stuck in 'executing'.
Fix: rewrite both assent notes (pending-approval path and pure-plan-approval
path) to be directive about WHAT TO DO NEXT: call update_plan_step(running)
then run for each remaining step. The 'don't re-request' guidance is now
scoped to 'THOSE SPECIFIC' executions, not all run calls.
Also bump maxLLMRetries from 2 to 3 — the empty-response flake on complex
multi-turn flows benefits from one more retry.
reopenSession was replacing plan steps on every follow-up message —
including approvals ('go ahead') — which destroyed the plan the operator
just approved, leaving the agent unable to track step progress and looping
run calls until maxIterations.
Fix: setGoal is the explicit signal for 'new sub-task' (the agent calls
it at the start of each follow-up direction). Step replacement now happens
there, not in reopenSession. An approval ('go ahead') does NOT call
set_goal, so the plan stays intact and the agent can execute + complete
it.
The check len(lastAssistantCalls) == 0 was too restrictive — it only
fired when the assistant had ZERO tool calls. But propose_plan + pre-plan
research are tool calls, so the assent window never opened when the
operator said 'go ahead' after a plan proposal. The agent then tried to
execute config_mutation run calls without the assent window, they queued
for approval, and the turn deadlocked.
Fix: check len(pending) == 0 (no pending APPROVALS) instead of
len(lastAssistantCalls) == 0 (no tool calls at all).
A follow-up on an executing session (first turn didn't complete_task) is
still a new direction — the old plan's steps must not block the new one.
Previously reopenSession was a no-op for executing sessions, leaving done
steps that caused errPlanInFlight on the next propose_plan call.
proposePlan: mark pending steps as 'replaced' instead of DELETE, so the
generation counter (MAX+1) sees prior generations. Without this, a first
plan that was proposed but never executed would be wiped, resetting the
counter — a follow-up's plan would look like generation 1 instead of 2.
plan-always-readonly: raise max_run_calls from 3 to 6 (agent inspects
thoroughly).
SOUL.md step 4: all-read-only plans skip the approval wait and execute
immediately. Only config_mutation/destructive steps need operator approval.
set_goal + propose_plan return text updated to match.
Fixes 3/4 eval failures where the agent proposed a plan then waited
for approval on a read-only task.
P1 plan-first: run handler refuses without propose_plan (structural gate,
not SOUL.md prose). Plan window decoupled from set_goal — config_mutation
auto-run only on operator approval (assent window). Closes the approval-free
config_mutation hole confirmed in session d0d562e0.
P2 iteration: reopenSession flips terminal→executing, marks prior plan steps
replaced, clears outcome. proposePlan excludes replaced from in-flight check,
bumps generation. A follow-up on a completed session starts a new sub-task
with a fresh plan — no more errPlanInFlight dead end.
P3 reasoning: accumulate per-iteration text into the persisted row instead
of overwriting with the last text event. Reload shows intermediate thinking,
not just the final summary.
P4 read-only allowlist: add find, tree, locate, systemctl list-timers/
list-unit-files/show, timedatectl, hostnamectl, systemd-analyze, rclone
ls/lsl/md5sum/check/cryptcheck. Fixes the find misclassification from
d0d562e0.
P5 eval harness: new assertion kinds (proposes_plan, plan_before_run,
plan_generations), multi-turn followups, fetch /sessions/{id}/plan. Four
manifests under evals/.
P6 SOUL.md: strip degenerate-case carve-out, add ITERATE step, update
set_goal guidance.
VERSION 0.6.0 → 0.7.0
Ships the 9 remaining post-fix items and a golden-conversation eval harness
that validates them against the live agent. All 4 evals pass.
SOUL.md (F.1, C.2, E.1):
- Consolidated three overlapping task-flow sections (MANDATORY TASK FLOW,
'Every chat is a task', 'AFTER EVERY TASK: WRITE BACK') into one. ~50
lines shorter. The operator's 'be more crisp' feedback.
- Added anti-patterns: don't re-execute on UI/sidebar complaints (C.2);
don't re-run fleet-wide audits when same-day knowledge exists (E.1).
- Updated approval vocabulary in step 4 to match tasks.go (approved/yes/
go/proceed/continue/ok/go ahead).
Tool-result strings (F.2):
- set_goal: tightened to 'Goal set. NEXT: pre-plan (read-only tools only).
Then propose_plan. Do not call run.'
- update_plan_step: added '(Advance with update_plan_step + run; do not
re-propose.)'
C.1 — completeTask rejects re-completion of a terminal session:
- Returns errTaskAlreadyComplete when status is already done/failed.
- The tool result directs: 'Task is already complete. Do not call
complete_task again. If the operator pointed out a UI/sidebar
inconsistency, fix it with update_plan_step...'
B.4 — Surface real model error text:
- chatWith's error event now includes finish_reason + refusal text:
'Nomos returned an empty or unusable response (finish_reason=length).
Retry or rephrase.' instead of generic 'empty response'.
- The resume-failed note already carried errText (B.3), which now has
the real context.
B.5 — Back off between resume retries (4s, 8s):
- resumeSession now sleeps before attempts 1 and 2 (exponential backoff).
A transient provider issue gets time to clear instead of 3 identical
calls in 3 seconds.
B.6 — Don't persist the empty placeholder as a visible bubble:
- If a chat turn ends with no text and no tool calls (model empty-response'd
and all retries failed), delete the placeholder row instead of persisting
an empty bubble. The error was already streamed via done+error=true.
E.2 — list_lxcs last-audited hint:
- The list_lxcs result now includes last_audited_at — the most recent
knowledge entry (tagged audit/update, or titled audit/update) linked
via an 'about' edge. The agent can see 'nextcloud — last audited today'
and skip re-running it.
Tool-call doubling bug fix (found by the eval harness):
- main.go + continue.go: the tool_use and tool_result events were both
appending separate entries to the persisted tool_calls array, doubling
every tool call in the transcript. Confirmed pre-existing (d9cdcee1,
v0.3.x era). Fixed: tool_use creates the entry, tool_result merges the
result into the same entry (matched by id). One entry per tool call.
Golden eval harness (cmd/nomos/eval/):
- A standalone Go program that loads YAML manifests of golden conversations
+ assertions, sends prompts to the chat endpoint, drains the SSE stream
(keeping the agent's context alive), and scores structural assertions
against the persisted transcript.
- 4 golden conversations covering: trivial read-only (degenerate case),
plan + proceed (the original duplication bug), UI complaint (no re-exec),
fleet audit (knowledge preferred over re-execution).
- Structural assertions only (tool-call sequences, plan steps, writeback,
completion) — text quality is model-dependent and not scored.
- Run: go run ./cmd/nomos/eval -gateway http://localhost:8092 -manifest
cmd/nomos/eval/evals/*.yaml (~$0.10/run in OpenRouter credits).
Eval results (4/4 passed):
trivial_readonly: 2 tool calls, no plan, no run
plan_advances_on_proceed: 13 tool calls, propose_plan x1, writes back
ui_complaint_no_rerun: 12 tool calls, propose_plan x1, writes back
knowledge_preferred_over_rerun: 7 tool calls, search_knowledge x1, 0 run
Version 0.5.2 -> 0.5.3 (minor: eval harness + structural hardening).
D.1 — complete_task structural gate:
- hadDiscovery(ctx, session) reports whether the session ran `run` successfully
against a live target (NOT get_entity/list_lxcs — those are DB lookups, not
new facts). A trivial Q&A that only calls get_entity is a degenerate case
and must NOT be blocked.
- complete_task with outcome=success is REFUSED when hadDiscovery && !
hadEntityWriteback. The refusal fires BEFORE completeTask runs, so the
session stays in 'executing' state and the agent must call
update_entity_attributes/create_relationship then retry complete_task.
An explicit failure/partial is allowed through (the agent is acknowledging
it didn't finish — no reason to force writeback).
- Replaces the prior advisory warning (5.5) which the agent consistently
ignored. The agent saw the warning and ended the task anyway; this gate
makes the writeback a hard prerequisite for success.
D.2 — propose_plan auto-append writeback step:
- When the agent proposes a plan whose steps don't mention
update_entity_attributes or create_relationship, D.2 appends a final
'Write back: update_entity_attributes + create_relationship +
upsert_knowledge' step before persisting. The result string tells the
agent it was appended.
- With the seq-order enforcement (5.6) and D.1's complete_task gate, the
agent must complete the writeback step (and actually call the tools) to
finish. Neither relies on the agent reading SOUL.md.
- Removed the old advisory writeback nudge from propose_plan's result
string — D.2 makes it structural.
- Updated the propose_plan tool description to state both gates crisply.
Verification:
- TestHadDiscoveryAndWriteback: hadDiscovery true only after a successful
`run`; false after failed run, get_entity, or no calls. hadEntityWriteback
true only after update_entity_attributes/create_relationship.
- e2e against the live agent (oikos-nomos-1, v0.5.1):
- D.2: agent proposed 3 steps (no writeback); D.2 auto-appended step 4
'Write back: update_entity_attributes + ...'. Result string said
'(appended a writeback step — your plan didn't include one; step 4)'.
- D.1: agent ran `run` (uptime on lxc:gitea), called complete_task, was
REFUSED ('Refused: this session ran run against live targets (discovery)
but did not call update_entity_attributes...'). Agent self-corrected:
called update_entity_attributes, retried complete_task, succeeded.
Knowledge loop closed end-to-end.
Version 0.5.0 -> 0.5.1 (patch: structural enforcement of existing intent).
Operator-reported bug: on 'proceed with the rest' the agent re-proposed the
plan, duplicating it in the sidebar. Root cause was a three-bug chain, not
one bug:
1. Trigger — model empty-response on 'proceed' (approval vocabulary didn't
list 'proceed', so the agent wasn't sure it was approved and no-op'd).
2. Amplifier — chatWith emitted 'error' without 'done' on empty response
(agent.go:370). The frontend's onComplete saw !receivedDone and
misclassified the model failure as a network disconnect, calling
handleDisconnect -> resumeSession.
3. Divergence — the reconnect note was generic ('report your state'), so
the agent re-proposed + re-executed instead of advancing the plan.
Fixes (shipped, e2e-validated against the live agent on oikos-nomos-1):
- A.2: proposePlan refuses re-proposal once a step has started (returns
errPlanInFlight). Drops the append-mode safety net (commit 5384499) that
was the direct source of the sidebar duplication. The agent must advance
with update_plan_step + run; the tool result directs it.
- A.1: proposePlan sets the 'generation' column on INSERT (migration 020
added the column + frontend grouping, but the INSERT never wired it).
- A.3: propose_plan tool description restated as a crisp contract (ONCE,
STOP and wait, REFUSES once a step started, advance with update_plan_step).
- F.3: approval vocabulary expanded to approved/yes/go/proceed/continue/ok/
go ahead; propose_plan result string tightened to an imperative.
- B.1: chatWith emits 'done' after 'error' on every terminal path via a new
emitError helper. The frontend now treats model errors as ended (not
disconnected), so no auto-reconnect -> resumeSession fires.
- B.2: reconnect/resume note carries the operator's last message + an
explicit 'advance the plan, do NOT call propose_plan again' directive when
a plan is in flight. Wired into all 4 resume entry points (reconnect,
/resume, idle-sweep, question-answer) via enrichResumeNote.
- B.3: resumeSession escalates the recovery note across its 3 attempts (final
retry: 'pick the lowest-pending step, mark it running, call run — do that
now') instead of 3 identical notes -> 3 identical empties.
Verification: TestProposePlan_RefuseInFlight replaces TestProposePlan_
AppendVsReplace. e2e conversations against the rebuilt container:
conv2 ('proceed with the rest') -> 0 propose_plan calls, plan stayed at
3 steps (was 6+ before), update_plan_step x5 + run x2 + complete_task.
conv3 (full plan, 'go ahead') -> apt-get update on lxc:dns auto-ran under
the plan window, update_entity_attributes writeback, clean complete_task.
nomos logs show zero reconnect/resume entries for the plan-proposing
sessions (the three-bug chain is closed).
Remaining (not in this commit): D.1 refuse complete_task without writeback
(next blocker), C.1/C.2, F.1/F.2 SOUL.md consolidation, B.4-B.6, E.1/E.2.
See plans/2026-07-14-post-fix-session-remainders.md.
Also: re-audit 2026-07-10-general-gated-execution.md — request_execution enum
retirement (60effcb) closes item 9; only auto-act revival (item 10) remains.
Version 0.4.1 -> 0.5.0 (minor: new structural behavior, not a bugfix).
SOUL.md: mandatory 6-step task flow at TOP of file, unmissable.
Agent MUST: set_goal → pre-plan (research only) → propose_plan → STOP
and wait for approval → execute (auto-run under plan window).
Backend:
- set_goal now opens plan window immediately (config_mutation auto-runs)
- set_goal result tells agent to do pre-plan + propose_plan, not run
- propose_plan result tells agent to STOP and wait for approval
- plan window value unified to 'active' (set_goal + propose_plan)
This prevents 23 individual approval popups — one plan approval instead.
Backend:
- proposePlan sets plan window in autonomy_settings (nomos:plan:<session>)
- run handler checks plan window — auto-executes config_mutation commands
within plan without per-action approval
- planWindowActive function in server.go
- Plan window cleaned up on completeTask (already covered by LIKE '%:' || )
Frontend:
- Removed 'Session graph' header bar
- Cooler empty states: Plan shows animated dots + 'Awaiting plan…',
Activity shows pulsing dots + 'Waiting for activity…'
- SessionDigest now includes live tool timeline, plan steps, knowledge
- ToolCallGroup compact: single-line with collapsible names only (no JSON)
- Activity bar moved to bottom of messages, smart scroll respects user position
- setGoal now sets status=executing (removed stuck planning state)
- PlanProgress merged into SessionDigest, removed from TaskContextPanel
- New toolTimeline derived store in chat.ts
RegisterHook + e.Cancel() prevents Wails from destroying the WebView
when the window is closed. The app now hides to the system tray. Left-
click on the tray icon correctly restores the window.
- Tray 'Check for Updates' now checks immediately and shows dialog
- Dialog has 'Install' and 'Later' buttons
- /update/check endpoint on local server for SPA to query
SPA navigates to 127.0.0.1:18901/oidc/start, passing ret URL.
Go opens browser, waits for callback, saves token, returns HTML with
<meta refresh> back to Wails app with ?desktop=1&token=TOKEN.
main.ts extracts token from URL on reload.
SPA fetches /oidc/open (returns session ID immediately), then polls
/oidc/result every 500ms. Go server opens browser in a goroutine.
Webview never leaves the Wails origin. Token is saved to keychain and
returned through the poll response.
The webview navigates to http://127.0.0.1:18901/oidc/open?apiUrl=...
The Go server opens the system browser to Authentik, waits for callback,
exchanges code for token, saves to keychain, then redirects the webview
back with ?desktop=1&token=TOKEN. main.ts extracts the token from URL.
The local HTTP server approach (fetch to 127.0.0.1) doesn't work in the
Wails webview. Simplify: use window.open() to launch OIDC in the real
browser. After authentication, the callback page at the server shows the
token. User copies and pastes into the Token tab.
Also fix: SetSize before app.Run() crashes with nil pointer — use
WebviewWindowOptions width/height directly from restored state.