mixdog 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -29,13 +29,17 @@ import {
|
|
|
29
29
|
traceAgentUsage,
|
|
30
30
|
} from '../agent-trace.mjs';
|
|
31
31
|
import {
|
|
32
|
-
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
33
32
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
33
|
+
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
34
|
+
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
35
|
+
streamStalledError,
|
|
34
36
|
createTimeoutSignal,
|
|
37
|
+
createPassthroughSignal,
|
|
35
38
|
} from '../stall-policy.mjs';
|
|
36
39
|
import { populateHttpStatusFromMessage, shouldFallbackTransport } from './retry-classifier.mjs';
|
|
37
40
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
38
41
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
42
|
+
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
39
43
|
import {
|
|
40
44
|
normalizeContentForOpenAIResponses,
|
|
41
45
|
splitToolContentForOpenAIResponses,
|
|
@@ -52,14 +56,13 @@ const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
|
52
56
|
const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
53
57
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
54
58
|
const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
55
|
-
// Version string baked into the models endpoint query — the OAuth backend
|
|
56
|
-
// request without it
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// on
|
|
61
|
-
//
|
|
62
|
-
// Cached 24h in-process; npm failure falls back to the floor below.
|
|
59
|
+
// Version string baked into the models endpoint query — the OAuth backend
|
|
60
|
+
// rejects the request without it, and gates new model exposures (e.g.
|
|
61
|
+
// gpt-5.5 only on >= 0.130.0) on this client_version header; older versions
|
|
62
|
+
// trigger a visibility-filtered catalog (e.g. only rollout models). Resolved
|
|
63
|
+
// dynamically from npm so newly-shipped models surface within a day instead
|
|
64
|
+
// of waiting on a hardcoded bump here. Cached 24h in-process; npm failure
|
|
65
|
+
// falls back to the floor below.
|
|
63
66
|
const CODEX_CLIENT_VERSION_FLOOR = '0.130.0';
|
|
64
67
|
const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
65
68
|
let _codexVersionCache = { value: null, fetchedAt: 0 };
|
|
@@ -160,6 +163,17 @@ function _displayCodexModel(id) {
|
|
|
160
163
|
return id.replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
function _positiveCodexContextWindow(value) {
|
|
167
|
+
const n = Number(value);
|
|
168
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _codexContextWindowFromApi(m) {
|
|
172
|
+
return _positiveCodexContextWindow(m?.context_window)
|
|
173
|
+
|| _positiveCodexContextWindow(m?.max_context_window)
|
|
174
|
+
|| null;
|
|
175
|
+
}
|
|
176
|
+
|
|
163
177
|
function _normalizeCodexModel(m) {
|
|
164
178
|
const id = m?.slug || m?.id;
|
|
165
179
|
const family = _codexFamily(id);
|
|
@@ -182,8 +196,8 @@ function _normalizeCodexModel(m) {
|
|
|
182
196
|
display: m?.display_name || id,
|
|
183
197
|
family,
|
|
184
198
|
provider: 'openai-oauth',
|
|
185
|
-
contextWindow: m
|
|
186
|
-
maxContextWindow: m?.max_context_window
|
|
199
|
+
contextWindow: _codexContextWindowFromApi(m),
|
|
200
|
+
maxContextWindow: _positiveCodexContextWindow(m?.max_context_window),
|
|
187
201
|
outputTokens: m?.max_output_tokens || m?.output_tokens || 32768,
|
|
188
202
|
autoCompactTokenLimit: m?.auto_compact_token_limit || null,
|
|
189
203
|
effectiveContextWindowPercent: m?.effective_context_window_percent || null,
|
|
@@ -572,7 +586,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
572
586
|
providerState: opts.providerState,
|
|
573
587
|
model,
|
|
574
588
|
});
|
|
575
|
-
// Match the body shape
|
|
589
|
+
// Match the request body shape the OAuth backend expects so the
|
|
576
590
|
// server-side auto-cache routes correctly. text.verbosity / include /
|
|
577
591
|
// tool_choice / parallel_tool_calls are all inert without side effects
|
|
578
592
|
// for most callers but their presence affects how the OAuth backend classifies the
|
|
@@ -603,8 +617,8 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
603
617
|
if (opts.fast === true) {
|
|
604
618
|
// 'priority' is the only fast-class value the OpenAI OAuth backend
|
|
605
619
|
// accepts on the wire: 'fast' is hard-rejected ("Unsupported
|
|
606
|
-
// service_tier: fast", probed 2026-06-11).
|
|
607
|
-
//
|
|
620
|
+
// service_tier: fast", probed 2026-06-11). Only send the request value
|
|
621
|
+
// when the model catalog advertises it.
|
|
608
622
|
if (codexModelSupportsServiceTier(model, 'priority')) {
|
|
609
623
|
body.service_tier = 'priority';
|
|
610
624
|
}
|
|
@@ -654,8 +668,8 @@ function _envPositiveInt(name, fallback) {
|
|
|
654
668
|
}
|
|
655
669
|
|
|
656
670
|
// Completed function_call.arguments parse for the OpenAI Responses stream.
|
|
657
|
-
//
|
|
658
|
-
//
|
|
671
|
+
// A function_call item arrives only on a completion/done signal, so a
|
|
672
|
+
// non-empty-but-malformed
|
|
659
673
|
// arguments string is deterministic bad JSON — NOT mid-stream truncation.
|
|
660
674
|
// Empty/whitespace input legitimately means "no arguments" → {}. A non-empty
|
|
661
675
|
// string that fails JSON.parse is surfaced as an invalid-args MARKER (instead
|
|
@@ -773,11 +787,20 @@ export async function sendViaHttpSse({
|
|
|
773
787
|
useModel,
|
|
774
788
|
fetchFn = fetch,
|
|
775
789
|
} = {}) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
)
|
|
790
|
+
// P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
|
|
791
|
+
// stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
|
|
792
|
+
// killed a healthy, still-streaming turn purely on elapsed time, unlike
|
|
793
|
+
// every other streaming provider path (anthropic-oauth uses the same
|
|
794
|
+
// createPassthroughSignal pattern — see anthropic-oauth.mjs "Option A").
|
|
795
|
+
// The stream is bounded instead by:
|
|
796
|
+
// (a) headerTimeout below (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a
|
|
797
|
+
// socket that never sends the initial response,
|
|
798
|
+
// (b) the SEMANTIC idle watchdog (_armSemanticIdle /
|
|
799
|
+
// PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS), which resets on every
|
|
800
|
+
// meaningful() chunk — a live stream stays alive, a truly silent
|
|
801
|
+
// one still aborts, and
|
|
802
|
+
// (c) externalSignal (client disconnect / replaced-by-newer-request).
|
|
803
|
+
const totalTimeout = createPassthroughSignal(externalSignal);
|
|
781
804
|
const headerTimeout = createTimeoutSignal(
|
|
782
805
|
totalTimeout.signal,
|
|
783
806
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
@@ -851,6 +874,33 @@ export async function sendViaHttpSse({
|
|
|
851
874
|
if (totalTimeout.signal.aborted) _onTotalAbort();
|
|
852
875
|
else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
|
|
853
876
|
}
|
|
877
|
+
// SEMANTIC idle watchdog: reset ONLY on meaningful() (text/reasoning/tool
|
|
878
|
+
// deltas), never on raw bytes/keepalive frames, so a stream that emits some
|
|
879
|
+
// deltas then goes silent trips a short, named terminal failure instead of
|
|
880
|
+
// hanging until the 30-min agent watchdog. Disablable via the shared env.
|
|
881
|
+
let _semanticIdleTimer = null;
|
|
882
|
+
const _clearSemanticIdle = () => {
|
|
883
|
+
if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
|
|
884
|
+
};
|
|
885
|
+
const _armSemanticIdle = () => {
|
|
886
|
+
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED || !(PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS > 0)) return;
|
|
887
|
+
_clearSemanticIdle();
|
|
888
|
+
_semanticIdleTimer = setTimeout(() => {
|
|
889
|
+
_streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
|
|
890
|
+
// Partial-final recovery: attach the
|
|
891
|
+
// streamed partial state so the agent loop can accept a wedged FINAL
|
|
892
|
+
// no-tool summary as a successful partial-final instead of dropping
|
|
893
|
+
// the result. pendingToolUse gates out any mid-flight tool call.
|
|
894
|
+
try {
|
|
895
|
+
_streamAbortReason.partialContent = content;
|
|
896
|
+
_streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
897
|
+
_streamAbortReason.pendingToolUse = pendingCalls.size > 0 || emittedToolCallIds.size > 0;
|
|
898
|
+
_streamAbortReason.partialModel = model || undefined;
|
|
899
|
+
} catch { /* best-effort enrichment */ }
|
|
900
|
+
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
901
|
+
}, PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS);
|
|
902
|
+
try { _semanticIdleTimer.unref?.(); } catch {}
|
|
903
|
+
};
|
|
854
904
|
let buffer = '';
|
|
855
905
|
let content = '';
|
|
856
906
|
let model = '';
|
|
@@ -885,13 +935,75 @@ export async function sendViaHttpSse({
|
|
|
885
935
|
// first complete frame still emits; only redundant re-emits are
|
|
886
936
|
// suppressed.
|
|
887
937
|
const emittedToolCallIds = new Set();
|
|
938
|
+
// Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
|
|
939
|
+
// identical native function_call must fire onToolCall exactly once.
|
|
940
|
+
const _toolDedupe = createToolCallDedupe();
|
|
888
941
|
const emitToolCall = (call) => {
|
|
889
942
|
if (!call || !call.id) return;
|
|
890
943
|
if (emittedToolCallIds.has(call.id)) return;
|
|
891
944
|
emittedToolCallIds.add(call.id);
|
|
945
|
+
if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
|
|
892
946
|
try { onToolCall?.(call); } catch {}
|
|
893
947
|
};
|
|
894
948
|
|
|
949
|
+
// Leaked tool-call guard. The model sometimes emits a tool call as plain
|
|
950
|
+
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
951
|
+
// `<|channel|>...to=functions.NAME...<|call|>`) inside
|
|
952
|
+
// `response.output_text.delta` instead of a native function_call. Route
|
|
953
|
+
// text through the guard so leaked calls are suppressed from the visible
|
|
954
|
+
// stream, synthesized (native `call_...` id shape), and dispatched like
|
|
955
|
+
// native ones. Known tool names come from the request body so recovery
|
|
956
|
+
// only fires for tools the model was actually offered. Additive: the
|
|
957
|
+
// native function_call path is untouched.
|
|
958
|
+
const _leakKnownTools = new Set(
|
|
959
|
+
(Array.isArray(body?.tools) ? body.tools : [])
|
|
960
|
+
.map((t) => (typeof t?.name === 'string' ? t.name : null))
|
|
961
|
+
.filter(Boolean),
|
|
962
|
+
);
|
|
963
|
+
const leakGuard = createLeakGuard({ knownToolNames: _leakKnownTools, harmony: true });
|
|
964
|
+
const dispatchLeakedCall = (recovered) => {
|
|
965
|
+
let args = recovered?.arguments;
|
|
966
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
967
|
+
const call = {
|
|
968
|
+
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
969
|
+
name: recovered.name,
|
|
970
|
+
arguments: args,
|
|
971
|
+
};
|
|
972
|
+
toolCalls.push(call);
|
|
973
|
+
emitToolCall(call);
|
|
974
|
+
};
|
|
975
|
+
const relayLeakText = (delta) => {
|
|
976
|
+
if (!leakGuard.enabled) {
|
|
977
|
+
content += delta || '';
|
|
978
|
+
if (delta && onTextDelta) {
|
|
979
|
+
emittedText = true;
|
|
980
|
+
try { onTextDelta(delta); } catch {}
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const { text, calls } = leakGuard.push(delta);
|
|
985
|
+
if (text) {
|
|
986
|
+
content += text;
|
|
987
|
+
if (onTextDelta) {
|
|
988
|
+
emittedText = true;
|
|
989
|
+
try { onTextDelta(text); } catch {}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
993
|
+
};
|
|
994
|
+
const flushLeak = () => {
|
|
995
|
+
if (!leakGuard.enabled) return;
|
|
996
|
+
const { text, calls } = leakGuard.flush();
|
|
997
|
+
if (text) {
|
|
998
|
+
content += text;
|
|
999
|
+
if (onTextDelta) {
|
|
1000
|
+
emittedText = true;
|
|
1001
|
+
try { onTextDelta(text); } catch {}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1005
|
+
};
|
|
1006
|
+
|
|
895
1007
|
const pushWebSearchCall = (item) => {
|
|
896
1008
|
if (!item || item.type !== 'web_search_call') return;
|
|
897
1009
|
const key = item.id || JSON.stringify(item.action || item);
|
|
@@ -939,6 +1051,7 @@ export async function sendViaHttpSse({
|
|
|
939
1051
|
};
|
|
940
1052
|
const meaningful = () => {
|
|
941
1053
|
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
1054
|
+
_armSemanticIdle();
|
|
942
1055
|
try { onStreamDelta?.(); } catch {}
|
|
943
1056
|
};
|
|
944
1057
|
const handleEvent = (event) => {
|
|
@@ -949,12 +1062,8 @@ export async function sendViaHttpSse({
|
|
|
949
1062
|
if (event.response?.id) responseId = event.response.id;
|
|
950
1063
|
break;
|
|
951
1064
|
case 'response.output_text.delta':
|
|
952
|
-
content += event.delta || '';
|
|
953
1065
|
meaningful();
|
|
954
|
-
|
|
955
|
-
emittedText = true;
|
|
956
|
-
try { onTextDelta(event.delta); } catch {}
|
|
957
|
-
}
|
|
1066
|
+
relayLeakText(event.delta || '');
|
|
958
1067
|
break;
|
|
959
1068
|
case 'response.reasoning_text.delta':
|
|
960
1069
|
case 'response.reasoning_summary_text.delta':
|
|
@@ -966,6 +1075,22 @@ export async function sendViaHttpSse({
|
|
|
966
1075
|
name: event.item.name || '',
|
|
967
1076
|
callId: event.item.call_id || '',
|
|
968
1077
|
});
|
|
1078
|
+
} else if (event.item?.type === 'tool_search_call') {
|
|
1079
|
+
// Mark tool_search as in-flight the moment the item is
|
|
1080
|
+
// added, mirroring function_call above, so the semantic
|
|
1081
|
+
// idle watchdog's pendingToolUse gate (pendingCalls.size)
|
|
1082
|
+
// sees a mid-flight tool_search and never lets stall
|
|
1083
|
+
// recovery drop it before response.output_item.done.
|
|
1084
|
+
// kind:'tool_search' tags the entry so the shared
|
|
1085
|
+
// function_call_arguments.done handler (below) never
|
|
1086
|
+
// mistakes it for a function call by id collision/empty id.
|
|
1087
|
+
if (event.item.id) {
|
|
1088
|
+
pendingCalls.set(event.item.id, {
|
|
1089
|
+
name: 'tool_search',
|
|
1090
|
+
callId: event.item.call_id || '',
|
|
1091
|
+
kind: 'tool_search',
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
969
1094
|
}
|
|
970
1095
|
break;
|
|
971
1096
|
case 'response.function_call_arguments.delta':
|
|
@@ -974,6 +1099,7 @@ export async function sendViaHttpSse({
|
|
|
974
1099
|
case 'response.function_call_arguments.done': {
|
|
975
1100
|
const itemId = event.item_id || '';
|
|
976
1101
|
const pending = pendingCalls.get(itemId);
|
|
1102
|
+
if (pending?.kind === 'tool_search') { meaningful(); break; }
|
|
977
1103
|
const call = {
|
|
978
1104
|
id: pending?.callId || event.call_id || '',
|
|
979
1105
|
name: pending?.name || event.name || '',
|
|
@@ -1006,6 +1132,7 @@ export async function sendViaHttpSse({
|
|
|
1006
1132
|
}
|
|
1007
1133
|
}
|
|
1008
1134
|
} else if (item.type === 'tool_search_call') {
|
|
1135
|
+
pendingCalls.delete(item.id || '');
|
|
1009
1136
|
pushToolSearchCall(item);
|
|
1010
1137
|
} else if (item.type === 'custom_tool_call') {
|
|
1011
1138
|
pushCustomToolCall(item);
|
|
@@ -1030,7 +1157,20 @@ export async function sendViaHttpSse({
|
|
|
1030
1157
|
for (const item of resp.output || []) {
|
|
1031
1158
|
if (item.type === 'message') {
|
|
1032
1159
|
for (const part of item.content || []) {
|
|
1033
|
-
if (!content && part.type === 'output_text')
|
|
1160
|
+
if (!content && part.type === 'output_text') {
|
|
1161
|
+
// Completed-output fallback (no streamed text).
|
|
1162
|
+
// Route through the leak guard so a tool call
|
|
1163
|
+
// leaked only in the final bundle is recovered
|
|
1164
|
+
// rather than surfaced as visible content. push
|
|
1165
|
+
// with final=true flushes fully (no held tail).
|
|
1166
|
+
if (leakGuard.enabled) {
|
|
1167
|
+
const { text, calls } = leakGuard.push(part.text || '', true);
|
|
1168
|
+
content += text;
|
|
1169
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1170
|
+
} else {
|
|
1171
|
+
content += part.text || '';
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1034
1174
|
if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
|
|
1035
1175
|
}
|
|
1036
1176
|
} else if (item.type === 'reasoning') {
|
|
@@ -1116,10 +1256,12 @@ export async function sendViaHttpSse({
|
|
|
1116
1256
|
|
|
1117
1257
|
try {
|
|
1118
1258
|
while (true) {
|
|
1119
|
-
if (totalTimeout.signal
|
|
1259
|
+
if (totalTimeout.signal?.aborted) {
|
|
1260
|
+
_clearSemanticIdle();
|
|
1120
1261
|
const reason = totalTimeout.signal.reason;
|
|
1121
1262
|
throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
|
|
1122
1263
|
}
|
|
1264
|
+
if (_streamAbortReason) throw _streamAbortReason;
|
|
1123
1265
|
const { value, done } = await reader.read();
|
|
1124
1266
|
if (done) break;
|
|
1125
1267
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -1140,12 +1282,16 @@ export async function sendViaHttpSse({
|
|
|
1140
1282
|
const event = _parseSseFrame(frame);
|
|
1141
1283
|
if (event) handleEvent(event);
|
|
1142
1284
|
}
|
|
1285
|
+
// Flush any partial-sentinel tail held back mid-stream so legitimate
|
|
1286
|
+
// trailing text is never lost (streamed-text path).
|
|
1287
|
+
flushLeak();
|
|
1143
1288
|
} catch (err) {
|
|
1144
1289
|
// Live-text invariant: once a non-empty chunk has been relayed it
|
|
1145
1290
|
// cannot be withdrawn — flag the error so no upstream layer retries.
|
|
1146
1291
|
if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
|
|
1147
1292
|
throw err;
|
|
1148
1293
|
} finally {
|
|
1294
|
+
_clearSemanticIdle();
|
|
1149
1295
|
try { reader.releaseLock?.(); } catch {}
|
|
1150
1296
|
if (_onTotalAbort && totalTimeout.signal) {
|
|
1151
1297
|
try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
|
|
@@ -1186,15 +1332,27 @@ export async function sendViaHttpSse({
|
|
|
1186
1332
|
serviceTier,
|
|
1187
1333
|
});
|
|
1188
1334
|
}
|
|
1335
|
+
// Dedupe the returned array by name+args (Fix 2, array side): a synthetic
|
|
1336
|
+
// leaked call and an identical native function_call must not both survive,
|
|
1337
|
+
// else the agent loop executes the side-effecting tool twice.
|
|
1338
|
+
const _returnedToolCalls = toolCalls.length
|
|
1339
|
+
? dedupeToolCallList(toolCalls.map(({ _pendingItemId, ...t }) => t))
|
|
1340
|
+
: undefined;
|
|
1189
1341
|
return {
|
|
1190
1342
|
content,
|
|
1191
1343
|
model: liveModel,
|
|
1192
1344
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1193
|
-
toolCalls:
|
|
1345
|
+
toolCalls: _returnedToolCalls,
|
|
1194
1346
|
citations: citations.length ? citations : undefined,
|
|
1195
1347
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
1196
1348
|
usage: usage || undefined,
|
|
1197
1349
|
stopReason: stopReason || undefined,
|
|
1350
|
+
// P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
|
|
1351
|
+
// fallback maps status:'incomplete'/reason=max_output_tokens to
|
|
1352
|
+
// stopReason='length' above and treats it as success). Flag it so
|
|
1353
|
+
// loop.mjs can surface a truncation warning instead of accepting
|
|
1354
|
+
// silently-cut content as a clean final answer.
|
|
1355
|
+
...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
|
|
1198
1356
|
responseId: responseId || undefined,
|
|
1199
1357
|
serviceTier: serviceTier || undefined,
|
|
1200
1358
|
};
|
|
@@ -1328,8 +1486,8 @@ export class OpenAIOAuthProvider {
|
|
|
1328
1486
|
const onTextDelta = typeof opts.onTextDelta === 'function' ? opts.onTextDelta : null;
|
|
1329
1487
|
const externalSignal = opts.signal || null;
|
|
1330
1488
|
const _sendSessionId = opts.sessionId || '(none)';
|
|
1331
|
-
const
|
|
1332
|
-
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)}
|
|
1489
|
+
const _sendAgent = opts.agent || '(none)';
|
|
1490
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} agent=${_sendAgent} expiringInMs=${this.tokens?.expires_at ? this.tokens.expires_at - Date.now() : 'unknown'}\n`); }
|
|
1333
1491
|
// Build request body in parallel with auth resolution. ensureAuth is
|
|
1334
1492
|
// a no-op fast-path on cached tokens, but a refresh round-trip can
|
|
1335
1493
|
// take 300ms+; the body build (message serialisation) overlaps cleanly.
|
|
@@ -1389,6 +1547,26 @@ export class OpenAIOAuthProvider {
|
|
|
1389
1547
|
this._forceHttpFallback = true;
|
|
1390
1548
|
this._forceHttpFallbackUntil = Date.now() + ttlMs;
|
|
1391
1549
|
};
|
|
1550
|
+
const traceWsError = (err, stage = 'primary') => {
|
|
1551
|
+
try {
|
|
1552
|
+
appendAgentTrace({
|
|
1553
|
+
sessionId: poolKey,
|
|
1554
|
+
iteration,
|
|
1555
|
+
kind: 'transport_error',
|
|
1556
|
+
provider: 'openai-oauth',
|
|
1557
|
+
model: useModel,
|
|
1558
|
+
transport: 'websocket',
|
|
1559
|
+
payload: {
|
|
1560
|
+
stage,
|
|
1561
|
+
error_code: err?.code || null,
|
|
1562
|
+
error_http_status: Number(err?.httpStatus || 0) || null,
|
|
1563
|
+
error_classifier: err?.retryClassifier || err?.midstreamClassifier || null,
|
|
1564
|
+
live_text_emitted: err?.liveTextEmitted === true || err?.unsafeToRetry === true,
|
|
1565
|
+
message: String(err?.message || err || '').slice(0, 500),
|
|
1566
|
+
},
|
|
1567
|
+
});
|
|
1568
|
+
} catch {}
|
|
1569
|
+
};
|
|
1392
1570
|
const dispatchHttp = async (reason, originalErr = null, { sticky = false } = {}) => {
|
|
1393
1571
|
appendAgentTrace({
|
|
1394
1572
|
sessionId: poolKey,
|
|
@@ -1411,7 +1589,7 @@ export class OpenAIOAuthProvider {
|
|
|
1411
1589
|
process.stderr.write('[openai-oauth] WebSocket bypassed (forced); using HTTP/SSE\n');
|
|
1412
1590
|
}
|
|
1413
1591
|
} else {
|
|
1414
|
-
process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1592
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1415
1593
|
}
|
|
1416
1594
|
const result = await sendHttp({
|
|
1417
1595
|
auth,
|
|
@@ -1459,11 +1637,12 @@ export class OpenAIOAuthProvider {
|
|
|
1459
1637
|
// Prefer WebSocket for hot cache/delta transport; fall back to HTTP/SSE
|
|
1460
1638
|
// after retry-exhausted handshake/acquire/no-first-event failures.
|
|
1461
1639
|
try {
|
|
1462
|
-
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel}
|
|
1640
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel} agent=${_sendAgent} sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} iteration=${iteration ?? '(none)'}\n`); }
|
|
1463
1641
|
const result = await dispatchWs(false);
|
|
1464
1642
|
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1465
1643
|
return recordLiveModel(result);
|
|
1466
1644
|
} catch (err) {
|
|
1645
|
+
traceWsError(err, 'primary');
|
|
1467
1646
|
const status = err?.httpStatus;
|
|
1468
1647
|
// Live-text invariant: if the WS attempt already relayed a
|
|
1469
1648
|
// non-empty text chunk to the client, NO recovery path may reissue
|
|
@@ -1482,6 +1661,7 @@ export class OpenAIOAuthProvider {
|
|
|
1482
1661
|
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1483
1662
|
return recordLiveModel(result);
|
|
1484
1663
|
} catch (retryErr) {
|
|
1664
|
+
traceWsError(retryErr, 'auth_retry');
|
|
1485
1665
|
if (_shouldUseOpenAIHttpFallback(retryErr, externalSignal)) {
|
|
1486
1666
|
try {
|
|
1487
1667
|
return await dispatchHttp(
|
|
@@ -71,6 +71,24 @@ export class OpenAIDirectProvider {
|
|
|
71
71
|
// so gpt-5.4-mini can opt into Priority even when the OAuth catalog does
|
|
72
72
|
// not advertise a Fast tier for its OAuth endpoint.
|
|
73
73
|
applyOpenAIDirectFastTier(body, useModel, opts);
|
|
74
|
+
// P0 audit fix: buildRequestBody (openai-oauth.mjs) defaults
|
|
75
|
+
// store:false (env-gated, MIXDOG_OAI_STORE), which is correct for
|
|
76
|
+
// the openai-oauth ChatGPT-subscription backend — that backend keeps
|
|
77
|
+
// its own conversation state via the WS handshake session_id
|
|
78
|
+
// (see openai-oauth-ws.mjs "conversation slot ... in-memory prefix
|
|
79
|
+
// state"), independent of the public Responses API `store` field.
|
|
80
|
+
// The public OpenAI direct WS path below, however, talks to the real
|
|
81
|
+
// api.openai.com Responses API, where `previous_response_id`
|
|
82
|
+
// continuation is only valid when the anchored response was actually
|
|
83
|
+
// stored — store:false + previous_response_id is a broken
|
|
84
|
+
// combination there (the server has nothing to look up). This
|
|
85
|
+
// provider's WS transport always injects previous_response_id via
|
|
86
|
+
// openai-oauth-ws.mjs's delta path once a response id is cached, so
|
|
87
|
+
// force store:true here — same override xAI's Responses path takes
|
|
88
|
+
// (see openai-compat.mjs _doSendXaiResponses/_doSendXaiResponsesWebSocket:
|
|
89
|
+
// "the public endpoint currently returns previous_response_not_found
|
|
90
|
+
// ... unless the chain is stored").
|
|
91
|
+
body.store = true;
|
|
74
92
|
// Public Responses API supports prompt_cache_retention='24h' at no
|
|
75
93
|
// extra cost (same cached_input_tokens billing as the default 5–10
|
|
76
94
|
// min in-memory cache). openai-oauth rejects the parameter, so it's
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
2
|
-
import * as fsp from 'node:fs/promises';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
3
|
+
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
4
4
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
5
5
|
import { getOpenCodeGoAuthCookie } from '../../../shared/config.mjs';
|
|
6
|
+
import { round, cleanString as clean } from './lib/usage-primitives.mjs';
|
|
6
7
|
|
|
7
8
|
const CACHE_FILE = 'opencode-go-usage-cache.json';
|
|
8
9
|
const LIVE_TTL_MS = 5 * 60_000;
|
|
@@ -18,23 +19,14 @@ const LIMITS_USD = Object.freeze({
|
|
|
18
19
|
const DISK_JSON_MEMORY_TTL_MS = 1000;
|
|
19
20
|
let diskJsonCache = { at: 0, file: '', value: null };
|
|
20
21
|
|
|
22
|
+
// Local unguarded `num`: this module intentionally coerces '' to 0 via
|
|
23
|
+
// Number(''), unlike the guarded shared num() in lib/usage-primitives.mjs.
|
|
24
|
+
// Behavior differs on empty-string input, so it stays local.
|
|
21
25
|
function num(value, fallback = null) {
|
|
22
26
|
const n = Number(value);
|
|
23
27
|
return Number.isFinite(n) ? n : fallback;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
function round(value, digits = 4) {
|
|
27
|
-
const n = Number(value);
|
|
28
|
-
if (!Number.isFinite(n)) return null;
|
|
29
|
-
const scale = 10 ** digits;
|
|
30
|
-
return Math.round(n * scale) / scale;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function clean(value) {
|
|
34
|
-
const text = typeof value === 'string' ? value.trim() : '';
|
|
35
|
-
return text || null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
30
|
function cachePath() {
|
|
39
31
|
return join(resolvePluginData(), CACHE_FILE);
|
|
40
32
|
}
|
|
@@ -53,12 +45,18 @@ function readJson(file) {
|
|
|
53
45
|
}
|
|
54
46
|
}
|
|
55
47
|
|
|
48
|
+
// Synchronous atomic+lock write (updateJsonAtomicSync) instead of the prior
|
|
49
|
+
// fire-and-forget fsp.writeFile: this cache is single-entry (one snapshot
|
|
50
|
+
// per file, no cross-process merge), so the lock protects against a torn
|
|
51
|
+
// write racing readers, not a lost-update merge. Only one write happens
|
|
52
|
+
// per successful fetch (TTL-gated, at most once per LIVE_TTL_MS), so the
|
|
53
|
+
// switch off async has no meaningful latency impact on the request path.
|
|
56
54
|
function writeJson(file, value) {
|
|
57
|
-
|
|
55
|
+
let next = null;
|
|
58
56
|
try {
|
|
59
|
-
|
|
60
|
-
void fsp.writeFile(file, JSON.stringify(value, null, 2), 'utf8').catch(() => {});
|
|
57
|
+
next = updateJsonAtomicSync(file, () => value, { lock: true, fsyncDir: true, timeoutMs: 1000 }); // best-effort cache write: short lock timeout, don't block on contention
|
|
61
58
|
} catch {}
|
|
59
|
+
if (next) diskJsonCache = { at: Date.now(), file, value: next }; // only mirror on confirmed write, avoid phantom cache on lock timeout
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
function freshSnapshot(snapshot, ttlMs) {
|
|
@@ -243,7 +241,41 @@ export function openCodeGoUsageConfigStatus(config = {}) {
|
|
|
243
241
|
};
|
|
244
242
|
}
|
|
245
243
|
|
|
244
|
+
// Primary discovery: GET /auth with the auth cookie, follow-manual. The
|
|
245
|
+
// console redirects authenticated sessions straight to /workspace/{id};
|
|
246
|
+
// unauthenticated/invalid cookies redirect to /auth/authorize instead.
|
|
247
|
+
// This avoids depending on the hashed server-fn id used by the /_server
|
|
248
|
+
// probe (WORKSPACES_SERVER_ID), which can change across console deploys.
|
|
249
|
+
async function fetchWorkspaceIdFromAuthRedirect(authCookie, { signal } = {}) {
|
|
250
|
+
let res;
|
|
251
|
+
try {
|
|
252
|
+
res = await fetch(`${BASE_URL}/auth`, {
|
|
253
|
+
signal,
|
|
254
|
+
redirect: 'manual',
|
|
255
|
+
headers: requestHeaders(authCookie),
|
|
256
|
+
});
|
|
257
|
+
} catch {
|
|
258
|
+
return null; // network/redirect-mode quirk: let the _server fallback decide
|
|
259
|
+
}
|
|
260
|
+
if (res.status === 401 || res.status === 403) {
|
|
261
|
+
const err = new Error('OpenCode Go console auth failed');
|
|
262
|
+
err.code = 'OPENCODE_GO_USAGE_AUTH_FAILED';
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
const location = res.headers.get('location') || '';
|
|
266
|
+
if (!location) return null;
|
|
267
|
+
if (/(?:^|\/|\.\/)auth\/authorize\b/.test(location)) {
|
|
268
|
+
const err = new Error('OpenCode Go console auth failed');
|
|
269
|
+
err.code = 'OPENCODE_GO_USAGE_AUTH_FAILED';
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
const workspaceMatch = location.match(/\/workspace\/(wrk_[a-zA-Z0-9]+)/);
|
|
273
|
+
return normalizeWorkspaceId(workspaceMatch ? workspaceMatch[1] : location);
|
|
274
|
+
}
|
|
275
|
+
|
|
246
276
|
async function fetchWorkspaceId(authCookie, { signal } = {}) {
|
|
277
|
+
const fromRedirect = await fetchWorkspaceIdFromAuthRedirect(authCookie, { signal });
|
|
278
|
+
if (fromRedirect) return fromRedirect;
|
|
247
279
|
const url = new URL(`${BASE_URL}/_server`);
|
|
248
280
|
url.searchParams.set('id', WORKSPACES_SERVER_ID);
|
|
249
281
|
const res = await fetch(url, {
|
|
@@ -201,7 +201,8 @@ export function getProvider(name) {
|
|
|
201
201
|
// unregistered providers default to false (the openai/gemini majority).
|
|
202
202
|
export function providerInputExcludesCache(name) {
|
|
203
203
|
const p = getProvider(name);
|
|
204
|
-
|
|
204
|
+
if (p?.constructor?.inputExcludesCache === true) return true;
|
|
205
|
+
return String(name || '').toLowerCase().includes('anthropic');
|
|
205
206
|
}
|
|
206
207
|
export function getAllProviders() {
|
|
207
208
|
// Defensive copy — callers must not mutate the live registry or retain
|