Files
kandev/docs/specs/agents/requirements/subagent-context-persistence.md
T
2026-08-24 11:09:03 +00:00

19 KiB
Raw Blame History

status, system, created, updated, owners
status system created updated owners
draft agents 2026-08-12 2026-08-14
nova28

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 and tool_call_id are all present, THEN a task_session_subagents row SHALL exist for (task_session_id, agent_execution_id, tool_call_id) — where agent_execution_id is the frame's execution id or 'unknown' per AC-31.
  • AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.2: (legacy AC-1a) observed_at SHALL 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 initial tool_call carries no rawInput, so recognition occurs on a later tool_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 the skipped_no_identity writer 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 different turn_id, THEN the existing row SHALL be updated per AC-3 and its original turn_id SHALL 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 different agent_execution_id is 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_id is non-empty, THEN the row SHALL be written with that parent_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, or duration_ms, THEN the corresponding column SHALL be NULL. The system SHALL NOT write 0 for 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_count as exactly 0 — that is, SubagentTaskPayload.ToolUseCount is a non-nil pointer whose value is 0 — THEN the column SHALL be 0, 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, or duration_ms is negative, THEN the system SHALL store NULL for that field, SHALL increment the anomalous_value writer 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, THEN agent_status and tool_status SHALL hold their last observed values (or NULL if none was ever reported) and settled_at SHALL 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_status value Kandev does not recognise, THEN it SHALL be stored verbatim and SHALL NOT affect settled_at. agent_status has an open, provider-defined vocabulary.
  • AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.15: (legacy AC-11) WHEN a frame's ACP tool_status is first classified terminal by isTerminalToolStatus, THEN settled_at SHALL 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_at is already set, THEN settled_at SHALL remain set, tool_status SHALL 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_id or in agent_execution_id are 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_subagents rows SHALL be removed by the foreign key cascade, consistent with task_session_messages and task_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. (Mirrors task_external_id_migration_test.go: seed a pre-migration row, call runMigrations twice, 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/db error 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 a WARN log 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 existing task_session_messages row whose metadata.normalized.kind is subagent_task and whose task_session_id, task_id and metadata.tool_call_id are 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 greatest created_at, tiebroken by the greatest id, 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 0 and 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_since NOR subagent_context_backfill_through SHALL be written, the failure SHALL be logged at WARN per 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_since is absent while subagent_context_capture_since is present, THEN a consumer SHALL read that as "this installation captures subagent contexts but has not successfully applied Amendment 1", and SHALL NOT interpret any agent_execution_id value 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 … SELECT and both of its kandev_meta key 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_through has unknown fan-out, not zero fan-out; rows with source = 'backfill' are limited to what survived in message metadata and SHALL NOT be compared like-for-like against live rows without disclosing the provenance split. Absence of a row is never evidence of absence of a subagent before subagent_context_capture_since.
  • AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.40: (legacy AC-26) The system SHALL expose counters, under the existing expvar convention used by routing_* and subproc_*, for: attempted, persisted, skipped_no_identity, anomalous_value, failed, and unknown_execution (AC-31).
  • AC-AGENTS-SUBAGENT-CONTEXT-PERSISTENCE-001.41: (legacy AC-27) WHEN a persist attempt fails, THEN the system SHALL log at WARN with the session id and tool-call id, SHALL increment the failed counter, 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_since is 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) on task_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 ExecutionID is non-empty, THEN the row's agent_execution_id SHALL 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 own agent_execution_id, and the later execution's row SHALL be unchanged in every column — including tool_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_since SHALL 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.