176 lines
8.4 KiB
Markdown
176 lines
8.4 KiB
Markdown
---
|
|
status: complete
|
|
updated: 2026-08-09
|
|
created: 2026-08-02
|
|
owner: kandev
|
|
---
|
|
|
|
# Interrupted Task Indicator
|
|
|
|
## Why
|
|
|
|
When Kandev crashes, is killed, or restarts while tasks are mid-turn, those
|
|
sessions are interrupted: the agent process is gone and the turn was cut short.
|
|
Startup reconciliation already flips such sessions to `WAITING_FOR_INPUT` and
|
|
the task to `REVIEW`, so after a restart an interrupted task is visually
|
|
indistinguishable from a task that finished its turn and is waiting normally.
|
|
Operators cannot tell which tasks lost work to the crash. This spec adds a
|
|
durable "interrupted" marker set at startup for tasks whose sessions were
|
|
mid-turn, surfaced as a red icon in the task list.
|
|
|
|
## What
|
|
|
|
- At startup, when reconciliation finds a session that was `STARTING` or
|
|
`RUNNING` (mid-turn) when the backend died, the owning task is marked
|
|
interrupted with a durable task metadata key `interrupted_at` holding the
|
|
reconciliation timestamp (RFC 3339 UTC).
|
|
- Sessions that were `WAITING_FOR_INPUT`, `IDLE`, `CREATED`, or terminal at
|
|
startup are not marked: they were not interrupted mid-work.
|
|
- Archived or deleted tasks are not marked (they do not appear in the active
|
|
task list).
|
|
- The task API exposes `interrupted: true` on the task DTO while the marker is
|
|
present; it is derived from the metadata key, so no schema migration is
|
|
required.
|
|
- Task list surfaces render a red alert icon beside the task while
|
|
`interrupted` is true: sidebar task rows, the mobile task-switcher drawer
|
|
(shared row rendering), kanban board cards, graph/swimlane nodes, the rich
|
|
task-list row, and the open-task state icon. The icon carries an accessible
|
|
label and tooltip ("Interrupted by restart").
|
|
- The marker clears when a session of the task next enters `STARTING` or
|
|
`RUNNING` — work has resumed, so the task is no longer interrupted — and the
|
|
cleared state is published over the existing `task.updated` event so open
|
|
clients drop the icon live.
|
|
- The red icon renders instead of the idle/done affordances only. Pending
|
|
permission, pending clarification, generating/background activity, and
|
|
running spinners keep their existing precedence; terminal `COMPLETED`,
|
|
`FAILED`, and `CANCELLED` states keep their existing icons.
|
|
|
|
## Data model
|
|
|
|
No schema change. The marker lives in the existing `tasks.metadata` JSON
|
|
column under a new reserved key:
|
|
|
|
```
|
|
MetaKeyInterruptedAt = "interrupted_at" // value: RFC 3339 UTC timestamp string
|
|
```
|
|
|
|
Task metadata is already serialized into the API task DTO and already
|
|
survives restarts. The repository's existing `SetTaskMetadataKey` /
|
|
`RemoveTaskMetadataKey` helpers (concurrent-key-safe JSON patch, both SQLite
|
|
and Postgres) are the only writers; nothing else may mutate the key.
|
|
|
|
## API surface
|
|
|
|
- `v1.Task` gains `interrupted bool json:"interrupted,omitempty"`.
|
|
- The field is computed at DTO conversion time in both task serializers
|
|
(`internal/task/dto` and `internal/task/models` ToAPI) as
|
|
`metadata["interrupted_at"] != nil`. It is not a stored column.
|
|
- The field flows through every existing task payload automatically: boot
|
|
payload, `kanban.update`, `task.created`, `task.updated`, `task.state_changed`,
|
|
and `GET /api/v1/tasks/:id`.
|
|
- No new HTTP route, WebSocket action, or event type.
|
|
|
|
## State machine
|
|
|
|
The marker follows this lifecycle:
|
|
|
|
- `unmarked`: no `interrupted_at` key.
|
|
- `marked`: `interrupted_at` present; task DTO reports `interrupted: true`.
|
|
- `cleared`: key removed; DTO reports `interrupted: false` (omitted).
|
|
|
|
Transitions:
|
|
|
|
- `unmarked -> marked` at startup reconciliation when a preserved
|
|
`executors_running` row's session was `STARTING` or `RUNNING` and the task
|
|
exists and is not archived. Overwriting an existing key is safe: a marked
|
|
task cannot have a mid-turn session at the next startup unless it was
|
|
resumed and crashed again.
|
|
- `marked -> cleared` when any session of the task transitions into
|
|
`STARTING` or `RUNNING` (launch, resume, prompt dispatch, agent-ready
|
|
wake). The transition chokepoint in the orchestrator
|
|
(`updateTaskSessionStateWithHook` / `setSessionStarting`; the implementer
|
|
verifies the funnel covers every start path) removes the key and publishes
|
|
`task.updated` only when the key was actually removed.
|
|
- Terminal task/session transitions do not clear the marker; terminal state
|
|
icons take precedence over the red icon, and archiving/deleting the task
|
|
removes the row with its metadata.
|
|
|
|
## Permissions
|
|
|
|
None. The marker is display metadata on a task the viewer can already see. It
|
|
is user-writable only through the generic task metadata update surface and has
|
|
no effect beyond the icon.
|
|
|
|
## Failure modes
|
|
|
|
- A metadata write fails during reconciliation: the marker is skipped with a
|
|
warning and startup continues; the task simply shows no icon until the next
|
|
restart.
|
|
- A metadata removal fails during a session start: the marker survives and the
|
|
next successful start transition clears it; the icon may briefly persist
|
|
behind the running spinner, which takes precedence.
|
|
- Remote executors: the backend connection is severed even if the remote agent
|
|
is still alive. The task is marked. If the remote status poll later reports
|
|
the session running again and the session transitions, the marker clears; if
|
|
not, the marker stays until the next resume. This is acceptable — from
|
|
Kandev's perspective the turn was cut off.
|
|
- An already-open client during startup reconnects to the boot payload, which
|
|
carries the marker; no startup-time event is required.
|
|
- A `task.updated` event that omits `interrupted` must not wipe a set marker in
|
|
client state (frontend merge preserves the field when the payload omits it,
|
|
mirroring `foreground_activity`).
|
|
|
|
## Persistence guarantees
|
|
|
|
- The marker is durable in `tasks.metadata` and survives any number of
|
|
restarts until a session start clears it.
|
|
- Clearing is a normal key removal; a crash between removal and the
|
|
`task.updated` publish leaves the client icon stale until the next payload,
|
|
never a corrupt row.
|
|
|
|
## Scenarios
|
|
|
|
- **GIVEN** a task whose primary session was `RUNNING` when the backend died,
|
|
**WHEN** the backend starts and reconciliation runs, **THEN** the task's
|
|
metadata contains `interrupted_at`, the task DTO reports `interrupted: true`,
|
|
and the sidebar row, board card, graph node, rich list row, and mobile
|
|
switcher row show the red alert icon.
|
|
- **GIVEN** that task also has a stored `generating` task summary, **WHEN** reconciliation changes
|
|
the primary session to `WAITING_FOR_INPUT`, **THEN** the summary clears `generating` and the red
|
|
alert icon is not hidden by a stale running spinner.
|
|
- **GIVEN** a task whose session was `WAITING_FOR_INPUT` when the backend
|
|
died, **WHEN** reconciliation runs, **THEN** the task is not marked and shows
|
|
no red icon.
|
|
- **GIVEN** an archived task with a mid-turn session row, **WHEN**
|
|
reconciliation runs, **THEN** the task is not marked.
|
|
- **GIVEN** a marked task, **WHEN** the user resumes the session and it enters
|
|
`STARTING`/`RUNNING`, **THEN** the marker is removed, `task.updated` is
|
|
published, and every surface drops the red icon (running spinners take over).
|
|
- **GIVEN** a marked task that is never resumed, **WHEN** the backend restarts
|
|
again, **THEN** the marker survives and the icon still renders.
|
|
- **GIVEN** a marked task whose state is `COMPLETED`, **WHEN** any surface
|
|
renders it, **THEN** the green done icon wins and no red icon is shown.
|
|
- **GIVEN** a `task.updated` event without an `interrupted` field for a marked
|
|
task, **WHEN** a client merges it, **THEN** the client keeps the marker.
|
|
|
|
## Out of scope
|
|
|
|
- Changing the task or session state machine (no new task state; the task
|
|
remains `REVIEW` with a `WAITING_FOR_INPUT` session after reconciliation).
|
|
- Marking sessions or tasks `FAILED` on interruption; resumability behavior is
|
|
unchanged.
|
|
- Persisting the marker as a typed column; metadata is the single source.
|
|
- Office agent cards and the Office dashboard surface, which use their own
|
|
status presentation.
|
|
- Reconstructing detached remote-work liveness after restart.
|
|
- A user-facing action to clear or dismiss the icon manually.
|
|
|
|
## Related
|
|
|
|
- [Task Runtime Cleanup](runtime-cleanup.md) — startup reconciliation semantics
|
|
the marker hooks into.
|
|
- [Sidebar Task Completion Icons](../ui/sidebar-task-completion-icons.md) —
|
|
the shared task-row icon precedence this feature extends.
|
|
- [Backend Runtime-State Ownership](../../plans/backend-runtime-state-ownership/plan.md) —
|
|
startup publication that clears stale generating activity.
|