- AgentIndicator now shows only during stream or when running tools exist (not on session status=executing which never cleared) - Activity timeline: oldest-first ordering (reads top-to-bottom naturally) - Removed unused liveStatus derivation and currentTask import from Chat - Plan: Phase A+B+C for activity gaps + plan-approve-once policy
7.8 KiB
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, 8get_entity, 4get_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
executingwith 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:
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():
// 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:
// 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
runcalls from the plan turn auto-grant - Activity timeline shows plan approval as one entry: "✓ Plan approved — 12 actions"
- Subsequent
runcalls 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 |
| 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 |