# 2026-07-14 — Session review + Activity timeline refinements **Status:** Planned ## Session analysis: `722d8878` (2026-07-14T10:45) "Fleet-wide audit: check all services, identify what needs updating" ### What happened - User asked for fleet audit → agent called `set_goal` (session → executing) - 82 tool calls in ONE turn: 58 `run`, 8 `get_entity`, 4 `get_relations`, etc. - **No `propose_plan`** — no plan steps, entered executing directly - **No `complete_task`** — agent produced a final text response but never closed the task - Session stuck at `executing` with a text conclusion but no terminal state ### Gaps found | # | Issue | Root cause | |---|---|---| | 1 | "Agent is thinking" stuck at end | `liveStatus === 'executing'` is true even after the turn ends. AgentIndicator shows generic message because there are no running activity entries to describe. | | 2 | No plan proposed | Model skipped `propose_plan` — possibly because it found prior knowledge and skipped to execution. `setGoal` now sets `executing` directly (our fix), which makes `propose_plan` optional — but the sidebar "Plan" section shows "No plan yet" permanently. | | 3 | Task never completed | Model produced final text but never called `complete_task`. The idle sweep (2 min now) will nudge, then auto-close. | | 4 | Approvals invisible in Activity | 58 `run` calls, many requiring approval. These show in chat via InlineApproval but NOT in the Activity timeline. The operator has to switch to Ops page to track approvals. | | 5 | Activity is newest-first | Timeline shows newest at top. Feels unnatural for a sequential log — bottom-scrolling with newest at bottom is more intuitive for "watching" what the agent does. | | 6 | Knowledge recorded not shown | The agent recorded knowledge but it doesn't appear in Activity if it came from prior knowledge entries via `get_knowledge_content`. | --- ## Plan ### 1. Fix "Agent is thinking" stuck indicator **Root cause:** AgentIndicator shows when `active={$streaming || liveStatus === 'executing'}`. After the turn completes, `liveStatus` is still `'executing'` and there are no running entries, so the label falls through to the generic `"Agent is thinking…"` fallback. **Fix:** Change the active condition to only show when there's actual work: ```ts active={$streaming || $activityLog.some((e) => e.status === 'running')} ``` This way it shows during streaming AND when there are running tools (auto-continuation), but NOT when the session is just "executing" with no active work. Also: when the last assistant message has text AND no pending tool calls, auto-hide the indicator. The `liveStatus === 'executing'` check is too broad — it covers the entire session lifetime. ### 2. Approvals in Activity timeline **What:** InlineApproval cards show in chat but not in Activity. Every `run` that queues an execution with "requires approval" should appear as an entry in the Activity timeline. **How:** - In `activity.ts`, detect tool results containing "requires approval" + execution ID - Add `type: 'approval_pending'` entries with the execution ID, target, action, and status - Poll execution status and update the entry (pending → approved → running → completed/failed) - The InlineApproval component stays in chat for the Approve/Deny buttons - Activity shows the full lifecycle: approval requested → approved → running → done ### 3. Old-to-new ordering **Fix:** Remove `.sort((a, b) => b.timestamp - a.timestamp)` → change to `.sort((a, b) => a.timestamp - b.timestamp)` or no sort at all (entries are already added in chronological order). This means the timeline reads top-to-bottom as the session unfolds. Currently `newest at top` means the "Goal" and "Plan" entries appear at the bottom, which is confusing. ### 4. Knowledge detection **Fix:** Extend the knowledge detection in `activity.ts` to also catch `upsert_knowledge` calls from `tool_use` events (not just `tool_result`), so the entry appears as "running" while recording and then "done" when the result comes back. ### 5. Auto-hide indicator when turn ends with text **Fix:** Detect when the last assistant message has text content AND there are no pending tool_use entries without matching tool_result. In that case, the turn is complete — don't show the indicator. --- ## Plan-approve-once (new policy) ### Problem Today: agent calls `propose_plan` + `run` × 10 in the same turn. Each `run` queues an individual approval. Operator sees 10 "requires approval" cards. After operator types "yes", each one is individually approved, THEN the assent window opens and future calls auto-run. The operator shouldn't see per-action approvals when they already approved the plan. The plan IS the approval. Individual actions within an approved plan should auto-execute. ### Target ``` User: "audit fleet" Agent: "Here's my plan: 1. List LXCs 2. Check apt on each 3. Report" ← proposes plan [Proposed plan: 3 steps] [Approve plan?] User: "approved" Agent: ◉ Listing containers… ← auto-runs ◉ Checking apt on lxc:jellyfin… ← auto-runs ... "Done. 19 LXCs have pending updates." ``` One approval for the plan. All actions within it auto-execute. No per-action approval cards. Only re-approve when the agent calls `propose_plan` again (significant plan change). ### How (server-side) The classification logic in `internal/mcp/server.go:run()` needs to know whether a plan-approval-assent-window is active for this session. Currently it checks `autonomy_settings` for the assent window key. The change: when `propose_plan` is called, pre-activate the window with a "plan-proposed" state. When the operator approves, transition to "plan-active". `run` calls within an active plan window auto-execute at `config_mutation` level. Key change in `store.go:proposePlan()`: ```go // Pre-record a plan-proposed window so that run calls know a plan is pending approval. // Once approved, this becomes the full assent window. key := planWindowKey(agentID, sessionID) s.pool.Exec(ctx, `INSERT INTO autonomy_settings (key, value) VALUES ($1, 'proposed') ON CONFLICT (key) DO UPDATE SET value = 'proposed'`, key) ``` Then in the `run` handler, check for `plan-active` OR `assent-active`: ```go // If a plan window is active, this run call is part of the approved plan // and config_mutation commands auto-execute without individual approval. if planWindowActive(ctx, agentID, sessionID) { ... } ``` ### How (frontend) - Instead of 10 individual InlineApproval cards, show ONE "Approve plan?" card - When approved, all queued `run` calls from the plan turn auto-grant - Activity timeline shows plan approval as one entry: "✓ Plan approved — 12 actions" - Subsequent `run` calls show with an "auto (plan)" badge instead of approval cards --- ## Implementation plan (consolidated) ### Phase A — Quick fixes (today) | # | Fix | |---|---| | A1 | AgentIndicator: only show when `$streaming || hasRunningActivity` (not on `liveStatus === 'executing'`) | | A2 | Activity timeline: old-to-new ordering | | A3 | Knowledge entries: detect `tool_use` events for running state | ### Phase B — Approvals in Activity | # | Fix | |---|---| | B1 | `activityLog`: add `approval_pending` / `approval_granted` / `execution_running` / `execution_done` lifecycle entries | | B2 | Poll execution status and update Activity entries inline | | B3 | InlineApproval stays in chat (needs operator action), but lifecycle tracked in Activity | ### Phase C — Plan-approve-once (policy change) | # | Fix | |---|---| | C1 | Backend: plan window in `autonomy_settings`, checked by `run` handler | | C2 | Frontend: single "Approve plan?" card instead of per-action cards | | C3 | Backend: when plan is approved, auto-grant all pending executions from the plan turn | | C4 | Activity: plan approval as single timeline entry with action count badge |