fix(13-08): declined actions never re-prompt + timeout chain covers the human wait

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>
This commit is contained in:
archipelago
2026-08-05 14:50:17 -04:00
co-authored by Claude Fable 5
parent 44c864ac14
commit 2d1f09d8c8
5 changed files with 118 additions and 5 deletions
@@ -278,14 +278,17 @@ export const archyBridge = {
text,
})
// Matches the node's ASSISTANT_HTTP_TIMEOUT (180s) — the assistant
// loop's tool-calling round trip can legitimately take that long.
// Must cover the node's WHOLE turn: multiple model round trips
// (ASSISTANT_HTTP_TIMEOUT 180s each) plus the confirm gate's
// human-speed wait (CONFIRM_TIMEOUT 300s). Kept just ABOVE the host
// page's own assistant.chat RPC timeout (420s) so the host's error
// path — which also expires the confirm dialog — fires first.
setTimeout(() => {
if (pendingRequests.has(id)) {
pendingRequests.delete(id)
reject(new Error('Chat request timed out'))
}
}, 180000)
}, 430000)
})
},