Skip to content

feat(ask-user): timed waits with late-reply handling - #270

Merged
sambitcreate merged 5 commits into
mainfrom
feature/timed-ask-user
Sep 29, 2026
Merged

sambitcreate merged 5 commits into
mainfrom
feature/timed-ask-user

Conversation

@sambitcreate

Copy link
Copy Markdown
Owner

Summary

ask_user_question waits 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

  • Shared contract (renderer/shared/ask-user-question.ts):
    • Optional strict-ISO expiresAt on prompts, and a main-only timedOut on responses.
    • Timeout clamping (30–3600 s, unattended default 300 s).
    • A formatter that turns a late answer into a follow-up message.
  • Coordinator (main/services/ask-user-question-coordinator.ts):
    • Owns a per-prompt deadline and settles on timeout as {cancelled:true, timedOut:true}.
    • A response that arrives after the deadline is settled as a timeout, even before the timer fires.
    • respondWithOutcome returns answered, expired or rejected. The last 64 expired prompts are remembered.
  • Tool (ask-user-question-extension.ts):
    • New timeoutSeconds parameter and an explicit best-judgement timed-out result.
    • resolveAskUserQuestionTimeoutMs policy: attended desktop gets no deadline unless requested; Remote-owned runs always get one, capped at 5 min.
    • Wired through llm-client for the tool and the advisor picker. timedOut is also cancelled, so the vendored advisor copy needs no re-vendor.
  • IPC: chat:answerQuestionnaire returns {status: "answered" | "expired"} and still throws on rejection.
  • Desktop UI:
  • Remote (aiden-remote-streams.ts):
    • question_required and the pending-question snapshot now carry the prompt's real expiresAt, capped at the existing 5-min Remote lifetime.
    • The wire shape is unchanged, so there is no contract revision bump.
  • iOS / Android: when a question expires locally, both clients explain that Aiden continued and suggest sending the answer as a message. Previously they dropped it silently.
  • Docs:
    • New docs/plans/timed-ask-user-plan.md plus a row in the plans index.
    • Remote API expiresAt semantics.
    • .memory/timed-ask-user.md and .papercuts notes.

Tests

  • npm run test:ask-user-question: 17 pass. New coverage:
    • Coordinator: timeout, late expired vs rejected, deadline race, untimed.
    • Extension: timeout policy and the timed-out message.
    • Shared contract: strict expiresAt, timedOut cannot be forged, the follow-up formatter.
    • Expiry notice: render test.
  • 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/cli extensions tests (7).
  • Android: AidenQuestionTest and AidenChatTest pass.
  • tsc --noEmit and eslint on the changed files are clean.
  • Not run: iOS XCTest. The iOS change is a single assignment, and no ViewModel test covers respondToQuestion.

Follow-ups

  • Timeout policy for the CLI's headless ask-user wait.
  • Native late-answer composer prefill and a live-expiry card state on iOS and Android.
  • An optional deadline for detached or background desktop renderer runs.

🤖 Generated with Claude Code

…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>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Luna | 𝕏

Comment thread renderer/main/chat-pane.tsx
Comment thread main/services/ask-user-question-extension.ts
@very-hermes-bot

very-hermes-bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Hermes Review Bot

Confidence: 5

Engine: agy/gemini-3.8-flash-high
Review mode: full
Head: 7d56fbb70d98e67a7039305e852a67829b67f74b
Generated: 2026-09-29T06:29:03+00:00
Reviews: 1

Summary

Timed waits allow ask_user_question to resolve with an explicit best-judgement directive when unattended or when a deadline is requested, preventing agent turns from blocking indefinitely. The main-process coordinator owns deadlines, stamps canonical RFC 3339 expiresAt instants onto prompts, settles racing replies as expired if submitted past the deadline, and tracks recently expired prompt IDs. Desktop and remote surfaces retain expired prompts rather than discarding them: desktop turns late answers into Send/Queue follow-up messages, Remote synchronizes stream deadlines to the agent timeout (capped at 5 minutes), and the CLI applies a unified shrinking deadline across all terminal select prompts. Maintainers should double-check that attended desktop runs continue waiting without an automatic deadline unless the agent explicitly supplies timeoutSeconds.

Confidence Score: 5/5

Traced 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
  • renderer/shared/ask-user-question.ts: Adds timeout constants, expiresAt prompt typing, timedOut response typing, and helper functions for calculating remaining time, retaining expired prompts, and formatting follow-up messages.
  • main/services/ask-user-question-coordinator.ts: Manages prompt deadlines, fires timeout settlements, enforces expiration on racing late responses, and tracks recent expired prompt IDs for owner attribution.
  • main/services/ask-user-question-extension.ts: Adds the timeoutSeconds tool parameter schema, resolution policy (unattended vs attended), and explicit best-judgement feedback for timed-out executions.
  • main/services/llm-client.ts: Passes timeout configuration into questionnaire requests for agent turns and advisor picker, and adds answerQuestionnaireWithOutcome.
  • main/handlers/chat.ts & renderer/lib/ipc.ts: Updates chat:answerQuestionnaire IPC handler to return the settlement status (answered | expired) rather than throwing on expired prompts.
  • renderer/main/chat-pane.tsx: Retains expired prompts in the UI after agent continuation, displays the expiry notice, and routes late answers to the message queue/send handler as follow-up messages.
  • renderer/components/ask-user-question-expiry-notice.tsx: Renders the soft status callout and action buttons (Send/Queue follow-up, Discard) for expired questionnaires and late answers.
  • main/services/aiden-remote-streams.ts: Synchronizes Remote stream question lifetimes with the coordinator's expiresAt deadline while enforcing the 5-minute Remote maximum.
  • packages/cli/src/extensions/ask-user-question-core.ts & ask-user-question.ts: Implements single-deadline tracking across all dialog prompts in the CLI terminal questionnaire loop, preserving accumulated multi-select options.
  • ios/.../AidenChatFeature.swift & android/.../AidenChatViewModel.kt: Surfaced informative error messages when answering an expired Remote question instead of silently clearing the prompt.

Findings

No findings.

📊 Sequence Diagram
sequenceDiagram
  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
Loading

Machine-Readable Findings

[]

Last reviewed commit: 7d56fbb70d98
Reviews (1) · Comment /hermes review to trigger a new review · /hermes review full for full re-review

@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

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
timestamp=2026-09-27T17:22:28.483Z level=ERROR run=25621607 message="stream error" providerID=opencode-go modelID=glm-5.3-flash session.id=ses_f1c1e41b9ffeJkyO4gaDqbHnYV small=false agent=build mode=primary error.error.message="Streaming response failed: [server_error] request failed" error.error.type=server_error error.error.param=null
timestamp=2026-09-27T17:35:50.499Z level=ERROR run=25621607 message="stream error" providerID=opencode-go modelID=glm-5.3-flash session.id=ses_f1c1e41b9ffeJkyO4gaDqbHnYV small=false agent=build mode=primary error.error="AI_APICallError: Go usage limit exceeded"

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using GLM Flash | 𝕏

# Conflicts:
#	.papercuts/troubleshooting.md
# Conflicts:
#	.papercuts/troubleshooting.md

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Luna | 𝕏

if (remaining !== undefined && remaining <= 0) return { kind: "timedOut" };
const choice = await ui.select(title, options, {
...(signal ? { signal } : {}),
...(remaining !== undefined ? { timeout: remaining } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sambitcreate
sambitcreate merged commit fef8d22 into main Sep 29, 2026
24 checks passed
@sambitcreate
sambitcreate deleted the feature/timed-ask-user branch September 29, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants