feat(ask-user): timed waits with late-reply handling - #270
Conversation
…ly follow-ups - ask_user_question accepts optional timeoutSeconds (30-3600). The main-owned coordinator stamps expiresAt and settles on the deadline with a main-only timedOut response. The agent is told to proceed on best judgement or its stated default. - Unattended (Remote-owned) runs always expire within the existing 5-minute Remote question lifetime. Remote streams now project the real agent deadline as expiresAt. There is no wire change or revision bump. - chat:answerQuestionnaire reports answered/expired. The desktop marks expired cards and offers to send or queue a late answer as a follow-up message. - iOS and Android explain an expired question instead of dropping it silently. - Docs: plan, plans index, Remote API expiresAt semantics, memory note, papercuts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
The desktop removes expired prompts when generation finishes, leaving no way to submit a late answer; the shared timeout option is also ignored by the CLI.
Reviewed changes This review covers timed ask-user deadlines across the coordinator, shared tool and IPC contract, desktop late-answer UI, Remote streams, mobile expiry messaging, tests, and documentation.
- Deadline policy The coordinator settles timed-out prompts, distinguishes expired replies, and applies a five-minute cap to Remote-owned waits.
- Late-answer recovery Desktop offers a follow-up action for answers received after expiry while the prompt remains available.
- Remote and mobile behavior Remote publishes the effective expiry and iOS/Android explain when a submitted answer arrives too late.
- Validation and documentation Focused tests and the plan/API documentation cover the new timeout and expiry behavior.
GPT Luna | 𝕏
Hermes Review BotConfidence: 5 Engine: SummaryTimed waits allow Confidence Score: 5/5Traced all timeout paths, timer races, IPC outcomes, and cancellation lifecycles across main, renderer, remote streams, mobile error handling, and the CLI dialog loop, with full test verification. 📁 Important Files Changed
FindingsNo findings. 📊 Sequence DiagramsequenceDiagram
autonumber
participant Agent as Agent (Pi Tool / LLM)
participant Coord as Coordinator (AskUserQuestionCoordinator)
participant Renderer as Desktop Renderer (ChatPane)
Agent->>Coord: request({ timeoutMs: 30000, questions })
Coord->>Renderer: chat:questionnaire (with expiresAt)
Note over Coord, Renderer: 30s deadline passes
Coord-->>Agent: response ({ cancelled: true, timedOut: true })
Note over Agent: Agent resumes with best judgement
Renderer->>Coord: chat:answerQuestionnaire (late user reply)
Coord-->>Renderer: { status: "expired" }
Note over Renderer: Prompts user to Send or Queue as follow-up
Machine-Readable Findings[]
|
|
Pullfrog stalled The agent stopped emitting events for 125s and was killed by the activity-timeout watchdog. The model produced no output at all before the stall — the request was sent but nothing came back. This is usually transient; re-running often succeeds. Recent agent stderr
|
# Conflicts: # .papercuts/troubleshooting.md
# Conflicts: # .papercuts/troubleshooting.md
There was a problem hiding this comment.
ℹ️ No critical issues — minor timing suggestion inline.
Reviewed changes This incremental review covers the two feature commits after the prior Pullfrog review, including CLI deadline propagation and late desktop answer recovery.
- Retained expired desktop prompts. Generation completion or error now preserves prompts whose deadlines have passed, allowing a late answer to become a follow-up.
- Applied CLI deadlines. Terminal selections now receive the remaining time and abort signal under one questionnaire-wide deadline.
- Preserved completed multi-selections. Exhausting all choices now records the accumulated selections before continuing.
GPT Luna | 𝕏
| if (remaining !== undefined && remaining <= 0) return { kind: "timedOut" }; | ||
| const choice = await ui.select(title, options, { | ||
| ...(signal ? { signal } : {}), | ||
| ...(remaining !== undefined ? { timeout: remaining } : {}), |
There was a problem hiding this comment.
The pinned Pi TUI selector rounds timeout up to whole seconds and expires on a one-second interval, so it can remain interactive for almost another second after this absolute deadline. A selection accepted in that gap is then discarded by the post-await check, losing an answer while the selector still appears active.
Technical details
# Dismiss CLI selection at its absolute deadline
## Affected sites
- `packages/cli/src/extensions/ask-user-question-core.ts:49-55` — the helper awaits the selector and rejects a choice returned after the deadline.
- Pi `CountdownTimer` rounds with `Math.ceil(timeoutMs / 1000)` and decrements once per second: https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/src/modes/interactive/components/countdown-timer.ts#L13-L30
- The selector's expiry callback cancels the selection: https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/src/modes/interactive/components/extension-selector.ts#L55-L62
## Required outcome
- The active selector must stop accepting input at the questionnaire's absolute deadline, so it does not present a choice that the helper will discard.
## Suggested approach
- Dismiss the selector with a signal fired at the exact remaining deadline, combined with the tool-call abort signal.
Summary
ask_user_questionwaits can now time out, so an unattended run no longer blocks forever. When the deadline passes, the agent gets an explicit "no answer received — proceed with your best judgement or stated default" result. An answer that arrives after that is not lost: the desktop marks the card expired and offers to send the answer as a follow-up.Source
DeepSeek harness comparison page, digests 2026-09-24..09-27 (timed ask-user waits and late-reply handling).
Changes
renderer/shared/ask-user-question.ts):expiresAton prompts, and a main-onlytimedOuton responses.main/services/ask-user-question-coordinator.ts):{cancelled:true, timedOut:true}.respondWithOutcomereturns answered, expired or rejected. The last 64 expired prompts are remembered.ask-user-question-extension.ts):timeoutSecondsparameter and an explicit best-judgement timed-out result.resolveAskUserQuestionTimeoutMspolicy: attended desktop gets no deadline unless requested; Remote-owned runs always get one, capped at 5 min.llm-clientfor the tool and the advisor picker. timedOut is also cancelled, so the vendored advisor copy needs no re-vendor.chat:answerQuestionnairereturns{status: "answered" | "expired"}and still throws on rejection.AskUserQuestionExpiryNotice, with an expired state and a late-answer state offering Send/Queue follow-up and Discard.chat-pane.tsx.AskUserQuestionComposerandstyles.cssare untouched, so this does not overlap open PR Align ask-user-question composer with lettered A–D card #122.aiden-remote-streams.ts):question_requiredand the pending-question snapshot now carry the prompt's realexpiresAt, capped at the existing 5-min Remote lifetime.docs/plans/timed-ask-user-plan.mdplus a row in the plans index.expiresAtsemantics..memory/timed-ask-user.mdand.papercutsnotes.Tests
npm run test:ask-user-question: 17 pass. New coverage:expiresAt,timedOutcannot be forged, the follow-up formatter.npm run test:aiden-remote: all pass. New test: Remote questions expire at the agent deadline, never later than the Remote lifetime.npm run test:advisor(28); ipc-contract, chat.parse and advisor-runtime (35); ci-test-registry (9).packages/cliextensions tests (7).AidenQuestionTestandAidenChatTestpass.tsc --noEmitand eslint on the changed files are clean.respondToQuestion.Follow-ups
🤖 Generated with Claude Code