mixdog 0.9.51 → 0.9.52
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/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +324 -3
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +320 -1
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +5 -7
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +130 -47
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +122 -63
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +6 -3
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/tui/dist/index.mjs +73 -278
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.52",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -57,7 +57,9 @@
|
|
|
57
57
|
"test:shipmode": "node --test scripts/ship-mode-test.mjs",
|
|
58
58
|
"test:shellhardening": "node --test scripts/shell-hardening-test.mjs scripts/shell-failure-diagnostics-test.mjs",
|
|
59
59
|
"test:placeholder": "node --test scripts/compacted-placeholder-scrub-test.mjs",
|
|
60
|
-
"test:providers": "node --test scripts/provider-toolcall-test.mjs",
|
|
60
|
+
"test:providers": "node --test scripts/provider-toolcall-test.mjs scripts/provider-contract-test.mjs",
|
|
61
|
+
"test:provider-admission": "node --test scripts/provider-admission-scheduler-test.mjs",
|
|
62
|
+
"test:resource-admission": "node --test scripts/resource-admission-test.mjs",
|
|
61
63
|
"test:deferred-tools": "node --test scripts/deferred-tool-loading-test.mjs",
|
|
62
64
|
"test:anthropic-oauth-race": "node --test scripts/anthropic-oauth-refresh-race-test.mjs",
|
|
63
65
|
"test:grok-oauth-race": "node --test scripts/grok-oauth-refresh-race-test.mjs",
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
"test:release-focused": "npm run test:release-assets && npm run test:patch-binary-cache && npm run test:providers && npm run test:deferred-tools && npm run smoke:compact && node --test scripts/code-graph-aggregate-cwd-test.mjs && npm run test:code-graph-dispatch && node --test scripts/code-graph-disk-hit-test.mjs && npm run test:shellhardening && node --test scripts/windows-hide-spawn-options-test.mjs && node --test scripts/tui-transcript-perf-test.mjs",
|
|
73
75
|
"test:native-edit-wire": "node --test scripts/native-edit-wire-test.mjs",
|
|
74
76
|
"test:patch-binary-cache": "node --test scripts/patch-binary-cache-test.mjs",
|
|
75
|
-
"test:session": "node --test scripts/session-orphan-sweep-test.mjs",
|
|
77
|
+
"test:session": "node --test scripts/session-orphan-sweep-test.mjs scripts/interrupted-turn-history-test.mjs",
|
|
76
78
|
"test:rebindtail": "node --test scripts/forwarder-rebind-tail-test.mjs",
|
|
77
79
|
"failures": "node scripts/tool-failures.mjs",
|
|
78
80
|
"trace:llm": "node scripts/llm-trace-summary.mjs",
|
|
@@ -20,13 +20,15 @@ function makeEngine({ abortSettles = false, recoveryMs = 30 } = {}) {
|
|
|
20
20
|
const notices = [];
|
|
21
21
|
const requeued = [];
|
|
22
22
|
const discardedCompletionKeys = [];
|
|
23
|
+
const abortReasons = [];
|
|
23
24
|
let drainCount = 0;
|
|
24
25
|
let state = { items: [], queued: [], busy: false, commandBusy: false, spinner: null, thinking: null, lastTurn: null };
|
|
25
26
|
const bag = {
|
|
26
27
|
runtime: {
|
|
27
28
|
id: null,
|
|
28
29
|
consumePendingSessionReset: () => null,
|
|
29
|
-
abort: () => {
|
|
30
|
+
abort: (reason) => {
|
|
31
|
+
abortReasons.push(reason);
|
|
30
32
|
if (abortSettles) bag.set({ busy: false, spinner: null, thinking: null, lastTurn: null });
|
|
31
33
|
return true;
|
|
32
34
|
},
|
|
@@ -70,9 +72,23 @@ function makeEngine({ abortSettles = false, recoveryMs = 30 } = {}) {
|
|
|
70
72
|
getDrainCount: () => drainCount,
|
|
71
73
|
getRequeued: () => requeued,
|
|
72
74
|
getDiscardedCompletionKeys: () => discardedCompletionKeys,
|
|
75
|
+
getAbortReasons: () => abortReasons,
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
test('Esc uses Claude-compatible user-cancel and queued interrupt reasons', () => {
|
|
80
|
+
const normal = makeEngine({ abortSettles: true });
|
|
81
|
+
normal.bag.set({ busy: true });
|
|
82
|
+
normal.api.abort();
|
|
83
|
+
assert.deepEqual(normal.getAbortReasons(), ['user-cancel']);
|
|
84
|
+
|
|
85
|
+
const queued = makeEngine({ abortSettles: true });
|
|
86
|
+
queued.bag.set({ busy: true });
|
|
87
|
+
queued.bag.pending.push({ kind: 'prompt', text: 'queued redirect' });
|
|
88
|
+
queued.api.abort();
|
|
89
|
+
assert.deepEqual(queued.getAbortReasons(), ['interrupt']);
|
|
90
|
+
});
|
|
91
|
+
|
|
76
92
|
test('starved abort → bounded recovery hard-releases busy and re-kicks drain', async () => {
|
|
77
93
|
const { api, bag, getNotices, getDrainCount } = makeEngine({ abortSettles: false, recoveryMs: 25 });
|
|
78
94
|
bag.set({ busy: true, spinner: { active: true } });
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getSessionProgressSnapshot,
|
|
10
10
|
markSessionAskStart,
|
|
11
11
|
markSessionStreamDelta,
|
|
12
|
+
updateSessionStage,
|
|
12
13
|
} from '../src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs';
|
|
13
14
|
import {
|
|
14
15
|
evaluateAgentWatchdogAbort,
|
|
@@ -26,6 +27,10 @@ import { parseSSEStream } from '../src/runtime/agent/orchestrator/providers/anth
|
|
|
26
27
|
import { _streamResponse } from '../src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs';
|
|
27
28
|
import { sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs';
|
|
28
29
|
import { shouldFallbackTransport } from '../src/runtime/agent/orchestrator/providers/retry-classifier.mjs';
|
|
30
|
+
import {
|
|
31
|
+
ProviderAdmissionScheduler,
|
|
32
|
+
wrapProviderAdmission,
|
|
33
|
+
} from '../src/runtime/agent/orchestrator/providers/admission-scheduler.mjs';
|
|
29
34
|
|
|
30
35
|
const policy = {
|
|
31
36
|
firstTransportMs: 120_000,
|
|
@@ -62,6 +67,35 @@ test('independent first transport and semantic deadlines', async (t) => {
|
|
|
62
67
|
assert.equal(snapshot().hasVisibleProgress, false);
|
|
63
68
|
});
|
|
64
69
|
|
|
70
|
+
test('genuinely queued scheduler request remains outside the real watchdog clock', async (t) => {
|
|
71
|
+
const id = `liveness-admission-${Date.now()}`;
|
|
72
|
+
t.after(() => _clearSessionRuntime(id));
|
|
73
|
+
const scheduler = new ProviderAdmissionScheduler({ concurrency: 1 });
|
|
74
|
+
let releaseFirst;
|
|
75
|
+
const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
|
|
76
|
+
const provider = wrapProviderAdmission({
|
|
77
|
+
async send(_messages, _model, _tools, opts) {
|
|
78
|
+
if (opts.block) return firstGate;
|
|
79
|
+
return 'queued-ok';
|
|
80
|
+
},
|
|
81
|
+
}, 'watchdog-integration', scheduler);
|
|
82
|
+
markSessionAskStart(id);
|
|
83
|
+
const entry = _getRuntimeEntry(id);
|
|
84
|
+
entry.askStartedAt = Date.now() - 900_000;
|
|
85
|
+
entry.lastProgressAt = entry.askStartedAt;
|
|
86
|
+
const first = provider.send([], 'm', [], { block: true });
|
|
87
|
+
const second = provider.send([], 'm', [], {
|
|
88
|
+
onStageChange: (stage) => updateSessionStage(id, stage),
|
|
89
|
+
});
|
|
90
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
91
|
+
assert.equal(entry.modelRequestStartedAt, null);
|
|
92
|
+
assert.equal(evaluateAgentWatchdogAbort(getSessionProgressSnapshot(id), Date.now(), policy), null);
|
|
93
|
+
releaseFirst('first');
|
|
94
|
+
await first;
|
|
95
|
+
assert.equal(await second, 'queued-ok');
|
|
96
|
+
assert.ok(entry.modelRequestStartedAt >= Date.now() - 100);
|
|
97
|
+
});
|
|
98
|
+
|
|
65
99
|
test('status labels distinguish transport, semantic, reasoning, text, tool, and true stall', async (t) => {
|
|
66
100
|
const id = `liveness-status-${Date.now()}`;
|
|
67
101
|
t.after(() => _clearSessionRuntime(id));
|
|
@@ -417,6 +451,32 @@ function wedgedOpenAiHttpResponse() {
|
|
|
417
451
|
};
|
|
418
452
|
}
|
|
419
453
|
|
|
454
|
+
function firstEventThenWedgedOpenAiHttpResponse() {
|
|
455
|
+
const first = encoder.encode(
|
|
456
|
+
`event: response.created\ndata: ${JSON.stringify({
|
|
457
|
+
type: 'response.created',
|
|
458
|
+
response: { id: 'r1', model: 'gpt' },
|
|
459
|
+
})}\n\n`,
|
|
460
|
+
);
|
|
461
|
+
let emitted = false;
|
|
462
|
+
return {
|
|
463
|
+
ok: true,
|
|
464
|
+
status: 200,
|
|
465
|
+
headers: new Map(),
|
|
466
|
+
body: { getReader: () => ({
|
|
467
|
+
read: () => {
|
|
468
|
+
if (!emitted) {
|
|
469
|
+
emitted = true;
|
|
470
|
+
return Promise.resolve({ done: false, value: first });
|
|
471
|
+
}
|
|
472
|
+
return new Promise(() => {});
|
|
473
|
+
},
|
|
474
|
+
cancel: () => new Promise(() => {}),
|
|
475
|
+
releaseLock: () => {},
|
|
476
|
+
}) },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
420
480
|
test('OAuth HTTP SSE actively rejects a wedged reader on external and semantic aborts', async () => {
|
|
421
481
|
const keepAlive = setInterval(() => {}, 10);
|
|
422
482
|
try {
|
|
@@ -429,7 +489,7 @@ test('OAuth HTTP SSE actively rejects a wedged reader on external and semantic a
|
|
|
429
489
|
opts: {},
|
|
430
490
|
externalSignal: external.signal,
|
|
431
491
|
useModel: 'gpt',
|
|
432
|
-
fetchFn: async () =>
|
|
492
|
+
fetchFn: async () => firstEventThenWedgedOpenAiHttpResponse(),
|
|
433
493
|
});
|
|
434
494
|
setTimeout(() => external.abort(externalReason), 10);
|
|
435
495
|
await assert.rejects(externalRun, (err) => err === externalReason);
|
|
@@ -439,7 +499,7 @@ test('OAuth HTTP SSE actively rejects a wedged reader on external and semantic a
|
|
|
439
499
|
body: {},
|
|
440
500
|
opts: { _semanticIdleTimeoutMs: 25 },
|
|
441
501
|
useModel: 'gpt',
|
|
442
|
-
fetchFn: async () =>
|
|
502
|
+
fetchFn: async () => firstEventThenWedgedOpenAiHttpResponse(),
|
|
443
503
|
});
|
|
444
504
|
await assert.rejects(semanticRun, (err) => (
|
|
445
505
|
err?.name === 'StreamStalledError'
|
|
@@ -451,6 +511,23 @@ test('OAuth HTTP SSE actively rejects a wedged reader on external and semantic a
|
|
|
451
511
|
}
|
|
452
512
|
});
|
|
453
513
|
|
|
514
|
+
test('OAuth HTTP SSE enforces first server event independently of response headers', async () => {
|
|
515
|
+
const keepAlive = setInterval(() => {}, 10);
|
|
516
|
+
try {
|
|
517
|
+
const startedAt = Date.now();
|
|
518
|
+
await assert.rejects(sendViaHttpSse({
|
|
519
|
+
auth: { type: 'openai-direct', apiKey: 'test' },
|
|
520
|
+
body: {},
|
|
521
|
+
opts: { _firstServerEventTimeoutMs: 40, _semanticIdleTimeoutMs: 5 },
|
|
522
|
+
useModel: 'gpt',
|
|
523
|
+
fetchFn: async () => wedgedOpenAiHttpResponse(),
|
|
524
|
+
}), (err) => err?.code === 'EPROVIDERTIMEOUT' && err?.firstByteTimeout === true);
|
|
525
|
+
assert.ok(Date.now() - startedAt >= 30, 'semantic idle must not shorten the first-event deadline');
|
|
526
|
+
} finally {
|
|
527
|
+
clearInterval(keepAlive);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
454
531
|
test('completion bundles report actual reasoning/text/tool kinds across compat, WS, and OAuth SSE', async () => {
|
|
455
532
|
const output = [
|
|
456
533
|
{ type: 'reasoning', id: 'rs_1', encrypted_content: 'opaque', summary: [] },
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { AnthropicProvider } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
|
|
5
|
+
import {
|
|
6
|
+
ProviderAdmissionScheduler,
|
|
7
|
+
wrapProviderAdmission,
|
|
8
|
+
} from '../src/runtime/agent/orchestrator/providers/admission-scheduler.mjs';
|
|
9
|
+
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
|
|
12
|
+
function sseResponse(events, headers = new Map()) {
|
|
13
|
+
const bytes = encoder.encode(events.map((event) => (
|
|
14
|
+
`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`
|
|
15
|
+
)).join(''));
|
|
16
|
+
let sent = false;
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
status: 200,
|
|
20
|
+
headers,
|
|
21
|
+
body: { getReader: () => ({
|
|
22
|
+
async read() {
|
|
23
|
+
if (sent) return { done: true };
|
|
24
|
+
sent = true;
|
|
25
|
+
return { done: false, value: bytes };
|
|
26
|
+
},
|
|
27
|
+
async cancel() {},
|
|
28
|
+
releaseLock() {},
|
|
29
|
+
}) },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('real wrapped Anthropic retries report HTTP and SSE 429 before eventual success', async () => {
|
|
34
|
+
let now = 10_000;
|
|
35
|
+
const scheduled = [];
|
|
36
|
+
const scheduler = new ProviderAdmissionScheduler({
|
|
37
|
+
concurrency: 4,
|
|
38
|
+
now: () => now,
|
|
39
|
+
setTimer(fn, delay) {
|
|
40
|
+
const timer = { fn, delay };
|
|
41
|
+
scheduled.push(timer);
|
|
42
|
+
return timer;
|
|
43
|
+
},
|
|
44
|
+
clearTimer() {},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const provider = Object.create(AnthropicProvider.prototype);
|
|
48
|
+
provider.name = 'anthropic';
|
|
49
|
+
provider.config = { apiKey: 'fixture-key' };
|
|
50
|
+
provider.fastModeBetaHeaderLatched = false;
|
|
51
|
+
let attempts = 0;
|
|
52
|
+
provider.client = { messages: { create() {
|
|
53
|
+
attempts += 1;
|
|
54
|
+
return { async asResponse() {
|
|
55
|
+
if (attempts === 1) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
status: 429,
|
|
59
|
+
headers: new Map([['retry-after-ms', '7']]),
|
|
60
|
+
async text() { return 'limited'; },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (attempts === 2) {
|
|
64
|
+
return sseResponse([
|
|
65
|
+
{ type: 'error', error: { type: 'rate_limit_error', message: 'rate limit' } },
|
|
66
|
+
], new Map([['retry-after-ms', '11']]));
|
|
67
|
+
}
|
|
68
|
+
return sseResponse([
|
|
69
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
70
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'text' } },
|
|
71
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'ok' } },
|
|
72
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
73
|
+
{ type: 'message_stop' },
|
|
74
|
+
]);
|
|
75
|
+
} };
|
|
76
|
+
} } };
|
|
77
|
+
|
|
78
|
+
wrapProviderAdmission(provider, 'anthropic', scheduler);
|
|
79
|
+
const result = await provider.send([], 'claude-sonnet-4-5', [], {});
|
|
80
|
+
assert.equal(result.content, 'ok');
|
|
81
|
+
assert.equal(attempts, 3);
|
|
82
|
+
|
|
83
|
+
const lane = [...scheduler.lanes.values()][0];
|
|
84
|
+
assert.equal(lane.limit, 1, 'each internally recovered 429 adapts the lane');
|
|
85
|
+
assert.equal(lane.cooldownUntil, now + 11, 'full SSE Retry-After reaches admission');
|
|
86
|
+
assert.ok(scheduled.some((timer) => timer.delay === 7));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('terminal Anthropic 429 is adapted once by admission, not provider retry notification', async () => {
|
|
90
|
+
const previous = process.env.CLAUDE_CODE_MAX_RETRIES;
|
|
91
|
+
process.env.CLAUDE_CODE_MAX_RETRIES = '0';
|
|
92
|
+
try {
|
|
93
|
+
const scheduler = new ProviderAdmissionScheduler({
|
|
94
|
+
concurrency: 4,
|
|
95
|
+
setTimer() { return { unref() {} }; },
|
|
96
|
+
clearTimer() {},
|
|
97
|
+
});
|
|
98
|
+
const provider = Object.create(AnthropicProvider.prototype);
|
|
99
|
+
provider.name = 'anthropic';
|
|
100
|
+
provider.config = { apiKey: 'fixture-key' };
|
|
101
|
+
provider.fastModeBetaHeaderLatched = false;
|
|
102
|
+
provider.client = { messages: { create() {
|
|
103
|
+
return { async asResponse() {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
status: 429,
|
|
107
|
+
headers: new Map([['retry-after-ms', '20']]),
|
|
108
|
+
async text() { return 'terminal'; },
|
|
109
|
+
};
|
|
110
|
+
} };
|
|
111
|
+
} } };
|
|
112
|
+
wrapProviderAdmission(provider, 'anthropic', scheduler);
|
|
113
|
+
await assert.rejects(provider.send([], 'claude-sonnet-4-5', [], {}), (err) => err?.status === 429);
|
|
114
|
+
assert.equal([...scheduler.lanes.values()][0]?.limit, 2);
|
|
115
|
+
} finally {
|
|
116
|
+
if (previous == null) delete process.env.CLAUDE_CODE_MAX_RETRIES;
|
|
117
|
+
else process.env.CLAUDE_CODE_MAX_RETRIES = previous;
|
|
118
|
+
}
|
|
119
|
+
});
|