feat(mcp): always-explicit retryable on structured errors + transient inference (P1-1) (#875)

Agents could not distinguish 'keep retrying' from 'stop, this is
broken' (agent.feedback): retryable was emitted only when a registry
entry declared true — absent otherwise, including for genuinely
transient DB/network failures whose SQLSTATE is lost when tools wrap
them as Error('Database error: ...').

- StructuredError.retryable is now a required boolean. Registry
  declaration wins; otherwise isTransientFailure() infers from Postgres
  SQLSTATEs (40001/40P01/57014/08xxx/53xxx/55P03), upstream HTTP
  statuses (408/429/5xx), and message signatures that survive wrapping
  (deadlock, serialization, statement timeout, fetch/socket failures).
- Unclassified transient failures surface as stable code
  TRANSIENT_ERROR (new registry entry, retryable: true).
- categorize_transaction accepts idempotency_key — the tool agents
  blind-retry after client-side approval-elicitation drops; the key
  makes that retry replay-safe instead of double-staging.
- Contract documented in .claude/rules/mcp-server.md. The planned
  'kind' field was dropped: the code registry already encodes it;
  retryable is the agent-actionable bit.

Every tool error already flows through the single dispatch point
(toToolError -> getStructuredError), so coverage is universal without
per-tool migration. Full unit suite: 6575 tests green.

Part of dev_docs/mcp_optimization_plan.md (P1-1).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-03 10:29:58 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 001de3c844
commit 250cc7c450
6 changed files with 135 additions and 10 deletions
+9
View File
@@ -47,6 +47,15 @@ const GENERIC: Record<string, StructuredErrorEntry> = {
message_sv: 'Något gick fel. Försök igen.',
message_en: 'An unexpected error occurred.',
},
// Unclassified-but-transient failures (DB deadlock/timeout, connection
// drop, upstream 5xx/429) inferred by isTransientFailure() when no
// specific code applies. Stable code so agents can dispatch on it.
TRANSIENT_ERROR: {
httpStatus: 503,
message_sv: 'Tillfälligt fel — försök igen om en stund.',
message_en: 'Transient failure — retry the same request after a short backoff.',
retryable: true,
},
INTERNAL_ERROR: {
httpStatus: 500,
message_sv: 'Ett oväntat serverfel uppstod. Försök igen senare.',