mixdog 0.9.45 → 0.9.47
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 -2
- package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
- package/scripts/agent-model-liveness-test.mjs +618 -0
- package/scripts/agent-trace-io-test.mjs +68 -4
- package/scripts/bench-run.mjs +30 -3
- package/scripts/compact-pressure-test.mjs +187 -4
- package/scripts/compact-prior-context-flatten-test.mjs +252 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -6
- package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
- package/scripts/context-mcp-metering-test.mjs +75 -0
- package/scripts/explore-prompt-policy-test.mjs +15 -16
- package/scripts/explore-timeout-cancel-test.mjs +345 -0
- package/scripts/headless-pristine-execution-test.mjs +614 -0
- package/scripts/internal-comms-smoke.mjs +15 -6
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/memory-core-input-test.mjs +137 -0
- package/scripts/memory-rule-contract-test.mjs +5 -5
- package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
- package/scripts/parent-abort-link-test.mjs +22 -0
- package/scripts/provider-toolcall-test.mjs +22 -0
- package/scripts/reactive-compact-persist-smoke.mjs +8 -4
- package/scripts/session-bench.mjs +3 -70
- package/scripts/task-bench.mjs +0 -2
- package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
- package/scripts/tool-smoke.mjs +21 -21
- package/scripts/tool-tui-presentation-test.mjs +68 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
- package/src/app.mjs +28 -103
- package/src/cli.mjs +17 -13
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/headless-command.mjs +139 -0
- package/src/headless-role.mjs +121 -10
- package/src/help.mjs +4 -1
- package/src/rules/agent/00-common.md +3 -3
- package/src/rules/agent/00-core.md +8 -9
- package/src/rules/agent/20-skip-protocol.md +2 -3
- package/src/rules/agent/30-explorer.md +50 -56
- package/src/rules/agent/40-cycle1-agent.md +10 -12
- package/src/rules/agent/41-cycle2-agent.md +12 -9
- package/src/rules/agent/42-cycle3-agent.md +4 -6
- package/src/rules/lead/01-general.md +5 -6
- package/src/rules/lead/02-channels.md +1 -1
- package/src/rules/lead/lead-brief.md +14 -17
- package/src/rules/lead/lead-tool.md +3 -3
- package/src/rules/shared/01-tool.md +41 -43
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
- package/src/runtime/agent/orchestrator/config.mjs +96 -30
- package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/runtime/memory/index.mjs +0 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
- package/src/runtime/memory/lib/http-router.mjs +0 -193
- package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +5 -6
- package/src/runtime/shared/config.mjs +11 -34
- package/src/runtime/shared/pristine-execution-contract.json +75 -0
- package/src/runtime/shared/pristine-execution.mjs +356 -0
- package/src/runtime/shared/provider-api-key.mjs +43 -0
- package/src/runtime/shared/provider-auth-binding.mjs +21 -0
- package/src/session-runtime/context-status.mjs +61 -13
- package/src/session-runtime/mcp-glue.mjs +29 -2
- package/src/session-runtime/plugin-mcp.mjs +7 -0
- package/src/session-runtime/resource-api.mjs +38 -5
- package/src/session-runtime/runtime-core.mjs +5 -1
- package/src/session-runtime/session-turn-api.mjs +14 -2
- package/src/session-runtime/settings-api.mjs +5 -0
- package/src/session-runtime/tool-catalog.mjs +13 -2
- package/src/session-runtime/tool-defs.mjs +1 -3
- package/src/standalone/agent-task-status.mjs +50 -11
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/explore-tool.mjs +257 -49
- package/src/standalone/seeds.mjs +1 -0
- package/src/tui/App.jsx +23 -10
- package/src/tui/app/use-transcript-scroll.mjs +4 -3
- package/src/tui/app/use-transcript-window.mjs +12 -21
- package/src/tui/components/ContextPanel.jsx +19 -25
- package/src/tui/dist/index.mjs +77 -65
- package/src/tui/engine/agent-envelope.mjs +16 -5
- package/src/tui/engine/labels.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +21 -51
- package/src/workflows/solo/WORKFLOW.md +12 -17
- package/src/workflows/solo-review/WORKFLOW.md +0 -47
|
@@ -252,6 +252,7 @@ export async function sendViaHttpSse({
|
|
|
252
252
|
throw new Error('OpenAI OAuth HTTP fallback returned no response body');
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
try { onStreamDelta?.('transport'); } catch {}
|
|
255
256
|
try { onStageChange?.('streaming'); } catch {}
|
|
256
257
|
const sseStartedAt = Date.now();
|
|
257
258
|
const reader = response.body.getReader();
|
|
@@ -266,6 +267,13 @@ export async function sendViaHttpSse({
|
|
|
266
267
|
// partial response surface as success — so record the abort reason and
|
|
267
268
|
// re-throw it after the loop unblocks (see below).
|
|
268
269
|
let _streamAbortReason = null;
|
|
270
|
+
let _pendingReadReject = null;
|
|
271
|
+
const _rejectPendingRead = (err) => {
|
|
272
|
+
if (!_pendingReadReject) return;
|
|
273
|
+
const reject = _pendingReadReject;
|
|
274
|
+
_pendingReadReject = null;
|
|
275
|
+
reject(err);
|
|
276
|
+
};
|
|
269
277
|
let _onTotalAbort = null;
|
|
270
278
|
if (totalTimeout.signal) {
|
|
271
279
|
_onTotalAbort = () => {
|
|
@@ -274,6 +282,7 @@ export async function sendViaHttpSse({
|
|
|
274
282
|
? reason
|
|
275
283
|
: new Error('OpenAI OAuth HTTP fallback aborted');
|
|
276
284
|
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
285
|
+
_rejectPendingRead(_streamAbortReason);
|
|
277
286
|
};
|
|
278
287
|
if (totalTimeout.signal.aborted) _onTotalAbort();
|
|
279
288
|
else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
|
|
@@ -286,11 +295,17 @@ export async function sendViaHttpSse({
|
|
|
286
295
|
const _clearSemanticIdle = () => {
|
|
287
296
|
if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
|
|
288
297
|
};
|
|
298
|
+
const semanticIdleOverrideMs = Number(opts?._semanticIdleTimeoutMs);
|
|
299
|
+
const semanticIdleMs = Number.isFinite(semanticIdleOverrideMs) && semanticIdleOverrideMs > 0
|
|
300
|
+
? semanticIdleOverrideMs
|
|
301
|
+
: PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
302
|
+
const semanticIdleEnabled = (Number.isFinite(semanticIdleOverrideMs) && semanticIdleOverrideMs > 0)
|
|
303
|
+
|| PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
|
|
289
304
|
const _armSemanticIdle = () => {
|
|
290
|
-
if (!
|
|
305
|
+
if (!semanticIdleEnabled || !(semanticIdleMs > 0)) return;
|
|
291
306
|
_clearSemanticIdle();
|
|
292
307
|
_semanticIdleTimer = setTimeout(() => {
|
|
293
|
-
_streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback',
|
|
308
|
+
_streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', semanticIdleMs, { emittedToolCall: emittedToolCallIds.size > 0 });
|
|
294
309
|
// Partial-final recovery: attach the
|
|
295
310
|
// streamed partial state so the agent loop can accept a wedged FINAL
|
|
296
311
|
// no-tool summary as a successful partial-final instead of dropping
|
|
@@ -310,7 +325,8 @@ export async function sendViaHttpSse({
|
|
|
310
325
|
_streamAbortReason.partialModel = model || undefined;
|
|
311
326
|
} catch { /* best-effort enrichment */ }
|
|
312
327
|
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
313
|
-
|
|
328
|
+
_rejectPendingRead(_streamAbortReason);
|
|
329
|
+
}, semanticIdleMs);
|
|
314
330
|
try { _semanticIdleTimer.unref?.(); } catch {}
|
|
315
331
|
};
|
|
316
332
|
let buffer = '';
|
|
@@ -404,6 +420,7 @@ export async function sendViaHttpSse({
|
|
|
404
420
|
};
|
|
405
421
|
toolCalls.push(call);
|
|
406
422
|
emitToolCall(call);
|
|
423
|
+
meaningful('tool');
|
|
407
424
|
};
|
|
408
425
|
const relayLeakText = (delta) => {
|
|
409
426
|
if (!leakGuard.enabled) {
|
|
@@ -412,11 +429,13 @@ export async function sendViaHttpSse({
|
|
|
412
429
|
emittedText = true;
|
|
413
430
|
try { onTextDelta(delta); } catch {}
|
|
414
431
|
}
|
|
432
|
+
if (delta) meaningful('text');
|
|
415
433
|
return;
|
|
416
434
|
}
|
|
417
435
|
const { text, calls } = leakGuard.push(delta);
|
|
418
436
|
if (text) {
|
|
419
437
|
content += text;
|
|
438
|
+
meaningful('text');
|
|
420
439
|
if (onTextDelta) {
|
|
421
440
|
emittedText = true;
|
|
422
441
|
try { onTextDelta(text); } catch {}
|
|
@@ -482,10 +501,10 @@ export async function sendViaHttpSse({
|
|
|
482
501
|
toolCalls.push(call);
|
|
483
502
|
emitToolCall(call);
|
|
484
503
|
};
|
|
485
|
-
const meaningful = () => {
|
|
504
|
+
const meaningful = (kind = 'semantic') => {
|
|
486
505
|
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
487
506
|
_armSemanticIdle();
|
|
488
|
-
try { onStreamDelta?.(); } catch {}
|
|
507
|
+
try { onStreamDelta?.(kind); } catch {}
|
|
489
508
|
};
|
|
490
509
|
const handleEvent = (event) => {
|
|
491
510
|
if (!event || typeof event.type !== 'string') return;
|
|
@@ -493,14 +512,14 @@ export async function sendViaHttpSse({
|
|
|
493
512
|
case 'response.created':
|
|
494
513
|
if (event.response?.model) model = event.response.model;
|
|
495
514
|
if (event.response?.id) responseId = event.response.id;
|
|
515
|
+
meaningful('semantic');
|
|
496
516
|
break;
|
|
497
517
|
case 'response.output_text.delta':
|
|
498
|
-
meaningful();
|
|
499
518
|
relayLeakText(event.delta || '');
|
|
500
519
|
break;
|
|
501
520
|
case 'response.reasoning_text.delta':
|
|
502
521
|
case 'response.reasoning_summary_text.delta':
|
|
503
|
-
meaningful();
|
|
522
|
+
if (event.delta) meaningful('reasoning');
|
|
504
523
|
break;
|
|
505
524
|
case 'response.output_item.added':
|
|
506
525
|
if (event.item?.type === 'function_call') {
|
|
@@ -537,16 +556,17 @@ export async function sendViaHttpSse({
|
|
|
537
556
|
markActiveToolItem(event.item);
|
|
538
557
|
_toolInFlight = true;
|
|
539
558
|
}
|
|
559
|
+
meaningful(_toolInFlight ? 'tool' : 'semantic');
|
|
540
560
|
break;
|
|
541
561
|
case 'response.function_call_arguments.delta':
|
|
542
562
|
markActiveToolItem(null, event.item_id);
|
|
543
563
|
_toolInFlight = true;
|
|
544
|
-
meaningful();
|
|
564
|
+
meaningful('tool');
|
|
545
565
|
break;
|
|
546
566
|
case 'response.function_call_arguments.done': {
|
|
547
567
|
const itemId = event.item_id || '';
|
|
548
568
|
const pending = pendingCalls.get(itemId);
|
|
549
|
-
if (pending?.kind === 'tool_search') { meaningful(); break; }
|
|
569
|
+
if (pending?.kind === 'tool_search') { meaningful('tool'); break; }
|
|
550
570
|
const call = {
|
|
551
571
|
id: pending?.callId || event.call_id || '',
|
|
552
572
|
name: pending?.name || event.name || '',
|
|
@@ -558,13 +578,13 @@ export async function sendViaHttpSse({
|
|
|
558
578
|
delete call._pendingItemId;
|
|
559
579
|
emitToolCall(call);
|
|
560
580
|
}
|
|
561
|
-
meaningful();
|
|
581
|
+
meaningful('tool');
|
|
562
582
|
break;
|
|
563
583
|
}
|
|
564
584
|
case 'response.custom_tool_call_input.delta':
|
|
565
585
|
markActiveToolItem(null, event.item_id);
|
|
566
586
|
_toolInFlight = true;
|
|
567
|
-
meaningful();
|
|
587
|
+
meaningful('tool');
|
|
568
588
|
break;
|
|
569
589
|
case 'response.output_item.done': {
|
|
570
590
|
const item = event.item || {};
|
|
@@ -597,8 +617,10 @@ export async function sendViaHttpSse({
|
|
|
597
617
|
pushCustomToolCall(item);
|
|
598
618
|
clearActiveToolItem(item, item.id || '');
|
|
599
619
|
_toolInFlight = pendingCalls.size > 0 || activeToolItems.size > 0;
|
|
600
|
-
meaningful();
|
|
601
620
|
}
|
|
621
|
+
meaningful(item.type === 'reasoning'
|
|
622
|
+
? 'reasoning'
|
|
623
|
+
: (/tool|function_call/.test(item.type || '') ? 'tool' : 'semantic'));
|
|
602
624
|
break;
|
|
603
625
|
}
|
|
604
626
|
case 'response.completed': {
|
|
@@ -615,6 +637,7 @@ export async function sendViaHttpSse({
|
|
|
615
637
|
raw: serviceTier ? { ...resp.usage, service_tier: serviceTier } : resp.usage,
|
|
616
638
|
};
|
|
617
639
|
}
|
|
640
|
+
let reportedBundleProgress = false;
|
|
618
641
|
for (const item of resp.output || []) {
|
|
619
642
|
if (item.type === 'message') {
|
|
620
643
|
for (const part of item.content || []) {
|
|
@@ -627,22 +650,38 @@ export async function sendViaHttpSse({
|
|
|
627
650
|
if (leakGuard.enabled) {
|
|
628
651
|
const { text, calls } = leakGuard.push(part.text || '', true);
|
|
629
652
|
content += text;
|
|
653
|
+
if (text) {
|
|
654
|
+
meaningful('text');
|
|
655
|
+
reportedBundleProgress = true;
|
|
656
|
+
}
|
|
630
657
|
for (const c of calls) dispatchLeakedCall(c);
|
|
658
|
+
if (calls.length) reportedBundleProgress = true;
|
|
631
659
|
} else {
|
|
632
660
|
content += part.text || '';
|
|
661
|
+
if (part.text) {
|
|
662
|
+
meaningful('text');
|
|
663
|
+
reportedBundleProgress = true;
|
|
664
|
+
}
|
|
633
665
|
}
|
|
634
666
|
}
|
|
635
667
|
if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
|
|
636
668
|
}
|
|
637
669
|
} else if (item.type === 'reasoning') {
|
|
638
670
|
pushReasoningItem(item);
|
|
671
|
+
meaningful('reasoning');
|
|
672
|
+
reportedBundleProgress = true;
|
|
639
673
|
} else if (item.type === 'web_search_call') {
|
|
640
674
|
pushWebSearchCall(item);
|
|
675
|
+
meaningful('tool');
|
|
676
|
+
reportedBundleProgress = true;
|
|
641
677
|
} else if (item.type === 'tool_search_call') {
|
|
642
678
|
pushToolSearchCall(item);
|
|
679
|
+
meaningful('tool');
|
|
680
|
+
reportedBundleProgress = true;
|
|
643
681
|
} else if (item.type === 'custom_tool_call') {
|
|
644
682
|
pushCustomToolCall(item);
|
|
645
|
-
meaningful();
|
|
683
|
+
meaningful('tool');
|
|
684
|
+
reportedBundleProgress = true;
|
|
646
685
|
} else if (item.type === 'function_call') {
|
|
647
686
|
// Match the still-pending placeholder by item id, or
|
|
648
687
|
// an already-recorded call by its canonical call_id —
|
|
@@ -667,8 +706,11 @@ export async function sendViaHttpSse({
|
|
|
667
706
|
toolCalls.push(call);
|
|
668
707
|
emitToolCall(call);
|
|
669
708
|
}
|
|
709
|
+
meaningful('tool');
|
|
710
|
+
reportedBundleProgress = true;
|
|
670
711
|
}
|
|
671
712
|
}
|
|
713
|
+
if (!reportedBundleProgress) meaningful('semantic');
|
|
672
714
|
completed = true;
|
|
673
715
|
break;
|
|
674
716
|
}
|
|
@@ -754,8 +796,13 @@ export async function sendViaHttpSse({
|
|
|
754
796
|
throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
|
|
755
797
|
}
|
|
756
798
|
if (_streamAbortReason) throw _streamAbortReason;
|
|
757
|
-
const { value, done } = await
|
|
799
|
+
const { value, done } = await new Promise((resolve, reject) => {
|
|
800
|
+
_pendingReadReject = reject;
|
|
801
|
+
reader.read().then(resolve, reject);
|
|
802
|
+
});
|
|
803
|
+
_pendingReadReject = null;
|
|
758
804
|
if (done) break;
|
|
805
|
+
try { onStreamDelta?.('transport'); } catch {}
|
|
759
806
|
buffer += decoder.decode(value, { stream: true });
|
|
760
807
|
const parsed = _sseEventsFromBuffer(buffer);
|
|
761
808
|
buffer = parsed.rest;
|
|
@@ -786,6 +833,7 @@ export async function sendViaHttpSse({
|
|
|
786
833
|
_stampToolSafety(err);
|
|
787
834
|
throw err;
|
|
788
835
|
} finally {
|
|
836
|
+
_pendingReadReject = null;
|
|
789
837
|
_clearSemanticIdle();
|
|
790
838
|
try { reader.releaseLock?.(); } catch {}
|
|
791
839
|
if (_onTotalAbort && totalTimeout.signal) {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* parsing, and tracing.
|
|
25
25
|
*/
|
|
26
26
|
import { createHash } from 'crypto';
|
|
27
|
+
import { performance } from 'node:perf_hooks';
|
|
27
28
|
import {
|
|
28
29
|
traceAgentFetch,
|
|
29
30
|
traceAgentSse,
|
|
@@ -76,6 +77,7 @@ export {
|
|
|
76
77
|
parseToolSearchArgs,
|
|
77
78
|
_streamResponse,
|
|
78
79
|
_cacheObservation as _cacheObservationForTest,
|
|
80
|
+
_cacheContinuityResetReason as _cacheContinuityResetReasonForTest,
|
|
79
81
|
_warmupContinuityTrace as _warmupContinuityTraceForTest,
|
|
80
82
|
};
|
|
81
83
|
|
|
@@ -412,7 +414,7 @@ export function _withCodexWsClientMetadata(frame, entry, enabled, context = {})
|
|
|
412
414
|
};
|
|
413
415
|
}
|
|
414
416
|
|
|
415
|
-
function _cacheObservation({ entry, result }) {
|
|
417
|
+
function _cacheObservation({ entry, result, continuityResetReason = null }) {
|
|
416
418
|
const inputTokens = _num(result?.usage?.inputTokens, 0);
|
|
417
419
|
const promptTokens = _num(result?.usage?.promptTokens, 0) || inputTokens;
|
|
418
420
|
const cachedTokens = _num(result?.usage?.cachedTokens, 0);
|
|
@@ -421,7 +423,11 @@ function _cacheObservation({ entry, result }) {
|
|
|
421
423
|
const promptThreshold = _envPositiveInt('MIXDOG_OAI_CACHE_MISS_PROMPT_TOKENS', 4096);
|
|
422
424
|
const dropRatio = _envRatio('MIXDOG_OAI_CACHE_MISS_DROP_RATIO', 0.6);
|
|
423
425
|
const dropThreshold = Math.floor(previousMaxCached * dropRatio);
|
|
424
|
-
|
|
426
|
+
// A full-frame chain break (most commonly compaction/input-prefix rewrite)
|
|
427
|
+
// starts a new prompt shape. Comparing its small new prompt against the
|
|
428
|
+
// old shape's lifetime high-water creates a "cache drop" on every following
|
|
429
|
+
// iteration until the new transcript grows past 60% of the old one.
|
|
430
|
+
const wasWarm = !continuityResetReason && previousMaxCached >= warmThreshold;
|
|
425
431
|
const cacheRatio = promptTokens > 0 ? cachedTokens / promptTokens : null;
|
|
426
432
|
const zeroMiss = wasWarm && promptTokens >= promptThreshold && cachedTokens === 0;
|
|
427
433
|
const partialDrop = wasWarm
|
|
@@ -443,6 +449,7 @@ function _cacheObservation({ entry, result }) {
|
|
|
443
449
|
dropThreshold,
|
|
444
450
|
cacheRatio,
|
|
445
451
|
actualMiss,
|
|
452
|
+
continuityResetReason,
|
|
446
453
|
missReason: zeroMiss
|
|
447
454
|
? 'warm_session_zero_cached_tokens'
|
|
448
455
|
: partialDrop
|
|
@@ -451,6 +458,34 @@ function _cacheObservation({ entry, result }) {
|
|
|
451
458
|
};
|
|
452
459
|
}
|
|
453
460
|
|
|
461
|
+
function _requestInputExtends(previousInput, currentInput) {
|
|
462
|
+
if (!Array.isArray(previousInput) || !Array.isArray(currentInput)) return false;
|
|
463
|
+
if (currentInput.length < previousInput.length) return false;
|
|
464
|
+
return previousInput.every(
|
|
465
|
+
(item, index) => _stableStringify(item) === _stableStringify(currentInput[index]),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function _cacheContinuityResetReason({ mode, deltaReason, entry, body }) {
|
|
470
|
+
if (mode === 'delta') return null;
|
|
471
|
+
if (deltaReason && !['no_anchor', 'full_forced', 'full_default'].includes(deltaReason)) {
|
|
472
|
+
return deltaReason;
|
|
473
|
+
}
|
|
474
|
+
// ws-full bypasses _computeDelta's structural comparisons and reports only
|
|
475
|
+
// full_default. Re-run the two cheap snapshot checks so compaction or any
|
|
476
|
+
// other prompt rewrite still retires the old prompt's cache high-water.
|
|
477
|
+
if (deltaReason !== 'full_default' || !entry?.lastResponseId) return null;
|
|
478
|
+
const currentSansInput = _stableStringify(_sansInput(body));
|
|
479
|
+
if (entry.lastRequestSansInput && currentSansInput !== entry.lastRequestSansInput) {
|
|
480
|
+
return 'request_properties_changed';
|
|
481
|
+
}
|
|
482
|
+
if (Array.isArray(entry.lastRequestInput)
|
|
483
|
+
&& !_requestInputExtends(entry.lastRequestInput, Array.isArray(body?.input) ? body.input : [])) {
|
|
484
|
+
return 'input_prefix_mismatch';
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
454
489
|
// Warmup→first-real continuity trace (Codex prewarm_websocket parity
|
|
455
490
|
// observability). Pure/deterministic so it unit-tests without a live socket.
|
|
456
491
|
// The R23 finding forbids the post-warmup request rewrite, so parity is
|
|
@@ -495,6 +530,7 @@ export async function _acquireWithRetry({
|
|
|
495
530
|
codexHeaders,
|
|
496
531
|
forceFresh,
|
|
497
532
|
onRetry,
|
|
533
|
+
onBackoffSlept,
|
|
498
534
|
externalSignal,
|
|
499
535
|
_acquire = acquireWebSocket,
|
|
500
536
|
_sleepFn = _defaultSleep,
|
|
@@ -560,8 +596,10 @@ export async function _acquireWithRetry({
|
|
|
560
596
|
} catch {}
|
|
561
597
|
// Sleep is abort-aware: an abort during backoff rejects immediately
|
|
562
598
|
// instead of burning the remaining wait.
|
|
563
|
-
|
|
564
|
-
|
|
599
|
+
const sleepStart = performance.now();
|
|
600
|
+
try {
|
|
601
|
+
if (externalSignal) {
|
|
602
|
+
await new Promise((resolve, reject) => {
|
|
565
603
|
const t = setTimeout(() => {
|
|
566
604
|
externalSignal.removeEventListener('abort', onAbort);
|
|
567
605
|
resolve();
|
|
@@ -573,9 +611,12 @@ export async function _acquireWithRetry({
|
|
|
573
611
|
};
|
|
574
612
|
if (externalSignal.aborted) { onAbort(); return; }
|
|
575
613
|
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
614
|
+
});
|
|
615
|
+
} else {
|
|
616
|
+
await _sleepFn(backoff);
|
|
617
|
+
}
|
|
618
|
+
} finally {
|
|
619
|
+
try { onBackoffSlept?.(performance.now() - sleepStart); } catch {}
|
|
579
620
|
}
|
|
580
621
|
}
|
|
581
622
|
}
|
|
@@ -624,6 +665,7 @@ export async function sendViaWebSocket({
|
|
|
624
665
|
_streamFn = _streamResponse,
|
|
625
666
|
_sendFrameFn = _sendFrame,
|
|
626
667
|
_sleepFn = _defaultSleep,
|
|
668
|
+
_sendSpanTraceFn = appendAgentTrace,
|
|
627
669
|
}) {
|
|
628
670
|
// Bounded mid-stream retry: if an attempt's stream dies after
|
|
629
671
|
// response.created but before response.completed from a transient cause
|
|
@@ -678,12 +720,53 @@ export async function sendViaWebSocket({
|
|
|
678
720
|
const codexHandshakeHeaders = useCodexWsClientMetadata
|
|
679
721
|
? _codexWsCompatibilityHeaders({ ...codexMetadataContext, handshake: true })
|
|
680
722
|
: null;
|
|
723
|
+
// One compact row per logical iteration. Values aggregate all handshake
|
|
724
|
+
// and mid-stream attempts, including warmup, without retaining request data.
|
|
725
|
+
const sendSpan = {
|
|
726
|
+
poolAcquireMs: 0,
|
|
727
|
+
requestBuildSerializationMs: 0,
|
|
728
|
+
preResponseCreatedMs: 0,
|
|
729
|
+
firstEventMs: 0,
|
|
730
|
+
retryBackoffMs: 0,
|
|
731
|
+
handshakeRetries: 0,
|
|
732
|
+
acquireAttempts: 0,
|
|
733
|
+
acquireMode: null,
|
|
734
|
+
emitted: false,
|
|
735
|
+
};
|
|
736
|
+
const emitSendSpan = (outcome) => {
|
|
737
|
+
if (sendSpan.emitted) return;
|
|
738
|
+
sendSpan.emitted = true;
|
|
739
|
+
const payload = {
|
|
740
|
+
provider: traceProvider,
|
|
741
|
+
model: useModel,
|
|
742
|
+
transport: 'websocket',
|
|
743
|
+
acquire_mode: sendSpan.acquireMode || 'failed',
|
|
744
|
+
acquire_attempts: sendSpan.acquireAttempts,
|
|
745
|
+
handshake_retries: sendSpan.handshakeRetries,
|
|
746
|
+
pool_acquire_ms: sendSpan.poolAcquireMs,
|
|
747
|
+
request_build_serialization_ms: sendSpan.requestBuildSerializationMs,
|
|
748
|
+
pre_response_created_ms: sendSpan.preResponseCreatedMs,
|
|
749
|
+
first_event_ms: sendSpan.firstEventMs,
|
|
750
|
+
retry_backoff_ms: sendSpan.retryBackoffMs,
|
|
751
|
+
outcome,
|
|
752
|
+
};
|
|
753
|
+
try {
|
|
754
|
+
_sendSpanTraceFn({
|
|
755
|
+
sessionId: poolKey,
|
|
756
|
+
iteration,
|
|
757
|
+
kind: 'send_spans',
|
|
758
|
+
...payload,
|
|
759
|
+
payload,
|
|
760
|
+
});
|
|
761
|
+
} catch {}
|
|
762
|
+
};
|
|
681
763
|
|
|
682
764
|
for (let attemptIndex = 0; attemptIndex <= MAX_MIDSTREAM_RETRIES; attemptIndex++) {
|
|
683
|
-
const handshakeStart =
|
|
765
|
+
const handshakeStart = performance.now();
|
|
684
766
|
let acquired;
|
|
685
767
|
let handshakeRetries = 0;
|
|
686
768
|
const handshakeRetryClassifiers = [];
|
|
769
|
+
sendSpan.acquireAttempts += 1;
|
|
687
770
|
try { onStageChange?.('requesting'); } catch {}
|
|
688
771
|
try {
|
|
689
772
|
acquired = await _acquireWithRetryFn({
|
|
@@ -703,17 +786,20 @@ export async function sendViaWebSocket({
|
|
|
703
786
|
maxAttempts: (fastFallback && attemptIndex === 0) ? 1 : HANDSHAKE_MAX_ATTEMPTS,
|
|
704
787
|
onRetry: (info) => {
|
|
705
788
|
handshakeRetries += 1;
|
|
789
|
+
sendSpan.handshakeRetries += 1;
|
|
706
790
|
if (info?.classifier) handshakeRetryClassifiers.push(info.classifier);
|
|
707
791
|
},
|
|
792
|
+
onBackoffSlept: (ms) => { sendSpan.retryBackoffMs += ms; },
|
|
708
793
|
});
|
|
709
794
|
} catch (err) {
|
|
795
|
+
sendSpan.poolAcquireMs += performance.now() - handshakeStart;
|
|
710
796
|
const classifier = err?.retryClassifier || (err?.code === 'EWSACQUIRETIMEOUT' ? 'acquire_timeout' : null);
|
|
711
797
|
const classifiers = [...handshakeRetryClassifiers];
|
|
712
798
|
if (classifier && !classifiers.includes(classifier)) classifiers.push(classifier);
|
|
713
799
|
if (err?.httpStatus != null || classifier || handshakeRetries > 0 || classifiers.length > 0) {
|
|
714
800
|
traceAgentFetch({
|
|
715
801
|
sessionId: poolKey,
|
|
716
|
-
headersMs:
|
|
802
|
+
headersMs: performance.now() - handshakeStart,
|
|
717
803
|
httpStatus: Number(err?.httpStatus || 0),
|
|
718
804
|
provider: traceProvider,
|
|
719
805
|
model: useModel,
|
|
@@ -727,11 +813,15 @@ export async function sendViaWebSocket({
|
|
|
727
813
|
// the caller's turn actually tripped on).
|
|
728
814
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
729
815
|
try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
|
|
816
|
+
emitSendSpan('error');
|
|
730
817
|
throw _stampTool(_stampLiveText(firstAttemptError));
|
|
731
818
|
}
|
|
819
|
+
emitSendSpan('error');
|
|
732
820
|
throw _stampTool(_stampLiveText(err));
|
|
733
821
|
}
|
|
734
822
|
const { entry, reused } = acquired;
|
|
823
|
+
sendSpan.poolAcquireMs += performance.now() - handshakeStart;
|
|
824
|
+
sendSpan.acquireMode = entry?.ephemeral ? 'ephemeral' : (reused ? 'reused' : 'new');
|
|
735
825
|
// Re-seed the retry attempt's fresh entry with the prior attempt's
|
|
736
826
|
// last successful anchor so _computeDelta sees a non-null
|
|
737
827
|
// lastInputPrefixHash and prev_response_id, keeping the same xAI
|
|
@@ -746,7 +836,7 @@ export async function sendViaWebSocket({
|
|
|
746
836
|
}
|
|
747
837
|
traceAgentFetch({
|
|
748
838
|
sessionId: poolKey,
|
|
749
|
-
headersMs:
|
|
839
|
+
headersMs: performance.now() - handshakeStart,
|
|
750
840
|
httpStatus: reused ? 0 : 101,
|
|
751
841
|
provider: traceProvider,
|
|
752
842
|
model: useModel,
|
|
@@ -801,6 +891,7 @@ export async function sendViaWebSocket({
|
|
|
801
891
|
// has no prior request state. A reused pooled socket with a live
|
|
802
892
|
// chain must go straight to the real request.
|
|
803
893
|
if (warmupBody && typeof warmupBody === 'object' && attemptIndex === 0 && !entry.lastResponseId) {
|
|
894
|
+
const warmupBuildStart = performance.now();
|
|
804
895
|
// Codex WS prewarm parity (opt-in): codex's prewarm frame is a
|
|
805
896
|
// minimal generate:false request that omits transport-only
|
|
806
897
|
// fields (stream/background) on the wire (prewarm_websocket,
|
|
@@ -824,7 +915,8 @@ export async function sendViaWebSocket({
|
|
|
824
915
|
const wireWarmupFrame = _withCodexWsClientMetadata(warmupFrame, entry, useCodexWsClientMetadata, warmupMetadataContext);
|
|
825
916
|
wireFrameHadTurnState = !!wireWarmupFrame?.client_metadata?.['x-codex-turn-state'];
|
|
826
917
|
wireFrameMetadataTrace = _metadataTrace(wireWarmupFrame?.client_metadata);
|
|
827
|
-
|
|
918
|
+
sendSpan.requestBuildSerializationMs += performance.now() - warmupBuildStart;
|
|
919
|
+
await _sendFrameFn(entry, wireWarmupFrame, sendSpan);
|
|
828
920
|
const warmupStart = Date.now();
|
|
829
921
|
const warmupState = {
|
|
830
922
|
attemptIndex,
|
|
@@ -835,6 +927,8 @@ export async function sendViaWebSocket({
|
|
|
835
927
|
model: useModel,
|
|
836
928
|
traceProvider,
|
|
837
929
|
warmup: true,
|
|
930
|
+
sendSpan,
|
|
931
|
+
sendStartedAt: performance.now(),
|
|
838
932
|
};
|
|
839
933
|
warmupResult = await _streamFn({
|
|
840
934
|
entry,
|
|
@@ -914,6 +1008,7 @@ export async function sendViaWebSocket({
|
|
|
914
1008
|
lastInputPrefixHash: null,
|
|
915
1009
|
}
|
|
916
1010
|
: entry;
|
|
1011
|
+
const requestBuildStart = performance.now();
|
|
917
1012
|
const delta = _computeDelta({ entry: deltaEntry, body: requestBody, traceProvider });
|
|
918
1013
|
({ mode, frame } = delta);
|
|
919
1014
|
deltaReason = delta.reason || null;
|
|
@@ -959,10 +1054,13 @@ export async function sendViaWebSocket({
|
|
|
959
1054
|
const reason = externalSignal.reason;
|
|
960
1055
|
throw reason instanceof Error ? reason : new Error('Aborted');
|
|
961
1056
|
}
|
|
962
|
-
|
|
1057
|
+
sendSpan.requestBuildSerializationMs += performance.now() - requestBuildStart;
|
|
1058
|
+
await _sendFrameFn(entry, wireFrame, sendSpan);
|
|
1059
|
+
midState.sendSpan = sendSpan;
|
|
1060
|
+
midState.sendStartedAt = performance.now();
|
|
963
1061
|
|
|
964
1062
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
965
|
-
process.stderr.write(`[agent-trace] ws-streaming-start sinceAcquire=${
|
|
1063
|
+
process.stderr.write(`[agent-trace] ws-streaming-start sinceAcquire=${Math.round(performance.now() - handshakeStart)}ms\n`);
|
|
966
1064
|
}
|
|
967
1065
|
try { onStageChange?.('streaming'); } catch {}
|
|
968
1066
|
result = await _streamFn({
|
|
@@ -1034,7 +1132,15 @@ export async function sendViaWebSocket({
|
|
|
1034
1132
|
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(line);
|
|
1035
1133
|
emittedProgress.push(line);
|
|
1036
1134
|
} catch {}
|
|
1037
|
-
|
|
1135
|
+
const sleepStart = performance.now();
|
|
1136
|
+
try {
|
|
1137
|
+
await _sleepWithAbort(backoff, externalSignal, _sleepFn);
|
|
1138
|
+
} catch (sleepErr) {
|
|
1139
|
+
sendSpan.retryBackoffMs += performance.now() - sleepStart;
|
|
1140
|
+
emitSendSpan('error');
|
|
1141
|
+
throw sleepErr;
|
|
1142
|
+
}
|
|
1143
|
+
sendSpan.retryBackoffMs += performance.now() - sleepStart;
|
|
1038
1144
|
continue;
|
|
1039
1145
|
}
|
|
1040
1146
|
// Not retryable, OR we've already exhausted the retry budget.
|
|
@@ -1056,8 +1162,10 @@ export async function sendViaWebSocket({
|
|
|
1056
1162
|
firstAttemptError.suppressed = list;
|
|
1057
1163
|
}
|
|
1058
1164
|
} catch {}
|
|
1165
|
+
emitSendSpan('error');
|
|
1059
1166
|
throw _stampTool(_stampLiveText(firstAttemptError));
|
|
1060
1167
|
}
|
|
1168
|
+
emitSendSpan('error');
|
|
1061
1169
|
throw _stampTool(_stampLiveText(err));
|
|
1062
1170
|
}
|
|
1063
1171
|
const liveModel = result.model || useModel;
|
|
@@ -1099,6 +1207,12 @@ export async function sendViaWebSocket({
|
|
|
1099
1207
|
const priorEntryResponseId = typeof entry?.lastResponseId === 'string' && entry.lastResponseId.length > 0
|
|
1100
1208
|
? entry.lastResponseId
|
|
1101
1209
|
: null;
|
|
1210
|
+
const cacheContinuityResetReason = _cacheContinuityResetReason({
|
|
1211
|
+
mode,
|
|
1212
|
+
deltaReason,
|
|
1213
|
+
entry,
|
|
1214
|
+
body: requestBody,
|
|
1215
|
+
});
|
|
1102
1216
|
if (result.responseId && keepResponseChain) {
|
|
1103
1217
|
entry.lastResponseId = result.responseId;
|
|
1104
1218
|
entry.lastRequestSansInput = _stableStringify(_sansInput(requestBody));
|
|
@@ -1124,7 +1238,11 @@ export async function sendViaWebSocket({
|
|
|
1124
1238
|
// warmup usage in first (R18) made prompt_tokens spike on it=1 and
|
|
1125
1239
|
// then "shrink" on it=2, faking prefix-rewrite/cache-drop signals in
|
|
1126
1240
|
// every warmup session (debugger 2026-07-03).
|
|
1127
|
-
const cacheObservation = _cacheObservation({
|
|
1241
|
+
const cacheObservation = _cacheObservation({
|
|
1242
|
+
entry,
|
|
1243
|
+
result,
|
|
1244
|
+
continuityResetReason: cacheContinuityResetReason,
|
|
1245
|
+
});
|
|
1128
1246
|
if (warmupResult?.usage) {
|
|
1129
1247
|
result.usage = _combineUsageWithWarmup(result.usage, warmupResult.usage);
|
|
1130
1248
|
}
|
|
@@ -1207,10 +1325,13 @@ export async function sendViaWebSocket({
|
|
|
1207
1325
|
});
|
|
1208
1326
|
} catch {}
|
|
1209
1327
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1328
|
+
// Rebase after a genuine provider retreat so one eviction produces one
|
|
1329
|
+
// diagnostic instead of a long run of duplicate "dropped" rows. The
|
|
1330
|
+
// request that exposed the retreat has already rebuilt the prefix; its
|
|
1331
|
+
// observed cached count is the correct baseline for recovery.
|
|
1332
|
+
entry.promptCacheMaxCachedTokens = (cacheObservation.actualMiss || cacheObservation.continuityResetReason)
|
|
1333
|
+
? cacheObservation.cachedTokens
|
|
1334
|
+
: Math.max(_num(entry.promptCacheMaxCachedTokens, 0), cacheObservation.cachedTokens);
|
|
1214
1335
|
// Early-session cache-miss ledger (first up-to-3 real requests on this
|
|
1215
1336
|
// socket) for the warmup→first-real continuity trace below. Warmup
|
|
1216
1337
|
// itself is excluded — this block only runs on the real send.
|
|
@@ -1340,6 +1461,7 @@ export async function sendViaWebSocket({
|
|
|
1340
1461
|
// Leave a breadcrumb on the result so downstream callers can observe
|
|
1341
1462
|
// that a retry was used (0 = first-try success, up to 2 for ws_1006/1011).
|
|
1342
1463
|
try { Object.defineProperty(out, '__midstreamRetries', { value: attemptIndex, enumerable: false }); } catch {}
|
|
1464
|
+
emitSendSpan('ok');
|
|
1343
1465
|
return out;
|
|
1344
1466
|
}
|
|
1345
1467
|
// Unreachable — the loop either returns or throws above.
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
11
|
import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
12
|
+
import { join, resolve } from 'path';
|
|
13
13
|
import { getPluginData } from '../config.mjs';
|
|
14
14
|
import { enrichModels } from './model-catalog.mjs';
|
|
15
15
|
import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
16
|
-
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
16
|
+
import { writeJsonAtomicSync, withFileLock } from '../../../shared/atomic-file.mjs';
|
|
17
|
+
import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
|
|
17
18
|
import { makeModelCache } from './model-cache.mjs';
|
|
18
19
|
|
|
19
20
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
@@ -171,6 +172,10 @@ export async function ensureLatestCodexModel(provider) {
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
function getOwnTokenPath() {
|
|
175
|
+
const bound = boundProviderAuthPath('openai-oauth');
|
|
176
|
+
if (bound) return resolve(bound);
|
|
177
|
+
const explicit = process.env.OPENAI_OAUTH_CREDENTIALS_PATH;
|
|
178
|
+
if (explicit) return resolve(explicit);
|
|
174
179
|
const dir = getPluginData();
|
|
175
180
|
if (!existsSync(dir))
|
|
176
181
|
mkdirSync(dir, { recursive: true });
|
|
@@ -255,6 +260,9 @@ function saveTokens(tokens) {
|
|
|
255
260
|
const target = getOwnTokenPath();
|
|
256
261
|
writeJsonAtomicSync(target, tokens, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
257
262
|
}
|
|
263
|
+
function getRefreshLockPath() {
|
|
264
|
+
return `${getOwnTokenPath()}.refresh.lock`;
|
|
265
|
+
}
|
|
258
266
|
|
|
259
267
|
export function forgetOpenAIOAuthCredentials() {
|
|
260
268
|
let removed = false;
|
|
@@ -682,7 +690,7 @@ export class OpenAIOAuthProvider {
|
|
|
682
690
|
}
|
|
683
691
|
|
|
684
692
|
const startingTokens = this.tokens || disk;
|
|
685
|
-
_oauthRefreshInFlight = (async () => {
|
|
693
|
+
_oauthRefreshInFlight = withFileLock(getRefreshLockPath(), async () => {
|
|
686
694
|
const latest = loadTokens() || startingTokens;
|
|
687
695
|
const latestValidAfter = Date.now() + (force ? 0 : TOKEN_REFRESH_SKEW_MS);
|
|
688
696
|
if (latest?.access_token && latest.access_token !== currentToken
|
|
@@ -720,7 +728,7 @@ export class OpenAIOAuthProvider {
|
|
|
720
728
|
}
|
|
721
729
|
throw new Error(`OpenAI OAuth token refresh failed (${msg}). Re-authenticate via provider login.`);
|
|
722
730
|
}
|
|
723
|
-
})
|
|
731
|
+
}).finally(() => { _oauthRefreshInFlight = null; });
|
|
724
732
|
|
|
725
733
|
this.tokens = await _oauthRefreshInFlight;
|
|
726
734
|
return this.tokens;
|