mixdog 0.9.3 → 0.9.4
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 +7 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +23 -63
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +84 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +945 -8094
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +23 -101
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +19652 -18630
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +67 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -0,0 +1,1105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openai-ws-stream.mjs — WS Responses stream consumer + incremental-input
|
|
3
|
+
* delta engine for the OpenAI OAuth WebSocket transport.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from openai-oauth-ws.mjs:
|
|
6
|
+
* - request/response item matching + delta computation (_computeDelta,
|
|
7
|
+
* _sansInput, _logicalResponseItemMatch, ...) used to send only the
|
|
8
|
+
* input tail on a warm socket,
|
|
9
|
+
* - the per-response stream loop (_streamResponse): event parsing, idle/
|
|
10
|
+
* pre-created watchdogs, leak guard, tool-call dedupe, usage assembly.
|
|
11
|
+
*
|
|
12
|
+
* sendViaWebSocket (openai-oauth-ws.mjs) is the only production caller;
|
|
13
|
+
* scripts import parseToolSearchArgs/_logicalResponseItemMatch via the
|
|
14
|
+
* openai-oauth-ws.mjs re-exports.
|
|
15
|
+
*/
|
|
16
|
+
import { randomBytes } from 'crypto';
|
|
17
|
+
import {
|
|
18
|
+
extractCachedTokens,
|
|
19
|
+
appendAgentTrace,
|
|
20
|
+
} from '../agent-trace.mjs';
|
|
21
|
+
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
22
|
+
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
23
|
+
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
24
|
+
import {
|
|
25
|
+
PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS,
|
|
26
|
+
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
27
|
+
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
28
|
+
streamStalledError,
|
|
29
|
+
} from '../stall-policy.mjs';
|
|
30
|
+
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
31
|
+
import { _wsErrLabel } from './openai-ws-pool.mjs';
|
|
32
|
+
import {
|
|
33
|
+
_sansInput,
|
|
34
|
+
_stableStringify,
|
|
35
|
+
_cloneJson,
|
|
36
|
+
_logicalResponseItemMatch,
|
|
37
|
+
_stripResponseItemsFromHead,
|
|
38
|
+
_computeDelta,
|
|
39
|
+
_estimateFrameTokens,
|
|
40
|
+
} from './openai-ws-delta.mjs';
|
|
41
|
+
import {
|
|
42
|
+
_combineUsageWithWarmup,
|
|
43
|
+
_parseEvent,
|
|
44
|
+
_incompleteReasonFromEvent,
|
|
45
|
+
_isMaxOutputIncompleteReason,
|
|
46
|
+
_httpStatusFromWsClose,
|
|
47
|
+
} from './openai-ws-events.mjs';
|
|
48
|
+
|
|
49
|
+
// Facade re-exports: the delta/matching helpers and usage/event helpers below
|
|
50
|
+
// were extracted to openai-ws-delta.mjs / openai-ws-events.mjs (no behavior
|
|
51
|
+
// change). Re-exported here so existing importers of openai-ws-stream.mjs
|
|
52
|
+
// (openai-oauth-ws.mjs et al) keep resolving these symbols unchanged.
|
|
53
|
+
export {
|
|
54
|
+
_sansInput,
|
|
55
|
+
_stableStringify,
|
|
56
|
+
_cloneJson,
|
|
57
|
+
_logicalResponseItemMatch,
|
|
58
|
+
_stripResponseItemsFromHead,
|
|
59
|
+
_computeDelta,
|
|
60
|
+
_estimateFrameTokens,
|
|
61
|
+
_combineUsageWithWarmup,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Pre-`response.created` deadline. Once the socket is open and the
|
|
65
|
+
// response.create frame is sent, a healthy server emits response.created
|
|
66
|
+
// within seconds. If it stalls past this short bound the socket has wedged
|
|
67
|
+
// post-upgrade with zero server events — treat it as a fast, retryable
|
|
68
|
+
// first-byte timeout. This is the ONLY pre-stream watchdog; once any server
|
|
69
|
+
// event arrives the single inter-chunk idle timer below takes over.
|
|
70
|
+
// Only this short window is shortened; the post-`response.created`
|
|
71
|
+
// inter-chunk / reasoning span keeps the longer deadlines below.
|
|
72
|
+
// Positive-int coercion for per-call timeout overrides: finite > 0 → floor,
|
|
73
|
+
// else fallback.
|
|
74
|
+
export function _positiveInt(value, fallback = 0) {
|
|
75
|
+
const n = Number(value);
|
|
76
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
77
|
+
}
|
|
78
|
+
export const WS_PRE_RESPONSE_CREATED_MS = (() => {
|
|
79
|
+
const raw = process.env.MIXDOG_PROVIDER_WS_PRE_RESPONSE_CREATED_TIMEOUT_MS;
|
|
80
|
+
const n = Number(raw);
|
|
81
|
+
if (Number.isFinite(n) && n > 0) return Math.min(Math.max(n, 1_000), 120_000);
|
|
82
|
+
return 10_000;
|
|
83
|
+
})();
|
|
84
|
+
// Single inter-chunk idle timer. Resets on EVERY received frame — any frame,
|
|
85
|
+
// including metadata/keepalive, proves the socket is live.
|
|
86
|
+
export const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
|
|
87
|
+
const X_CODEX_TURN_STATE_HEADER = 'x-codex-turn-state';
|
|
88
|
+
|
|
89
|
+
function _responseItemKey(item, fallbackIndex = 0) {
|
|
90
|
+
if (!item || typeof item !== 'object') return `primitive:${fallbackIndex}`;
|
|
91
|
+
if (item.id) return `${item.type || 'item'}:id:${item.id}`;
|
|
92
|
+
if (item.call_id) return `${item.type || 'item'}:call:${item.call_id}`;
|
|
93
|
+
try { return `${item.type || 'item'}:json:${_stableStringify(item)}`; } catch {}
|
|
94
|
+
return `${item.type || 'item'}:${fallbackIndex}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _headerString(headers, name) {
|
|
98
|
+
if (!headers || typeof headers !== 'object') return null;
|
|
99
|
+
const wanted = String(name).toLowerCase();
|
|
100
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
101
|
+
if (String(key).toLowerCase() !== wanted) continue;
|
|
102
|
+
if (typeof value === 'string' && value) return value;
|
|
103
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
const first = value.find((item) => typeof item === 'string' && item);
|
|
106
|
+
if (first) return first;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _headerKeys(headers) {
|
|
113
|
+
if (!headers || typeof headers !== 'object') return [];
|
|
114
|
+
const keys = [];
|
|
115
|
+
for (const key of Object.keys(headers)) {
|
|
116
|
+
const normalized = String(key || '').trim().toLowerCase();
|
|
117
|
+
if (normalized) keys.push(normalized);
|
|
118
|
+
}
|
|
119
|
+
return [...new Set(keys)].sort();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _hasHeaderKey(headers, name) {
|
|
123
|
+
const wanted = String(name || '').trim().toLowerCase();
|
|
124
|
+
if (!wanted) return false;
|
|
125
|
+
return _headerKeys(headers).includes(wanted);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _captureTurnStateFromEvent(entry, event) {
|
|
129
|
+
if (!entry || entry.turnState || !event || typeof event !== 'object') return;
|
|
130
|
+
const turnState = _headerString(event.headers, X_CODEX_TURN_STATE_HEADER)
|
|
131
|
+
|| _headerString(event.response?.headers, X_CODEX_TURN_STATE_HEADER)
|
|
132
|
+
|| _headerString(event.response?.metadata?.headers, X_CODEX_TURN_STATE_HEADER)
|
|
133
|
+
|| _headerString(event.metadata?.headers, X_CODEX_TURN_STATE_HEADER);
|
|
134
|
+
if (turnState) entry.turnState = turnState;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _traceWsHeaderKeys(entry, event, midState, traceProvider, model) {
|
|
138
|
+
try {
|
|
139
|
+
if (!entry || !event || typeof event !== 'object') return;
|
|
140
|
+
const eventType = typeof event.type === 'string' ? event.type : '';
|
|
141
|
+
if (eventType !== 'response.created' && eventType !== 'response.metadata') return;
|
|
142
|
+
const topLevelHeaderKeys = _headerKeys(event.headers);
|
|
143
|
+
const responseHeaderKeys = _headerKeys(event.response?.headers);
|
|
144
|
+
const responseMetadataHeaderKeys = _headerKeys(event.response?.metadata?.headers);
|
|
145
|
+
const eventMetadataHeaderKeys = _headerKeys(event.metadata?.headers);
|
|
146
|
+
const hasAnyHeaders = topLevelHeaderKeys.length > 0
|
|
147
|
+
|| responseHeaderKeys.length > 0
|
|
148
|
+
|| responseMetadataHeaderKeys.length > 0
|
|
149
|
+
|| eventMetadataHeaderKeys.length > 0;
|
|
150
|
+
if (hasAnyHeaders && entry.wsHeaderKeysFinalTraced) return;
|
|
151
|
+
if (!hasAnyHeaders && entry.wsHeaderKeysEmptyTraced) return;
|
|
152
|
+
const iteration = Number(midState?.iteration);
|
|
153
|
+
const payload = {
|
|
154
|
+
provider: midState?.traceProvider || traceProvider,
|
|
155
|
+
transport: 'websocket',
|
|
156
|
+
event_type: eventType,
|
|
157
|
+
model: midState?.model || model || null,
|
|
158
|
+
top_level_header_keys: topLevelHeaderKeys,
|
|
159
|
+
response_header_keys: responseHeaderKeys,
|
|
160
|
+
response_metadata_header_keys: responseMetadataHeaderKeys,
|
|
161
|
+
event_metadata_header_keys: eventMetadataHeaderKeys,
|
|
162
|
+
has_turn_state_header: _hasHeaderKey(event.headers, X_CODEX_TURN_STATE_HEADER)
|
|
163
|
+
|| _hasHeaderKey(event.response?.headers, X_CODEX_TURN_STATE_HEADER)
|
|
164
|
+
|| _hasHeaderKey(event.response?.metadata?.headers, X_CODEX_TURN_STATE_HEADER)
|
|
165
|
+
|| _hasHeaderKey(event.metadata?.headers, X_CODEX_TURN_STATE_HEADER),
|
|
166
|
+
values_redacted: true,
|
|
167
|
+
};
|
|
168
|
+
appendAgentTrace({
|
|
169
|
+
sessionId: midState?.sessionId || null,
|
|
170
|
+
iteration: Number.isFinite(iteration) ? iteration : null,
|
|
171
|
+
kind: 'ws_header_keys',
|
|
172
|
+
provider: payload.provider,
|
|
173
|
+
model: payload.model,
|
|
174
|
+
transport: 'websocket',
|
|
175
|
+
event_type: eventType,
|
|
176
|
+
payload,
|
|
177
|
+
});
|
|
178
|
+
if (hasAnyHeaders) entry.wsHeaderKeysFinalTraced = true;
|
|
179
|
+
else entry.wsHeaderKeysEmptyTraced = true;
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
// _wsErrLabel moved to openai-ws-pool.mjs (imported above).
|
|
183
|
+
// Delta/matching helpers (_sansInput, _stableStringify, _cloneJson,
|
|
184
|
+
// _logicalResponseItemMatch, _stripResponseItemsFromHead, _computeDelta,
|
|
185
|
+
// _estimateFrameTokens) → openai-ws-delta.mjs. Usage/event helpers
|
|
186
|
+
// (_combineUsageWithWarmup, _parseEvent, _incompleteReasonFromEvent,
|
|
187
|
+
// _isMaxOutputIncompleteReason, _httpStatusFromWsClose) → openai-ws-events.mjs.
|
|
188
|
+
// All imported + re-exported at the top of this file.
|
|
189
|
+
// tool_search_call.arguments parse. Module-scope (exported) for direct test
|
|
190
|
+
// coverage. Same policy as the function_call_arguments.done path and
|
|
191
|
+
// openai-oauth _parseJsonObject —
|
|
192
|
+
// object passes through; null/non-string/empty/whitespace → {} (no args); a
|
|
193
|
+
// non-empty string that fails JSON.parse is deterministic bad JSON, surfaced
|
|
194
|
+
// as an invalid-args MARKER (not silently swallowed to {}) so the dispatch
|
|
195
|
+
// loop returns an is_error tool_result and the model self-corrects in the same
|
|
196
|
+
// turn.
|
|
197
|
+
export function parseToolSearchArgs(value) {
|
|
198
|
+
if (value && typeof value === 'object') {
|
|
199
|
+
// Reject arrays — the tool_search schema is an object
|
|
200
|
+
// ({query,select,limit}); an array must never pass through as args.
|
|
201
|
+
return Array.isArray(value) ? {} : value;
|
|
202
|
+
}
|
|
203
|
+
if (typeof value !== 'string' || !value.trim()) return {};
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(value);
|
|
206
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return makeInvalidToolArgsMarker(value, err instanceof Error ? err.message : String(err));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function _streamResponse({
|
|
213
|
+
entry,
|
|
214
|
+
externalSignal,
|
|
215
|
+
onStreamDelta,
|
|
216
|
+
onToolCall,
|
|
217
|
+
onTextDelta,
|
|
218
|
+
state,
|
|
219
|
+
logSuppressedReasoningDeltas = true,
|
|
220
|
+
traceProvider = 'openai-oauth',
|
|
221
|
+
_timeouts = null,
|
|
222
|
+
knownToolNames = null,
|
|
223
|
+
}) {
|
|
224
|
+
const errLabel = _wsErrLabel(traceProvider);
|
|
225
|
+
const socket = entry.socket;
|
|
226
|
+
const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
|
|
227
|
+
const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
|
|
228
|
+
const _streamingStart = Date.now();
|
|
229
|
+
let _firstDeltaEmitted = false;
|
|
230
|
+
let content = '';
|
|
231
|
+
let model = '';
|
|
232
|
+
let responseId = '';
|
|
233
|
+
let responseServiceTier = '';
|
|
234
|
+
const toolCalls = [];
|
|
235
|
+
const webSearchCalls = [];
|
|
236
|
+
const webSearchCallKeys = new Set();
|
|
237
|
+
const responseItemsAdded = [];
|
|
238
|
+
const responseItemKeys = new Set();
|
|
239
|
+
const citations = [];
|
|
240
|
+
const citationKeys = new Set();
|
|
241
|
+
const pendingCalls = new Map();
|
|
242
|
+
// Tool-work-in-flight flag: set the moment a function/custom tool call's
|
|
243
|
+
// input starts streaming (before it lands in pendingCalls/toolCalls).
|
|
244
|
+
// Gates partial-final SUCCESS so a stall mid tool-input never looks text-only.
|
|
245
|
+
let _toolInFlight = false;
|
|
246
|
+
// Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
|
|
247
|
+
// identical native function_call must fire onToolCall exactly once. Every
|
|
248
|
+
// dispatch site routes through emitToolCallDedupe.
|
|
249
|
+
const _toolDedupe = createToolCallDedupe();
|
|
250
|
+
const emitToolCallDedupe = (call) => {
|
|
251
|
+
if (!_toolDedupe.shouldDispatch(call?.name, call?.arguments)) return;
|
|
252
|
+
midState.emittedToolCall = true;
|
|
253
|
+
try { onToolCall?.(call); } catch {}
|
|
254
|
+
};
|
|
255
|
+
// Reasoning items collected from response.output_item.done (or salvaged
|
|
256
|
+
// from response.completed.response.output). The request still includes
|
|
257
|
+
// `reasoning.encrypted_content` so the server keeps emitting the blobs,
|
|
258
|
+
// but explicit input-side replay is INTENTIONALLY OMITTED in
|
|
259
|
+
// convertMessagesToResponsesInput (openai-oauth.mjs:233-238) — openai-oauth
|
|
260
|
+
// rejects the same `rs_*` id twice in one handshake session_id with a
|
|
261
|
+
// "Duplicate item" error. Server-side conversation state already carries
|
|
262
|
+
// the prefix forward across the WS_IDLE_MS window. The collected
|
|
263
|
+
// reasoningItems below are surfaced for trace/debugging only; they do
|
|
264
|
+
// not feed back into the next request body.
|
|
265
|
+
const reasoningItems = [];
|
|
266
|
+
let reasoningTextDeltaCount = 0;
|
|
267
|
+
let reasoningSummaryTextDeltaCount = 0;
|
|
268
|
+
let reasoningOtherDeltaCount = 0;
|
|
269
|
+
let reasoningDeltaLogEmitted = false;
|
|
270
|
+
const pushReasoningItem = (item) => {
|
|
271
|
+
if (!item || item.type !== 'reasoning') return;
|
|
272
|
+
if (!item.encrypted_content) return;
|
|
273
|
+
reasoningItems.push({
|
|
274
|
+
id: item.id || '',
|
|
275
|
+
encrypted_content: item.encrypted_content,
|
|
276
|
+
summary: Array.isArray(item.summary) ? item.summary : [],
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
const pushResponseItem = (item) => {
|
|
280
|
+
if (!item || typeof item !== 'object') return;
|
|
281
|
+
const key = _responseItemKey(item, responseItemsAdded.length);
|
|
282
|
+
if (responseItemKeys.has(key)) {
|
|
283
|
+
const existing = responseItemsAdded.find((candidate, index) => _responseItemKey(candidate, index) === key);
|
|
284
|
+
if (existing?.type === 'function_call' && item.type === 'function_call') {
|
|
285
|
+
if (!existing.call_id && item.call_id) existing.call_id = item.call_id;
|
|
286
|
+
if (!existing.name && item.name) existing.name = item.name;
|
|
287
|
+
if ((existing.arguments == null || existing.arguments === '') && item.arguments != null) existing.arguments = item.arguments;
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
responseItemKeys.add(key);
|
|
292
|
+
responseItemsAdded.push(_cloneJson(item));
|
|
293
|
+
};
|
|
294
|
+
const enrichFunctionCallResponseItem = ({ itemId = '', callId = '', name = '', argumentsText = '' } = {}) => {
|
|
295
|
+
for (const item of responseItemsAdded) {
|
|
296
|
+
if (item?.type !== 'function_call') continue;
|
|
297
|
+
if (itemId && item.id && item.id !== itemId) continue;
|
|
298
|
+
if (callId && item.call_id && item.call_id !== callId) continue;
|
|
299
|
+
if (!item.call_id && callId) item.call_id = callId;
|
|
300
|
+
if (!item.name && name) item.name = name;
|
|
301
|
+
if ((item.arguments == null || item.arguments === '') && argumentsText) item.arguments = argumentsText;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const pushCitation = (raw, fallbackTitle = '') => {
|
|
305
|
+
const url = raw?.url || raw?.uri || raw?.href || '';
|
|
306
|
+
if (!url || citationKeys.has(url)) return;
|
|
307
|
+
citationKeys.add(url);
|
|
308
|
+
citations.push({
|
|
309
|
+
title: raw?.title || fallbackTitle || '',
|
|
310
|
+
url,
|
|
311
|
+
snippet: raw?.snippet || raw?.text || raw?.description || '',
|
|
312
|
+
source: 'openai-oauth',
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
const pushOutputTextAnnotations = (contentPart) => {
|
|
316
|
+
const annotations = Array.isArray(contentPart?.annotations) ? contentPart.annotations : [];
|
|
317
|
+
for (const annotation of annotations) pushCitation(annotation);
|
|
318
|
+
};
|
|
319
|
+
const pushWebSearchCall = (item) => {
|
|
320
|
+
if (!item || item.type !== 'web_search_call') return;
|
|
321
|
+
let key = item.id || '';
|
|
322
|
+
if (!key) {
|
|
323
|
+
try { key = JSON.stringify(item.action || item); } catch { key = `${webSearchCalls.length}`; }
|
|
324
|
+
}
|
|
325
|
+
if (webSearchCallKeys.has(key)) return;
|
|
326
|
+
webSearchCallKeys.add(key);
|
|
327
|
+
webSearchCalls.push({
|
|
328
|
+
id: item.id || '',
|
|
329
|
+
status: item.status || '',
|
|
330
|
+
action: item.action || null,
|
|
331
|
+
});
|
|
332
|
+
const action = item.action || {};
|
|
333
|
+
if (action.url) pushCitation({ url: action.url, title: action.query || '' });
|
|
334
|
+
if (Array.isArray(action.urls)) {
|
|
335
|
+
for (const url of action.urls) pushCitation({ url, title: action.query || '' });
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const pushCustomToolCall = (item) => {
|
|
339
|
+
const call = customToolCallFromResponseItem(item);
|
|
340
|
+
if (!call || toolCalls.some((existing) => existing.id === call.id)) return;
|
|
341
|
+
toolCalls.push(call);
|
|
342
|
+
emitToolCallDedupe(call);
|
|
343
|
+
};
|
|
344
|
+
const pushToolSearchCall = (item) => {
|
|
345
|
+
if (!item || item.type !== 'tool_search_call') return;
|
|
346
|
+
const callId = item.call_id || item.id || '';
|
|
347
|
+
if (!callId || toolCalls.some((call) => call.id === callId)) return;
|
|
348
|
+
const call = {
|
|
349
|
+
id: callId,
|
|
350
|
+
name: 'tool_search',
|
|
351
|
+
arguments: parseToolSearchArgs(item.arguments),
|
|
352
|
+
nativeType: 'tool_search_call',
|
|
353
|
+
};
|
|
354
|
+
toolCalls.push(call);
|
|
355
|
+
emitToolCallDedupe(call);
|
|
356
|
+
};
|
|
357
|
+
// Leaked tool-call guard. The model sometimes emits a tool call as plain
|
|
358
|
+
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
359
|
+
// `<|channel|>...to=functions.NAME...<|call|>`) inside
|
|
360
|
+
// `response.output_text.delta` instead of a native function_call. Route
|
|
361
|
+
// text through the guard so leaked calls are suppressed from the visible
|
|
362
|
+
// stream, synthesized (native `call_...` id shape), and dispatched like
|
|
363
|
+
// native ones. Additive: the native function_call path is untouched.
|
|
364
|
+
const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
|
|
365
|
+
const dispatchLeakedCall = (recovered) => {
|
|
366
|
+
let args = recovered?.arguments;
|
|
367
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
368
|
+
const call = {
|
|
369
|
+
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
370
|
+
name: recovered.name,
|
|
371
|
+
arguments: args,
|
|
372
|
+
};
|
|
373
|
+
if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
|
|
374
|
+
toolCalls.push(call);
|
|
375
|
+
midState.emittedToolCall = true;
|
|
376
|
+
try { onToolCall?.(call); } catch {}
|
|
377
|
+
};
|
|
378
|
+
const relayLeakText = (delta) => {
|
|
379
|
+
if (!leakGuard.enabled) {
|
|
380
|
+
content += delta || '';
|
|
381
|
+
if (delta && onTextDelta) {
|
|
382
|
+
if (state) state.emittedText = true;
|
|
383
|
+
try { onTextDelta(delta); } catch {}
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const { text, calls } = leakGuard.push(delta);
|
|
388
|
+
if (text) {
|
|
389
|
+
content += text;
|
|
390
|
+
if (onTextDelta) {
|
|
391
|
+
if (state) state.emittedText = true;
|
|
392
|
+
try { onTextDelta(text); } catch {}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
396
|
+
};
|
|
397
|
+
const flushLeak = () => {
|
|
398
|
+
if (!leakGuard.enabled) return;
|
|
399
|
+
const { text, calls } = leakGuard.flush();
|
|
400
|
+
if (text) {
|
|
401
|
+
content += text;
|
|
402
|
+
if (onTextDelta) {
|
|
403
|
+
if (state) state.emittedText = true;
|
|
404
|
+
try { onTextDelta(text); } catch {}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
408
|
+
};
|
|
409
|
+
const logReasoningDeltaSuppression = () => {
|
|
410
|
+
if (!logSuppressedReasoningDeltas) return;
|
|
411
|
+
const total = reasoningTextDeltaCount + reasoningSummaryTextDeltaCount + reasoningOtherDeltaCount;
|
|
412
|
+
if (reasoningDeltaLogEmitted || total === 0) return;
|
|
413
|
+
reasoningDeltaLogEmitted = true;
|
|
414
|
+
process.stderr.write(`[openai-oauth-ws] suppressed reasoning text deltas from user content count=${total} text=${reasoningTextDeltaCount} summary=${reasoningSummaryTextDeltaCount} other=${reasoningOtherDeltaCount}\n`);
|
|
415
|
+
};
|
|
416
|
+
let usage;
|
|
417
|
+
let stopReason = null;
|
|
418
|
+
let incompleteReason = null;
|
|
419
|
+
let done = false;
|
|
420
|
+
let terminalError = null;
|
|
421
|
+
// Mid-stream retry classifier needs to distinguish "stream died before we
|
|
422
|
+
// even saw response.created" from "stream died after we had a partial
|
|
423
|
+
// response but before completion". Mutate the shared state object so the
|
|
424
|
+
// caller can inspect flags on the error path without us having to attach
|
|
425
|
+
// them manually at every reject site.
|
|
426
|
+
const midState = state || {};
|
|
427
|
+
midState.sawResponseCreated = midState.sawResponseCreated || false;
|
|
428
|
+
midState.sawCompleted = midState.sawCompleted || false;
|
|
429
|
+
midState.wsCloseCode = null;
|
|
430
|
+
midState.responseFailedPayload = null;
|
|
431
|
+
let idleTimer = null;
|
|
432
|
+
let keepaliveTimer = null;
|
|
433
|
+
let abortHandler = null;
|
|
434
|
+
let messageHandler = null;
|
|
435
|
+
let closeHandler = null;
|
|
436
|
+
let errorHandler = null;
|
|
437
|
+
// SEMANTIC idle timer: distinct from the inter-chunk
|
|
438
|
+
// timer, which resets on EVERY frame (rate_limits/metadata/keepalive keep
|
|
439
|
+
// the socket "alive"). This timer resets ONLY on meaningful output deltas
|
|
440
|
+
// (text/reasoning/tool args — the same events that call onStreamDelta) so a
|
|
441
|
+
// stream that emits some deltas then goes silent (server keepalive frames
|
|
442
|
+
// only) trips a short, named terminal StreamStalledError instead of coasting
|
|
443
|
+
// to the 300s inter-chunk cap / 30-min agent watchdog.
|
|
444
|
+
let semanticIdleTimer = null;
|
|
445
|
+
const semanticIdleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
446
|
+
const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
|
|
447
|
+
|
|
448
|
+
return new Promise((resolve, reject) => {
|
|
449
|
+
// Pre-stream watchdog: the timer fires if the server never sends a
|
|
450
|
+
// first event (response.created) within preResponseCreatedMs
|
|
451
|
+
// after our last frame. The socket is open and the response.create
|
|
452
|
+
// frame was sent, but no server event has come back — a wedged
|
|
453
|
+
// post-upgrade socket. Healthy servers ack within seconds, so this
|
|
454
|
+
// window is intentionally short (WS_PRE_RESPONSE_CREATED_MS, ~10s).
|
|
455
|
+
// Once ANY server event arrives, resetIdle() cancels this watchdog and
|
|
456
|
+
// the single inter-chunk idle timer takes over — silent gaps
|
|
457
|
+
// mid-reasoning (openai-oauth spending 50s+ producing reasoning
|
|
458
|
+
// tokens) are normal and should not abort the turn.
|
|
459
|
+
const armPreStreamWatchdog = () => {
|
|
460
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
461
|
+
idleTimer = setTimeout(() => {
|
|
462
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
463
|
+
process.stderr.write(`[agent-trace] ws-timeout kind=first-byte afterMs=${preResponseCreatedMs}\n`);
|
|
464
|
+
}
|
|
465
|
+
traceWsTimeout('first_byte_timeout', preResponseCreatedMs);
|
|
466
|
+
const err = new Error(`WS stream: no first server event within ${preResponseCreatedMs}ms`);
|
|
467
|
+
// Tag the close code so _classifyMidstreamError sees a 4000
|
|
468
|
+
// (our local pre-stream watchdog code) and routes through
|
|
469
|
+
// the post-upgrade-no-first-event retryable bucket.
|
|
470
|
+
err.wsCloseCode = 4000;
|
|
471
|
+
// Tag the error object itself (not just midState): the warmup
|
|
472
|
+
// path streams under a separate warmupState and rethrows on
|
|
473
|
+
// timeout BEFORE it can copy flags to the outer midState, so the
|
|
474
|
+
// outer catch's _classifyMidstreamError would otherwise see
|
|
475
|
+
// sawResponseCreated=false + close 4000 and hit the pre-created
|
|
476
|
+
// deny gate. err.firstByteTimeout makes both paths retryable.
|
|
477
|
+
err.firstByteTimeout = true;
|
|
478
|
+
midState.firstByteTimeout = true;
|
|
479
|
+
terminalError = err;
|
|
480
|
+
try { socket.close(4000, 'first_byte_timeout'); } catch {}
|
|
481
|
+
// socket.close() may not settle a half-open WS (closeHandler never
|
|
482
|
+
// fires) — reject directly so the turn retries instead of hanging
|
|
483
|
+
// until the 600s watchdog. finish() is idempotent (Promise settles
|
|
484
|
+
// once; cleanup is null-safe).
|
|
485
|
+
finish();
|
|
486
|
+
}, preResponseCreatedMs);
|
|
487
|
+
};
|
|
488
|
+
let interChunkTimer = null;
|
|
489
|
+
const traceWsTimeout = (event, timeoutMs) => {
|
|
490
|
+
try {
|
|
491
|
+
const iteration = Number(midState.iteration);
|
|
492
|
+
const attemptIndex = Number(midState.attemptIndex);
|
|
493
|
+
const payload = {
|
|
494
|
+
provider: midState.traceProvider || traceProvider,
|
|
495
|
+
transport: 'websocket',
|
|
496
|
+
event,
|
|
497
|
+
timeout_ms: timeoutMs,
|
|
498
|
+
elapsed_ms: Date.now() - _streamingStart,
|
|
499
|
+
model: midState.model || model || null,
|
|
500
|
+
attempt_index: Number.isFinite(attemptIndex) ? attemptIndex : null,
|
|
501
|
+
warmup: midState.warmup === true,
|
|
502
|
+
saw_response_created: midState.sawResponseCreated === true,
|
|
503
|
+
};
|
|
504
|
+
appendAgentTrace({
|
|
505
|
+
sessionId: midState.sessionId || null,
|
|
506
|
+
iteration: Number.isFinite(iteration) ? iteration : null,
|
|
507
|
+
kind: 'ws_timeout',
|
|
508
|
+
...payload,
|
|
509
|
+
payload,
|
|
510
|
+
});
|
|
511
|
+
} catch {}
|
|
512
|
+
};
|
|
513
|
+
const clearPreStreamWatchdog = () => {
|
|
514
|
+
if (idleTimer) {
|
|
515
|
+
clearTimeout(idleTimer);
|
|
516
|
+
idleTimer = null;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const resetInterChunk = () => {
|
|
520
|
+
if (interChunkTimer) clearTimeout(interChunkTimer);
|
|
521
|
+
interChunkTimer = setTimeout(() => {
|
|
522
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
523
|
+
process.stderr.write(`[agent-trace] ws-timeout kind=inter-chunk afterMs=${interChunkMs}\n`);
|
|
524
|
+
}
|
|
525
|
+
traceWsTimeout('inter_chunk_timeout', interChunkMs);
|
|
526
|
+
terminalError = new Error(`WS stream: inter-chunk inactivity for ${interChunkMs}ms`);
|
|
527
|
+
try { socket.close(4000, 'inter_chunk_timeout'); } catch {}
|
|
528
|
+
// Same half-open guard as the pre-stream watchdog: reject directly
|
|
529
|
+
// so a stuck socket.close() cannot leave the Promise pending.
|
|
530
|
+
finish();
|
|
531
|
+
}, interChunkMs);
|
|
532
|
+
};
|
|
533
|
+
// pi per-event idle: (re)armed only on meaningful output deltas via
|
|
534
|
+
// bumpSemanticIdle(). Keepalive/metadata frames DON'T touch it, so a
|
|
535
|
+
// deltas-then-silent wedge trips this short semantic window.
|
|
536
|
+
const resetSemanticIdle = () => {
|
|
537
|
+
if (!semanticIdleEnabled) return;
|
|
538
|
+
if (semanticIdleTimer) clearTimeout(semanticIdleTimer);
|
|
539
|
+
semanticIdleTimer = setTimeout(() => {
|
|
540
|
+
traceWsTimeout('semantic_idle_timeout', semanticIdleMs);
|
|
541
|
+
terminalError = streamStalledError('Responses WS', semanticIdleMs, { emittedToolCall: !!midState?.emittedToolCall });
|
|
542
|
+
// Partial-final recovery: attach streamed partial state so
|
|
543
|
+
// a wedged FINAL no-tool summary can be accepted as partial-final
|
|
544
|
+
// success by the loop. pendingToolUse gates out mid-flight tools.
|
|
545
|
+
try {
|
|
546
|
+
terminalError.partialContent = content;
|
|
547
|
+
terminalError.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
548
|
+
terminalError.pendingToolUse = pendingCalls.size > 0
|
|
549
|
+
|| !!midState?.emittedToolCall
|
|
550
|
+
|| toolCalls.length > 0
|
|
551
|
+
|| _toolInFlight === true;
|
|
552
|
+
terminalError.partialModel = model || undefined;
|
|
553
|
+
} catch { /* best-effort enrichment */ }
|
|
554
|
+
try { terminalError.wsCloseCode = 4000; } catch {}
|
|
555
|
+
try { socket.close(4000, 'semantic_idle_timeout'); } catch {}
|
|
556
|
+
finish();
|
|
557
|
+
}, semanticIdleMs);
|
|
558
|
+
try { semanticIdleTimer.unref?.(); } catch {}
|
|
559
|
+
};
|
|
560
|
+
// Single idle reset — called on EVERY parsed server event (matches
|
|
561
|
+
// codex, which resets one idle timer on every received WS frame). Any
|
|
562
|
+
// frame proves the socket is live; there is no separate "meaningful
|
|
563
|
+
// output" gate. Also clears the pre-stream watchdog defensively in case
|
|
564
|
+
// the first event is not response.created.
|
|
565
|
+
const resetIdle = () => {
|
|
566
|
+
clearPreStreamWatchdog();
|
|
567
|
+
resetInterChunk();
|
|
568
|
+
};
|
|
569
|
+
// Meaningful-output progress bump: called by the same delta cases that
|
|
570
|
+
// call onStreamDelta (text/reasoning/tool args). Arms the semantic idle.
|
|
571
|
+
const bumpSemanticIdle = () => { resetSemanticIdle(); };
|
|
572
|
+
const cleanup = () => {
|
|
573
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
574
|
+
if (interChunkTimer) { clearTimeout(interChunkTimer); interChunkTimer = null; }
|
|
575
|
+
if (semanticIdleTimer) { clearTimeout(semanticIdleTimer); semanticIdleTimer = null; }
|
|
576
|
+
if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
|
|
577
|
+
if (messageHandler) socket.off('message', messageHandler);
|
|
578
|
+
if (closeHandler) socket.off('close', closeHandler);
|
|
579
|
+
if (errorHandler) socket.off('error', errorHandler);
|
|
580
|
+
if (abortHandler && externalSignal) externalSignal.removeEventListener('abort', abortHandler);
|
|
581
|
+
};
|
|
582
|
+
const finish = () => {
|
|
583
|
+
logReasoningDeltaSuppression();
|
|
584
|
+
// Flush any partial-sentinel tail held back mid-stream so
|
|
585
|
+
// legitimate trailing text is never lost (streamed-text path).
|
|
586
|
+
flushLeak();
|
|
587
|
+
cleanup();
|
|
588
|
+
if (terminalError) { reject(terminalError); return; }
|
|
589
|
+
resolve({
|
|
590
|
+
content,
|
|
591
|
+
model,
|
|
592
|
+
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
593
|
+
responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
|
|
594
|
+
// Dedupe by name+args (Fix 2, array side) so an identical
|
|
595
|
+
// synthetic-leaked + native pair can't run the tool twice.
|
|
596
|
+
toolCalls: toolCalls.length ? dedupeToolCallList(toolCalls) : undefined,
|
|
597
|
+
citations: citations.length ? citations : undefined,
|
|
598
|
+
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
599
|
+
usage,
|
|
600
|
+
stopReason: stopReason || undefined,
|
|
601
|
+
// P1 audit fix: mirror the HTTP/SSE fallback's truncated flag
|
|
602
|
+
// for the WS path (sendViaWebSocket spreads this result
|
|
603
|
+
// through to the provider caller unchanged).
|
|
604
|
+
...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
|
|
605
|
+
incompleteReason: incompleteReason || undefined,
|
|
606
|
+
responseId: responseId || undefined,
|
|
607
|
+
serviceTier: responseServiceTier || undefined,
|
|
608
|
+
});
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
messageHandler = (data) => {
|
|
612
|
+
resetIdle();
|
|
613
|
+
// resetIdle() above resets the SINGLE inter-chunk idle timer on
|
|
614
|
+
// EVERY received frame — response.created, metadata,
|
|
615
|
+
// rate_limits, and all deltas keep the socket alive. Separately, do
|
|
616
|
+
// NOT call onStreamDelta for every frame — metadata/keepalive frames
|
|
617
|
+
// must not reset the agent stall watchdog's lastStreamDeltaAt. Only
|
|
618
|
+
// meaningful output (text delta / tool call) updates that timestamp.
|
|
619
|
+
const text = typeof data === 'string' ? data : data.toString('utf-8');
|
|
620
|
+
const event = _parseEvent(text);
|
|
621
|
+
if (!event) return;
|
|
622
|
+
_traceWsHeaderKeys(entry, event, midState, traceProvider, model);
|
|
623
|
+
_captureTurnStateFromEvent(entry, event);
|
|
624
|
+
if (event.error) {
|
|
625
|
+
const err = new Error(event.error.message || 'Responses WS error');
|
|
626
|
+
try {
|
|
627
|
+
err.payload = event.error;
|
|
628
|
+
populateHttpStatusFromMessage(err);
|
|
629
|
+
} catch {}
|
|
630
|
+
terminalError = err;
|
|
631
|
+
finish();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (typeof event.type !== 'string') return;
|
|
635
|
+
switch (event.type) {
|
|
636
|
+
case 'response.created':
|
|
637
|
+
midState.sawResponseCreated = true;
|
|
638
|
+
if (event.response?.model) model = event.response.model;
|
|
639
|
+
if (event.response?.id) responseId = event.response.id;
|
|
640
|
+
// Server ack (first event). resetIdle() at the top of
|
|
641
|
+
// messageHandler already cleared the pre-stream watchdog and
|
|
642
|
+
// armed the single idle timer — no extra bookkeeping here.
|
|
643
|
+
break;
|
|
644
|
+
case 'response.output_text.delta':
|
|
645
|
+
try {
|
|
646
|
+
if (!_firstDeltaEmitted) {
|
|
647
|
+
_firstDeltaEmitted = true;
|
|
648
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
649
|
+
process.stderr.write(`[agent-trace] ws-first-delta sinceStreaming=${Date.now() - _streamingStart}ms\n`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
onStreamDelta?.();
|
|
653
|
+
} catch {}
|
|
654
|
+
bumpSemanticIdle();
|
|
655
|
+
// Live text relay (gateway): forward the raw text chunk so
|
|
656
|
+
// the client renders first tokens before the final replay.
|
|
657
|
+
// Tool-call/argument deltas intentionally stay off this path.
|
|
658
|
+
// Invariant: once a non-empty chunk has been relayed live it
|
|
659
|
+
// cannot be withdrawn, so flag the attempt so a later
|
|
660
|
+
// mid-stream/truncated failure is NOT retried (retry would
|
|
661
|
+
// concatenate a second attempt onto rendered text).
|
|
662
|
+
// Routed through the leaked-tool-call guard: appends to
|
|
663
|
+
// `content`, forwards visible text via onTextDelta, and
|
|
664
|
+
// recovers/dispatches any leaked known-tool call.
|
|
665
|
+
relayLeakText(event.delta || '');
|
|
666
|
+
break;
|
|
667
|
+
case 'response.reasoning_text.delta':
|
|
668
|
+
case 'response.reasoning_summary_text.delta':
|
|
669
|
+
if (event.type === 'response.reasoning_text.delta') reasoningTextDeltaCount += 1;
|
|
670
|
+
else reasoningSummaryTextDeltaCount += 1;
|
|
671
|
+
// Reasoning text is live model progress — refresh
|
|
672
|
+
// lastStreamDeltaAt so stream-watchdog does not flag a
|
|
673
|
+
// long reasoning span as a stall. The local WS idle timer
|
|
674
|
+
// was already reset by resetIdle() at the top of
|
|
675
|
+
// messageHandler. Reasoning is still suppressed from user
|
|
676
|
+
// content (no `content +=` here).
|
|
677
|
+
try { onStreamDelta?.(); } catch {}
|
|
678
|
+
bumpSemanticIdle();
|
|
679
|
+
break;
|
|
680
|
+
case 'response.output_item.added':
|
|
681
|
+
if (event.item?.type === 'function_call') {
|
|
682
|
+
pendingCalls.set(event.item.id || '', {
|
|
683
|
+
name: event.item.name || '',
|
|
684
|
+
callId: event.item.call_id || '',
|
|
685
|
+
});
|
|
686
|
+
_toolInFlight = true;
|
|
687
|
+
} else if (event.item?.type === 'custom_tool_call') {
|
|
688
|
+
_toolInFlight = true;
|
|
689
|
+
} else if (event.item?.type === 'tool_search_call') {
|
|
690
|
+
// Mark tool_search in-flight at item-added time, same
|
|
691
|
+
// as function_call/custom_tool_call above, so the
|
|
692
|
+
// semantic-idle stall gate's pendingToolUse never
|
|
693
|
+
// drops a mid-flight tool_search before
|
|
694
|
+
// response.output_item.done.
|
|
695
|
+
_toolInFlight = true;
|
|
696
|
+
}
|
|
697
|
+
break;
|
|
698
|
+
case 'response.function_call_arguments.delta':
|
|
699
|
+
_toolInFlight = true;
|
|
700
|
+
try { onStreamDelta?.(); } catch {}
|
|
701
|
+
bumpSemanticIdle();
|
|
702
|
+
break;
|
|
703
|
+
case 'response.custom_tool_call_input.delta':
|
|
704
|
+
_toolInFlight = true;
|
|
705
|
+
try { onStreamDelta?.(); } catch {}
|
|
706
|
+
bumpSemanticIdle();
|
|
707
|
+
break;
|
|
708
|
+
case 'response.function_call_arguments.done': {
|
|
709
|
+
const itemId = event.item_id || '';
|
|
710
|
+
const pending = pendingCalls.get(itemId);
|
|
711
|
+
// function_call_arguments.done is a completion signal:
|
|
712
|
+
// empty/whitespace → no args ({}); a non-empty string that
|
|
713
|
+
// fails JSON.parse is deterministic bad JSON. Surface an
|
|
714
|
+
// invalid-args MARKER (not silent {}) so the dispatch loop
|
|
715
|
+
// returns an is_error tool_result and the model re-issues
|
|
716
|
+
// valid JSON in the same turn.
|
|
717
|
+
let args = {};
|
|
718
|
+
{
|
|
719
|
+
const _argText = typeof event.arguments === 'string' ? event.arguments : '';
|
|
720
|
+
if (_argText.trim() !== '') {
|
|
721
|
+
try {
|
|
722
|
+
args = JSON.parse(_argText);
|
|
723
|
+
} catch (err) {
|
|
724
|
+
args = makeInvalidToolArgsMarker(_argText, err instanceof Error ? err.message : String(err));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
enrichFunctionCallResponseItem({
|
|
729
|
+
itemId,
|
|
730
|
+
callId: pending?.callId || event.call_id || '',
|
|
731
|
+
name: pending?.name || event.name || '',
|
|
732
|
+
argumentsText: event.arguments || JSON.stringify(args),
|
|
733
|
+
});
|
|
734
|
+
if (pending?.callId && pending?.name) {
|
|
735
|
+
const call = { id: pending.callId, name: pending.name, arguments: args };
|
|
736
|
+
toolCalls.push(call);
|
|
737
|
+
emitToolCallDedupe(call);
|
|
738
|
+
} else {
|
|
739
|
+
// Synthesizing a `tc_${Date.now()}` callId here would
|
|
740
|
+
// make the next turn fail to match the model's
|
|
741
|
+
// function_call_output reference. Defer instead and
|
|
742
|
+
// salvage call_id/name from the final
|
|
743
|
+
// response.completed.output bundle below. If salvage
|
|
744
|
+
// also fails we fail the stream explicitly — masking
|
|
745
|
+
// the gap with a synthetic id just shifts the failure
|
|
746
|
+
// one turn later under a confusing "No tool output
|
|
747
|
+
// found for function call" error.
|
|
748
|
+
toolCalls.push({
|
|
749
|
+
id: pending?.callId || '',
|
|
750
|
+
name: pending?.name || '',
|
|
751
|
+
arguments: args,
|
|
752
|
+
_pendingItemId: itemId,
|
|
753
|
+
_deferred: true,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
try { onStreamDelta?.(); } catch {}
|
|
757
|
+
bumpSemanticIdle();
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
case 'response.output_item.done':
|
|
761
|
+
pushResponseItem(event.item);
|
|
762
|
+
// function_call / output_text already captured via their
|
|
763
|
+
// dedicated streaming events. The one shape we still need
|
|
764
|
+
// here is `reasoning` — carries encrypted_content that
|
|
765
|
+
// must be replayed on the next input to keep the openai-oauth
|
|
766
|
+
// server-side prompt cache prefix warm.
|
|
767
|
+
if (event.item?.type === 'reasoning') pushReasoningItem(event.item);
|
|
768
|
+
if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
|
|
769
|
+
if (event.item?.type === 'tool_search_call') {
|
|
770
|
+
pushToolSearchCall(event.item);
|
|
771
|
+
}
|
|
772
|
+
if (event.item?.type === 'custom_tool_call') {
|
|
773
|
+
pushCustomToolCall(event.item);
|
|
774
|
+
}
|
|
775
|
+
break;
|
|
776
|
+
case 'response.completed': {
|
|
777
|
+
const completedServiceTier = event.response?.service_tier || event.response?.serviceTier || '';
|
|
778
|
+
if (completedServiceTier) responseServiceTier = String(completedServiceTier);
|
|
779
|
+
if (event.response?.usage) {
|
|
780
|
+
const u = event.response.usage;
|
|
781
|
+
const rawUsage = responseServiceTier
|
|
782
|
+
? { ...u, service_tier: responseServiceTier }
|
|
783
|
+
: u;
|
|
784
|
+
usage = {
|
|
785
|
+
inputTokens: u.input_tokens || 0,
|
|
786
|
+
outputTokens: u.output_tokens || 0,
|
|
787
|
+
cachedTokens: extractCachedTokens(u),
|
|
788
|
+
// openai-oauth reports input_tokens as the total
|
|
789
|
+
// prompt volume (cached portion is a subset, not
|
|
790
|
+
// additive). Alias into the cross-provider
|
|
791
|
+
// `promptTokens` field so downstream loggers have
|
|
792
|
+
// uniform semantics.
|
|
793
|
+
promptTokens: u.input_tokens || 0,
|
|
794
|
+
raw: rawUsage,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
if (!model && event.response?.model) model = event.response.model;
|
|
798
|
+
if (!responseId && event.response?.id) responseId = event.response.id;
|
|
799
|
+
if (event.response?.output) {
|
|
800
|
+
for (const item of event.response.output) {
|
|
801
|
+
pushResponseItem(item);
|
|
802
|
+
if (!content && item.type === 'message') {
|
|
803
|
+
for (const c of item.content || []) {
|
|
804
|
+
if (c.type === 'output_text') {
|
|
805
|
+
// Completed-output fallback (no streamed
|
|
806
|
+
// text). Route through the leak guard so
|
|
807
|
+
// a tool call leaked only in the final
|
|
808
|
+
// bundle is recovered, not surfaced as
|
|
809
|
+
// visible content. final=true → full flush.
|
|
810
|
+
if (leakGuard.enabled) {
|
|
811
|
+
const { text, calls } = leakGuard.push(c.text || '', true);
|
|
812
|
+
content += text;
|
|
813
|
+
for (const lc of calls) dispatchLeakedCall(lc);
|
|
814
|
+
} else {
|
|
815
|
+
content += c.text || '';
|
|
816
|
+
}
|
|
817
|
+
pushOutputTextAnnotations(c);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (item.type === 'message') {
|
|
822
|
+
for (const c of item.content || []) {
|
|
823
|
+
if (c.type === 'output_text') pushOutputTextAnnotations(c);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (item.type === 'web_search_call') pushWebSearchCall(item);
|
|
827
|
+
if (item.type === 'tool_search_call') pushToolSearchCall(item);
|
|
828
|
+
if (item.type === 'custom_tool_call') pushCustomToolCall(item);
|
|
829
|
+
// Salvage path: some streams emit reasoning only
|
|
830
|
+
// inside the final response.completed.output
|
|
831
|
+
// bundle (no per-item .done event). Dedup by id.
|
|
832
|
+
if (item.type === 'reasoning'
|
|
833
|
+
&& !reasoningItems.some(r => r.id === item.id)) {
|
|
834
|
+
pushReasoningItem(item);
|
|
835
|
+
}
|
|
836
|
+
// Salvage path for function_call: when
|
|
837
|
+
// arguments.done fired before (or without) a
|
|
838
|
+
// matching output_item.added, the deferred tool
|
|
839
|
+
// call placeholder has empty id/name. The
|
|
840
|
+
// completed.output bundle carries the canonical
|
|
841
|
+
// call_id/name; fill them in and emit onToolCall.
|
|
842
|
+
if (item.type === 'function_call') {
|
|
843
|
+
const tc = toolCalls.find(
|
|
844
|
+
(t) => t._deferred && t._pendingItemId === (item.id || ''),
|
|
845
|
+
);
|
|
846
|
+
if (tc) {
|
|
847
|
+
if (!tc.id && item.call_id) tc.id = item.call_id;
|
|
848
|
+
if (!tc.name && item.name) tc.name = item.name;
|
|
849
|
+
if (tc.id && tc.name) {
|
|
850
|
+
delete tc._deferred;
|
|
851
|
+
delete tc._pendingItemId;
|
|
852
|
+
emitToolCallDedupe(tc);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// Salvage validation. Any deferred call still missing
|
|
859
|
+
// id/name would propagate to the next turn as a
|
|
860
|
+
// function_call_output the server can't anchor. Fail the
|
|
861
|
+
// stream now so the caller sees a deterministic error
|
|
862
|
+
// instead of a cryptic mismatch one turn later.
|
|
863
|
+
const unresolved = toolCalls.find((t) => t._deferred);
|
|
864
|
+
if (unresolved) {
|
|
865
|
+
terminalError = new Error(
|
|
866
|
+
`${errLabel} function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`,
|
|
867
|
+
);
|
|
868
|
+
finish();
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
midState.sawCompleted = true;
|
|
872
|
+
done = true;
|
|
873
|
+
finish();
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
case 'response.done': {
|
|
877
|
+
// response.done is the terminal frame for some openai-oauth
|
|
878
|
+
// streams that never emit a separate response.completed.
|
|
879
|
+
// Route through the same completed/failed/incomplete
|
|
880
|
+
// normalization based on event.response.status so a
|
|
881
|
+
// server-side abort (incomplete / failed) does not slip
|
|
882
|
+
// through as success. status === 'completed' falls
|
|
883
|
+
// through to the success path with sawCompleted set;
|
|
884
|
+
// anything else is converted into a terminal error.
|
|
885
|
+
const status = event.response?.status || '';
|
|
886
|
+
if (status === 'failed') {
|
|
887
|
+
midState.responseFailedPayload = event;
|
|
888
|
+
const msg = event.response?.error?.message
|
|
889
|
+
|| event.error?.message
|
|
890
|
+
|| event.message
|
|
891
|
+
|| 'response.done failed';
|
|
892
|
+
terminalError = Object.assign(
|
|
893
|
+
new Error(`${errLabel} response.done failed: ${msg}`),
|
|
894
|
+
{ responseFailed: event },
|
|
895
|
+
);
|
|
896
|
+
populateHttpStatusFromMessage(terminalError, msg);
|
|
897
|
+
done = true;
|
|
898
|
+
finish();
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
if (status === 'incomplete') {
|
|
902
|
+
const reasonStr = _incompleteReasonFromEvent(event);
|
|
903
|
+
if (_isMaxOutputIncompleteReason(reasonStr)) {
|
|
904
|
+
incompleteReason = reasonStr;
|
|
905
|
+
stopReason = 'length';
|
|
906
|
+
midState.sawCompleted = true;
|
|
907
|
+
done = true;
|
|
908
|
+
finish();
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
terminalError = Object.assign(
|
|
912
|
+
new Error(`${errLabel} response.done incomplete: ${reasonStr}`),
|
|
913
|
+
{ responseIncomplete: event, incompleteReason: reasonStr },
|
|
914
|
+
);
|
|
915
|
+
done = true;
|
|
916
|
+
finish();
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
if (status && status !== 'completed') {
|
|
920
|
+
terminalError = Object.assign(
|
|
921
|
+
new Error(`${errLabel} response.done unexpected status: ${status}`),
|
|
922
|
+
{ responseDoneStatus: status },
|
|
923
|
+
);
|
|
924
|
+
done = true;
|
|
925
|
+
finish();
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
midState.sawCompleted = true;
|
|
929
|
+
done = true;
|
|
930
|
+
finish();
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
case 'response.incomplete': {
|
|
934
|
+
// Most incomplete reasons are real failures. max_output_tokens
|
|
935
|
+
// maps cleanly to Anthropic's stop_reason=max_tokens; treating
|
|
936
|
+
// it as an error makes Claude retry the same over-budget turn.
|
|
937
|
+
const reasonStr = _incompleteReasonFromEvent(event);
|
|
938
|
+
if (_isMaxOutputIncompleteReason(reasonStr)) {
|
|
939
|
+
incompleteReason = reasonStr;
|
|
940
|
+
stopReason = 'length';
|
|
941
|
+
midState.sawCompleted = true;
|
|
942
|
+
done = true;
|
|
943
|
+
finish();
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
terminalError = Object.assign(
|
|
947
|
+
new Error(`${errLabel} response.incomplete: ${reasonStr}`),
|
|
948
|
+
{ responseIncomplete: event, incompleteReason: reasonStr },
|
|
949
|
+
);
|
|
950
|
+
finish();
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
case 'response.failed': {
|
|
954
|
+
// Stash the payload so the mid-stream classifier can sniff
|
|
955
|
+
// network_error / stream_disconnected without re-parsing.
|
|
956
|
+
midState.responseFailedPayload = event;
|
|
957
|
+
const msg = event.response?.error?.message
|
|
958
|
+
|| event.error?.message
|
|
959
|
+
|| event.message
|
|
960
|
+
|| 'response.failed';
|
|
961
|
+
terminalError = Object.assign(new Error(`${errLabel} response.failed: ${msg}`), {
|
|
962
|
+
responseFailed: event,
|
|
963
|
+
});
|
|
964
|
+
// Sniff the server message for transient/auth/permanent
|
|
965
|
+
// hints so the handshake / mid-stream retry classifiers
|
|
966
|
+
// can route by httpStatus. Without this, server-side
|
|
967
|
+
// events like "Our servers are currently overloaded"
|
|
968
|
+
// surfaced as unclassified errors and skipped the
|
|
969
|
+
// 5xx retry bucket entirely.
|
|
970
|
+
populateHttpStatusFromMessage(terminalError, msg);
|
|
971
|
+
finish();
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
case 'error': {
|
|
975
|
+
const errMsg = String(event.message || event.error?.message || 'unknown');
|
|
976
|
+
terminalError = new Error(`${errLabel} error: ${errMsg}`);
|
|
977
|
+
populateHttpStatusFromMessage(terminalError, errMsg);
|
|
978
|
+
finish();
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
default:
|
|
982
|
+
// Catch any other reasoning-delta variants (e.g.
|
|
983
|
+
// `response.reasoning.<sub>.delta`) so they are counted
|
|
984
|
+
// and suppressed, never reaching the user content buffer.
|
|
985
|
+
if (typeof event.type === 'string'
|
|
986
|
+
&& event.type.startsWith('response.reasoning')
|
|
987
|
+
&& event.type.endsWith('.delta')) {
|
|
988
|
+
reasoningOtherDeltaCount += 1;
|
|
989
|
+
// These ARE live model progress (reviewer Medium): a
|
|
990
|
+
// provider that emits only these reasoning variants for a
|
|
991
|
+
// long span would otherwise trip the SEMANTIC idle abort.
|
|
992
|
+
// Refresh both the watchdog and the semantic idle timer,
|
|
993
|
+
// matching the named reasoning_text.delta case above.
|
|
994
|
+
try { onStreamDelta?.(); } catch {}
|
|
995
|
+
bumpSemanticIdle();
|
|
996
|
+
}
|
|
997
|
+
// Trace-only events (response.in_progress, etc.)
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
closeHandler = (code, reason) => {
|
|
1002
|
+
if (done) return;
|
|
1003
|
+
midState.wsCloseCode = code;
|
|
1004
|
+
if (!terminalError) {
|
|
1005
|
+
const r = reason?.toString?.('utf-8') || '';
|
|
1006
|
+
const httpStatus = _httpStatusFromWsClose(code, r);
|
|
1007
|
+
terminalError = Object.assign(
|
|
1008
|
+
new Error(`OpenAI OAuth WS closed before response.completed (code=${code}${r ? `, reason=${r}` : ''})`),
|
|
1009
|
+
{ wsCloseCode: code, wsCloseReason: r, ...(httpStatus ? { httpStatus } : {}) },
|
|
1010
|
+
);
|
|
1011
|
+
} else if (terminalError && !terminalError.wsCloseCode) {
|
|
1012
|
+
try { terminalError.wsCloseCode = code; } catch {}
|
|
1013
|
+
try { terminalError.httpStatus = terminalError.httpStatus || _httpStatusFromWsClose(code, reason?.toString?.('utf-8') || ''); } catch {}
|
|
1014
|
+
}
|
|
1015
|
+
finish();
|
|
1016
|
+
};
|
|
1017
|
+
errorHandler = (err) => {
|
|
1018
|
+
if (done) return;
|
|
1019
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
1020
|
+
if (terminalError) {
|
|
1021
|
+
// Preserve the first terminalError; chain the later socket
|
|
1022
|
+
// error in via `cause` (or `suppressed` if cause already set)
|
|
1023
|
+
// so diagnostics keep the original failure visible.
|
|
1024
|
+
try {
|
|
1025
|
+
if (!terminalError.cause) terminalError.cause = wrapped;
|
|
1026
|
+
else {
|
|
1027
|
+
const list = Array.isArray(terminalError.suppressed)
|
|
1028
|
+
? terminalError.suppressed
|
|
1029
|
+
: [];
|
|
1030
|
+
list.push(wrapped);
|
|
1031
|
+
terminalError.suppressed = list;
|
|
1032
|
+
}
|
|
1033
|
+
} catch {}
|
|
1034
|
+
} else {
|
|
1035
|
+
terminalError = wrapped;
|
|
1036
|
+
}
|
|
1037
|
+
try { socket.close(4001, 'stream_error'); } catch {}
|
|
1038
|
+
finish();
|
|
1039
|
+
};
|
|
1040
|
+
if (externalSignal) {
|
|
1041
|
+
abortHandler = () => {
|
|
1042
|
+
if (done) return;
|
|
1043
|
+
const reason = externalSignal.reason;
|
|
1044
|
+
terminalError = reason instanceof Error ? reason : new Error('OpenAI OAuth WS aborted by session close');
|
|
1045
|
+
// Tag: was this a user/caller abort, or a watchdog abort?
|
|
1046
|
+
// Mid-stream retry must skip user aborts but may retry watchdog
|
|
1047
|
+
// aborts. The caller-owned AbortController surfaces through
|
|
1048
|
+
// externalSignal; the agent stall watchdog signals via a reason
|
|
1049
|
+
// object whose name === 'AgentStallAbortError'. stream-watchdog
|
|
1050
|
+
// uses StreamStalledAbortError. Anything else → treat as user.
|
|
1051
|
+
const reasonName = reason?.name || '';
|
|
1052
|
+
if (reasonName === 'AgentStallAbortError'
|
|
1053
|
+
|| reasonName === 'StreamStalledAbortError') {
|
|
1054
|
+
midState.watchdogAbort = reasonName;
|
|
1055
|
+
} else {
|
|
1056
|
+
midState.userAbort = true;
|
|
1057
|
+
}
|
|
1058
|
+
try { socket.close(4002, 'aborted'); } catch {}
|
|
1059
|
+
finish();
|
|
1060
|
+
};
|
|
1061
|
+
if (externalSignal.aborted) { abortHandler(); return; }
|
|
1062
|
+
externalSignal.addEventListener('abort', abortHandler, { once: true });
|
|
1063
|
+
}
|
|
1064
|
+
socket.on('message', messageHandler);
|
|
1065
|
+
socket.on('close', closeHandler);
|
|
1066
|
+
socket.on('error', errorHandler);
|
|
1067
|
+
armPreStreamWatchdog();
|
|
1068
|
+
// Periodic client-side WS ping while the stream is active. The server's
|
|
1069
|
+
// server closes with 1011 "keepalive ping timeout" when it thinks the
|
|
1070
|
+
// peer is silent during long reasoning windows where no data frames
|
|
1071
|
+
// flow. Sending a ping every 10s from our side keeps the socket warm.
|
|
1072
|
+
// The interval is unref'd so it never holds the event loop open, and
|
|
1073
|
+
// cleanup() clears it on every terminal path (completed / close /
|
|
1074
|
+
// error / abort / mid-stream retry teardown).
|
|
1075
|
+
keepaliveTimer = setInterval(() => {
|
|
1076
|
+
try {
|
|
1077
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
1078
|
+
socket.ping();
|
|
1079
|
+
} catch {}
|
|
1080
|
+
}, 10_000);
|
|
1081
|
+
try { keepaliveTimer.unref?.(); } catch {}
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Classify a handshake error for retry eligibility.
|
|
1087
|
+
*
|
|
1088
|
+
* Default-deny: anything we don't recognize as transient returns null (treat
|
|
1089
|
+
* as permanent). Permanent buckets (401/403/404/429) also return null — the
|
|
1090
|
+
* server has made a deterministic decision that a retry can't change.
|
|
1091
|
+
*
|
|
1092
|
+
* Returns one of:
|
|
1093
|
+
* 'timeout' — `ws` handshakeTimeout fired
|
|
1094
|
+
* 'reset' — ECONNRESET / socket hang up
|
|
1095
|
+
* 'dns' — EAI_AGAIN / ENOTFOUND / EAI_NODATA
|
|
1096
|
+
* 'refused' — ECONNREFUSED
|
|
1097
|
+
* 'network' — ENETUNREACH / EHOSTUNREACH / EPIPE
|
|
1098
|
+
* 'acquire_timeout' — hard client-side open/acquire deadline fired
|
|
1099
|
+
* 'http_5xx' (with specific status e.g. 'http_503') — server overload
|
|
1100
|
+
* null — not retryable
|
|
1101
|
+
*/
|
|
1102
|
+
// Thin re-export wrapper: handshake classification now lives in the shared
|
|
1103
|
+
// retry-classifier (classifyHandshakeError). Kept here as a named export so
|
|
1104
|
+
// internal call sites (_acquireWithRetry) and any external importer keep
|
|
1105
|
+
// resolving the same symbol.
|