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
@@ -102,10 +102,26 @@ function firstByteCompatStreamError(label) {
102
102
  return err;
103
103
  }
104
104
 
105
- async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, idleLabel, emittedToolCall } = {}) {
105
+ async function nextAsyncWithWatchdog(iterator, {
106
+ signal,
107
+ idleMs,
108
+ idleDeadlineAt,
109
+ idleEnabled,
110
+ idleLabel,
111
+ emittedToolCall,
112
+ } = {}) {
106
113
  let idleTimer = null;
107
114
  let idleReject = null;
108
115
  let idleTimedOut = false;
116
+ let iteratorCloseRequested = false;
117
+ const closeIterator = () => {
118
+ if (iteratorCloseRequested) return;
119
+ iteratorCloseRequested = true;
120
+ try {
121
+ const closing = iterator?.return?.();
122
+ if (closing && typeof closing.catch === 'function') closing.catch(() => {});
123
+ } catch { /* closing must never replace the watchdog/abort error */ }
124
+ };
109
125
  // Double-dispatch guard (reviewer High): if a tool call was already emitted
110
126
  // this stream, a stall must be unsafe-to-retry so withRetry() won't replay
111
127
  // the turn and re-run the side-effecting tool. `emittedToolCall` may be a
@@ -117,6 +133,10 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
117
133
  const armIdle = () => {
118
134
  if (!idleEnabled || !(idleMs > 0)) return;
119
135
  if (idleTimer) clearTimeout(idleTimer);
136
+ const deadline = Number(idleDeadlineAt);
137
+ const delayMs = Number.isFinite(deadline) && deadline > 0
138
+ ? Math.max(0, deadline - Date.now())
139
+ : idleMs;
120
140
  idleTimer = setTimeout(() => {
121
141
  idleTimedOut = true;
122
142
  // SEMANTIC idle abort: this timer is (re)armed only around waiting
@@ -125,12 +145,13 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
125
145
  // StreamStalledError so the retry-classifier treats it as a stream
126
146
  // failure (owner gets notified) rather than a user cancel.
127
147
  const e = streamStalledError(idleLabel || 'compat SSE', idleMs, { emittedToolCall: didEmitToolCall() });
148
+ closeIterator();
128
149
  if (idleReject) {
129
150
  const r = idleReject;
130
151
  idleReject = null;
131
152
  r(e);
132
153
  }
133
- }, idleMs);
154
+ }, delayMs);
134
155
  if (typeof idleTimer.unref === 'function') idleTimer.unref();
135
156
  };
136
157
  armIdle();
@@ -139,6 +160,7 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
139
160
  idleReject = reject;
140
161
  if (signal?.aborted) {
141
162
  const reason = signal.reason;
163
+ closeIterator();
142
164
  reject(reason instanceof Error ? reason : new Error('compat stream aborted'));
143
165
  return;
144
166
  }
@@ -146,6 +168,7 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
146
168
  if (signal) {
147
169
  onAbort = () => {
148
170
  const reason = signal.reason;
171
+ closeIterator();
149
172
  reject(reason instanceof Error ? reason : new Error('compat stream aborted'));
150
173
  };
151
174
  signal.addEventListener('abort', onAbort, { once: true });
@@ -293,14 +316,34 @@ function isMaxOutputIncompleteReason(reason) {
293
316
  return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
294
317
  }
295
318
 
296
- export async function consumeCompatChatCompletionStream(stream, { signal, label, onStreamDelta, onToolCall, onTextDelta, parseToolCalls, knownToolNames } = {}) {
319
+ export async function consumeCompatChatCompletionStream(stream, {
320
+ signal,
321
+ label,
322
+ onStreamDelta,
323
+ onToolCall,
324
+ onTextDelta,
325
+ parseToolCalls,
326
+ knownToolNames,
327
+ semanticIdleTimeoutMs,
328
+ } = {}) {
329
+ // Reaching the consumer means the HTTP response/stream object exists.
330
+ // Record transport health without satisfying semantic model activity.
331
+ try { onStreamDelta?.('transport'); } catch {}
297
332
  const iterator = stream[Symbol.asyncIterator]();
298
333
  const firstByteTimeout = createTimeoutSignal(signal, PROVIDER_FIRST_BYTE_TIMEOUT_MS, `${label} first byte`);
299
- const idleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
334
+ const idleOverrideEnabled = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0;
335
+ const idleEnabled = idleOverrideEnabled || PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
300
336
  // Per-event (last-event-relative) SEMANTIC idle: nextAsyncWithWatchdog arms
301
337
  // the timer only while awaiting the NEXT stream event, so a stream that
302
338
  // emits some deltas then goes silent trips it within the window.
303
- const idleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
339
+ const idleMs = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0
340
+ ? Number(semanticIdleTimeoutMs)
341
+ : PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
342
+ let semanticIdleDeadlineAt = 0;
343
+ const reportProgress = (kind) => {
344
+ if (kind !== 'transport') semanticIdleDeadlineAt = Date.now() + idleMs;
345
+ try { onStreamDelta?.(kind); } catch {}
346
+ };
304
347
  let sawFirstEvent = false;
305
348
  let content = '';
306
349
  let reasoningContent = '';
@@ -318,6 +361,14 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
318
361
  // Fix 2: one dedupe per stream, shared by the synthetic leaked-call
319
362
  // dispatch and every native emit so an identical (name,args) fires once.
320
363
  const _toolDedupe = createToolCallDedupe();
364
+ // Persistent stream state: leaked calls dispatch eagerly, before the final
365
+ // native parse. If the iterator later fails this latch makes the failure
366
+ // unsafe-to-retry so the eager side effect cannot run twice.
367
+ const streamEmitState = {
368
+ emittedToolCallKeys: new Set(),
369
+ emittedToolCall: false,
370
+ _toolDedupe,
371
+ };
321
372
  // Leaked tool-call guard: the model sometimes emits a tool call as plain
322
373
  // text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
323
374
  // `<|channel|>...to=functions.NAME...<|call|>`) inside `delta.content`
@@ -328,8 +379,8 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
328
379
  const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
329
380
  const dispatchLeakedCall = (recovered) => {
330
381
  const call = synthLeakedOpenAICall(recovered);
331
- const emitState = { emittedToolCallKeys: new Set(), _toolDedupe };
332
- emitCompatToolCallOnce(emitState, call, onToolCall);
382
+ emitCompatToolCallOnce(streamEmitState, call, onToolCall);
383
+ reportProgress('tool');
333
384
  return call;
334
385
  };
335
386
  const leakedCalls = [];
@@ -337,6 +388,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
337
388
  const { text, calls } = leakGuard.push(delta);
338
389
  if (text) {
339
390
  content += text;
391
+ reportProgress('text');
340
392
  if (onTextDelta) {
341
393
  emittedText = true;
342
394
  try { onTextDelta(text); } catch {}
@@ -348,6 +400,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
348
400
  const { text, calls } = leakGuard.flush();
349
401
  if (text) {
350
402
  content += text;
403
+ reportProgress('text');
351
404
  if (onTextDelta) {
352
405
  emittedText = true;
353
406
  try { onTextDelta(text); } catch {}
@@ -362,21 +415,25 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
362
415
  // first-byte timer (createTimeoutSignal already chains parent).
363
416
  signal: sawFirstEvent ? signal : firstByteTimeout.signal,
364
417
  idleMs,
365
- idleEnabled: sawFirstEvent && idleEnabled,
418
+ idleDeadlineAt: semanticIdleDeadlineAt,
419
+ idleEnabled: sawFirstEvent && idleEnabled && semanticIdleDeadlineAt > 0,
366
420
  idleLabel: `${label} SSE idle`,
367
421
  // A stall after a tool call has already been dispatched (native
368
422
  // or recovered-leaked) must be unsafe-to-retry (no double-run).
369
- emittedToolCall: () => leakedCalls.length > 0 || toolAcc.size > 0,
423
+ emittedToolCall: () => streamEmitState.emittedToolCall || toolAcc.size > 0,
370
424
  });
371
425
  if (done) break;
372
426
  if (!sawFirstEvent) {
373
427
  sawFirstEvent = true;
374
428
  firstByteTimeout.cleanup();
375
429
  }
376
- try { onStreamDelta?.(); } catch {}
430
+ try { onStreamDelta?.('transport'); } catch {}
377
431
  if (chunk?.id) responseId = chunk.id;
378
432
  if (chunk?.model) model = chunk.model;
379
433
  const choice = chunk?.choices?.[0];
434
+ if (typeof choice?.delta?.role === 'string' && choice.delta.role) {
435
+ reportProgress('semantic');
436
+ }
380
437
  if (choice?.delta?.content) {
381
438
  // Live text relay (gateway): explicit assistant text delta,
382
439
  // routed through the leaked-tool-call guard (which appends to
@@ -386,6 +443,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
386
443
  relayText(choice.delta.content);
387
444
  } else {
388
445
  content += choice.delta.content;
446
+ reportProgress('text');
389
447
  if (onTextDelta) {
390
448
  emittedText = true;
391
449
  try { onTextDelta(choice.delta.content); } catch {}
@@ -394,6 +452,12 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
394
452
  }
395
453
  if (typeof choice?.delta?.reasoning_content === 'string') {
396
454
  reasoningContent += choice.delta.reasoning_content;
455
+ if (choice.delta.reasoning_content) {
456
+ reportProgress('reasoning');
457
+ }
458
+ }
459
+ if (Array.isArray(choice?.delta?.tool_calls) && choice.delta.tool_calls.length) {
460
+ reportProgress('tool');
397
461
  }
398
462
  mergeToolCallDelta(toolAcc, choice?.delta?.tool_calls, toolBucketState);
399
463
  if (choice?.finish_reason) stopReason = choice.finish_reason;
@@ -414,7 +478,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
414
478
  err.pendingToolUse = toolAcc.size > 0 || leakedCalls.length > 0;
415
479
  err.partialModel = model || undefined;
416
480
  } catch { /* best-effort */ }
417
- throw err;
481
+ throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
418
482
  }
419
483
  // Partial-final recovery: on a mid-stream stall, attach the
420
484
  // streamed partial state so the loop can accept a wedged FINAL no-tool
@@ -427,7 +491,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
427
491
  err.partialModel = model || undefined;
428
492
  } catch { /* best-effort */ }
429
493
  }
430
- throw err;
494
+ throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
431
495
  } finally {
432
496
  firstByteTimeout.cleanup();
433
497
  }
@@ -450,7 +514,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
450
514
  err.partialModel = model || undefined;
451
515
  } catch { /* best-effort */ }
452
516
  }
453
- throw err;
517
+ throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
454
518
  }
455
519
  const message = {
456
520
  content: content || null,
@@ -478,11 +542,10 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
478
542
  try { err.message += ` finish_reason=${stopReason}`; } catch {}
479
543
  }
480
544
  if (emittedText) markErrorLiveTextEmitted(err);
481
- throw err;
545
+ throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
482
546
  }
483
547
  if (Array.isArray(toolCalls) && toolCalls.length) {
484
- const emitState = { emittedToolCallKeys: new Set(), _toolDedupe };
485
- for (const call of toolCalls) emitCompatToolCallOnce(emitState, call, onToolCall);
548
+ for (const call of toolCalls) emitCompatToolCallOnce(streamEmitState, call, onToolCall);
486
549
  }
487
550
  // Fold recovered leaked calls into the returned toolCalls so the dispatch
488
551
  // loop treats them exactly like native ones. They were already emitted via
@@ -534,21 +597,30 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
534
597
  case 'response.created':
535
598
  if (event.response?.model) state.model = event.response.model;
536
599
  if (event.response?.id) state.responseId = event.response.id;
600
+ try { onStreamDelta?.('semantic'); } catch {}
537
601
  break;
538
602
  case 'response.output_text.delta':
539
603
  state.sawOutput = true;
540
- try { onStreamDelta?.(); } catch {}
541
604
  // Route assistant text through the leaked-tool-call guard (appends
542
605
  // to state.content, forwards visible text, recovers leaked calls).
543
606
  if (relayLeakText) relayLeakText(event.delta || '');
544
607
  else {
545
608
  state.content += event.delta || '';
609
+ if (event.delta) {
610
+ try { onStreamDelta?.('text'); } catch {}
611
+ }
546
612
  if (event.delta && onTextDelta) {
547
613
  state.emittedText = true;
548
614
  try { onTextDelta(event.delta); } catch {}
549
615
  }
550
616
  }
551
617
  break;
618
+ case 'response.reasoning_text.delta':
619
+ case 'response.reasoning_summary_text.delta':
620
+ if (event.delta) {
621
+ try { onStreamDelta?.('reasoning'); } catch {}
622
+ }
623
+ break;
552
624
  case 'response.output_item.added':
553
625
  if (event.item?.type === 'function_call') {
554
626
  state.pendingCalls.set(event.item.id || '', {
@@ -568,14 +640,14 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
568
640
  state.toolTracker?.mark(event.item);
569
641
  state.toolInFlight = true;
570
642
  }
571
- try { onStreamDelta?.(); } catch {}
643
+ try { onStreamDelta?.(state.toolInFlight ? 'tool' : 'semantic'); } catch {}
572
644
  break;
573
645
  case 'response.function_call_arguments.delta':
574
646
  // A tool call's args are streaming — mark tool work in-flight so a
575
647
  // mid-args stall is NEVER accepted as a text-only partial-final.
576
648
  state.toolTracker?.mark(null, event.item_id);
577
649
  state.toolInFlight = true;
578
- try { onStreamDelta?.(); } catch {}
650
+ try { onStreamDelta?.('tool'); } catch {}
579
651
  break;
580
652
  case 'response.custom_tool_call_input.delta':
581
653
  // Custom-tool input streams before output_item.done records the call
@@ -583,7 +655,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
583
655
  // final success (otherwise a tool-bearing turn looks text-only).
584
656
  state.toolTracker?.mark(null, event.item_id);
585
657
  state.toolInFlight = true;
586
- try { onStreamDelta?.(); } catch {}
658
+ try { onStreamDelta?.('tool'); } catch {}
587
659
  break;
588
660
  case 'response.function_call_arguments.done': {
589
661
  const itemId = event.item_id || '';
@@ -599,7 +671,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
599
671
  state.toolCalls.push(call);
600
672
  if (call.id && call.name) delete call._pendingItemId;
601
673
  emitCompatToolCallOnce(state, call, onToolCall);
602
- try { onStreamDelta?.(); } catch {}
674
+ try { onStreamDelta?.('tool'); } catch {}
603
675
  break;
604
676
  }
605
677
  case 'response.output_item.done': {
@@ -638,7 +710,10 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
638
710
  state.toolTracker?.clear(item, item.id || '');
639
711
  state.toolInFlight = state.pendingCalls.size > 0 || (state.toolTracker ? state.toolTracker.items.size > 0 : false);
640
712
  }
641
- try { onStreamDelta?.(); } catch {}
713
+ const kind = item.type === 'reasoning'
714
+ ? 'reasoning'
715
+ : (/tool|function_call|web_search_call/.test(item.type || '') ? 'tool' : 'semantic');
716
+ try { onStreamDelta?.(kind); } catch {}
642
717
  break;
643
718
  }
644
719
  case 'response.completed': {
@@ -647,7 +722,20 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
647
722
  state.completedResponse = resp;
648
723
  if (!state.model && resp.model) state.model = resp.model;
649
724
  if (!state.responseId && resp.id) state.responseId = resp.id;
650
- if (!state.content) state.content = responseOutputText(resp);
725
+ let reportedBundleProgress = false;
726
+ if (!state.content) {
727
+ const fallbackText = responseOutputText(resp);
728
+ if (fallbackText) {
729
+ if (relayLeakText) {
730
+ const result = relayLeakText(fallbackText, true);
731
+ reportedBundleProgress = !!(result?.text || result?.tool);
732
+ } else {
733
+ state.content = fallbackText;
734
+ try { onStreamDelta?.('text'); } catch {}
735
+ reportedBundleProgress = true;
736
+ }
737
+ }
738
+ }
651
739
  for (const item of resp.output || []) {
652
740
  if (item?.type === 'function_call') {
653
741
  const itemId = item.id || '';
@@ -667,13 +755,27 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
667
755
  state.toolCalls.push(call);
668
756
  emitCompatToolCallOnce(state, call, onToolCall);
669
757
  }
758
+ try { onStreamDelta?.('tool'); } catch {}
759
+ reportedBundleProgress = true;
670
760
  } else if (item?.type === 'tool_search_call') {
671
761
  pushToolSearchCall(item);
762
+ try { onStreamDelta?.('tool'); } catch {}
763
+ reportedBundleProgress = true;
672
764
  } else if (item?.type === 'custom_tool_call') {
673
765
  pushCustomToolCall(item);
766
+ try { onStreamDelta?.('tool'); } catch {}
767
+ reportedBundleProgress = true;
768
+ } else if (item?.type === 'reasoning') {
769
+ try { onStreamDelta?.('reasoning'); } catch {}
770
+ reportedBundleProgress = true;
771
+ } else if (item?.type === 'web_search_call') {
772
+ try { onStreamDelta?.('tool'); } catch {}
773
+ reportedBundleProgress = true;
674
774
  }
675
775
  }
676
- try { onStreamDelta?.(); } catch {}
776
+ if (!reportedBundleProgress) {
777
+ try { onStreamDelta?.('semantic'); } catch {}
778
+ }
677
779
  break;
678
780
  }
679
781
  case 'response.done':
@@ -748,12 +850,17 @@ export async function consumeCompatResponsesStream(stream, {
748
850
  parseResponsesToolCalls,
749
851
  responseOutputText,
750
852
  knownToolNames,
853
+ semanticIdleTimeoutMs,
751
854
  } = {}) {
855
+ try { onStreamDelta?.('transport'); } catch {}
752
856
  const iterator = stream[Symbol.asyncIterator]();
753
857
  const firstByteTimeout = createTimeoutSignal(signal, PROVIDER_FIRST_BYTE_TIMEOUT_MS, `${label} first byte`);
754
- const idleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
858
+ const idleOverrideEnabled = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0;
859
+ const idleEnabled = idleOverrideEnabled || PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
755
860
  // Per-event (last-event-relative) SEMANTIC idle — see the Chat path note.
756
- const idleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
861
+ const idleMs = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0
862
+ ? Number(semanticIdleTimeoutMs)
863
+ : PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
757
864
  const state = {
758
865
  content: '',
759
866
  model: '',
@@ -781,6 +888,11 @@ export async function consumeCompatResponsesStream(stream, {
781
888
  // has been forwarded. A later failure is non-retryable (rendered text
782
889
  // cannot be withdrawn; a retry would concatenate attempts).
783
890
  emittedText: false,
891
+ semanticIdleDeadlineAt: 0,
892
+ };
893
+ const reportProgress = (kind) => {
894
+ if (kind !== 'transport') state.semanticIdleDeadlineAt = Date.now() + idleMs;
895
+ try { onStreamDelta?.(kind); } catch {}
784
896
  };
785
897
  let sawFirstEvent = false;
786
898
  // Leaked tool-call guard for the Responses text stream. Same recovery as
@@ -792,18 +904,21 @@ export async function consumeCompatResponsesStream(stream, {
792
904
  const call = synthLeakedOpenAICall(recovered);
793
905
  emitCompatToolCallOnce(state, call, onToolCall);
794
906
  leakedCalls.push(call);
907
+ reportProgress('tool');
795
908
  };
796
909
  const relayLeakText = leakGuard.enabled
797
- ? (delta) => {
798
- const { text, calls } = leakGuard.push(delta);
910
+ ? (delta, final = false) => {
911
+ const { text, calls } = leakGuard.push(delta, final);
799
912
  if (text) {
800
913
  state.content += text;
914
+ reportProgress('text');
801
915
  if (onTextDelta) {
802
916
  state.emittedText = true;
803
917
  try { onTextDelta(text); } catch {}
804
918
  }
805
919
  }
806
920
  for (const c of calls) dispatchLeakedCall(c);
921
+ return { text: !!text, tool: calls.length > 0 };
807
922
  }
808
923
  : null;
809
924
  const flushLeak = () => {
@@ -811,6 +926,7 @@ export async function consumeCompatResponsesStream(stream, {
811
926
  const { text, calls } = leakGuard.flush();
812
927
  if (text) {
813
928
  state.content += text;
929
+ reportProgress('text');
814
930
  if (onTextDelta) {
815
931
  state.emittedText = true;
816
932
  try { onTextDelta(text); } catch {}
@@ -818,13 +934,14 @@ export async function consumeCompatResponsesStream(stream, {
818
934
  }
819
935
  for (const c of calls) dispatchLeakedCall(c);
820
936
  };
821
- const deps = { label, parseResponsesToolCalls, responseOutputText, onStreamDelta, onToolCall, onTextDelta, relayLeakText };
937
+ const deps = { label, parseResponsesToolCalls, responseOutputText, onStreamDelta: reportProgress, onToolCall, onTextDelta, relayLeakText };
822
938
  try {
823
939
  while (true) {
824
940
  const { value: event, done } = await nextAsyncWithWatchdog(iterator, {
825
941
  signal: sawFirstEvent ? signal : firstByteTimeout.signal,
826
942
  idleMs,
827
- idleEnabled: sawFirstEvent && idleEnabled,
943
+ idleDeadlineAt: state.semanticIdleDeadlineAt,
944
+ idleEnabled: sawFirstEvent && idleEnabled && state.semanticIdleDeadlineAt > 0,
828
945
  idleLabel: `${label} SSE idle`,
829
946
  // Unsafe-to-retry once any tool call (native or recovered-leaked)
830
947
  // has been emitted this stream — avoid a double side-effect.
@@ -835,6 +952,7 @@ export async function consumeCompatResponsesStream(stream, {
835
952
  sawFirstEvent = true;
836
953
  firstByteTimeout.cleanup();
837
954
  }
955
+ reportProgress('transport');
838
956
  handleCompatResponsesStreamEvent(event, state, deps);
839
957
  }
840
958
  flushLeak();
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
- import { loadConfig } from '../config.mjs';
2
+ import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
3
3
  import { withRetry } from './retry-classifier.mjs';
4
4
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
5
5
  import { sendViaWebSocket } from './openai-oauth-ws.mjs';
@@ -152,13 +152,11 @@ export class OpenAICompatProvider {
152
152
  }
153
153
  reloadApiKey() {
154
154
  try {
155
- const freshConfig = loadConfig();
156
- const cfg = freshConfig.providers?.[this.name];
157
155
  const preset = PRESETS[this.name];
158
- const newKey = cfg?.apiKey || this.config.apiKey;
159
- const baseURL = assertSafeBaseURL(cfg?.baseURL || this.config.baseURL || preset?.baseURL || 'http://localhost:8080/v1', this.name);
156
+ const newKey = getAgentApiKey(this.name) || this.config.apiKey;
157
+ const baseURL = assertSafeBaseURL(this.config.baseURL || preset?.baseURL || 'http://localhost:8080/v1', this.name);
160
158
  if (newKey) {
161
- this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey, baseURL };
159
+ this.config = { ...(this.config || {}), apiKey: newKey, baseURL };
162
160
  this.baseURL = baseURL;
163
161
  this.apiKey = newKey;
164
162
  this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(this.config.extraHeaders || {}) };
@@ -180,7 +178,7 @@ export class OpenAICompatProvider {
180
178
  if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
181
179
  throw err;
182
180
  }
183
- process.stderr.write(`[provider] Auth error, re-reading config...\n`);
181
+ process.stderr.write(`[provider] Auth error, re-reading provider authentication...\n`);
184
182
  this.reloadApiKey();
185
183
  return await this._doSend(messages, model, tools, sendOpts);
186
184
  }