fix: first confirmation of chat tool calls always failed — canonicalize fingerprint JSON - #289
Conversation
Every first confirmation of a chat tool call failed deterministically: the pending operation's arguments are persisted as jsonb, which reorders keys and strips whitespace, so re-executing from the stored copy hashed to a different fingerprint than the original tool call and TryConsumeFreshConfirmation never matched (the retry then worked because the second pending op was created from the already-normalized form). The fingerprint now canonicalizes arguments (ordinal-sorted keys, compact re-serialization) before hashing, making it stable across storage round-trips and client/server whitespace differences. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Code Review: PR #289Scope: PR #289 — canonicalize fingerprint JSON so jsonb round-trips match on confirm SummaryTwo-file diff: Traced the full call chain to confirm this is a root-cause fix, not a symptom patch:
FindingsCritical / High / Medium: none. Low / Info:
SecurityReviewed the canonicalization for collision and DoS risk: it only reorders object keys and normalizes whitespace — the values themselves (via Contract driftN/A — no DTO, Controller route, or What's good
ValidationBuild/test could not be re-run from this review session (sandbox restriction on this pass). Relying on the PR's own reported run: Domain 486 ✓ / Application 2571 ✓ / Infrastructure 1368 ✓. Recommend confirming via CI's Build/Test checks before merge. 🤖 Generated with Claude Code |



Problem
Every confirmation-gated chat tool call failed its first Confirm with
confirmation_requiredand succeeded on retry — reproduced consistently in launch QA (bulk habit creation via Astra).Root cause:
PendingAgentOperationState.ArgumentsJsonis a jsonb column. PostgreSQL normalizes jsonb (reorders object keys, strips whitespace), soAiController.ExecutePendingOperationre-executes with arguments whoseGetRawText()differs byte-for-byte from the original tool call. The recomputed fingerprint never matched the stored one,TryConsumeFreshConfirmationfailed, and policy demanded a fresh confirmation — whose pending op, built from the already-normalized form, then matched on the second attempt. Deterministic fail-once-work-twice. This predates #288 (the raw-string comparison had the identical mismatch).Fix
AgentOperationFingerprint.Computenow canonicalizes the arguments before hashing: parse → recursive re-serialization with ordinal-sorted object keys, compact output. Both construction sites (executor + MCP middleware) already funnel through this helper, so creation and consumption agree regardless of storage normalization or client/server whitespace. Non-JSON input hashes as opaque text.Deploy note: pending operations in flight at deploy time mismatch once more (TTL is minutes); after that, first-confirm works everywhere.
Validation
New tests: fingerprint stable across key-reordering/whitespace (jsonb-style round-trip), opaque non-JSON handling; existing bound/determinism/differentiation tests unchanged. Full suite: Domain 486 ✓ / Application 2571 ✓ / Infrastructure 1368 ✓.
Refs #288, found in thomasluizon/orbit-ui-mobile#382 launch QA
🤖 Generated with Claude Code