fix: restore pending asks after resume

This commit is contained in:
Erik Kotsiuba
2026-08-12 14:18:22 +02:00
parent ce230db275
commit 4de596ae54
10 changed files with 757 additions and 5 deletions
+5
View File
@@ -18,6 +18,8 @@ The codebase is split so the implementation reads through file boundaries and na
- `src/answer-commands.ts` — `/answer`, `/answer:again`, and `/ask:replay` command wiring
- `src/answer-extraction.ts` — configured extraction model selection, synthetic `ask_user` tool-call extraction, and tolerant text fallback
- `src/ask-payload-store.ts` — branch-aware persisted ask payload lookup
- `src/pending-ask.ts` — active-branch unresolved tool-call detection and recovery dismissal persistence
- `src/resume-pending-ask.ts` — detached lifecycle wiring and recovered answer delivery
- `src/notifications.ts` — best-effort ask notification payload rendering and channel execution
- `src/remote-ask.ts` — package-prefixed local event contract, active-flow registry, and explicit remote submission validation
- `src/schema.ts` — TypeBox schema
@@ -78,6 +80,9 @@ The codebase is split so the implementation reads through file boundaries and na
- persisted ask settings are migrated to the current schema version in memory, validated, and normalized before use without rewriting the config file on load
- config schema migrations preserve user-provided values and add new defaults only when fields are absent
- replay payload lookup scans only the current session branch and revalidates payloads before use
- interrupted-ask recovery selects only the newest unresolved `ask_user` call on the active branch, prefers its valid persisted payload, and validates original arguments as fallback
- recovered forms open only for TUI `startup`, `resume`, and `fork` events; the flow stays detached from the lifecycle handler
- recovered submit and cancel both persist dismissal before the same call can reopen; manual `/ask:replay` ignores that recovery marker
- invalid persisted keymaps fall back to default keymaps for the current session without discarding valid behaviour, notification, or answer settings
- invalid notification channels are skipped and fall back to the default bell channel if none are valid
- ask settings behaviour and notification enabled changes attempt to persist immediately from the settings list; save failures revert the change and show an error; config reset is guarded by a short double-press confirmation
+11
View File
@@ -213,6 +213,7 @@ This document defines the stable external behavior. It does not explain internal
- `/answer` extraction may use an internal `freeform: true` option for open-ended questions with no explicit choices; these render as user-input-only questions with the label `Type your answer:`, no numbered option row, and no selection caret; this marker is not part of the public `ask_user` tool contract
- `/answer:again` command to replay the latest `/answer`-extracted form on the current branch
- `/ask:replay` command to replay the latest real `ask_user` form on the current branch
- automatic recovery of the newest unresolved `ask_user` form on startup, resume, or fork
- ask settings list with binary behaviour/notification toggles and a guarded reset-to-defaults action
- `?` in the ask flow and `/ask-settings` in pi open the same lightweight ask settings overlay
- settings attempt to persist immediately when changed: `Auto-submit when answered without notes`, `Confirm dismiss when dirty`, `Double-press review shortcuts`, `Notifications`, and `Show footer hints`; `Present single-select as multi-select` persists immediately when saving succeeds but applies only to new/replayed ask flows; save failures revert the setting and show a manual-edit message; resetting config to defaults requires pressing the reset action twice within a short confirmation window
@@ -295,6 +296,16 @@ See [`remote-events.md`](remote-events.md) for payload shapes, examples, and a l
- submitted or elaborated command-flow results are sent back with user-message semantics
- replay commands scan only `ctx.sessionManager.getBranch()`, ignore sibling/future branch payloads, and revalidate stored payloads before opening the UI
## Interrupted ask resume
- on `session_start` with reason `startup`, `resume`, or `fork`, pi-ask finds the newest `ask_user` tool call on the active branch that has neither a tool result nor an `ask:pending-dismissed` entry
- recovery does not run for `new`, `reload`, or non-TUI sessions
- the matching valid `ask:payload` supplies the form; if it is missing or invalid, pi-ask validates and uses the original tool call arguments instead
- the recovery flow is detached from `session_start`, so an open form does not block other lifecycle handlers
- because the interrupted `execute` promise no longer exists, submit sends the result with the same user-message semantics as replay commands
- submit and cancel both append `ask:pending-dismissed`, which prevents another automatic reopen; `/ask:replay` still works
- recovered flows emit remote lifecycle events with source `ask:resume`
The fallback message includes normalized pending questions and options so the caller can re-ask them manually. `details.questions` still contains normalized question metadata, while `details.answers` stays empty until a user responds.
## Skill alignment (advisory)
+3 -3
View File
@@ -25,14 +25,14 @@ type PiAskStartedEvent = {
version: 1;
flowId: string;
toolCallId?: string;
source: "tool" | "answer" | "answer:again" | "ask:replay";
source: "tool" | "answer" | "answer:again" | "ask:replay" | "ask:resume";
title?: string;
questions: AskQuestion[];
createdAt: number;
};
```
Use `flowId` for submit/correlation. Use `questions[].id` and `questions[].options[].value` for answers.
Use `flowId` for submit/correlation. Use `questions[].id` and `questions[].options[].value` for answers. `ask:resume` identifies a form recovered from an interrupted tool call.
## Submit an answer
@@ -111,7 +111,7 @@ type PiAskCompletedEvent = {
version: 1;
flowId: string;
toolCallId?: string;
source: "tool" | "answer" | "answer:again" | "ask:replay";
source: "tool" | "answer" | "answer:again" | "ask:replay" | "ask:resume";
result: AskResult;
completedAt: number;
};