Files
oikos/plans/2026-07-14-activity-gaps.md
dtoro 2ed169d239
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
fix stuck 'Agent is thinking' + flip activity to old-to-new
- 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
2026-07-14 12:56:42 +02:00

7.8 KiB
Raw Permalink Blame History

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:

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 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
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