Two more on-device UAT findings:
1. Deny-retry loop: the model, told 'the user declined', simply called the
tool again — each retry minted a fresh pending and re-opened the dialog
(T-13-50 habituation, mechanized). ToolExecCtx now remembers declined
actions for the turn, keyed by confirm::action_key — the same canonical
(tool_name, validated_args) identity the nonce binds — and execute_tool
refuses a re-ask before the gate, minting nothing. Regression test
declined_action_never_reprompts_same_turn.
2. Timeout chain: rpcClient's 15s default aborted every confirmable turn
client-side while the node kept the pending alive — the next turn then
re-announced it (modal over and over) and every wait read as 'timed
out'. assistant.chat now rides a 420s timeout; AIUI's bridge goes
180s→430s so the host's error path (which also expires the dialog)
always fires first. Declined ToolResult text now also tells the model
to stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On-device UAT: the operator was timed out mid-read (120s), the chat turn
returned 'declined' while the dialog was still up, and their Approve then
hit a dead entry ('no such pending confirmation', 13:37:12 log). Nothing
executed — the gate failed safe — but the UX was a lie in both directions.
- CONFIRM_TIMEOUT 120s→300s: human-speed per T-13-51's own rubric.
- ContextBroker dispatches aiui:tool-confirm-expired when a pending action
vanishes node-side (poll) or the turn ends; Chat.vue closes the modal on
it. Same host-only CustomEvent discipline; iframe has no path to it.
- Two new tests; 21/21 green across toolConfirm + chatAiuiEmbed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Executor stopped deliberately for a session restart (bypass-permissions relaunch).
Executor's final report: 'cargo test assistant confirm-gate suite 28/28 green,
individual nonce test passes; committing Task 1 next — first verify the
tools.rs/grants.rs/backends diffs are formatting-only.'
Task 1 (D-07/D-11 confirm gate, backend) is implemented and test-green but this
checkpoint is verbatim-uncommitted-state, NOT the reviewed atomic Task 1 commit:
continuation executor should verify diffs, then reset --soft or commit-on-top
into proper feat(13-08) task commits. Task 2 (ToolConfirmModal.vue trusted
chrome, Chat.vue + contextBroker.ts wiring) is partially built, tests written.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- confirm.rs: ConfirmGate/PendingConfirmation/Confirmed/PendingSnapshot/
ResolveRefusal API skeleton (request/resolve/mint_nonce/build_description
still todo!()) plus the five named confirm tests: S-02 nonce binding,
S-03 no-model-text, S-08 distinct resources, S-09 restart drops pending,
timeout declines, and the no-shared-lock-across-the-wait case
- mod.rs: ToolExecCtx gains the confirm gate (global by default, injectable
for tests) and the S-01 destructive_tool_requires_confirm test with a
seeded installed-app snapshot
- verified RED: 7 new tests fail (todo! cores + unfilled destructive branch)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>