Follow-up from PR #54 (Ask in the lane D container). Copilot's final review summary on validated head 6f047ac says "unresolved moderate issues remain in cancellation handling" but gives no finding or location. This issue records the most likely concrete cause, checked in the code. It is not a reproduced failure.
What the code does
The reason a question stops (timeout, shutdown, or user cancellation) crosses three layers only as message text:
runner/questions.ts aborts with new Error('Agent timed out. Try again.') or new Error('Server stopped. Retry the question.').
runner/question-agent.ts (#ask) posts { type: 'cancel', reason: signal.reason.message } to the worker.
runner/question-worker.ts aborts with new Error(message.reason), and runner/question-container.ts (around line 208) rebuilds lane D's StopReason by regex: /timed out/ → timeout, /Server stopped/ → shutdown, otherwise cancelled.
AGENTS.md ("Async jobs and polling") requires the original timeout, cancellation and shutdown reason to be preserved through every layer. Matching on wording means changing a user-facing message silently changes the lane D stop reason, and the result is then reported as cancelled.
To close this issue
- Carry the reason as a typed value (for example,
StopReason or { kind: 'timeout' | 'shutdown' | 'cancelled', message }) from Questions through the bridge message and the worker to handle.cancel(). Keep the user-facing message separate.
- Add a regression per reason. It should assert the
StopReason that reaches the adapter and the persisted answer error. It must fail if a message's wording changes.
- If investigation finds a different concrete cancellation defect behind the review summary, record it here instead.
Follow-up from PR #54 (Ask in the lane D container). Copilot's final review summary on validated head
6f047acsays "unresolved moderate issues remain in cancellation handling" but gives no finding or location. This issue records the most likely concrete cause, checked in the code. It is not a reproduced failure.What the code does
The reason a question stops (timeout, shutdown, or user cancellation) crosses three layers only as message text:
runner/questions.tsaborts withnew Error('Agent timed out. Try again.')ornew Error('Server stopped. Retry the question.').runner/question-agent.ts(#ask) posts{ type: 'cancel', reason: signal.reason.message }to the worker.runner/question-worker.tsaborts withnew Error(message.reason), andrunner/question-container.ts(around line 208) rebuilds lane D'sStopReasonby regex:/timed out/→timeout,/Server stopped/→shutdown, otherwisecancelled.AGENTS.md ("Async jobs and polling") requires the original timeout, cancellation and shutdown reason to be preserved through every layer. Matching on wording means changing a user-facing message silently changes the lane D stop reason, and the result is then reported as
cancelled.To close this issue
StopReasonor{ kind: 'timeout' | 'shutdown' | 'cancelled', message }) fromQuestionsthrough the bridge message and the worker tohandle.cancel(). Keep the user-facing message separate.StopReasonthat reaches the adapter and the persisted answer error. It must fail if a message's wording changes.