Files
kandev/docs/specs/clarification-active-lifecycle/spec.md
T
2026-08-17 11:14:12 +00:00

37 KiB

status, created, owner
status created owner
approved 2026-08-14 kandev

Active Clarification Lifecycle

Why

A clarification can outlive the agent wait that created it. Kandev keeps that detached question answerable so a timeout or connection loss does not discard required user input. That durability must not let a question from an older turn remain operational after the session has accepted newer work.

One stale clarification currently can reappear in chat, restore a task-row question icon, and block a workflow transition after the user has dismissed a newer question. A real question in a secondary session can also produce a correct task-row icon while task navigation opens the clean primary session, hiding the action the icon represents.

What

  • A clarification bundle is active only when at least one row in the bundle is pending and the bundle belongs to the session's current turn.
  • A detached bundle remains active and answerable while its turn remains current. Detachment sets agent_disconnected=true; it does not by itself resolve the question.
  • After a session successfully enters a terminal state, its current-turn pending clarification bundles expire so no answerable overlay survives a completed, failed, or cancelled session.
  • Acceptance of a newer turn supersedes every pending clarification from an older turn. Superseded rows remain transcript history but cannot drive a chat overlay, task/session pending projection, workflow guard, turn-completion detach pass, or late agent resume.
  • Deleting every message from the newer turn does not move ownership backward or reactivate an older clarification.
  • All backend consumers derive active clarification state from one repository rule. Event payloads trigger projection refreshes; they are not a second source of pending truth.
  • Repeated detach/completion processing is a semantic no-op after a bundle is already detached. It emits no duplicate message.updated occurrence.
  • Detachment claims pending, non-detached rows from the current durable turn in one database update and publishes only the rows that update returns. A concurrent answer or newer turn cannot be overwritten by a stale read-modify-write detachment.
  • Clarification-pause cancellation snapshots turn authority before detachment. With a wired turn service, both a specific turn and the absence of a turn are explicit expectations; a first or successor turn created during detachment cannot be cancelled by the stale pause. Installations without a turn service retain the legacy unscoped fallback.
  • Resolving, rejecting, cancelling, expiring, or deleting one bundle changes only that bundle. It cannot clear or re-arm another bundle in the same session.
  • The chat's Skip action rejects the exact visible bundle through the existing response endpoint. A live waiter receives the rejection in the same turn. A detached current-turn bundle is persisted as rejected without resuming the agent.
  • An affirmative response to a detached current-turn bundle returns success only after the orchestrator accepts one resume dispatch within a bounded wait. The response waits for prompt acknowledgement, not agent-turn completion. Before dispatch, the successor is durably reserved but marked unpublished so provider frames can reference it without making it current. Immediately before the external executor call, Kandev durably marks the reservation attempted. That marker is the at-most-once boundary: a crash can no longer prove whether agentctl accepted the prompt, so restart preserves the successor and keeps the claimed bundle terminal. Acknowledgement publishes a recovery-clean turn.started payload while the recovery metadata remains durable, then clears that metadata after the event bus accepts the event. HTTP success requires both operations. A rejected event publication therefore remains discoverable by startup reconciliation. Recovery atomically replaces an accepted or ambiguous reservation with a durable start-event outbox marker. Before admitting work, startup replays turn.started, followed by turn.completed when the turn is already terminal, and clears the marker only after the event bus accepts every required event. Failed replay fails startup and leaves the marker for the next attempt. Every public turn event, including a completion racing live cleanup or emitted during recovery, strips the private prompt-dispatch fields. If the predecessor's delayed ready event overlaps this private reservation, ready handling waits for the reservation to resolve and then revalidates prompt generation before touching turn or workflow state. It cannot complete the reserved successor or run predecessor completion actions against it. When the reservation rolls back, a ready event whose predecessor generation still owns the session continues through normal completion so the predecessor and its queue are not stranded. A generationless ready event that overlapped the reservation is dropped after resolution because ownership cannot be proven. If the attempt marker cannot be persisted, dispatch does not occur and a fresh bounded context rolls back the reservation and session claim even when the transport context was cancelled, so the answer remains retryable. If agentctl synchronously rejects the prompt, the reservation is rolled back and the answer can be restored. If agentctl accepts the prompt but publication or later transport handling fails, the endpoint returns a server error, performs normal prompt-failure cleanup, and keeps the claimed bundle terminal because retrying could dispatch the answer twice. Startup deletes only an empty, unattempted unpublished reservation and restores the exact clarification rows claimed for its dispatch; an attempt marker or message evidence instead proves dispatch ambiguity and preserves the successor. Authority and recovery treat boolean true, strings "true" and "1", and numeric 1 as equivalent pending/attempted flags across SQLite and PostgreSQL. If reservation reconciliation is unavailable or fails, orchestrator startup fails before watcher, scheduler, or prompt admission starts; the next start retries recovery. A production turn repository must provide this recovery capability through its compile-time contract. A rejection persists terminal status without resuming the agent.
  • Every response atomically claims current-turn ownership and persists a response-delivery recovery intent before it can reach a live waiter or request a detached resume. A live waiter runs durable delivery confirmation before returning the response to the agent; enqueue alone does not retire the intent. Once confirmation starts, it owns its durable operation through completion even if the responder's bounded wait expires. Its input claim remains immutable, its result remains local to the callback, and any compensating restore serializes against finalization so only one durable outcome wins. The detached path retires the intent only at its durable resume boundary. Startup first reconciles prompt reservations, then restores an unhanded current-turn claim to pending; a terminal session or newer authoritative turn instead retires the stale intent without reactivating history. Terminal message updates are published only after delivery succeeds. If detached resume acceptance fails, the endpoint returns an error and restores the still-current bundle to pending so the same answer can be retried. Restored rows publish after commit even when synchronous task-summary acknowledgement fails, preventing clients from retaining the terminal snapshot while the endpoint still returns the acknowledgement error. A publication or summary-convergence error after the database restore does not make that retry unsafe; durable pending state remains authoritative. Once agentctl accepts the prompt, later publication or completion errors cannot roll back the successor turn or reopen the answer. A primary-answer watchdog carries the clarification turn ID and revalidates that ID both before fallback and inside serialized prompt admission, so it cannot dispatch a stale answer into a successor turn. Its fallback keeps the watchdog cancellation context through authority reads and prompt admission so session activity or service shutdown interrupts in-flight recovery work.
  • A current-turn bundle remains answerable while any sibling question is pending. Recovery claims only those pending rows, preserves siblings already made terminal by an earlier partial write, and restores only the claimed rows if detached delivery fails. Primary delivery events and detached recovery derive one turn identity from the same bundle rule: legacy empty turn IDs do not mask a consistent non-empty identity, while conflicting non-empty IDs invalidate the identity.
  • Any response to a superseded or terminal bundle returns conflict, performs no message mutation, and initiates no agent resume. Current clients close their obsolete local overlay through the existing conflict handling.
  • Persisted task status summaries reconcile pending_action against current-turn repository state on source events and task-list/boot reads. Existing summaries are repaired, not only missing rows.
  • When a task row advertises a pending action, desktop and phone task activation load the task's sessions from the server and select the newest input-capable session whose pending_action matches the task action. Before applying that response, activation revalidates the task-summary revision and pending action. If either changes while the task remains present, activation discards the delayed session choice and opens the task-only route; phone activation also closes the sheet. Overlapping authoritative loads are generation-guarded per task, so an older response cannot replace a newer session snapshot. This pending owner outranks remembered-session and primary-session preferences. If the task still advertises pending input but no matching input-capable owner exists, activation releases the outgoing session layout and fails closed to the task route without guessing a session. Normal preference order returns only for a clean task. If the task projection disappears while that authoritative load is in flight, desktop and phone leave the selection inert instead of navigating to a deleted task, including tasks using the legacy pending-action projection. A forced load aborted by a newer load is also inert; it is not treated as a request failure requiring task-only fallback. Each mounted phone task sheet owns its selection generation, so simultaneous instances cannot invalidate one another's in-flight task choice.

Data model

No schema change.

  • Clarification questions remain task_session_messages rows with type = "clarification_request".
  • Rows in one bundle share metadata.pending_id; terminal status remains in metadata.status.
  • task_session_messages.turn_id associates a question with its turn. The newest authoritative durable task_session_turns record for the session identifies the current turn; deleting messages does not delete that parent turn or move ownership backward.
  • A pre-acknowledgement successor carries metadata.prompt_dispatch_pending=true plus the source clarification turn, pending ID, and exact claimed message IDs. An empty row carrying only the pending marker is not turn authority. Immediately before external dispatch, metadata.prompt_dispatch_attempted=true records the at-most-once boundary and makes the successor authoritative if the process stops in the ambiguous window. A message referencing the successor is independent durable evidence of ambiguous acceptance.
  • metadata.agent_disconnected=true records that no in-memory waiter owns an otherwise active bundle.
  • A superseded row may retain metadata.status = "pending". Pending metadata is historical evidence, not sufficient proof that the request is operational.
  • A missing-status row in an older turn is superseded history. Turn ownership takes precedence over the legacy rule that missing status means pending.
  • TaskSession.pending_action and TaskStatusSummary.pending_action remain bounded derived fields. They are reconstructable and never become independent clarification state.

API surface

No new route. Task-session responses add pending_action_revision beside the existing pending_action projection.

  • GET /api/v1/tasks/:taskId/task-sessions continues to expose each session's current derived pending_action; task navigation uses this existing field.
  • Semantic session.message.added, session.message.updated, and session.message.deleted notifications include the authoritative per-session pending_action after mutations that can change it. The field is explicit null when clean and omitted when projection fails or the event is a replaceable content-only update; clients preserve the prior value when it is omitted. Each projection includes a pending_action_revision, shared with REST task-session snapshots. Its decimal epoch is a database-backed, monotonically allocated backend generation; sequence orders reads within that generation. Clients compare both fields and reject any older result, including an unseen pre-restart epoch delivered after client state has been rebuilt.
  • GET /api/v1/task-sessions/:sessionId/turns continues to expose durable turn history; unpublished reservations stay hidden until publication or durable message evidence, including while an attempt marker makes them internal current-turn authority. Attempted reservations are preserved across restart because their dispatch outcome is ambiguous. Visible history is ordered ascending by started_at, created_at, then id, matching the reverse ordering used to select the current turn.
  • Task list, workflow snapshot, and boot payloads continue to expose task-level pending_action in the status summary and legacy fallback fields.
  • POST /api/v1/clarification/:pendingId/respond uses one state-based contract:
    • active_live: answer or rejection returns success and is delivered to the same-turn waiter.
    • active_detached: an answer returns success only after the orchestrator acknowledges one resume dispatch and durably publishes its successor within a bounded wait; it does not wait for the resumed turn to complete. Rejection returns success and persists without resuming the agent. If resume acceptance fails, an answer returns a server error and the still-current bundle remains answerable for retry. If acceptance succeeds but successor publication fails, the endpoint returns a non-retryable server error and keeps the bundle terminal.
    • superseded_history or terminal: answer or rejection returns conflict, performs no write, and initiates no agent resume.
    • delivery_claimed: a concurrent second response returns conflict, performs no write, and initiates no agent resume because the first response already claimed every pending row.
  • POST /api/v1/clarification/:pendingId/cancel remains the low-level cancellation path for a request still owned by the in-memory clarification store. The chat Skip control uses /respond with rejected=true, including for detached requests.

State machine

One clarification bundle has five operational states:

  1. active_live: rows are pending in the current turn and an in-memory waiter exists.
  2. active_detached: rows are pending in the current turn, no waiter exists, and agent_disconnected=true records deferred-answer behavior.
  3. delivery_claimed: current-turn rows carry their provisional terminal answer plus a durable response-delivery intent, but no handoff boundary has been acknowledged yet.
  4. terminal: every actionable row is answered, rejected, cancelled, expired, or deleted, with no outstanding delivery intent.
  5. superseded_history: rows still carry pending history, but a newer turn is current.

Transitions:

  • Request creation enters active_live.
  • Wait timeout, disconnect, or turn teardown moves active_live -> active_detached once.
  • A response first moves either active state to delivery_claimed. Successful answer delivery or Skip then moves that exact pending_id to terminal. For a live response, the waiter must durably confirm consumption before returning it to the agent. A backend restart before any handoff restores a still-current delivery_claimed bundle to its prior active state; if a newer turn or terminal session already superseded it, recovery retires the intent and preserves terminal history. Cancel, expiry, or deletion moves an active state directly to terminal. A failed detached resume acceptance returns to active_detached while the same turn remains current only when dispatch is known not to have been accepted and rollback succeeds. A post-attempt crash or post-acceptance publication failure remains terminal because the prompt may already be running.
  • Acceptance of a newer turn moves any older pending bundle to superseded_history operationally; no history rewrite is required.
  • Neither terminal nor superseded_history can become active again. Only the provisional delivery_claimed state is recoverable. A new request creates a new bundle identity; message deletion cannot reverse this transition.

Permissions

No authorization change. A user can see, answer, or dismiss only clarification data for a task and session they can already access. Session selection does not broaden task visibility.

Failure modes

  • Active-state repository read fails: workflow guarding fails closed, and projections keep the last known pending value. A later message event or list/boot read retries convergence.
  • Terminal-session expiry persistence fails: the terminal session state still quarantines pending history from task/session projections and interactive overlays; a stale response claim remains rejected by the terminal-session predicate.
  • A forced task-session list cannot project authoritative pending actions: fail the HTTP or WebSocket request instead of returning a successful list with empty pending ownership.
  • Summary compare-and-set loses a race: reload the newer summary, reapply authoritative pending state, and retry within a bounded loop. Exhaustion is an error, so restored-state acknowledgement is withheld until the pending action is durably confirmed. Task-list and boot reads explicitly invalidate that task's stale summary on repair failure so an unchanged browser cache clears it and exposes the authoritative coarse pending action. Ordinary summary omission remains a partial-response no-op, and an invalidating response cannot erase a newer live summary received while that read was in flight. Never overwrite unrelated newer summary fields.
  • Summary repair persists but its WebSocket publication fails: the initiating response carries the corrected summary; other clients converge on their next event or read.
  • A stale browser submits an older-turn answer: return conflict, do not update runtime ownership, and do not dispatch a prompt.
  • Detached resume context resolution or orchestrator acceptance fails: use a non-cancelled context with a finite deadline for acceptance and persistence, withhold terminal message events, restore the still-current bundle to pending, and return a retryable server error instead of reporting false success. Attempt to refresh and persist the task summary synchronously from authoritative pending rows, and publish the committed restored messages even if that acknowledgement fails. A later event or read repairs the summary cache; the response still reports that the durable answer can be retried.
  • Persisting a successor turn or dispatch-attempt marker fails: use a fresh bounded context to roll back the session claim and any reserved successor before making an external executor call, restore the still-current bundle, and return a retryable server error.
  • Reserved-successor rollback fails or has an ambiguous durable outcome: keep the live reservation unresolved so ready handling waits and later prompt admission remains blocked until restart recovery reconciles the row.
  • Clarification detachment, expiry persistence, and terminal bundle publication ignore request cancellation but always use a fresh bounded context, so a database lock or synchronous summary refresh cannot hold the per-session pause or HTTP response indefinitely.
  • Agentctl accepts a detached resume but durable successor publication fails: return a non-retryable server error, keep the claimed bundle terminal, and make later rollback fail closed so the accepted answer cannot be dispatched again in-process.
  • Live waiter delivery fails unexpectedly: restore the durable claim only if its turn remains current, report whether retry state was recovered, and never restore it after a successor turn is accepted. A started confirmation may finish after the responder's bounded wait, but it cannot mutate the responder's claim snapshot and its durable finalization serializes against that restore.
  • Historical partial terminalization leaves pending and terminal siblings in one current-turn bundle: complete the pending siblings without rewriting terminal history or returning a permanent conflict.
  • A malformed persisted pending row has no matching durable turn: drain any live in-memory waiter, but keep the row inert. If such pre-turn history is encountered, repair it through explicit data cleanup rather than treating it as current input authority.
  • Session loading fails during task activation: retain existing navigation fallback instead of stranding the user in the task drawer or on an unchanged URL.
  • A newer task-summary revision arrives while pending-owner loading is in flight: discard the delayed owner-session result, open the requested task through the task-only fallback, and let the current projection render the authoritative pending owner.
  • Backend stops after reserving a detached-answer successor but before the attempt marker: startup restores only the clarification rows claimed for that dispatch and removes the empty reservation.
  • Backend stops after the attempt marker but before dispatch acknowledgement: startup fails closed, preserves the successor as authoritative, and keeps its exact clarification claim terminal. Output referencing the reservation provides the same conservative authority even without the marker.
  • A delayed predecessor ready event arrives between the attempt marker and agentctl acknowledgement: wait for the live reservation outcome, then reject the event if its prompt generation was superseded; never complete the reserved successor or evaluate predecessor workflow completion against it. If the reservation rolls back and the predecessor generation still owns the session, process the ready event normally instead of stranding the predecessor turn. Drop a generationless event after the wait because it cannot be correlated safely with the predecessor.
  • Unpublished-reservation reconciliation fails during startup: fail startup before event processing or prompt admission begins so no new turn can supersede an unrecovered clarification claim.
  • Response-delivery intent reconciliation fails during startup: fail startup before event processing or prompt admission begins. Never leave an unhanded terminal claim permanently unanswerable.
  • Unpublished-reservation recovery metadata contains a non-string or empty claimed-message ID: fail the recovery transaction and startup, preserving the reservation and claimed rows for diagnosis and retry.

Persistence guarantees

  • Message history remains durable and is not destructively rewritten merely because a newer turn exists.
  • Active clarification state is reconstructable after restart from message status plus the newest authoritative durable turn.
  • Current-turn ownership is reconstructable from durable turn rows even when a turn has no remaining messages.
  • Unpublished detached-answer reservations carry enough recovery identity to restore only their own claimed rows. A durable attempt marker separates safe rollback from ambiguous dispatch, and startup reconciliation runs even when no executor record remains.
  • Provisional terminal response claims carry a durable delivery intent. Startup restores that exact current-turn claim when no live or detached handoff became authoritative, while prompt-reservation recovery owns any detached dispatch that reached its durable reservation boundary.
  • Task summaries are caches. Boot and task-list reads correct a stale persisted pending_action with a monotonic revision while preserving all unrelated summary fields.
  • No one-off mutation or backfill of an existing installation database is required. Deploying the corrected derivation makes historical older-turn pending rows inert and repairs their summaries on normal reads.

Scenarios

  • GIVEN a clarification wait disconnects and no newer turn exists, WHEN the task is reloaded, THEN the detached question remains visible and answerable, its session and task advertise clarification, and workflow completion stays blocked.
  • GIVEN an older turn retains a detached pending row, WHEN the session accepts a newer ordinary turn, THEN the old row remains in history but no overlay, pending projection, detach event, or workflow barrier derives from it.
  • GIVEN a newer turn superseded an older pending question, WHEN every message in the newer turn is deleted, THEN the durable newer turn remains current and the older question stays inert.
  • GIVEN an attempted successor reservation is authoritative but intentionally hidden from client turn history, WHEN the session projection explicitly reports no clarification action, THEN a pending question on the visible predecessor stays inert in chat and pending-input indicators.
  • GIVEN a browser cached an explicit clean session projection, WHEN a new current-turn clarification message arrives, THEN the same event advances the session projection to clarification before pending discovery so the question remains visible and answerable.
  • GIVEN a task-session list request is in flight, WHEN a newer semantic message event updates the same session before the HTTP response resolves, THEN the response's older projection revision cannot replace the WebSocket projection (and the reverse delivery order is equally safe).
  • GIVEN an old detached question and a newer clarification bundle, WHEN the user skips the newer bundle and reloads, THEN neither bundle reappears, the task question icon is absent, and later turn completion cannot re-arm the old bundle.
  • GIVEN a persisted summary says pending_action=clarification while current-turn repository state has no pending action, WHEN a boot or task-list payload is built, THEN the summary revision advances with no pending action and all unrelated fields remain unchanged.
  • GIVEN a task's secondary session owns a current clarification while the primary session is clean, WHEN the user activates the task from the desktop sidebar or phone task drawer, THEN Kandev bypasses any cached session list, selects the secondary session, closes the drawer when applicable, and shows the question.
  • GIVEN clarification pause observes no active turn, WHEN detachment creates the session's first turn before cancellation registration completes, THEN the pause rejects its stale cancellation and does not cancel or complete that new turn.
  • GIVEN pending-owner loading began for one task-summary revision, WHEN a newer summary changes the pending action before the session response is applied, THEN desktop and phone activation do not navigate to the obsolete owner, open the selected task through the task-only fallback, and close the phone drawer.
  • GIVEN two forced session loads overlap for one task, WHEN the older response finishes last, THEN it cannot overwrite the newer session snapshot or revive an obsolete pending owner.
  • GIVEN a forced session load is pending in the phone task sheet, WHEN a responsive-layout change unmounts it and mounts the tablet sheet before that load finishes, THEN the old continuation cannot navigate, change the active selection, or close the replacement sheet.
  • GIVEN a pending task has no loaded owner, WHEN activation falls back to its task route, THEN the outgoing session layout is released before the active session is cleared.
  • GIVEN a stale browser still displays a superseded question, WHEN it submits an answer, THEN the server returns conflict and does not resume or otherwise prompt the agent.
  • GIVEN a detached current-turn answer is not accepted by the orchestrator, WHEN the response endpoint fails, THEN it returns a server error, keeps the question answerable, and a later retry delivers exactly one accepted resume request without a failed-dispatch turn superseding the bundle.
  • GIVEN agentctl accepts a detached answer but successor publication fails, WHEN the response endpoint completes, THEN it returns an error, keeps the answer terminal, and does not make that accepted successor eligible for rollback or in-process redispatch.
  • GIVEN agentctl accepts a detached answer and later transport handling also fails, WHEN prompt handling returns, THEN normal failure cleanup runs and the accepted-answer marker still prevents the terminal clarification from being reopened.
  • GIVEN the backend stops after terminalizing a detached answer and reserving its successor but before marking dispatch attempted, WHEN it starts again, THEN it deletes the empty reservation, restores the exact claimed rows to pending, and leaves pre-existing terminal siblings unchanged.
  • GIVEN the backend stops after marking a detached-answer dispatch attempted but before observing acknowledgement, WHEN it starts again, THEN it preserves the successor as current and keeps the exact claimed rows terminal rather than risking duplicate answer dispatch.
  • GIVEN the backend stops after claiming a current-turn response but before either a live waiter or detached resumer receives it, WHEN it starts again, THEN startup restores the exact claimed rows to pending and the same answer can be submitted again.
  • GIVEN a response is enqueued to a live waiter, WHEN durable confirmation has not completed, THEN the waiter does not return the response and restart recovery may safely restore the claim.
  • GIVEN durable confirmation starts but outlives the responder's bounded wait, WHEN the responder attempts recovery, THEN the immutable claim is race-free and repository serialization lets either finalization or restore win without reopening a successfully finalized response.
  • GIVEN cancellation removes a live clarification after a responder loads it but before response delivery is claimed, WHEN that responder continues, THEN it returns not-found immediately so the caller can use detached recovery instead of waiting for an orphaned delivery confirmation. If response delivery wins first, later cancellation cannot preempt its durable confirmation.
  • GIVEN a primary-answer watchdog survives until another turn supersedes its clarification turn, WHEN its fallback timer expires, THEN both the preflight and serialized prompt-admission checks reject the stale answer without prompting or cancelling the successor.
  • GIVEN a primary-answer watchdog has entered fallback authority or prompt work, WHEN session activity or service shutdown cancels the watchdog, THEN that cancellation reaches the in-flight repository and prompt calls.
  • GIVEN a reserved successor is marked dispatch-attempted but remains unpublished, WHEN a client loads turn history before agentctl accepts or rejects it, THEN the successor is omitted until publication or durable message evidence prevents rollback from leaving stale client-only history.
  • GIVEN a predecessor ready event arrives while a detached-answer successor is reserved, WHEN the successor dispatch resolves, THEN the handler revalidates prompt generation and the stale predecessor cannot complete the successor or run on_turn_complete against it; if reservation rollback leaves that predecessor generation authoritative, its ready event completes normally.
  • GIVEN reserved-successor deletion returns an error, WHEN rollback handling completes, THEN the reservation waiter remains unresolved and another prompt cannot enter that session before restart recovery.
  • GIVEN an unpublished reservation stores an attempted marker as boolean true, string "true" or "1", or numeric 1, WHEN startup recovery runs, THEN it preserves the ambiguous successor and clears recovery metadata instead of restoring the answer and deleting the turn.
  • GIVEN a generationless ready event waits on a live reservation, WHEN the reservation rolls back, THEN Kandev drops the uncorrelatable event without changing session or workflow state.
  • GIVEN a cancelled request triggers clarification detach or expiry, WHEN repository work begins, THEN it uses a non-cancelled context with a finite deadline.
  • GIVEN a session transition to completed, failed, or cancelled is durably accepted, WHEN the transition publishes, THEN current-turn pending clarification bundles expire before a terminal session can leave an interactive overlay behind.
  • GIVEN a detached answer reaches synchronous orchestrator resume, WHEN the orchestrator does not acknowledge it before the bounded deadline, THEN Kandev treats acceptance as failed and restores the still-current bundle.
  • GIVEN startup cannot reconcile an unpublished prompt reservation, WHEN the orchestrator starts, THEN startup returns an error before watcher, scheduler, or prompt admission begins.
  • GIVEN restart recovery preserves an accepted or ambiguous prompt reservation, WHEN its public start event may have been missed, THEN startup durably retains and retries an ordered event replay until turn.started and any already-required turn.completed event are accepted before clearing the recovery marker.
  • GIVEN an unpublished prompt reservation contains a malformed claimed-message ID list, WHEN startup recovery decodes it, THEN recovery fails closed without deleting the reservation or reopening only a subset of its claimed rows.
  • GIVEN pending ownership changes while desktop or mobile task selection loads sessions, WHEN the delayed load settles, THEN Kandev ignores its stale owner choice but still opens the selected task through the task-only fallback, and the mobile sheet closes.
  • GIVEN a task-list refresh observes a detached answer's temporary terminal claim, WHEN resume rejection restores that claim, THEN Kandev acknowledges durable summary convergence before reporting retryability and publishes the restored pending rows so other clients converge.
  • GIVEN live publication or synchronous summary acknowledgement fails after restored clarification rows commit, WHEN the detached resume returns, THEN Kandev reports the convergence error while still identifying the durably restored answer as safe to retry.
  • GIVEN the selected task projection disappears while desktop or mobile session loading is in flight, WHEN the delayed load settles, THEN Kandev leaves task and session selection unchanged.
  • GIVEN a newer desktop or mobile forced session load aborts an older load for the same task, WHEN the older continuation handles AbortError, THEN it leaves the winning task and session selection unchanged.
  • GIVEN two phone task sheets are mounted, WHEN both have an in-flight task selection, THEN closing or selecting within one sheet does not invalidate the other sheet's selection generation.
  • GIVEN authoritative pending-action projection fails while listing a task's sessions, WHEN the HTTP or WebSocket list request completes, THEN it returns an internal error rather than a false clean-session response.
  • GIVEN two request identities produce terminal and pending events close together, WHEN the projector refreshes, THEN its result matches current repository state rather than event order.

Out of scope

  • Rewriting or deleting historical pending message rows solely to clean old data.
  • A dedicated clarification table or schema migration.
  • Redesigning the desktop sidebar, phone task drawer, chat cards, or clarification carousel.
  • Changing permission-request lifecycle semantics; pending-session navigation reuses the shared pending_action type for permission parity.
  • Changing notification history for a clarification that was valid when its notification fired.
  • Automatically choosing a non-primary session when the task has no current pending action.