mixdog 0.9.87 → 0.9.89

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 (167) hide show
  1. package/package.json +13 -7
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +29 -35
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
  43. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
  44. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  46. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  50. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  51. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  54. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  55. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  56. package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  58. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  59. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  60. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  61. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  62. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  63. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  64. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  65. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  66. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  67. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  68. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  69. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  70. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  71. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  72. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  73. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  74. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  77. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  87. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  88. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
  89. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  90. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
  91. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
  93. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  94. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
  95. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  96. package/src/runtime/media/lanes.mjs +2 -2
  97. package/src/runtime/media/renditions.mjs +101 -17
  98. package/src/runtime/media/renditions.test.mjs +54 -0
  99. package/src/runtime/media/store.mjs +111 -43
  100. package/src/runtime/media/store.test.mjs +53 -11
  101. package/src/runtime/memory/index.mjs +26 -50
  102. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  103. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  104. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  105. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  106. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  107. package/src/runtime/memory/lib/http-router.mjs +4 -0
  108. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  109. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
  110. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  111. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  112. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  113. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  114. package/src/runtime/memory/tool-defs.mjs +3 -3
  115. package/src/runtime/shared/atomic-file.mjs +5 -1
  116. package/src/runtime/shared/child-guardian.mjs +73 -2
  117. package/src/runtime/shared/provider-api-key.mjs +22 -5
  118. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  119. package/src/session-runtime/config-helpers.mjs +6 -5
  120. package/src/session-runtime/lifecycle-api.mjs +16 -4
  121. package/src/session-runtime/media-api.mjs +31 -16
  122. package/src/session-runtime/prewarm.mjs +2 -24
  123. package/src/session-runtime/runtime-core.mjs +33 -15
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +0 -6
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +90 -36
  131. package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
  132. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  133. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  134. package/src/standalone/agent-tool.mjs +17 -17
  135. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  136. package/src/standalone/channel-daemon.mjs +8 -0
  137. package/src/standalone/explore-tool.mjs +1 -1
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +25 -3
  141. package/src/tui/App.jsx +5 -3
  142. package/src/tui/app/resume-picker.mjs +7 -2
  143. package/src/tui/app/route-pickers.mjs +11 -117
  144. package/src/tui/app/slash-dispatch.mjs +13 -3
  145. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  146. package/src/tui/app/transcript-window.mjs +17 -93
  147. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  148. package/src/tui/app/use-transcript-window.mjs +64 -114
  149. package/src/tui/components/Markdown.jsx +19 -2
  150. package/src/tui/components/Message.jsx +8 -3
  151. package/src/tui/components/ToolExecution.jsx +7 -3
  152. package/src/tui/dist/index.mjs +243 -223
  153. package/src/tui/engine/live-share.mjs +15 -5
  154. package/src/tui/engine/render-timing.mjs +2 -2
  155. package/src/tui/engine/session-api-ext.mjs +74 -4
  156. package/src/tui/engine/session-flow.mjs +3 -1
  157. package/src/tui/engine.mjs +2 -2
  158. package/src/tui/hooks/useSharedTick.mjs +0 -2
  159. package/src/tui/index.jsx +5 -5
  160. package/src/ui/statusline-agents.mjs +44 -11
  161. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  162. package/src/workflows/default/WORKFLOW.md +3 -3
  163. package/src/workflows/solo/WORKFLOW.md +5 -3
  164. package/src/runtime/media/index.mjs +0 -18
  165. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  166. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  167. package/src/standalone/agent-shard/shard-pool.mjs +0 -443
@@ -19,7 +19,8 @@ import {
19
19
  extractCachedTokens,
20
20
  appendAgentTrace,
21
21
  } from '../agent-trace.mjs';
22
- import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
22
+ import { typedStatusFrom } from './retry-classifier.mjs';
23
+ import { stampStreamOutcome, STREAM_TRANSPORTS } from './lib/stream-outcome.mjs';
23
24
  import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
24
25
  import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
25
26
  import {
@@ -51,7 +52,6 @@ import {
51
52
  } from './openai-ws-events.mjs';
52
53
  import {
53
54
  createActiveToolItemTracker,
54
- hasCompleteToolCall as sharedHasCompleteToolCall,
55
55
  } from './tool-stream-state.mjs';
56
56
  import {
57
57
  enableSessionTransportTracking,
@@ -125,6 +125,21 @@ function _writeWsLifecycleTrace(lifecycle) {
125
125
  process.stderr.write(`[ws-trace] t=${new Date().toISOString()} lifecycle=${lifecycle}\n`);
126
126
  }
127
127
 
128
+ // Wire-level `end_turn` on a terminal Responses frame (codex-rs
129
+ // codex-api/src/sse/responses.rs ResponseCompleted.end_turn: Option<bool>,
130
+ // consumed in core/src/session/turn.rs:2299 as "false ⇒ needs follow-up").
131
+ // The field is optional: only a real boolean is normalized; anything else —
132
+ // including a missing field — stays undefined so absence is preserved and no
133
+ // caller can mistake "server said nothing" for "server said true/false".
134
+ export function _endTurnFromEvent(event) {
135
+ if (!event || typeof event !== 'object') return undefined;
136
+ const fromResponse = event.response?.end_turn;
137
+ if (typeof fromResponse === 'boolean') return fromResponse;
138
+ const topLevel = event.end_turn;
139
+ if (typeof topLevel === 'boolean') return topLevel;
140
+ return undefined;
141
+ }
142
+
128
143
  function _responseItemKey(item, fallbackIndex = 0) {
129
144
  if (!item || typeof item !== 'object') return `primitive:${fallbackIndex}`;
130
145
  if (item.id) return `${item.type || 'item'}:id:${item.id}`;
@@ -309,12 +324,6 @@ export async function _streamResponse({
309
324
  // input starts streaming (before it lands in pendingCalls/toolCalls).
310
325
  // Gates partial-final SUCCESS so a stall mid tool-input never looks text-only.
311
326
  let _toolInFlight = false;
312
- // Early tool-call settle latch. Set when a fully-formed tool call is
313
- // available but the server never emits response.completed/response.done —
314
- // we resolve successfully anyway and surface `closeSocket` so the pool
315
- // discards (never reuses) the socket, which may still carry the missing
316
- // terminal frames as orphans.
317
- let _earlySettled = false;
318
327
  // Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
319
328
  // identical native function_call must fire onToolCall exactly once. Every
320
329
  // dispatch site routes through emitToolCallDedupe.
@@ -494,6 +503,9 @@ export async function _streamResponse({
494
503
  let usage;
495
504
  let stopReason = null;
496
505
  let incompleteReason = null;
506
+ // Normalized wire `end_turn` from the terminal frame; stays undefined
507
+ // unless the server actually supplied a boolean.
508
+ let endTurn;
497
509
  let done = false;
498
510
  let terminalError = null;
499
511
  // Mid-stream retry classifier needs to distinguish "stream died before we
@@ -640,19 +652,41 @@ export async function _streamResponse({
640
652
  firstMeaningfulTimer = null;
641
653
  }
642
654
  };
655
+ // Streamed partial state for a stream that ended WITHOUT the terminal
656
+ // response.completed/done frame. The transport never synthesizes a
657
+ // completion: it reports the continuation plus what it had, and the
658
+ // loop decides (partial text failure / explicit tool-call turn).
659
+ // `pendingToolUse` means "a tool input never finished streaming" — a
660
+ // fully assembled call is not pending.
661
+ const _attachWsPartialState = (err) => {
662
+ if (!err) return err;
663
+ // Fold the held leak-guard tail into `content` FIRST so the partial
664
+ // snapshot keeps legitimate trailing text; finish() then skips
665
+ // flushLeak (terminalError set) without losing it.
666
+ try { flushLeak(); } catch { /* best-effort */ }
667
+ try {
668
+ err.partialContent = content;
669
+ err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
670
+ err.pendingToolUse = pendingCalls.size > 0
671
+ || activeToolItems.size > 0
672
+ || _toolInFlight === true;
673
+ err.partialModel = model || undefined;
674
+ } catch { /* best-effort enrichment */ }
675
+ return err;
676
+ };
643
677
  const resetInterChunk = () => {
644
678
  if (interChunkTimer) clearTimeout(interChunkTimer);
645
679
  interChunkTimer = setTimeout(() => {
646
- // Early settle: a complete tool call is already captured with no
647
- // deferred salvage pending, so silence here is the missing
648
- // response.completed/done — resolve with the tool call instead
649
- // of a stall error the loop would retry.
650
- if (settleEarlyOnToolCall('inter_chunk')) return;
651
680
  if (process.env.MIXDOG_DEBUG_AGENT) {
652
681
  process.stderr.write(`[agent-trace] ws-timeout kind=inter-chunk afterMs=${interChunkMs}\n`);
653
682
  }
654
683
  traceWsTimeout('inter_chunk_timeout', interChunkMs);
655
684
  terminalError = new Error(`WS stream: inter-chunk inactivity for ${interChunkMs}ms`);
685
+ // No terminal frame arrived, so this is a CONTINUATION, never a
686
+ // synthesized completion. Attach the streamed partial state so
687
+ // the loop's partial/tool recovery can finalize it explicitly.
688
+ terminalError.streamStalled = true;
689
+ _attachWsPartialState(terminalError);
656
690
  try { socket.close(4000, 'inter_chunk_timeout'); } catch {}
657
691
  // Same half-open guard as the pre-stream watchdog: reject directly
658
692
  // so a stuck socket.close() cannot leave the Promise pending.
@@ -666,29 +700,12 @@ export async function _streamResponse({
666
700
  if (!semanticIdleEnabled) return;
667
701
  if (semanticIdleTimer) clearTimeout(semanticIdleTimer);
668
702
  semanticIdleTimer = setTimeout(() => {
669
- // Early settle before treating tool-work silence as a stall: if
670
- // a complete tool call is available and no deferred salvage
671
- // remains, the turn result is already final even without
672
- // response.completed/done.
673
- if (settleEarlyOnToolCall('semantic_idle')) return;
674
703
  traceWsTimeout('semantic_idle_timeout', semanticIdleMs);
675
704
  terminalError = streamStalledError('Responses WS', semanticIdleMs, { emittedToolCall: !!midState?.emittedToolCall });
676
705
  // Partial-final recovery: attach streamed partial state so
677
706
  // a wedged FINAL no-tool summary can be accepted as partial-final
678
707
  // success by the loop. pendingToolUse gates out mid-flight tools.
679
- // Fold the held leak-guard tail into `content` FIRST so the
680
- // partial snapshot below keeps legitimate trailing text; finish()
681
- // then skips flushLeak (terminalError set) without losing it.
682
- flushLeak();
683
- try {
684
- terminalError.partialContent = content;
685
- terminalError.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
686
- terminalError.pendingToolUse = pendingCalls.size > 0
687
- || !!midState?.emittedToolCall
688
- || toolCalls.length > 0
689
- || _toolInFlight === true;
690
- terminalError.partialModel = model || undefined;
691
- } catch { /* best-effort enrichment */ }
708
+ _attachWsPartialState(terminalError);
692
709
  try { terminalError.wsCloseCode = 4000; } catch {}
693
710
  try { socket.close(4000, 'semantic_idle_timeout'); } catch {}
694
711
  finish();
@@ -737,7 +754,33 @@ export async function _streamResponse({
737
754
  // Flush any partial-sentinel tail held back mid-stream so
738
755
  // legitimate trailing text is never lost (streamed-text path).
739
756
  cleanup();
740
- if (terminalError) { reject(terminalError); return; }
757
+ if (terminalError) {
758
+ // Canonical stream-outcome contract: ONE authoritative record
759
+ // of what this WS turn produced, stamped on every reject path
760
+ // (watchdog stall, abnormal close, server error, abort). Legacy
761
+ // aliases are preserved by stampStreamOutcome().
762
+ try {
763
+ stampStreamOutcome(terminalError, {
764
+ transport: STREAM_TRANSPORTS.WS,
765
+ provider: 'openai-responses',
766
+ terminalObserved: midState.sawCompleted === true,
767
+ continuation: midState.sawCompleted !== true,
768
+ textEmitted: midState.emittedText === true,
769
+ textObservedChars: content.length,
770
+ reasoningEmitted: midState.emittedReasoning === true,
771
+ toolCallsStarted: midState.startedToolCall === true || pendingCalls.size > 0,
772
+ toolCallsComplete: toolCalls.length,
773
+ toolCallsDispatched: midState.emittedToolCall === true ? Math.max(1, toolCalls.length) : 0,
774
+ pendingToolInput: pendingCalls.size > 0 || _toolInFlight === true,
775
+ userAbort: midState.userAbort === true,
776
+ // A terminal frame with end_turn=false keeps the same
777
+ // user turn open: terminal observed, still continuation.
778
+ ...(endTurn === false ? { continuationDeclared: true } : {}),
779
+ });
780
+ } catch { /* stamping is best-effort; aliases already set */ }
781
+ reject(terminalError);
782
+ return;
783
+ }
741
784
  resolve({
742
785
  content,
743
786
  model,
@@ -754,44 +797,13 @@ export async function _streamResponse({
754
797
  // for the WS path (sendViaWebSocket spreads this result
755
798
  // through to the provider caller unchanged).
756
799
  ...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
757
- // Early tool-call settle: tell the caller to drop (not pool)
758
- // this socket. The server may still emit the skipped
759
- // response.completed/done as orphan frames; reusing the socket
760
- // would read them as the next request's stream and wedge it.
761
- ...(_earlySettled ? { closeSocket: true } : {}),
762
800
  incompleteReason: incompleteReason || undefined,
801
+ // Only present when the terminal frame carried the wire field.
802
+ ...(typeof endTurn === 'boolean' ? { endTurn } : {}),
763
803
  responseId: responseId || undefined,
764
804
  serviceTier: responseServiceTier || undefined,
765
805
  });
766
806
  };
767
- // Early tool-call settle guard. A fully-formed tool call (real call_id +
768
- // name, not a deferred salvage placeholder) means the turn's actionable
769
- // result is already complete. When the server then goes silent without
770
- // response.completed/response.done, resolve successfully with that tool
771
- // call rather than surfacing a stall error the loop gates out via
772
- // pendingToolUse and retries. Returns true if it settled the stream.
773
- const hasCompleteToolCall = () => sharedHasCompleteToolCall({
774
- toolCalls,
775
- pendingSize: pendingCalls.size,
776
- activeSize: activeToolItems.size,
777
- toolInFlight: _toolInFlight,
778
- });
779
- const settleEarlyOnToolCall = (reason) => {
780
- if (done || terminalError) return false;
781
- if (!hasCompleteToolCall()) return false;
782
- _earlySettled = true;
783
- // Treat as a normal completion for downstream state: we have the
784
- // canonical tool call, only the terminal lifecycle frame is missing.
785
- midState.sawCompleted = true;
786
- midState.wsEarlySettle = reason;
787
- done = true;
788
- // Close and mark keep=false (via closeSocket in the resolve). The
789
- // late/never response.completed cannot then leak onto a pooled reuse.
790
- try { socket.close(4000, `early_settle_${reason}`); } catch {}
791
- finish();
792
- return true;
793
- };
794
-
795
807
  messageHandler = (data) => {
796
808
  const frameBytes = _incomingFrameByteLength(data);
797
809
  if (maxIncomingFrameBytes > 0 && frameBytes != null && frameBytes > maxIncomingFrameBytes) {
@@ -837,7 +849,10 @@ export async function _streamResponse({
837
849
  const err = new Error(event.error.message || 'Responses WS error');
838
850
  try {
839
851
  err.payload = event.error;
840
- populateHttpStatusFromMessage(err);
852
+ // Typed evidence only: a server error event without a
853
+ // numeric status stays unclassified (surfaced, not retried).
854
+ const typed = typedStatusFrom(event.error, event);
855
+ if (typed) err.httpStatus = typed;
841
856
  } catch {}
842
857
  terminalError = err;
843
858
  finish();
@@ -1187,6 +1202,13 @@ export async function _streamResponse({
1187
1202
  finish();
1188
1203
  break;
1189
1204
  }
1205
+ // Wire-level completion signal (optional). Captured after
1206
+ // salvage validation so a failed stream never reports a
1207
+ // turn-completion hint; absence stays undefined.
1208
+ {
1209
+ const wireEndTurn = _endTurnFromEvent(event);
1210
+ if (typeof wireEndTurn === 'boolean') endTurn = wireEndTurn;
1211
+ }
1190
1212
  midState.sawCompleted = true;
1191
1213
  done = true;
1192
1214
  finish();
@@ -1212,7 +1234,10 @@ export async function _streamResponse({
1212
1234
  new Error(`${errLabel} response.done failed: ${msg}`),
1213
1235
  { responseFailed: event },
1214
1236
  );
1215
- populateHttpStatusFromMessage(terminalError, msg);
1237
+ {
1238
+ const typed = typedStatusFrom(event.response?.error, event.error, event);
1239
+ if (typed) terminalError.httpStatus = typed;
1240
+ }
1216
1241
  done = true;
1217
1242
  finish();
1218
1243
  break;
@@ -1244,6 +1269,13 @@ export async function _streamResponse({
1244
1269
  finish();
1245
1270
  break;
1246
1271
  }
1272
+ // Success-shaped response.done (status '' or 'completed')
1273
+ // is the terminal frame for streams that never emit
1274
+ // response.completed — carry the same optional end_turn.
1275
+ {
1276
+ const wireEndTurn = _endTurnFromEvent(event);
1277
+ if (typeof wireEndTurn === 'boolean') endTurn = wireEndTurn;
1278
+ }
1247
1279
  midState.sawCompleted = true;
1248
1280
  done = true;
1249
1281
  finish();
@@ -1280,20 +1312,23 @@ export async function _streamResponse({
1280
1312
  terminalError = Object.assign(new Error(`${errLabel} response.failed: ${msg}`), {
1281
1313
  responseFailed: event,
1282
1314
  });
1283
- // Sniff the server message for transient/auth/permanent
1284
- // hints so the handshake / mid-stream retry classifiers
1285
- // can route by httpStatus. Without this, server-side
1286
- // events like "Our servers are currently overloaded"
1287
- // surfaced as unclassified errors and skipped the
1288
- // 5xx retry bucket entirely.
1289
- populateHttpStatusFromMessage(terminalError, msg);
1315
+ // TYPED status only (event/error payload). A failure whose
1316
+ // only evidence is message text is 'unknown' to the retry
1317
+ // classifiers and is surfaced instead of replayed.
1318
+ {
1319
+ const typed = typedStatusFrom(event.response?.error, event.error, event);
1320
+ if (typed) terminalError.httpStatus = typed;
1321
+ }
1290
1322
  finish();
1291
1323
  break;
1292
1324
  }
1293
1325
  case 'error': {
1294
1326
  const errMsg = String(event.message || event.error?.message || 'unknown');
1295
1327
  terminalError = new Error(`${errLabel} error: ${errMsg}`);
1296
- populateHttpStatusFromMessage(terminalError, errMsg);
1328
+ {
1329
+ const typed = typedStatusFrom(event.error, event);
1330
+ if (typed) terminalError.httpStatus = typed;
1331
+ }
1297
1332
  finish();
1298
1333
  break;
1299
1334
  }