You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: stop reporting expired questions as user dismissals (#85)
## Related Issue
No issue — reported by a Windows desktop user: the agent asked a
question, the user answered, and the agent replied that the question was
dismissed. The problem is described below.
## Problem
A question asked through the daemon path (`AskUserQuestion` →
`packages/server` `QuestionService` → REST/WS → `apps/pythinker-web`
`QuestionCard`) expired after **60 seconds**. On expiry the tool caught
the rejection and told the model:
> User dismissed the question without answering.
So the agent reported a dismissal for a question the user was still
reading. The desktop app (Electron → loopback server → web UI) is where
users meet it: the card disappears at 60 s, a late answer POST returns
`40902`, and the user sees only a toast. Transport failures and server
shutdown produced the same false message, because `ask-user.ts` mapped
**every** throw to a dismissal.
Two further defects sit in the same path:
- The answers that reach the model were synthesized ids —
`{"answers":{"q_0":"opt_0_1"}}` — with no question text and no option
label. The terminal and ACP surfaces both send `{ question text: option
label }`, and `mcp/elicitation.ts` reads answers by question text, so
MCP elicitation was broken over this path too.
- A stray `Escape` anywhere in the page silently destroyed a pending
question, through a document-level key handler.
## What changed
- **The timer is a 30-minute lease, not a network timeout.** A pending
question waits on a human, so the timer only guards against a leak.
Removing it entirely was considered and rejected: the daemon bridge
drops the `AbortSignal` and `loop/tool-call.ts` ends an aborted tool
with a grace sentinel while the broker entry stays pending, so a
timer-free question would become an unbounded orphan.
- **Causes are preserved at the tool boundary.** Expiry carries the new
`question.expired` error code — class identity does not survive the RPC
JSON round-trip, so the code is the only reliable discriminator — and
returns a neutral "not answered" result. Other failures return real tool
errors. Only a null or empty answer still means "dismissed".
- **Answers keep the words the user saw**: question text keys, option
label values, joined with `, ` for multi-select. `toAgentCoreResponse`
now takes the originating request, and
`QuestionService.resolveProtocolResponse` supplies it, since the pending
entry is the only holder.
- **`Escape` no longer dismisses.** The visible Dismiss button is
unchanged.
- **The web card warns** when less than five minutes of the lease
remain, using the `expires_at` the protocol already delivered and
nothing consumed.
## Verification
Run locally on this branch:
- `pnpm --filter @pymodel/agent-core exec vitest run` — 3577 passed, 1
skipped
- `pnpm --filter @pymodel/server exec vitest run` — 503 passed
- `pnpm --filter @pymodel/pythinker-web exec vitest run` — 263 passed
- `pnpm run typecheck` — passed
- `pnpm run lint` — clean
The rewritten adapter tests were confirmed RED against the old behaviour
first (`expected { q_0: 'opt_0_1' } to deeply equal { 'Which animal?':
'Dog' }`), so they can fail.
## Checklist
- [x] I have read the
[CONTRIBUTING](https://github.com/PyModel/pythinker-code/blob/main/CONTRIBUTING.md)
document.
- [x] I have linked a related issue, or explained the problem above.
- [x] I have added tests that prove my feature works.
- [x] Ran `gen-changesets` skill, or this PR needs no changeset.
- [ ] Ran `gen-docs` skill, or this PR needs no doc update.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Question response leases now last up to 30 minutes.
* Added live expiration warnings during the final five minutes,
including under one minute.
* Responses now retain question text and selected option labels.
* **Bug Fixes**
* Expired questions now show a clear expiration message instead of a
generic error.
* Dismissal and delivery failures are handled separately for more
accurate feedback.
* Minimized question cards no longer respond to keyboard input,
including Escape.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Questions no longer expire after 60 seconds, expired questions are not reported as user dismissals, answers retain question text and option labels, and Escape no longer dismisses a question.
Copy file name to clipboardExpand all lines: apps/pythinker-web/AGENTS.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,13 +23,13 @@ The browser web UI for Pythinker Code — a peer to the TUI in `apps/pythinker-c
23
23
- Shared components go in `src/components/`; reusable logic goes in `src/composables/` with a `use` prefix.
24
24
- There is **no auto-import plugin** and **no path alias** — `#/` and `@/` are intentionally unused. Write relative imports (`../i18n`, `./config`).
25
25
26
-
## i18n (normative — keeping locales in sync is manual)
26
+
## i18n (normative — the app is English-only)
27
27
28
28
- Setup: `src/i18n/index.ts`, vue-i18n in Composition mode (`legacy: false`), fallback `en`. The active locale is persisted in `localStorage` under `pythinker-locale`.
29
-
- Locale files: `src/i18n/locales/{en,zh}/<namespace>.ts`, each `export default { ... } as const`. New namespaces are registered in `src/i18n/locales/index.ts`.
29
+
-**`en` is the only locale.**`src/i18n/locales/` contains exactly one directory, and `locales/index.ts` registers only `en`. Do not add a second locale, and do not "restore parity" with one that does not exist.
30
+
- Locale files: `src/i18n/locales/en/<namespace>.ts`, each `export default { ... } as const`. New namespaces are registered in `src/i18n/locales/index.ts`.
30
31
- Reference with `const { t } = useI18n()` and `t('namespace.key')` (same form in templates).
31
-
-**Adding a key:** add it to **both**`en/<ns>.ts` and `zh/<ns>.ts`. **Adding a namespace:** create the file in both locales **and** register it in `locales/index.ts`.
32
-
- There is **no automated missing-key or en/zh parity check**. Keeping the two locales in sync is a manual responsibility — do not leave a key present in only one locale.
32
+
-**Adding a key:** add it to `en/<ns>.ts`. **Adding a namespace:** create the file under `en/`**and** register it in `locales/index.ts`.
0 commit comments