instar 1.3.792 → 1.3.793

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-09T10:17:12.654Z",
5
- "instarVersion": "1.3.792",
4
+ "generatedAt": "2026-07-09T21:29:43.497Z",
5
+ "instarVersion": "1.3.793",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -11,7 +11,7 @@
11
11
  "domain": "identity",
12
12
  "sourcePath": "src/core/PostUpdateMigrator.ts",
13
13
  "installedPath": ".instar/hooks/instar/session-start.sh",
14
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
14
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
15
15
  "since": "2025-01-01"
16
16
  },
17
17
  "hook:dangerous-command-guard": {
@@ -20,7 +20,7 @@
20
20
  "domain": "safety",
21
21
  "sourcePath": "src/core/PostUpdateMigrator.ts",
22
22
  "installedPath": ".instar/hooks/instar/dangerous-command-guard.sh",
23
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
23
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
24
24
  "since": "2025-01-01"
25
25
  },
26
26
  "hook:grounding-before-messaging": {
@@ -29,7 +29,7 @@
29
29
  "domain": "safety",
30
30
  "sourcePath": "src/core/PostUpdateMigrator.ts",
31
31
  "installedPath": ".instar/hooks/instar/grounding-before-messaging.sh",
32
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
32
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
33
33
  "since": "2025-01-01"
34
34
  },
35
35
  "hook:compaction-recovery": {
@@ -38,7 +38,7 @@
38
38
  "domain": "identity",
39
39
  "sourcePath": "src/core/PostUpdateMigrator.ts",
40
40
  "installedPath": ".instar/hooks/instar/compaction-recovery.sh",
41
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
41
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
42
42
  "since": "2025-01-01"
43
43
  },
44
44
  "hook:external-operation-gate": {
@@ -47,7 +47,7 @@
47
47
  "domain": "safety",
48
48
  "sourcePath": "src/core/PostUpdateMigrator.ts",
49
49
  "installedPath": ".instar/hooks/instar/external-operation-gate.js",
50
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
50
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
51
51
  "since": "2025-01-01"
52
52
  },
53
53
  "hook:deferral-detector": {
@@ -56,7 +56,7 @@
56
56
  "domain": "safety",
57
57
  "sourcePath": "src/core/PostUpdateMigrator.ts",
58
58
  "installedPath": ".instar/hooks/instar/deferral-detector.js",
59
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
59
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
60
60
  "since": "2025-01-01"
61
61
  },
62
62
  "hook:self-stop-guard": {
@@ -65,7 +65,7 @@
65
65
  "domain": "coherence",
66
66
  "sourcePath": "src/core/PostUpdateMigrator.ts",
67
67
  "installedPath": ".instar/hooks/instar/self-stop-guard.js",
68
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
68
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
69
69
  "since": "2025-01-01"
70
70
  },
71
71
  "hook:post-action-reflection": {
@@ -74,7 +74,7 @@
74
74
  "domain": "evolution",
75
75
  "sourcePath": "src/core/PostUpdateMigrator.ts",
76
76
  "installedPath": ".instar/hooks/instar/post-action-reflection.js",
77
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
77
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
78
78
  "since": "2025-01-01"
79
79
  },
80
80
  "hook:external-communication-guard": {
@@ -83,7 +83,7 @@
83
83
  "domain": "safety",
84
84
  "sourcePath": "src/core/PostUpdateMigrator.ts",
85
85
  "installedPath": ".instar/hooks/instar/external-communication-guard.js",
86
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
86
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
87
87
  "since": "2025-01-01"
88
88
  },
89
89
  "hook:scope-coherence-collector": {
@@ -92,7 +92,7 @@
92
92
  "domain": "coherence",
93
93
  "sourcePath": "src/core/PostUpdateMigrator.ts",
94
94
  "installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
95
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
95
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
96
96
  "since": "2025-01-01"
97
97
  },
98
98
  "hook:scope-coherence-checkpoint": {
@@ -101,7 +101,7 @@
101
101
  "domain": "coherence",
102
102
  "sourcePath": "src/core/PostUpdateMigrator.ts",
103
103
  "installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
104
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
104
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
105
105
  "since": "2025-01-01"
106
106
  },
107
107
  "hook:free-text-guard": {
@@ -110,7 +110,7 @@
110
110
  "domain": "safety",
111
111
  "sourcePath": "src/core/PostUpdateMigrator.ts",
112
112
  "installedPath": ".instar/hooks/instar/free-text-guard.sh",
113
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
113
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
114
114
  "since": "2025-01-01"
115
115
  },
116
116
  "hook:claim-intercept": {
@@ -119,7 +119,7 @@
119
119
  "domain": "coherence",
120
120
  "sourcePath": "src/core/PostUpdateMigrator.ts",
121
121
  "installedPath": ".instar/hooks/instar/claim-intercept.js",
122
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
122
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
123
123
  "since": "2025-01-01"
124
124
  },
125
125
  "hook:claim-intercept-response": {
@@ -128,7 +128,7 @@
128
128
  "domain": "coherence",
129
129
  "sourcePath": "src/core/PostUpdateMigrator.ts",
130
130
  "installedPath": ".instar/hooks/instar/claim-intercept-response.js",
131
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
131
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
132
132
  "since": "2025-01-01"
133
133
  },
134
134
  "hook:stop-gate-router": {
@@ -137,7 +137,7 @@
137
137
  "domain": "safety",
138
138
  "sourcePath": "src/core/PostUpdateMigrator.ts",
139
139
  "installedPath": ".instar/hooks/instar/stop-gate-router.js",
140
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
140
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
141
141
  "since": "2025-01-01"
142
142
  },
143
143
  "hook:auto-approve-permissions": {
@@ -146,7 +146,7 @@
146
146
  "domain": "safety",
147
147
  "sourcePath": "src/core/PostUpdateMigrator.ts",
148
148
  "installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
149
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
149
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
150
150
  "since": "2025-01-01"
151
151
  },
152
152
  "job:health-check": {
@@ -1562,7 +1562,7 @@
1562
1562
  "type": "subsystem",
1563
1563
  "domain": "updates",
1564
1564
  "sourcePath": "src/core/PostUpdateMigrator.ts",
1565
- "contentHash": "3f313485a6a337aa34c0e018a150aa71e93c83f7501218f68de6e1ec5fda059e",
1565
+ "contentHash": "c9ff3ce2f1841e0305cea35f4cebad57a815e5a8eaead7c8cad19ec41fbe7b6f",
1566
1566
  "since": "2025-01-01"
1567
1567
  },
1568
1568
  "subsystem:scheduler": {
@@ -913,6 +913,7 @@ ${ROUTING_SPEND_CLAUDEMD_SECTION(port)}
913
913
  - See current routing: \`curl -H "Authorization: Bearer $AUTH" "http://localhost:${port}/intelligence/routing"\` → \`{ defaultFramework, components: [{ component, category, framework, available }], coverage }\`. On a Codex-active agent, sentinel/gate/reflector resolve to \`codex-cli\`; \`job\` (cost-bearing background work, e.g. CartographerSweep) stays on the agent default.
914
914
  - Override in \`.instar/config.json\` → \`sessions.componentFrameworks\`, e.g. \`{ "categories": { "sentinel": "codex-cli", "gate": "codex-cli" }, "overrides": { "CoherenceReviewer": "claude-code" }, "fallback": "default" }\` — an explicitly-set block is used verbatim (the default no longer applies). Categories: \`sentinel | gate | job | reflector | other\`. Resolution: \`overrides[name] → categories[category] → default\`. Frameworks: \`claude-code | codex-cli | gemini-cli | pi-cli\`. **Rollback lever:** set \`componentFrameworks\` to \`{}\` (explicit empty) to force everything back to the default framework with no swap — today's pre-default behavior.
915
915
  - Each framework gets its own circuit breaker (a Claude trip can't pause Codex). If a routed framework's CLI is missing it degrades to the default and reports it. A gating call's failure-swap is bounded by a per-attempt timeout (default 5s, \`intelligence.swapAttemptTimeoutMs\`) so a slow provider is abandoned at the cap, not waited on in full. Routes INTERNAL component calls only — spawned interactive sessions stay governed by \`topicFrameworks\`.
916
+ - **Non-gating calls also get a bounded swap now** (\`intelligence.nonGatingFailureSwap\`, default ON): a non-gating internal call (e.g. \`TopicIntentExtractor\`) that suffers an INVOCATION-level primary failure (the CLI spawn/timeout/empty-output errored with ZERO tokens) gets ONE bounded, herd-safe swap onto the next active off-Claude framework instead of hard-erroring — tighter than the gating swap: at most one step, NEVER onto \`claude-code\`/the default framework (non-gating background traffic must never herd onto the Claude tail), and NEVER on a content/parse error that already burned tokens (the caller fail-opens that). Disable with \`intelligence.nonGatingFailureSwap.enabled: false\`. Proactive: "why did my background classifier's error rate drop / does a non-gating call fall back too?" → this bounded swap. (Spec: \`docs/specs/nongating-failure-swap.md\`.)
916
917
  - **When to use** (PROACTIVE): when the user is hitting rate limits and asks how to spread load, or says "run my sentinels on Codex" / "move the background checks off Claude" → point them at \`sessions.componentFrameworks\` and \`GET /intelligence/routing\`. Restart sessions to apply (config is read into the router at the call path, but a file edit needs the server to pick it up). (Spec: \`docs/specs/per-component-framework-routing.md\`.)
917
918
 
918
919
  **Topic Profile (per-topic model, thinking, framework pins)** — Every conversation topic can carry a durable profile pinning its BASELINE model (an explicit id OR a tier — never both), thinking depth (\`off\`/\`low\`/\`medium\`/\`high\`/\`max\`), and framework (\`claude-code\`/\`codex-cli\`/…). Pins survive restarts and follow the topic. **The conversational surface is PRIMARY** (PROACTIVE — these are the triggers): when the user says "use codex here", "pin this topic to Fable", or "set high thinking on this topic", that IS the request — propose the change back in plain words, confirm, and the pin is durable from then on. NEVER instruct the user to type \`/topic\`; the \`/topic\` command exists only as a power-user convenience.
@@ -0,0 +1,80 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The `IntelligenceRouter` failure-swap now covers NON-gating internal calls, fixing a class where a
9
+ non-gating background component hard-errored instead of trying a healthy fallback door.
10
+
11
+ Background: internal components run off Claude by default (Codex → Pi → Gemini → Claude). When a
12
+ GATING call's primary provider fails at runtime it already swaps down that chain. NON-gating calls
13
+ did not — they re-threw straight to the caller's heuristic. In production, `TopicIntentExtractor`
14
+ (non-gating, routed to codex-cli/gpt-5.4-mini) showed a 28% error rate (122/428 over 7 days), every
15
+ error row zero-usage — i.e. the codex `exec` invocation itself failed/timed out/returned empty. The
16
+ GATING calls beside it errored ~1.5% precisely because they swap.
17
+
18
+ The fix extends the swap to non-gating calls with a TIGHTER bound than gating calls get:
19
+ - It fires ONLY on an INVOCATION-level primary failure (the primary threw AND produced zero tokens).
20
+ A content/parse error that carried tokens does NOT swap (the caller fail-opens that, per
21
+ provider-fallback §6.4). The router composes an `onUsage` capture onto the primary attempt to
22
+ observe token production; a provider that never surfaces usage (gemini) is treated as
23
+ invocation-level (the conservative, error-reducing direction).
24
+ - At most `maxAttempts` (default 1) steps down the active `failureSwap` tail, each target
25
+ circuit-checked and bounded by the existing `intelligence.swapAttemptTimeoutMs` per-attempt cap
26
+ (also flowed through as the provider's `timeoutMs`).
27
+ - NEVER onto `claude-code` or the default framework — the provider-fallback §6.2 invariant that
28
+ non-gating background traffic must never herd onto the last-resort Claude tail. If the only
29
+ remaining tail entry is claude-code, the call re-throws to its heuristic (today's behavior).
30
+ - Metrics honesty is automatic: each provider's own CircuitBreaking wrapper records its own
31
+ feature_metrics row (the failed codex primary keeps its zero-usage error row; the pi swap records
32
+ pi's usage/model). `usageCoverage` is unaffected.
33
+
34
+ New config: `intelligence.nonGatingFailureSwap: { enabled?: boolean; maxAttempts?: number }`,
35
+ inline-defaulted at the router construction site (`enabled` default true; the codexExecJson /
36
+ swapAttemptTimeoutMs precedent — no ConfigDefaults/migrateConfig entry). Set `enabled: false` to
37
+ restore the old hard-error behavior. Gating-call behavior, deferrable behavior,
38
+ `sessions.componentFrameworks` semantics, the spawn-cap funnel, and nature-routing are all
39
+ untouched. On a Claude-only agent (no off-Claude CLI) the whole thing is a no-op.
40
+
41
+ ## What to Tell Your User
42
+
43
+ Some of my quick background helpers — like the one that works out what a conversation topic is
44
+ about — used to just fail whenever the tool they run on had a brief hiccup, even though a healthy
45
+ backup tool was sitting right there. In real numbers, one of them was failing more than a quarter
46
+ of the time for exactly this reason. Now, when a background helper's tool fails to run, I quietly
47
+ try one backup tool before giving up, so far fewer of these little checks fall over. It never adds
48
+ a noticeable wait, it never pushes that background chatter onto your main Claude account, and it
49
+ only kicks in when the tool genuinely failed to run — not when it answered. You don't have to do
50
+ anything; it's on by default and it just makes me more reliable. If you ever want the old behavior
51
+ back, I can switch it off for you.
52
+
53
+ ## Summary of New Capabilities
54
+
55
+ - Non-gating internal calls now get a bounded, herd-safe provider swap on an invocation-level
56
+ failure, instead of hard-erroring — sharply reducing user-visible errors on components like the
57
+ topic classifier.
58
+ - New off-switch: `intelligence.nonGatingFailureSwap.enabled: false` restores the old hard-error
59
+ behavior; `maxAttempts` tunes how many tail steps a non-gating call may take (default 1).
60
+ - Proactive trigger for the agent: "why did my background classifier's error rate drop / does a
61
+ non-gating call fall back too?" → this bounded swap.
62
+
63
+ ## Evidence
64
+
65
+ Reproduction (production, 2026-07-09, from `GET /metrics/features` 7d + the overnight routing
66
+ investigation `/.instar/plans/overnight-routing-error-investigation.md`): `TopicIntentExtractor`
67
+ `byModel` (codex-cli/gpt-5.4-mini) = 434 calls, 123 errors, `errorRowsWithUsage: 0`, `fired: 0`,
68
+ 311 noop — a 28% error rate. The zero-usage on every error row is the tell: these are
69
+ invocation-level codex-exec failures (no tokens produced), not rate-limits (codex ~3% used) and not
70
+ parse errors (those carry tokens). The GATING `MessagingToneGate` errored at 1.5% and
71
+ `CoherenceReviewer` at 2.6% because they ride the failure-swap tail; the non-gating call did not.
72
+
73
+ Before: a non-gating primary invocation failure re-throws immediately → the 28% user-visible error
74
+ rate. After: the same failure swaps once onto the next active off-Claude framework (pi-cli, ~1.5%),
75
+ so the call succeeds instead of erroring. Verified by driving the exact router path end-to-end:
76
+ `tests/unit/nongating-failure-swap.test.ts` (13 — invocation-failure→swap, content-error→no-swap,
77
+ disabled/absent→old behavior, target-down→re-throw original, herd-safety never-onto-Claude while
78
+ gating still swaps, maxAttempts bound, tier preserved, slow-target abandoned at the cap),
79
+ `tests/integration/nongating-failure-swap-routing.test.ts` (3), and
80
+ `tests/e2e/nongating-failure-swap-lifecycle.test.ts` (2, real AgentServer init path, default-ON).
@@ -0,0 +1,54 @@
1
+ # Side-Effects Review — Non-Gating Failure-Swap (bounded provider swap for non-gating internal calls)
2
+
3
+ **Spec:** docs/specs/nongating-failure-swap.md (Tier-1 bug fix — bounded extension of the CONVERGED + approved `docs/specs/provider-fallback-default-policy.md`). **Parent principle:** No Silent Degradation to Brittle Fallback.
4
+ **Ships ON by default** (`intelligence.nonGatingFailureSwap.enabled`, inline-defaulted `?? true` at the router construction site — no persisted config block). No-op on a Claude-only agent (no off-Claude tail) and on any router constructed without the field (e.g. unit tests).
5
+ **Files:** src/core/IntelligenceRouter.ts, src/core/types.ts, src/commands/server.ts, src/scaffold/templates.ts, src/core/PostUpdateMigrator.ts, docs/specs/nongating-failure-swap.md (new), docs/specs/nongating-failure-swap.eli16.md (new), upgrades/next/nongating-failure-swap.md (new), upgrades/side-effects/nongating-failure-swap.md (new), tests/unit/nongating-failure-swap.test.ts (new), tests/unit/PostUpdateMigrator-nonGatingFailureSwap.test.ts (new), tests/integration/nongating-failure-swap-routing.test.ts (new), tests/e2e/nongating-failure-swap-lifecycle.test.ts (new)
6
+
7
+ ## What changed
8
+
9
+ 1. **IntelligenceRouter.ts — `IntelligenceRouterOptions`:** new optional `nonGatingFailureSwap?: { enabled: boolean; maxAttempts?: number }`. Absent ⇒ feature OFF (byte-identical legacy — a non-gating primary failure re-throws straight to the caller's heuristic).
10
+ 2. **IntelligenceRouter.ts — `evaluate()`:** after the existing `swapPositions`/`gatingDeadlineAt` computation, compute `nonGatingSwapEligible = !gating && !deferrable && !enforced && nonGatingFailureSwap.enabled === true && cfg.failureSwap.length > 0`. On the eligible path ONLY, compose an `onUsage` capture onto the primary attempt (`primaryEvalOptions`) so `primaryProducedTokens` records whether the primary produced any tokens; gating/deferrable/enforced calls use `evalOptions` verbatim (byte-identical). Inside the existing `if (swapPositions.length === 0)` branch, BEFORE the deferrable-queue + heuristic-fallthrough, if `nonGatingSwapEligible && !primaryProducedTokens` call the new `tryNonGatingSwap(...)`; on success return its result (before any heuristic-fallthrough tracking).
11
+ 3. **IntelligenceRouter.ts — new `tryNonGatingSwap()`:** attempts at most `maxAttempts` (default 1) steps down `cfg.failureSwap`, FILTERING OUT `claude-code`, the default framework, and the just-failed primary. Each target is `resolveProvider`-checked (binary-missing/circuit-open → skipped/caught) and bounded by the SAME `resolveSwapCap` + `withSwapTimeout` machinery the gating loop uses (the cap also flows through as the provider's `timeoutMs`). Emits `onDegrade` (`nongating-failure-swap:` on success, `nongating-swap-attempt-timeout:` on a cap fire) + `onResolved` on success. Returns `{ ok }`; on `{ ok:false }` the caller falls through to its existing heuristic (`onHeuristicFallthrough` + `throw err`).
12
+ 4. **types.ts:** new `intelligence.nonGatingFailureSwap?: { enabled?: boolean; maxAttempts?: number }` config field, documented as inline-defaulted (codexExecJson/swapAttemptTimeoutMs precedent — deliberately NOT in ConfigDefaults/migrateConfig).
13
+ 5. **server.ts (router construction):** wire `nonGatingFailureSwap: { enabled: config.intelligence?.nonGatingFailureSwap?.enabled ?? true, maxAttempts: config.intelligence?.nonGatingFailureSwap?.maxAttempts }` — the default-ON expression.
14
+ 6. **templates.ts + PostUpdateMigrator.ts:** a bullet under Per-Component Framework Routing (new agents) + an idempotent content-sniffed `migrateClaudeMd` corrective subsection (existing agents), marker `non-gating internal calls also get a bounded`.
15
+
16
+ ## Blast radius
17
+
18
+ - **Gating / deferrable / nature-enforced paths are untouched.** `nonGatingSwapEligible` is false for all of them, so `primaryEvalOptions === evalOptions` (no capture) and the new branch is never entered. The gating swap loop, the deferrable backoff/queue rungs, and the enforced-nature selection are byte-identical.
19
+ - **No new HTTP route, no new provider, no new spawn.** The non-gating swap reuses the existing per-framework providers (already built at boot via `buildProvider`) and the existing per-attempt cap machinery. `tryNonGatingSwap` never builds a new provider or spawns beyond what a normal swap attempt does.
20
+ - **Bounded blast on the swap itself:** at most `maxAttempts` (default 1) steps, each circuit-checked, each capped by `swapAttemptTimeoutMs` (default 5s). Worst-case added latency on a non-gating failure = `maxAttempts × cap`.
21
+ - **Off-Claude only.** `claude-code` and the default framework are FILTERED OUT of non-gating targets, so this can never push non-gating background traffic onto the last-resort Claude tail (the §6.2 herd invariant). On a Claude-only agent `cfg` is undefined / the tail is empty → strict no-op.
22
+
23
+ ## Risk + mitigation
24
+
25
+ - **Risk:** reintroduces the §6.2 herd (non-gating traffic floods a fallback under a broad rate-limit). **Mitigation:** the non-gating swap is STRICTLY more conservative than the gating swap — one step (default), circuit-checked (a target whose breaker is open throws fast → skipped), and NEVER onto Claude. Under a genuine rate-limit the target's own breaker damps repeat attempts. Proven by the herd-safety lens test (`never onto claude-code … but a GATING call does`) and the maxAttempts-bound test.
26
+ - **Risk:** swapping on a content/parse error double-spends tokens on a request that already burned some. **Mitigation:** the swap fires ONLY when the primary produced ZERO tokens (`primaryProducedTokens` false). A token-carrying failure is NOT swapped — the caller fail-opens it (§6.4). Proven by the `content/parse error that CARRIED tokens → NO swap` test.
27
+ - **Risk:** a slow fallback adds latency to a high-volume noop path. **Mitigation:** the per-attempt cap (`swapAttemptTimeoutMs`, default 5s) abandons a slow target via `withSwapTimeout` (the shipped crash-safe Promise.race form; timer cleared on settle). Proven by the `SLOW target abandoned at the cap` test.
28
+ - **Risk:** the `onUsage` capture interferes with the primary's own metrics/usage recording. **Mitigation:** the capture COMPOSES with the caller's onUsage (`callerOnUsage?.(u)`) and is downstream of the CircuitBreaking wrapper's own capture — additive, no clobber. Metrics honesty is automatic (each provider's wrapper records its own row keyed by serving framework/model); `usageCoverage` is unaffected.
29
+ - **Risk:** an error in the swap helper breaks the LLM call path. **Mitigation:** every path in `tryNonGatingSwap` ends at either a returned result or `{ ok:false }` → the caller's existing `throw err` (heuristic). It never introduces a new fail-closed and never swallows silently — the catch emits `onDegrade` on a cap fire and `continue`s (the same non-silent resilience pattern as the gating loop; not counted by the no-silent-fallbacks ratchet).
30
+
31
+ ## Migration parity
32
+
33
+ - **Config:** no `migrateConfig` needed — the knob is inline-defaulted at the construction site (`?? true`), so existing agents pick up the default-ON behavior purely from the new code shipping (the codexExecJson/swapAttemptTimeoutMs precedent). Absence ⇒ enabled default.
34
+ - **CLAUDE.md:** `generateClaudeMd` gains the bullet (new agents); `migrateClaudeMd` appends an idempotent content-sniffed corrective subsection (existing agents), marker `non-gating internal calls also get a bounded`. Covered by `tests/unit/PostUpdateMigrator-nonGatingFailureSwap.test.ts` (add-when-absent, idempotent, preserves content, skips when missing) + a template-emits-it assertion.
35
+
36
+ ## Dark-gate line-map
37
+
38
+ - UNCHANGED. `nonGatingFailureSwap` is inline-defaulted in `src/commands/server.ts` (`?? true`) and declared as an optional type in `types.ts`; it is NOT an `enabled:` line in `ConfigDefaults.ts`. The dark-gate attributor reads `ConfigDefaults.ts` only and matches `enabled:` lines, so no line shifted. Verified: `tests/unit/lint-dev-agent-dark-gate.test.ts` → green in the run batch.
39
+
40
+ ## Rollback
41
+
42
+ - Set `intelligence.nonGatingFailureSwap.enabled: false` → non-gating failures re-throw to the heuristic with no swap (today's behavior), no restart-to-rewire needed (config is read live in `resolveConfig`; the field is read at construction, so a restart is needed only if the operator wants to change it after boot — same posture as `swapAttemptTimeoutMs`). To fully revert: remove the `nonGatingFailureSwap` option + `tryNonGatingSwap` + the eligibility/capture block in `evaluate()` + the server wiring + the type + the CLAUDE.md bullet/migration. Additive throughout.
43
+
44
+ ## Tests
45
+
46
+ - `tests/unit/nongating-failure-swap.test.ts` (13) — the core behavior + both sides of every decision boundary: invocation-failure → one swap; content-error-with-usage → no swap; disabled + absent → old behavior; target down/circuit-open → skip + re-throw ORIGINAL error; gemini-primary (no usage) → conservative swap; herd-safety (never onto claude-code/default while GATING still does); gating unchanged (full-tail swap); maxAttempts=1 vs 2; model tier preserved; per-attempt cap passthrough; slow target abandoned at the cap.
47
+ - `tests/integration/nongating-failure-swap-routing.test.ts` (3) — a production-shaped router (computed default + the knob) SWAPS on a non-gating invocation failure; `GET /intelligence/routing` is unchanged (resolution, not swap); `{ enabled:false }` hard-errors.
48
+ - `tests/e2e/nongating-failure-swap-lifecycle.test.ts` (2) — real AgentServer init path: the intelligence-routing route is alive AND the wired router performs the swap via the SHIPPED default expression (config unset ⇒ enabled:true), proving the feature is alive + ON, not dark.
49
+ - `tests/unit/PostUpdateMigrator-nonGatingFailureSwap.test.ts` (5) — the migrateClaudeMd corrective (add/idempotent/preserve/skip) + template-emits-it.
50
+ - Regression: `no-silent-fallbacks`, `lint-dev-agent-dark-gate`, `provider-fallback-swap-timeout`, `per-target-swap-timeout`, `internalFrameworkDefault`, `intelligence-router`, `nature-routing-resolver`, `degradation-ladder`, `opus-claude-cli-gating-guardrail`, `provider-fallback-default-routing`, `intelligence-routing-routes/lifecycle` all green. tsc clean.
51
+
52
+ ## Agent awareness
53
+
54
+ - A "Non-gating calls also get a bounded swap now" bullet extends the Per-Component Framework Routing section in `generateClaudeMd`, and an idempotent `migrateClaudeMd` corrective subsection reaches existing agents. Proactive trigger documented: "why did my background classifier's error rate drop / does a non-gating call fall back too?".