mixdog 0.9.3 → 0.9.5
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/README.md +112 -38
- package/package.json +10 -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/r5-orchestrated-task.json +7 -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-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -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 +251 -32
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +3 -4
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- 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-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +25 -65
- 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 +9 -8
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +10 -7
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +647 -2056
- package/src/output-styles/default.md +30 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +8 -6
- package/src/output-styles/simple.md +21 -7
- package/src/rules/agent/00-common.md +6 -3
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +15 -0
- package/src/rules/lead/lead-tool.md +6 -15
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- 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 +105 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -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 +740 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -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/providers/provider-catalog-cache.mjs +80 -0
- 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 +275 -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 +278 -402
- 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/stall-policy.mjs +20 -1
- 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 +35 -44
- 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 +27 -318
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +247 -701
- package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
- package/src/runtime/channels/lib/event-queue.mjs +78 -13
- package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- 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/scheduler.mjs +27 -113
- 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 +313 -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 +43 -616
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +210 -1948
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- 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/ko-morph.mjs +195 -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 +69 -12
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +802 -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/config.mjs +14 -4
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.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 +119 -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 +205 -0
- package/src/session-runtime/quick-search-models.mjs +47 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +352 -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/session-runtime/workflow.mjs +1 -1
- 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 +138 -669
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +64 -14
- 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 +1059 -8110
- package/src/tui/app/app-format.mjs +213 -0
- package/src/tui/app/channel-pickers.mjs +508 -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 +356 -0
- package/src/tui/app/model-options.mjs +334 -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 +489 -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 +677 -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 +512 -0
- package/src/tui/app/use-transcript-window.mjs +607 -0
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +64 -15
- package/src/tui/components/PromptInput.jsx +33 -102
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +69 -15
- 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 +13553 -12384
- 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 +170 -574
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +65 -1
- 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 +77 -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 +16 -18
- package/src/workflows/sequential/WORKFLOW.md +16 -18
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +112 -7
- 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/channels/lib/holidays.mjs +0 -138
- 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
|
@@ -23,11 +23,8 @@
|
|
|
23
23
|
* auth bundle; this module handles connection caching, delta framing, event
|
|
24
24
|
* parsing, and tracing.
|
|
25
25
|
*/
|
|
26
|
-
import
|
|
27
|
-
import { errText } from '../../../shared/err-text.mjs';
|
|
28
|
-
import { createHash, randomBytes } from 'crypto';
|
|
26
|
+
import { createHash } from 'crypto';
|
|
29
27
|
import {
|
|
30
|
-
extractCachedTokens,
|
|
31
28
|
traceAgentFetch,
|
|
32
29
|
traceAgentSse,
|
|
33
30
|
traceAgentUsage,
|
|
@@ -39,53 +36,47 @@ import {
|
|
|
39
36
|
createStreamSafetyStamps,
|
|
40
37
|
jitterDelayMs,
|
|
41
38
|
MIDSTREAM_RETRY_POLICY,
|
|
42
|
-
populateHttpStatusFromMessage,
|
|
43
39
|
sleepWithAbort,
|
|
44
40
|
} from './retry-classifier.mjs';
|
|
45
|
-
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
46
|
-
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
47
41
|
import {
|
|
48
42
|
PROVIDER_RETRY_MAX_ATTEMPTS,
|
|
49
|
-
PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
|
|
50
|
-
PROVIDER_WS_HANDSHAKE_TIMEOUT_MS,
|
|
51
|
-
PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS,
|
|
52
|
-
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
53
|
-
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
54
|
-
streamStalledError,
|
|
55
|
-
resolveTimeoutMs,
|
|
56
43
|
} from '../stall-policy.mjs';
|
|
57
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
WS_IDLE_MS,
|
|
46
|
+
acquireWebSocket,
|
|
47
|
+
releaseWebSocket,
|
|
48
|
+
_sendFrame,
|
|
49
|
+
_closeAllPooledSockets,
|
|
50
|
+
drainOpenaiWsPool,
|
|
51
|
+
} from './openai-ws-pool.mjs';
|
|
52
|
+
import {
|
|
53
|
+
WS_PRE_RESPONSE_CREATED_MS,
|
|
54
|
+
WS_INTER_CHUNK_MS,
|
|
55
|
+
_sansInput,
|
|
56
|
+
_stableStringify,
|
|
57
|
+
_cloneJson,
|
|
58
|
+
_estimateFrameTokens,
|
|
59
|
+
_combineUsageWithWarmup,
|
|
60
|
+
_computeDelta,
|
|
61
|
+
_logicalResponseItemMatch,
|
|
62
|
+
parseToolSearchArgs,
|
|
63
|
+
_streamResponse,
|
|
64
|
+
} from './openai-ws-stream.mjs';
|
|
65
|
+
|
|
66
|
+
// Legacy import paths for scripts/tool-smoke.mjs, mixdog-session-runtime.mjs
|
|
67
|
+
// (drainOpenaiWsPool), scripts/provider-toolcall-test.mjs (parseToolSearchArgs,
|
|
68
|
+
// _logicalResponseItemMatch, _streamResponse) and other external callers.
|
|
69
|
+
export {
|
|
70
|
+
_closeAllPooledSockets,
|
|
71
|
+
drainOpenaiWsPool,
|
|
72
|
+
_logicalResponseItemMatch,
|
|
73
|
+
parseToolSearchArgs,
|
|
74
|
+
_streamResponse,
|
|
75
|
+
};
|
|
58
76
|
|
|
59
77
|
globalThis.__mixdogOpenaiWsRuntimeLoaded = true;
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
63
|
-
const OPENAI_WS_URL = 'wss://api.openai.com/v1/responses';
|
|
64
|
-
const XAI_WS_URL = 'wss://api.x.ai/v1/responses';
|
|
65
|
-
const WS_IDLE_MS = resolveTimeoutMs(
|
|
66
|
-
'MIXDOG_PROVIDER_WS_IDLE_MS',
|
|
67
|
-
20 * 60_000,
|
|
68
|
-
{ minMs: 60_000, maxMs: 60 * 60_000 },
|
|
69
|
-
);
|
|
70
|
-
const WS_HANDSHAKE_TIMEOUT_MS = PROVIDER_WS_HANDSHAKE_TIMEOUT_MS;
|
|
71
|
-
const WS_ACQUIRE_TIMEOUT_MS = PROVIDER_WS_ACQUIRE_TIMEOUT_MS;
|
|
72
|
-
// Pre-`response.created` deadline. Once the socket is open and the
|
|
73
|
-
// response.create frame is sent, a healthy server emits response.created
|
|
74
|
-
// within seconds. If it stalls past this short bound the socket has wedged
|
|
75
|
-
// post-upgrade with zero server events — treat it as a fast, retryable
|
|
76
|
-
// first-byte timeout. This is the ONLY pre-stream watchdog; once any server
|
|
77
|
-
// event arrives the single inter-chunk idle timer below takes over.
|
|
78
|
-
// Only this short window is shortened; the post-`response.created`
|
|
79
|
-
// inter-chunk / reasoning span keeps the longer deadlines below.
|
|
80
|
-
const WS_PRE_RESPONSE_CREATED_MS = (() => {
|
|
81
|
-
const raw = process.env.MIXDOG_PROVIDER_WS_PRE_RESPONSE_CREATED_TIMEOUT_MS;
|
|
82
|
-
const n = Number(raw);
|
|
83
|
-
if (Number.isFinite(n) && n > 0) return Math.min(Math.max(n, 1_000), 120_000);
|
|
84
|
-
return 10_000;
|
|
85
|
-
})();
|
|
86
|
-
// Single inter-chunk idle timer. Resets on EVERY received frame — any frame,
|
|
87
|
-
// including metadata/keepalive, proves the socket is live.
|
|
88
|
-
const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
|
|
79
|
+
// --- WS_PRE_RESPONSE_CREATED_MS / WS_INTER_CHUNK_MS: extracted to openai-ws-stream.mjs ---
|
|
89
80
|
// Mid-stream retry budgets + backoff now live in the shared MIDSTREAM_RETRY_POLICY
|
|
90
81
|
// table (retry-classifier.mjs). These aliases keep the local call sites readable
|
|
91
82
|
// and ensure the numbers exist in exactly ONE place.
|
|
@@ -113,2019 +104,11 @@ const WS_MIDSTREAM_POLICY = {
|
|
|
113
104
|
const HANDSHAKE_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS;
|
|
114
105
|
const HANDSHAKE_BACKOFF_BASE_MS = 500;
|
|
115
106
|
const HANDSHAKE_BACKOFF_CAP_MS = 5000;
|
|
116
|
-
// WS socket pool buckets are keyed by `poolKey` (the per-call sessionId)
|
|
117
|
-
// to isolate parallel agent invocations — each gets its own socket so
|
|
118
|
-
// a second caller cannot grab a sibling's mid-turn entry (openai-oauth would
|
|
119
|
-
// otherwise reject the new response.create with "No tool output found
|
|
120
|
-
// for function call ..."). The handshake `session_id` header/URL
|
|
121
|
-
// uses `cacheKey` — a prefix-scoped cache key derived from the configured
|
|
122
|
-
// provider namespace plus model/system/tools hash. Same-prefix sessions share
|
|
123
|
-
// server-side prompt cache, while unrelated main/worker prefixes no longer
|
|
124
|
-
// evict each other inside one static provider lane. The backend dedupes cache by
|
|
125
|
-
// handshake session_id, not by body.prompt_cache_key alone (measured
|
|
126
|
-
// 2026-04-19 after the v0.6.151 regression).
|
|
127
|
-
const MAX_POOLED_SOCKETS_PER_KEY = 8;
|
|
128
|
-
|
|
129
|
-
// poolKey -> Entry[]
|
|
130
|
-
// Entry: { socket, busy, idleTimer, lastResponseId, lastRequestSansInput,
|
|
131
|
-
// lastRequestInput, lastResponseItems, lastInputLen, turnState,
|
|
132
|
-
// closing, ephemeral }
|
|
133
|
-
const _wsPool = new Map();
|
|
134
|
-
|
|
135
|
-
// Final prompt_cache_key/session_id lane guard for OpenAI OAuth transports.
|
|
136
|
-
// The provider code may shard one logical prefix into N cache keys for 10+
|
|
137
|
-
// total parallelism; inside each final key we still serialize requests because
|
|
138
|
-
// Live probes show same-key concurrent WebSockets can randomly miss the
|
|
139
|
-
// server prompt cache even after warm-up.
|
|
140
|
-
const _openAiPromptCacheLanes = new Map();
|
|
141
|
-
const _openAiPromptCacheLaneRates = new Map();
|
|
142
|
-
const OPENAI_PROMPT_CACHE_LANE_RATE_WINDOW_MS = 60_000;
|
|
143
|
-
const OPENAI_PROMPT_CACHE_LANE_RATE_MAX_KEYS = 4096;
|
|
144
|
-
|
|
145
|
-
function _positiveInt(value, fallback = 0) {
|
|
146
|
-
const n = Number(value);
|
|
147
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function _nonNegativeInt(value, fallback = 0) {
|
|
151
|
-
const n = Number(value);
|
|
152
|
-
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function _cacheLaneHash(value) {
|
|
156
|
-
return createHash('sha256').update(String(value || '')).digest('hex').slice(0, 12);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function _openAiPromptCacheLaneMaxInFlight(sendOpts = {}) {
|
|
160
|
-
return _positiveInt(
|
|
161
|
-
sendOpts?.openaiCacheLaneMaxInFlight
|
|
162
|
-
?? sendOpts?.promptCacheLaneMaxInFlight
|
|
163
|
-
?? process.env.MIXDOG_OPENAI_CACHE_LANE_MAX_INFLIGHT
|
|
164
|
-
?? process.env.MIXDOG_OPENAI_CACHE_MAX_INFLIGHT,
|
|
165
|
-
1,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function _openAiPromptCacheLaneQueueTimeoutMs(sendOpts = {}) {
|
|
170
|
-
return _positiveInt(
|
|
171
|
-
sendOpts?.openaiCacheLaneQueueTimeoutMs
|
|
172
|
-
?? sendOpts?.promptCacheLaneQueueTimeoutMs
|
|
173
|
-
?? process.env.MIXDOG_OPENAI_CACHE_LANE_QUEUE_TIMEOUT_MS
|
|
174
|
-
?? process.env.MIXDOG_OPENAI_CACHE_QUEUE_TIMEOUT_MS,
|
|
175
|
-
0,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function _openAiPromptCacheLaneRateLimitPerMin(sendOpts = {}) {
|
|
180
|
-
return _nonNegativeInt(
|
|
181
|
-
sendOpts?.openaiCacheLaneRateLimitPerMin
|
|
182
|
-
?? sendOpts?.promptCacheLaneRateLimitPerMin
|
|
183
|
-
?? process.env.MIXDOG_OPENAI_CACHE_LANE_RPM
|
|
184
|
-
?? process.env.MIXDOG_OPENAI_CACHE_KEY_RPM,
|
|
185
|
-
12,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function _openAiPromptCacheLaneDeltaRateLimitPerMin(sendOpts = {}) {
|
|
190
|
-
return _nonNegativeInt(
|
|
191
|
-
sendOpts?.openaiCacheLaneDeltaRateLimitPerMin
|
|
192
|
-
?? sendOpts?.promptCacheLaneDeltaRateLimitPerMin
|
|
193
|
-
?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_RPM
|
|
194
|
-
?? process.env.MIXDOG_OPENAI_CACHE_DELTA_RPM,
|
|
195
|
-
60,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function _openAiPromptCacheLaneDeltaMaxItems(sendOpts = {}) {
|
|
200
|
-
return _nonNegativeInt(
|
|
201
|
-
sendOpts?.openaiCacheLaneDeltaMaxItems
|
|
202
|
-
?? sendOpts?.promptCacheLaneDeltaMaxItems
|
|
203
|
-
?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_MAX_ITEMS
|
|
204
|
-
?? process.env.MIXDOG_OPENAI_CACHE_DELTA_MAX_ITEMS,
|
|
205
|
-
8,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function _openAiPromptCacheLaneDeltaMaxTokens(sendOpts = {}) {
|
|
210
|
-
return _nonNegativeInt(
|
|
211
|
-
sendOpts?.openaiCacheLaneDeltaMaxTokens
|
|
212
|
-
?? sendOpts?.promptCacheLaneDeltaMaxTokens
|
|
213
|
-
?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_MAX_TOKENS
|
|
214
|
-
?? process.env.MIXDOG_OPENAI_CACHE_DELTA_MAX_TOKENS,
|
|
215
|
-
20_000,
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function _openAiPromptCacheLaneSlowTraceMs(sendOpts = {}) {
|
|
220
|
-
return _positiveInt(
|
|
221
|
-
sendOpts?.openaiCacheLaneSlowTraceMs
|
|
222
|
-
?? sendOpts?.promptCacheLaneSlowTraceMs
|
|
223
|
-
?? process.env.MIXDOG_OPENAI_CACHE_LANE_SLOW_MS,
|
|
224
|
-
3000,
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function _isOpenAiPromptCacheLaneAuth(auth) {
|
|
229
|
-
return auth?.type !== 'xai';
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function _getOpenAiPromptCacheLaneState(key, maxInFlight) {
|
|
233
|
-
let state = _openAiPromptCacheLanes.get(key);
|
|
234
|
-
if (!state) {
|
|
235
|
-
state = { key, active: 0, queue: [], maxInFlight, nextId: 0 };
|
|
236
|
-
_openAiPromptCacheLanes.set(key, state);
|
|
237
|
-
}
|
|
238
|
-
state.maxInFlight = maxInFlight;
|
|
239
|
-
return state;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function _cleanupOpenAiPromptCacheLane(state) {
|
|
243
|
-
if (state.active === 0 && state.queue.length === 0) {
|
|
244
|
-
_openAiPromptCacheLanes.delete(state.key);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function _getOpenAiPromptCacheLaneRateState(key) {
|
|
249
|
-
let state = _openAiPromptCacheLaneRates.get(key);
|
|
250
|
-
if (!state) {
|
|
251
|
-
state = { key, starts: [], lastUsedAt: Date.now() };
|
|
252
|
-
_openAiPromptCacheLaneRates.set(key, state);
|
|
253
|
-
}
|
|
254
|
-
state.lastUsedAt = Date.now();
|
|
255
|
-
if (_openAiPromptCacheLaneRates.size > OPENAI_PROMPT_CACHE_LANE_RATE_MAX_KEYS) {
|
|
256
|
-
let oldestKey = null;
|
|
257
|
-
let oldestAt = Infinity;
|
|
258
|
-
for (const [k, v] of _openAiPromptCacheLaneRates) {
|
|
259
|
-
if ((v?.lastUsedAt || 0) < oldestAt) {
|
|
260
|
-
oldestAt = v.lastUsedAt || 0;
|
|
261
|
-
oldestKey = k;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (oldestKey) _openAiPromptCacheLaneRates.delete(oldestKey);
|
|
265
|
-
}
|
|
266
|
-
return state;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function _pruneOpenAiPromptCacheLaneRateState(state, now = Date.now()) {
|
|
270
|
-
const cutoff = now - OPENAI_PROMPT_CACHE_LANE_RATE_WINDOW_MS;
|
|
271
|
-
while (state.starts.length > 0 && state.starts[0] <= cutoff) state.starts.shift();
|
|
272
|
-
state.lastUsedAt = now;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function _sleepWithSignal(ms, signal) {
|
|
276
|
-
if (ms <= 0) return Promise.resolve();
|
|
277
|
-
return new Promise((resolve, reject) => {
|
|
278
|
-
let timer = null;
|
|
279
|
-
let abortListener = null;
|
|
280
|
-
const cleanup = () => {
|
|
281
|
-
if (timer) clearTimeout(timer);
|
|
282
|
-
if (signal && abortListener) signal.removeEventListener('abort', abortListener);
|
|
283
|
-
};
|
|
284
|
-
abortListener = () => {
|
|
285
|
-
cleanup();
|
|
286
|
-
const reason = signal?.reason;
|
|
287
|
-
reject(reason instanceof Error ? reason : new Error('OpenAI prompt cache lane rate wait aborted'));
|
|
288
|
-
};
|
|
289
|
-
if (signal?.aborted) {
|
|
290
|
-
abortListener();
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
if (signal) signal.addEventListener('abort', abortListener, { once: true });
|
|
294
|
-
timer = setTimeout(() => {
|
|
295
|
-
cleanup();
|
|
296
|
-
resolve();
|
|
297
|
-
}, ms);
|
|
298
|
-
timer.unref?.();
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async function _reserveOpenAiPromptCacheLaneRate({ key, limitPerMin, signal, beforeWait }) {
|
|
303
|
-
if (!limitPerMin || limitPerMin <= 0) {
|
|
304
|
-
return { rateLimitPerMin: 0, rateWaitMs: 0, rateWindowCount: 0 };
|
|
305
|
-
}
|
|
306
|
-
const state = _getOpenAiPromptCacheLaneRateState(key);
|
|
307
|
-
const startedAt = Date.now();
|
|
308
|
-
let beforeWaitCalled = false;
|
|
309
|
-
while (true) {
|
|
310
|
-
const now = Date.now();
|
|
311
|
-
_pruneOpenAiPromptCacheLaneRateState(state, now);
|
|
312
|
-
if (state.starts.length < limitPerMin) {
|
|
313
|
-
state.starts.push(now);
|
|
314
|
-
return {
|
|
315
|
-
rateLimitPerMin: limitPerMin,
|
|
316
|
-
rateWaitMs: now - startedAt,
|
|
317
|
-
rateWindowCount: state.starts.length,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
const waitMs = Math.max(25, state.starts[0] + OPENAI_PROMPT_CACHE_LANE_RATE_WINDOW_MS - now);
|
|
321
|
-
if (!beforeWaitCalled && typeof beforeWait === 'function') {
|
|
322
|
-
beforeWaitCalled = true;
|
|
323
|
-
await beforeWait({
|
|
324
|
-
waitMs,
|
|
325
|
-
rateLimitPerMin: limitPerMin,
|
|
326
|
-
rateWindowCount: state.starts.length,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
await _sleepWithSignal(waitMs, signal);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function _resolveOpenAiPromptCacheRatePolicy(sendOpts = {}, info = {}) {
|
|
334
|
-
const mode = String(info?.mode || '').toLowerCase();
|
|
335
|
-
const frameInputItems = Number(info?.frameInputItems);
|
|
336
|
-
const deltaTokens = Number(info?.deltaTokens);
|
|
337
|
-
const hasPreviousResponseId = info?.hasPreviousResponseId === true;
|
|
338
|
-
const fullLimitPerMin = _openAiPromptCacheLaneRateLimitPerMin(sendOpts);
|
|
339
|
-
const deltaLimitPerMin = _openAiPromptCacheLaneDeltaRateLimitPerMin(sendOpts);
|
|
340
|
-
const deltaMaxItems = _openAiPromptCacheLaneDeltaMaxItems(sendOpts);
|
|
341
|
-
const deltaMaxTokens = _openAiPromptCacheLaneDeltaMaxTokens(sendOpts);
|
|
342
|
-
const itemCount = Number.isFinite(frameInputItems) ? frameInputItems : null;
|
|
343
|
-
const tokenCount = Number.isFinite(deltaTokens) ? deltaTokens : null;
|
|
344
|
-
const smallDeltaItems = itemCount == null || deltaMaxItems <= 0 || itemCount <= deltaMaxItems;
|
|
345
|
-
const smallDeltaTokens = tokenCount == null || deltaMaxTokens <= 0 || tokenCount <= deltaMaxTokens;
|
|
346
|
-
|
|
347
|
-
if (mode === 'delta' && hasPreviousResponseId && smallDeltaItems && smallDeltaTokens) {
|
|
348
|
-
return {
|
|
349
|
-
policy: deltaLimitPerMin > 0 ? 'delta_relaxed' : 'delta_unlimited',
|
|
350
|
-
limitPerMin: deltaLimitPerMin,
|
|
351
|
-
fullLimitPerMin,
|
|
352
|
-
deltaLimitPerMin,
|
|
353
|
-
deltaMaxItems,
|
|
354
|
-
deltaMaxTokens,
|
|
355
|
-
frameInputItems: itemCount,
|
|
356
|
-
deltaTokens: tokenCount,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
policy: mode === 'delta' ? 'delta_guarded' : 'full_guard',
|
|
362
|
-
limitPerMin: fullLimitPerMin,
|
|
363
|
-
fullLimitPerMin,
|
|
364
|
-
deltaLimitPerMin,
|
|
365
|
-
deltaMaxItems,
|
|
366
|
-
deltaMaxTokens,
|
|
367
|
-
frameInputItems: itemCount,
|
|
368
|
-
deltaTokens: tokenCount,
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function _removeQueuedOpenAiPromptCacheLaneRequest(state, request) {
|
|
373
|
-
const index = state.queue.indexOf(request);
|
|
374
|
-
if (index >= 0) state.queue.splice(index, 1);
|
|
375
|
-
_cleanupOpenAiPromptCacheLane(state);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function _releaseOpenAiPromptCacheLane(state) {
|
|
379
|
-
state.active = Math.max(0, state.active - 1);
|
|
380
|
-
while (state.queue.length > 0 && state.active < state.maxInFlight) {
|
|
381
|
-
const next = state.queue.shift();
|
|
382
|
-
next.cleanup?.();
|
|
383
|
-
state.active += 1;
|
|
384
|
-
next.resolve(_makeOpenAiPromptCacheLaneHandle(state, next.requestId, next.enqueuedAt, true));
|
|
385
|
-
}
|
|
386
|
-
_cleanupOpenAiPromptCacheLane(state);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function _makeOpenAiPromptCacheLaneHandle(state, requestId, enqueuedAt, queued) {
|
|
390
|
-
let released = false;
|
|
391
|
-
return {
|
|
392
|
-
requestId,
|
|
393
|
-
queued,
|
|
394
|
-
waitedMs: Date.now() - enqueuedAt,
|
|
395
|
-
activeCount: state.active,
|
|
396
|
-
queueDepth: state.queue.length,
|
|
397
|
-
release() {
|
|
398
|
-
if (released) return;
|
|
399
|
-
released = true;
|
|
400
|
-
_releaseOpenAiPromptCacheLane(state);
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function _acquireOpenAiPromptCacheLane({ key, maxInFlight, signal, timeoutMs }) {
|
|
406
|
-
const state = _getOpenAiPromptCacheLaneState(key, maxInFlight);
|
|
407
|
-
const requestId = ++state.nextId;
|
|
408
|
-
const enqueuedAt = Date.now();
|
|
409
|
-
if (state.active < state.maxInFlight) {
|
|
410
|
-
state.active += 1;
|
|
411
|
-
return Promise.resolve(_makeOpenAiPromptCacheLaneHandle(state, requestId, enqueuedAt, false));
|
|
412
|
-
}
|
|
413
|
-
return new Promise((resolve, reject) => {
|
|
414
|
-
const request = { requestId, enqueuedAt, resolve, reject, cleanup: null, timer: null, abortListener: null };
|
|
415
|
-
const cleanup = () => {
|
|
416
|
-
if (request.timer) clearTimeout(request.timer);
|
|
417
|
-
if (signal && request.abortListener) signal.removeEventListener('abort', request.abortListener);
|
|
418
|
-
};
|
|
419
|
-
request.cleanup = cleanup;
|
|
420
|
-
request.abortListener = () => {
|
|
421
|
-
cleanup();
|
|
422
|
-
_removeQueuedOpenAiPromptCacheLaneRequest(state, request);
|
|
423
|
-
const reason = signal?.reason;
|
|
424
|
-
reject(reason instanceof Error ? reason : new Error('OpenAI prompt cache lane wait aborted'));
|
|
425
|
-
};
|
|
426
|
-
if (signal?.aborted) {
|
|
427
|
-
request.abortListener();
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
if (signal) signal.addEventListener('abort', request.abortListener, { once: true });
|
|
431
|
-
if (timeoutMs > 0) {
|
|
432
|
-
request.timer = setTimeout(() => {
|
|
433
|
-
cleanup();
|
|
434
|
-
_removeQueuedOpenAiPromptCacheLaneRequest(state, request);
|
|
435
|
-
reject(new Error(`OpenAI prompt cache lane wait timed out after ${timeoutMs}ms`));
|
|
436
|
-
}, timeoutMs);
|
|
437
|
-
request.timer.unref?.();
|
|
438
|
-
}
|
|
439
|
-
state.queue.push(request);
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async function _withOpenAiPromptCacheLane({ auth, cacheKey, sendOpts, poolKey, iteration, traceProvider, useModel, externalSignal }, fn) {
|
|
444
|
-
if (!_isOpenAiPromptCacheLaneAuth(auth) || !cacheKey) {
|
|
445
|
-
return await fn({ enabled: false, maxInFlight: 0 });
|
|
446
|
-
}
|
|
447
|
-
const maxInFlight = _openAiPromptCacheLaneMaxInFlight(sendOpts);
|
|
448
|
-
if (maxInFlight <= 0) {
|
|
449
|
-
return await fn({ enabled: false, maxInFlight: 0 });
|
|
450
|
-
}
|
|
451
|
-
const laneKey = `openai-prompt:${traceProvider || 'openai'}:${useModel || 'default'}:${cacheKey}`;
|
|
452
|
-
const laneKeyHash = _cacheLaneHash(laneKey);
|
|
453
|
-
const state = _getOpenAiPromptCacheLaneState(laneKey, maxInFlight);
|
|
454
|
-
const queued = state.active >= state.maxInFlight;
|
|
455
|
-
if (queued) {
|
|
456
|
-
appendAgentTrace({
|
|
457
|
-
sessionId: poolKey,
|
|
458
|
-
iteration,
|
|
459
|
-
kind: 'cache_lane',
|
|
460
|
-
provider: traceProvider,
|
|
461
|
-
model: useModel,
|
|
462
|
-
event: 'queued',
|
|
463
|
-
lane_key_hash: laneKeyHash,
|
|
464
|
-
max_in_flight: maxInFlight,
|
|
465
|
-
active: state.active,
|
|
466
|
-
queue_depth: state.queue.length,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
const timeoutMs = _openAiPromptCacheLaneQueueTimeoutMs(sendOpts);
|
|
470
|
-
let handle = await _acquireOpenAiPromptCacheLane({ key: laneKey, maxInFlight, signal: externalSignal, timeoutMs });
|
|
471
|
-
let handleActive = true;
|
|
472
|
-
const laneMeta = {
|
|
473
|
-
enabled: true,
|
|
474
|
-
laneKeyHash,
|
|
475
|
-
maxInFlight,
|
|
476
|
-
ratePolicy: 'pending',
|
|
477
|
-
rateLimitPerMin: 0,
|
|
478
|
-
rateWaitMs: 0,
|
|
479
|
-
rateWindowCount: 0,
|
|
480
|
-
rateReleasedForWait: false,
|
|
481
|
-
rateReacquireWaitMs: 0,
|
|
482
|
-
queued: queued || handle.queued === true,
|
|
483
|
-
waitMs: handle.waitedMs,
|
|
484
|
-
activeAfterAcquire: handle.activeCount,
|
|
485
|
-
queueDepthAfterAcquire: handle.queueDepth,
|
|
486
|
-
async reserveRate(info = {}) {
|
|
487
|
-
if (laneMeta.ratePolicy !== 'pending') return laneMeta;
|
|
488
|
-
const policy = _resolveOpenAiPromptCacheRatePolicy(sendOpts, info);
|
|
489
|
-
let releasedForRateWait = false;
|
|
490
|
-
const rateMeta = await _reserveOpenAiPromptCacheLaneRate({
|
|
491
|
-
key: laneKey,
|
|
492
|
-
limitPerMin: policy.limitPerMin,
|
|
493
|
-
signal: externalSignal,
|
|
494
|
-
beforeWait: () => {
|
|
495
|
-
if (!handleActive) return;
|
|
496
|
-
handle.release();
|
|
497
|
-
handleActive = false;
|
|
498
|
-
releasedForRateWait = true;
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
let reacquireWaitMs = 0;
|
|
502
|
-
if (releasedForRateWait) {
|
|
503
|
-
const reacquired = await _acquireOpenAiPromptCacheLane({
|
|
504
|
-
key: laneKey,
|
|
505
|
-
maxInFlight,
|
|
506
|
-
signal: externalSignal,
|
|
507
|
-
timeoutMs,
|
|
508
|
-
});
|
|
509
|
-
handle = reacquired;
|
|
510
|
-
handleActive = true;
|
|
511
|
-
reacquireWaitMs = reacquired.waitedMs;
|
|
512
|
-
laneMeta.queued = laneMeta.queued || reacquired.queued === true;
|
|
513
|
-
laneMeta.waitMs = (Number(laneMeta.waitMs) || 0) + reacquireWaitMs;
|
|
514
|
-
laneMeta.activeAfterAcquire = reacquired.activeCount;
|
|
515
|
-
laneMeta.queueDepthAfterAcquire = reacquired.queueDepth;
|
|
516
|
-
}
|
|
517
|
-
Object.assign(laneMeta, {
|
|
518
|
-
ratePolicy: policy.policy,
|
|
519
|
-
rateLimitPerMin: rateMeta.rateLimitPerMin,
|
|
520
|
-
rateWaitMs: rateMeta.rateWaitMs,
|
|
521
|
-
rateWindowCount: rateMeta.rateWindowCount,
|
|
522
|
-
rateReleasedForWait: releasedForRateWait,
|
|
523
|
-
rateReacquireWaitMs: reacquireWaitMs,
|
|
524
|
-
rateFullLimitPerMin: policy.fullLimitPerMin,
|
|
525
|
-
rateDeltaLimitPerMin: policy.deltaLimitPerMin,
|
|
526
|
-
rateDeltaMaxItems: policy.deltaMaxItems,
|
|
527
|
-
rateDeltaMaxTokens: policy.deltaMaxTokens,
|
|
528
|
-
ratePolicyFrameInputItems: policy.frameInputItems,
|
|
529
|
-
ratePolicyDeltaTokens: policy.deltaTokens,
|
|
530
|
-
});
|
|
531
|
-
if (rateMeta.rateWaitMs > 0) {
|
|
532
|
-
appendAgentTrace({
|
|
533
|
-
sessionId: poolKey,
|
|
534
|
-
iteration,
|
|
535
|
-
kind: 'cache_lane',
|
|
536
|
-
provider: traceProvider,
|
|
537
|
-
model: useModel,
|
|
538
|
-
event: 'rate_wait',
|
|
539
|
-
lane_key_hash: laneKeyHash,
|
|
540
|
-
rate_policy: laneMeta.ratePolicy,
|
|
541
|
-
rate_limit_per_min: rateMeta.rateLimitPerMin,
|
|
542
|
-
rate_wait_ms: rateMeta.rateWaitMs,
|
|
543
|
-
rate_window_count: rateMeta.rateWindowCount,
|
|
544
|
-
released_for_rate_wait: releasedForRateWait,
|
|
545
|
-
reacquire_wait_ms: reacquireWaitMs,
|
|
546
|
-
frame_input_items: policy.frameInputItems,
|
|
547
|
-
delta_tokens: policy.deltaTokens,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
return laneMeta;
|
|
551
|
-
},
|
|
552
|
-
};
|
|
553
|
-
try {
|
|
554
|
-
return await fn(laneMeta);
|
|
555
|
-
} finally {
|
|
556
|
-
const slowTraceMs = _openAiPromptCacheLaneSlowTraceMs(sendOpts);
|
|
557
|
-
const slowWaitMs = Math.max(Number(laneMeta.rateWaitMs) || 0, Number(laneMeta.waitMs) || 0);
|
|
558
|
-
if (slowTraceMs > 0 && slowWaitMs >= slowTraceMs) {
|
|
559
|
-
appendAgentTrace({
|
|
560
|
-
sessionId: poolKey,
|
|
561
|
-
iteration,
|
|
562
|
-
kind: 'cache_lane_slow',
|
|
563
|
-
provider: traceProvider,
|
|
564
|
-
model: useModel,
|
|
565
|
-
event: laneMeta.rateWaitMs > 0 && laneMeta.waitMs > 0
|
|
566
|
-
? 'rate_and_queue_wait'
|
|
567
|
-
: laneMeta.rateWaitMs > 0
|
|
568
|
-
? 'rate_wait'
|
|
569
|
-
: 'queue_wait',
|
|
570
|
-
lane_key_hash: laneKeyHash,
|
|
571
|
-
payload: {
|
|
572
|
-
event: laneMeta.rateWaitMs > 0 && laneMeta.waitMs > 0
|
|
573
|
-
? 'rate_and_queue_wait'
|
|
574
|
-
: laneMeta.rateWaitMs > 0
|
|
575
|
-
? 'rate_wait'
|
|
576
|
-
: 'queue_wait',
|
|
577
|
-
provider: traceProvider,
|
|
578
|
-
model: useModel,
|
|
579
|
-
lane_key_hash: laneKeyHash,
|
|
580
|
-
threshold_ms: slowTraceMs,
|
|
581
|
-
max_wait_ms: slowWaitMs,
|
|
582
|
-
rate_policy: laneMeta.ratePolicy,
|
|
583
|
-
rate_limit_per_min: laneMeta.rateLimitPerMin,
|
|
584
|
-
rate_wait_ms: laneMeta.rateWaitMs,
|
|
585
|
-
rate_window_count: laneMeta.rateWindowCount,
|
|
586
|
-
released_for_rate_wait: laneMeta.rateReleasedForWait,
|
|
587
|
-
reacquire_wait_ms: laneMeta.rateReacquireWaitMs,
|
|
588
|
-
rate_full_limit_per_min: laneMeta.rateFullLimitPerMin,
|
|
589
|
-
rate_delta_limit_per_min: laneMeta.rateDeltaLimitPerMin,
|
|
590
|
-
rate_delta_max_items: laneMeta.rateDeltaMaxItems,
|
|
591
|
-
rate_delta_max_tokens: laneMeta.rateDeltaMaxTokens,
|
|
592
|
-
rate_policy_frame_input_items: laneMeta.ratePolicyFrameInputItems,
|
|
593
|
-
rate_policy_delta_tokens: laneMeta.ratePolicyDeltaTokens,
|
|
594
|
-
max_in_flight: laneMeta.maxInFlight,
|
|
595
|
-
queued: laneMeta.queued,
|
|
596
|
-
wait_ms: laneMeta.waitMs,
|
|
597
|
-
active_after_acquire: laneMeta.activeAfterAcquire,
|
|
598
|
-
queue_depth_after_acquire: laneMeta.queueDepthAfterAcquire,
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
if (handleActive) handle.release();
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function _getPoolArr(poolKey) {
|
|
607
|
-
if (!poolKey) return null;
|
|
608
|
-
let arr = _wsPool.get(poolKey);
|
|
609
|
-
if (!arr) {
|
|
610
|
-
arr = [];
|
|
611
|
-
_wsPool.set(poolKey, arr);
|
|
612
|
-
}
|
|
613
|
-
return arr;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function _removeFromPool(poolKey, entry) {
|
|
617
|
-
if (!poolKey) return;
|
|
618
|
-
const arr = _wsPool.get(poolKey);
|
|
619
|
-
if (!arr) return;
|
|
620
|
-
const idx = arr.indexOf(entry);
|
|
621
|
-
if (idx >= 0) arr.splice(idx, 1);
|
|
622
|
-
if (arr.length === 0) _wsPool.delete(poolKey);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function _scheduleIdleClose(poolKey, entry) {
|
|
626
|
-
if (!entry) return;
|
|
627
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
628
|
-
entry.idleTimer = setTimeout(() => {
|
|
629
|
-
if (entry.busy) return;
|
|
630
|
-
try { entry.socket.close(1000, 'idle_timeout'); } catch {}
|
|
631
|
-
_removeFromPool(poolKey, entry);
|
|
632
|
-
}, WS_IDLE_MS);
|
|
633
|
-
try { entry.idleTimer.unref?.(); } catch {}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function _clearIdle(entry) {
|
|
637
|
-
if (entry?.idleTimer) {
|
|
638
|
-
clearTimeout(entry.idleTimer);
|
|
639
|
-
entry.idleTimer = null;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function _isOpen(entry) {
|
|
644
|
-
return entry?.socket?.readyState === WebSocket.OPEN;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Awaited frame send. Asserts the socket is OPEN and resolves only after
|
|
648
|
-
// the underlying transport reports the buffered write succeeded (or fails)
|
|
649
|
-
// via the WebSocket send callback. Raw `socket.send(JSON.stringify(...))`
|
|
650
|
-
// is fire-and-forget — a wedged or half-closed socket silently queues the
|
|
651
|
-
// payload and the caller assumes it landed, then later times out waiting
|
|
652
|
-
// for a server event that will never arrive. Tag any failure with
|
|
653
|
-
// `wsSendFailed=true` so _classifyMidstreamError routes the next attempt
|
|
654
|
-
// through a fresh socket.
|
|
655
|
-
function _sendFrame(entry, frame) {
|
|
656
|
-
return new Promise((resolve, reject) => {
|
|
657
|
-
const socket = entry?.socket;
|
|
658
|
-
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
659
|
-
const err = new Error(`WS send: socket not OPEN (readyState=${socket?.readyState ?? 'n/a'})`);
|
|
660
|
-
err.wsSendFailed = true;
|
|
661
|
-
reject(err);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
let payload;
|
|
665
|
-
try { payload = JSON.stringify(frame); }
|
|
666
|
-
catch (e) {
|
|
667
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
668
|
-
err.wsSendFailed = true;
|
|
669
|
-
reject(err);
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
try {
|
|
673
|
-
// Do NOT await the send callback: on a wedged-but-OPEN socket the
|
|
674
|
-
// ws write callback may never fire, which would hang this Promise
|
|
675
|
-
// before _streamResponse arms its first-byte watchdog. Fire and
|
|
676
|
-
// resolve immediately; transport failures surface via the socket
|
|
677
|
-
// 'error'/'close' handlers and the first-byte watchdog.
|
|
678
|
-
socket.send(payload, () => {});
|
|
679
|
-
resolve();
|
|
680
|
-
} catch (e) {
|
|
681
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
682
|
-
err.wsSendFailed = true;
|
|
683
|
-
reject(err);
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cacheKey }) {
|
|
689
|
-
// xAI WS: do NOT pin x-grok-conv-id. Measured parallel runs show that
|
|
690
|
-
// forcing a routing shard via that header alternates cold caches across
|
|
691
|
-
// parallel workers; the automatic prompt-prefix cache holds up better
|
|
692
|
-
// when each handshake is unpinned. Reference: vercel/ai xai provider.
|
|
693
|
-
const headers = auth.type === 'xai'
|
|
694
|
-
? {
|
|
695
|
-
'Authorization': `Bearer ${auth.apiKey}`,
|
|
696
|
-
}
|
|
697
|
-
: auth.type === 'openai-direct'
|
|
698
|
-
? {
|
|
699
|
-
'Authorization': `Bearer ${auth.apiKey}`,
|
|
700
|
-
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
|
701
|
-
}
|
|
702
|
-
: {
|
|
703
|
-
'Authorization': `Bearer ${auth.access_token}`,
|
|
704
|
-
'chatgpt-account-id': auth.account_id || '',
|
|
705
|
-
'originator': CODEX_OAUTH_ORIGINATOR,
|
|
706
|
-
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
|
707
|
-
};
|
|
708
|
-
if (sessionToken) {
|
|
709
|
-
const sid = String(sessionToken);
|
|
710
|
-
headers['session_id'] = sid;
|
|
711
|
-
}
|
|
712
|
-
// x-client-request-id must be a per-request value so server-side request
|
|
713
|
-
// traces stay distinguishable across retries / reconnects sharing the same
|
|
714
|
-
// session_id. Reusing sessionToken (= cacheKey) collapsed every request
|
|
715
|
-
// for the same conversation onto one trace bucket.
|
|
716
|
-
headers['x-client-request-id'] = randomBytes(16).toString('hex');
|
|
717
|
-
if (turnState) headers['x-codex-turn-state'] = turnState;
|
|
718
|
-
return headers;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// handshake session_id is the conversation slot openai-oauth uses for in-memory
|
|
722
|
-
// prefix state. All orchestrator-internal dispatches for this provider share
|
|
723
|
-
// the same cacheKey (built in manager.mjs via providerCacheKey()), so they
|
|
724
|
-
// share the server-side prefix-cache shard across roles/sources.
|
|
725
|
-
function _mintSessionToken(cacheKey, auth) {
|
|
726
|
-
// xAI's public WebSocket endpoint uses the open connection plus
|
|
727
|
-
// response ids for continuation; unlike openai-oauth, it does not need the
|
|
728
|
-
// OAuth-specific session_id handshake shard.
|
|
729
|
-
if (auth?.type === 'xai') return null;
|
|
730
|
-
return cacheKey || 'mixdog-default';
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }) {
|
|
734
|
-
const headers = _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey });
|
|
735
|
-
const baseUrl = auth.type === 'xai'
|
|
736
|
-
? XAI_WS_URL
|
|
737
|
-
: auth.type === 'openai-direct'
|
|
738
|
-
? OPENAI_WS_URL
|
|
739
|
-
: CODEX_WS_URL;
|
|
740
|
-
const _wsOpenStart = Date.now();
|
|
741
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
742
|
-
process.stderr.write(`[agent-trace] ws-open-start url=${baseUrl} tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} ts=${_wsOpenStart}\n`);
|
|
743
|
-
}
|
|
744
|
-
const url = baseUrl + (sessionToken ? `?session_id=${encodeURIComponent(String(sessionToken))}` : '');
|
|
745
|
-
return new Promise((resolve, reject) => {
|
|
746
|
-
let settled = false;
|
|
747
|
-
let abortListener = null;
|
|
748
|
-
let acquireTimer = null;
|
|
749
|
-
const settle = (ok, val) => {
|
|
750
|
-
if (settled) return;
|
|
751
|
-
settled = true;
|
|
752
|
-
if (acquireTimer) {
|
|
753
|
-
clearTimeout(acquireTimer);
|
|
754
|
-
acquireTimer = null;
|
|
755
|
-
}
|
|
756
|
-
if (abortListener && externalSignal) {
|
|
757
|
-
try { externalSignal.removeEventListener('abort', abortListener); } catch {}
|
|
758
|
-
}
|
|
759
|
-
(ok ? resolve : reject)(val);
|
|
760
|
-
};
|
|
761
|
-
const socket = new WebSocket(url, { headers, handshakeTimeout: WS_HANDSHAKE_TIMEOUT_MS });
|
|
762
|
-
acquireTimer = setTimeout(() => {
|
|
763
|
-
if (settled) return;
|
|
764
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
765
|
-
process.stderr.write(`[agent-trace] ws-open-fail kind=acquire_timeout timeoutMs=${WS_ACQUIRE_TIMEOUT_MS} elapsed=${Date.now() - _wsOpenStart}ms\n`);
|
|
766
|
-
}
|
|
767
|
-
try { socket.terminate(); } catch {}
|
|
768
|
-
settle(false, Object.assign(
|
|
769
|
-
new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} acquire timed out before open (${WS_ACQUIRE_TIMEOUT_MS}ms)`),
|
|
770
|
-
{ code: 'EWSACQUIRETIMEOUT', acquireTimeoutMs: WS_ACQUIRE_TIMEOUT_MS },
|
|
771
|
-
));
|
|
772
|
-
}, WS_ACQUIRE_TIMEOUT_MS);
|
|
773
|
-
try { acquireTimer.unref?.(); } catch {}
|
|
774
|
-
const capturedHeaders = { turnState: null };
|
|
775
|
-
socket.once('upgrade', (res) => {
|
|
776
|
-
try {
|
|
777
|
-
const ts = res?.headers?.['x-codex-turn-state'];
|
|
778
|
-
if (typeof ts === 'string' && ts.length) capturedHeaders.turnState = ts;
|
|
779
|
-
} catch {}
|
|
780
|
-
});
|
|
781
|
-
socket.once('open', () => {
|
|
782
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
783
|
-
process.stderr.write(`[agent-trace] ws-open-ok elapsed=${Date.now() - _wsOpenStart}ms\n`);
|
|
784
|
-
}
|
|
785
|
-
settle(true, { socket, turnState: capturedHeaders.turnState });
|
|
786
|
-
});
|
|
787
|
-
socket.once('error', (err) => {
|
|
788
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
789
|
-
process.stderr.write(`[agent-trace] ws-open-fail kind=error msg=${String(err?.message || err).slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
|
|
790
|
-
}
|
|
791
|
-
try { socket.terminate(); } catch {}
|
|
792
|
-
settle(false, err instanceof Error ? err : Object.assign(new Error(errText(err) || 'openai-oauth WS error'), { wsErrorEvent: true, original: err }));
|
|
793
|
-
});
|
|
794
|
-
socket.once('close', (code, reason) => {
|
|
795
|
-
// Half-open handshake: the peer closed before 'open'/'error' fired
|
|
796
|
-
// (TCP RST / TLS edge). Without this the connect Promise never
|
|
797
|
-
// settles and only the 600s outer watchdog can break the stall
|
|
798
|
-
// (observed stage=requesting 601s hang). Open-path closes are
|
|
799
|
-
// no-ops here because settle() has already flipped `settled`.
|
|
800
|
-
if (settled) return;
|
|
801
|
-
try { socket.terminate(); } catch {}
|
|
802
|
-
settle(false, Object.assign(
|
|
803
|
-
new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} handshake closed before open (code=${code})`),
|
|
804
|
-
{ wsCloseCode: code, wsCloseReason: (reason && reason.toString) ? reason.toString('utf-8') : '' }));
|
|
805
|
-
});
|
|
806
|
-
socket.once('unexpected-response', (_req, res) => {
|
|
807
|
-
if (settled) return;
|
|
808
|
-
const status = res?.statusCode || 0;
|
|
809
|
-
let body = '';
|
|
810
|
-
res.on('data', c => { if (body.length < 2048) body += c.toString('utf-8'); });
|
|
811
|
-
res.on('end', () => {
|
|
812
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
813
|
-
process.stderr.write(`[agent-trace] ws-open-fail kind=http status=${status} body=${body.slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
|
|
814
|
-
}
|
|
815
|
-
try { socket.terminate(); } catch {}
|
|
816
|
-
settle(false, Object.assign(new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} handshake ${status}: ${body.slice(0, 200)}`), { httpStatus: status, httpBody: body }));
|
|
817
|
-
});
|
|
818
|
-
});
|
|
819
|
-
if (externalSignal) {
|
|
820
|
-
const onAbort = () => {
|
|
821
|
-
try { socket.terminate(); } catch {}
|
|
822
|
-
const reason = externalSignal.reason;
|
|
823
|
-
settle(false, reason instanceof Error ? reason : new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} handshake aborted`));
|
|
824
|
-
};
|
|
825
|
-
if (externalSignal.aborted) { onAbort(); return; }
|
|
826
|
-
abortListener = onAbort;
|
|
827
|
-
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
828
|
-
}
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
async function acquireWebSocket({ auth, poolKey, cacheKey, forceFresh, externalSignal }) {
|
|
833
|
-
const _acqStart = Date.now();
|
|
834
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
835
|
-
process.stderr.write(`[agent-trace] acquire-start poolKey=${poolKey} cacheKey=${cacheKey} forceFresh=${forceFresh} externalAborted=${!!externalSignal?.aborted} ts=${_acqStart}\n`);
|
|
836
|
-
}
|
|
837
|
-
if (externalSignal?.aborted) {
|
|
838
|
-
const reason = externalSignal.reason;
|
|
839
|
-
throw reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted');
|
|
840
|
-
}
|
|
841
|
-
if (poolKey && !forceFresh) {
|
|
842
|
-
const arr = _wsPool.get(poolKey) || [];
|
|
843
|
-
// Prune dead entries first.
|
|
844
|
-
for (let i = arr.length - 1; i >= 0; i--) {
|
|
845
|
-
if (!_isOpen(arr[i]) || arr[i].closing) {
|
|
846
|
-
_clearIdle(arr[i]);
|
|
847
|
-
arr.splice(i, 1);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
if (arr.length === 0) _wsPool.delete(poolKey);
|
|
851
|
-
// Reuse any idle open entry (cache-warm path).
|
|
852
|
-
const idle = arr.find(e => !e.busy);
|
|
853
|
-
if (idle) {
|
|
854
|
-
_clearIdle(idle);
|
|
855
|
-
idle.busy = true;
|
|
856
|
-
// Defensive: pre-existing pooled entries created before the
|
|
857
|
-
// prefix-hash field was introduced may not have it set. Normalize
|
|
858
|
-
// to null so the first delta check reads a deterministic value
|
|
859
|
-
// (and falls back to full-create instead of silently passing).
|
|
860
|
-
if (idle.lastInputPrefixHash === undefined) idle.lastInputPrefixHash = null;
|
|
861
|
-
if (idle.lastRequestInput === undefined) idle.lastRequestInput = null;
|
|
862
|
-
if (idle.lastResponseItems === undefined) idle.lastResponseItems = null;
|
|
863
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
864
|
-
process.stderr.write(`[agent-trace] acquire-reuse poolKey=${poolKey} openSockets=${arr.length} elapsed=${Date.now() - _acqStart}ms\n`);
|
|
865
|
-
}
|
|
866
|
-
return { entry: idle, reused: true };
|
|
867
|
-
}
|
|
868
|
-
// All entries busy and bucket at cap: fall through to ephemeral socket.
|
|
869
|
-
if (arr.length >= MAX_POOLED_SOCKETS_PER_KEY) {
|
|
870
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
871
|
-
process.stderr.write(`[agent-trace] acquire-ephemeral cacheKey=${cacheKey} reason=cap elapsed=${Date.now() - _acqStart}ms\n`);
|
|
872
|
-
}
|
|
873
|
-
const ephSessionToken = _mintSessionToken(cacheKey, auth);
|
|
874
|
-
const { socket, turnState } = await _openSocket({ auth, sessionToken: ephSessionToken, turnState: null, externalSignal, cacheKey });
|
|
875
|
-
// Drain-complete fence: same invariant as the normal acquire path —
|
|
876
|
-
// if drain fired during the await, do NOT push an ephemeral entry
|
|
877
|
-
// back into the pool.
|
|
878
|
-
if (_drainComplete) {
|
|
879
|
-
try { socket.close(1000, 'drain-complete'); } catch {}
|
|
880
|
-
throw new Error('WS pool drained — process exiting');
|
|
881
|
-
}
|
|
882
|
-
const entry = {
|
|
883
|
-
socket,
|
|
884
|
-
busy: true,
|
|
885
|
-
idleTimer: null,
|
|
886
|
-
lastResponseId: null,
|
|
887
|
-
lastRequestSansInput: null,
|
|
888
|
-
lastRequestInput: null,
|
|
889
|
-
lastResponseItems: null,
|
|
890
|
-
lastInputLen: 0,
|
|
891
|
-
lastInputPrefixHash: null,
|
|
892
|
-
turnState: turnState || null,
|
|
893
|
-
closing: false,
|
|
894
|
-
ephemeral: true,
|
|
895
|
-
sessionToken: ephSessionToken,
|
|
896
|
-
};
|
|
897
|
-
socket.on('close', () => { entry.closing = true; });
|
|
898
|
-
return { entry, reused: false };
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
// Parallel sockets must not inherit sibling turnState or the openai-oauth server
|
|
902
|
-
// treats the new request as a continuation of another in-flight turn and
|
|
903
|
-
// returns "No tool output found for function call …". turnState only
|
|
904
|
-
// propagates within a single entry across its own iterations.
|
|
905
|
-
const sessionToken = _mintSessionToken(cacheKey, auth);
|
|
906
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
907
|
-
process.stderr.write(`[agent-trace] acquire-new tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} elapsed=${Date.now() - _acqStart}ms\n`);
|
|
908
|
-
}
|
|
909
|
-
const { socket, turnState } = await _openSocket({ auth, sessionToken, turnState: null, externalSignal, cacheKey });
|
|
910
|
-
const entry = {
|
|
911
|
-
socket,
|
|
912
|
-
busy: true,
|
|
913
|
-
idleTimer: null,
|
|
914
|
-
lastResponseId: null,
|
|
915
|
-
lastRequestSansInput: null,
|
|
916
|
-
lastRequestInput: null,
|
|
917
|
-
lastResponseItems: null,
|
|
918
|
-
lastInputLen: 0,
|
|
919
|
-
lastInputPrefixHash: null,
|
|
920
|
-
turnState: turnState || null,
|
|
921
|
-
closing: false,
|
|
922
|
-
ephemeral: false,
|
|
923
|
-
sessionToken,
|
|
924
|
-
};
|
|
925
|
-
if (poolKey && !forceFresh) _getPoolArr(poolKey).push(entry);
|
|
926
|
-
socket.on('close', () => {
|
|
927
|
-
entry.closing = true;
|
|
928
|
-
_removeFromPool(poolKey, entry);
|
|
929
|
-
});
|
|
930
|
-
return { entry, reused: false };
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function releaseWebSocket({ entry, poolKey, keep }) {
|
|
934
|
-
if (!entry) return;
|
|
935
|
-
entry.busy = false;
|
|
936
|
-
if (!keep || !_isOpen(entry) || !poolKey || entry.ephemeral) {
|
|
937
|
-
try { entry.socket.close(1000, keep ? 'no_session' : 'release_no_keep'); } catch {}
|
|
938
|
-
_removeFromPool(poolKey, entry);
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
_scheduleIdleClose(poolKey, entry);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// If the cached request (sans input) matches the current one and the current
|
|
945
|
-
// input starts with the cached input, return only the tail. Otherwise return
|
|
946
|
-
// the full input (fresh turn).
|
|
947
|
-
function _sansInput(body) {
|
|
948
|
-
const { input: _ignored, previous_response_id: _prevIgnored, ...rest } = body;
|
|
949
|
-
return rest;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function _stableStringify(obj) {
|
|
953
|
-
// Shallow stable-ish: JSON.stringify with sorted top-level keys. Nested
|
|
954
|
-
// arrays (tools, include) are order-sensitive and reflect intent, so we
|
|
955
|
-
// do not sort them.
|
|
956
|
-
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return JSON.stringify(obj);
|
|
957
|
-
const keys = Object.keys(obj).sort();
|
|
958
|
-
const parts = [];
|
|
959
|
-
for (const k of keys) parts.push(JSON.stringify(k) + ':' + _stableStringify(obj[k]));
|
|
960
|
-
return '{' + parts.join(',') + '}';
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function _cloneJson(value) {
|
|
964
|
-
if (value == null) return value;
|
|
965
|
-
try { return JSON.parse(JSON.stringify(value)); } catch { return value; }
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function _responseItemKey(item, fallbackIndex = 0) {
|
|
969
|
-
if (!item || typeof item !== 'object') return `primitive:${fallbackIndex}`;
|
|
970
|
-
if (item.id) return `${item.type || 'item'}:id:${item.id}`;
|
|
971
|
-
if (item.call_id) return `${item.type || 'item'}:call:${item.call_id}`;
|
|
972
|
-
try { return `${item.type || 'item'}:json:${_stableStringify(item)}`; } catch {}
|
|
973
|
-
return `${item.type || 'item'}:${fallbackIndex}`;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
function _normalizeArguments(value) {
|
|
977
|
-
if (value == null) return '';
|
|
978
|
-
if (typeof value === 'string') {
|
|
979
|
-
const trimmed = value.trim();
|
|
980
|
-
try { return _stableStringify(JSON.parse(trimmed || '{}')); } catch { return trimmed; }
|
|
981
|
-
}
|
|
982
|
-
return _stableStringify(value);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function _normalizeContentPart(part) {
|
|
986
|
-
if (!part || typeof part !== 'object') return part;
|
|
987
|
-
const type = part.type === 'input_text' ? 'output_text' : part.type;
|
|
988
|
-
if (type === 'output_text') return { type, text: part.text || '' };
|
|
989
|
-
return part;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function _contentPartsEqual(a, b) {
|
|
993
|
-
const aa = Array.isArray(a) ? a.map(_normalizeContentPart) : [];
|
|
994
|
-
const bb = Array.isArray(b) ? b.map(_normalizeContentPart) : [];
|
|
995
|
-
return _stableStringify(aa) === _stableStringify(bb);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
export function _logicalResponseItemMatch(inputItem, responseItem) {
|
|
999
|
-
if (!inputItem || !responseItem) return false;
|
|
1000
|
-
const inputType = inputItem.type || (inputItem.role === 'assistant' ? 'message' : '');
|
|
1001
|
-
const responseType = responseItem.type || '';
|
|
1002
|
-
if (responseType === 'function_call') {
|
|
1003
|
-
if (inputType !== 'function_call') return false;
|
|
1004
|
-
const inputCallId = String(inputItem.call_id || '');
|
|
1005
|
-
const responseCallId = String(responseItem.call_id || '');
|
|
1006
|
-
const inputName = String(inputItem.name || '');
|
|
1007
|
-
const responseName = String(responseItem.name || '');
|
|
1008
|
-
if (inputCallId && responseCallId) {
|
|
1009
|
-
// call_id is the server-side anchor. The replayed history may carry
|
|
1010
|
-
// locally compacted arguments, but previous_response_id already
|
|
1011
|
-
// points at the canonical output item.
|
|
1012
|
-
return inputCallId === responseCallId && inputName === responseName;
|
|
1013
|
-
}
|
|
1014
|
-
return inputName === responseName
|
|
1015
|
-
&& _normalizeArguments(inputItem.arguments) === _normalizeArguments(responseItem.arguments);
|
|
1016
|
-
}
|
|
1017
|
-
if (responseType === 'tool_search_call') {
|
|
1018
|
-
if (inputType !== 'tool_search_call') return false;
|
|
1019
|
-
const inputCallId = String(inputItem.call_id || '');
|
|
1020
|
-
const responseCallId = String(responseItem.call_id || '');
|
|
1021
|
-
if (inputCallId && responseCallId) return inputCallId === responseCallId;
|
|
1022
|
-
return _normalizeArguments(inputItem.arguments) === _normalizeArguments(responseItem.arguments);
|
|
1023
|
-
}
|
|
1024
|
-
if (responseType === 'custom_tool_call') {
|
|
1025
|
-
if (inputType !== 'custom_tool_call') return false;
|
|
1026
|
-
const inputCallId = String(inputItem.call_id || '');
|
|
1027
|
-
const responseCallId = String(responseItem.call_id || '');
|
|
1028
|
-
const inputName = String(inputItem.name || '');
|
|
1029
|
-
const responseName = String(responseItem.name || '');
|
|
1030
|
-
if (inputCallId && responseCallId) return inputCallId === responseCallId && inputName === responseName;
|
|
1031
|
-
return inputName === responseName && String(inputItem.input || '') === String(responseItem.input || '');
|
|
1032
|
-
}
|
|
1033
|
-
if (responseType === 'message') {
|
|
1034
|
-
const inputRole = inputItem.role || (inputType === 'message' ? 'assistant' : '');
|
|
1035
|
-
const responseRole = responseItem.role || 'assistant';
|
|
1036
|
-
return inputType === 'message'
|
|
1037
|
-
&& inputRole === responseRole
|
|
1038
|
-
&& _contentPartsEqual(inputItem.content, responseItem.content);
|
|
1039
|
-
}
|
|
1040
|
-
if (responseType === 'reasoning') {
|
|
1041
|
-
return inputType === 'reasoning'
|
|
1042
|
-
&& (!!responseItem.id ? inputItem.id === responseItem.id : true)
|
|
1043
|
-
&& (!!responseItem.encrypted_content
|
|
1044
|
-
? inputItem.encrypted_content === responseItem.encrypted_content
|
|
1045
|
-
: true);
|
|
1046
|
-
}
|
|
1047
|
-
if (responseType === 'web_search_call') {
|
|
1048
|
-
return inputType === 'web_search_call'
|
|
1049
|
-
&& (!!responseItem.id ? inputItem.id === responseItem.id : true)
|
|
1050
|
-
&& _stableStringify(inputItem.action || null) === _stableStringify(responseItem.action || null);
|
|
1051
|
-
}
|
|
1052
|
-
if (inputType !== responseType) return false;
|
|
1053
|
-
const stripVolatile = (item) => {
|
|
1054
|
-
if (!item || typeof item !== 'object') return item;
|
|
1055
|
-
const { id: _id, status: _status, ...rest } = item;
|
|
1056
|
-
return rest;
|
|
1057
|
-
};
|
|
1058
|
-
return _stableStringify(stripVolatile(inputItem)) === _stableStringify(stripVolatile(responseItem));
|
|
1059
|
-
}
|
|
1060
107
|
|
|
1061
|
-
|
|
1062
|
-
return _stableStringify(a) === _stableStringify(b);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function _stripRequestPrefix(curInput, prevInput) {
|
|
1066
|
-
const current = Array.isArray(curInput) ? curInput : [];
|
|
1067
|
-
const previous = Array.isArray(prevInput) ? prevInput : [];
|
|
1068
|
-
if (current.length < previous.length) return null;
|
|
1069
|
-
for (let i = 0; i < previous.length; i += 1) {
|
|
1070
|
-
if (!_requestInputItemsMatch(current[i], previous[i])) return null;
|
|
1071
|
-
}
|
|
1072
|
-
return current.slice(previous.length);
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
function _isReplayLikeHead(item, responseItem) {
|
|
1076
|
-
if (!item || !responseItem) return false;
|
|
1077
|
-
const inputType = item.type || (item.role === 'assistant' ? 'message' : '');
|
|
1078
|
-
const responseType = responseItem.type || '';
|
|
1079
|
-
if (responseType === 'message') return inputType === 'message';
|
|
1080
|
-
if (responseType === 'function_call') return inputType === 'function_call';
|
|
1081
|
-
if (responseType === 'tool_search_call') return inputType === 'tool_search_call';
|
|
1082
|
-
return inputType === responseType;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function _stripResponseItemsFromHead(items, responseItems) {
|
|
1086
|
-
const tail = Array.isArray(items) ? items : [];
|
|
1087
|
-
const outputs = Array.isArray(responseItems) ? responseItems : [];
|
|
1088
|
-
let cursor = 0;
|
|
1089
|
-
let stripped = 0;
|
|
1090
|
-
let skipped = 0;
|
|
1091
|
-
for (const output of outputs) {
|
|
1092
|
-
if (cursor >= tail.length) break;
|
|
1093
|
-
if (_logicalResponseItemMatch(tail[cursor], output)) {
|
|
1094
|
-
cursor += 1;
|
|
1095
|
-
stripped += 1;
|
|
1096
|
-
continue;
|
|
1097
|
-
}
|
|
1098
|
-
if (_isReplayLikeHead(tail[cursor], output)) {
|
|
1099
|
-
return {
|
|
1100
|
-
ok: false,
|
|
1101
|
-
reason: `response_output_mismatch:${output?.type || 'unknown'}`,
|
|
1102
|
-
tail,
|
|
1103
|
-
stripped,
|
|
1104
|
-
skipped,
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
skipped += 1;
|
|
1108
|
-
}
|
|
1109
|
-
return { ok: true, reason: null, tail: tail.slice(cursor), stripped, skipped };
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function _computeDelta({ entry, body }) {
|
|
1113
|
-
if (!entry || !entry.lastRequestSansInput || !entry.lastResponseId) {
|
|
1114
|
-
return { mode: 'full', reason: 'no_anchor', frame: { type: 'response.create', ...body } };
|
|
1115
|
-
}
|
|
1116
|
-
if (!Array.isArray(entry.lastRequestInput)) {
|
|
1117
|
-
return { mode: 'full', reason: 'no_input_snapshot', frame: { type: 'response.create', ...body } };
|
|
1118
|
-
}
|
|
1119
|
-
const curSans = _stableStringify(_sansInput(body));
|
|
1120
|
-
if (curSans !== entry.lastRequestSansInput) {
|
|
1121
|
-
return { mode: 'full', reason: 'request_properties_changed', frame: { type: 'response.create', ...body } };
|
|
1122
|
-
}
|
|
1123
|
-
const curInput = Array.isArray(body.input) ? body.input : [];
|
|
1124
|
-
const afterPreviousInput = _stripRequestPrefix(curInput, entry.lastRequestInput);
|
|
1125
|
-
if (!afterPreviousInput) {
|
|
1126
|
-
return { mode: 'full', reason: 'input_prefix_mismatch', frame: { type: 'response.create', ...body } };
|
|
1127
|
-
}
|
|
1128
|
-
const stripped = _stripResponseItemsFromHead(afterPreviousInput, entry.lastResponseItems);
|
|
1129
|
-
if (!stripped.ok) {
|
|
1130
|
-
return { mode: 'full', reason: stripped.reason, frame: { type: 'response.create', ...body } };
|
|
1131
|
-
}
|
|
1132
|
-
return {
|
|
1133
|
-
mode: 'delta',
|
|
1134
|
-
reason: null,
|
|
1135
|
-
strippedResponseItems: stripped.stripped,
|
|
1136
|
-
skippedResponseItems: stripped.skipped,
|
|
1137
|
-
frame: {
|
|
1138
|
-
...body,
|
|
1139
|
-
type: 'response.create',
|
|
1140
|
-
previous_response_id: entry.lastResponseId,
|
|
1141
|
-
input: stripped.tail,
|
|
1142
|
-
},
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
function _estimateFrameTokens(frame) {
|
|
1147
|
-
try {
|
|
1148
|
-
const s = JSON.stringify(frame);
|
|
1149
|
-
return Math.ceil(s.length / 4);
|
|
1150
|
-
} catch { return 0; }
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
function _usageNum(value) {
|
|
1154
|
-
const n = Number(value || 0);
|
|
1155
|
-
return Number.isFinite(n) ? n : 0;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
function _combineUsageWithWarmup(actual, warmup) {
|
|
1159
|
-
if (!warmup) return actual;
|
|
1160
|
-
if (!actual) return warmup;
|
|
1161
|
-
const actualRaw = actual.raw || {};
|
|
1162
|
-
const warmupRaw = warmup.raw || {};
|
|
1163
|
-
const actualTicks = _usageNum(actualRaw.cost_in_usd_ticks);
|
|
1164
|
-
const warmupTicks = _usageNum(warmupRaw.cost_in_usd_ticks);
|
|
1165
|
-
return {
|
|
1166
|
-
...actual,
|
|
1167
|
-
inputTokens: _usageNum(actual.inputTokens) + _usageNum(warmup.inputTokens),
|
|
1168
|
-
outputTokens: _usageNum(actual.outputTokens) + _usageNum(warmup.outputTokens),
|
|
1169
|
-
cachedTokens: _usageNum(actual.cachedTokens) + _usageNum(warmup.cachedTokens),
|
|
1170
|
-
promptTokens: _usageNum(actual.promptTokens) + _usageNum(warmup.promptTokens),
|
|
1171
|
-
warmupInputTokens: _usageNum(warmup.inputTokens),
|
|
1172
|
-
warmupCachedTokens: _usageNum(warmup.cachedTokens),
|
|
1173
|
-
warmupOutputTokens: _usageNum(warmup.outputTokens),
|
|
1174
|
-
raw: {
|
|
1175
|
-
...actualRaw,
|
|
1176
|
-
warmup_usage: warmupRaw,
|
|
1177
|
-
...(actualTicks || warmupTicks ? { cost_in_usd_ticks: actualTicks + warmupTicks } : {}),
|
|
1178
|
-
},
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function _parseEvent(raw) {
|
|
1183
|
-
try { return JSON.parse(raw); } catch { return null; }
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function _incompleteReasonFromEvent(event) {
|
|
1187
|
-
const reasonObj = event?.response?.incomplete_details
|
|
1188
|
-
|| event?.incomplete_details
|
|
1189
|
-
|| event?.response?.status_details
|
|
1190
|
-
|| null;
|
|
1191
|
-
return String(reasonObj?.reason || event?.response?.status || 'incomplete');
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
function _isMaxOutputIncompleteReason(reason) {
|
|
1195
|
-
return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
function _httpStatusFromWsClose(code, reason) {
|
|
1199
|
-
const n = Number(code || 0);
|
|
1200
|
-
const r = String(reason || '').toLowerCase();
|
|
1201
|
-
if (n === 4401
|
|
1202
|
-
|| /\b(?:unauthorized|unauthorised|authentication|auth(?:enticated?)?|not authenticated|token expired|access token)\b/.test(r)) {
|
|
1203
|
-
return 401;
|
|
1204
|
-
}
|
|
1205
|
-
if (n === 4403 || /\b(?:forbidden|policy|permission denied)\b/.test(r)) return 403;
|
|
1206
|
-
if (n === 4429 || /\b(?:rate limit|quota)\b/.test(r)) return 429;
|
|
1207
|
-
return 0;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
function _wsErrLabel(p) {
|
|
1211
|
-
if (p === 'xai') return 'xAI WS';
|
|
1212
|
-
if (p === 'openai-direct' || p === 'openai') return 'OpenAI WS';
|
|
1213
|
-
return 'OpenAI OAuth WS';
|
|
1214
|
-
}
|
|
1215
|
-
// tool_search_call.arguments parse. Module-scope (exported) for direct test
|
|
1216
|
-
// coverage. Same policy as the function_call_arguments.done path and
|
|
1217
|
-
// openai-oauth _parseJsonObject —
|
|
1218
|
-
// object passes through; null/non-string/empty/whitespace → {} (no args); a
|
|
1219
|
-
// non-empty string that fails JSON.parse is deterministic bad JSON, surfaced
|
|
1220
|
-
// as an invalid-args MARKER (not silently swallowed to {}) so the dispatch
|
|
1221
|
-
// loop returns an is_error tool_result and the model self-corrects in the same
|
|
1222
|
-
// turn.
|
|
1223
|
-
export function parseToolSearchArgs(value) {
|
|
1224
|
-
if (value && typeof value === 'object') {
|
|
1225
|
-
// Reject arrays — the tool_search schema is an object
|
|
1226
|
-
// ({query,select,limit}); an array must never pass through as args.
|
|
1227
|
-
return Array.isArray(value) ? {} : value;
|
|
1228
|
-
}
|
|
1229
|
-
if (typeof value !== 'string' || !value.trim()) return {};
|
|
1230
|
-
try {
|
|
1231
|
-
const parsed = JSON.parse(value);
|
|
1232
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
1233
|
-
} catch (err) {
|
|
1234
|
-
return makeInvalidToolArgsMarker(value, err instanceof Error ? err.message : String(err));
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
export async function _streamResponse({
|
|
1238
|
-
entry,
|
|
1239
|
-
externalSignal,
|
|
1240
|
-
onStreamDelta,
|
|
1241
|
-
onToolCall,
|
|
1242
|
-
onTextDelta,
|
|
1243
|
-
state,
|
|
1244
|
-
logSuppressedReasoningDeltas = true,
|
|
1245
|
-
traceProvider = 'openai-oauth',
|
|
1246
|
-
_timeouts = null,
|
|
1247
|
-
knownToolNames = null,
|
|
1248
|
-
}) {
|
|
1249
|
-
const errLabel = _wsErrLabel(traceProvider);
|
|
1250
|
-
const socket = entry.socket;
|
|
1251
|
-
const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
|
|
1252
|
-
const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
|
|
1253
|
-
const _streamingStart = Date.now();
|
|
1254
|
-
let _firstDeltaEmitted = false;
|
|
1255
|
-
let content = '';
|
|
1256
|
-
let model = '';
|
|
1257
|
-
let responseId = '';
|
|
1258
|
-
let responseServiceTier = '';
|
|
1259
|
-
const toolCalls = [];
|
|
1260
|
-
const webSearchCalls = [];
|
|
1261
|
-
const webSearchCallKeys = new Set();
|
|
1262
|
-
const responseItemsAdded = [];
|
|
1263
|
-
const responseItemKeys = new Set();
|
|
1264
|
-
const citations = [];
|
|
1265
|
-
const citationKeys = new Set();
|
|
1266
|
-
const pendingCalls = new Map();
|
|
1267
|
-
// Tool-work-in-flight flag: set the moment a function/custom tool call's
|
|
1268
|
-
// input starts streaming (before it lands in pendingCalls/toolCalls).
|
|
1269
|
-
// Gates partial-final SUCCESS so a stall mid tool-input never looks text-only.
|
|
1270
|
-
let _toolInFlight = false;
|
|
1271
|
-
// Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
|
|
1272
|
-
// identical native function_call must fire onToolCall exactly once. Every
|
|
1273
|
-
// dispatch site routes through emitToolCallDedupe.
|
|
1274
|
-
const _toolDedupe = createToolCallDedupe();
|
|
1275
|
-
const emitToolCallDedupe = (call) => {
|
|
1276
|
-
if (!_toolDedupe.shouldDispatch(call?.name, call?.arguments)) return;
|
|
1277
|
-
midState.emittedToolCall = true;
|
|
1278
|
-
try { onToolCall?.(call); } catch {}
|
|
1279
|
-
};
|
|
1280
|
-
// Reasoning items collected from response.output_item.done (or salvaged
|
|
1281
|
-
// from response.completed.response.output). The request still includes
|
|
1282
|
-
// `reasoning.encrypted_content` so the server keeps emitting the blobs,
|
|
1283
|
-
// but explicit input-side replay is INTENTIONALLY OMITTED in
|
|
1284
|
-
// convertMessagesToResponsesInput (openai-oauth.mjs:233-238) — openai-oauth
|
|
1285
|
-
// rejects the same `rs_*` id twice in one handshake session_id with a
|
|
1286
|
-
// "Duplicate item" error. Server-side conversation state already carries
|
|
1287
|
-
// the prefix forward across the WS_IDLE_MS window. The collected
|
|
1288
|
-
// reasoningItems below are surfaced for trace/debugging only; they do
|
|
1289
|
-
// not feed back into the next request body.
|
|
1290
|
-
const reasoningItems = [];
|
|
1291
|
-
let reasoningTextDeltaCount = 0;
|
|
1292
|
-
let reasoningSummaryTextDeltaCount = 0;
|
|
1293
|
-
let reasoningOtherDeltaCount = 0;
|
|
1294
|
-
let reasoningDeltaLogEmitted = false;
|
|
1295
|
-
const pushReasoningItem = (item) => {
|
|
1296
|
-
if (!item || item.type !== 'reasoning') return;
|
|
1297
|
-
if (!item.encrypted_content) return;
|
|
1298
|
-
reasoningItems.push({
|
|
1299
|
-
id: item.id || '',
|
|
1300
|
-
encrypted_content: item.encrypted_content,
|
|
1301
|
-
summary: Array.isArray(item.summary) ? item.summary : [],
|
|
1302
|
-
});
|
|
1303
|
-
};
|
|
1304
|
-
const pushResponseItem = (item) => {
|
|
1305
|
-
if (!item || typeof item !== 'object') return;
|
|
1306
|
-
const key = _responseItemKey(item, responseItemsAdded.length);
|
|
1307
|
-
if (responseItemKeys.has(key)) {
|
|
1308
|
-
const existing = responseItemsAdded.find((candidate, index) => _responseItemKey(candidate, index) === key);
|
|
1309
|
-
if (existing?.type === 'function_call' && item.type === 'function_call') {
|
|
1310
|
-
if (!existing.call_id && item.call_id) existing.call_id = item.call_id;
|
|
1311
|
-
if (!existing.name && item.name) existing.name = item.name;
|
|
1312
|
-
if ((existing.arguments == null || existing.arguments === '') && item.arguments != null) existing.arguments = item.arguments;
|
|
1313
|
-
}
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
responseItemKeys.add(key);
|
|
1317
|
-
responseItemsAdded.push(_cloneJson(item));
|
|
1318
|
-
};
|
|
1319
|
-
const enrichFunctionCallResponseItem = ({ itemId = '', callId = '', name = '', argumentsText = '' } = {}) => {
|
|
1320
|
-
for (const item of responseItemsAdded) {
|
|
1321
|
-
if (item?.type !== 'function_call') continue;
|
|
1322
|
-
if (itemId && item.id && item.id !== itemId) continue;
|
|
1323
|
-
if (callId && item.call_id && item.call_id !== callId) continue;
|
|
1324
|
-
if (!item.call_id && callId) item.call_id = callId;
|
|
1325
|
-
if (!item.name && name) item.name = name;
|
|
1326
|
-
if ((item.arguments == null || item.arguments === '') && argumentsText) item.arguments = argumentsText;
|
|
1327
|
-
}
|
|
1328
|
-
};
|
|
1329
|
-
const pushCitation = (raw, fallbackTitle = '') => {
|
|
1330
|
-
const url = raw?.url || raw?.uri || raw?.href || '';
|
|
1331
|
-
if (!url || citationKeys.has(url)) return;
|
|
1332
|
-
citationKeys.add(url);
|
|
1333
|
-
citations.push({
|
|
1334
|
-
title: raw?.title || fallbackTitle || '',
|
|
1335
|
-
url,
|
|
1336
|
-
snippet: raw?.snippet || raw?.text || raw?.description || '',
|
|
1337
|
-
source: 'openai-oauth',
|
|
1338
|
-
});
|
|
1339
|
-
};
|
|
1340
|
-
const pushOutputTextAnnotations = (contentPart) => {
|
|
1341
|
-
const annotations = Array.isArray(contentPart?.annotations) ? contentPart.annotations : [];
|
|
1342
|
-
for (const annotation of annotations) pushCitation(annotation);
|
|
1343
|
-
};
|
|
1344
|
-
const pushWebSearchCall = (item) => {
|
|
1345
|
-
if (!item || item.type !== 'web_search_call') return;
|
|
1346
|
-
let key = item.id || '';
|
|
1347
|
-
if (!key) {
|
|
1348
|
-
try { key = JSON.stringify(item.action || item); } catch { key = `${webSearchCalls.length}`; }
|
|
1349
|
-
}
|
|
1350
|
-
if (webSearchCallKeys.has(key)) return;
|
|
1351
|
-
webSearchCallKeys.add(key);
|
|
1352
|
-
webSearchCalls.push({
|
|
1353
|
-
id: item.id || '',
|
|
1354
|
-
status: item.status || '',
|
|
1355
|
-
action: item.action || null,
|
|
1356
|
-
});
|
|
1357
|
-
const action = item.action || {};
|
|
1358
|
-
if (action.url) pushCitation({ url: action.url, title: action.query || '' });
|
|
1359
|
-
if (Array.isArray(action.urls)) {
|
|
1360
|
-
for (const url of action.urls) pushCitation({ url, title: action.query || '' });
|
|
1361
|
-
}
|
|
1362
|
-
};
|
|
1363
|
-
const pushCustomToolCall = (item) => {
|
|
1364
|
-
const call = customToolCallFromResponseItem(item);
|
|
1365
|
-
if (!call || toolCalls.some((existing) => existing.id === call.id)) return;
|
|
1366
|
-
toolCalls.push(call);
|
|
1367
|
-
emitToolCallDedupe(call);
|
|
1368
|
-
};
|
|
1369
|
-
const pushToolSearchCall = (item) => {
|
|
1370
|
-
if (!item || item.type !== 'tool_search_call') return;
|
|
1371
|
-
const callId = item.call_id || item.id || '';
|
|
1372
|
-
if (!callId || toolCalls.some((call) => call.id === callId)) return;
|
|
1373
|
-
const call = {
|
|
1374
|
-
id: callId,
|
|
1375
|
-
name: 'tool_search',
|
|
1376
|
-
arguments: parseToolSearchArgs(item.arguments),
|
|
1377
|
-
nativeType: 'tool_search_call',
|
|
1378
|
-
};
|
|
1379
|
-
toolCalls.push(call);
|
|
1380
|
-
emitToolCallDedupe(call);
|
|
1381
|
-
};
|
|
1382
|
-
// Leaked tool-call guard. The model sometimes emits a tool call as plain
|
|
1383
|
-
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
1384
|
-
// `<|channel|>...to=functions.NAME...<|call|>`) inside
|
|
1385
|
-
// `response.output_text.delta` instead of a native function_call. Route
|
|
1386
|
-
// text through the guard so leaked calls are suppressed from the visible
|
|
1387
|
-
// stream, synthesized (native `call_...` id shape), and dispatched like
|
|
1388
|
-
// native ones. Additive: the native function_call path is untouched.
|
|
1389
|
-
const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
|
|
1390
|
-
const dispatchLeakedCall = (recovered) => {
|
|
1391
|
-
let args = recovered?.arguments;
|
|
1392
|
-
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
1393
|
-
const call = {
|
|
1394
|
-
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
1395
|
-
name: recovered.name,
|
|
1396
|
-
arguments: args,
|
|
1397
|
-
};
|
|
1398
|
-
if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
|
|
1399
|
-
toolCalls.push(call);
|
|
1400
|
-
midState.emittedToolCall = true;
|
|
1401
|
-
try { onToolCall?.(call); } catch {}
|
|
1402
|
-
};
|
|
1403
|
-
const relayLeakText = (delta) => {
|
|
1404
|
-
if (!leakGuard.enabled) {
|
|
1405
|
-
content += delta || '';
|
|
1406
|
-
if (delta && onTextDelta) {
|
|
1407
|
-
if (state) state.emittedText = true;
|
|
1408
|
-
try { onTextDelta(delta); } catch {}
|
|
1409
|
-
}
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
const { text, calls } = leakGuard.push(delta);
|
|
1413
|
-
if (text) {
|
|
1414
|
-
content += text;
|
|
1415
|
-
if (onTextDelta) {
|
|
1416
|
-
if (state) state.emittedText = true;
|
|
1417
|
-
try { onTextDelta(text); } catch {}
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
for (const c of calls) dispatchLeakedCall(c);
|
|
1421
|
-
};
|
|
1422
|
-
const flushLeak = () => {
|
|
1423
|
-
if (!leakGuard.enabled) return;
|
|
1424
|
-
const { text, calls } = leakGuard.flush();
|
|
1425
|
-
if (text) {
|
|
1426
|
-
content += text;
|
|
1427
|
-
if (onTextDelta) {
|
|
1428
|
-
if (state) state.emittedText = true;
|
|
1429
|
-
try { onTextDelta(text); } catch {}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
for (const c of calls) dispatchLeakedCall(c);
|
|
1433
|
-
};
|
|
1434
|
-
const logReasoningDeltaSuppression = () => {
|
|
1435
|
-
if (!logSuppressedReasoningDeltas) return;
|
|
1436
|
-
const total = reasoningTextDeltaCount + reasoningSummaryTextDeltaCount + reasoningOtherDeltaCount;
|
|
1437
|
-
if (reasoningDeltaLogEmitted || total === 0) return;
|
|
1438
|
-
reasoningDeltaLogEmitted = true;
|
|
1439
|
-
process.stderr.write(`[openai-oauth-ws] suppressed reasoning text deltas from user content count=${total} text=${reasoningTextDeltaCount} summary=${reasoningSummaryTextDeltaCount} other=${reasoningOtherDeltaCount}\n`);
|
|
1440
|
-
};
|
|
1441
|
-
let usage;
|
|
1442
|
-
let stopReason = null;
|
|
1443
|
-
let incompleteReason = null;
|
|
1444
|
-
let done = false;
|
|
1445
|
-
let terminalError = null;
|
|
1446
|
-
// Mid-stream retry classifier needs to distinguish "stream died before we
|
|
1447
|
-
// even saw response.created" from "stream died after we had a partial
|
|
1448
|
-
// response but before completion". Mutate the shared state object so the
|
|
1449
|
-
// caller can inspect flags on the error path without us having to attach
|
|
1450
|
-
// them manually at every reject site.
|
|
1451
|
-
const midState = state || {};
|
|
1452
|
-
midState.sawResponseCreated = midState.sawResponseCreated || false;
|
|
1453
|
-
midState.sawCompleted = midState.sawCompleted || false;
|
|
1454
|
-
midState.wsCloseCode = null;
|
|
1455
|
-
midState.responseFailedPayload = null;
|
|
1456
|
-
let idleTimer = null;
|
|
1457
|
-
let keepaliveTimer = null;
|
|
1458
|
-
let abortHandler = null;
|
|
1459
|
-
let messageHandler = null;
|
|
1460
|
-
let closeHandler = null;
|
|
1461
|
-
let errorHandler = null;
|
|
1462
|
-
// SEMANTIC idle timer: distinct from the inter-chunk
|
|
1463
|
-
// timer, which resets on EVERY frame (rate_limits/metadata/keepalive keep
|
|
1464
|
-
// the socket "alive"). This timer resets ONLY on meaningful output deltas
|
|
1465
|
-
// (text/reasoning/tool args — the same events that call onStreamDelta) so a
|
|
1466
|
-
// stream that emits some deltas then goes silent (server keepalive frames
|
|
1467
|
-
// only) trips a short, named terminal StreamStalledError instead of coasting
|
|
1468
|
-
// to the 300s inter-chunk cap / 30-min agent watchdog.
|
|
1469
|
-
let semanticIdleTimer = null;
|
|
1470
|
-
const semanticIdleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
1471
|
-
const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
|
|
1472
|
-
|
|
1473
|
-
return new Promise((resolve, reject) => {
|
|
1474
|
-
// Pre-stream watchdog: the timer fires if the server never sends a
|
|
1475
|
-
// first event (response.created) within preResponseCreatedMs
|
|
1476
|
-
// after our last frame. The socket is open and the response.create
|
|
1477
|
-
// frame was sent, but no server event has come back — a wedged
|
|
1478
|
-
// post-upgrade socket. Healthy servers ack within seconds, so this
|
|
1479
|
-
// window is intentionally short (WS_PRE_RESPONSE_CREATED_MS, ~10s).
|
|
1480
|
-
// Once ANY server event arrives, resetIdle() cancels this watchdog and
|
|
1481
|
-
// the single inter-chunk idle timer takes over — silent gaps
|
|
1482
|
-
// mid-reasoning (openai-oauth spending 50s+ producing reasoning
|
|
1483
|
-
// tokens) are normal and should not abort the turn.
|
|
1484
|
-
const armPreStreamWatchdog = () => {
|
|
1485
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
1486
|
-
idleTimer = setTimeout(() => {
|
|
1487
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1488
|
-
process.stderr.write(`[agent-trace] ws-timeout kind=first-byte afterMs=${preResponseCreatedMs}\n`);
|
|
1489
|
-
}
|
|
1490
|
-
traceWsTimeout('first_byte_timeout', preResponseCreatedMs);
|
|
1491
|
-
const err = new Error(`WS stream: no first server event within ${preResponseCreatedMs}ms`);
|
|
1492
|
-
// Tag the close code so _classifyMidstreamError sees a 4000
|
|
1493
|
-
// (our local pre-stream watchdog code) and routes through
|
|
1494
|
-
// the post-upgrade-no-first-event retryable bucket.
|
|
1495
|
-
err.wsCloseCode = 4000;
|
|
1496
|
-
// Tag the error object itself (not just midState): the warmup
|
|
1497
|
-
// path streams under a separate warmupState and rethrows on
|
|
1498
|
-
// timeout BEFORE it can copy flags to the outer midState, so the
|
|
1499
|
-
// outer catch's _classifyMidstreamError would otherwise see
|
|
1500
|
-
// sawResponseCreated=false + close 4000 and hit the pre-created
|
|
1501
|
-
// deny gate. err.firstByteTimeout makes both paths retryable.
|
|
1502
|
-
err.firstByteTimeout = true;
|
|
1503
|
-
midState.firstByteTimeout = true;
|
|
1504
|
-
terminalError = err;
|
|
1505
|
-
try { socket.close(4000, 'first_byte_timeout'); } catch {}
|
|
1506
|
-
// socket.close() may not settle a half-open WS (closeHandler never
|
|
1507
|
-
// fires) — reject directly so the turn retries instead of hanging
|
|
1508
|
-
// until the 600s watchdog. finish() is idempotent (Promise settles
|
|
1509
|
-
// once; cleanup is null-safe).
|
|
1510
|
-
finish();
|
|
1511
|
-
}, preResponseCreatedMs);
|
|
1512
|
-
};
|
|
1513
|
-
let interChunkTimer = null;
|
|
1514
|
-
const traceWsTimeout = (event, timeoutMs) => {
|
|
1515
|
-
try {
|
|
1516
|
-
const iteration = Number(midState.iteration);
|
|
1517
|
-
const attemptIndex = Number(midState.attemptIndex);
|
|
1518
|
-
const payload = {
|
|
1519
|
-
provider: midState.traceProvider || traceProvider,
|
|
1520
|
-
transport: 'websocket',
|
|
1521
|
-
event,
|
|
1522
|
-
timeout_ms: timeoutMs,
|
|
1523
|
-
elapsed_ms: Date.now() - _streamingStart,
|
|
1524
|
-
model: midState.model || model || null,
|
|
1525
|
-
attempt_index: Number.isFinite(attemptIndex) ? attemptIndex : null,
|
|
1526
|
-
warmup: midState.warmup === true,
|
|
1527
|
-
saw_response_created: midState.sawResponseCreated === true,
|
|
1528
|
-
};
|
|
1529
|
-
appendAgentTrace({
|
|
1530
|
-
sessionId: midState.sessionId || null,
|
|
1531
|
-
iteration: Number.isFinite(iteration) ? iteration : null,
|
|
1532
|
-
kind: 'ws_timeout',
|
|
1533
|
-
...payload,
|
|
1534
|
-
payload,
|
|
1535
|
-
});
|
|
1536
|
-
} catch {}
|
|
1537
|
-
};
|
|
1538
|
-
const clearPreStreamWatchdog = () => {
|
|
1539
|
-
if (idleTimer) {
|
|
1540
|
-
clearTimeout(idleTimer);
|
|
1541
|
-
idleTimer = null;
|
|
1542
|
-
}
|
|
1543
|
-
};
|
|
1544
|
-
const resetInterChunk = () => {
|
|
1545
|
-
if (interChunkTimer) clearTimeout(interChunkTimer);
|
|
1546
|
-
interChunkTimer = setTimeout(() => {
|
|
1547
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1548
|
-
process.stderr.write(`[agent-trace] ws-timeout kind=inter-chunk afterMs=${interChunkMs}\n`);
|
|
1549
|
-
}
|
|
1550
|
-
traceWsTimeout('inter_chunk_timeout', interChunkMs);
|
|
1551
|
-
terminalError = new Error(`WS stream: inter-chunk inactivity for ${interChunkMs}ms`);
|
|
1552
|
-
try { socket.close(4000, 'inter_chunk_timeout'); } catch {}
|
|
1553
|
-
// Same half-open guard as the pre-stream watchdog: reject directly
|
|
1554
|
-
// so a stuck socket.close() cannot leave the Promise pending.
|
|
1555
|
-
finish();
|
|
1556
|
-
}, interChunkMs);
|
|
1557
|
-
};
|
|
1558
|
-
// pi per-event idle: (re)armed only on meaningful output deltas via
|
|
1559
|
-
// bumpSemanticIdle(). Keepalive/metadata frames DON'T touch it, so a
|
|
1560
|
-
// deltas-then-silent wedge trips this short semantic window.
|
|
1561
|
-
const resetSemanticIdle = () => {
|
|
1562
|
-
if (!semanticIdleEnabled) return;
|
|
1563
|
-
if (semanticIdleTimer) clearTimeout(semanticIdleTimer);
|
|
1564
|
-
semanticIdleTimer = setTimeout(() => {
|
|
1565
|
-
traceWsTimeout('semantic_idle_timeout', semanticIdleMs);
|
|
1566
|
-
terminalError = streamStalledError('Responses WS', semanticIdleMs, { emittedToolCall: !!midState?.emittedToolCall });
|
|
1567
|
-
// Partial-final recovery: attach streamed partial state so
|
|
1568
|
-
// a wedged FINAL no-tool summary can be accepted as partial-final
|
|
1569
|
-
// success by the loop. pendingToolUse gates out mid-flight tools.
|
|
1570
|
-
try {
|
|
1571
|
-
terminalError.partialContent = content;
|
|
1572
|
-
terminalError.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
1573
|
-
terminalError.pendingToolUse = pendingCalls.size > 0
|
|
1574
|
-
|| !!midState?.emittedToolCall
|
|
1575
|
-
|| toolCalls.length > 0
|
|
1576
|
-
|| _toolInFlight === true;
|
|
1577
|
-
terminalError.partialModel = model || undefined;
|
|
1578
|
-
} catch { /* best-effort enrichment */ }
|
|
1579
|
-
try { terminalError.wsCloseCode = 4000; } catch {}
|
|
1580
|
-
try { socket.close(4000, 'semantic_idle_timeout'); } catch {}
|
|
1581
|
-
finish();
|
|
1582
|
-
}, semanticIdleMs);
|
|
1583
|
-
try { semanticIdleTimer.unref?.(); } catch {}
|
|
1584
|
-
};
|
|
1585
|
-
// Single idle reset — called on EVERY parsed server event (matches
|
|
1586
|
-
// codex, which resets one idle timer on every received WS frame). Any
|
|
1587
|
-
// frame proves the socket is live; there is no separate "meaningful
|
|
1588
|
-
// output" gate. Also clears the pre-stream watchdog defensively in case
|
|
1589
|
-
// the first event is not response.created.
|
|
1590
|
-
const resetIdle = () => {
|
|
1591
|
-
clearPreStreamWatchdog();
|
|
1592
|
-
resetInterChunk();
|
|
1593
|
-
};
|
|
1594
|
-
// Meaningful-output progress bump: called by the same delta cases that
|
|
1595
|
-
// call onStreamDelta (text/reasoning/tool args). Arms the semantic idle.
|
|
1596
|
-
const bumpSemanticIdle = () => { resetSemanticIdle(); };
|
|
1597
|
-
const cleanup = () => {
|
|
1598
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
1599
|
-
if (interChunkTimer) { clearTimeout(interChunkTimer); interChunkTimer = null; }
|
|
1600
|
-
if (semanticIdleTimer) { clearTimeout(semanticIdleTimer); semanticIdleTimer = null; }
|
|
1601
|
-
if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
|
|
1602
|
-
if (messageHandler) socket.off('message', messageHandler);
|
|
1603
|
-
if (closeHandler) socket.off('close', closeHandler);
|
|
1604
|
-
if (errorHandler) socket.off('error', errorHandler);
|
|
1605
|
-
if (abortHandler && externalSignal) externalSignal.removeEventListener('abort', abortHandler);
|
|
1606
|
-
};
|
|
1607
|
-
const finish = () => {
|
|
1608
|
-
logReasoningDeltaSuppression();
|
|
1609
|
-
// Flush any partial-sentinel tail held back mid-stream so
|
|
1610
|
-
// legitimate trailing text is never lost (streamed-text path).
|
|
1611
|
-
flushLeak();
|
|
1612
|
-
cleanup();
|
|
1613
|
-
if (terminalError) { reject(terminalError); return; }
|
|
1614
|
-
resolve({
|
|
1615
|
-
content,
|
|
1616
|
-
model,
|
|
1617
|
-
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1618
|
-
responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
|
|
1619
|
-
// Dedupe by name+args (Fix 2, array side) so an identical
|
|
1620
|
-
// synthetic-leaked + native pair can't run the tool twice.
|
|
1621
|
-
toolCalls: toolCalls.length ? dedupeToolCallList(toolCalls) : undefined,
|
|
1622
|
-
citations: citations.length ? citations : undefined,
|
|
1623
|
-
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
1624
|
-
usage,
|
|
1625
|
-
stopReason: stopReason || undefined,
|
|
1626
|
-
// P1 audit fix: mirror the HTTP/SSE fallback's truncated flag
|
|
1627
|
-
// for the WS path (sendViaWebSocket spreads this result
|
|
1628
|
-
// through to the provider caller unchanged).
|
|
1629
|
-
...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
|
|
1630
|
-
incompleteReason: incompleteReason || undefined,
|
|
1631
|
-
responseId: responseId || undefined,
|
|
1632
|
-
serviceTier: responseServiceTier || undefined,
|
|
1633
|
-
});
|
|
1634
|
-
};
|
|
1635
|
-
|
|
1636
|
-
messageHandler = (data) => {
|
|
1637
|
-
resetIdle();
|
|
1638
|
-
// resetIdle() above resets the SINGLE inter-chunk idle timer on
|
|
1639
|
-
// EVERY received frame — response.created, metadata,
|
|
1640
|
-
// rate_limits, and all deltas keep the socket alive. Separately, do
|
|
1641
|
-
// NOT call onStreamDelta for every frame — metadata/keepalive frames
|
|
1642
|
-
// must not reset the agent stall watchdog's lastStreamDeltaAt. Only
|
|
1643
|
-
// meaningful output (text delta / tool call) updates that timestamp.
|
|
1644
|
-
const text = typeof data === 'string' ? data : data.toString('utf-8');
|
|
1645
|
-
const event = _parseEvent(text);
|
|
1646
|
-
if (!event) return;
|
|
1647
|
-
if (event.error) {
|
|
1648
|
-
const err = new Error(event.error.message || 'Responses WS error');
|
|
1649
|
-
try {
|
|
1650
|
-
err.payload = event.error;
|
|
1651
|
-
populateHttpStatusFromMessage(err);
|
|
1652
|
-
} catch {}
|
|
1653
|
-
terminalError = err;
|
|
1654
|
-
finish();
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
if (typeof event.type !== 'string') return;
|
|
1658
|
-
switch (event.type) {
|
|
1659
|
-
case 'response.created':
|
|
1660
|
-
midState.sawResponseCreated = true;
|
|
1661
|
-
if (event.response?.model) model = event.response.model;
|
|
1662
|
-
if (event.response?.id) responseId = event.response.id;
|
|
1663
|
-
// Server ack (first event). resetIdle() at the top of
|
|
1664
|
-
// messageHandler already cleared the pre-stream watchdog and
|
|
1665
|
-
// armed the single idle timer — no extra bookkeeping here.
|
|
1666
|
-
break;
|
|
1667
|
-
case 'response.output_text.delta':
|
|
1668
|
-
try {
|
|
1669
|
-
if (!_firstDeltaEmitted) {
|
|
1670
|
-
_firstDeltaEmitted = true;
|
|
1671
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1672
|
-
process.stderr.write(`[agent-trace] ws-first-delta sinceStreaming=${Date.now() - _streamingStart}ms\n`);
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
onStreamDelta?.();
|
|
1676
|
-
} catch {}
|
|
1677
|
-
bumpSemanticIdle();
|
|
1678
|
-
// Live text relay (gateway): forward the raw text chunk so
|
|
1679
|
-
// the client renders first tokens before the final replay.
|
|
1680
|
-
// Tool-call/argument deltas intentionally stay off this path.
|
|
1681
|
-
// Invariant: once a non-empty chunk has been relayed live it
|
|
1682
|
-
// cannot be withdrawn, so flag the attempt so a later
|
|
1683
|
-
// mid-stream/truncated failure is NOT retried (retry would
|
|
1684
|
-
// concatenate a second attempt onto rendered text).
|
|
1685
|
-
// Routed through the leaked-tool-call guard: appends to
|
|
1686
|
-
// `content`, forwards visible text via onTextDelta, and
|
|
1687
|
-
// recovers/dispatches any leaked known-tool call.
|
|
1688
|
-
relayLeakText(event.delta || '');
|
|
1689
|
-
break;
|
|
1690
|
-
case 'response.reasoning_text.delta':
|
|
1691
|
-
case 'response.reasoning_summary_text.delta':
|
|
1692
|
-
if (event.type === 'response.reasoning_text.delta') reasoningTextDeltaCount += 1;
|
|
1693
|
-
else reasoningSummaryTextDeltaCount += 1;
|
|
1694
|
-
// Reasoning text is live model progress — refresh
|
|
1695
|
-
// lastStreamDeltaAt so stream-watchdog does not flag a
|
|
1696
|
-
// long reasoning span as a stall. The local WS idle timer
|
|
1697
|
-
// was already reset by resetIdle() at the top of
|
|
1698
|
-
// messageHandler. Reasoning is still suppressed from user
|
|
1699
|
-
// content (no `content +=` here).
|
|
1700
|
-
try { onStreamDelta?.(); } catch {}
|
|
1701
|
-
bumpSemanticIdle();
|
|
1702
|
-
break;
|
|
1703
|
-
case 'response.output_item.added':
|
|
1704
|
-
if (event.item?.type === 'function_call') {
|
|
1705
|
-
pendingCalls.set(event.item.id || '', {
|
|
1706
|
-
name: event.item.name || '',
|
|
1707
|
-
callId: event.item.call_id || '',
|
|
1708
|
-
});
|
|
1709
|
-
_toolInFlight = true;
|
|
1710
|
-
} else if (event.item?.type === 'custom_tool_call') {
|
|
1711
|
-
_toolInFlight = true;
|
|
1712
|
-
} else if (event.item?.type === 'tool_search_call') {
|
|
1713
|
-
// Mark tool_search in-flight at item-added time, same
|
|
1714
|
-
// as function_call/custom_tool_call above, so the
|
|
1715
|
-
// semantic-idle stall gate's pendingToolUse never
|
|
1716
|
-
// drops a mid-flight tool_search before
|
|
1717
|
-
// response.output_item.done.
|
|
1718
|
-
_toolInFlight = true;
|
|
1719
|
-
}
|
|
1720
|
-
break;
|
|
1721
|
-
case 'response.function_call_arguments.delta':
|
|
1722
|
-
_toolInFlight = true;
|
|
1723
|
-
try { onStreamDelta?.(); } catch {}
|
|
1724
|
-
bumpSemanticIdle();
|
|
1725
|
-
break;
|
|
1726
|
-
case 'response.custom_tool_call_input.delta':
|
|
1727
|
-
_toolInFlight = true;
|
|
1728
|
-
try { onStreamDelta?.(); } catch {}
|
|
1729
|
-
bumpSemanticIdle();
|
|
1730
|
-
break;
|
|
1731
|
-
case 'response.function_call_arguments.done': {
|
|
1732
|
-
const itemId = event.item_id || '';
|
|
1733
|
-
const pending = pendingCalls.get(itemId);
|
|
1734
|
-
// function_call_arguments.done is a completion signal:
|
|
1735
|
-
// empty/whitespace → no args ({}); a non-empty string that
|
|
1736
|
-
// fails JSON.parse is deterministic bad JSON. Surface an
|
|
1737
|
-
// invalid-args MARKER (not silent {}) so the dispatch loop
|
|
1738
|
-
// returns an is_error tool_result and the model re-issues
|
|
1739
|
-
// valid JSON in the same turn.
|
|
1740
|
-
let args = {};
|
|
1741
|
-
{
|
|
1742
|
-
const _argText = typeof event.arguments === 'string' ? event.arguments : '';
|
|
1743
|
-
if (_argText.trim() !== '') {
|
|
1744
|
-
try {
|
|
1745
|
-
args = JSON.parse(_argText);
|
|
1746
|
-
} catch (err) {
|
|
1747
|
-
args = makeInvalidToolArgsMarker(_argText, err instanceof Error ? err.message : String(err));
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
enrichFunctionCallResponseItem({
|
|
1752
|
-
itemId,
|
|
1753
|
-
callId: pending?.callId || event.call_id || '',
|
|
1754
|
-
name: pending?.name || event.name || '',
|
|
1755
|
-
argumentsText: event.arguments || JSON.stringify(args),
|
|
1756
|
-
});
|
|
1757
|
-
if (pending?.callId && pending?.name) {
|
|
1758
|
-
const call = { id: pending.callId, name: pending.name, arguments: args };
|
|
1759
|
-
toolCalls.push(call);
|
|
1760
|
-
emitToolCallDedupe(call);
|
|
1761
|
-
} else {
|
|
1762
|
-
// Synthesizing a `tc_${Date.now()}` callId here would
|
|
1763
|
-
// make the next turn fail to match the model's
|
|
1764
|
-
// function_call_output reference. Defer instead and
|
|
1765
|
-
// salvage call_id/name from the final
|
|
1766
|
-
// response.completed.output bundle below. If salvage
|
|
1767
|
-
// also fails we fail the stream explicitly — masking
|
|
1768
|
-
// the gap with a synthetic id just shifts the failure
|
|
1769
|
-
// one turn later under a confusing "No tool output
|
|
1770
|
-
// found for function call" error.
|
|
1771
|
-
toolCalls.push({
|
|
1772
|
-
id: pending?.callId || '',
|
|
1773
|
-
name: pending?.name || '',
|
|
1774
|
-
arguments: args,
|
|
1775
|
-
_pendingItemId: itemId,
|
|
1776
|
-
_deferred: true,
|
|
1777
|
-
});
|
|
1778
|
-
}
|
|
1779
|
-
try { onStreamDelta?.(); } catch {}
|
|
1780
|
-
bumpSemanticIdle();
|
|
1781
|
-
break;
|
|
1782
|
-
}
|
|
1783
|
-
case 'response.output_item.done':
|
|
1784
|
-
pushResponseItem(event.item);
|
|
1785
|
-
// function_call / output_text already captured via their
|
|
1786
|
-
// dedicated streaming events. The one shape we still need
|
|
1787
|
-
// here is `reasoning` — carries encrypted_content that
|
|
1788
|
-
// must be replayed on the next input to keep the openai-oauth
|
|
1789
|
-
// server-side prompt cache prefix warm.
|
|
1790
|
-
if (event.item?.type === 'reasoning') pushReasoningItem(event.item);
|
|
1791
|
-
if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
|
|
1792
|
-
if (event.item?.type === 'tool_search_call') {
|
|
1793
|
-
pushToolSearchCall(event.item);
|
|
1794
|
-
}
|
|
1795
|
-
if (event.item?.type === 'custom_tool_call') {
|
|
1796
|
-
pushCustomToolCall(event.item);
|
|
1797
|
-
}
|
|
1798
|
-
break;
|
|
1799
|
-
case 'response.completed': {
|
|
1800
|
-
const completedServiceTier = event.response?.service_tier || event.response?.serviceTier || '';
|
|
1801
|
-
if (completedServiceTier) responseServiceTier = String(completedServiceTier);
|
|
1802
|
-
if (event.response?.usage) {
|
|
1803
|
-
const u = event.response.usage;
|
|
1804
|
-
const rawUsage = responseServiceTier
|
|
1805
|
-
? { ...u, service_tier: responseServiceTier }
|
|
1806
|
-
: u;
|
|
1807
|
-
usage = {
|
|
1808
|
-
inputTokens: u.input_tokens || 0,
|
|
1809
|
-
outputTokens: u.output_tokens || 0,
|
|
1810
|
-
cachedTokens: extractCachedTokens(u),
|
|
1811
|
-
// openai-oauth reports input_tokens as the total
|
|
1812
|
-
// prompt volume (cached portion is a subset, not
|
|
1813
|
-
// additive). Alias into the cross-provider
|
|
1814
|
-
// `promptTokens` field so downstream loggers have
|
|
1815
|
-
// uniform semantics.
|
|
1816
|
-
promptTokens: u.input_tokens || 0,
|
|
1817
|
-
raw: rawUsage,
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
|
-
if (!model && event.response?.model) model = event.response.model;
|
|
1821
|
-
if (!responseId && event.response?.id) responseId = event.response.id;
|
|
1822
|
-
if (event.response?.output) {
|
|
1823
|
-
for (const item of event.response.output) {
|
|
1824
|
-
pushResponseItem(item);
|
|
1825
|
-
if (!content && item.type === 'message') {
|
|
1826
|
-
for (const c of item.content || []) {
|
|
1827
|
-
if (c.type === 'output_text') {
|
|
1828
|
-
// Completed-output fallback (no streamed
|
|
1829
|
-
// text). Route through the leak guard so
|
|
1830
|
-
// a tool call leaked only in the final
|
|
1831
|
-
// bundle is recovered, not surfaced as
|
|
1832
|
-
// visible content. final=true → full flush.
|
|
1833
|
-
if (leakGuard.enabled) {
|
|
1834
|
-
const { text, calls } = leakGuard.push(c.text || '', true);
|
|
1835
|
-
content += text;
|
|
1836
|
-
for (const lc of calls) dispatchLeakedCall(lc);
|
|
1837
|
-
} else {
|
|
1838
|
-
content += c.text || '';
|
|
1839
|
-
}
|
|
1840
|
-
pushOutputTextAnnotations(c);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
if (item.type === 'message') {
|
|
1845
|
-
for (const c of item.content || []) {
|
|
1846
|
-
if (c.type === 'output_text') pushOutputTextAnnotations(c);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
if (item.type === 'web_search_call') pushWebSearchCall(item);
|
|
1850
|
-
if (item.type === 'tool_search_call') pushToolSearchCall(item);
|
|
1851
|
-
if (item.type === 'custom_tool_call') pushCustomToolCall(item);
|
|
1852
|
-
// Salvage path: some streams emit reasoning only
|
|
1853
|
-
// inside the final response.completed.output
|
|
1854
|
-
// bundle (no per-item .done event). Dedup by id.
|
|
1855
|
-
if (item.type === 'reasoning'
|
|
1856
|
-
&& !reasoningItems.some(r => r.id === item.id)) {
|
|
1857
|
-
pushReasoningItem(item);
|
|
1858
|
-
}
|
|
1859
|
-
// Salvage path for function_call: when
|
|
1860
|
-
// arguments.done fired before (or without) a
|
|
1861
|
-
// matching output_item.added, the deferred tool
|
|
1862
|
-
// call placeholder has empty id/name. The
|
|
1863
|
-
// completed.output bundle carries the canonical
|
|
1864
|
-
// call_id/name; fill them in and emit onToolCall.
|
|
1865
|
-
if (item.type === 'function_call') {
|
|
1866
|
-
const tc = toolCalls.find(
|
|
1867
|
-
(t) => t._deferred && t._pendingItemId === (item.id || ''),
|
|
1868
|
-
);
|
|
1869
|
-
if (tc) {
|
|
1870
|
-
if (!tc.id && item.call_id) tc.id = item.call_id;
|
|
1871
|
-
if (!tc.name && item.name) tc.name = item.name;
|
|
1872
|
-
if (tc.id && tc.name) {
|
|
1873
|
-
delete tc._deferred;
|
|
1874
|
-
delete tc._pendingItemId;
|
|
1875
|
-
emitToolCallDedupe(tc);
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
// Salvage validation. Any deferred call still missing
|
|
1882
|
-
// id/name would propagate to the next turn as a
|
|
1883
|
-
// function_call_output the server can't anchor. Fail the
|
|
1884
|
-
// stream now so the caller sees a deterministic error
|
|
1885
|
-
// instead of a cryptic mismatch one turn later.
|
|
1886
|
-
const unresolved = toolCalls.find((t) => t._deferred);
|
|
1887
|
-
if (unresolved) {
|
|
1888
|
-
terminalError = new Error(
|
|
1889
|
-
`${errLabel} function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`,
|
|
1890
|
-
);
|
|
1891
|
-
finish();
|
|
1892
|
-
break;
|
|
1893
|
-
}
|
|
1894
|
-
midState.sawCompleted = true;
|
|
1895
|
-
done = true;
|
|
1896
|
-
finish();
|
|
1897
|
-
break;
|
|
1898
|
-
}
|
|
1899
|
-
case 'response.done': {
|
|
1900
|
-
// response.done is the terminal frame for some openai-oauth
|
|
1901
|
-
// streams that never emit a separate response.completed.
|
|
1902
|
-
// Route through the same completed/failed/incomplete
|
|
1903
|
-
// normalization based on event.response.status so a
|
|
1904
|
-
// server-side abort (incomplete / failed) does not slip
|
|
1905
|
-
// through as success. status === 'completed' falls
|
|
1906
|
-
// through to the success path with sawCompleted set;
|
|
1907
|
-
// anything else is converted into a terminal error.
|
|
1908
|
-
const status = event.response?.status || '';
|
|
1909
|
-
if (status === 'failed') {
|
|
1910
|
-
midState.responseFailedPayload = event;
|
|
1911
|
-
const msg = event.response?.error?.message
|
|
1912
|
-
|| event.error?.message
|
|
1913
|
-
|| event.message
|
|
1914
|
-
|| 'response.done failed';
|
|
1915
|
-
terminalError = Object.assign(
|
|
1916
|
-
new Error(`${errLabel} response.done failed: ${msg}`),
|
|
1917
|
-
{ responseFailed: event },
|
|
1918
|
-
);
|
|
1919
|
-
populateHttpStatusFromMessage(terminalError, msg);
|
|
1920
|
-
done = true;
|
|
1921
|
-
finish();
|
|
1922
|
-
break;
|
|
1923
|
-
}
|
|
1924
|
-
if (status === 'incomplete') {
|
|
1925
|
-
const reasonStr = _incompleteReasonFromEvent(event);
|
|
1926
|
-
if (_isMaxOutputIncompleteReason(reasonStr)) {
|
|
1927
|
-
incompleteReason = reasonStr;
|
|
1928
|
-
stopReason = 'length';
|
|
1929
|
-
midState.sawCompleted = true;
|
|
1930
|
-
done = true;
|
|
1931
|
-
finish();
|
|
1932
|
-
break;
|
|
1933
|
-
}
|
|
1934
|
-
terminalError = Object.assign(
|
|
1935
|
-
new Error(`${errLabel} response.done incomplete: ${reasonStr}`),
|
|
1936
|
-
{ responseIncomplete: event, incompleteReason: reasonStr },
|
|
1937
|
-
);
|
|
1938
|
-
done = true;
|
|
1939
|
-
finish();
|
|
1940
|
-
break;
|
|
1941
|
-
}
|
|
1942
|
-
if (status && status !== 'completed') {
|
|
1943
|
-
terminalError = Object.assign(
|
|
1944
|
-
new Error(`${errLabel} response.done unexpected status: ${status}`),
|
|
1945
|
-
{ responseDoneStatus: status },
|
|
1946
|
-
);
|
|
1947
|
-
done = true;
|
|
1948
|
-
finish();
|
|
1949
|
-
break;
|
|
1950
|
-
}
|
|
1951
|
-
midState.sawCompleted = true;
|
|
1952
|
-
done = true;
|
|
1953
|
-
finish();
|
|
1954
|
-
break;
|
|
1955
|
-
}
|
|
1956
|
-
case 'response.incomplete': {
|
|
1957
|
-
// Most incomplete reasons are real failures. max_output_tokens
|
|
1958
|
-
// maps cleanly to Anthropic's stop_reason=max_tokens; treating
|
|
1959
|
-
// it as an error makes Claude retry the same over-budget turn.
|
|
1960
|
-
const reasonStr = _incompleteReasonFromEvent(event);
|
|
1961
|
-
if (_isMaxOutputIncompleteReason(reasonStr)) {
|
|
1962
|
-
incompleteReason = reasonStr;
|
|
1963
|
-
stopReason = 'length';
|
|
1964
|
-
midState.sawCompleted = true;
|
|
1965
|
-
done = true;
|
|
1966
|
-
finish();
|
|
1967
|
-
break;
|
|
1968
|
-
}
|
|
1969
|
-
terminalError = Object.assign(
|
|
1970
|
-
new Error(`${errLabel} response.incomplete: ${reasonStr}`),
|
|
1971
|
-
{ responseIncomplete: event, incompleteReason: reasonStr },
|
|
1972
|
-
);
|
|
1973
|
-
finish();
|
|
1974
|
-
break;
|
|
1975
|
-
}
|
|
1976
|
-
case 'response.failed': {
|
|
1977
|
-
// Stash the payload so the mid-stream classifier can sniff
|
|
1978
|
-
// network_error / stream_disconnected without re-parsing.
|
|
1979
|
-
midState.responseFailedPayload = event;
|
|
1980
|
-
const msg = event.response?.error?.message
|
|
1981
|
-
|| event.error?.message
|
|
1982
|
-
|| event.message
|
|
1983
|
-
|| 'response.failed';
|
|
1984
|
-
terminalError = Object.assign(new Error(`${errLabel} response.failed: ${msg}`), {
|
|
1985
|
-
responseFailed: event,
|
|
1986
|
-
});
|
|
1987
|
-
// Sniff the server message for transient/auth/permanent
|
|
1988
|
-
// hints so the handshake / mid-stream retry classifiers
|
|
1989
|
-
// can route by httpStatus. Without this, server-side
|
|
1990
|
-
// events like "Our servers are currently overloaded"
|
|
1991
|
-
// surfaced as unclassified errors and skipped the
|
|
1992
|
-
// 5xx retry bucket entirely.
|
|
1993
|
-
populateHttpStatusFromMessage(terminalError, msg);
|
|
1994
|
-
finish();
|
|
1995
|
-
break;
|
|
1996
|
-
}
|
|
1997
|
-
case 'error': {
|
|
1998
|
-
const errMsg = String(event.message || event.error?.message || 'unknown');
|
|
1999
|
-
terminalError = new Error(`${errLabel} error: ${errMsg}`);
|
|
2000
|
-
populateHttpStatusFromMessage(terminalError, errMsg);
|
|
2001
|
-
finish();
|
|
2002
|
-
break;
|
|
2003
|
-
}
|
|
2004
|
-
default:
|
|
2005
|
-
// Catch any other reasoning-delta variants (e.g.
|
|
2006
|
-
// `response.reasoning.<sub>.delta`) so they are counted
|
|
2007
|
-
// and suppressed, never reaching the user content buffer.
|
|
2008
|
-
if (typeof event.type === 'string'
|
|
2009
|
-
&& event.type.startsWith('response.reasoning')
|
|
2010
|
-
&& event.type.endsWith('.delta')) {
|
|
2011
|
-
reasoningOtherDeltaCount += 1;
|
|
2012
|
-
// These ARE live model progress (reviewer Medium): a
|
|
2013
|
-
// provider that emits only these reasoning variants for a
|
|
2014
|
-
// long span would otherwise trip the SEMANTIC idle abort.
|
|
2015
|
-
// Refresh both the watchdog and the semantic idle timer,
|
|
2016
|
-
// matching the named reasoning_text.delta case above.
|
|
2017
|
-
try { onStreamDelta?.(); } catch {}
|
|
2018
|
-
bumpSemanticIdle();
|
|
2019
|
-
}
|
|
2020
|
-
// Trace-only events (response.in_progress, etc.)
|
|
2021
|
-
break;
|
|
2022
|
-
}
|
|
2023
|
-
};
|
|
2024
|
-
closeHandler = (code, reason) => {
|
|
2025
|
-
if (done) return;
|
|
2026
|
-
midState.wsCloseCode = code;
|
|
2027
|
-
if (!terminalError) {
|
|
2028
|
-
const r = reason?.toString?.('utf-8') || '';
|
|
2029
|
-
const httpStatus = _httpStatusFromWsClose(code, r);
|
|
2030
|
-
terminalError = Object.assign(
|
|
2031
|
-
new Error(`OpenAI OAuth WS closed before response.completed (code=${code}${r ? `, reason=${r}` : ''})`),
|
|
2032
|
-
{ wsCloseCode: code, wsCloseReason: r, ...(httpStatus ? { httpStatus } : {}) },
|
|
2033
|
-
);
|
|
2034
|
-
} else if (terminalError && !terminalError.wsCloseCode) {
|
|
2035
|
-
try { terminalError.wsCloseCode = code; } catch {}
|
|
2036
|
-
try { terminalError.httpStatus = terminalError.httpStatus || _httpStatusFromWsClose(code, reason?.toString?.('utf-8') || ''); } catch {}
|
|
2037
|
-
}
|
|
2038
|
-
finish();
|
|
2039
|
-
};
|
|
2040
|
-
errorHandler = (err) => {
|
|
2041
|
-
if (done) return;
|
|
2042
|
-
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
2043
|
-
if (terminalError) {
|
|
2044
|
-
// Preserve the first terminalError; chain the later socket
|
|
2045
|
-
// error in via `cause` (or `suppressed` if cause already set)
|
|
2046
|
-
// so diagnostics keep the original failure visible.
|
|
2047
|
-
try {
|
|
2048
|
-
if (!terminalError.cause) terminalError.cause = wrapped;
|
|
2049
|
-
else {
|
|
2050
|
-
const list = Array.isArray(terminalError.suppressed)
|
|
2051
|
-
? terminalError.suppressed
|
|
2052
|
-
: [];
|
|
2053
|
-
list.push(wrapped);
|
|
2054
|
-
terminalError.suppressed = list;
|
|
2055
|
-
}
|
|
2056
|
-
} catch {}
|
|
2057
|
-
} else {
|
|
2058
|
-
terminalError = wrapped;
|
|
2059
|
-
}
|
|
2060
|
-
try { socket.close(4001, 'stream_error'); } catch {}
|
|
2061
|
-
finish();
|
|
2062
|
-
};
|
|
2063
|
-
if (externalSignal) {
|
|
2064
|
-
abortHandler = () => {
|
|
2065
|
-
if (done) return;
|
|
2066
|
-
const reason = externalSignal.reason;
|
|
2067
|
-
terminalError = reason instanceof Error ? reason : new Error('OpenAI OAuth WS aborted by session close');
|
|
2068
|
-
// Tag: was this a user/caller abort, or a watchdog abort?
|
|
2069
|
-
// Mid-stream retry must skip user aborts but may retry watchdog
|
|
2070
|
-
// aborts. The caller-owned AbortController surfaces through
|
|
2071
|
-
// externalSignal; the agent stall watchdog signals via a reason
|
|
2072
|
-
// object whose name === 'AgentStallAbortError'. stream-watchdog
|
|
2073
|
-
// uses StreamStalledAbortError. Anything else → treat as user.
|
|
2074
|
-
const reasonName = reason?.name || '';
|
|
2075
|
-
if (reasonName === 'AgentStallAbortError'
|
|
2076
|
-
|| reasonName === 'StreamStalledAbortError') {
|
|
2077
|
-
midState.watchdogAbort = reasonName;
|
|
2078
|
-
} else {
|
|
2079
|
-
midState.userAbort = true;
|
|
2080
|
-
}
|
|
2081
|
-
try { socket.close(4002, 'aborted'); } catch {}
|
|
2082
|
-
finish();
|
|
2083
|
-
};
|
|
2084
|
-
if (externalSignal.aborted) { abortHandler(); return; }
|
|
2085
|
-
externalSignal.addEventListener('abort', abortHandler, { once: true });
|
|
2086
|
-
}
|
|
2087
|
-
socket.on('message', messageHandler);
|
|
2088
|
-
socket.on('close', closeHandler);
|
|
2089
|
-
socket.on('error', errorHandler);
|
|
2090
|
-
armPreStreamWatchdog();
|
|
2091
|
-
// Periodic client-side WS ping while the stream is active. The server's
|
|
2092
|
-
// server closes with 1011 "keepalive ping timeout" when it thinks the
|
|
2093
|
-
// peer is silent during long reasoning windows where no data frames
|
|
2094
|
-
// flow. Sending a ping every 10s from our side keeps the socket warm.
|
|
2095
|
-
// The interval is unref'd so it never holds the event loop open, and
|
|
2096
|
-
// cleanup() clears it on every terminal path (completed / close /
|
|
2097
|
-
// error / abort / mid-stream retry teardown).
|
|
2098
|
-
keepaliveTimer = setInterval(() => {
|
|
2099
|
-
try {
|
|
2100
|
-
if (socket.readyState !== WebSocket.OPEN) return;
|
|
2101
|
-
socket.ping();
|
|
2102
|
-
} catch {}
|
|
2103
|
-
}, 10_000);
|
|
2104
|
-
try { keepaliveTimer.unref?.(); } catch {}
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
108
|
+
// --- WS pool/handshake/acquire/release: extracted to openai-ws-pool.mjs ---
|
|
2107
109
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
*
|
|
2111
|
-
* Default-deny: anything we don't recognize as transient returns null (treat
|
|
2112
|
-
* as permanent). Permanent buckets (401/403/404/429) also return null — the
|
|
2113
|
-
* server has made a deterministic decision that a retry can't change.
|
|
2114
|
-
*
|
|
2115
|
-
* Returns one of:
|
|
2116
|
-
* 'timeout' — `ws` handshakeTimeout fired
|
|
2117
|
-
* 'reset' — ECONNRESET / socket hang up
|
|
2118
|
-
* 'dns' — EAI_AGAIN / ENOTFOUND / EAI_NODATA
|
|
2119
|
-
* 'refused' — ECONNREFUSED
|
|
2120
|
-
* 'network' — ENETUNREACH / EHOSTUNREACH / EPIPE
|
|
2121
|
-
* 'acquire_timeout' — hard client-side open/acquire deadline fired
|
|
2122
|
-
* 'http_5xx' (with specific status e.g. 'http_503') — server overload
|
|
2123
|
-
* null — not retryable
|
|
2124
|
-
*/
|
|
2125
|
-
// Thin re-export wrapper: handshake classification now lives in the shared
|
|
2126
|
-
// retry-classifier (classifyHandshakeError). Kept here as a named export so
|
|
2127
|
-
// internal call sites (_acquireWithRetry) and any external importer keep
|
|
2128
|
-
// resolving the same symbol.
|
|
110
|
+
// --- Delta/matching helpers + parseToolSearchArgs: extracted to openai-ws-stream.mjs ---
|
|
111
|
+
// --- Stream consumer (_streamResponse): extracted to openai-ws-stream.mjs ---
|
|
2129
112
|
export function _classifyHandshakeError(err) {
|
|
2130
113
|
return classifyHandshakeError(err);
|
|
2131
114
|
}
|
|
@@ -2204,6 +187,182 @@ function _sleepWithAbort(ms, externalSignal, sleepFn = _defaultSleep) {
|
|
|
2204
187
|
return sleepWithAbort(ms, externalSignal, sleepFn, 'OpenAI OAuth WS retry backoff aborted');
|
|
2205
188
|
}
|
|
2206
189
|
|
|
190
|
+
function _num(value, fallback = 0) {
|
|
191
|
+
const n = Number(value);
|
|
192
|
+
return Number.isFinite(n) ? n : fallback;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _envPositiveInt(name, fallback) {
|
|
196
|
+
const n = Number(process.env[name]);
|
|
197
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _envRatio(name, fallback) {
|
|
201
|
+
const n = Number(process.env[name]);
|
|
202
|
+
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _cleanMetaString(value) {
|
|
206
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _hashText(value, chars = 24) {
|
|
210
|
+
return createHash('sha256').update(String(value || '')).digest('hex').slice(0, chars);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function _sessionStartedAtUnixMs(sessionId) {
|
|
214
|
+
const parts = String(sessionId || '').split('_');
|
|
215
|
+
for (const part of parts) {
|
|
216
|
+
if (/^\d{12,}$/.test(part)) {
|
|
217
|
+
const n = Number(part);
|
|
218
|
+
if (Number.isFinite(n) && n > 0) return Math.floor(n);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return Date.now();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function _codexRequestKind(sendOpts, sessionId) {
|
|
225
|
+
const explicit = _cleanMetaString(sendOpts?.requestKind || sendOpts?.codexRequestKind);
|
|
226
|
+
if (explicit) return explicit;
|
|
227
|
+
return String(sessionId || '').includes(':compact') ? 'compaction' : 'turn';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _codexInstallationId(sendOpts) {
|
|
231
|
+
return _cleanMetaString(sendOpts?.installationId || sendOpts?.codexInstallationId || process.env.MIXDOG_CODEX_INSTALLATION_ID)
|
|
232
|
+
|| `mixdog-${_hashText(`${process.env.USERPROFILE || process.env.HOME || ''}:${process.cwd()}`, 32)}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts } = {}) {
|
|
236
|
+
const sessionId = _cleanMetaString(sendOpts?.codexSessionId || sendOpts?.session?.codexSessionId || poolKey || cacheKey)
|
|
237
|
+
|| 'mixdog-session';
|
|
238
|
+
const threadId = _cleanMetaString(sendOpts?.threadId || sendOpts?.codexThreadId || sendOpts?.session?.threadId || cacheKey || sessionId)
|
|
239
|
+
|| sessionId;
|
|
240
|
+
const turnId = _cleanMetaString(sendOpts?.turnId || sendOpts?.codexTurnId || sendOpts?.session?.turnId || sessionId)
|
|
241
|
+
|| sessionId;
|
|
242
|
+
const windowId = _cleanMetaString(sendOpts?.windowId || sendOpts?.codexWindowId || sendOpts?.session?.windowId)
|
|
243
|
+
|| `${threadId}:1`;
|
|
244
|
+
const installationId = _codexInstallationId(sendOpts);
|
|
245
|
+
const startedAt = Number.isFinite(Number(sendOpts?.turnStartedAtUnixMs))
|
|
246
|
+
? Math.floor(Number(sendOpts.turnStartedAtUnixMs))
|
|
247
|
+
: _sessionStartedAtUnixMs(sessionId);
|
|
248
|
+
const requestKind = _codexRequestKind(sendOpts, sessionId);
|
|
249
|
+
const turnMetadata = {
|
|
250
|
+
installation_id: installationId,
|
|
251
|
+
session_id: sessionId,
|
|
252
|
+
thread_id: threadId,
|
|
253
|
+
turn_id: turnId,
|
|
254
|
+
window_id: windowId,
|
|
255
|
+
request_kind: requestKind,
|
|
256
|
+
turn_started_at_unix_ms: startedAt,
|
|
257
|
+
};
|
|
258
|
+
const metadata = {
|
|
259
|
+
'x-codex-installation-id': installationId,
|
|
260
|
+
session_id: sessionId,
|
|
261
|
+
thread_id: threadId,
|
|
262
|
+
turn_id: turnId,
|
|
263
|
+
'x-codex-window-id': windowId,
|
|
264
|
+
'x-codex-turn-metadata': JSON.stringify(turnMetadata),
|
|
265
|
+
};
|
|
266
|
+
// NOTE: no entry-level caching — pooled sockets outlive turns, and codex
|
|
267
|
+
// rebuilds metadata per request (responses_metadata.rs client_metadata()).
|
|
268
|
+
// Caching the first turn's turn_id on the socket would replay stale turn
|
|
269
|
+
// identity for every later turn on that connection.
|
|
270
|
+
return metadata;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _metadataTrace(metadata) {
|
|
274
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
275
|
+
return { count: 0, hash: null, hasTurnMetadata: false, hasThreadId: false };
|
|
276
|
+
}
|
|
277
|
+
const keys = Object.keys(metadata).sort();
|
|
278
|
+
return {
|
|
279
|
+
count: keys.length,
|
|
280
|
+
hash: _hashText(keys.map((key) => `${key}=${metadata[key]}`).join('\n'), 12),
|
|
281
|
+
hasTurnMetadata: typeof metadata['x-codex-turn-metadata'] === 'string' && metadata['x-codex-turn-metadata'].length > 0,
|
|
282
|
+
hasThreadId: typeof metadata.thread_id === 'string' && metadata.thread_id.length > 0,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function _codexWsCompatibilityHeaders(context = {}) {
|
|
287
|
+
const metadata = _codexMetadataBase(null, context);
|
|
288
|
+
const headers = {};
|
|
289
|
+
if (metadata['x-codex-window-id']) headers['x-codex-window-id'] = metadata['x-codex-window-id'];
|
|
290
|
+
if (metadata['x-codex-turn-metadata']) headers['x-codex-turn-metadata'] = metadata['x-codex-turn-metadata'];
|
|
291
|
+
// codex sends x-codex-installation-id on EVERY request incl. the WS
|
|
292
|
+
// handshake (client.rs:582-584). We previously carried it only in the
|
|
293
|
+
// body client_metadata; the server may gate x-codex-turn-state issuance
|
|
294
|
+
// on handshake-level client identity, so mirror codex here.
|
|
295
|
+
if (metadata['x-codex-installation-id']) headers['x-codex-installation-id'] = metadata['x-codex-installation-id'];
|
|
296
|
+
return headers;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function _withCodexWsClientMetadata(frame, entry, enabled, context = {}) {
|
|
300
|
+
if (!enabled || !frame || typeof frame !== 'object') return frame;
|
|
301
|
+
const base = _codexMetadataBase(entry, context);
|
|
302
|
+
const metadata = {
|
|
303
|
+
...base,
|
|
304
|
+
...(frame.client_metadata && typeof frame.client_metadata === 'object' ? frame.client_metadata : {}),
|
|
305
|
+
'x-codex-ws-stream-request-start-ms': String(Date.now()),
|
|
306
|
+
};
|
|
307
|
+
if (entry && typeof entry === 'object') {
|
|
308
|
+
// codex scopes x-codex-turn-state to ONE turn (client.rs:263-279 —
|
|
309
|
+
// "must not send it between different turns"). Pooled sockets span
|
|
310
|
+
// turns, so key the stored token by the turn that captured it and
|
|
311
|
+
// drop it when the turn changes.
|
|
312
|
+
if (entry.turnState && entry.turnStateTurnId && entry.turnStateTurnId !== base.turn_id) {
|
|
313
|
+
entry.turnState = null;
|
|
314
|
+
entry.turnStateTurnId = null;
|
|
315
|
+
}
|
|
316
|
+
entry.currentTurnId = base.turn_id;
|
|
317
|
+
}
|
|
318
|
+
if (entry?.turnState) {
|
|
319
|
+
metadata['x-codex-turn-state'] = String(entry.turnState);
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
...frame,
|
|
323
|
+
client_metadata: metadata,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function _cacheObservation({ entry, result }) {
|
|
328
|
+
const inputTokens = _num(result?.usage?.inputTokens, 0);
|
|
329
|
+
const promptTokens = _num(result?.usage?.promptTokens, 0) || inputTokens;
|
|
330
|
+
const cachedTokens = _num(result?.usage?.cachedTokens, 0);
|
|
331
|
+
const previousMaxCached = _num(entry?.promptCacheMaxCachedTokens, 0);
|
|
332
|
+
const warmThreshold = _envPositiveInt('MIXDOG_OAI_CACHE_MISS_WARM_TOKENS', 2048);
|
|
333
|
+
const promptThreshold = _envPositiveInt('MIXDOG_OAI_CACHE_MISS_PROMPT_TOKENS', 4096);
|
|
334
|
+
const dropRatio = _envRatio('MIXDOG_OAI_CACHE_MISS_DROP_RATIO', 0.6);
|
|
335
|
+
const dropThreshold = Math.floor(previousMaxCached * dropRatio);
|
|
336
|
+
const wasWarm = previousMaxCached >= warmThreshold;
|
|
337
|
+
const cacheRatio = promptTokens > 0 ? cachedTokens / promptTokens : null;
|
|
338
|
+
const zeroMiss = wasWarm && promptTokens >= promptThreshold && cachedTokens === 0;
|
|
339
|
+
const partialDrop = wasWarm
|
|
340
|
+
&& promptTokens >= promptThreshold
|
|
341
|
+
&& cachedTokens > 0
|
|
342
|
+
&& previousMaxCached > 0
|
|
343
|
+
&& cachedTokens < dropThreshold;
|
|
344
|
+
const actualMiss = zeroMiss || partialDrop;
|
|
345
|
+
return {
|
|
346
|
+
inputTokens,
|
|
347
|
+
promptTokens,
|
|
348
|
+
cachedTokens,
|
|
349
|
+
uncachedTokens: Math.max(0, promptTokens - cachedTokens),
|
|
350
|
+
previousMaxCached,
|
|
351
|
+
wasWarm,
|
|
352
|
+
warmThreshold,
|
|
353
|
+
promptThreshold,
|
|
354
|
+
dropRatio,
|
|
355
|
+
dropThreshold,
|
|
356
|
+
cacheRatio,
|
|
357
|
+
actualMiss,
|
|
358
|
+
missReason: zeroMiss
|
|
359
|
+
? 'warm_session_zero_cached_tokens'
|
|
360
|
+
: partialDrop
|
|
361
|
+
? 'warm_session_cached_tokens_dropped'
|
|
362
|
+
: null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
2207
366
|
/**
|
|
2208
367
|
* Run `_acquire({auth, poolKey, cacheKey})` with bounded exponential-backoff
|
|
2209
368
|
* retry on transient handshake failures. The injection seams (`_acquire`,
|
|
@@ -2218,6 +377,7 @@ export async function _acquireWithRetry({
|
|
|
2218
377
|
auth,
|
|
2219
378
|
poolKey,
|
|
2220
379
|
cacheKey,
|
|
380
|
+
codexHeaders,
|
|
2221
381
|
forceFresh,
|
|
2222
382
|
onRetry,
|
|
2223
383
|
externalSignal,
|
|
@@ -2237,7 +397,7 @@ export async function _acquireWithRetry({
|
|
|
2237
397
|
process.stderr.write(`[agent-trace] ws-handshake-attempt n=${attempt}\n`);
|
|
2238
398
|
}
|
|
2239
399
|
}
|
|
2240
|
-
return await _acquire({ auth, poolKey, cacheKey, forceFresh, externalSignal });
|
|
400
|
+
return await _acquire({ auth, poolKey, cacheKey, codexHeaders, forceFresh, externalSignal });
|
|
2241
401
|
} catch (err) {
|
|
2242
402
|
lastErr = err;
|
|
2243
403
|
const classifier = _classifyHandshakeError(err);
|
|
@@ -2388,17 +548,12 @@ export async function sendViaWebSocket({
|
|
|
2388
548
|
// this carry-forward would not help and is therefore gated to xAI.
|
|
2389
549
|
let carryForwardCache = null;
|
|
2390
550
|
const emittedProgress = [];
|
|
551
|
+
const useCodexWsClientMetadata = traceProvider === 'openai-oauth';
|
|
552
|
+
const codexMetadataContext = { poolKey, cacheKey, sendOpts };
|
|
553
|
+
const codexHandshakeHeaders = useCodexWsClientMetadata
|
|
554
|
+
? _codexWsCompatibilityHeaders(codexMetadataContext)
|
|
555
|
+
: null;
|
|
2391
556
|
|
|
2392
|
-
return await _withOpenAiPromptCacheLane({
|
|
2393
|
-
auth,
|
|
2394
|
-
cacheKey,
|
|
2395
|
-
sendOpts,
|
|
2396
|
-
poolKey,
|
|
2397
|
-
iteration,
|
|
2398
|
-
traceProvider,
|
|
2399
|
-
useModel,
|
|
2400
|
-
externalSignal,
|
|
2401
|
-
}, async (promptCacheLane) => {
|
|
2402
557
|
for (let attemptIndex = 0; attemptIndex <= MAX_MIDSTREAM_RETRIES; attemptIndex++) {
|
|
2403
558
|
const handshakeStart = Date.now();
|
|
2404
559
|
let acquired;
|
|
@@ -2410,6 +565,7 @@ export async function sendViaWebSocket({
|
|
|
2410
565
|
auth,
|
|
2411
566
|
poolKey,
|
|
2412
567
|
cacheKey,
|
|
568
|
+
codexHeaders: codexHandshakeHeaders,
|
|
2413
569
|
// Retry attempt must not reuse a pooled socket — the prior
|
|
2414
570
|
// one is either torn down or in an unknown state.
|
|
2415
571
|
forceFresh: forceFresh || attemptIndex > 0,
|
|
@@ -2502,18 +658,23 @@ export async function sendViaWebSocket({
|
|
|
2502
658
|
let deltaReason = null;
|
|
2503
659
|
let strippedResponseItems = 0;
|
|
2504
660
|
let skippedResponseItems = 0;
|
|
661
|
+
let wireFrameHadTurnState = false;
|
|
662
|
+
let wireFrameMetadataTrace = _metadataTrace(null);
|
|
663
|
+
let framePrefixHash = null;
|
|
664
|
+
let framePrefixHeadHash = null;
|
|
665
|
+
let framePrefixPrevMatch = null;
|
|
2505
666
|
let result;
|
|
2506
667
|
const streamTimeouts = null;
|
|
2507
668
|
try {
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
deltaTokens: _estimateFrameTokens({ type: 'response.create', ...warmupBody }),
|
|
2513
|
-
hasPreviousResponseId: false,
|
|
2514
|
-
});
|
|
669
|
+
// codex prewarm gate (client.rs:1686-1688): only when the session
|
|
670
|
+
// has no prior request state. A reused pooled socket with a live
|
|
671
|
+
// chain must go straight to the real request.
|
|
672
|
+
if (warmupBody && typeof warmupBody === 'object' && attemptIndex === 0 && !entry.lastResponseId) {
|
|
2515
673
|
const warmupFrame = { type: 'response.create', ...warmupBody };
|
|
2516
|
-
|
|
674
|
+
const wireWarmupFrame = _withCodexWsClientMetadata(warmupFrame, entry, useCodexWsClientMetadata, codexMetadataContext);
|
|
675
|
+
wireFrameHadTurnState = !!wireWarmupFrame?.client_metadata?.['x-codex-turn-state'];
|
|
676
|
+
wireFrameMetadataTrace = _metadataTrace(wireWarmupFrame?.client_metadata);
|
|
677
|
+
await _sendFrameFn(entry, wireWarmupFrame);
|
|
2517
678
|
const warmupStart = Date.now();
|
|
2518
679
|
const warmupState = {
|
|
2519
680
|
attemptIndex,
|
|
@@ -2573,13 +734,15 @@ export async function sendViaWebSocket({
|
|
|
2573
734
|
payload: warmupPayload,
|
|
2574
735
|
});
|
|
2575
736
|
} catch {}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
737
|
+
// Do NOT rewrite the main request after warmup (R23 finding).
|
|
738
|
+
// The old prev_id+no-instructions rewrite made it=1's frame a
|
|
739
|
+
// different shape from it=2+ full frames, so the first real
|
|
740
|
+
// full-frame cache write only landed at it=2 and it=3 raced
|
|
741
|
+
// its propagation (cached=0 early misses). Keeping it=1 as a
|
|
742
|
+
// normal full frame makes it byte-identical to the warmup's
|
|
743
|
+
// prefix (instant full hit on the cache warmup just wrote)
|
|
744
|
+
// and keeps every subsequent frame one consistent shape.
|
|
745
|
+
// Delta opt-in still chains via entry.lastResponseId above.
|
|
2583
746
|
}
|
|
2584
747
|
|
|
2585
748
|
const delta = _computeDelta({ entry, body: requestBody });
|
|
@@ -2587,13 +750,34 @@ export async function sendViaWebSocket({
|
|
|
2587
750
|
deltaReason = delta.reason || null;
|
|
2588
751
|
strippedResponseItems = delta.strippedResponseItems || 0;
|
|
2589
752
|
skippedResponseItems = delta.skippedResponseItems || 0;
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
})
|
|
753
|
+
const wireFrame = _withCodexWsClientMetadata(frame, entry, useCodexWsClientMetadata, codexMetadataContext);
|
|
754
|
+
wireFrameHadTurnState = !!wireFrame?.client_metadata?.['x-codex-turn-state'];
|
|
755
|
+
wireFrameMetadataTrace = _metadataTrace(wireFrame?.client_metadata);
|
|
756
|
+
deltaTokens = _estimateFrameTokens(wireFrame);
|
|
757
|
+
// Prefix-consistency probe (item-level). Serialized-JSON byte
|
|
758
|
+
// prefixes can never match across appends (the shorter frame ends
|
|
759
|
+
// in "]}" where the longer has ","), so compare what the server's
|
|
760
|
+
// prefix cache actually sees: the non-input request header and the
|
|
761
|
+
// per-item content of the input array. prev_match=true means the
|
|
762
|
+
// current call's header is identical and its first N input items
|
|
763
|
+
// equal the previous call's N items (append-only history).
|
|
764
|
+
try {
|
|
765
|
+
const { client_metadata: _cm, input: frameInput, ...frameHeader } = frame;
|
|
766
|
+
const headerHash = _hashText(JSON.stringify(frameHeader), 16);
|
|
767
|
+
const itemHashes = (Array.isArray(frameInput) ? frameInput : [])
|
|
768
|
+
.map((item) => _hashText(JSON.stringify(item), 12));
|
|
769
|
+
framePrefixHash = headerHash;
|
|
770
|
+
framePrefixHeadHash = _hashText(itemHashes.join(','), 16);
|
|
771
|
+
const prevHeader = entry.lastFrameHeaderHash;
|
|
772
|
+
const prevItems = entry.lastFrameItemHashes;
|
|
773
|
+
if (prevHeader && Array.isArray(prevItems)) {
|
|
774
|
+
framePrefixPrevMatch = headerHash === prevHeader
|
|
775
|
+
&& itemHashes.length >= prevItems.length
|
|
776
|
+
&& prevItems.every((h, i) => itemHashes[i] === h);
|
|
777
|
+
}
|
|
778
|
+
entry.lastFrameHeaderHash = headerHash;
|
|
779
|
+
entry.lastFrameItemHashes = itemHashes;
|
|
780
|
+
} catch {}
|
|
2597
781
|
|
|
2598
782
|
// Re-check abort after acquire/warmup — narrow window where
|
|
2599
783
|
// externalSignal could fire between successful acquire and
|
|
@@ -2606,7 +790,7 @@ export async function sendViaWebSocket({
|
|
|
2606
790
|
const reason = externalSignal.reason;
|
|
2607
791
|
throw reason instanceof Error ? reason : new Error('Aborted');
|
|
2608
792
|
}
|
|
2609
|
-
await _sendFrameFn(entry,
|
|
793
|
+
await _sendFrameFn(entry, wireFrame);
|
|
2610
794
|
|
|
2611
795
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
2612
796
|
process.stderr.write(`[agent-trace] ws-streaming-start sinceAcquire=${Date.now() - handshakeStart}ms\n`);
|
|
@@ -2748,6 +932,11 @@ export async function sendViaWebSocket({
|
|
|
2748
932
|
entry.lastInputPrefixHash = null;
|
|
2749
933
|
}
|
|
2750
934
|
|
|
935
|
+
// Cache observation must see the MAIN request's usage only. Folding
|
|
936
|
+
// warmup usage in first (R18) made prompt_tokens spike on it=1 and
|
|
937
|
+
// then "shrink" on it=2, faking prefix-rewrite/cache-drop signals in
|
|
938
|
+
// every warmup session (debugger 2026-07-03).
|
|
939
|
+
const cacheObservation = _cacheObservation({ entry, result });
|
|
2751
940
|
if (warmupResult?.usage) {
|
|
2752
941
|
result.usage = _combineUsageWithWarmup(result.usage, warmupResult.usage);
|
|
2753
942
|
}
|
|
@@ -2768,11 +957,51 @@ export async function sendViaWebSocket({
|
|
|
2768
957
|
provider: traceProvider,
|
|
2769
958
|
serviceTier: responseServiceTier,
|
|
2770
959
|
});
|
|
960
|
+
const requestHasPreviousResponseId = typeof frame.previous_response_id === 'string' && frame.previous_response_id.length > 0;
|
|
961
|
+
const transportCacheKeyHash = cacheKey
|
|
962
|
+
? createHash('sha256').update(String(cacheKey)).digest('hex').slice(0, 12)
|
|
963
|
+
: null;
|
|
964
|
+
if (cacheObservation.actualMiss) {
|
|
965
|
+
try {
|
|
966
|
+
appendAgentTrace({
|
|
967
|
+
sessionId: poolKey,
|
|
968
|
+
iteration,
|
|
969
|
+
kind: 'cache_miss',
|
|
970
|
+
provider: traceProvider,
|
|
971
|
+
model: liveModel,
|
|
972
|
+
transport: 'websocket',
|
|
973
|
+
payload: {
|
|
974
|
+
provider: traceProvider,
|
|
975
|
+
model: liveModel,
|
|
976
|
+
transport: 'websocket',
|
|
977
|
+
ws_mode: mode,
|
|
978
|
+
reason: cacheObservation.missReason || 'warm_session_cache_miss',
|
|
979
|
+
cached_tokens: cacheObservation.cachedTokens,
|
|
980
|
+
prompt_tokens: cacheObservation.promptTokens,
|
|
981
|
+
input_tokens: cacheObservation.inputTokens,
|
|
982
|
+
uncached_tokens: cacheObservation.uncachedTokens,
|
|
983
|
+
cache_ratio: cacheObservation.cacheRatio,
|
|
984
|
+
previous_max_cached_tokens: cacheObservation.previousMaxCached,
|
|
985
|
+
cache_key_hash: transportCacheKeyHash,
|
|
986
|
+
warm_threshold_tokens: cacheObservation.warmThreshold,
|
|
987
|
+
prompt_threshold_tokens: cacheObservation.promptThreshold,
|
|
988
|
+
drop_ratio: cacheObservation.dropRatio,
|
|
989
|
+
drop_threshold_tokens: cacheObservation.dropThreshold,
|
|
990
|
+
request_has_previous_response_id: requestHasPreviousResponseId,
|
|
991
|
+
chain_delta_reason: mode === 'delta' ? null : deltaReason,
|
|
992
|
+
body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
|
|
993
|
+
frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
|
|
994
|
+
response_id: result.responseId || null,
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
} catch {}
|
|
998
|
+
}
|
|
999
|
+
entry.promptCacheMaxCachedTokens = Math.max(
|
|
1000
|
+
_num(entry.promptCacheMaxCachedTokens, 0),
|
|
1001
|
+
cacheObservation.cachedTokens,
|
|
1002
|
+
);
|
|
2771
1003
|
// Extra WS-specific observability: transport + per-iteration delta bytes.
|
|
2772
1004
|
try {
|
|
2773
|
-
const transportCacheKeyHash = cacheKey
|
|
2774
|
-
? createHash('sha256').update(String(cacheKey)).digest('hex').slice(0, 12)
|
|
2775
|
-
: null;
|
|
2776
1005
|
const transportPayload = {
|
|
2777
1006
|
provider: traceProvider,
|
|
2778
1007
|
transport: 'websocket',
|
|
@@ -2789,22 +1018,26 @@ export async function sendViaWebSocket({
|
|
|
2789
1018
|
midstream_retries: attemptIndex,
|
|
2790
1019
|
response_id: result.responseId || null,
|
|
2791
1020
|
cache_key_hash: transportCacheKeyHash,
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
1021
|
+
request_has_previous_response_id: requestHasPreviousResponseId,
|
|
1022
|
+
cached_tokens: cacheObservation.cachedTokens,
|
|
1023
|
+
prompt_tokens: cacheObservation.promptTokens,
|
|
1024
|
+
input_tokens: cacheObservation.inputTokens,
|
|
1025
|
+
uncached_tokens: cacheObservation.uncachedTokens,
|
|
1026
|
+
cache_ratio: cacheObservation.cacheRatio,
|
|
1027
|
+
actual_cache_miss: cacheObservation.actualMiss,
|
|
1028
|
+
actual_cache_miss_reason: cacheObservation.missReason,
|
|
1029
|
+
previous_max_cached_tokens: cacheObservation.previousMaxCached,
|
|
1030
|
+
cache_drop_threshold_tokens: cacheObservation.dropThreshold,
|
|
1031
|
+
frame_prefix_hash: framePrefixHash,
|
|
1032
|
+
frame_prefix_head_hash: framePrefixHeadHash,
|
|
1033
|
+
frame_prefix_prev_match: framePrefixPrevMatch,
|
|
1034
|
+
ws_client_metadata: useCodexWsClientMetadata,
|
|
1035
|
+
ws_client_metadata_key_count: wireFrameMetadataTrace.count,
|
|
1036
|
+
ws_client_metadata_hash: wireFrameMetadataTrace.hash,
|
|
1037
|
+
ws_client_metadata_has_turn_metadata: wireFrameMetadataTrace.hasTurnMetadata,
|
|
1038
|
+
ws_client_metadata_has_thread_id: wireFrameMetadataTrace.hasThreadId,
|
|
1039
|
+
ws_client_metadata_has_turn_state: wireFrameHadTurnState,
|
|
1040
|
+
ws_entry_turn_state_available: useCodexWsClientMetadata && !!entry.turnState,
|
|
2808
1041
|
chain_delta_reason: mode === 'delta' ? null : deltaReason,
|
|
2809
1042
|
chain_stripped_response_items: strippedResponseItems,
|
|
2810
1043
|
chain_skipped_response_items: skippedResponseItems,
|
|
@@ -2825,7 +1058,10 @@ export async function sendViaWebSocket({
|
|
|
2825
1058
|
...transportPayload,
|
|
2826
1059
|
payload: transportPayload,
|
|
2827
1060
|
});
|
|
2828
|
-
|
|
1061
|
+
const chainFallback = mode !== 'delta'
|
|
1062
|
+
&& deltaReason
|
|
1063
|
+
&& !['no_anchor', 'full_forced'].includes(deltaReason);
|
|
1064
|
+
if (chainFallback || (mode === 'delta' && deltaReason)) {
|
|
2829
1065
|
appendAgentTrace({
|
|
2830
1066
|
sessionId: poolKey,
|
|
2831
1067
|
iteration,
|
|
@@ -2839,7 +1075,11 @@ export async function sendViaWebSocket({
|
|
|
2839
1075
|
ws_mode: mode,
|
|
2840
1076
|
reason: mode === 'delta' ? deltaReason : (deltaReason || 'full_frame'),
|
|
2841
1077
|
cache_key_hash: transportCacheKeyHash,
|
|
2842
|
-
|
|
1078
|
+
cached_tokens: cacheObservation.cachedTokens,
|
|
1079
|
+
prompt_tokens: cacheObservation.promptTokens,
|
|
1080
|
+
uncached_tokens: cacheObservation.uncachedTokens,
|
|
1081
|
+
cache_ratio: cacheObservation.cacheRatio,
|
|
1082
|
+
actual_cache_miss: cacheObservation.actualMiss,
|
|
2843
1083
|
request_has_previous_response_id: transportPayload.request_has_previous_response_id,
|
|
2844
1084
|
chain_stripped_response_items: strippedResponseItems,
|
|
2845
1085
|
chain_skipped_response_items: skippedResponseItems,
|
|
@@ -2876,26 +1116,4 @@ export async function sendViaWebSocket({
|
|
|
2876
1116
|
}
|
|
2877
1117
|
// Unreachable — the loop either returns or throws above.
|
|
2878
1118
|
throw _stampTool(_stampLiveText(firstAttemptError || new Error('sendViaWebSocket: unreachable')));
|
|
2879
|
-
});
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
// Drain-complete fence — set true once _closeAllPooledSockets runs so any
|
|
2883
|
-
// in-flight acquire that resumes after drain throws instead of pushing a
|
|
2884
|
-
// fresh socket into the cleared pool. Single-set, process-lifetime invariant.
|
|
2885
|
-
let _drainComplete = false;
|
|
2886
|
-
|
|
2887
|
-
// Drain hook — self-registered exit drain.
|
|
2888
|
-
// Force-closes pooled sockets and fences subsequent acquires.
|
|
2889
|
-
// `drainOpenaiWsPool` alias matches the registry's `drain*` naming convention;
|
|
2890
|
-
// `_closeAllPooledSockets` kept for backward compat with existing call sites.
|
|
2891
|
-
export function _closeAllPooledSockets(reason = 'shutdown') {
|
|
2892
|
-
_drainComplete = true;
|
|
2893
|
-
for (const arr of _wsPool.values()) {
|
|
2894
|
-
for (const entry of arr) {
|
|
2895
|
-
try { entry.socket.close(1000, reason); } catch {}
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
_wsPool.clear();
|
|
2899
1119
|
}
|
|
2900
|
-
export const drainOpenaiWsPool = _closeAllPooledSockets;
|
|
2901
|
-
process.on('exit', drainOpenaiWsPool);
|