fix: send late clarification answers as messages (#3798) (#3799)

This commit is contained in:
Carlos Florêncio
2026-09-19 21:35:23 +01:00
committed by GitHub
parent a27423f960
commit 02d260ff7d
43 changed files with 3311 additions and 71 deletions
@@ -0,0 +1,40 @@
# ADR-2026-09-18-late-clarification-messages: Answer earlier questions as new messages
**Status:** accepted
**Date:** 2026-09-18
**Area:** frontend, workflow
## Context
Current-turn ownership prevents stale tool responses from resuming obsolete work.
Users still need to answer questions after the original waiter or turn ends.
Removing the stale panel alone loses that conversation affordance.
## Decision
Preserve current-turn authority for the original tool response. Offer late
answers as ordinary user messages in the question's original conversation.
Use existing message admission, steering, queue ordering, and retry identity.
An affirmative submission receiving a recognized inactive response preserves
its answers and continues through that message path. Closing sends nothing.
The decision extends the presentation of the
[current-turn ownership rule](2026-08-14-current-turn-clarification-ownership.md).
It does not grant historical questions operational authority. Ordinary messages
remain subject to existing session availability and other live input barriers.
## Consequences
Users can answer earlier questions without resurrecting stale tool waits.
Message admission must retain drafts and stable identities across failures.
The UI must distinguish tool delivery, new-message delivery, and queue admission.
The Inbox History tab can retain its read-only boundary and link to conversation.
## Alternatives Considered
- Reopen historical tool requests: rejected because this restores obsolete
workflow barriers and can resume the wrong turn.
- Remove inactive controls without a late-answer path: rejected because users
lose the ability to respond to questions the agent asked.
- Add a separate late-answer transport: rejected because ordinary message
admission already owns sending, queueing, and duplicate prevention.
@@ -0,0 +1,307 @@
---
created: 2026-09-18
status: complete
requirements:
- REQ-TASKS-CLARIFICATION-LIFECYCLE-001
- REQ-TASKS-CLARIFICATION-RESPONSE-RELIABILITY-001
system_design:
- ../../specs/tasks/system-design/clarification-active-lifecycle.md
- ../../specs/tasks/system-design/clarification-response-reliability.md
legacy_specs: []
---
# Implementation Plan: Close or answer inactive clarification questions
## Overview
Repair the confirmed inactive-response path associated with
[issue #3798](https://github.com/kdlbs/kandev/issues/3798) and preserve a useful
conversation path after the original clarification waiter ends. Task 01
reconciles inactive responses, race ordering, and Escape behavior. Task 02
supersedes its removal-only presentation with late answers delivered as
ordinary messages through the source task and session.
## Evidence and root cause
Investigation base: `26254fe51f`. The report names `57bbae10ad` but provides no
network trace or confirmed reproduction. Both revisions wire the X-shaped
`ClarificationSkipButton` to rejection, separately from local collapse.
`useClarificationGroup` classifies `409 not_active` as `expired`. It updates
message status only for `ok`. `useResolveCallback` also handles only `ok`.
The overlay renders its expired banner beside enabled question actions.
Without an authoritative message update, the question remains pending locally.
Further X clicks repeat the request instead of removing the obsolete panel.
A temporary test copied the existing overlay harness and clicked X twice.
Both responses returned `409 {"code":"not_active"}`. After both settled, the
test confirmed two requests, `rejected=true`, and no message-store update.
Its final removal assertion failed because X remained mounted and enabled.
The temporary test was removed; permanent regressions are recorded in Task 01.
This proves one repairable path, not the cause of every reported occurrence.
Timeouts, failed requests, and malformed responses remain distinct outcomes.
## Revised contract and delivery scope
The user's latest direction takes precedence over the historical Task 01
approach below. Reuse `REQ-TASKS-CLARIFICATION-LIFECYCLE-001`, amended criteria
`.1`/`.2` and new criteria `.4` through `.9`. Requirements, design, and the
[late-answer ADR](../../decisions/2026-09-18-late-clarification-messages.md)
now separate operational tool responses from ordinary messages.
- Preserve earlier questions in transcript history with an answer-as-new-message action.
- Preserve submitted answers across `409 not_active` and send through normal admission.
- Keep X/Close as dismissal without a new message for inactive questions.
- Reuse existing sending/steering/queue rules; no new busy-agent policy is introduced.
- Keep Inbox History read-only and use its existing source-conversation navigation.
- Keep Task 01's race and Escape fixes wherever applicable.
- Task 02 owns UI, admission adapter, localization, and focused regression coverage.
UI-02: Earlier question, shared desktop and phone inline composition.
```text
[Earlier question ]
[Answer as new message ]
opens:
[Question and answer choices ]
[Custom answer ]
[Close] [Send as new message ]
[Failure: draft retained, Retry available ]
```
A recognized inactive result during answer submission uses the same delivery
path with the captured draft. Sent or queued admission replaces the form with
its actual outcome. Closing returns focus to its opener. Keep the existing
phone scroll owner and safe areas; controls have 44px coarse-pointer targets.
This view covers lifecycle criteria `.4` through `.9`. Exact spacing is illustrative.
The technical approach and results below retain Task 01's historical repair
details and record Task 02's revised implementation separately.
## Requirement conformance
- `AC-TASKS-CLARIFICATION-LIFECYCLE-001.2` requires obsolete questions to stop
being answerable. Reuse this active requirement without creating a repair spec.
- `AC-TASKS-CLARIFICATION-RESPONSE-RELIABILITY-001.1` distinguishes expiry from
acceptance and retryable failure.
- `AC-TASKS-CLARIFICATION-RESPONSE-RELIABILITY-001.4` preserves retry controls
after actual failures. Do not make every unsuccessful response disappear.
- The response-reliability design now specifies cache reconciliation. Existing
authority and persistence decisions remain unchanged.
## Scope
### In scope
- Reconcile submitted inactive bundles without overwriting newer authority,
terminal siblings, or restored pending rows.
- Keep expired static overlays free of response controls and preserve the
existing Escape, collapse, and generation fences.
- Offer historical unanswered questions an answer-as-new-message flow on
desktop and phone.
- Preserve affirmative answers after recognized inactive responses and deliver
them through ordinary message admission with stable retry identity.
- Localize the new actions and feedback, update public task guidance, and
validate the shared hook plus desktop/mobile browser flows.
### Out of scope
- Backend mutations, new endpoints, database migrations, and agent cancellation.
- Changing Escape/collapse semantics or treating expiry as successful rejection.
- Redesigning the Inbox, task summaries, or the clarification layout.
- Claiming that this repairs an unobserved timeout or backend failure.
## Technical approach
In `apps/web/hooks/domains/session/use-clarification-group.ts`, extend the
existing response reconciliation for `expired`. Read the current store rows
before changing only pending rows from the submitted bundle to local `expired`.
Match message ID, session ID, and pending ID. Preserve current metadata and
terminal siblings; never insert a missing row from the submitted snapshot.
Use `isPendingClarificationMessage` for the existing legacy pending semantics.
Compare `updated_at` independently for each row before applying expiry so a newer
authoritative restoration wins while unchanged pending siblings remain eligible.
If the request generation is stale while the same pending ID is current again,
skip the cache write; an old bundle remains eligible for retirement while a
different pending ID is active.
Keep response identity separate from current UI identity. An old request may
retire its own stale rows but cannot change a replacement bundle's controls,
outcome callback, or in-flight guard. Preserve the existing generation fence.
Do not store expiry in the backend or suppress later authoritative restoration.
In `clarification-input-overlay.tsx`, render only the existing expired notice
when static props retain an inactive bundle. Do not register active response
shortcuts in that state. Keep `no_longer_active` distinct from `resolved` and
leave `onResolved` success-only. Guard direct submission/retry calls after
expiry until bundle replacement or an authoritative restoration resets state.
Task chat, Quick Chat, and run transcripts derive pending questions from the
shared message cache. Their existing selectors remove the obsolete panel.
The Inbox retains its existing `no_longer_active` handling. No host-specific
successful-response callback substitutes for cache reconciliation.
For Task 02, the shared late-answer adapter captures the source task/session,
question bundle, and answers before invoking `useMessageHandler`. It reuses
ordinary input-mode and queue admission, keeps the captured client message ID
across remounts, and exposes sent, queued, and retryable failure outcomes.
Historical transcript rows open the same inline question form in explicit
new-message mode. The source conversation remains the routing authority.
## ASCII UI preview
UI-01: Task chat after X receives `409 not_active`, desktop and phone.
```text
Before After
[Question X v] [Conversation / question history]
[Question no longer active] [Message composer ]
[Answer choices ]
[Message composer ]
```
The stale panel disappears; history remains. A static host can show the expired
notice without response controls. The diagram uses illustrative copy.
Phone retains the existing focused chat and inline composer, with no new sheet.
`ClarificationPanelSection` and `mobile-clarification.spec.ts` are the nearest
shipped exemplars. Keep current scroll ownership, safe-area handling, and
44px coarse-pointer targets for remaining controls. Shared state owns expiry.
Map this view to lifecycle AC `.2` and response-reliability AC `.1`.
## Tests
- `use-clarification-group.test.ts`: inactive Skip updates only matching
pending rows; preserve terminal siblings and unrelated bundles in one fixture.
- `use-clarification-group.regressions.test.ts`: delayed A response after B
mounts, deleted rows, latest metadata preservation, authoritative restoration,
and direct resubmission guards.
- `clarification-input-overlay.test.tsx`: `X retires an inactive bundle without
another rejection request`; replace the existing expired-banner expectation
with a non-actionable notice and no success callback. Cover answer retention
and late-message success, queue, and retry feedback.
- `clarification-request-message.test.tsx` and the formatter tests cover the
historical action, current-turn exclusion, question context, selected
labels, custom text, and system-marker neutralization.
- `use-message-handler.test.ts` covers sent/queued outcomes, a current
clarification barrier, unavailable transport, and caller-owned retry IDs.
- Existing timeout, retry, malformed-response, success, and collapse tests
remain required. These cover response-reliability AC `.1` and `.4`.
## E2E tests
Add a scenario tagged `inactive dismissal` in `e2e/tests/chat/clarification.spec.ts`
and `e2e/tests/chat/mobile-clarification.spec.ts`. Use `chromium` and
`mobile-chrome`, respectively. Intercept the exact response request with
`409 not_active`, while retaining the stale pending message in the client.
Arm the HTTP causal wait before clicking/tapping X. Assert panel removal,
composer availability, and no second rejection request. A newer bundle must
remain answerable. Also retain existing real-backend Skip success coverage.
Add `late answer` scenarios to the same desktop and phone files. Assert that a
historical question can open the form, that the admitted message contains the
question and answer, and that coarse-pointer controls remain reachable. Keep
the active submission fallback covered by the submit-failure scenario.
## Work orders
- [x] [Task 01: Reconcile inactive clarification responses](task-01-reconcile-inactive-responses.md)
- [x] [Task 02: Send late answers as new messages](task-02-late-answer-messages.md)
Execute sequentially. Task 02 depends on Task 01 and revises its user-facing outcome.
## Verification results
The temporary reproduction failed at the expected control-removal assertion.
The existing hook, overlay, and panel suites passed: 3 files, 73 tests.
```bash
(cd apps/web && pnpm exec vitest run hooks/domains/session/use-clarification-group.test.ts components/task/chat/clarification-input-overlay.test.tsx components/task/chat/clarification-panel-section.test.tsx)
```
Documentation catalog validation passed: 291 decisions and 1015 specifications.
The full specification lint initially exposed a pre-existing duplicate
`AC-TASKS-QUEUED-SESSION-OWNERSHIP-003.8` in
`docs/specs/tasks/requirements/queued-session-ownership.md`; the unrelated
conversation-surface criterion was renumbered to `.003.10` so the catalog has
unique acceptance IDs. The full lint then passed. `git diff --check` passed.
The normal commit hooks passed without a bypass. The issue assignment was
verified as `carlosflorencio`.
## Implementation results
Task 01 is complete. The hook now reconciles only current pending rows from the
submitted inactive bundle, preserves newer metadata and terminal siblings, and
blocks repeated actions until replacement or authoritative restoration. A newer
authoritative `updated_at` wins over a delayed inactive response, and stale
A→B→A generations cannot expire the bundle that is current again. Static expired
overlays retain only the non-actionable notice and do not arm their Escape
guard. Desktop and phone E2E cover the same intercepted inactive response.
The browser-first regression failed before the correction at the expected
store-update and stale-control assertions. After the correction, validation
passed:
The review regressions also failed before the correction: the delayed restored
bundle became `expired`, the A→B→A path wrote an expired row, and the expired
overlay still claimed Escape. Each now passes.
The later PR review found three follow-up defects in retry ownership, mounted
late-message state, and Inbox source-session hydration. The remediation adds
post-await pending ID, generation, and bundle guards; a shared source-keyed
admission owner; and conditional session/message hydration before ordinary
admission. New deterministic coverage records delayed retry rejection and
success across bundle replacement, unmount/remount draft recovery, active to
transcript sharing, and an Inbox send with empty source caches.
The review was explicitly code-only. No local build, typecheck, unit, browser,
or E2E command was run for that remediation. The normal commit hooks and
`git diff --check` are recorded separately from the prior implementation
validation below.
- Task 02 focused Vitest: 8 files, 142 tests passed.
- Targeted ESLint: no errors or warnings on changed frontend and E2E files.
- Targeted Prettier, TypeScript typecheck, `make build-web`, `make build-backend`, and
`git diff --check` passed.
- Desktop historical late-answer E2E: 1 test passed.
- Phone historical late-answer E2E: 1 test passed.
- Active 409 late-answer fallback E2E: 1 test passed.
- E2E fixture plugin packaging passed.
- Documentation catalog validation passed: 291 decisions and 1015
specifications.
- `i18n:check` passed for all supported catalogs. The all-locale
`i18n:zh-hant` helper still stops on the two pre-existing simplified
`workflows.openAgentSettings` entries; the changed `task` namespace converted
successfully for both Traditional Chinese catalogs.
- Full specification lint passed after the pre-existing duplicate acceptance
ID was corrected to `AC-TASKS-QUEUED-SESSION-OWNERSHIP-003.10`.
- PR documentation coverage passed after nesting the late-answer criteria under
the lifecycle requirement heading.
- The review follow-up now compares message versions with `parseTurnTimestamp`,
preserving nanosecond ordering and rejecting malformed timestamps. Expiry is
selective per row, so an unchanged pending sibling still retires beside a
newer restored row. Companion parking work orders reference the corrected
`.003.10` criterion.
## Related delivery records
[Response reliability](../clarification-response-reliability/plan.md) and
[active lifecycle](../clarification-active-lifecycle/plan.md) remain historical
delivery records. This follow-up does not change their completed scopes or results.
## Risks
- A 409 is not proof that this caller rejected the question. Preserve outcome identity.
- Another caller's failed delivery can restore pending state. Authoritative
restoration must remain possible; do not add permanent local suppression.
- A delayed response must not overwrite newer metadata or terminal siblings.
- The report lacks runtime evidence. Investigate separately if the failure
persists with a successful response after this focused repair.
## Documentation impact
Task 01 did not need public documentation changes. Task 02 must document the
late-answer action and its normal send/queue behavior in the existing task
conversation guide. This is recorded in `docs/public/tasks-and-workflows.md`;
queued admission is described as queueing, not agent receipt.
@@ -0,0 +1,194 @@
---
id: "01-reconcile-inactive-responses"
title: "Reconcile inactive clarification responses"
status: done
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-TASKS-CLARIFICATION-LIFECYCLE-001
- REQ-TASKS-CLARIFICATION-RESPONSE-RELIABILITY-001
acceptance_criteria:
- AC-TASKS-CLARIFICATION-LIFECYCLE-001.2
- AC-TASKS-CLARIFICATION-RESPONSE-RELIABILITY-001.1
- AC-TASKS-CLARIFICATION-RESPONSE-RELIABILITY-001.4
system_design:
- ../../specs/tasks/system-design/clarification-active-lifecycle.md
- ../../specs/tasks/system-design/clarification-response-reliability.md
---
# Task 01: Reconcile inactive clarification responses
## Follow-up scope
This work order records the completed removal-only implementation at PR #3799
head `6edc7320dd`. The user's later direction supersedes that presentation.
[Task 02](task-02-late-answer-messages.md) adds answers as ordinary messages.
The results below are historical evidence, not validation of the new scope.
## Summary
Close stale task-chat questions when X receives an authoritative inactive response.
Keep expiry distinct from successful rejection and retryable failure.
## In scope
- Shared hook reconciliation against the latest store snapshot.
- Non-actionable expired rendering and response-shortcut cleanup.
- Focused hook/component regressions and desktop/phone browser evidence.
- Accurate plan and work-order results after implementation.
## Out of scope
Backend changes, permanent dismissal storage, new UI copy, layout redesign,
and changing Escape/collapse into rejection.
## Acceptance
1. A recognized inactive response retires only matching pending rows in the
submitted bundle. Task chat removes the panel without a WebSocket event.
Preserve terminal siblings, unrelated bundles, deleted rows, and newer metadata.
2. A static expired overlay exposes no answer, Skip, Submit, or Retry action.
Expiry reports `no_longer_active`, never successful rejection. Unknown or
malformed responses and transport failures preserve retryable behavior.
3. A delayed A response cannot alter B's submission state or callbacks. A later
authoritative pending restoration remains answerable, including the same ID.
Desktop and phone pass the same inactive-dismissal flow.
## Implementation sequence
1. Read the linked requirements and design's inactive-response section.
2. Add the regression before changing production code. Mock `409 not_active`,
click X, and assert that no actionable stale question remains.
3. Reconcile submitted rows using their latest message-cache values. Reuse
`isPendingClarificationMessage`, including its missing-status compatibility.
Compare each latest `updated_at` with its submitted snapshot and preserve a
newer authoritative row while retiring unchanged pending siblings. Fence
cache expiry when the request generation is stale and the same pending ID is
current again; still retire an old bundle while another pending ID is active.
4. Keep current request-generation checks for UI state and callbacks. Test a
mixed bundle containing pending and terminal rows, plus an unrelated live bundle.
5. Render the expired notice without response actions for static hosts. Prevent
repeated hook submissions until replacement or authoritative restoration.
An ordinary rerender of stale props must not reset expiry. Do not arm the
Escape guard when the expired notice has no keyboard handler.
6. Add desktop and phone coverage, run the commands below, and record results.
## ASCII UI preview
UI-01: Task chat after inactive X response, shared desktop/phone structure.
```text
Before: [Question / expired notice / X / answers]
After: [Conversation / question history]
[Message composer ]
```
See the [full preview](plan.md#ascii-ui-preview) for composition and constraints.
The stale panel disappears; static hosts retain only the non-actionable notice.
Keep current phone scroll ownership, safe areas, and remaining touch targets.
This view covers lifecycle AC `.2` and response-reliability AC `.1`.
## Verification
Run from the repository root. Install dependencies first in a fresh worktree.
Run the new regression alone before the correction and record its expected failure.
Then run the complete checks below; E2E commands run sequentially.
```bash
(cd apps && pnpm install --frozen-lockfile)
(cd apps/web && pnpm exec vitest run hooks/domains/session/use-clarification-group.test.ts hooks/domains/session/use-clarification-group.regressions.test.ts hooks/domains/session/use-clarification-group.timeout.test.ts components/task/chat/clarification-input-overlay.test.tsx components/task/chat/clarification-panel-section.test.tsx)
(cd apps/web && pnpm exec eslint hooks/domains/session/use-clarification-group.ts components/task/chat/clarification-input-overlay.tsx)
(cd apps/web && pnpm run typecheck)
make build-web
make build-backend
(cd apps/web && pnpm e2e:run --no-build --project chromium -- tests/chat/clarification.spec.ts --grep 'inactive dismissal')
(cd apps/web && pnpm e2e:run --no-build --project mobile-chrome -- tests/chat/mobile-clarification.spec.ts --grep 'inactive dismissal')
python3 scripts/list-docs.py validate
python3 scripts/lint-spec-files.py --all
git diff --check
```
Use the current E2E fixture and causal-wait helpers. Intercept only the target
response and retain stale client data so a message event cannot hide the bug.
Inspect the phone rendering against UI-01. Do not claim screenshots or E2E
results from the design turn; those checks have not run.
## Files likely touched
- `apps/web/hooks/domains/session/use-clarification-group.ts`
- `apps/web/hooks/domains/session/use-clarification-group.test.ts`
- `apps/web/hooks/domains/session/use-clarification-group.regressions.test.ts`
- `apps/web/hooks/domains/session/use-clarification-group.timeout.test.ts`
- `apps/web/components/task/chat/clarification-input-overlay.tsx`
- `apps/web/components/task/chat/clarification-input-overlay.test.tsx`
- `apps/web/components/task/chat/clarification-panel-section.test.tsx`
- `apps/web/e2e/tests/chat/clarification.spec.ts`
- `apps/web/e2e/tests/chat/mobile-clarification.spec.ts`
Read-only integration points include `lib/utils/pending-clarification.ts`,
`hooks/use-processed-messages.ts`, `components/task/task-chat-panel.tsx`,
`components/quick-chat/quick-chat-content.tsx`, and `components/runs/run-transcript.tsx`
under `apps/web/`. Inspect the Inbox's existing outcome consumer too.
## Dependencies
None. Execute only after the user's explicit implementation request.
## Risks
Local expiry must not become a backend write or permanent suppression. Read
current rows before cache updates; a submitted snapshot can contain stale metadata.
The report's exact runtime cause remains unconfirmed beyond this reproduced path.
## Parallelism
`sequential`
## Inputs
- [Evidence and root cause](plan.md#evidence-and-root-cause).
- [Lifecycle requirements](../../specs/tasks/requirements/clarification-active-lifecycle.md).
- [Response requirements](../../specs/tasks/requirements/clarification-response-reliability.md).
- [Client recovery design](../../specs/tasks/system-design/clarification-response-reliability.md#inactive-response-reconciliation).
- Existing expired-banner test and successful Skip/store-update tests.
## Results
Implemented the inactive-response repair. A recognized `409 not_active` now
reconciles only matching pending rows from the latest message cache to local
`expired` status, preserving newer metadata and terminal siblings while
skipping deleted rows and unrelated bundles. A newer authoritative `updated_at`
wins over a delayed inactive response, and stale A→B→A generations cannot
expire the bundle that is current again. The overlay exposes only the existing
expired notice, does not arm its Escape guard, and direct submit, Skip, and
Retry calls are guarded until a replacement or authoritative pending
restoration arrives.
Added shared hook and overlay regressions, delayed-response coverage, and
desktop/mobile E2E scenarios. The browser-first regression failed before the
production change at the expected cache-update and stale-control assertions.
The review regressions then failed before the ordering and Escape-guard
correction, and pass after it.
Validation passed:
- Focused Vitest: 5 files, 96 tests.
- Strict timestamp comparison preserves RFC3339Nano ordering and rejects
malformed or Date-normalized values.
- Selective expiry preserves a newer restored row while retiring an unchanged
pending sibling in the same bundle.
- Targeted ESLint: no errors or warnings.
- Prettier and TypeScript typecheck.
- `make build-web` and `make build-backend`.
- Desktop inactive-dismissal E2E: 1 passed.
- Phone inactive-dismissal E2E: 1 passed.
- E2E fixture plugin packaging.
- Documentation catalog validation, full specification lint, and
`git diff --check`.
The full specification lint first exposed a pre-existing duplicate acceptance
ID in the queued-session ownership requirements. The unrelated
conversation-surface criterion was renumbered to
`AC-TASKS-QUEUED-SESSION-OWNERSHIP-003.10`; the final catalog lint passed.
Companion parking work orders now reference that corrected criterion.
@@ -0,0 +1,221 @@
---
id: "02-late-answer-messages"
title: "Send late answers as new messages"
status: done
wave: 2
depends_on:
- 01-reconcile-inactive-responses
plan: "plan.md"
requirements:
- REQ-TASKS-CLARIFICATION-LIFECYCLE-001
acceptance_criteria:
- AC-TASKS-CLARIFICATION-LIFECYCLE-001.4
- AC-TASKS-CLARIFICATION-LIFECYCLE-001.5
- AC-TASKS-CLARIFICATION-LIFECYCLE-001.6
- AC-TASKS-CLARIFICATION-LIFECYCLE-001.7
- AC-TASKS-CLARIFICATION-LIFECYCLE-001.8
- AC-TASKS-CLARIFICATION-LIFECYCLE-001.9
system_design:
- ../../specs/tasks/system-design/clarification-active-lifecycle.md
- ../../specs/tasks/system-design/clarification-response-reliability.md
---
# Task 02: Send late answers as new messages
## Summary
Let users answer earlier human clarification questions through ordinary messages.
Preserve answers when tool submission receives a recognized inactive result.
This revises PR #3799's removal-only behavior without reopening tool authority.
## In scope
- Transcript entry point and inline late-answer form on desktop and phone.
- A shared question/answer formatter and adapter to ordinary message admission.
- Recognized inactive-answer fallback, draft retention, stable retry identity,
correct session targeting, and explicit sent/queued feedback.
- Existing live-waiter and detached-current-turn paths remain operational.
- Localization in all supported catalogs and focused public documentation.
## Out of scope
Backend tool-response authority changes, replacement sessions, forced interruption,
Inbox History mutations, permission approvals, and parent-agent question routing.
Do not make historical questions count as pending workflow input.
## Acceptance
1. A user can answer an earlier unanswered question from its conversation.
Question prompts, selected labels, and custom text reach ordinary admission
for the source task/session. Idle, busy, and unavailable cases match chat.
2. Recognized inactive results preserve affirmative answers for the same path.
Close sends nothing. Unknown conflicts and uncertain tool transport results
retain the existing response retry path instead of sending a duplicate message.
3. Failures retain drafts; retries reuse admission identity. Navigation and
newer questions cannot redirect delivery or lose a pending admission.
Desktop and phone distinguish queued admission from sent messages.
## Technical approach
Extend `useClarificationGroup` with a typed late-message callback/outcome.
Capture the submitted questions and answers before awaiting the response.
Do not emit a destructive host outcome before fallback admission settles.
Keep an admission owner mounted independently of the expiring overlay, so
its draft and stable request ID survive cache updates and close/reopen cycles.
Reuse `useMessageHandler`, `deriveSessionInputMode`, and `useQueue` through
one shared adapter. Do not issue raw prompt or queue requests from components.
Read the source session's latest state; an obsolete question must not force
`hasPendingClarification=true`. A separate current question keeps its barrier.
Retain existing request-generation and per-row version fences.
Add a shared formatter with focused tests. Render source questions and answers
as ordinary user text, not a privileged system marker. Never derive the target
from global active selection after asynchronous work.
`ClarificationRequestMessage` exposes the history action for human questions
that are no longer operational. Reuse the existing question form with explicit
new-message mode and Close. The message renderer supplies needed conversation
context. Read-only surfaces navigate to that conversation; the Inbox History
read path remains free of mutations. Existing authenticated message admission
owns access and availability checks.
## ASCII UI preview
UI-02: Shared desktop and phone inline flow, from transcript history.
```text
[Earlier question ]
[Answer as new message ]
opens:
[Question and answer choices ]
[Custom answer ]
[Close] [Send as new message ]
[Failure: draft retained, Retry available ]
```
See [the combined preview](plan.md#revised-contract-and-delivery-scope).
Reuse inline clarification controls and existing scroll ownership. Keep phone
safe areas and 44px coarse-pointer targets. Return focus to opener on Close.
Localize action and sent/queued feedback. Map to criteria `.4` through `.9`.
## Tests and verification
Deterministic coverage covers both entry points: inactive response after an
answer submission, and direct historical-question answering. The shared
message-handler tests cover source-session barriers, busy queue admission,
unavailable transport, and stable caller-owned retry IDs. Overlay and group
tests cover answer retention, retry feedback, current-question exclusion,
timeout/error separation, and zero-message Close behavior. Existing
live-answer and Skip tests stay green.
Add `late answer` cases to desktop/mobile clarification browser files. Assert
actual admitted message or queue content with the original question and answers,
not only disappearance of the overlay. A failed admission preserves the form.
Run these commands from the repository root during implementation, not design:
```bash
(cd apps && pnpm install --frozen-lockfile)
(cd apps/web && pnpm exec vitest run hooks/domains/session/use-clarification-group.test.ts hooks/domains/session/use-clarification-group.regressions.test.ts hooks/domains/session/use-clarification-group.timeout.test.ts hooks/use-message-handler.test.ts hooks/domains/session/session-input-mode.test.ts components/task/chat/clarification-input-overlay.test.tsx components/task/chat/messages/clarification-request-message.test.tsx)
(cd apps/web && pnpm run typecheck)
(cd apps/web && pnpm run i18n:zh-hant && pnpm run i18n:check)
make build-web
make build-backend
(cd apps/web && pnpm e2e:run --no-build --project chromium -- tests/chat/clarification.spec.ts --grep 'late answer')
(cd apps/web && pnpm e2e:run --no-build --project mobile-chrome -- tests/chat/mobile-clarification.spec.ts --grep 'late answer')
python3 scripts/list-docs.py validate
python3 scripts/lint-spec-files.py --all
git diff --check
```
Include any newly extracted helper test files in the exact Vitest command
before implementation completion. Record real results and rendered UI checks.
## Files likely touched
- `apps/web/hooks/domains/session/use-clarification-group.ts` and its tests.
- `apps/web/hooks/use-message-handler.ts` and its existing tests if adaptation is needed.
- `apps/web/components/task/chat/clarification-input-overlay.tsx` and its tests.
- `apps/web/components/task/chat/messages/clarification-request-message.tsx` and its tests.
- `apps/web/components/task/chat/message-renderer.tsx`.
- Task chat, Quick Chat, and run-transcript host callback wiring as required.
- `apps/web/e2e/tests/chat/clarification.spec.ts`.
- `apps/web/e2e/tests/chat/mobile-clarification.spec.ts`.
- Relevant `apps/web/src/locales/*/task.json` and `chat.json` catalogs.
- `docs/public/tasks-and-workflows.md`.
## Dependencies
Task 01, PR #3799 head `6edc7320dd`. Re-read its current head before implementation.
## Risks
A tool failure can be ambiguous. Fall back only on recognized inactive results.
Cache retirement must not unmount the owner of draft/admission state.
Historical content must never become trusted system instructions.
Current-turn authority and Inbox History isolation remain unchanged.
## Parallelism
`sequential`
## Inputs
- Updated lifecycle criteria `.4` through `.9`.
- Response-reliability design's inactive-response and late-answer section.
- ADR `2026-09-18-late-clarification-messages`.
- Task 01's completed race and Escape regressions.
## Results
Task 02 is complete. The shared late-answer adapter captures the source task and
session, formats question context with selected labels and custom text as
ordinary user content, and delivers it through the existing message admission
and queue rules. A recognized inactive affirmative response preserves the
answers and uses that same adapter. Close only dismisses the form. Unknown or
ambiguous failures remain on the original retry path.
The transcript action is available for historical unanswered questions, while a
current pending turn does not expose a duplicate answer action. Desktop and
phone use the same inline form with localized sent, queued, and retry feedback.
The source question bundle and client admission ID remain stable across
navigation and remounts. Inbox History remains read-only and continues to link
to the source conversation.
Validation passed:
- Focused Vitest: 8 files, 142 tests.
- TypeScript typecheck, `make build-web`, and `make build-backend`.
- `i18n:check`; changed `task` namespaces converted for `zh-hk` and `zh-tw`.
The all-locale converter still reports the two unrelated historical
`workflows.openAgentSettings` residuals.
- E2E: desktop historical late answer, mobile historical late answer, and
active 409 fallback, 1 test each.
- Documentation catalog validation, full specification lint, targeted Prettier, and
`git diff --check`.
- PR documentation coverage passed after nesting the late-answer criteria under
the lifecycle requirement heading.
- Targeted ESLint reported no errors or warnings on changed frontend and E2E
files.
## Review remediation
The code-review follow-up hardens late admission across the same source task,
session, and clarification bundle. Retry completion now captures and checks the
pending ID, request generation, and submitted bundle before every post-await
state update, cache reconciliation, and outcome callback. A delayed retry from
an older bundle cannot replace the state of a newer bundle or a returned
generation.
Late-message status, draft answers, and client admission identity now live in a
module-owned external record shared by active, transcript, and Inbox hosts.
When the source session or message cache is absent, the adapter hydrates them
through the existing session APIs before ordinary send, steer, or queue
admission derives its input mode. The regression coverage includes delayed
failure across unmount/remount, active-to-transcript state sharing, missing
Inbox caches, and A→B and A→B→A delayed retry races.
This review was code-only by instruction. No local builds, typechecks, unit
tests, browser tests, or E2E tests were run for the remediation. The normal
commit hooks and `git diff --check` remain the applicable local checks.
@@ -41,7 +41,7 @@ resume on open even while a session is parked. The screenshot is presentation
evidence; this plan does not claim a new live-state investigation of that task.
The old inspection requirement 001.1 and parked-note requirement 003.3 are
explicitly superseded by 001.9 and 003.8. Criteria 001.7/8 retain restart and
explicitly superseded by 001.9 and 003.10. Criteria 001.7/8 retain restart and
settled-queue coverage. Queue identity, automatic admission, callback correlation,
and the deadlock fixes remain compatibility constraints.
@@ -123,7 +123,7 @@ The task drawer remains the entry point. The session picker selects the
conversation and normal recovery follows. The dedicated mobile layout, safe-area
navigation, and single chat scroll owner remain. No hover interaction is required.
UI-01 and UI-02 cover AC 001.9 and 003.8. Placement and absence of parking chrome
UI-01 and UI-02 cover AC 001.9 and 003.10. Placement and absence of parking chrome
are requirements; text spacing is illustrative. Existing recovery errors, queue
waiting, loading, and explicit controls keep their established presentation.
The nearest mobile exemplar is `mobile-queued-session-ownership.spec.ts`.
@@ -144,7 +144,7 @@ AC suffixes use `AC-TASKS-QUEUED-SESSION-OWNERSHIP-`.
| Archive, explicit cancellation, completion, authorization failure | Existing lifecycle restrictions remain | 001.9 |
| Allowed status followed by conflicting queue or route change | Guarded admission rejects stale execution | 001.2, 001.6; 002.6 |
| Restart after switch or queue settlement | Conversation context survives; opening requests normal recovery | 001.7, 001.8, 001.9 |
| Desktop/phone with stored parking marker | No parked note; ordinary controls and real queue status remain | 003.8 |
| Desktop/phone with stored parking marker | No parked note; ordinary controls and real queue status remain | 003.10 |
## E2E tests
@@ -63,7 +63,7 @@ Both: No parked-session row; genuine task queue status remains when needed.
Keep the existing desktop tabs and phone task drawer/session picker.
The chat remains the single conversation scroll owner. No new touch targets or
hover-only behavior are introduced. Cover AC 001.9 and 003.8.
hover-only behavior are introduced. Cover AC 001.9 and 003.10.
## Verification
+14
View File
@@ -461,6 +461,20 @@ The preference only gates opening a task. Choosing **Start agent** (or a
workflow step transition) always starts the agent as usual, and a failed or
interrupted session still shows its recovery actions.
## Answer clarification questions
When an agent asks a clarification question, answer it from the question panel
while the session is waiting. If the question becomes inactive before you
answer, its entry remains in the conversation with **Answer as new message**.
That action includes the question and your answer in a normal conversation
message. It does not reopen the old tool request.
Normal message rules still apply. An idle session receives the message; a busy
session sends or queues it according to its current input mode. If delivery
fails, the form keeps your answer so you can retry. **Close question** removes
the answer surface without sending anything, while the question stays in the
conversation history.
## Task dependencies
A task can declare that it **depends on** one or more other tasks. This is a
@@ -15,7 +15,7 @@ owners:
Clarification bundles remain answerable only for the session turn that owns them, while detached and recovery paths preserve the response contract.
Tool responses belong to the owning session turn. Users can send answers to earlier questions as new messages without reopening the original tool request.
@@ -32,6 +32,9 @@ hiding the action the icon represents.
## What
The active/answerable rules in this section describe the original tool request.
They do not prohibit the separate late-answer message action specified below.
- 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
@@ -130,8 +133,7 @@ hiding the action the icon represents.
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.
initiates no agent resume. Clients distinguish rejection from an affirmative answer. An affirmative answer can continue as a new user message under the late-answer criteria below.
- 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
@@ -158,7 +160,7 @@ hiding the action the icon represents.
**Intent:** Clarification bundles remain answerable only for the session turn that owns them, while detached and recovery paths preserve the response contract.
**Intent:** Tool responses belong to the owning session turn. Users can send answers to earlier questions as new messages without reopening the original tool request.
@@ -166,12 +168,39 @@ hiding the action the icon represents.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.1:** When a clarification is pending, the system shall expose and process only the bundle owned by the session's current turn.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.2:** When a newer turn or terminal session state supersedes a bundle, the system shall keep its transcript history without leaving it answerable or able to block workflow progress.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.1:** When a clarification is pending, the system shall process tool responses only for the bundle owned by the session's current turn. Earlier questions can accept new-message answers under the late-answer criteria below.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.2:** When a newer turn or terminal session state supersedes a bundle, the system shall keep its transcript history without leaving the original tool request answerable or able to block workflow progress. Users can answer through a separate new-message action.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.3:** When detached clarification recovery succeeds or fails, the system shall return the bounded response and durable recovery outcome defined by the lifecycle contract.
#### Late answers as new messages
These criteria extend the existing lifecycle requirement. They separate an
operational tool response from a user choosing to continue the conversation.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.4:** When an unanswered question is
superseded or expired, its transcript entry shall offer an answer-as-new-message
action on desktop and phone. It shall not claim that the agent is still waiting.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.5:** When an affirmative answer receives
a recognized inactive result, the interface shall preserve the submitted answers
and send them with their question context through ordinary message delivery.
It shall not silently discard those answers or report successful tool delivery.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.6:** A late answer shall target the
question's original task and session. It shall follow ordinary sending,
steering, queue ordering, and unavailable-session behavior. An idle input-capable
session shall receive it without requiring the original tool waiter.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.7:** Closing an inactive question shall
dismiss its answer surface without sending a message or resuming the agent.
The question shall remain in history. Active rejection keeps its existing semantics.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.8:** A failed late-message admission shall
preserve the answer and offer retry. An ambiguous transport result shall not
automatically start another delivery path. Retrying one admission shall reuse
its identity rather than create duplicate messages.
- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.9:** Sending a late answer shall not reopen
the original tool request, restore its workflow barrier, or resolve another
pending question. The interface shall distinguish sent from queued admission.
## Out of scope
@@ -97,6 +97,13 @@ No new route. Task-session responses add `pending_action_revision` beside the ex
still owned by the in-memory clarification store. The chat Skip control uses `/respond` with
`rejected=true`, including for detached requests.
## Late conversation replies
The tool-response contract above does not prohibit an ordinary user message
about an earlier question. See [late-answer delivery](clarification-response-reliability.md#inactive-response-reconciliation-and-late-answers)
for transcript actions and inactive-response fallback. That path does not claim
the historical bundle or revive its operational authority.
## State machine
One clarification bundle has five operational states:
@@ -3,6 +3,7 @@ status: current
system: tasks
requirements:
- REQ-TASKS-CLARIFICATION-RESPONSE-RELIABILITY-001
- REQ-TASKS-CLARIFICATION-LIFECYCLE-001
---
# Clarification response reliability System Design
@@ -133,6 +134,68 @@ winner when the first request committed, or claims the still-pending bundle
when the first request did not. A 409 `not_active` remains an expired outcome,
not a successful submission.
### Inactive-response reconciliation and late answers
This section also maps `REQ-TASKS-CLARIFICATION-LIFECYCLE-001`, criteria
`.4` through `.9`. The [late-answer decision](../../../decisions/2026-09-18-late-clarification-messages.md)
separates operational tool authority from ordinary conversation.
`useClarificationGroup` captures question prompts, option labels, custom text,
answers, task ID, and session ID before submitting. An affirmative response
classified as inactive transfers that snapshot to a shared late-message action.
It does not clear the draft before message admission succeeds. Rejection instead
retires the obsolete surface without sending an ordinary message.
Use the existing `useMessageHandler` admission path for late messages. Format
question prompts and selected labels with custom answers as ordinary user text.
Do not interpolate them into a privileged system block. A directly opened
historical question uses this path without retrying the obsolete response endpoint.
A recognized inactive result after an affirmative submit uses the same path
without requiring the user to enter the answers again.
A typed host callback connects the shared question UI to message admission.
Resolve its task/session from the source question, not global active selection.
Retain the captured identity across navigation. Task chat and Quick Chat wire
this callback through their existing chat handler. A transcript question opens
an inline late-answer form using the same formatter and admission adapter.
Read-only run surfaces link to the source conversation rather than acquire a
second message transport. The Inbox History tab remains read-only; its existing
conversation navigation provides access to the transcript action.
Refresh message/session authority after an inactive result before deriving
promptability. The stale question itself must not force `hasPendingClarification`
to true and queue an otherwise idle session forever. A different active question
retains its barrier. Reuse `deriveSessionInputMode` and queue admission for busy
sessions; do not introduce forced interruption or a separate queue policy.
Unavailable sessions preserve the answer and use ordinary recovery feedback.
No automatic replacement task or session is created.
Keep the existing stable admission identity across retries, including when the
message surface closes and reopens while admission is unresolved. A rejected
admission preserves the draft; an uncertain admission reconciles by that same
identity. Do not send ordinary messages after timeouts, malformed responses,
unknown conflicts, or generic server failures in the tool-response path.
Those outcomes retain its existing retry flow.
Separate tool outcomes from message outcomes. `no_longer_active` is not a
successful tool response; sent/queued late answers need their own outcome.
Consumers must not unmount an answer draft merely on `no_longer_active` while
new-message admission is pending or has failed. Success-only `onResolved`
remains specific to the original response path.
Cache retirement preserves current row metadata, newer authoritative versions,
terminal siblings, deleted rows, and request-generation ownership. Keep the
per-row RFC3339Nano version checks from the inactive-dismissal repair. Retiring
operational state must not remove the transcript's late-answer entry point.
An expired surface has working Close and new-message actions. Its Escape guard
must match an actually mounted handler. New-message admission never marks the
historical tool request answered or reactivates its pending-action projection.
Desktop and phone use the existing inline question form. Keep the current scroll
owner and safe areas, provide 44px coarse-pointer actions, and return focus to
the transcript opener or composer after closing. Translate new labels in all
supported catalogs. The action explicitly says it sends a new message.
Local collapse, dismiss, Escape, and task navigation do not mutate the bundle.
Skip remains the explicit rejection path. These controls may be disabled only
while the bounded request is in flight and become available again on a