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.
Files changed (117) hide show
  1. package/package.json +5 -2
  2. package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
  3. package/scripts/agent-model-liveness-test.mjs +618 -0
  4. package/scripts/agent-trace-io-test.mjs +68 -4
  5. package/scripts/bench-run.mjs +30 -3
  6. package/scripts/compact-pressure-test.mjs +187 -4
  7. package/scripts/compact-prior-context-flatten-test.mjs +252 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +8 -6
  9. package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
  10. package/scripts/context-mcp-metering-test.mjs +75 -0
  11. package/scripts/explore-prompt-policy-test.mjs +15 -16
  12. package/scripts/explore-timeout-cancel-test.mjs +345 -0
  13. package/scripts/headless-pristine-execution-test.mjs +614 -0
  14. package/scripts/internal-comms-smoke.mjs +15 -6
  15. package/scripts/live-worker-smoke.mjs +1 -1
  16. package/scripts/memory-core-input-test.mjs +137 -0
  17. package/scripts/memory-rule-contract-test.mjs +5 -5
  18. package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
  19. package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
  20. package/scripts/parent-abort-link-test.mjs +22 -0
  21. package/scripts/provider-toolcall-test.mjs +22 -0
  22. package/scripts/reactive-compact-persist-smoke.mjs +8 -4
  23. package/scripts/session-bench.mjs +3 -70
  24. package/scripts/task-bench.mjs +0 -2
  25. package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
  26. package/scripts/tool-smoke.mjs +21 -21
  27. package/scripts/tool-tui-presentation-test.mjs +68 -0
  28. package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
  29. package/src/app.mjs +28 -103
  30. package/src/cli.mjs +17 -13
  31. package/src/headless-command.mjs +139 -0
  32. package/src/headless-role.mjs +121 -10
  33. package/src/help.mjs +4 -1
  34. package/src/rules/agent/00-common.md +3 -3
  35. package/src/rules/agent/00-core.md +8 -9
  36. package/src/rules/agent/20-skip-protocol.md +2 -3
  37. package/src/rules/agent/30-explorer.md +50 -56
  38. package/src/rules/agent/40-cycle1-agent.md +10 -12
  39. package/src/rules/agent/41-cycle2-agent.md +12 -9
  40. package/src/rules/agent/42-cycle3-agent.md +4 -6
  41. package/src/rules/lead/01-general.md +5 -6
  42. package/src/rules/lead/02-channels.md +1 -1
  43. package/src/rules/lead/lead-brief.md +14 -17
  44. package/src/rules/lead/lead-tool.md +3 -3
  45. package/src/rules/shared/01-tool.md +41 -43
  46. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
  48. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
  49. package/src/runtime/agent/orchestrator/config.mjs +96 -30
  50. package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  52. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
  53. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  54. package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
  55. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
  56. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
  58. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
  60. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
  61. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
  64. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
  65. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
  66. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
  67. package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
  69. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
  71. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
  74. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
  76. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
  77. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
  78. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
  80. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
  81. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  82. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
  84. package/src/runtime/memory/index.mjs +0 -1
  85. package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
  86. package/src/runtime/memory/lib/http-router.mjs +0 -193
  87. package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
  88. package/src/runtime/memory/tool-defs.mjs +5 -6
  89. package/src/runtime/shared/config.mjs +11 -34
  90. package/src/runtime/shared/pristine-execution-contract.json +75 -0
  91. package/src/runtime/shared/pristine-execution.mjs +356 -0
  92. package/src/runtime/shared/provider-api-key.mjs +43 -0
  93. package/src/runtime/shared/provider-auth-binding.mjs +21 -0
  94. package/src/runtime/shared/update-checker.mjs +40 -10
  95. package/src/session-runtime/context-status.mjs +61 -13
  96. package/src/session-runtime/mcp-glue.mjs +29 -2
  97. package/src/session-runtime/plugin-mcp.mjs +7 -0
  98. package/src/session-runtime/resource-api.mjs +38 -5
  99. package/src/session-runtime/runtime-core.mjs +5 -1
  100. package/src/session-runtime/session-turn-api.mjs +14 -2
  101. package/src/session-runtime/settings-api.mjs +5 -0
  102. package/src/session-runtime/tool-catalog.mjs +13 -2
  103. package/src/session-runtime/tool-defs.mjs +1 -3
  104. package/src/standalone/agent-task-status.mjs +50 -11
  105. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  106. package/src/standalone/explore-tool.mjs +257 -49
  107. package/src/standalone/seeds.mjs +1 -0
  108. package/src/tui/App.jsx +26 -16
  109. package/src/tui/app/use-transcript-scroll.mjs +4 -3
  110. package/src/tui/app/use-transcript-window.mjs +12 -21
  111. package/src/tui/components/ContextPanel.jsx +19 -25
  112. package/src/tui/dist/index.mjs +89 -78
  113. package/src/tui/engine/agent-envelope.mjs +16 -5
  114. package/src/tui/engine/labels.mjs +1 -1
  115. package/src/workflows/default/WORKFLOW.md +21 -51
  116. package/src/workflows/solo/WORKFLOW.md +12 -17
  117. 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
- return;
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
- relayLeakText(event.delta || '');
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') pushWebSearchCall(item);
1047
- if (item.type === 'tool_search_call') pushToolSearchCall(item);
1048
- if (item.type === 'custom_tool_call') pushCustomToolCall(item);
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 { loadConfig } from '../config.mjs';
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 in config after this provider instance was built, so
61
- // re-read providers.openai.apiKey from disk before the single retry.
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 freshConfig = loadConfig();
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 || {}), ...(cfg || {}), apiKey: newKey };
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 apiKey from config and retry once over WS.
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
- async function loadProviderExport(cacheKey, spec, exportName) {
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();