mixdog 0.8.0 → 0.9.0
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 +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenAI
|
|
2
|
+
* OpenAI OAuth subscription — WebSocket transport.
|
|
3
3
|
*
|
|
4
4
|
* Single dispatch path for the openai-oauth provider (SSE removed in
|
|
5
5
|
* v0.6.117). Uses the `responses_websockets=2026-02-06` beta WebSocket
|
|
@@ -28,12 +28,13 @@ import { errText } from '../../../shared/err-text.mjs';
|
|
|
28
28
|
import { createHash, randomBytes } from 'crypto';
|
|
29
29
|
import {
|
|
30
30
|
extractCachedTokens,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from '../
|
|
31
|
+
traceAgentFetch,
|
|
32
|
+
traceAgentSse,
|
|
33
|
+
traceAgentUsage,
|
|
34
|
+
appendAgentTrace,
|
|
35
|
+
} from '../agent-trace.mjs';
|
|
36
36
|
import { jitterDelayMs, populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
37
|
+
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
37
38
|
import {
|
|
38
39
|
PROVIDER_RETRY_MAX_ATTEMPTS,
|
|
39
40
|
PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
PROVIDER_WS_HANDSHAKE_TIMEOUT_MS,
|
|
42
43
|
PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS,
|
|
43
44
|
} from '../stall-policy.mjs';
|
|
45
|
+
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
44
46
|
|
|
45
47
|
globalThis.__mixdogOpenaiWsRuntimeLoaded = true;
|
|
46
48
|
|
|
@@ -69,9 +71,9 @@ const WS_PRE_RESPONSE_CREATED_MS = (() => {
|
|
|
69
71
|
})();
|
|
70
72
|
// Inter-chunk inactivity after first meaningful output.
|
|
71
73
|
const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
|
|
72
|
-
const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT =
|
|
73
|
-
const MIDSTREAM_DEFAULT_RETRY_LIMIT =
|
|
74
|
-
const MIDSTREAM_BACKOFF_MS = [250, 1000];
|
|
74
|
+
const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = 4;
|
|
75
|
+
const MIDSTREAM_DEFAULT_RETRY_LIMIT = 2;
|
|
76
|
+
const MIDSTREAM_BACKOFF_MS = [250, 1000, 2000, 4000];
|
|
75
77
|
|
|
76
78
|
// Handshake retry policy. The `ws` library surfaces a bare
|
|
77
79
|
// `Opening handshake has timed out` Error after handshakeTimeout; transient
|
|
@@ -88,14 +90,14 @@ const HANDSHAKE_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS;
|
|
|
88
90
|
const HANDSHAKE_BACKOFF_BASE_MS = 500;
|
|
89
91
|
const HANDSHAKE_BACKOFF_CAP_MS = 5000;
|
|
90
92
|
// WS socket pool buckets are keyed by `poolKey` (the per-call sessionId)
|
|
91
|
-
// to isolate parallel
|
|
92
|
-
// a second caller cannot grab a sibling's mid-turn entry (
|
|
93
|
+
// to isolate parallel agent invocations — each gets its own socket so
|
|
94
|
+
// a second caller cannot grab a sibling's mid-turn entry (openai-oauth would
|
|
93
95
|
// otherwise reject the new response.create with "No tool output found
|
|
94
|
-
// for function call ..."). The
|
|
96
|
+
// for function call ..."). The handshake `session_id` header/URL
|
|
95
97
|
// uses `cacheKey` — a prefix-scoped cache key derived from the configured
|
|
96
98
|
// provider namespace plus model/system/tools hash. Same-prefix sessions share
|
|
97
99
|
// server-side prompt cache, while unrelated main/worker prefixes no longer
|
|
98
|
-
// evict each other inside one static provider lane.
|
|
100
|
+
// evict each other inside one static provider lane. The backend dedupes cache by
|
|
99
101
|
// handshake session_id, not by body.prompt_cache_key alone (measured
|
|
100
102
|
// 2026-04-19 after the v0.6.151 regression).
|
|
101
103
|
const MAX_POOLED_SOCKETS_PER_KEY = 8;
|
|
@@ -106,10 +108,10 @@ const MAX_POOLED_SOCKETS_PER_KEY = 8;
|
|
|
106
108
|
// closing, ephemeral }
|
|
107
109
|
const _wsPool = new Map();
|
|
108
110
|
|
|
109
|
-
// Final prompt_cache_key/session_id lane guard for OpenAI
|
|
111
|
+
// Final prompt_cache_key/session_id lane guard for OpenAI OAuth transports.
|
|
110
112
|
// The provider code may shard one logical prefix into N cache keys for 10+
|
|
111
113
|
// total parallelism; inside each final key we still serialize requests because
|
|
112
|
-
//
|
|
114
|
+
// Live probes show same-key concurrent WebSockets can randomly miss the
|
|
113
115
|
// server prompt cache even after warm-up.
|
|
114
116
|
const _openAiPromptCacheLanes = new Map();
|
|
115
117
|
const _openAiPromptCacheLaneRates = new Map();
|
|
@@ -121,6 +123,11 @@ function _positiveInt(value, fallback = 0) {
|
|
|
121
123
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
function _nonNegativeInt(value, fallback = 0) {
|
|
127
|
+
const n = Number(value);
|
|
128
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
function _cacheLaneHash(value) {
|
|
125
132
|
return createHash('sha256').update(String(value || '')).digest('hex').slice(0, 12);
|
|
126
133
|
}
|
|
@@ -146,7 +153,7 @@ function _openAiPromptCacheLaneQueueTimeoutMs(sendOpts = {}) {
|
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
function _openAiPromptCacheLaneRateLimitPerMin(sendOpts = {}) {
|
|
149
|
-
return
|
|
156
|
+
return _nonNegativeInt(
|
|
150
157
|
sendOpts?.openaiCacheLaneRateLimitPerMin
|
|
151
158
|
?? sendOpts?.promptCacheLaneRateLimitPerMin
|
|
152
159
|
?? process.env.MIXDOG_OPENAI_CACHE_LANE_RPM
|
|
@@ -155,6 +162,45 @@ function _openAiPromptCacheLaneRateLimitPerMin(sendOpts = {}) {
|
|
|
155
162
|
);
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
function _openAiPromptCacheLaneDeltaRateLimitPerMin(sendOpts = {}) {
|
|
166
|
+
return _nonNegativeInt(
|
|
167
|
+
sendOpts?.openaiCacheLaneDeltaRateLimitPerMin
|
|
168
|
+
?? sendOpts?.promptCacheLaneDeltaRateLimitPerMin
|
|
169
|
+
?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_RPM
|
|
170
|
+
?? process.env.MIXDOG_OPENAI_CACHE_DELTA_RPM,
|
|
171
|
+
60,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _openAiPromptCacheLaneDeltaMaxItems(sendOpts = {}) {
|
|
176
|
+
return _nonNegativeInt(
|
|
177
|
+
sendOpts?.openaiCacheLaneDeltaMaxItems
|
|
178
|
+
?? sendOpts?.promptCacheLaneDeltaMaxItems
|
|
179
|
+
?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_MAX_ITEMS
|
|
180
|
+
?? process.env.MIXDOG_OPENAI_CACHE_DELTA_MAX_ITEMS,
|
|
181
|
+
8,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _openAiPromptCacheLaneDeltaMaxTokens(sendOpts = {}) {
|
|
186
|
+
return _nonNegativeInt(
|
|
187
|
+
sendOpts?.openaiCacheLaneDeltaMaxTokens
|
|
188
|
+
?? sendOpts?.promptCacheLaneDeltaMaxTokens
|
|
189
|
+
?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_MAX_TOKENS
|
|
190
|
+
?? process.env.MIXDOG_OPENAI_CACHE_DELTA_MAX_TOKENS,
|
|
191
|
+
20_000,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _openAiPromptCacheLaneSlowTraceMs(sendOpts = {}) {
|
|
196
|
+
return _positiveInt(
|
|
197
|
+
sendOpts?.openaiCacheLaneSlowTraceMs
|
|
198
|
+
?? sendOpts?.promptCacheLaneSlowTraceMs
|
|
199
|
+
?? process.env.MIXDOG_OPENAI_CACHE_LANE_SLOW_MS,
|
|
200
|
+
3000,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
158
204
|
function _isOpenAiPromptCacheLaneAuth(auth) {
|
|
159
205
|
return auth?.type !== 'xai';
|
|
160
206
|
}
|
|
@@ -229,12 +275,13 @@ function _sleepWithSignal(ms, signal) {
|
|
|
229
275
|
});
|
|
230
276
|
}
|
|
231
277
|
|
|
232
|
-
async function _reserveOpenAiPromptCacheLaneRate({ key, limitPerMin, signal }) {
|
|
278
|
+
async function _reserveOpenAiPromptCacheLaneRate({ key, limitPerMin, signal, beforeWait }) {
|
|
233
279
|
if (!limitPerMin || limitPerMin <= 0) {
|
|
234
280
|
return { rateLimitPerMin: 0, rateWaitMs: 0, rateWindowCount: 0 };
|
|
235
281
|
}
|
|
236
282
|
const state = _getOpenAiPromptCacheLaneRateState(key);
|
|
237
283
|
const startedAt = Date.now();
|
|
284
|
+
let beforeWaitCalled = false;
|
|
238
285
|
while (true) {
|
|
239
286
|
const now = Date.now();
|
|
240
287
|
_pruneOpenAiPromptCacheLaneRateState(state, now);
|
|
@@ -247,10 +294,57 @@ async function _reserveOpenAiPromptCacheLaneRate({ key, limitPerMin, signal }) {
|
|
|
247
294
|
};
|
|
248
295
|
}
|
|
249
296
|
const waitMs = Math.max(25, state.starts[0] + OPENAI_PROMPT_CACHE_LANE_RATE_WINDOW_MS - now);
|
|
297
|
+
if (!beforeWaitCalled && typeof beforeWait === 'function') {
|
|
298
|
+
beforeWaitCalled = true;
|
|
299
|
+
await beforeWait({
|
|
300
|
+
waitMs,
|
|
301
|
+
rateLimitPerMin: limitPerMin,
|
|
302
|
+
rateWindowCount: state.starts.length,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
250
305
|
await _sleepWithSignal(waitMs, signal);
|
|
251
306
|
}
|
|
252
307
|
}
|
|
253
308
|
|
|
309
|
+
export function _resolveOpenAiPromptCacheRatePolicy(sendOpts = {}, info = {}) {
|
|
310
|
+
const mode = String(info?.mode || '').toLowerCase();
|
|
311
|
+
const frameInputItems = Number(info?.frameInputItems);
|
|
312
|
+
const deltaTokens = Number(info?.deltaTokens);
|
|
313
|
+
const hasPreviousResponseId = info?.hasPreviousResponseId === true;
|
|
314
|
+
const fullLimitPerMin = _openAiPromptCacheLaneRateLimitPerMin(sendOpts);
|
|
315
|
+
const deltaLimitPerMin = _openAiPromptCacheLaneDeltaRateLimitPerMin(sendOpts);
|
|
316
|
+
const deltaMaxItems = _openAiPromptCacheLaneDeltaMaxItems(sendOpts);
|
|
317
|
+
const deltaMaxTokens = _openAiPromptCacheLaneDeltaMaxTokens(sendOpts);
|
|
318
|
+
const itemCount = Number.isFinite(frameInputItems) ? frameInputItems : null;
|
|
319
|
+
const tokenCount = Number.isFinite(deltaTokens) ? deltaTokens : null;
|
|
320
|
+
const smallDeltaItems = itemCount == null || deltaMaxItems <= 0 || itemCount <= deltaMaxItems;
|
|
321
|
+
const smallDeltaTokens = tokenCount == null || deltaMaxTokens <= 0 || tokenCount <= deltaMaxTokens;
|
|
322
|
+
|
|
323
|
+
if (mode === 'delta' && hasPreviousResponseId && smallDeltaItems && smallDeltaTokens) {
|
|
324
|
+
return {
|
|
325
|
+
policy: deltaLimitPerMin > 0 ? 'delta_relaxed' : 'delta_unlimited',
|
|
326
|
+
limitPerMin: deltaLimitPerMin,
|
|
327
|
+
fullLimitPerMin,
|
|
328
|
+
deltaLimitPerMin,
|
|
329
|
+
deltaMaxItems,
|
|
330
|
+
deltaMaxTokens,
|
|
331
|
+
frameInputItems: itemCount,
|
|
332
|
+
deltaTokens: tokenCount,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
policy: mode === 'delta' ? 'delta_guarded' : 'full_guard',
|
|
338
|
+
limitPerMin: fullLimitPerMin,
|
|
339
|
+
fullLimitPerMin,
|
|
340
|
+
deltaLimitPerMin,
|
|
341
|
+
deltaMaxItems,
|
|
342
|
+
deltaMaxTokens,
|
|
343
|
+
frameInputItems: itemCount,
|
|
344
|
+
deltaTokens: tokenCount,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
254
348
|
function _removeQueuedOpenAiPromptCacheLaneRequest(state, request) {
|
|
255
349
|
const index = state.queue.indexOf(request);
|
|
256
350
|
if (index >= 0) state.queue.splice(index, 1);
|
|
@@ -332,29 +426,10 @@ async function _withOpenAiPromptCacheLane({ auth, cacheKey, sendOpts, poolKey, i
|
|
|
332
426
|
}
|
|
333
427
|
const laneKey = `openai-prompt:${traceProvider || 'openai'}:${useModel || 'default'}:${cacheKey}`;
|
|
334
428
|
const laneKeyHash = _cacheLaneHash(laneKey);
|
|
335
|
-
const rateMeta = await _reserveOpenAiPromptCacheLaneRate({
|
|
336
|
-
key: laneKey,
|
|
337
|
-
limitPerMin: _openAiPromptCacheLaneRateLimitPerMin(sendOpts),
|
|
338
|
-
signal: externalSignal,
|
|
339
|
-
});
|
|
340
|
-
if (rateMeta.rateWaitMs > 0) {
|
|
341
|
-
appendBridgeTrace({
|
|
342
|
-
sessionId: poolKey,
|
|
343
|
-
iteration,
|
|
344
|
-
kind: 'cache_lane',
|
|
345
|
-
provider: traceProvider,
|
|
346
|
-
model: useModel,
|
|
347
|
-
event: 'rate_wait',
|
|
348
|
-
lane_key_hash: laneKeyHash,
|
|
349
|
-
rate_limit_per_min: rateMeta.rateLimitPerMin,
|
|
350
|
-
rate_wait_ms: rateMeta.rateWaitMs,
|
|
351
|
-
rate_window_count: rateMeta.rateWindowCount,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
429
|
const state = _getOpenAiPromptCacheLaneState(laneKey, maxInFlight);
|
|
355
430
|
const queued = state.active >= state.maxInFlight;
|
|
356
431
|
if (queued) {
|
|
357
|
-
|
|
432
|
+
appendAgentTrace({
|
|
358
433
|
sessionId: poolKey,
|
|
359
434
|
iteration,
|
|
360
435
|
kind: 'cache_lane',
|
|
@@ -368,23 +443,139 @@ async function _withOpenAiPromptCacheLane({ auth, cacheKey, sendOpts, poolKey, i
|
|
|
368
443
|
});
|
|
369
444
|
}
|
|
370
445
|
const timeoutMs = _openAiPromptCacheLaneQueueTimeoutMs(sendOpts);
|
|
371
|
-
|
|
446
|
+
let handle = await _acquireOpenAiPromptCacheLane({ key: laneKey, maxInFlight, signal: externalSignal, timeoutMs });
|
|
447
|
+
let handleActive = true;
|
|
372
448
|
const laneMeta = {
|
|
373
449
|
enabled: true,
|
|
374
450
|
laneKeyHash,
|
|
375
451
|
maxInFlight,
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
452
|
+
ratePolicy: 'pending',
|
|
453
|
+
rateLimitPerMin: 0,
|
|
454
|
+
rateWaitMs: 0,
|
|
455
|
+
rateWindowCount: 0,
|
|
456
|
+
rateReleasedForWait: false,
|
|
457
|
+
rateReacquireWaitMs: 0,
|
|
379
458
|
queued: queued || handle.queued === true,
|
|
380
459
|
waitMs: handle.waitedMs,
|
|
381
460
|
activeAfterAcquire: handle.activeCount,
|
|
382
461
|
queueDepthAfterAcquire: handle.queueDepth,
|
|
462
|
+
async reserveRate(info = {}) {
|
|
463
|
+
if (laneMeta.ratePolicy !== 'pending') return laneMeta;
|
|
464
|
+
const policy = _resolveOpenAiPromptCacheRatePolicy(sendOpts, info);
|
|
465
|
+
let releasedForRateWait = false;
|
|
466
|
+
const rateMeta = await _reserveOpenAiPromptCacheLaneRate({
|
|
467
|
+
key: laneKey,
|
|
468
|
+
limitPerMin: policy.limitPerMin,
|
|
469
|
+
signal: externalSignal,
|
|
470
|
+
beforeWait: () => {
|
|
471
|
+
if (!handleActive) return;
|
|
472
|
+
handle.release();
|
|
473
|
+
handleActive = false;
|
|
474
|
+
releasedForRateWait = true;
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
let reacquireWaitMs = 0;
|
|
478
|
+
if (releasedForRateWait) {
|
|
479
|
+
const reacquired = await _acquireOpenAiPromptCacheLane({
|
|
480
|
+
key: laneKey,
|
|
481
|
+
maxInFlight,
|
|
482
|
+
signal: externalSignal,
|
|
483
|
+
timeoutMs,
|
|
484
|
+
});
|
|
485
|
+
handle = reacquired;
|
|
486
|
+
handleActive = true;
|
|
487
|
+
reacquireWaitMs = reacquired.waitedMs;
|
|
488
|
+
laneMeta.queued = laneMeta.queued || reacquired.queued === true;
|
|
489
|
+
laneMeta.waitMs = (Number(laneMeta.waitMs) || 0) + reacquireWaitMs;
|
|
490
|
+
laneMeta.activeAfterAcquire = reacquired.activeCount;
|
|
491
|
+
laneMeta.queueDepthAfterAcquire = reacquired.queueDepth;
|
|
492
|
+
}
|
|
493
|
+
Object.assign(laneMeta, {
|
|
494
|
+
ratePolicy: policy.policy,
|
|
495
|
+
rateLimitPerMin: rateMeta.rateLimitPerMin,
|
|
496
|
+
rateWaitMs: rateMeta.rateWaitMs,
|
|
497
|
+
rateWindowCount: rateMeta.rateWindowCount,
|
|
498
|
+
rateReleasedForWait: releasedForRateWait,
|
|
499
|
+
rateReacquireWaitMs: reacquireWaitMs,
|
|
500
|
+
rateFullLimitPerMin: policy.fullLimitPerMin,
|
|
501
|
+
rateDeltaLimitPerMin: policy.deltaLimitPerMin,
|
|
502
|
+
rateDeltaMaxItems: policy.deltaMaxItems,
|
|
503
|
+
rateDeltaMaxTokens: policy.deltaMaxTokens,
|
|
504
|
+
ratePolicyFrameInputItems: policy.frameInputItems,
|
|
505
|
+
ratePolicyDeltaTokens: policy.deltaTokens,
|
|
506
|
+
});
|
|
507
|
+
if (rateMeta.rateWaitMs > 0) {
|
|
508
|
+
appendAgentTrace({
|
|
509
|
+
sessionId: poolKey,
|
|
510
|
+
iteration,
|
|
511
|
+
kind: 'cache_lane',
|
|
512
|
+
provider: traceProvider,
|
|
513
|
+
model: useModel,
|
|
514
|
+
event: 'rate_wait',
|
|
515
|
+
lane_key_hash: laneKeyHash,
|
|
516
|
+
rate_policy: laneMeta.ratePolicy,
|
|
517
|
+
rate_limit_per_min: rateMeta.rateLimitPerMin,
|
|
518
|
+
rate_wait_ms: rateMeta.rateWaitMs,
|
|
519
|
+
rate_window_count: rateMeta.rateWindowCount,
|
|
520
|
+
released_for_rate_wait: releasedForRateWait,
|
|
521
|
+
reacquire_wait_ms: reacquireWaitMs,
|
|
522
|
+
frame_input_items: policy.frameInputItems,
|
|
523
|
+
delta_tokens: policy.deltaTokens,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return laneMeta;
|
|
527
|
+
},
|
|
383
528
|
};
|
|
384
529
|
try {
|
|
385
530
|
return await fn(laneMeta);
|
|
386
531
|
} finally {
|
|
387
|
-
|
|
532
|
+
const slowTraceMs = _openAiPromptCacheLaneSlowTraceMs(sendOpts);
|
|
533
|
+
const slowWaitMs = Math.max(Number(laneMeta.rateWaitMs) || 0, Number(laneMeta.waitMs) || 0);
|
|
534
|
+
if (slowTraceMs > 0 && slowWaitMs >= slowTraceMs) {
|
|
535
|
+
appendAgentTrace({
|
|
536
|
+
sessionId: poolKey,
|
|
537
|
+
iteration,
|
|
538
|
+
kind: 'cache_lane_slow',
|
|
539
|
+
provider: traceProvider,
|
|
540
|
+
model: useModel,
|
|
541
|
+
event: laneMeta.rateWaitMs > 0 && laneMeta.waitMs > 0
|
|
542
|
+
? 'rate_and_queue_wait'
|
|
543
|
+
: laneMeta.rateWaitMs > 0
|
|
544
|
+
? 'rate_wait'
|
|
545
|
+
: 'queue_wait',
|
|
546
|
+
lane_key_hash: laneKeyHash,
|
|
547
|
+
payload: {
|
|
548
|
+
event: laneMeta.rateWaitMs > 0 && laneMeta.waitMs > 0
|
|
549
|
+
? 'rate_and_queue_wait'
|
|
550
|
+
: laneMeta.rateWaitMs > 0
|
|
551
|
+
? 'rate_wait'
|
|
552
|
+
: 'queue_wait',
|
|
553
|
+
provider: traceProvider,
|
|
554
|
+
model: useModel,
|
|
555
|
+
lane_key_hash: laneKeyHash,
|
|
556
|
+
threshold_ms: slowTraceMs,
|
|
557
|
+
max_wait_ms: slowWaitMs,
|
|
558
|
+
rate_policy: laneMeta.ratePolicy,
|
|
559
|
+
rate_limit_per_min: laneMeta.rateLimitPerMin,
|
|
560
|
+
rate_wait_ms: laneMeta.rateWaitMs,
|
|
561
|
+
rate_window_count: laneMeta.rateWindowCount,
|
|
562
|
+
released_for_rate_wait: laneMeta.rateReleasedForWait,
|
|
563
|
+
reacquire_wait_ms: laneMeta.rateReacquireWaitMs,
|
|
564
|
+
rate_full_limit_per_min: laneMeta.rateFullLimitPerMin,
|
|
565
|
+
rate_delta_limit_per_min: laneMeta.rateDeltaLimitPerMin,
|
|
566
|
+
rate_delta_max_items: laneMeta.rateDeltaMaxItems,
|
|
567
|
+
rate_delta_max_tokens: laneMeta.rateDeltaMaxTokens,
|
|
568
|
+
rate_policy_frame_input_items: laneMeta.ratePolicyFrameInputItems,
|
|
569
|
+
rate_policy_delta_tokens: laneMeta.ratePolicyDeltaTokens,
|
|
570
|
+
max_in_flight: laneMeta.maxInFlight,
|
|
571
|
+
queued: laneMeta.queued,
|
|
572
|
+
wait_ms: laneMeta.waitMs,
|
|
573
|
+
active_after_acquire: laneMeta.activeAfterAcquire,
|
|
574
|
+
queue_depth_after_acquire: laneMeta.queueDepthAfterAcquire,
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (handleActive) handle.release();
|
|
388
579
|
}
|
|
389
580
|
}
|
|
390
581
|
|
|
@@ -503,14 +694,14 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
|
|
|
503
694
|
return headers;
|
|
504
695
|
}
|
|
505
696
|
|
|
506
|
-
// handshake session_id is the conversation slot
|
|
697
|
+
// handshake session_id is the conversation slot openai-oauth uses for in-memory
|
|
507
698
|
// prefix state. All orchestrator-internal dispatches for this provider share
|
|
508
699
|
// the same cacheKey (built in manager.mjs via providerCacheKey()), so they
|
|
509
700
|
// share the server-side prefix-cache shard across roles/sources.
|
|
510
701
|
function _mintSessionToken(cacheKey, auth) {
|
|
511
702
|
// xAI's public WebSocket endpoint uses the open connection plus
|
|
512
|
-
// response ids for continuation; unlike
|
|
513
|
-
//
|
|
703
|
+
// response ids for continuation; unlike openai-oauth, it does not need the
|
|
704
|
+
// OAuth-specific session_id handshake shard.
|
|
514
705
|
if (auth?.type === 'xai') return null;
|
|
515
706
|
return cacheKey || 'mixdog-default';
|
|
516
707
|
}
|
|
@@ -523,8 +714,8 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
|
|
|
523
714
|
? OPENAI_WS_URL
|
|
524
715
|
: CODEX_WS_URL;
|
|
525
716
|
const _wsOpenStart = Date.now();
|
|
526
|
-
if (process.env.
|
|
527
|
-
process.stderr.write(`[
|
|
717
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
718
|
+
process.stderr.write(`[agent-trace] ws-open-start url=${baseUrl} tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} ts=${_wsOpenStart}\n`);
|
|
528
719
|
}
|
|
529
720
|
const url = baseUrl + (sessionToken ? `?session_id=${encodeURIComponent(String(sessionToken))}` : '');
|
|
530
721
|
return new Promise((resolve, reject) => {
|
|
@@ -546,8 +737,8 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
|
|
|
546
737
|
const socket = new WebSocket(url, { headers, handshakeTimeout: WS_HANDSHAKE_TIMEOUT_MS });
|
|
547
738
|
acquireTimer = setTimeout(() => {
|
|
548
739
|
if (settled) return;
|
|
549
|
-
if (process.env.
|
|
550
|
-
process.stderr.write(`[
|
|
740
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
741
|
+
process.stderr.write(`[agent-trace] ws-open-fail kind=acquire_timeout timeoutMs=${WS_ACQUIRE_TIMEOUT_MS} elapsed=${Date.now() - _wsOpenStart}ms\n`);
|
|
551
742
|
}
|
|
552
743
|
try { socket.terminate(); } catch {}
|
|
553
744
|
settle(false, Object.assign(
|
|
@@ -564,14 +755,14 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
|
|
|
564
755
|
} catch {}
|
|
565
756
|
});
|
|
566
757
|
socket.once('open', () => {
|
|
567
|
-
if (process.env.
|
|
568
|
-
process.stderr.write(`[
|
|
758
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
759
|
+
process.stderr.write(`[agent-trace] ws-open-ok elapsed=${Date.now() - _wsOpenStart}ms\n`);
|
|
569
760
|
}
|
|
570
761
|
settle(true, { socket, turnState: capturedHeaders.turnState });
|
|
571
762
|
});
|
|
572
763
|
socket.once('error', (err) => {
|
|
573
|
-
if (process.env.
|
|
574
|
-
process.stderr.write(`[
|
|
764
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
765
|
+
process.stderr.write(`[agent-trace] ws-open-fail kind=error msg=${String(err?.message || err).slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
|
|
575
766
|
}
|
|
576
767
|
try { socket.terminate(); } catch {}
|
|
577
768
|
settle(false, err instanceof Error ? err : Object.assign(new Error(errText(err) || 'openai-oauth WS error'), { wsErrorEvent: true, original: err }));
|
|
@@ -594,8 +785,8 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
|
|
|
594
785
|
let body = '';
|
|
595
786
|
res.on('data', c => { if (body.length < 2048) body += c.toString('utf-8'); });
|
|
596
787
|
res.on('end', () => {
|
|
597
|
-
if (process.env.
|
|
598
|
-
process.stderr.write(`[
|
|
788
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
789
|
+
process.stderr.write(`[agent-trace] ws-open-fail kind=http status=${status} body=${body.slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
|
|
599
790
|
}
|
|
600
791
|
try { socket.terminate(); } catch {}
|
|
601
792
|
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 }));
|
|
@@ -616,12 +807,12 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
|
|
|
616
807
|
|
|
617
808
|
async function acquireWebSocket({ auth, poolKey, cacheKey, forceFresh, externalSignal }) {
|
|
618
809
|
const _acqStart = Date.now();
|
|
619
|
-
if (process.env.
|
|
620
|
-
process.stderr.write(`[
|
|
810
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
811
|
+
process.stderr.write(`[agent-trace] acquire-start poolKey=${poolKey} cacheKey=${cacheKey} forceFresh=${forceFresh} externalAborted=${!!externalSignal?.aborted} ts=${_acqStart}\n`);
|
|
621
812
|
}
|
|
622
813
|
if (externalSignal?.aborted) {
|
|
623
814
|
const reason = externalSignal.reason;
|
|
624
|
-
throw reason instanceof Error ? reason : new Error('
|
|
815
|
+
throw reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted');
|
|
625
816
|
}
|
|
626
817
|
if (poolKey && !forceFresh) {
|
|
627
818
|
const arr = _wsPool.get(poolKey) || [];
|
|
@@ -645,15 +836,15 @@ async function acquireWebSocket({ auth, poolKey, cacheKey, forceFresh, externalS
|
|
|
645
836
|
if (idle.lastInputPrefixHash === undefined) idle.lastInputPrefixHash = null;
|
|
646
837
|
if (idle.lastRequestInput === undefined) idle.lastRequestInput = null;
|
|
647
838
|
if (idle.lastResponseItems === undefined) idle.lastResponseItems = null;
|
|
648
|
-
if (process.env.
|
|
649
|
-
process.stderr.write(`[
|
|
839
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
840
|
+
process.stderr.write(`[agent-trace] acquire-reuse poolKey=${poolKey} openSockets=${arr.length} elapsed=${Date.now() - _acqStart}ms\n`);
|
|
650
841
|
}
|
|
651
842
|
return { entry: idle, reused: true };
|
|
652
843
|
}
|
|
653
844
|
// All entries busy and bucket at cap: fall through to ephemeral socket.
|
|
654
845
|
if (arr.length >= MAX_POOLED_SOCKETS_PER_KEY) {
|
|
655
|
-
if (process.env.
|
|
656
|
-
process.stderr.write(`[
|
|
846
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
847
|
+
process.stderr.write(`[agent-trace] acquire-ephemeral cacheKey=${cacheKey} reason=cap elapsed=${Date.now() - _acqStart}ms\n`);
|
|
657
848
|
}
|
|
658
849
|
const ephSessionToken = _mintSessionToken(cacheKey, auth);
|
|
659
850
|
const { socket, turnState } = await _openSocket({ auth, sessionToken: ephSessionToken, turnState: null, externalSignal, cacheKey });
|
|
@@ -683,13 +874,13 @@ async function acquireWebSocket({ auth, poolKey, cacheKey, forceFresh, externalS
|
|
|
683
874
|
return { entry, reused: false };
|
|
684
875
|
}
|
|
685
876
|
}
|
|
686
|
-
// Parallel sockets must not inherit sibling turnState or the
|
|
877
|
+
// Parallel sockets must not inherit sibling turnState or the openai-oauth server
|
|
687
878
|
// treats the new request as a continuation of another in-flight turn and
|
|
688
879
|
// returns "No tool output found for function call …". turnState only
|
|
689
880
|
// propagates within a single entry across its own iterations.
|
|
690
881
|
const sessionToken = _mintSessionToken(cacheKey, auth);
|
|
691
|
-
if (process.env.
|
|
692
|
-
process.stderr.write(`[
|
|
882
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
883
|
+
process.stderr.write(`[agent-trace] acquire-new tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} elapsed=${Date.now() - _acqStart}ms\n`);
|
|
693
884
|
}
|
|
694
885
|
const { socket, turnState } = await _openSocket({ auth, sessionToken, turnState: null, externalSignal, cacheKey });
|
|
695
886
|
const entry = {
|
|
@@ -780,16 +971,41 @@ function _contentPartsEqual(a, b) {
|
|
|
780
971
|
return _stableStringify(aa) === _stableStringify(bb);
|
|
781
972
|
}
|
|
782
973
|
|
|
783
|
-
function _logicalResponseItemMatch(inputItem, responseItem) {
|
|
974
|
+
export function _logicalResponseItemMatch(inputItem, responseItem) {
|
|
784
975
|
if (!inputItem || !responseItem) return false;
|
|
785
976
|
const inputType = inputItem.type || (inputItem.role === 'assistant' ? 'message' : '');
|
|
786
977
|
const responseType = responseItem.type || '';
|
|
787
978
|
if (responseType === 'function_call') {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
979
|
+
if (inputType !== 'function_call') return false;
|
|
980
|
+
const inputCallId = String(inputItem.call_id || '');
|
|
981
|
+
const responseCallId = String(responseItem.call_id || '');
|
|
982
|
+
const inputName = String(inputItem.name || '');
|
|
983
|
+
const responseName = String(responseItem.name || '');
|
|
984
|
+
if (inputCallId && responseCallId) {
|
|
985
|
+
// call_id is the server-side anchor. The replayed history may carry
|
|
986
|
+
// locally compacted arguments, but previous_response_id already
|
|
987
|
+
// points at the canonical output item.
|
|
988
|
+
return inputCallId === responseCallId && inputName === responseName;
|
|
989
|
+
}
|
|
990
|
+
return inputName === responseName
|
|
791
991
|
&& _normalizeArguments(inputItem.arguments) === _normalizeArguments(responseItem.arguments);
|
|
792
992
|
}
|
|
993
|
+
if (responseType === 'tool_search_call') {
|
|
994
|
+
if (inputType !== 'tool_search_call') return false;
|
|
995
|
+
const inputCallId = String(inputItem.call_id || '');
|
|
996
|
+
const responseCallId = String(responseItem.call_id || '');
|
|
997
|
+
if (inputCallId && responseCallId) return inputCallId === responseCallId;
|
|
998
|
+
return _normalizeArguments(inputItem.arguments) === _normalizeArguments(responseItem.arguments);
|
|
999
|
+
}
|
|
1000
|
+
if (responseType === 'custom_tool_call') {
|
|
1001
|
+
if (inputType !== 'custom_tool_call') return false;
|
|
1002
|
+
const inputCallId = String(inputItem.call_id || '');
|
|
1003
|
+
const responseCallId = String(responseItem.call_id || '');
|
|
1004
|
+
const inputName = String(inputItem.name || '');
|
|
1005
|
+
const responseName = String(responseItem.name || '');
|
|
1006
|
+
if (inputCallId && responseCallId) return inputCallId === responseCallId && inputName === responseName;
|
|
1007
|
+
return inputName === responseName && String(inputItem.input || '') === String(responseItem.input || '');
|
|
1008
|
+
}
|
|
793
1009
|
if (responseType === 'message') {
|
|
794
1010
|
const inputRole = inputItem.role || (inputType === 'message' ? 'assistant' : '');
|
|
795
1011
|
const responseRole = responseItem.role || 'assistant';
|
|
@@ -838,6 +1054,7 @@ function _isReplayLikeHead(item, responseItem) {
|
|
|
838
1054
|
const responseType = responseItem.type || '';
|
|
839
1055
|
if (responseType === 'message') return inputType === 'message';
|
|
840
1056
|
if (responseType === 'function_call') return inputType === 'function_call';
|
|
1057
|
+
if (responseType === 'tool_search_call') return inputType === 'tool_search_call';
|
|
841
1058
|
return inputType === responseType;
|
|
842
1059
|
}
|
|
843
1060
|
|
|
@@ -969,7 +1186,25 @@ function _httpStatusFromWsClose(code, reason) {
|
|
|
969
1186
|
function _wsErrLabel(p) {
|
|
970
1187
|
if (p === 'xai') return 'xAI WS';
|
|
971
1188
|
if (p === 'openai-direct' || p === 'openai') return 'OpenAI WS';
|
|
972
|
-
return '
|
|
1189
|
+
return 'OpenAI OAuth WS';
|
|
1190
|
+
}
|
|
1191
|
+
// tool_search_call.arguments parse. Module-scope (exported) for direct test
|
|
1192
|
+
// coverage. Native convergence (openai-oauth / anthropic-oauth / opencode): same policy
|
|
1193
|
+
// as the function_call_arguments.done path and openai-oauth _parseJsonObject —
|
|
1194
|
+
// object passes through; null/non-string/empty/whitespace → {} (no args); a
|
|
1195
|
+
// non-empty string that fails JSON.parse is deterministic bad JSON, surfaced
|
|
1196
|
+
// as an invalid-args MARKER (not silently swallowed to {}) so the dispatch
|
|
1197
|
+
// loop returns an is_error tool_result and the model self-corrects in the same
|
|
1198
|
+
// turn.
|
|
1199
|
+
export function parseToolSearchArgs(value) {
|
|
1200
|
+
if (value && typeof value === 'object') return value;
|
|
1201
|
+
if (typeof value !== 'string' || !value.trim()) return {};
|
|
1202
|
+
try {
|
|
1203
|
+
const parsed = JSON.parse(value);
|
|
1204
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
return makeInvalidToolArgsMarker(value, err instanceof Error ? err.message : String(err));
|
|
1207
|
+
}
|
|
973
1208
|
}
|
|
974
1209
|
export async function _streamResponse({
|
|
975
1210
|
entry,
|
|
@@ -996,7 +1231,6 @@ export async function _streamResponse({
|
|
|
996
1231
|
const toolCalls = [];
|
|
997
1232
|
const webSearchCalls = [];
|
|
998
1233
|
const webSearchCallKeys = new Set();
|
|
999
|
-
const compactionItems = [];
|
|
1000
1234
|
const responseItemsAdded = [];
|
|
1001
1235
|
const responseItemKeys = new Set();
|
|
1002
1236
|
const citations = [];
|
|
@@ -1006,7 +1240,7 @@ export async function _streamResponse({
|
|
|
1006
1240
|
// from response.completed.response.output). The request still includes
|
|
1007
1241
|
// `reasoning.encrypted_content` so the server keeps emitting the blobs,
|
|
1008
1242
|
// but explicit input-side replay is INTENTIONALLY OMITTED in
|
|
1009
|
-
// convertMessagesToResponsesInput (openai-oauth.mjs:233-238) —
|
|
1243
|
+
// convertMessagesToResponsesInput (openai-oauth.mjs:233-238) — openai-oauth
|
|
1010
1244
|
// rejects the same `rs_*` id twice in one handshake session_id with a
|
|
1011
1245
|
// "Duplicate item" error. Server-side conversation state already carries
|
|
1012
1246
|
// the prefix forward across the WS_IDLE_MS window. The collected
|
|
@@ -1085,10 +1319,26 @@ export async function _streamResponse({
|
|
|
1085
1319
|
for (const url of action.urls) pushCitation({ url, title: action.query || '' });
|
|
1086
1320
|
}
|
|
1087
1321
|
};
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
if (!
|
|
1091
|
-
|
|
1322
|
+
const pushCustomToolCall = (item) => {
|
|
1323
|
+
const call = customToolCallFromResponseItem(item);
|
|
1324
|
+
if (!call || toolCalls.some((existing) => existing.id === call.id)) return;
|
|
1325
|
+
toolCalls.push(call);
|
|
1326
|
+
midState.emittedToolCall = true;
|
|
1327
|
+
try { onToolCall?.(call); } catch {}
|
|
1328
|
+
};
|
|
1329
|
+
const pushToolSearchCall = (item) => {
|
|
1330
|
+
if (!item || item.type !== 'tool_search_call') return;
|
|
1331
|
+
const callId = item.call_id || item.id || '';
|
|
1332
|
+
if (!callId || toolCalls.some((call) => call.id === callId)) return;
|
|
1333
|
+
const call = {
|
|
1334
|
+
id: callId,
|
|
1335
|
+
name: 'tool_search',
|
|
1336
|
+
arguments: parseToolSearchArgs(item.arguments),
|
|
1337
|
+
nativeType: 'tool_search_call',
|
|
1338
|
+
};
|
|
1339
|
+
toolCalls.push(call);
|
|
1340
|
+
midState.emittedToolCall = true;
|
|
1341
|
+
try { onToolCall?.(call); } catch {}
|
|
1092
1342
|
};
|
|
1093
1343
|
const logReasoningDeltaSuppression = () => {
|
|
1094
1344
|
if (!logSuppressedReasoningDeltas) return;
|
|
@@ -1128,13 +1378,13 @@ export async function _streamResponse({
|
|
|
1128
1378
|
// window is intentionally short (~25s). Once response.created (or
|
|
1129
1379
|
// any other meaningful event) arrives, the timer is cancelled and
|
|
1130
1380
|
// the longer inter-chunk inactivity watchdog takes over — silent
|
|
1131
|
-
// gaps mid-reasoning (
|
|
1381
|
+
// gaps mid-reasoning (openai-oauth spending 50s+ producing reasoning
|
|
1132
1382
|
// tokens) are normal and should not abort the turn.
|
|
1133
1383
|
const armPreStreamWatchdog = () => {
|
|
1134
1384
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1135
1385
|
idleTimer = setTimeout(() => {
|
|
1136
|
-
if (process.env.
|
|
1137
|
-
process.stderr.write(`[
|
|
1386
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1387
|
+
process.stderr.write(`[agent-trace] ws-timeout kind=first-byte afterMs=${preResponseCreatedMs}\n`);
|
|
1138
1388
|
}
|
|
1139
1389
|
traceWsTimeout('first_byte_timeout', preResponseCreatedMs);
|
|
1140
1390
|
const err = new Error(`WS stream: no first server event within ${preResponseCreatedMs}ms`);
|
|
@@ -1178,7 +1428,7 @@ export async function _streamResponse({
|
|
|
1178
1428
|
saw_response_created: midState.sawResponseCreated === true,
|
|
1179
1429
|
first_meaningful_seen: firstMeaningfulSeen === true,
|
|
1180
1430
|
};
|
|
1181
|
-
|
|
1431
|
+
appendAgentTrace({
|
|
1182
1432
|
sessionId: midState.sessionId || null,
|
|
1183
1433
|
iteration: Number.isFinite(iteration) ? iteration : null,
|
|
1184
1434
|
kind: 'ws_timeout',
|
|
@@ -1203,8 +1453,8 @@ export async function _streamResponse({
|
|
|
1203
1453
|
if (firstMeaningfulSeen || firstMeaningfulMs <= 0) return;
|
|
1204
1454
|
clearFirstMeaningfulWatchdog();
|
|
1205
1455
|
firstMeaningfulTimer = setTimeout(() => {
|
|
1206
|
-
if (process.env.
|
|
1207
|
-
process.stderr.write(`[
|
|
1456
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1457
|
+
process.stderr.write(`[agent-trace] ws-timeout kind=first-meaningful afterMs=${firstMeaningfulMs}\n`);
|
|
1208
1458
|
}
|
|
1209
1459
|
traceWsTimeout('first_meaningful_timeout', firstMeaningfulMs);
|
|
1210
1460
|
const err = new Error(`WS stream: no meaningful output within ${firstMeaningfulMs}ms after response.created`);
|
|
@@ -1219,8 +1469,8 @@ export async function _streamResponse({
|
|
|
1219
1469
|
const resetInterChunk = () => {
|
|
1220
1470
|
if (interChunkTimer) clearTimeout(interChunkTimer);
|
|
1221
1471
|
interChunkTimer = setTimeout(() => {
|
|
1222
|
-
if (process.env.
|
|
1223
|
-
process.stderr.write(`[
|
|
1472
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1473
|
+
process.stderr.write(`[agent-trace] ws-timeout kind=inter-chunk afterMs=${interChunkMs}\n`);
|
|
1224
1474
|
}
|
|
1225
1475
|
traceWsTimeout('inter_chunk_timeout', interChunkMs);
|
|
1226
1476
|
terminalError = new Error(`WS stream: inter-chunk inactivity for ${interChunkMs}ms`);
|
|
@@ -1237,7 +1487,7 @@ export async function _streamResponse({
|
|
|
1237
1487
|
};
|
|
1238
1488
|
// Called on every event that carries real output tokens or tool
|
|
1239
1489
|
// progress. `response.created` is only an ACK and must not count here:
|
|
1240
|
-
// a wedged
|
|
1490
|
+
// a wedged openai-oauth stream can ACK immediately and then never produce
|
|
1241
1491
|
// text/reasoning/tool deltas, holding the prompt-cache lane for the
|
|
1242
1492
|
// full inter-chunk window.
|
|
1243
1493
|
const onMeaningfulOutput = () => {
|
|
@@ -1263,19 +1513,12 @@ export async function _streamResponse({
|
|
|
1263
1513
|
const finish = () => {
|
|
1264
1514
|
logReasoningDeltaSuppression();
|
|
1265
1515
|
cleanup();
|
|
1266
|
-
if (!terminalError && midState.expectCompaction === true && compactionItems.length !== 1) {
|
|
1267
|
-
terminalError = new Error(
|
|
1268
|
-
`${errLabel} remote compaction expected exactly one compaction output item, got ${compactionItems.length}`,
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
1516
|
if (terminalError) { reject(terminalError); return; }
|
|
1272
1517
|
resolve({
|
|
1273
1518
|
content,
|
|
1274
1519
|
model,
|
|
1275
1520
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1276
1521
|
responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
|
|
1277
|
-
compactionItem: compactionItems.length === 1 ? compactionItems[0] : undefined,
|
|
1278
|
-
compactionItems: compactionItems.length ? compactionItems : undefined,
|
|
1279
1522
|
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
1280
1523
|
citations: citations.length ? citations : undefined,
|
|
1281
1524
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
@@ -1290,7 +1533,7 @@ export async function _streamResponse({
|
|
|
1290
1533
|
messageHandler = (data) => {
|
|
1291
1534
|
resetIdle();
|
|
1292
1535
|
// Do NOT call onStreamDelta for every frame — metadata/keepalive frames
|
|
1293
|
-
// must not reset
|
|
1536
|
+
// must not reset the agent stall watchdog's lastStreamDeltaAt. Only
|
|
1294
1537
|
// meaningful output (text delta / tool call) updates that timestamp.
|
|
1295
1538
|
const text = typeof data === 'string' ? data : data.toString('utf-8');
|
|
1296
1539
|
const event = _parseEvent(text);
|
|
@@ -1321,8 +1564,8 @@ export async function _streamResponse({
|
|
|
1321
1564
|
try {
|
|
1322
1565
|
if (!_firstDeltaEmitted) {
|
|
1323
1566
|
_firstDeltaEmitted = true;
|
|
1324
|
-
if (process.env.
|
|
1325
|
-
process.stderr.write(`[
|
|
1567
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1568
|
+
process.stderr.write(`[agent-trace] ws-first-delta sinceStreaming=${Date.now() - _streamingStart}ms\n`);
|
|
1326
1569
|
}
|
|
1327
1570
|
}
|
|
1328
1571
|
onStreamDelta?.();
|
|
@@ -1370,11 +1613,30 @@ export async function _streamResponse({
|
|
|
1370
1613
|
try { onStreamDelta?.(); } catch {}
|
|
1371
1614
|
onMeaningfulOutput();
|
|
1372
1615
|
break;
|
|
1616
|
+
case 'response.custom_tool_call_input.delta':
|
|
1617
|
+
try { onStreamDelta?.(); } catch {}
|
|
1618
|
+
onMeaningfulOutput();
|
|
1619
|
+
break;
|
|
1373
1620
|
case 'response.function_call_arguments.done': {
|
|
1374
1621
|
const itemId = event.item_id || '';
|
|
1375
1622
|
const pending = pendingCalls.get(itemId);
|
|
1623
|
+
// function_call_arguments.done is a completion signal:
|
|
1624
|
+
// empty/whitespace → no args ({}); a non-empty string that
|
|
1625
|
+
// fails JSON.parse is deterministic bad JSON. Native
|
|
1626
|
+
// convergence: surface an invalid-args MARKER (not silent
|
|
1627
|
+
// {}) so the dispatch loop returns an is_error tool_result
|
|
1628
|
+
// and the model re-issues valid JSON in the same turn.
|
|
1376
1629
|
let args = {};
|
|
1377
|
-
|
|
1630
|
+
{
|
|
1631
|
+
const _argText = typeof event.arguments === 'string' ? event.arguments : '';
|
|
1632
|
+
if (_argText.trim() !== '') {
|
|
1633
|
+
try {
|
|
1634
|
+
args = JSON.parse(_argText);
|
|
1635
|
+
} catch (err) {
|
|
1636
|
+
args = makeInvalidToolArgsMarker(_argText, err instanceof Error ? err.message : String(err));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1378
1640
|
enrichFunctionCallResponseItem({
|
|
1379
1641
|
itemId,
|
|
1380
1642
|
callId: pending?.callId || event.call_id || '',
|
|
@@ -1413,12 +1675,16 @@ export async function _streamResponse({
|
|
|
1413
1675
|
// function_call / output_text already captured via their
|
|
1414
1676
|
// dedicated streaming events. The one shape we still need
|
|
1415
1677
|
// here is `reasoning` — carries encrypted_content that
|
|
1416
|
-
// must be replayed on the next input to keep the
|
|
1678
|
+
// must be replayed on the next input to keep the openai-oauth
|
|
1417
1679
|
// server-side prompt cache prefix warm.
|
|
1418
1680
|
if (event.item?.type === 'reasoning') pushReasoningItem(event.item);
|
|
1419
1681
|
if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
|
|
1420
|
-
if (
|
|
1421
|
-
|
|
1682
|
+
if (event.item?.type === 'tool_search_call') {
|
|
1683
|
+
pushToolSearchCall(event.item);
|
|
1684
|
+
onMeaningfulOutput();
|
|
1685
|
+
}
|
|
1686
|
+
if (event.item?.type === 'custom_tool_call') {
|
|
1687
|
+
pushCustomToolCall(event.item);
|
|
1422
1688
|
onMeaningfulOutput();
|
|
1423
1689
|
}
|
|
1424
1690
|
break;
|
|
@@ -1434,7 +1700,7 @@ export async function _streamResponse({
|
|
|
1434
1700
|
inputTokens: u.input_tokens || 0,
|
|
1435
1701
|
outputTokens: u.output_tokens || 0,
|
|
1436
1702
|
cachedTokens: extractCachedTokens(u),
|
|
1437
|
-
//
|
|
1703
|
+
// openai-oauth reports input_tokens as the total
|
|
1438
1704
|
// prompt volume (cached portion is a subset, not
|
|
1439
1705
|
// additive). Alias into the cross-provider
|
|
1440
1706
|
// `promptTokens` field so downstream loggers have
|
|
@@ -1462,9 +1728,8 @@ export async function _streamResponse({
|
|
|
1462
1728
|
}
|
|
1463
1729
|
}
|
|
1464
1730
|
if (item.type === 'web_search_call') pushWebSearchCall(item);
|
|
1465
|
-
if (
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1731
|
+
if (item.type === 'tool_search_call') pushToolSearchCall(item);
|
|
1732
|
+
if (item.type === 'custom_tool_call') pushCustomToolCall(item);
|
|
1468
1733
|
// Salvage path: some streams emit reasoning only
|
|
1469
1734
|
// inside the final response.completed.output
|
|
1470
1735
|
// bundle (no per-item .done event). Dedup by id.
|
|
@@ -1514,7 +1779,7 @@ export async function _streamResponse({
|
|
|
1514
1779
|
break;
|
|
1515
1780
|
}
|
|
1516
1781
|
case 'response.done': {
|
|
1517
|
-
// response.done is the terminal frame for some
|
|
1782
|
+
// response.done is the terminal frame for some openai-oauth
|
|
1518
1783
|
// streams that never emit a separate response.completed.
|
|
1519
1784
|
// Route through the same completed/failed/incomplete
|
|
1520
1785
|
// normalization based on event.response.status so a
|
|
@@ -1638,7 +1903,7 @@ export async function _streamResponse({
|
|
|
1638
1903
|
const r = reason?.toString?.('utf-8') || '';
|
|
1639
1904
|
const httpStatus = _httpStatusFromWsClose(code, r);
|
|
1640
1905
|
terminalError = Object.assign(
|
|
1641
|
-
new Error(`
|
|
1906
|
+
new Error(`OpenAI OAuth WS closed before response.completed (code=${code}${r ? `, reason=${r}` : ''})`),
|
|
1642
1907
|
{ wsCloseCode: code, wsCloseReason: r, ...(httpStatus ? { httpStatus } : {}) },
|
|
1643
1908
|
);
|
|
1644
1909
|
} else if (terminalError && !terminalError.wsCloseCode) {
|
|
@@ -1674,15 +1939,15 @@ export async function _streamResponse({
|
|
|
1674
1939
|
abortHandler = () => {
|
|
1675
1940
|
if (done) return;
|
|
1676
1941
|
const reason = externalSignal.reason;
|
|
1677
|
-
terminalError = reason instanceof Error ? reason : new Error('
|
|
1942
|
+
terminalError = reason instanceof Error ? reason : new Error('OpenAI OAuth WS aborted by session close');
|
|
1678
1943
|
// Tag: was this a user/caller abort, or a watchdog abort?
|
|
1679
1944
|
// Mid-stream retry must skip user aborts but may retry watchdog
|
|
1680
1945
|
// aborts. The caller-owned AbortController surfaces through
|
|
1681
|
-
// externalSignal;
|
|
1682
|
-
// object whose name === '
|
|
1946
|
+
// externalSignal; the agent stall watchdog signals via a reason
|
|
1947
|
+
// object whose name === 'AgentStallAbortError'. stream-watchdog
|
|
1683
1948
|
// uses StreamStalledAbortError. Anything else → treat as user.
|
|
1684
1949
|
const reasonName = reason?.name || '';
|
|
1685
|
-
if (reasonName === '
|
|
1950
|
+
if (reasonName === 'AgentStallAbortError'
|
|
1686
1951
|
|| reasonName === 'StreamStalledAbortError') {
|
|
1687
1952
|
midState.watchdogAbort = reasonName;
|
|
1688
1953
|
} else {
|
|
@@ -1698,10 +1963,10 @@ export async function _streamResponse({
|
|
|
1698
1963
|
socket.on('close', closeHandler);
|
|
1699
1964
|
socket.on('error', errorHandler);
|
|
1700
1965
|
armPreStreamWatchdog();
|
|
1701
|
-
// Periodic client-side WS ping while the stream is active.
|
|
1966
|
+
// Periodic client-side WS ping while the stream is active. The server's
|
|
1702
1967
|
// server closes with 1011 "keepalive ping timeout" when it thinks the
|
|
1703
1968
|
// peer is silent during long reasoning windows where no data frames
|
|
1704
|
-
// flow. Sending a ping every
|
|
1969
|
+
// flow. Sending a ping every 10s from our side keeps the socket warm.
|
|
1705
1970
|
// The interval is unref'd so it never holds the event loop open, and
|
|
1706
1971
|
// cleanup() clears it on every terminal path (completed / close /
|
|
1707
1972
|
// error / abort / mid-stream retry teardown).
|
|
@@ -1710,7 +1975,7 @@ export async function _streamResponse({
|
|
|
1710
1975
|
if (socket.readyState !== WebSocket.OPEN) return;
|
|
1711
1976
|
socket.ping();
|
|
1712
1977
|
} catch {}
|
|
1713
|
-
},
|
|
1978
|
+
}, 10_000);
|
|
1714
1979
|
try { keepaliveTimer.unref?.(); } catch {}
|
|
1715
1980
|
});
|
|
1716
1981
|
}
|
|
@@ -1771,7 +2036,7 @@ export function _classifyHandshakeError(err) {
|
|
|
1771
2036
|
* after completion would replay a finished turn.
|
|
1772
2037
|
*
|
|
1773
2038
|
* Retry buckets:
|
|
1774
|
-
* '
|
|
2039
|
+
* 'agent_stall' — AgentStallAbortError from agent stall watchdog
|
|
1775
2040
|
* 'stream_stalled' — StreamStalledAbortError from stream-watchdog
|
|
1776
2041
|
* 'ws_1006' — abnormal close (connection lost)
|
|
1777
2042
|
* 'ws_1011' — server unexpected condition
|
|
@@ -1811,7 +2076,7 @@ export function _classifyMidstreamError(err, state) {
|
|
|
1811
2076
|
// leaves the caller with an orphaned tool_use that the next turn cannot
|
|
1812
2077
|
// pair to a tool_result, which the provider rejects with a hard 400.
|
|
1813
2078
|
// The duplicate-side-effect risk is preferable to deterministic worker
|
|
1814
|
-
// death, especially for detached
|
|
2079
|
+
// death, especially for detached agents that re-dispatch idempotently.
|
|
1815
2080
|
if (state.emittedToolCall) {
|
|
1816
2081
|
const _cc = Number(err?.wsCloseCode || state.wsCloseCode || 0);
|
|
1817
2082
|
if (!(_cc === 1000 && state.sawResponseCreated && !state.sawCompleted)) return null;
|
|
@@ -1867,13 +2132,13 @@ export function _classifyMidstreamError(err, state) {
|
|
|
1867
2132
|
}
|
|
1868
2133
|
|
|
1869
2134
|
const name = err?.name || '';
|
|
1870
|
-
if (name === '
|
|
2135
|
+
if (name === 'AgentStallAbortError') return _allowMidstreamRetry('agent_stall', attemptIndex);
|
|
1871
2136
|
if (name === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
|
|
1872
2137
|
|
|
1873
2138
|
// Watchdog abort surfaced via externalSignal handler → err is the reason
|
|
1874
2139
|
// itself. state.watchdogAbort captures the class name when the error
|
|
1875
2140
|
// shape was preserved but the name was stripped by some wrapper.
|
|
1876
|
-
if (state.watchdogAbort === '
|
|
2141
|
+
if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstreamRetry('agent_stall', attemptIndex);
|
|
1877
2142
|
if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
|
|
1878
2143
|
|
|
1879
2144
|
// WS close codes: prefer the decorated property, fall back to state.
|
|
@@ -1923,7 +2188,8 @@ function _allowMidstreamRetry(classifier, attemptIndex) {
|
|
|
1923
2188
|
}
|
|
1924
2189
|
|
|
1925
2190
|
function _midstreamBackoffFor(retryNumber) {
|
|
1926
|
-
|
|
2191
|
+
const raw = MIDSTREAM_BACKOFF_MS[Math.min(Math.max(retryNumber, 1), MIDSTREAM_BACKOFF_MS.length) - 1];
|
|
2192
|
+
return jitterDelayMs(raw);
|
|
1927
2193
|
}
|
|
1928
2194
|
|
|
1929
2195
|
function _backoffFor(attempt) {
|
|
@@ -1948,7 +2214,7 @@ async function _sleepWithAbort(ms, externalSignal, sleepFn = _defaultSleep) {
|
|
|
1948
2214
|
const onAbort = () => {
|
|
1949
2215
|
clearTimeout(t);
|
|
1950
2216
|
const reason = externalSignal.reason;
|
|
1951
|
-
reject(reason instanceof Error ? reason : new Error('
|
|
2217
|
+
reject(reason instanceof Error ? reason : new Error('OpenAI OAuth WS retry backoff aborted'));
|
|
1952
2218
|
};
|
|
1953
2219
|
if (externalSignal.aborted) { onAbort(); return; }
|
|
1954
2220
|
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
@@ -1980,12 +2246,12 @@ export async function _acquireWithRetry({
|
|
|
1980
2246
|
for (let attempt = 1; attempt <= HANDSHAKE_MAX_ATTEMPTS; attempt++) {
|
|
1981
2247
|
if (externalSignal?.aborted) {
|
|
1982
2248
|
const reason = externalSignal.reason;
|
|
1983
|
-
throw reason instanceof Error ? reason : new Error('
|
|
2249
|
+
throw reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted');
|
|
1984
2250
|
}
|
|
1985
2251
|
try {
|
|
1986
2252
|
if (attempt > 1) {
|
|
1987
|
-
if (process.env.
|
|
1988
|
-
process.stderr.write(`[
|
|
2253
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
2254
|
+
process.stderr.write(`[agent-trace] ws-handshake-attempt n=${attempt}\n`);
|
|
1989
2255
|
}
|
|
1990
2256
|
}
|
|
1991
2257
|
return await _acquire({ auth, poolKey, cacheKey, forceFresh, externalSignal });
|
|
@@ -2041,7 +2307,7 @@ export async function _acquireWithRetry({
|
|
|
2041
2307
|
const onAbort = () => {
|
|
2042
2308
|
clearTimeout(t);
|
|
2043
2309
|
const reason = externalSignal.reason;
|
|
2044
|
-
reject(reason instanceof Error ? reason : new Error('
|
|
2310
|
+
reject(reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted'));
|
|
2045
2311
|
};
|
|
2046
2312
|
if (externalSignal.aborted) { onAbort(); return; }
|
|
2047
2313
|
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
@@ -2121,12 +2387,21 @@ export async function sendViaWebSocket({
|
|
|
2121
2387
|
}
|
|
2122
2388
|
return e;
|
|
2123
2389
|
};
|
|
2390
|
+
// Tool-call relay invariant: same latch pattern as live text — once a tool
|
|
2391
|
+
// call was forwarded, surfaced errors must block HTTP fallback / reissue.
|
|
2392
|
+
let toolEmittedAcrossAttempts = false;
|
|
2393
|
+
const _stampTool = (e) => {
|
|
2394
|
+
if (toolEmittedAcrossAttempts && e) {
|
|
2395
|
+
try { e.emittedToolCall = true; e.unsafeToRetry = true; } catch {}
|
|
2396
|
+
}
|
|
2397
|
+
return e;
|
|
2398
|
+
};
|
|
2124
2399
|
// Server-side xAI conversation anchor preserved across mid-stream
|
|
2125
2400
|
// retries. xAI keys its conversation by previous_response_id alone
|
|
2126
2401
|
// (sessionToken is null for xAI in _mintSessionToken); a forceFresh
|
|
2127
2402
|
// socket on retry would otherwise drop prev_id and cold-start a new
|
|
2128
2403
|
// server-side conversation, evicting every prefix the prior attempts
|
|
2129
|
-
// warmed.
|
|
2404
|
+
// warmed. openai-oauth / openai-direct anchor by per-socket session_id, where
|
|
2130
2405
|
// this carry-forward would not help and is therefore gated to xAI.
|
|
2131
2406
|
let carryForwardCache = null;
|
|
2132
2407
|
const emittedProgress = [];
|
|
@@ -2166,7 +2441,7 @@ export async function sendViaWebSocket({
|
|
|
2166
2441
|
const classifiers = [...handshakeRetryClassifiers];
|
|
2167
2442
|
if (classifier && !classifiers.includes(classifier)) classifiers.push(classifier);
|
|
2168
2443
|
if (err?.httpStatus != null || classifier || handshakeRetries > 0 || classifiers.length > 0) {
|
|
2169
|
-
|
|
2444
|
+
traceAgentFetch({
|
|
2170
2445
|
sessionId: poolKey,
|
|
2171
2446
|
headersMs: Date.now() - handshakeStart,
|
|
2172
2447
|
httpStatus: Number(err?.httpStatus || 0),
|
|
@@ -2182,9 +2457,9 @@ export async function sendViaWebSocket({
|
|
|
2182
2457
|
// the caller's turn actually tripped on).
|
|
2183
2458
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
2184
2459
|
try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
|
|
2185
|
-
throw _stampLiveText(firstAttemptError);
|
|
2460
|
+
throw _stampTool(_stampLiveText(firstAttemptError));
|
|
2186
2461
|
}
|
|
2187
|
-
throw _stampLiveText(err);
|
|
2462
|
+
throw _stampTool(_stampLiveText(err));
|
|
2188
2463
|
}
|
|
2189
2464
|
const { entry, reused } = acquired;
|
|
2190
2465
|
// Re-seed the retry attempt's fresh entry with the prior attempt's
|
|
@@ -2199,7 +2474,7 @@ export async function sendViaWebSocket({
|
|
|
2199
2474
|
entry.lastRequestInput = carryForwardCache.lastRequestInput;
|
|
2200
2475
|
entry.lastResponseItems = carryForwardCache.lastResponseItems;
|
|
2201
2476
|
}
|
|
2202
|
-
|
|
2477
|
+
traceAgentFetch({
|
|
2203
2478
|
sessionId: poolKey,
|
|
2204
2479
|
headersMs: Date.now() - handshakeStart,
|
|
2205
2480
|
httpStatus: reused ? 0 : 101,
|
|
@@ -2245,14 +2520,15 @@ export async function sendViaWebSocket({
|
|
|
2245
2520
|
let strippedResponseItems = 0;
|
|
2246
2521
|
let skippedResponseItems = 0;
|
|
2247
2522
|
let result;
|
|
2248
|
-
const streamTimeouts =
|
|
2249
|
-
? {
|
|
2250
|
-
firstMeaningfulMs: WS_INTER_CHUNK_MS,
|
|
2251
|
-
interChunkMs: WS_INTER_CHUNK_MS,
|
|
2252
|
-
}
|
|
2253
|
-
: null;
|
|
2523
|
+
const streamTimeouts = null;
|
|
2254
2524
|
try {
|
|
2255
2525
|
if (warmupBody && typeof warmupBody === 'object' && attemptIndex === 0) {
|
|
2526
|
+
await promptCacheLane?.reserveRate?.({
|
|
2527
|
+
mode: 'full',
|
|
2528
|
+
frameInputItems: Array.isArray(warmupBody.input) ? warmupBody.input.length : null,
|
|
2529
|
+
deltaTokens: _estimateFrameTokens({ type: 'response.create', ...warmupBody }),
|
|
2530
|
+
hasPreviousResponseId: false,
|
|
2531
|
+
});
|
|
2256
2532
|
const warmupFrame = { type: 'response.create', ...warmupBody };
|
|
2257
2533
|
await _sendFrameFn(entry, warmupFrame);
|
|
2258
2534
|
const warmupStart = Date.now();
|
|
@@ -2306,7 +2582,7 @@ export async function sendViaWebSocket({
|
|
|
2306
2582
|
output_tokens: warmupResult.usage?.outputTokens || 0,
|
|
2307
2583
|
prompt_tokens: warmupResult.usage?.promptTokens || 0,
|
|
2308
2584
|
};
|
|
2309
|
-
|
|
2585
|
+
appendAgentTrace({
|
|
2310
2586
|
sessionId: poolKey,
|
|
2311
2587
|
iteration,
|
|
2312
2588
|
kind: 'cache_warmup',
|
|
@@ -2329,6 +2605,12 @@ export async function sendViaWebSocket({
|
|
|
2329
2605
|
strippedResponseItems = delta.strippedResponseItems || 0;
|
|
2330
2606
|
skippedResponseItems = delta.skippedResponseItems || 0;
|
|
2331
2607
|
deltaTokens = _estimateFrameTokens(frame);
|
|
2608
|
+
await promptCacheLane?.reserveRate?.({
|
|
2609
|
+
mode,
|
|
2610
|
+
frameInputItems: Array.isArray(frame.input) ? frame.input.length : null,
|
|
2611
|
+
deltaTokens,
|
|
2612
|
+
hasPreviousResponseId: typeof frame.previous_response_id === 'string' && frame.previous_response_id.length > 0,
|
|
2613
|
+
});
|
|
2332
2614
|
|
|
2333
2615
|
// Re-check abort after acquire/warmup — narrow window where
|
|
2334
2616
|
// externalSignal could fire between successful acquire and
|
|
@@ -2343,8 +2625,8 @@ export async function sendViaWebSocket({
|
|
|
2343
2625
|
}
|
|
2344
2626
|
await _sendFrameFn(entry, frame);
|
|
2345
2627
|
|
|
2346
|
-
if (process.env.
|
|
2347
|
-
process.stderr.write(`[
|
|
2628
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
2629
|
+
process.stderr.write(`[agent-trace] ws-streaming-start sinceAcquire=${Date.now() - handshakeStart}ms\n`);
|
|
2348
2630
|
}
|
|
2349
2631
|
try { onStageChange?.('streaming'); } catch {}
|
|
2350
2632
|
result = await _streamFn({
|
|
@@ -2385,7 +2667,11 @@ export async function sendViaWebSocket({
|
|
|
2385
2667
|
// error (firstAttemptError) must still carry the marker.
|
|
2386
2668
|
liveTextEmittedAcrossAttempts = true;
|
|
2387
2669
|
}
|
|
2670
|
+
if (midState.emittedToolCall) {
|
|
2671
|
+
toolEmittedAcrossAttempts = true;
|
|
2672
|
+
}
|
|
2388
2673
|
_stampLiveText(err);
|
|
2674
|
+
_stampTool(err);
|
|
2389
2675
|
const classifier = _classifyMidstreamError(err, midState);
|
|
2390
2676
|
const retryLimit = classifier ? _midstreamRetryLimit(classifier) : 0;
|
|
2391
2677
|
if (classifier && attemptIndex < retryLimit) {
|
|
@@ -2424,12 +2710,12 @@ export async function sendViaWebSocket({
|
|
|
2424
2710
|
firstAttemptError.suppressed = list;
|
|
2425
2711
|
}
|
|
2426
2712
|
} catch {}
|
|
2427
|
-
throw _stampLiveText(firstAttemptError);
|
|
2713
|
+
throw _stampTool(_stampLiveText(firstAttemptError));
|
|
2428
2714
|
}
|
|
2429
|
-
throw _stampLiveText(err);
|
|
2715
|
+
throw _stampTool(_stampLiveText(err));
|
|
2430
2716
|
}
|
|
2431
2717
|
const liveModel = result.model || useModel;
|
|
2432
|
-
|
|
2718
|
+
traceAgentSse({
|
|
2433
2719
|
sessionId: poolKey,
|
|
2434
2720
|
sseParseMs: Date.now() - sseStart,
|
|
2435
2721
|
provider: traceProvider,
|
|
@@ -2438,10 +2724,21 @@ export async function sendViaWebSocket({
|
|
|
2438
2724
|
});
|
|
2439
2725
|
|
|
2440
2726
|
const resultToolCallCount = Array.isArray(result.toolCalls) ? result.toolCalls.length : 0;
|
|
2441
|
-
|
|
2727
|
+
// Keep the conversation chain whenever the server gave us a response id.
|
|
2728
|
+
// `incompleteReason` is ONLY ever set for max_output_tokens-class
|
|
2729
|
+
// truncation (every other incomplete status throws upstream), and in
|
|
2730
|
+
// that case the response IS valid and the server preserves its
|
|
2731
|
+
// response_id as a continuation anchor. Dropping the chain here forced
|
|
2732
|
+
// the NEXT turn to cold-start (no_anchor → full resend), which the
|
|
2733
|
+
// trace logs showed repeating 50-78x in long max-output sessions. If a
|
|
2734
|
+
// truncated turn's response items don't line up next turn,
|
|
2735
|
+
// _stripResponseItemsFromHead still falls back to a full send on its
|
|
2736
|
+
// own, so retaining the anchor cannot corrupt the cache — it only adds
|
|
2737
|
+
// a delta fast-path when the items DO match.
|
|
2738
|
+
const keepResponseChain = !!result.responseId;
|
|
2442
2739
|
const keepSocket = true;
|
|
2443
2740
|
|
|
2444
|
-
// Update cache state for the next iteration in this session.
|
|
2741
|
+
// Update cache state for the next iteration in this session. openai-oauth
|
|
2445
2742
|
// keeps the previous response anchor even when the model emitted tool
|
|
2446
2743
|
// calls: the next request is previous input + server output items
|
|
2447
2744
|
// + tool results, and _computeDelta strips the first two parts so the
|
|
@@ -2473,7 +2770,7 @@ export async function sendViaWebSocket({
|
|
|
2473
2770
|
|
|
2474
2771
|
const requestedServiceTier = body?.service_tier || null;
|
|
2475
2772
|
const responseServiceTier = result.serviceTier || result.usage?.raw?.service_tier || null;
|
|
2476
|
-
|
|
2773
|
+
traceAgentUsage({
|
|
2477
2774
|
sessionId: poolKey,
|
|
2478
2775
|
iteration,
|
|
2479
2776
|
inputTokens: result.usage?.inputTokens || 0,
|
|
@@ -2489,6 +2786,9 @@ export async function sendViaWebSocket({
|
|
|
2489
2786
|
});
|
|
2490
2787
|
// Extra WS-specific observability: transport + per-iteration delta bytes.
|
|
2491
2788
|
try {
|
|
2789
|
+
const transportCacheKeyHash = cacheKey
|
|
2790
|
+
? createHash('sha256').update(String(cacheKey)).digest('hex').slice(0, 12)
|
|
2791
|
+
: null;
|
|
2492
2792
|
const transportPayload = {
|
|
2493
2793
|
provider: traceProvider,
|
|
2494
2794
|
transport: 'websocket',
|
|
@@ -2504,15 +2804,18 @@ export async function sendViaWebSocket({
|
|
|
2504
2804
|
handshake_retry_classifiers: handshakeRetryClassifiers,
|
|
2505
2805
|
midstream_retries: attemptIndex,
|
|
2506
2806
|
response_id: result.responseId || null,
|
|
2507
|
-
cache_key_hash:
|
|
2508
|
-
? createHash('sha256').update(String(cacheKey)).digest('hex').slice(0, 12)
|
|
2509
|
-
: null,
|
|
2807
|
+
cache_key_hash: transportCacheKeyHash,
|
|
2510
2808
|
cache_lane_enabled: promptCacheLane?.enabled === true,
|
|
2511
2809
|
cache_lane_key_hash: promptCacheLane?.laneKeyHash || null,
|
|
2810
|
+
cache_lane_rate_policy: promptCacheLane?.ratePolicy || null,
|
|
2512
2811
|
cache_lane_max_in_flight: Number.isFinite(Number(promptCacheLane?.maxInFlight)) ? Number(promptCacheLane.maxInFlight) : null,
|
|
2513
2812
|
cache_lane_rate_limit_per_min: Number.isFinite(Number(promptCacheLane?.rateLimitPerMin)) ? Number(promptCacheLane.rateLimitPerMin) : null,
|
|
2813
|
+
cache_lane_rate_full_limit_per_min: Number.isFinite(Number(promptCacheLane?.rateFullLimitPerMin)) ? Number(promptCacheLane.rateFullLimitPerMin) : null,
|
|
2814
|
+
cache_lane_rate_delta_limit_per_min: Number.isFinite(Number(promptCacheLane?.rateDeltaLimitPerMin)) ? Number(promptCacheLane.rateDeltaLimitPerMin) : null,
|
|
2514
2815
|
cache_lane_rate_wait_ms: Number.isFinite(Number(promptCacheLane?.rateWaitMs)) ? Number(promptCacheLane.rateWaitMs) : null,
|
|
2515
2816
|
cache_lane_rate_window_count: Number.isFinite(Number(promptCacheLane?.rateWindowCount)) ? Number(promptCacheLane.rateWindowCount) : null,
|
|
2817
|
+
cache_lane_rate_released_for_wait: promptCacheLane?.rateReleasedForWait === true,
|
|
2818
|
+
cache_lane_rate_reacquire_wait_ms: Number.isFinite(Number(promptCacheLane?.rateReacquireWaitMs)) ? Number(promptCacheLane.rateReacquireWaitMs) : null,
|
|
2516
2819
|
cache_lane_wait_ms: Number.isFinite(Number(promptCacheLane?.waitMs)) ? Number(promptCacheLane.waitMs) : null,
|
|
2517
2820
|
cache_lane_queued: promptCacheLane?.queued === true,
|
|
2518
2821
|
cache_lane_active: Number.isFinite(Number(promptCacheLane?.activeAfterAcquire)) ? Number(promptCacheLane.activeAfterAcquire) : null,
|
|
@@ -2531,13 +2834,40 @@ export async function sendViaWebSocket({
|
|
|
2531
2834
|
keep_socket: keepSocket,
|
|
2532
2835
|
keep_response_chain: keepResponseChain,
|
|
2533
2836
|
};
|
|
2534
|
-
|
|
2837
|
+
appendAgentTrace({
|
|
2535
2838
|
sessionId: poolKey,
|
|
2536
2839
|
iteration,
|
|
2537
2840
|
kind: 'transport',
|
|
2538
2841
|
...transportPayload,
|
|
2539
2842
|
payload: transportPayload,
|
|
2540
2843
|
});
|
|
2844
|
+
if (mode !== 'delta' || deltaReason) {
|
|
2845
|
+
appendAgentTrace({
|
|
2846
|
+
sessionId: poolKey,
|
|
2847
|
+
iteration,
|
|
2848
|
+
kind: 'cache_break',
|
|
2849
|
+
provider: traceProvider,
|
|
2850
|
+
model: liveModel,
|
|
2851
|
+
payload: {
|
|
2852
|
+
provider: traceProvider,
|
|
2853
|
+
model: liveModel,
|
|
2854
|
+
transport: 'websocket',
|
|
2855
|
+
ws_mode: mode,
|
|
2856
|
+
reason: mode === 'delta' ? deltaReason : (deltaReason || 'full_frame'),
|
|
2857
|
+
cache_key_hash: transportCacheKeyHash,
|
|
2858
|
+
cache_lane_key_hash: promptCacheLane?.laneKeyHash || null,
|
|
2859
|
+
request_has_previous_response_id: transportPayload.request_has_previous_response_id,
|
|
2860
|
+
chain_stripped_response_items: strippedResponseItems,
|
|
2861
|
+
chain_skipped_response_items: skippedResponseItems,
|
|
2862
|
+
chain_response_items: Array.isArray(result.responseItems) ? result.responseItems.length : 0,
|
|
2863
|
+
body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
|
|
2864
|
+
frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
|
|
2865
|
+
frame_has_instructions: transportPayload.frame_has_instructions,
|
|
2866
|
+
keep_response_chain: keepResponseChain,
|
|
2867
|
+
tool_call_count: resultToolCallCount,
|
|
2868
|
+
},
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2541
2871
|
} catch {}
|
|
2542
2872
|
|
|
2543
2873
|
releaseWebSocket({ entry, poolKey, keep: keepSocket });
|
|
@@ -2561,7 +2891,7 @@ export async function sendViaWebSocket({
|
|
|
2561
2891
|
return out;
|
|
2562
2892
|
}
|
|
2563
2893
|
// Unreachable — the loop either returns or throws above.
|
|
2564
|
-
throw _stampLiveText(firstAttemptError || new Error('sendViaWebSocket: unreachable'));
|
|
2894
|
+
throw _stampTool(_stampLiveText(firstAttemptError || new Error('sendViaWebSocket: unreachable')));
|
|
2565
2895
|
});
|
|
2566
2896
|
}
|
|
2567
2897
|
|