463 lines
37 KiB
Markdown
463 lines
37 KiB
Markdown
---
|
|
status: approved
|
|
created: 2026-08-14
|
|
owner: 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.
|