gsd-pi 2.81.0-dev.3cddbbba2 → 2.81.0-dev.72a81bdf3
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.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +100 -95
- package/dist/resources/extensions/gsd/auto-recovery.js +6 -181
- package/dist/resources/extensions/gsd/auto.js +6 -3
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +2 -5
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +9 -0
- package/dist/resources/extensions/gsd/gsd-db.js +7 -23
- package/dist/resources/extensions/gsd/markdown-renderer.js +0 -95
- package/dist/resources/extensions/gsd/recovery-classification.js +15 -1
- package/dist/resources/extensions/gsd/session-lock.js +40 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +131 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +247 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +50 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +87 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +50 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +124 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +32 -0
- package/dist/resources/extensions/gsd/state-reconciliation/errors.js +41 -0
- package/dist/resources/extensions/gsd/state-reconciliation/index.js +99 -0
- package/dist/resources/extensions/gsd/state-reconciliation/registry.js +24 -0
- package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +43 -0
- package/dist/resources/extensions/gsd/state-reconciliation/types.js +3 -0
- package/dist/resources/extensions/gsd/state-reconciliation.js +5 -26
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +25 -17
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -209
- package/src/resources/extensions/gsd/auto.ts +7 -3
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +2 -5
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +12 -0
- package/src/resources/extensions/gsd/gsd-db.ts +7 -23
- package/src/resources/extensions/gsd/markdown-renderer.ts +4 -95
- package/src/resources/extensions/gsd/recovery-classification.ts +18 -1
- package/src/resources/extensions/gsd/session-lock.ts +41 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +172 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +337 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +69 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +109 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +68 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +185 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +46 -0
- package/src/resources/extensions/gsd/state-reconciliation/errors.ts +67 -0
- package/src/resources/extensions/gsd/state-reconciliation/index.ts +142 -0
- package/src/resources/extensions/gsd/state-reconciliation/registry.ts +27 -0
- package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +60 -0
- package/src/resources/extensions/gsd/state-reconciliation/types.ts +83 -0
- package/src/resources/extensions/gsd/state-reconciliation.ts +21 -53
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +81 -10
- package/src/resources/extensions/gsd/tests/integration/integration-proof.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +952 -0
- /package/dist/web/standalone/.next/static/{F5x9E6H9k_52fjqyql93y → rIkMv4YSNlfSeqmGqWVns}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{F5x9E6H9k_52fjqyql93y → rIkMv4YSNlfSeqmGqWVns}/_ssgManifest.js +0 -0
|
@@ -1,57 +1,25 @@
|
|
|
1
1
|
// Project/App: GSD-2
|
|
2
|
-
// File Purpose: ADR-
|
|
2
|
+
// File Purpose: ADR-017 State Reconciliation Module — public entry point.
|
|
3
|
+
// Re-exports the drift-driven implementation from the state-reconciliation/
|
|
4
|
+
// folder so existing import paths (./state-reconciliation.js) keep working.
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
export {
|
|
7
|
+
reconcileBeforeDispatch,
|
|
8
|
+
ReconciliationFailedError,
|
|
9
|
+
DRIFT_REGISTRY,
|
|
10
|
+
} from "./state-reconciliation/index.js";
|
|
6
11
|
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
ok: false;
|
|
16
|
-
reason: string;
|
|
17
|
-
stateSnapshot?: GSDState;
|
|
18
|
-
repaired: readonly string[];
|
|
19
|
-
blockers: readonly string[];
|
|
20
|
-
};
|
|
12
|
+
export type {
|
|
13
|
+
DriftContext,
|
|
14
|
+
DriftHandler,
|
|
15
|
+
DriftRecord,
|
|
16
|
+
ReconciliationDeps,
|
|
17
|
+
ReconciliationFailureDetail,
|
|
18
|
+
ReconciliationResult,
|
|
19
|
+
} from "./state-reconciliation/index.js";
|
|
21
20
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const defaultDeps: StateReconciliationDeps = {
|
|
28
|
-
invalidateStateCache,
|
|
29
|
-
deriveState,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export async function reconcileBeforeDispatch(
|
|
33
|
-
basePath: string,
|
|
34
|
-
deps: StateReconciliationDeps = defaultDeps,
|
|
35
|
-
opts?: DeriveStateOptions,
|
|
36
|
-
): Promise<StateReconciliationResult> {
|
|
37
|
-
deps.invalidateStateCache();
|
|
38
|
-
const stateSnapshot = await deps.deriveState(basePath, opts);
|
|
39
|
-
const blockers = stateSnapshot.blockers ?? [];
|
|
40
|
-
|
|
41
|
-
if (blockers.length > 0 || stateSnapshot.phase === "blocked") {
|
|
42
|
-
return {
|
|
43
|
-
ok: false,
|
|
44
|
-
reason: blockers[0] ?? `State reconciliation blocked in phase ${stateSnapshot.phase}`,
|
|
45
|
-
stateSnapshot,
|
|
46
|
-
repaired: ["derive-state-cache-invalidated"],
|
|
47
|
-
blockers,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
ok: true,
|
|
53
|
-
stateSnapshot,
|
|
54
|
-
repaired: ["derive-state-cache-invalidated"],
|
|
55
|
-
blockers,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
21
|
+
export { reconcileBeforeSpawn } from "./state-reconciliation/spawn-gate.js";
|
|
22
|
+
export type {
|
|
23
|
+
SpawnGateDeps,
|
|
24
|
+
SpawnGateResult,
|
|
25
|
+
} from "./state-reconciliation/spawn-gate.js";
|
|
@@ -62,7 +62,7 @@ test("#2007 bug 2: recentUnits.push is unconditional — not gated on pendingVer
|
|
|
62
62
|
);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
test("#2007 bug 2:
|
|
65
|
+
test("#2007 bug 2: pendingVerificationRetry state is available for dispatch regression coverage", () => {
|
|
66
66
|
const s = new AutoSession();
|
|
67
67
|
s.pendingVerificationRetry = {
|
|
68
68
|
unitId: "M001/S01/T01",
|
|
@@ -2144,7 +2144,7 @@ test("stuck detection: window resets recovery when deriveState returns a differe
|
|
|
2144
2144
|
);
|
|
2145
2145
|
});
|
|
2146
2146
|
|
|
2147
|
-
test("stuck detection:
|
|
2147
|
+
test("stuck detection: verification retries remain visible to the sliding window", async () => {
|
|
2148
2148
|
_resetPendingResolve();
|
|
2149
2149
|
mock.timers.enable({ apis: ["Date", "setTimeout"], now: 20_000 });
|
|
2150
2150
|
|
|
@@ -2199,8 +2199,9 @@ test("stuck detection: does not push to window during verification retry", async
|
|
|
2199
2199
|
|
|
2200
2200
|
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
2201
2201
|
|
|
2202
|
-
// Resolve agent_end for
|
|
2203
|
-
|
|
2202
|
+
// Resolve agent_end for 3 attempts. The 4th iteration should stop before
|
|
2203
|
+
// dispatch because retry dispatches stay visible to stuck detection.
|
|
2204
|
+
for (let i = 1; i <= 3; i++) {
|
|
2204
2205
|
await waitForMicrotasks(() => pi.calls.length === i, `dispatch ${i}`);
|
|
2205
2206
|
resolveAgentEnd(makeEvent());
|
|
2206
2207
|
await drainMicrotasks(100);
|
|
@@ -2209,16 +2210,14 @@ test("stuck detection: does not push to window during verification retry", async
|
|
|
2209
2210
|
|
|
2210
2211
|
await loopPromise;
|
|
2211
2212
|
|
|
2212
|
-
// Even though same unit was derived 4 times, verification retries should
|
|
2213
|
-
// not push to the sliding window, so stuck detection should not have fired
|
|
2214
2213
|
assert.ok(
|
|
2215
|
-
|
|
2216
|
-
`stuck detection should
|
|
2214
|
+
stopReason.includes("Stuck"),
|
|
2215
|
+
`stuck detection should fire during repeated verification retries, got: ${stopReason}`,
|
|
2217
2216
|
);
|
|
2218
2217
|
assert.equal(
|
|
2219
2218
|
verifyCallCount,
|
|
2220
|
-
|
|
2221
|
-
"verification should
|
|
2219
|
+
3,
|
|
2220
|
+
"verification should stop before a 4th repeated retry dispatch",
|
|
2222
2221
|
);
|
|
2223
2222
|
} finally {
|
|
2224
2223
|
mock.timers.reset();
|
|
@@ -2304,7 +2303,8 @@ test("detectStuck: truncates long error strings", () => {
|
|
|
2304
2303
|
{ key: "A", error: longError },
|
|
2305
2304
|
]);
|
|
2306
2305
|
assert.ok(result?.stuck);
|
|
2307
|
-
assert.ok(result!.reason.
|
|
2306
|
+
assert.ok(result!.reason.includes(longError.slice(0, 200)), "reason should include the truncated error prefix");
|
|
2307
|
+
assert.equal(result!.reason.includes(longError), false, "reason should not include the full long error");
|
|
2308
2308
|
});
|
|
2309
2309
|
|
|
2310
2310
|
// NOTE: the "stuck-detected" / "stuck-counter-reset" debug-log grep was
|
|
@@ -3207,6 +3207,77 @@ test("dispatch Worktree Safety wins before stuck detection for execute-task with
|
|
|
3207
3207
|
);
|
|
3208
3208
|
});
|
|
3209
3209
|
|
|
3210
|
+
test("runDispatch runs stuck detection while artifact verification retry is pending (#5719)", async (t) => {
|
|
3211
|
+
_resetPendingResolve();
|
|
3212
|
+
|
|
3213
|
+
const ctx = makeMockCtx();
|
|
3214
|
+
const pi = makeMockPi();
|
|
3215
|
+
const notifications: string[] = [];
|
|
3216
|
+
ctx.ui.notify = (msg: string) => { notifications.push(msg); };
|
|
3217
|
+
|
|
3218
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-5719-retry-stuck-"));
|
|
3219
|
+
t.after(() => rmSync(basePath, { recursive: true, force: true }));
|
|
3220
|
+
|
|
3221
|
+
const s = makeLoopSession({
|
|
3222
|
+
basePath,
|
|
3223
|
+
pendingVerificationRetry: {
|
|
3224
|
+
unitId: "M001/S01/T01",
|
|
3225
|
+
failureContext: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
|
|
3226
|
+
attempt: 1,
|
|
3227
|
+
},
|
|
3228
|
+
});
|
|
3229
|
+
const deps = makeMockDeps();
|
|
3230
|
+
const loopState = {
|
|
3231
|
+
recentUnits: [
|
|
3232
|
+
{
|
|
3233
|
+
key: "execute-task/M001/S01/T01",
|
|
3234
|
+
error: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
|
|
3235
|
+
},
|
|
3236
|
+
{ key: "plan-slice/M001/S02", error: "other failure" },
|
|
3237
|
+
{
|
|
3238
|
+
key: "complete-slice/M001/S01",
|
|
3239
|
+
error: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
|
|
3240
|
+
},
|
|
3241
|
+
],
|
|
3242
|
+
stuckRecoveryAttempts: 0,
|
|
3243
|
+
consecutiveFinalizeTimeouts: 0,
|
|
3244
|
+
};
|
|
3245
|
+
|
|
3246
|
+
const result = await runDispatch(
|
|
3247
|
+
{
|
|
3248
|
+
ctx,
|
|
3249
|
+
pi,
|
|
3250
|
+
s,
|
|
3251
|
+
deps,
|
|
3252
|
+
prefs: undefined,
|
|
3253
|
+
iteration: 1,
|
|
3254
|
+
flowId: "test-flow",
|
|
3255
|
+
nextSeq: () => 1,
|
|
3256
|
+
},
|
|
3257
|
+
{
|
|
3258
|
+
state: {
|
|
3259
|
+
phase: "executing",
|
|
3260
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
3261
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
3262
|
+
activeTask: { id: "T01" },
|
|
3263
|
+
registry: [{ id: "M001", status: "active" }],
|
|
3264
|
+
blockers: [],
|
|
3265
|
+
} as any,
|
|
3266
|
+
mid: "M001",
|
|
3267
|
+
midTitle: "Test",
|
|
3268
|
+
},
|
|
3269
|
+
loopState,
|
|
3270
|
+
);
|
|
3271
|
+
|
|
3272
|
+
assert.equal(result.action, "next", "level-1 stuck recovery should still allow the recovery dispatch");
|
|
3273
|
+
assert.equal(loopState.stuckRecoveryAttempts, 1, "stuck recovery should record the first recovery attempt");
|
|
3274
|
+
assert.ok(deps.callLog.includes("invalidateAllCaches"), "stuck recovery should invalidate caches");
|
|
3275
|
+
assert.ok(
|
|
3276
|
+
notifications.some((n) => n.includes("Missing file referenced twice")),
|
|
3277
|
+
"notification should surface the repeated ENOENT stuck reason",
|
|
3278
|
+
);
|
|
3279
|
+
});
|
|
3280
|
+
|
|
3210
3281
|
test("dispatch Worktree Safety stops unknown unit types with missing Tool Contract", async (t) => {
|
|
3211
3282
|
_resetPendingResolve();
|
|
3212
3283
|
|
|
@@ -62,8 +62,8 @@ import {
|
|
|
62
62
|
renderRoadmapCheckboxes,
|
|
63
63
|
renderAllFromDb,
|
|
64
64
|
detectStaleRenders,
|
|
65
|
-
repairStaleRenders,
|
|
66
65
|
} from "../../markdown-renderer.ts";
|
|
66
|
+
import { repairStaleRenders } from "../../state-reconciliation/drift/stale-render.ts";
|
|
67
67
|
|
|
68
68
|
// ── State derivation ──────────────────────────────────────────────────────
|
|
69
69
|
import {
|
|
@@ -24,8 +24,8 @@ import {
|
|
|
24
24
|
renderPlanFromDb,
|
|
25
25
|
renderTaskPlanFromDb,
|
|
26
26
|
detectStaleRenders,
|
|
27
|
-
repairStaleRenders,
|
|
28
27
|
} from '../markdown-renderer.ts';
|
|
28
|
+
import { repairStaleRenders } from '../state-reconciliation/drift/stale-render.ts';
|
|
29
29
|
import {
|
|
30
30
|
parseRoadmap,
|
|
31
31
|
parsePlan,
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
insertMilestone,
|
|
15
15
|
insertSlice,
|
|
16
16
|
setSliceSketchFlag,
|
|
17
|
-
autoHealSketchFlags,
|
|
18
17
|
getSlice,
|
|
19
18
|
} from "../gsd-db.ts";
|
|
19
|
+
import { autoHealSketchFlags } from "../state-reconciliation/drift/sketch-flag.ts";
|
|
20
20
|
import { deriveStateFromDb } from "../state.ts";
|
|
21
21
|
import { resolveDispatch } from "../auto-dispatch.ts";
|
|
22
22
|
import type { DispatchContext } from "../auto-dispatch.ts";
|
|
@@ -42,16 +42,19 @@ test("State Reconciliation invalidates cache and returns reconciled state", asyn
|
|
|
42
42
|
assert.equal(result.ok && result.stateSnapshot, state);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
test("State Reconciliation
|
|
45
|
+
test("State Reconciliation surfaces terminal blockers in result (ADR-017)", async () => {
|
|
46
|
+
// Under ADR-017, blockers are terminal but do not throw — they ride along
|
|
47
|
+
// in the result so the orchestrator adapter can map them to ok=false.
|
|
46
48
|
const result = await reconcileBeforeDispatch("/project", {
|
|
47
49
|
invalidateStateCache() {},
|
|
48
50
|
async deriveState() {
|
|
49
51
|
return makeState({ phase: "blocked", blockers: ["slice lock missing"] });
|
|
50
52
|
},
|
|
53
|
+
registry: [],
|
|
51
54
|
});
|
|
52
55
|
|
|
53
|
-
assert.equal(result.ok,
|
|
54
|
-
assert.
|
|
56
|
+
assert.equal(result.ok, true);
|
|
57
|
+
assert.deepEqual(result.blockers, ["slice lock missing"]);
|
|
55
58
|
});
|
|
56
59
|
|
|
57
60
|
test("Tool Contract compiles known Unit prompt and tool policy", () => {
|
|
@@ -188,6 +188,30 @@ test("empty-content aborted during session-switch is silently ignored", () => {
|
|
|
188
188
|
assert.equal(cancelledWith, null);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
test("completed assistant content with aborted stopReason during session-switch is ignored", () => {
|
|
192
|
+
// newSession() can abort the just-finished provider stream while the last
|
|
193
|
+
// assistant message still carries the completed unit summary. That is a
|
|
194
|
+
// session-transition artifact, not a cancellation for the next unit.
|
|
195
|
+
let cancelledWith: unknown = null;
|
|
196
|
+
const resolveCancelled = (ctx: ErrorContext) => {
|
|
197
|
+
cancelledWith = ctx;
|
|
198
|
+
return true;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
_handleSessionSwitchAgentEnd(
|
|
202
|
+
{
|
|
203
|
+
stopReason: "aborted",
|
|
204
|
+
content: [{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: "Implemented T01 and verified the slice task is complete.",
|
|
207
|
+
}],
|
|
208
|
+
},
|
|
209
|
+
resolveCancelled,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
assert.equal(cancelledWith, null);
|
|
213
|
+
});
|
|
214
|
+
|
|
191
215
|
test("non-abort errors during session-switch are not propagated through this helper", () => {
|
|
192
216
|
// Real provider errors (rate-limit, network, unsupported-model) are handled
|
|
193
217
|
// by the post-switch retry pipeline — not by the in-flight switch handler.
|