Files
kandev/docs/decisions/2026-08-01-separate-task-summary-session-stream-traffic.md
T
2026-08-02 16:06:14 +01:00

103 lines
5.7 KiB
Markdown

# ADR-2026-08-01-separate-task-summary-session-stream-traffic: Separate Task Summary and Session Stream Traffic
**Status:** accepted
**Date:** 2026-08-01
**Area:** backend, frontend, protocol
## Context
Task switchers need a small set of facts for every visible task: runtime state,
pending user input, recoverable agent errors, aggregate Git changes, and pull
request status. Those facts currently come from several stores. In particular,
the desktop sidebar subscribes to every task's primary session to obtain live
Git status, while desktop and mobile also inspect loaded session messages and
metadata to derive pending and error indicators.
A captured reload with 27 sessions produced 700 WebSocket frames (3.23 MB) in
five seconds, including 74 `session.subscribe` and 47 `session.unsubscribe`
requests. Switching tasks rebuilds the subscription set. Each session
subscription can replay the full session snapshot and then carry messages,
shell activity, model configuration, MCP status, and other data that a task row
does not use.
The WebSocket client currently puts correlated responses and unsolicited
notifications on one bounded FIFO queue. When that queue is full, a frame can
be dropped without closing the socket. This makes a fast, persisted
`message.add` operation appear to time out, and delayed model snapshots can
temporarily remove the model selector. Increasing the message timeout would
hide the symptom without removing the fan-out or the ambiguous response loss.
## Decision
Kandev will maintain a bounded, rebuildable `TaskStatusSummary` read model for
task-level surfaces. It is carried on task snapshots and updated through the
existing workspace-scoped task event path. The summary contains only the facts
needed to classify and decorate a task row; transcripts, file paths, diffs,
shell output, model configuration, and MCP payloads remain session-owned and
are never copied into it.
Session, message, Git, and pull-request domains remain authoritative. The
summary projector consumes their semantic changes, persists the latest compact
projection with a monotonic per-task revision, and emits an update only when
the projected value changes. A missing projection is rebuilt from authoritative
state and never causes the browser to subscribe to background sessions.
Task switchers consume task snapshots plus task-summary deltas. They do not
subscribe to session streams. Full session streams are limited to sessions
explicitly opened in an active detail surface. Repeated subscription and focus
requests are idempotent: only a new subscription receives a full initial
snapshot, and focus changes polling priority without replaying session data.
Targeted refresh operations use targeted actions rather than `session.focus`.
Git-summary freshness is tied to runtime ownership rather than browser
interest. A live execution keeps a slow monitoring baseline, active focus may
upgrade it to fast monitoring, and settled tasks use the latest persisted
snapshot. Removing a browser's background subscriptions therefore cannot pause
the source that feeds task-level Git status.
Correlated WebSocket responses and errors use a reserved, prioritized delivery
path separate from best-effort notifications. A response must be queued or the
connection must fail explicitly; it must not be silently discarded behind
session notification traffic. User message submission also carries a stable
client-generated message ID. Retrying that ID returns the accepted message
without rerunning turn-start hooks or dispatching the prompt twice.
## Consequences
Workspace traffic scales with task-summary changes and explicitly opened
sessions rather than with every task's full session activity. Switching tasks
has constant subscription work and no longer tears down and recreates an
all-task subscription fan-out. Desktop and mobile task switchers read the same
status contract and preserve their existing icon precedence.
The summary is eventually consistent by design. Revisions prevent stale
deltas from overwriting newer task state, and reconnect/task-list hydration
repairs missed updates. Projection failures leave the last valid summary in
place and are observable; detailed domain data remains available on demand.
The implementation adds a compact persistence model and projection wiring
across several domains. It also requires explicit queue semantics in the
WebSocket gateway and an idempotency contract for `message.add`. These are more
moving parts than a frontend-only optimization, but they remove the coupling
that caused the traffic amplification and protect every request type from the
same queue-pressure failure mode.
## Alternatives Considered
- **Keep all session subscriptions but reduce individual payloads.** Rejected
because task switching would still perform O(number of tasks) subscription
churn and task rows would remain coupled to session-internal events.
- **Derive one task aggregate entirely in the frontend.** Rejected because the
browser would still need every authoritative stream, every client would
duplicate precedence rules, and a suspended browser could not repair the
aggregate without replaying those streams.
- **Copy full session, Git, or PR records onto the task row.** Rejected because
it would create competing sources of truth and make a bounded list payload
grow with transcripts, repositories, or pull requests.
- **Poll task details from REST.** Rejected because polling introduces stale
badges and repeated full-list reads while the workspace task event channel
already provides an appropriate bounded delivery path.
- **Only enlarge the WebSocket queue or message timeout.** Rejected because
queue pressure would remain proportional to background sessions and a full
queue could still make accepted writes ambiguous.