mixdog 0.9.2 → 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 +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- 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 +169 -918
- 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 +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- 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/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- 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.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- 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/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- 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-cycle2.mjs +56 -3
- 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 +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- 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/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/transcript-writer.mjs +29 -2
- 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 +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- 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 +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -6,6 +6,7 @@ import { join } from 'path';
|
|
|
6
6
|
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
7
7
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
8
8
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
9
|
+
import { num, round, cleanString } from './lib/usage-primitives.mjs';
|
|
9
10
|
|
|
10
11
|
const CACHE_FILE = 'gateway-oauth-usage-cache.json';
|
|
11
12
|
const LIVE_CACHE_TTL_MS = 60_000;
|
|
@@ -21,24 +22,6 @@ const lastWarnAt = new Map();
|
|
|
21
22
|
let pendingDiskSnapshots = new Map();
|
|
22
23
|
let pendingDiskFlushTimer = null;
|
|
23
24
|
|
|
24
|
-
function num(value, fallback = null) {
|
|
25
|
-
if (value === null || value === undefined || value === '') return fallback;
|
|
26
|
-
const n = Number(value);
|
|
27
|
-
return Number.isFinite(n) ? n : fallback;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function round(value, digits = 4) {
|
|
31
|
-
const n = Number(value);
|
|
32
|
-
if (!Number.isFinite(n)) return null;
|
|
33
|
-
const scale = 10 ** digits;
|
|
34
|
-
return Math.round(n * scale) / scale;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function cleanString(value) {
|
|
38
|
-
const s = typeof value === 'string' ? value.trim() : '';
|
|
39
|
-
return s || null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
25
|
function providerKey(routeInfo = {}) {
|
|
43
26
|
return String(routeInfo?.provider || '').toLowerCase();
|
|
44
27
|
}
|
|
@@ -301,7 +284,7 @@ function balanceFromCredits(credits, source) {
|
|
|
301
284
|
}
|
|
302
285
|
|
|
303
286
|
function balanceFromExtraUsage(extra) {
|
|
304
|
-
if (!extra || typeof extra !== 'object'
|
|
287
|
+
if (!extra || typeof extra !== 'object') return null;
|
|
305
288
|
const limit = num(extra.monthly_limit, null);
|
|
306
289
|
const used = num(extra.used_credits, 0);
|
|
307
290
|
if (limit === null) return null;
|
|
@@ -315,6 +298,57 @@ function balanceFromExtraUsage(extra) {
|
|
|
315
298
|
};
|
|
316
299
|
}
|
|
317
300
|
|
|
301
|
+
function balanceFromAnthropicSpend(spend) {
|
|
302
|
+
if (!spend || typeof spend !== 'object') return null;
|
|
303
|
+
const currency = cleanString(spend.used?.currency ?? spend.limit?.currency) || 'USD';
|
|
304
|
+
// spend.balance may be a plain number or a {amount_minor, exponent} money
|
|
305
|
+
// object like used/limit; support both shapes.
|
|
306
|
+
const directBalance = spend.balance && typeof spend.balance === 'object'
|
|
307
|
+
? (() => {
|
|
308
|
+
const minor = num(spend.balance.amount_minor, null);
|
|
309
|
+
return minor === null ? null : minor / (10 ** num(spend.balance.exponent, 2));
|
|
310
|
+
})()
|
|
311
|
+
: num(spend.balance, null);
|
|
312
|
+
if (directBalance !== null) {
|
|
313
|
+
return {
|
|
314
|
+
source: 'anthropic-oauth-spend',
|
|
315
|
+
remainingUsd: round(directBalance, 4),
|
|
316
|
+
spentUsd: round(num(spend.used?.amount_minor, 0) / (10 ** num(spend.used?.exponent, 2)), 4),
|
|
317
|
+
currency,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const usedMinor = num(spend.used?.amount_minor, null);
|
|
322
|
+
const usedExponent = num(spend.used?.exponent, 2);
|
|
323
|
+
const usedUsd = usedMinor === null ? null : usedMinor / (10 ** usedExponent);
|
|
324
|
+
|
|
325
|
+
const capMinor = num(spend.cap?.credits?.amount_minor, null);
|
|
326
|
+
if (capMinor !== null && usedUsd !== null) {
|
|
327
|
+
const capExponent = num(spend.cap?.credits?.exponent, 2);
|
|
328
|
+
const capUsd = capMinor / (10 ** capExponent);
|
|
329
|
+
return {
|
|
330
|
+
source: 'anthropic-oauth-spend',
|
|
331
|
+
remainingUsd: round(Math.max(0, capUsd - usedUsd), 4),
|
|
332
|
+
spentUsd: round(usedUsd, 4),
|
|
333
|
+
currency,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const limitMinor = num(spend.limit?.amount_minor, null);
|
|
338
|
+
if (limitMinor !== null && limitMinor > 0 && usedUsd !== null) {
|
|
339
|
+
const limitExponent = num(spend.limit?.exponent, 2);
|
|
340
|
+
const limitUsd = limitMinor / (10 ** limitExponent);
|
|
341
|
+
return {
|
|
342
|
+
source: 'anthropic-oauth-spend',
|
|
343
|
+
remainingUsd: round(Math.max(0, limitUsd - usedUsd), 4),
|
|
344
|
+
spentUsd: round(usedUsd, 4),
|
|
345
|
+
currency,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
318
352
|
function normalizeOpenAIWhamUsage(data) {
|
|
319
353
|
const rate = data?.rate_limit && typeof data.rate_limit === 'object' ? data.rate_limit : null;
|
|
320
354
|
if (!rate) return null;
|
|
@@ -388,7 +422,7 @@ function normalizeAnthropicUsage(data, source = 'anthropic-oauth') {
|
|
|
388
422
|
provider: 'anthropic-oauth',
|
|
389
423
|
source,
|
|
390
424
|
quotaWindows: windows,
|
|
391
|
-
balance: balanceFromExtraUsage(data.extra_usage),
|
|
425
|
+
balance: balanceFromAnthropicSpend(data.spend) || balanceFromExtraUsage(data.extra_usage),
|
|
392
426
|
rawKeys: Object.keys(data || {}).sort(),
|
|
393
427
|
};
|
|
394
428
|
}
|
|
@@ -31,8 +31,7 @@ function truncatedCompatStreamError(label, detail) {
|
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// Invalid-tool-args marker
|
|
35
|
-
// opencode): completed-but-malformed tool_call arguments JSON must NOT throw
|
|
34
|
+
// Invalid-tool-args marker: completed-but-malformed tool_call arguments JSON must NOT throw
|
|
36
35
|
// (kills the turn) NOR be silently swallowed to `{}`. Instead the parse
|
|
37
36
|
// failure is carried as data on the tool call's `arguments` slot so the
|
|
38
37
|
// dispatch loop can turn it into an is_error tool_result and let the model
|
|
@@ -49,8 +48,7 @@ export function isInvalidToolArgsMarker(value) {
|
|
|
49
48
|
return !!value && typeof value === 'object' && value.__invalidToolArgs === true;
|
|
50
49
|
}
|
|
51
50
|
/** Model-facing tool_result text for a tool call whose arguments failed to
|
|
52
|
-
* parse
|
|
53
|
-
* `failed to parse function arguments` — instructs an in-turn retry. */
|
|
51
|
+
* parse; instructs the model to retry with valid JSON in the same turn. */
|
|
54
52
|
export function formatInvalidToolArgsResult(call) {
|
|
55
53
|
const name = call?.name || 'tool';
|
|
56
54
|
const detail = call?.arguments?.__parseError || 'arguments were not valid JSON';
|
|
@@ -85,9 +83,9 @@ export function parseCompletedToolCallArgumentsJson(raw, label, meta) {
|
|
|
85
83
|
// Invariant: a completion/finish signal was observed for this tool call
|
|
86
84
|
// (finish_reason present, or a per-call/response "done" event fired), so
|
|
87
85
|
// the arguments are NOT mid-stream-truncated — they are complete but
|
|
88
|
-
// malformed.
|
|
89
|
-
//
|
|
90
|
-
//
|
|
86
|
+
// malformed. Return an invalid-args MARKER (not a throw) so the
|
|
87
|
+
// dispatch loop feeds the parse error back to the model as a
|
|
88
|
+
// tool_result and the model self-corrects in the same turn. Only an
|
|
91
89
|
// unfinished stream (no finishReason) stays the retryable truncation
|
|
92
90
|
// case — that transient behavior is deliberately preserved.
|
|
93
91
|
if (meta?.finishReason) {
|
|
@@ -406,7 +404,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
406
404
|
} catch (err) {
|
|
407
405
|
// Any mid-stream failure after live text was relayed is non-retryable.
|
|
408
406
|
if (emittedText) throw markErrorLiveTextEmitted(err);
|
|
409
|
-
// Partial-final recovery
|
|
407
|
+
// Partial-final recovery: on a mid-stream stall, attach the
|
|
410
408
|
// streamed partial state so the loop can accept a wedged FINAL no-tool
|
|
411
409
|
// summary as partial-final success. pendingToolUse gates out any
|
|
412
410
|
// in-flight/emitted tool call.
|
|
@@ -488,12 +486,15 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
488
486
|
if (!item || item.type !== 'tool_search_call') return;
|
|
489
487
|
const callId = item.call_id || item.id || '';
|
|
490
488
|
if (!callId || state.toolCalls.some((call) => call.id === callId)) return;
|
|
489
|
+
const _tsArgs = item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
|
|
490
|
+
? item.arguments
|
|
491
|
+
: parseCompletedToolCallArgumentsJson(item.arguments || '{}', label, { id: callId, name: 'tool_search', finishReason: 'done' });
|
|
491
492
|
const call = {
|
|
492
493
|
id: callId,
|
|
493
494
|
name: 'tool_search',
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
495
|
+
// Schema is a plain object ({query,select,limit}); an array must
|
|
496
|
+
// never pass through as args.
|
|
497
|
+
arguments: (_tsArgs && typeof _tsArgs === 'object' && !Array.isArray(_tsArgs)) ? _tsArgs : {},
|
|
497
498
|
nativeType: 'tool_search_call',
|
|
498
499
|
};
|
|
499
500
|
state.toolCalls.push(call);
|
|
@@ -533,6 +534,12 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
533
534
|
state.toolInFlight = true;
|
|
534
535
|
} else if (event.item?.type === 'custom_tool_call') {
|
|
535
536
|
state.toolInFlight = true;
|
|
537
|
+
} else if (event.item?.type === 'tool_search_call') {
|
|
538
|
+
// Mark tool_search in-flight at item-added time, same as
|
|
539
|
+
// function_call/custom_tool_call above, so the stall-recovery
|
|
540
|
+
// pendingToolUse gate never drops a mid-flight tool_search
|
|
541
|
+
// before response.output_item.done pushes it.
|
|
542
|
+
state.toolInFlight = true;
|
|
536
543
|
}
|
|
537
544
|
try { onStreamDelta?.(); } catch {}
|
|
538
545
|
break;
|
|
@@ -766,7 +773,7 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
766
773
|
}
|
|
767
774
|
flushLeak();
|
|
768
775
|
} catch (err) {
|
|
769
|
-
// Partial-final recovery
|
|
776
|
+
// Partial-final recovery: attach streamed partial state so a
|
|
770
777
|
// wedged FINAL no-tool summary can be accepted as partial-final success.
|
|
771
778
|
if (err?.streamStalled === true) {
|
|
772
779
|
try {
|
|
@@ -1032,12 +1032,15 @@ export function parseResponsesToolCalls(response, label) {
|
|
|
1032
1032
|
const call = customToolCallFromResponseItem(item);
|
|
1033
1033
|
if (call) out.push(call);
|
|
1034
1034
|
} else if (item?.type === 'tool_search_call') {
|
|
1035
|
+
const _tsArgs = item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
|
|
1036
|
+
? item.arguments
|
|
1037
|
+
: parseCompletedToolCallArgumentsJson(item.arguments || '{}', label, { id: item.call_id || item.id, name: 'tool_search', finishReason });
|
|
1035
1038
|
out.push({
|
|
1036
1039
|
id: item.call_id || item.id,
|
|
1037
1040
|
name: 'tool_search',
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
+
// Schema is a plain object ({query,select,limit}); an array
|
|
1042
|
+
// (parsed JSON or passthrough) must never pass through as args.
|
|
1043
|
+
arguments: (_tsArgs && typeof _tsArgs === 'object' && !Array.isArray(_tsArgs)) ? _tsArgs : {},
|
|
1041
1044
|
nativeType: 'tool_search_call',
|
|
1042
1045
|
});
|
|
1043
1046
|
}
|
|
@@ -1284,8 +1287,7 @@ export class OpenAICompatProvider {
|
|
|
1284
1287
|
const opts = sendOpts || {};
|
|
1285
1288
|
// Re-warm a kept-alive socket to the provider origin before the turn so
|
|
1286
1289
|
// the request hot path lands on a live socket instead of paying a cold
|
|
1287
|
-
// TLS handshake after an idle gap. Fire-and-forget; never awaited.
|
|
1288
|
-
// mirrors anthropic-oauth's send()-start preconnect.
|
|
1290
|
+
// TLS handshake after an idle gap. Fire-and-forget; never awaited.
|
|
1289
1291
|
preconnect(this.baseURL);
|
|
1290
1292
|
if (this.name === 'xai' && useXaiResponsesApi(opts, this.config)) {
|
|
1291
1293
|
if (useXaiResponsesWebSocket(opts, this.config)) {
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* `input` delta plus `previous_response_id`, skipping the full
|
|
10
10
|
* tools/system/history prefix each turn.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
12
|
+
* Incremental-input reuse is decided by diffing against the cached request
|
|
13
|
+
* the socket last sent (see _sansInput below), and requests carry a
|
|
14
|
+
* turn-state echo header so the backend can correlate WS frames to the
|
|
15
|
+
* in-flight turn.
|
|
16
16
|
*
|
|
17
17
|
* Exposes:
|
|
18
18
|
* sendViaWebSocket({ auth, body, sendOpts, onStreamDelta, onToolCall,
|
|
@@ -83,9 +83,8 @@ const WS_PRE_RESPONSE_CREATED_MS = (() => {
|
|
|
83
83
|
if (Number.isFinite(n) && n > 0) return Math.min(Math.max(n, 1_000), 120_000);
|
|
84
84
|
return 10_000;
|
|
85
85
|
})();
|
|
86
|
-
// Single inter-chunk idle timer. Resets on EVERY received frame
|
|
87
|
-
//
|
|
88
|
-
// anthropic-oauth resetIdleTimer on every chunk).
|
|
86
|
+
// Single inter-chunk idle timer. Resets on EVERY received frame — any frame,
|
|
87
|
+
// including metadata/keepalive, proves the socket is live.
|
|
89
88
|
const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
|
|
90
89
|
// Mid-stream retry budgets + backoff now live in the shared MIDSTREAM_RETRY_POLICY
|
|
91
90
|
// table (retry-classifier.mjs). These aliases keep the local call sites readable
|
|
@@ -109,8 +108,8 @@ const WS_MIDSTREAM_POLICY = {
|
|
|
109
108
|
// decision and just double the user-visible latency.
|
|
110
109
|
// Aligned to the cross-provider default (retry-classifier DEFAULT_MAX_ATTEMPTS=5,
|
|
111
110
|
// anthropic-oauth MAX_ATTEMPTS=5, withRetry-using providers all default to 5).
|
|
112
|
-
//
|
|
113
|
-
//
|
|
111
|
+
// Bumped from 3 so this provider exhausts the same number of transient-5xx
|
|
112
|
+
// attempts as the others before surfacing failure to the caller.
|
|
114
113
|
const HANDSHAKE_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS;
|
|
115
114
|
const HANDSHAKE_BACKOFF_BASE_MS = 500;
|
|
116
115
|
const HANDSHAKE_BACKOFF_CAP_MS = 5000;
|
|
@@ -942,9 +941,9 @@ function releaseWebSocket({ entry, poolKey, keep }) {
|
|
|
942
941
|
_scheduleIdleClose(poolKey, entry);
|
|
943
942
|
}
|
|
944
943
|
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
944
|
+
// If the cached request (sans input) matches the current one and the current
|
|
945
|
+
// input starts with the cached input, return only the tail. Otherwise return
|
|
946
|
+
// the full input (fresh turn).
|
|
948
947
|
function _sansInput(body) {
|
|
949
948
|
const { input: _ignored, previous_response_id: _prevIgnored, ...rest } = body;
|
|
950
949
|
return rest;
|
|
@@ -1214,19 +1213,23 @@ function _wsErrLabel(p) {
|
|
|
1214
1213
|
return 'OpenAI OAuth WS';
|
|
1215
1214
|
}
|
|
1216
1215
|
// tool_search_call.arguments parse. Module-scope (exported) for direct test
|
|
1217
|
-
// coverage.
|
|
1218
|
-
//
|
|
1216
|
+
// coverage. Same policy as the function_call_arguments.done path and
|
|
1217
|
+
// openai-oauth _parseJsonObject —
|
|
1219
1218
|
// object passes through; null/non-string/empty/whitespace → {} (no args); a
|
|
1220
1219
|
// non-empty string that fails JSON.parse is deterministic bad JSON, surfaced
|
|
1221
1220
|
// as an invalid-args MARKER (not silently swallowed to {}) so the dispatch
|
|
1222
1221
|
// loop returns an is_error tool_result and the model self-corrects in the same
|
|
1223
1222
|
// turn.
|
|
1224
1223
|
export function parseToolSearchArgs(value) {
|
|
1225
|
-
if (value && typeof value === 'object')
|
|
1224
|
+
if (value && typeof value === 'object') {
|
|
1225
|
+
// Reject arrays — the tool_search schema is an object
|
|
1226
|
+
// ({query,select,limit}); an array must never pass through as args.
|
|
1227
|
+
return Array.isArray(value) ? {} : value;
|
|
1228
|
+
}
|
|
1226
1229
|
if (typeof value !== 'string' || !value.trim()) return {};
|
|
1227
1230
|
try {
|
|
1228
1231
|
const parsed = JSON.parse(value);
|
|
1229
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
1232
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
1230
1233
|
} catch (err) {
|
|
1231
1234
|
return makeInvalidToolArgsMarker(value, err instanceof Error ? err.message : String(err));
|
|
1232
1235
|
}
|
|
@@ -1456,7 +1459,7 @@ export async function _streamResponse({
|
|
|
1456
1459
|
let messageHandler = null;
|
|
1457
1460
|
let closeHandler = null;
|
|
1458
1461
|
let errorHandler = null;
|
|
1459
|
-
// SEMANTIC idle timer
|
|
1462
|
+
// SEMANTIC idle timer: distinct from the inter-chunk
|
|
1460
1463
|
// timer, which resets on EVERY frame (rate_limits/metadata/keepalive keep
|
|
1461
1464
|
// the socket "alive"). This timer resets ONLY on meaningful output deltas
|
|
1462
1465
|
// (text/reasoning/tool args — the same events that call onStreamDelta) so a
|
|
@@ -1561,7 +1564,7 @@ export async function _streamResponse({
|
|
|
1561
1564
|
semanticIdleTimer = setTimeout(() => {
|
|
1562
1565
|
traceWsTimeout('semantic_idle_timeout', semanticIdleMs);
|
|
1563
1566
|
terminalError = streamStalledError('Responses WS', semanticIdleMs, { emittedToolCall: !!midState?.emittedToolCall });
|
|
1564
|
-
// Partial-final recovery
|
|
1567
|
+
// Partial-final recovery: attach streamed partial state so
|
|
1565
1568
|
// a wedged FINAL no-tool summary can be accepted as partial-final
|
|
1566
1569
|
// success by the loop. pendingToolUse gates out mid-flight tools.
|
|
1567
1570
|
try {
|
|
@@ -1633,7 +1636,7 @@ export async function _streamResponse({
|
|
|
1633
1636
|
messageHandler = (data) => {
|
|
1634
1637
|
resetIdle();
|
|
1635
1638
|
// resetIdle() above resets the SINGLE inter-chunk idle timer on
|
|
1636
|
-
// EVERY received frame
|
|
1639
|
+
// EVERY received frame — response.created, metadata,
|
|
1637
1640
|
// rate_limits, and all deltas keep the socket alive. Separately, do
|
|
1638
1641
|
// NOT call onStreamDelta for every frame — metadata/keepalive frames
|
|
1639
1642
|
// must not reset the agent stall watchdog's lastStreamDeltaAt. Only
|
|
@@ -1706,6 +1709,13 @@ export async function _streamResponse({
|
|
|
1706
1709
|
_toolInFlight = true;
|
|
1707
1710
|
} else if (event.item?.type === 'custom_tool_call') {
|
|
1708
1711
|
_toolInFlight = true;
|
|
1712
|
+
} else if (event.item?.type === 'tool_search_call') {
|
|
1713
|
+
// Mark tool_search in-flight at item-added time, same
|
|
1714
|
+
// as function_call/custom_tool_call above, so the
|
|
1715
|
+
// semantic-idle stall gate's pendingToolUse never
|
|
1716
|
+
// drops a mid-flight tool_search before
|
|
1717
|
+
// response.output_item.done.
|
|
1718
|
+
_toolInFlight = true;
|
|
1709
1719
|
}
|
|
1710
1720
|
break;
|
|
1711
1721
|
case 'response.function_call_arguments.delta':
|
|
@@ -1723,10 +1733,10 @@ export async function _streamResponse({
|
|
|
1723
1733
|
const pending = pendingCalls.get(itemId);
|
|
1724
1734
|
// function_call_arguments.done is a completion signal:
|
|
1725
1735
|
// empty/whitespace → no args ({}); a non-empty string that
|
|
1726
|
-
// fails JSON.parse is deterministic bad JSON.
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1736
|
+
// fails JSON.parse is deterministic bad JSON. Surface an
|
|
1737
|
+
// invalid-args MARKER (not silent {}) so the dispatch loop
|
|
1738
|
+
// returns an is_error tool_result and the model re-issues
|
|
1739
|
+
// valid JSON in the same turn.
|
|
1730
1740
|
let args = {};
|
|
1731
1741
|
{
|
|
1732
1742
|
const _argText = typeof event.arguments === 'string' ? event.arguments : '';
|
|
@@ -56,14 +56,13 @@ const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
|
56
56
|
const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
57
57
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
58
58
|
const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
59
|
-
// Version string baked into the models endpoint query — the OAuth backend
|
|
60
|
-
// request without it
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// on
|
|
65
|
-
//
|
|
66
|
-
// 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.
|
|
67
66
|
const CODEX_CLIENT_VERSION_FLOOR = '0.130.0';
|
|
68
67
|
const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
69
68
|
let _codexVersionCache = { value: null, fetchedAt: 0 };
|
|
@@ -587,7 +586,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
587
586
|
providerState: opts.providerState,
|
|
588
587
|
model,
|
|
589
588
|
});
|
|
590
|
-
// Match the body shape
|
|
589
|
+
// Match the request body shape the OAuth backend expects so the
|
|
591
590
|
// server-side auto-cache routes correctly. text.verbosity / include /
|
|
592
591
|
// tool_choice / parallel_tool_calls are all inert without side effects
|
|
593
592
|
// for most callers but their presence affects how the OAuth backend classifies the
|
|
@@ -618,8 +617,8 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
618
617
|
if (opts.fast === true) {
|
|
619
618
|
// 'priority' is the only fast-class value the OpenAI OAuth backend
|
|
620
619
|
// accepts on the wire: 'fast' is hard-rejected ("Unsupported
|
|
621
|
-
// service_tier: fast", probed 2026-06-11).
|
|
622
|
-
//
|
|
620
|
+
// service_tier: fast", probed 2026-06-11). Only send the request value
|
|
621
|
+
// when the model catalog advertises it.
|
|
623
622
|
if (codexModelSupportsServiceTier(model, 'priority')) {
|
|
624
623
|
body.service_tier = 'priority';
|
|
625
624
|
}
|
|
@@ -669,8 +668,8 @@ function _envPositiveInt(name, fallback) {
|
|
|
669
668
|
}
|
|
670
669
|
|
|
671
670
|
// Completed function_call.arguments parse for the OpenAI Responses stream.
|
|
672
|
-
//
|
|
673
|
-
//
|
|
671
|
+
// A function_call item arrives only on a completion/done signal, so a
|
|
672
|
+
// non-empty-but-malformed
|
|
674
673
|
// arguments string is deterministic bad JSON — NOT mid-stream truncation.
|
|
675
674
|
// Empty/whitespace input legitimately means "no arguments" → {}. A non-empty
|
|
676
675
|
// string that fails JSON.parse is surfaced as an invalid-args MARKER (instead
|
|
@@ -888,7 +887,7 @@ export async function sendViaHttpSse({
|
|
|
888
887
|
_clearSemanticIdle();
|
|
889
888
|
_semanticIdleTimer = setTimeout(() => {
|
|
890
889
|
_streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
|
|
891
|
-
// Partial-final recovery
|
|
890
|
+
// Partial-final recovery: attach the
|
|
892
891
|
// streamed partial state so the agent loop can accept a wedged FINAL
|
|
893
892
|
// no-tool summary as a successful partial-final instead of dropping
|
|
894
893
|
// the result. pendingToolUse gates out any mid-flight tool call.
|
|
@@ -1076,6 +1075,22 @@ export async function sendViaHttpSse({
|
|
|
1076
1075
|
name: event.item.name || '',
|
|
1077
1076
|
callId: event.item.call_id || '',
|
|
1078
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
|
+
}
|
|
1079
1094
|
}
|
|
1080
1095
|
break;
|
|
1081
1096
|
case 'response.function_call_arguments.delta':
|
|
@@ -1084,6 +1099,7 @@ export async function sendViaHttpSse({
|
|
|
1084
1099
|
case 'response.function_call_arguments.done': {
|
|
1085
1100
|
const itemId = event.item_id || '';
|
|
1086
1101
|
const pending = pendingCalls.get(itemId);
|
|
1102
|
+
if (pending?.kind === 'tool_search') { meaningful(); break; }
|
|
1087
1103
|
const call = {
|
|
1088
1104
|
id: pending?.callId || event.call_id || '',
|
|
1089
1105
|
name: pending?.name || event.name || '',
|
|
@@ -1116,6 +1132,7 @@ export async function sendViaHttpSse({
|
|
|
1116
1132
|
}
|
|
1117
1133
|
}
|
|
1118
1134
|
} else if (item.type === 'tool_search_call') {
|
|
1135
|
+
pendingCalls.delete(item.id || '');
|
|
1119
1136
|
pushToolSearchCall(item);
|
|
1120
1137
|
} else if (item.type === 'custom_tool_call') {
|
|
1121
1138
|
pushCustomToolCall(item);
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
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
|
}
|
|
@@ -249,7 +241,41 @@ export function openCodeGoUsageConfigStatus(config = {}) {
|
|
|
249
241
|
};
|
|
250
242
|
}
|
|
251
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
|
+
|
|
252
276
|
async function fetchWorkspaceId(authCookie, { signal } = {}) {
|
|
277
|
+
const fromRedirect = await fetchWorkspaceIdFromAuthRedirect(authCookie, { signal });
|
|
278
|
+
if (fromRedirect) return fromRedirect;
|
|
253
279
|
const url = new URL(`${BASE_URL}/_server`);
|
|
254
280
|
url.searchParams.set('id', WORKSPACES_SERVER_ID);
|
|
255
281
|
const res = await fetch(url, {
|
|
@@ -232,12 +232,11 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
// ── Shared network-resilience interface ──────────────────────────────────────
|
|
235
|
-
// One home for the logic
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// Provider differences are passed as ARGUMENTS (policy objects), never
|
|
239
|
-
// on a hardcoded provider name.
|
|
240
|
-
// is the relocated original, selected by policy.mode.
|
|
235
|
+
// One home for the logic shared across providers: mid-stream classifier
|
|
236
|
+
// (WS + SSE), transport fallback predicate, stream-safety stamp latches,
|
|
237
|
+
// abort-aware sleep, handshake classifier, and the retry-budget table.
|
|
238
|
+
// Provider differences are passed as ARGUMENTS (policy objects), never
|
|
239
|
+
// branched on a hardcoded provider name.
|
|
241
240
|
|
|
242
241
|
// F) Retry-budget profiles as DATA. The numbers live ONLY here now.
|
|
243
242
|
// ws.transientCloseRetries (4) — ws_1006 / ws_1011 connection-loss buckets.
|
|
@@ -260,8 +259,8 @@ function _midstreamLimitFor(classifier, policy) {
|
|
|
260
259
|
return policy.defaultRetries
|
|
261
260
|
}
|
|
262
261
|
|
|
263
|
-
// WS gates each classifier against its own budget
|
|
264
|
-
//
|
|
262
|
+
// WS gates each classifier against its own budget. SSE applies a single
|
|
263
|
+
// top-of-function budget gate and
|
|
265
264
|
// then returns raw classifier strings, so perClassifierGate:false returns the
|
|
266
265
|
// classifier unconditionally here.
|
|
267
266
|
function _allowMidstream(classifier, attemptIndex, policy) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Compaction debug/diagnostic helpers extracted from loop.mjs.
|
|
2
|
+
import { estimateMessagesTokens } from '../context-utils.mjs';
|
|
3
|
+
|
|
4
|
+
export function estimateMessagesTokensSafe(messages) {
|
|
5
|
+
try { return estimateMessagesTokens(messages); }
|
|
6
|
+
catch { return null; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function compactDebugEnabled() {
|
|
10
|
+
return String(process.env.MIXDOG_COMPACT_DEBUG || '').trim() === '1';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function compactDiagnosticError(err) {
|
|
14
|
+
if (!err) return null;
|
|
15
|
+
const text = String(err?.message || err);
|
|
16
|
+
return text.length > 500 ? `${text.slice(0, 499)}…` : text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function compactByteLength(text) {
|
|
20
|
+
try { return Buffer.byteLength(String(text || ''), 'utf8'); }
|
|
21
|
+
catch { return String(text || '').length; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function compactDebugLog(scope, details = {}) {
|
|
25
|
+
if (!compactDebugEnabled()) return;
|
|
26
|
+
try { process.stderr.write(`[compact] ${scope} ${JSON.stringify(details)}\n`); }
|
|
27
|
+
catch { /* best-effort diagnostics only */ }
|
|
28
|
+
}
|