19 KiB
status, system, created, updated, owners
| status | system | created | updated | owners | |
|---|---|---|---|---|---|
| draft | agents | 2026-08-12 | 2026-08-14 |
|
Subagent context persistence Requirements
Overview
When an agent fans out to subagents (Claude's Task tool, OpenCode's task, Cursor's Task, Auggie's sub-agent-<type>:), Kandev already recognises the fan-out and already receives the child's identity and usage on the completion frame. It renders them, counts them in memory, and then keeps no queryable record of them.
Requirements
REQ-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001: Subagent context persistence
Intent: When an agent fans out to subagents (Claude's Task tool, OpenCode's task, Cursor's Task, Auggie's sub-agent-<type>:), Kandev already recognises the fan-out and already receives the child's identity and usage on the completion frame. It renders them, counts them in memory, and then keeps no queryable record of them.
Acceptance criteria
Each legacy criterion has a stable canonical ID below. The legacy label preserves traceability to the migrated source; the complete rationale, tables, SQL, failure modes, and verification scenarios remain in the linked system-design parts.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.1: (legacy AC-1) WHEN the orchestrator observes a tool-call frame whose normalized kind is
subagent_task, and a non-empty session id, task id andtool_call_idare all present, THEN atask_session_subagentsrow SHALL exist for(task_session_id, agent_execution_id, tool_call_id)— whereagent_execution_idis the frame's execution id or'unknown'per AC-31. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.2: (legacy AC-1a)
observed_atSHALL be the instant Kandev first observed a frame it could recognise as a subagent, not the instant the subagent began work. For Claude and OpenCode the initialtool_callcarries norawInput, so recognition occurs on a latertool_call_update; the column is therefore a Kandev observation time and SHALL NOT be published as a subagent start time. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.3: (legacy AC-2) WHEN a subagent frame carries an empty
tool_call_id, an empty session id, or an empty task id, THEN the system SHALL NOT write a row, SHALL increment theskipped_no_identitywriter counter, and SHALL NOT fail the enclosing message write or turn. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.4: (legacy AC-2a) WHEN a
(task_session_id, agent_execution_id, tool_call_id)that already has a row is observed again under a differentturn_id, THEN the existing row SHALL be updated per AC-3 and its originalturn_idSHALL be preserved. A second row SHALL NOT be created, and the fan-out SHALL NOT be re-attributed to the later turn. A frame bearing a differentagent_execution_idis a different key and is governed by AC-32, not by this criterion. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.5: (legacy AC-3) WHEN a later frame for an already-recorded
(task_session_id, agent_execution_id, tool_call_id)arrives, THEN the system SHALL update that same row and SHALL NOT create a second row. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.6: (legacy AC-4) WHEN a frame reports a field that the stored row already holds non-NULL, and the frame's value for that field is absent or empty, THEN the stored value SHALL be preserved unchanged. (This mirrors
applySubagentResult, which never blanks a value already learned from an earlier frame.) - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.7: (legacy AC-5) WHEN a frame reports a non-empty value for a field, THEN that value SHALL replace the stored value, EXCEPT for the fields enumerated below, which are governed by their own criteria and SHALL NOT be replaced by this rule.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.8: (legacy AC-6) WHEN a frame reports a subagent whose
parent_tool_call_idis non-empty, THEN the row SHALL be written with thatparent_tool_call_id, and SHALL NOT be suppressed for being nested. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.9: (legacy AC-7) WHEN an agent does not report
total_tokens,tool_use_count, orduration_ms, THEN the corresponding column SHALL be NULL. The system SHALL NOT write0for an unreported value. (This is not hypothetical: 190 of 253 observed rows report none of the three.) - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.10: (legacy AC-7a) For tool_use_count, nil means not reported and a non-nil zero is a reported zero. For plain total_tokens and duration_ms, zero means not reported; later absent, zero, or negative values SHALL NOT erase a learned value.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.11: (legacy AC-8) WHEN an agent reports
tool_use_countas exactly0— that is,SubagentTaskPayload.ToolUseCountis a non-nil pointer whose value is 0 — THEN the column SHALL be0, distinguishable by query from an unreported count. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.12: (legacy AC-9) WHEN a reported numeric value for
total_tokens,tool_use_count, orduration_msis negative, THEN the system SHALL store NULL for that field, SHALL increment theanomalous_valuewriter counter, and SHALL leave every other field of the frame unaffected. A negative count is not a measurement. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.13: (legacy AC-10) WHEN a subagent is never observed reaching a terminal
tool_status, THENagent_statusandtool_statusSHALL hold their last observed values (or NULL if none was ever reported) andsettled_atSHALL be NULL. The row SHALL still exist. (5 observed rows are in exactly this state.) - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.14: (legacy AC-10a) WHEN a frame reports an
agent_statusvalue Kandev does not recognise, THEN it SHALL be stored verbatim and SHALL NOT affectsettled_at.agent_statushas an open, provider-defined vocabulary. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.15: (legacy AC-11) WHEN a frame's ACP
tool_statusis first classified terminal byisTerminalToolStatus, THENsettled_atSHALL be set to that observation time and SHALL NOT be modified by any later frame. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.16: (legacy AC-12) WHEN a non-terminal frame arrives for a row whose
settled_atis already set, THENsettled_atSHALL remain set,tool_statusSHALL remain the terminal value, and the frame MAY still fill columns that are currently NULL. (Out-of-order and duplicate frame delivery is an observed reality on this stream.) - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.17: (legacy AC-13) WHEN subagent contexts are read for a session or a turn, THEN the order SHALL be
observed_at ASC, tool_call_id ASC, agent_execution_id ASC. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.18: (legacy AC-14) WHEN two frames for the same
(task_session_id, agent_execution_id, tool_call_id)are processed concurrently, THEN the write SHALL be a single atomic upsert statement whose conflict target is those three columns, and exactly one row SHALL result. The implementation SHALL NOT use a read-then-write sequence for field merging; the fill-forward and terminality rules are expressed inside the upsert's conflict clause. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.19: (legacy AC-15) WHEN two frames whose keys differ in
tool_call_idor inagent_execution_idare processed concurrently (the observed 3-, 4-, 5- and 8-way fan-outs, and the cross-execution collision of AC-32), THEN both rows SHALL be written, and neither SHALL be merged into, overwritten by, or lost to the other. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.20: (legacy AC-16) WHEN the parent session row is deleted, THEN its
task_session_subagentsrows SHALL be removed by the foreign key cascade, consistent withtask_session_messagesandtask_session_turns. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.21: (legacy AC-17) WHEN
runMigrations()runs against a database that predates this feature, THEN the table, its indexes and its constraints SHALL be created, and existing data SHALL be unaffected. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.22: (legacy AC-18) WHEN
runMigrations()is invoked twice in succession against the same database, THEN the second invocation SHALL succeed and SHALL leave the schema and the row set unchanged. (Mirrorstask_external_id_migration_test.go: seed a pre-migration row, callrunMigrationstwice, assert idempotence.) - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.23: (legacy AC-19) WHEN the migration runs against PostgreSQL, THEN it SHALL apply and replay with the same result as SQLite, using
internal/dberror classification rather than local error-string matching, per ADR 0027. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.24: (legacy AC-20) WHEN a statement in this migration fails, THEN — because the repository's migration runner swallows errors (
internal/db/migratelog.go:33) — the failure SHALL be observable as aWARNlog carrying the migration's name, and the writer's health signals (AC-24, AC-25) SHALL make the resulting absence of rows detectable rather than silent. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.25: (legacy AC-21) WHEN the migration runs, THEN it SHALL insert one row with
source = 'backfill'for every existingtask_session_messagesrow whosemetadata.normalized.kindissubagent_taskand whosetask_session_id,task_idandmetadata.tool_call_idare all non-empty. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.26: (legacy AC-21b) WHEN two or more qualifying messages share the same
(task_session_id, metadata.tool_call_id), THEN the backfill SHALL derive the row from the message with the greatestcreated_at, tiebroken by the greatestid, and SHALL insert exactly one row for that key. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.27: (legacy AC-21a) WHEN a qualifying message is missing any of those three identities, THEN the backfill SHALL skip it: no row, and the statement SHALL NOT fail.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.28: (legacy AC-22) Backfill conflicts SHALL use ON CONFLICT DO NOTHING: existing live/backfill rows are preserved and replay is idempotent; real execution IDs remain distinct from the unknown sentinel.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.29: (legacy AC-23) WHEN the backfill derives a value, THEN AC-7 through AC-9 and the empty-string-to-NULL rule SHALL apply identically: an absent JSON field becomes NULL, never
0and never''. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.30: (legacy AC-23a) The backfill SHALL use one unbounded INSERT … SELECT, derive terminal settled_at from top-level metadata.status, and scan the full message history once; it SHALL NOT batch or limit the migration.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.31: (legacy AC-23b) PostgreSQL backfill SHALL use the dialect-aware JSON helpers so empty or null metadata cannot abort the migration.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.32: (legacy AC-24) A successful first activation SHALL write the applicable kandev_meta activation keys once: subagent_context_capture_since, subagent_context_backfill_through, and the execution-boundary key when its schema contract holds. The values and their timestamp/sentinel semantics are defined in the migrated design.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.33: (legacy AC-24a) WHEN the AC-21 backfill statement fails, THEN NEITHER
subagent_context_capture_sinceNORsubagent_context_backfill_throughSHALL be written, the failure SHALL be logged atWARNper AC-20, and the backfill SHALL be attempted again on the next boot. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.34: (legacy AC-24b) WHEN
subagent_context_execution_sinceis absent whilesubagent_context_capture_sinceis present, THEN a consumer SHALL read that as "this installation captures subagent contexts but has not successfully applied Amendment 1", and SHALL NOT interpret anyagent_execution_idvalue on that installation as an execution identity. This is the durable, queryable signal that the amended shape is not in place (AC-33a). - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.35: (legacy AC-24c) Consumers SHALL distinguish all four states of capture_since and execution_since: both present means fully activated; capture only means the execution-aware amendment is not complete; execution only means backfill has not succeeded and no coverage boundary exists; neither means the feature has not successfully activated. Consumers SHALL use the keys, not row presence, to select the state.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.36: (legacy AC-24d) WHEN the backfill migration runs, THEN the AC-21
INSERT … SELECTand both of itskandev_metakey writes SHALL be atomic with respect to each other: either the insert commits and both keys are written, or neither key is written. A state in which rows were inserted but only one key exists SHALL NOT be an allowed outcome. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.37: (legacy AC-24e) Activation keys SHALL be parsed/bound as timestamps or normalized to one lexical form; raw key strings SHALL NOT be compared to timestamp columns.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.38: (legacy AC-24f) With no qualifying message, backfill_through SHALL be the empty-string sentinel, never parsed as a timestamp; consumers read it as no pre-capture coverage.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.39: (legacy AC-25) The published contract for consumers SHALL be: a session whose activity predates
subagent_context_backfill_throughhas unknown fan-out, not zero fan-out; rows withsource = 'backfill'are limited to what survived in message metadata and SHALL NOT be compared like-for-like againstliverows without disclosing the provenance split. Absence of a row is never evidence of absence of a subagent beforesubagent_context_capture_since. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.40: (legacy AC-26) The system SHALL expose counters, under the existing
expvarconvention used byrouting_*andsubproc_*, for:attempted,persisted,skipped_no_identity,anomalous_value,failed, andunknown_execution(AC-31). - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.41: (legacy AC-27) WHEN a persist attempt fails, THEN the system SHALL log at
WARNwith the session id and tool-call id, SHALL increment thefailedcounter, and SHALL NOT fail the enclosing message write, the turn, or the agent stream. Telemetry never breaks the product path. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.42: (legacy AC-28) The health check SHALL use the two directed, database-only anti-joins scoped by subagent_context_capture_since: expected message keys missing a subagent row are the actionable shortfall, while excess subagent keys are reported for attribution and SHALL NOT mask or cancel a shortfall.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.43: (legacy AC-28a) WHEN
subagent_context_capture_sinceis absent, THEN the AC-28 comparison SHALL NOT be run, and the key's absence SHALL itself be the health signal, read per AC-24c. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.44: (legacy AC-28b) WHEN a shortfall is observed, THEN it SHALL be confirmed by a second evaluation before being treated as evidence of a failing writer, OR both counts SHALL be taken within a single read transaction.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.45: (legacy AC-29) WHEN the writer has stopped while message writes continue, THEN the AC-28 comparison SHALL diverge, and the divergence SHALL be attributable in time via
MAX(observed_at)ontask_session_subagents. No separate heartbeat row is written, precisely so that the health signal cannot itself be the thing that silently stops. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.46: (legacy AC-30) WHEN the orchestrator records a subagent context from a frame whose
ExecutionIDis non-empty, THEN the row'sagent_execution_idSHALL be that value verbatim, and the row SHALL be keyed on(task_session_id, agent_execution_id, tool_call_id). - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.47: (legacy AC-31) Without an execution identity, live and backfill writes SHALL store agent_execution_id = unknown, never NULL or empty; live writes also increment unknown_execution.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.48: (legacy AC-32) WHEN two executions in the same session emit the same
tool_call_id, and a frame from the earlier execution arrives after the later execution's row exists, THEN the earlier frame SHALL create or update its own row keyed on its ownagent_execution_id, and the later execution's row SHALL be unchanged in every column — includingtool_status,settled_at, and the three metric columns. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.49: (legacy AC-33) Migration SHALL produce the execution-aware non-null column, three-column unique key, required indexes, preserved rows with unknown sentinels, and execution activation key on SQLite and PostgreSQL.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.50: (legacy AC-33a) WHEN the AC-33 migration fails or does not apply, THEN boot SHALL NOT abort, the failure SHALL be logged at
WARN,subagent_context_execution_sinceSHALL NOT be written (AC-24b), the table SHALL be left in its pre-amendment shape rather than a partial one, and the migration SHALL be attempted again on the next boot. - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.51: (legacy AC-33b) When AC-33 clauses 1–3 hold, runMigrations SHALL write execution_since if absent, regardless of whether rebuild or schema-init created the shape, and preserve an existing value.
- AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.52: (legacy AC-33c) WHEN
runMigrations()runs, THEN the AC-33 shape change and the AC-33b key write SHALL be attempted before the AC-21 backfill within the same pass. AND WHEN the AC-33 end state does not hold, THEN the backfill SHALL NOT run, SHALL NOT write either of its keys, and SHALL be attempted again on the next boot (AC-24a). - AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.53: (legacy AC-34) WHEN
runMigrations()is invoked twice in succession against a database migrated by AC-33, THEN the second invocation SHALL succeed, SHALL leave the schema and the row set unchanged, and SHALL NOT duplicate, drop, or re-sentinel any row.
System design
The migrated technical source is split into part 1, part 2, part 3, part 4, part 5, part 6, part 7, part 8.