mixdog 0.9.44 → 0.9.46
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/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/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/runtime/shared/update-checker.mjs +40 -10
- 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 +26 -16
- 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 +89 -78
- 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
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import WebSocket from 'ws';
|
|
10
10
|
import { errText } from '../../../shared/err-text.mjs';
|
|
11
11
|
import { createHash, randomBytes } from 'crypto';
|
|
12
|
+
import { performance } from 'node:perf_hooks';
|
|
12
13
|
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
13
14
|
import { join } from 'node:path';
|
|
14
15
|
import { codexOriginator, codexUserAgent, codexVersionHeader } from './codex-client-meta.mjs';
|
|
@@ -305,6 +306,14 @@ function _isOpen(entry) {
|
|
|
305
306
|
return entry?.socket?.readyState === WebSocket.OPEN;
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
function _setTransportReferenced(entry, referenced) {
|
|
310
|
+
const transport = entry?.socket?._socket;
|
|
311
|
+
try {
|
|
312
|
+
if (referenced) transport?.ref?.();
|
|
313
|
+
else transport?.unref?.();
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
316
|
+
|
|
308
317
|
function _clearLiveness(entry) {
|
|
309
318
|
if (entry?.pingTimer) {
|
|
310
319
|
clearInterval(entry.pingTimer);
|
|
@@ -377,7 +386,7 @@ function _armLiveness(poolKey, entry) {
|
|
|
377
386
|
// for a server event that will never arrive. Tag any failure with
|
|
378
387
|
// `wsSendFailed=true` so _classifyMidstreamError routes the next attempt
|
|
379
388
|
// through a fresh socket.
|
|
380
|
-
export function _sendFrame(entry, frame) {
|
|
389
|
+
export function _sendFrame(entry, frame, sendSpan = null) {
|
|
381
390
|
return new Promise((resolve, reject) => {
|
|
382
391
|
const socket = entry?.socket;
|
|
383
392
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
@@ -387,6 +396,7 @@ export function _sendFrame(entry, frame) {
|
|
|
387
396
|
return;
|
|
388
397
|
}
|
|
389
398
|
let payload;
|
|
399
|
+
const serializeStart = performance.now();
|
|
390
400
|
try { payload = JSON.stringify(frame); }
|
|
391
401
|
catch (e) {
|
|
392
402
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
@@ -394,6 +404,10 @@ export function _sendFrame(entry, frame) {
|
|
|
394
404
|
reject(err);
|
|
395
405
|
return;
|
|
396
406
|
}
|
|
407
|
+
if (sendSpan && typeof sendSpan === 'object') {
|
|
408
|
+
sendSpan.requestBuildSerializationMs = (sendSpan.requestBuildSerializationMs || 0)
|
|
409
|
+
+ (performance.now() - serializeStart);
|
|
410
|
+
}
|
|
397
411
|
_dumpFrame(payload);
|
|
398
412
|
try {
|
|
399
413
|
// Do NOT await the send callback: on a wedged-but-OPEN socket the
|
|
@@ -691,6 +705,7 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
691
705
|
// front makes the find() above skip it; on probe failure it is
|
|
692
706
|
// evicted (removed from arr) so the loop continues cleanly.
|
|
693
707
|
idle.busy = true;
|
|
708
|
+
_setTransportReferenced(idle, true);
|
|
694
709
|
if (WS_PING_ENABLED && Date.now() - (idle.lastAliveAt || 0) >= WS_LIVENESS_STALE_MS) {
|
|
695
710
|
const alive = await _pingProbe(idle, WS_PONG_TIMEOUT_MS);
|
|
696
711
|
if (!alive) {
|
|
@@ -803,6 +818,9 @@ export function releaseWebSocket({ entry, poolKey, keep }) {
|
|
|
803
818
|
entry.lastAliveAt = Date.now();
|
|
804
819
|
_scheduleIdleClose(poolKey, entry);
|
|
805
820
|
if (WS_PING_ENABLED) _armLiveness(poolKey, entry);
|
|
821
|
+
// Idle pooled sockets retain reuse state without retaining the process.
|
|
822
|
+
// acquireWebSocket re-refs the transport before probing or handing it out.
|
|
823
|
+
_setTransportReferenced(entry, false);
|
|
806
824
|
}
|
|
807
825
|
|
|
808
826
|
// Drain-complete fence — set true once _closeAllPooledSockets runs so any
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* openai-oauth-ws.mjs re-exports.
|
|
15
15
|
*/
|
|
16
16
|
import { randomBytes } from 'crypto';
|
|
17
|
+
import { performance } from 'node:perf_hooks';
|
|
17
18
|
import {
|
|
18
19
|
extractCachedTokens,
|
|
19
20
|
appendAgentTrace,
|
|
@@ -250,6 +251,12 @@ export async function _streamResponse({
|
|
|
250
251
|
const errLabel = _wsErrLabel(traceProvider);
|
|
251
252
|
const socket = entry.socket;
|
|
252
253
|
enableSessionTransportTracking(state?.sessionId);
|
|
254
|
+
// An already-open (possibly pooled) socket proves transport availability;
|
|
255
|
+
// response.created/reasoning/text/tool events independently satisfy the
|
|
256
|
+
// semantic deadline.
|
|
257
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
258
|
+
markSessionTransportActivity(state?.sessionId);
|
|
259
|
+
}
|
|
253
260
|
const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
|
|
254
261
|
const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
|
|
255
262
|
// First-MEANINGFUL-frame deadline. Distinct from preResponseCreatedMs (a
|
|
@@ -422,6 +429,7 @@ export async function _streamResponse({
|
|
|
422
429
|
toolCalls.push(call);
|
|
423
430
|
midState.emittedToolCall = true;
|
|
424
431
|
try { onToolCall?.(call); } catch {}
|
|
432
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
425
433
|
};
|
|
426
434
|
const relayLeakText = (delta) => {
|
|
427
435
|
if (!leakGuard.enabled) {
|
|
@@ -430,17 +438,22 @@ export async function _streamResponse({
|
|
|
430
438
|
if (state) state.emittedText = true;
|
|
431
439
|
try { onTextDelta(delta); } catch {}
|
|
432
440
|
}
|
|
433
|
-
|
|
441
|
+
if (delta) {
|
|
442
|
+
try { onStreamDelta?.('text'); } catch {}
|
|
443
|
+
}
|
|
444
|
+
return { text: !!delta, tool: false };
|
|
434
445
|
}
|
|
435
446
|
const { text, calls } = leakGuard.push(delta);
|
|
436
447
|
if (text) {
|
|
437
448
|
content += text;
|
|
449
|
+
try { onStreamDelta?.('text'); } catch {}
|
|
438
450
|
if (onTextDelta) {
|
|
439
451
|
if (state) state.emittedText = true;
|
|
440
452
|
try { onTextDelta(text); } catch {}
|
|
441
453
|
}
|
|
442
454
|
}
|
|
443
455
|
for (const c of calls) dispatchLeakedCall(c);
|
|
456
|
+
return { text: !!text, tool: calls.length > 0 };
|
|
444
457
|
};
|
|
445
458
|
const flushLeak = () => {
|
|
446
459
|
if (!leakGuard.enabled) return;
|
|
@@ -763,6 +776,11 @@ export async function _streamResponse({
|
|
|
763
776
|
};
|
|
764
777
|
|
|
765
778
|
messageHandler = (data) => {
|
|
779
|
+
if (midState.sendSpan && midState.sendStartedAt != null
|
|
780
|
+
&& midState.sendSpanAttemptFirstEvent !== true) {
|
|
781
|
+
midState.sendSpanAttemptFirstEvent = true;
|
|
782
|
+
midState.sendSpan.firstEventMs += performance.now() - midState.sendStartedAt;
|
|
783
|
+
}
|
|
766
784
|
resetIdle();
|
|
767
785
|
// resetIdle() above resets the SINGLE inter-chunk idle timer on
|
|
768
786
|
// EVERY received frame — response.created, metadata,
|
|
@@ -798,6 +816,11 @@ export async function _streamResponse({
|
|
|
798
816
|
switch (event.type) {
|
|
799
817
|
case 'response.created':
|
|
800
818
|
midState.sawResponseCreated = true;
|
|
819
|
+
if (midState.sendSpan && midState.sendStartedAt != null
|
|
820
|
+
&& midState.sendSpanAttemptResponseCreated !== true) {
|
|
821
|
+
midState.sendSpanAttemptResponseCreated = true;
|
|
822
|
+
midState.sendSpan.preResponseCreatedMs += performance.now() - midState.sendStartedAt;
|
|
823
|
+
}
|
|
801
824
|
if (event.response?.model) model = event.response.model;
|
|
802
825
|
if (event.response?.id) responseId = event.response.id;
|
|
803
826
|
// Server ack (first event). resetIdle() at the top of
|
|
@@ -807,21 +830,20 @@ export async function _streamResponse({
|
|
|
807
830
|
// first-meaningful watchdog (keepalive/metadata frames do
|
|
808
831
|
// NOT reach this case, so they never clear it).
|
|
809
832
|
clearFirstMeaningfulWatchdog();
|
|
833
|
+
try { onStreamDelta?.('semantic'); } catch {}
|
|
810
834
|
// Do not arm semantic idle until actual model progress.
|
|
811
835
|
// Transport-only reasoning heartbeats are bounded by the
|
|
812
836
|
// outer first-visible ceiling instead.
|
|
813
837
|
break;
|
|
814
838
|
case 'response.output_text.delta':
|
|
815
|
-
try {
|
|
839
|
+
if (event.delta) try {
|
|
816
840
|
if (!_firstDeltaEmitted) {
|
|
817
841
|
_firstDeltaEmitted = true;
|
|
818
842
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
819
843
|
process.stderr.write(`[agent-trace] ws-first-delta sinceStreaming=${Date.now() - _streamingStart}ms\n`);
|
|
820
844
|
}
|
|
821
845
|
}
|
|
822
|
-
onStreamDelta?.();
|
|
823
846
|
} catch {}
|
|
824
|
-
bumpSemanticIdle();
|
|
825
847
|
// Live text relay (gateway): forward the raw text chunk so
|
|
826
848
|
// the client renders first tokens before the final replay.
|
|
827
849
|
// Tool-call/argument deltas intentionally stay off this path.
|
|
@@ -832,7 +854,10 @@ export async function _streamResponse({
|
|
|
832
854
|
// Routed through the leaked-tool-call guard: appends to
|
|
833
855
|
// `content`, forwards visible text via onTextDelta, and
|
|
834
856
|
// recovers/dispatches any leaked known-tool call.
|
|
835
|
-
|
|
857
|
+
{
|
|
858
|
+
const progress = relayLeakText(event.delta || '');
|
|
859
|
+
if (progress?.text || progress?.tool) bumpSemanticIdle();
|
|
860
|
+
}
|
|
836
861
|
break;
|
|
837
862
|
case 'response.reasoning_text.delta':
|
|
838
863
|
case 'response.reasoning_summary_text.delta':
|
|
@@ -841,7 +866,7 @@ export async function _streamResponse({
|
|
|
841
866
|
// Only non-empty reasoning text is model progress. Empty
|
|
842
867
|
// deltas remain transport activity via resetIdle() above.
|
|
843
868
|
if (event.delta) {
|
|
844
|
-
try { onStreamDelta?.(); } catch {}
|
|
869
|
+
try { onStreamDelta?.('reasoning'); } catch {}
|
|
845
870
|
bumpSemanticIdle();
|
|
846
871
|
}
|
|
847
872
|
break;
|
|
@@ -869,17 +894,18 @@ export async function _streamResponse({
|
|
|
869
894
|
// timer so long server-side tool latency after item-added
|
|
870
895
|
// (before any arg delta) is not mistaken for a silent stall.
|
|
871
896
|
resetSemanticIdle();
|
|
897
|
+
try { onStreamDelta?.(_toolInFlight ? 'tool' : 'semantic'); } catch {}
|
|
872
898
|
break;
|
|
873
899
|
case 'response.function_call_arguments.delta':
|
|
874
900
|
markActiveToolItem(null, event.item_id);
|
|
875
901
|
_toolInFlight = true;
|
|
876
|
-
try { onStreamDelta?.(); } catch {}
|
|
902
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
877
903
|
bumpSemanticIdle();
|
|
878
904
|
break;
|
|
879
905
|
case 'response.custom_tool_call_input.delta':
|
|
880
906
|
markActiveToolItem(null, event.item_id);
|
|
881
907
|
_toolInFlight = true;
|
|
882
|
-
try { onStreamDelta?.(); } catch {}
|
|
908
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
883
909
|
bumpSemanticIdle();
|
|
884
910
|
break;
|
|
885
911
|
case 'response.function_call_arguments.done': {
|
|
@@ -935,7 +961,7 @@ export async function _streamResponse({
|
|
|
935
961
|
_deferred: true,
|
|
936
962
|
});
|
|
937
963
|
}
|
|
938
|
-
try { onStreamDelta?.(); } catch {}
|
|
964
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
939
965
|
bumpSemanticIdle();
|
|
940
966
|
break;
|
|
941
967
|
}
|
|
@@ -992,6 +1018,13 @@ export async function _streamResponse({
|
|
|
992
1018
|
// Item-done is genuine lifecycle progress — reset semantic
|
|
993
1019
|
// idle so latency before the next item/args does not stall.
|
|
994
1020
|
resetSemanticIdle();
|
|
1021
|
+
try {
|
|
1022
|
+
onStreamDelta?.(
|
|
1023
|
+
event.item?.type === 'reasoning'
|
|
1024
|
+
? 'reasoning'
|
|
1025
|
+
: (_toolInFlight || /tool|function_call/.test(event.item?.type || '') ? 'tool' : 'semantic'),
|
|
1026
|
+
);
|
|
1027
|
+
} catch {}
|
|
995
1028
|
break;
|
|
996
1029
|
case 'response.completed': {
|
|
997
1030
|
const completedServiceTier = event.response?.service_tier || event.response?.serviceTier || '';
|
|
@@ -1016,6 +1049,7 @@ export async function _streamResponse({
|
|
|
1016
1049
|
}
|
|
1017
1050
|
if (!model && event.response?.model) model = event.response.model;
|
|
1018
1051
|
if (!responseId && event.response?.id) responseId = event.response.id;
|
|
1052
|
+
let reportedBundleProgress = false;
|
|
1019
1053
|
if (event.response?.output) {
|
|
1020
1054
|
for (const item of event.response.output) {
|
|
1021
1055
|
pushResponseItem(item);
|
|
@@ -1030,9 +1064,18 @@ export async function _streamResponse({
|
|
|
1030
1064
|
if (leakGuard.enabled) {
|
|
1031
1065
|
const { text, calls } = leakGuard.push(c.text || '', true);
|
|
1032
1066
|
content += text;
|
|
1067
|
+
if (text) {
|
|
1068
|
+
try { onStreamDelta?.('text'); } catch {}
|
|
1069
|
+
reportedBundleProgress = true;
|
|
1070
|
+
}
|
|
1033
1071
|
for (const lc of calls) dispatchLeakedCall(lc);
|
|
1072
|
+
if (calls.length) reportedBundleProgress = true;
|
|
1034
1073
|
} else {
|
|
1035
1074
|
content += c.text || '';
|
|
1075
|
+
if (c.text) {
|
|
1076
|
+
try { onStreamDelta?.('text'); } catch {}
|
|
1077
|
+
reportedBundleProgress = true;
|
|
1078
|
+
}
|
|
1036
1079
|
}
|
|
1037
1080
|
pushOutputTextAnnotations(c);
|
|
1038
1081
|
}
|
|
@@ -1043,15 +1086,29 @@ export async function _streamResponse({
|
|
|
1043
1086
|
if (c.type === 'output_text') pushOutputTextAnnotations(c);
|
|
1044
1087
|
}
|
|
1045
1088
|
}
|
|
1046
|
-
if (item.type === 'web_search_call')
|
|
1047
|
-
|
|
1048
|
-
|
|
1089
|
+
if (item.type === 'web_search_call') {
|
|
1090
|
+
pushWebSearchCall(item);
|
|
1091
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
1092
|
+
reportedBundleProgress = true;
|
|
1093
|
+
}
|
|
1094
|
+
if (item.type === 'tool_search_call') {
|
|
1095
|
+
pushToolSearchCall(item);
|
|
1096
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
1097
|
+
reportedBundleProgress = true;
|
|
1098
|
+
}
|
|
1099
|
+
if (item.type === 'custom_tool_call') {
|
|
1100
|
+
pushCustomToolCall(item);
|
|
1101
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
1102
|
+
reportedBundleProgress = true;
|
|
1103
|
+
}
|
|
1049
1104
|
// Salvage path: some streams emit reasoning only
|
|
1050
1105
|
// inside the final response.completed.output
|
|
1051
1106
|
// bundle (no per-item .done event). Dedup by id.
|
|
1052
1107
|
if (item.type === 'reasoning'
|
|
1053
1108
|
&& !reasoningItems.some(r => r.id === item.id)) {
|
|
1054
1109
|
pushReasoningItem(item);
|
|
1110
|
+
try { onStreamDelta?.('reasoning'); } catch {}
|
|
1111
|
+
reportedBundleProgress = true;
|
|
1055
1112
|
}
|
|
1056
1113
|
// Salvage path for function_call: when
|
|
1057
1114
|
// arguments.done fired before (or without) a
|
|
@@ -1072,9 +1129,14 @@ export async function _streamResponse({
|
|
|
1072
1129
|
emitToolCallDedupe(tc);
|
|
1073
1130
|
}
|
|
1074
1131
|
}
|
|
1132
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
1133
|
+
reportedBundleProgress = true;
|
|
1075
1134
|
}
|
|
1076
1135
|
}
|
|
1077
1136
|
}
|
|
1137
|
+
if (!reportedBundleProgress) {
|
|
1138
|
+
try { onStreamDelta?.('semantic'); } catch {}
|
|
1139
|
+
}
|
|
1078
1140
|
// Salvage validation. Any deferred call still missing
|
|
1079
1141
|
// id/name would propagate to the next turn as a
|
|
1080
1142
|
// function_call_output the server can't anchor. Fail the
|
|
@@ -1209,7 +1271,7 @@ export async function _streamResponse({
|
|
|
1209
1271
|
// Only non-empty reasoning deltas are model progress;
|
|
1210
1272
|
// empty variants remain transport activity only.
|
|
1211
1273
|
if (event.delta) {
|
|
1212
|
-
try { onStreamDelta?.(); } catch {}
|
|
1274
|
+
try { onStreamDelta?.('reasoning'); } catch {}
|
|
1213
1275
|
bumpSemanticIdle();
|
|
1214
1276
|
}
|
|
1215
1277
|
}
|
|
@@ -1290,13 +1352,19 @@ export async function _streamResponse({
|
|
|
1290
1352
|
socket.on('message', messageHandler);
|
|
1291
1353
|
socket.on('close', closeHandler);
|
|
1292
1354
|
socket.on('error', errorHandler);
|
|
1355
|
+
tracePingHandler = () => {
|
|
1356
|
+
markSessionTransportActivity(midState.sessionId);
|
|
1357
|
+
if (WS_TRACE_ENABLED) _writeWsLifecycleTrace('ping');
|
|
1358
|
+
};
|
|
1359
|
+
tracePongHandler = () => {
|
|
1360
|
+
markSessionTransportActivity(midState.sessionId);
|
|
1361
|
+
if (WS_TRACE_ENABLED) _writeWsLifecycleTrace('pong');
|
|
1362
|
+
};
|
|
1363
|
+
socket.on('ping', tracePingHandler);
|
|
1364
|
+
socket.on('pong', tracePongHandler);
|
|
1293
1365
|
if (WS_TRACE_ENABLED) {
|
|
1294
1366
|
traceOpenHandler = () => _writeWsLifecycleTrace('open');
|
|
1295
|
-
tracePingHandler = () => _writeWsLifecycleTrace('ping');
|
|
1296
|
-
tracePongHandler = () => _writeWsLifecycleTrace('pong');
|
|
1297
1367
|
socket.on('open', traceOpenHandler);
|
|
1298
|
-
socket.on('ping', tracePingHandler);
|
|
1299
|
-
socket.on('pong', tracePongHandler);
|
|
1300
1368
|
if (socket.readyState === WebSocket.OPEN) _writeWsLifecycleTrace('open');
|
|
1301
1369
|
}
|
|
1302
1370
|
armPreStreamWatchdog();
|
|
@@ -19,7 +19,7 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
|
19
19
|
import { sendViaHttpSse, _envFlag } from './openai-oauth-http-sse.mjs';
|
|
20
20
|
import { shouldFallbackTransport } from './retry-classifier.mjs';
|
|
21
21
|
import { resolveOpenAiTransportPolicy } from './openai-transport-policy.mjs';
|
|
22
|
-
import {
|
|
22
|
+
import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
23
23
|
import {
|
|
24
24
|
resolveProviderCacheKey,
|
|
25
25
|
resolveProviderPromptCacheLane,
|
|
@@ -57,17 +57,15 @@ export class OpenAIDirectProvider {
|
|
|
57
57
|
return k;
|
|
58
58
|
}
|
|
59
59
|
// Auth-recovery mirror of openai-compat.reloadApiKey: on a 401/403 the key
|
|
60
|
-
// was likely rotated
|
|
61
|
-
//
|
|
60
|
+
// was likely rotated after this provider instance was built, so re-read
|
|
61
|
+
// only OpenAI's environment/keychain sources before the single retry.
|
|
62
62
|
// Returns the fresh key (or null if none) — no client to rebuild here since
|
|
63
63
|
// the WS/HTTP transports take the key per-call via the `auth` object.
|
|
64
64
|
reloadApiKey() {
|
|
65
65
|
try {
|
|
66
|
-
const
|
|
67
|
-
const cfg = freshConfig.providers?.openai;
|
|
68
|
-
const newKey = cfg?.apiKey || this.config.apiKey;
|
|
66
|
+
const newKey = getAgentApiKey('openai') || this.config.apiKey;
|
|
69
67
|
if (newKey) {
|
|
70
|
-
this.config = { ...(this.config || {}),
|
|
68
|
+
this.config = { ...(this.config || {}), apiKey: newKey };
|
|
71
69
|
return newKey;
|
|
72
70
|
}
|
|
73
71
|
} catch { /* best effort */ }
|
|
@@ -185,7 +183,7 @@ export class OpenAIDirectProvider {
|
|
|
185
183
|
const unsafeToRetry = err?.liveTextEmitted === true
|
|
186
184
|
|| err?.emittedToolCall === true
|
|
187
185
|
|| err?.unsafeToRetry === true;
|
|
188
|
-
// (1) 401/403 → reload
|
|
186
|
+
// (1) 401/403 → reload only OpenAI auth and retry once over WS.
|
|
189
187
|
// shouldFallbackTransport denies 401/403, so this branch owns
|
|
190
188
|
// its own guard.
|
|
191
189
|
if ((status === 401 || status === 403) && !unsafeToRetry) {
|
|
@@ -56,27 +56,52 @@ function configSignature(cfg) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
function abortError(signal) {
|
|
60
|
+
return signal?.reason instanceof Error ? signal.reason : new Error(String(signal?.reason || 'provider initialization aborted'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function throwIfAborted(signal) {
|
|
64
|
+
if (signal?.aborted) throw abortError(signal);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function awaitWithAbort(promise, signal) {
|
|
68
|
+
if (!(signal instanceof AbortSignal)) return promise;
|
|
69
|
+
throwIfAborted(signal);
|
|
70
|
+
let listener = null;
|
|
71
|
+
const aborted = new Promise((_, reject) => {
|
|
72
|
+
listener = () => reject(abortError(signal));
|
|
73
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
74
|
+
});
|
|
75
|
+
return Promise.race([promise, aborted]).finally(() => {
|
|
76
|
+
if (listener) {
|
|
77
|
+
try { signal.removeEventListener('abort', listener); } catch { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function loadProviderExport(cacheKey, spec, exportName, signal = null) {
|
|
60
83
|
if (!providerModulePromises.has(cacheKey)) {
|
|
61
84
|
providerModulePromises.set(cacheKey, import(spec));
|
|
62
85
|
}
|
|
63
|
-
const mod = await providerModulePromises.get(cacheKey);
|
|
86
|
+
const mod = await awaitWithAbort(providerModulePromises.get(cacheKey), signal);
|
|
87
|
+
throwIfAborted(signal);
|
|
64
88
|
const value = mod?.[exportName];
|
|
65
89
|
if (typeof value !== 'function') throw new Error(`provider export missing: ${exportName}`);
|
|
90
|
+
throwIfAborted(signal);
|
|
66
91
|
providerCtors.set(cacheKey, value);
|
|
67
92
|
return value;
|
|
68
93
|
}
|
|
69
94
|
|
|
70
|
-
async function loadProviderCtor(name) {
|
|
71
|
-
if (name === 'anthropic') return loadProviderExport('anthropic', './anthropic.mjs', 'AnthropicProvider');
|
|
72
|
-
if (name === 'gemini') return loadProviderExport('gemini', './gemini.mjs', 'GeminiProvider');
|
|
73
|
-
if (name === 'openai-oauth') return loadProviderExport('openai-oauth', './openai-oauth.mjs', 'OpenAIOAuthProvider');
|
|
74
|
-
if (name === 'anthropic-oauth') return loadProviderExport('anthropic-oauth', './anthropic-oauth.mjs', 'AnthropicOAuthProvider');
|
|
75
|
-
if (name === 'grok-oauth') return loadProviderExport('grok-oauth', './grok-oauth.mjs', 'GrokOAuthProvider');
|
|
76
|
-
if (name === 'openai') return loadProviderExport('openai', './openai-ws.mjs', 'OpenAIDirectProvider');
|
|
77
|
-
if (name === 'opencode-go') return loadProviderExport('opencode-go', './opencode-go.mjs', 'OpenCodeGoProvider');
|
|
95
|
+
async function loadProviderCtor(name, signal = null) {
|
|
96
|
+
if (name === 'anthropic') return loadProviderExport('anthropic', './anthropic.mjs', 'AnthropicProvider', signal);
|
|
97
|
+
if (name === 'gemini') return loadProviderExport('gemini', './gemini.mjs', 'GeminiProvider', signal);
|
|
98
|
+
if (name === 'openai-oauth') return loadProviderExport('openai-oauth', './openai-oauth.mjs', 'OpenAIOAuthProvider', signal);
|
|
99
|
+
if (name === 'anthropic-oauth') return loadProviderExport('anthropic-oauth', './anthropic-oauth.mjs', 'AnthropicOAuthProvider', signal);
|
|
100
|
+
if (name === 'grok-oauth') return loadProviderExport('grok-oauth', './grok-oauth.mjs', 'GrokOAuthProvider', signal);
|
|
101
|
+
if (name === 'openai') return loadProviderExport('openai', './openai-ws.mjs', 'OpenAIDirectProvider', signal);
|
|
102
|
+
if (name === 'opencode-go') return loadProviderExport('opencode-go', './opencode-go.mjs', 'OpenCodeGoProvider', signal);
|
|
78
103
|
if (Object.prototype.hasOwnProperty.call(OPENAI_COMPAT_PRESETS, name)) {
|
|
79
|
-
return loadProviderExport('openai-compat', './openai-compat.mjs', 'OpenAICompatProvider');
|
|
104
|
+
return loadProviderExport('openai-compat', './openai-compat.mjs', 'OpenAICompatProvider', signal);
|
|
80
105
|
}
|
|
81
106
|
throw new Error(`unknown enabled provider: ${name}`);
|
|
82
107
|
}
|
|
@@ -88,11 +113,12 @@ function instantiateProvider(name, Ctor, cfg) {
|
|
|
88
113
|
return new Ctor(cfg);
|
|
89
114
|
}
|
|
90
115
|
|
|
91
|
-
export async function initProviders(config) {
|
|
116
|
+
export async function initProviders(config, { signal = null } = {}) {
|
|
117
|
+
throwIfAborted(signal);
|
|
92
118
|
const sig = configSignature(config);
|
|
93
119
|
// Coalesce: an identical config is already mid-init — attach to it.
|
|
94
120
|
if (sig !== null && _inFlightPromise && _inFlightSig === sig) {
|
|
95
|
-
return _inFlightPromise;
|
|
121
|
+
return awaitWithAbort(_inFlightPromise, signal);
|
|
96
122
|
}
|
|
97
123
|
// Fast path: chain idle and the live registry already reflects this exact
|
|
98
124
|
// config — nothing to tear down or rebuild.
|
|
@@ -103,7 +129,7 @@ export async function initProviders(config) {
|
|
|
103
129
|
// signatures can never run their clear()+rebuild concurrently, regardless
|
|
104
130
|
// of caller-side gating (agent-tool gateOnPrior may release a queued init
|
|
105
131
|
// before the prior one settled). Errors do not poison the chain.
|
|
106
|
-
const run = () => _initProvidersUnsynchronized(config);
|
|
132
|
+
const run = () => _initProvidersUnsynchronized(config, signal);
|
|
107
133
|
const next = _initChain.then(run, run);
|
|
108
134
|
_initChain = next.then(() => {}, () => {});
|
|
109
135
|
const settle = () => {
|
|
@@ -118,10 +144,11 @@ export async function initProviders(config) {
|
|
|
118
144
|
);
|
|
119
145
|
_inFlightSig = sig;
|
|
120
146
|
_inFlightPromise = tracked;
|
|
121
|
-
return tracked;
|
|
147
|
+
return awaitWithAbort(tracked, signal);
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
async function _initProvidersUnsynchronized(config) {
|
|
150
|
+
async function _initProvidersUnsynchronized(config, signal = null) {
|
|
151
|
+
throwIfAborted(signal);
|
|
125
152
|
// Invariant: never wipe the live registry based on an empty / all-disabled
|
|
126
153
|
// config. Without this guard, a stale `loadAgentConfig()` (e.g. mid-reload
|
|
127
154
|
// or a transient FS hiccup) would land here as `{}` or `{...,enabled:false}`,
|
|
@@ -145,7 +172,8 @@ async function _initProvidersUnsynchronized(config) {
|
|
|
145
172
|
return { name, inst: providers.get(name), sig };
|
|
146
173
|
}
|
|
147
174
|
try {
|
|
148
|
-
const Ctor = await loadProviderCtor(name);
|
|
175
|
+
const Ctor = await loadProviderCtor(name, signal);
|
|
176
|
+
throwIfAborted(signal);
|
|
149
177
|
const inst = instantiateProvider(name, Ctor, cfg);
|
|
150
178
|
return { name, inst, sig };
|
|
151
179
|
}
|
|
@@ -155,6 +183,7 @@ async function _initProvidersUnsynchronized(config) {
|
|
|
155
183
|
}
|
|
156
184
|
}));
|
|
157
185
|
for (const result of enabledResults) {
|
|
186
|
+
throwIfAborted(signal);
|
|
158
187
|
if (!result) continue;
|
|
159
188
|
if (result.error) throw result.error;
|
|
160
189
|
next.set(result.name, result.inst);
|
|
@@ -182,6 +211,7 @@ async function _initProvidersUnsynchronized(config) {
|
|
|
182
211
|
if (signatures.has(name)) nextSignatures.set(name, signatures.get(name));
|
|
183
212
|
}
|
|
184
213
|
}
|
|
214
|
+
throwIfAborted(signal);
|
|
185
215
|
providers.clear();
|
|
186
216
|
for (const [k, v] of next) providers.set(k, v);
|
|
187
217
|
signatures.clear();
|