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.
Files changed (154) hide show
  1. package/package.json +2 -1
  2. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  3. package/scripts/build-tui.mjs +13 -1
  4. package/scripts/explore-bench.mjs +124 -0
  5. package/scripts/hook-bus-test.mjs +191 -0
  6. package/scripts/path-suffix-test.mjs +57 -0
  7. package/scripts/recall-bench.mjs +207 -0
  8. package/scripts/tool-smoke.mjs +7 -4
  9. package/src/agents/debugger/AGENT.md +2 -2
  10. package/src/agents/heavy-worker/AGENT.md +20 -11
  11. package/src/agents/reviewer/AGENT.md +2 -2
  12. package/src/agents/worker/AGENT.md +17 -11
  13. package/src/mixdog-session-runtime.mjs +424 -1812
  14. package/src/repl.mjs +5 -5
  15. package/src/rules/agent/30-explorer.md +8 -11
  16. package/src/rules/lead/lead-tool.md +9 -5
  17. package/src/rules/shared/01-tool.md +11 -5
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
  19. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
  21. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
  24. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
  27. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  28. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  29. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
  30. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
  31. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
  32. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
  33. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
  34. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
  35. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  36. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  37. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  38. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  39. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  40. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  41. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  42. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  43. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  44. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  45. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  46. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  47. package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
  48. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  49. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  50. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  51. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  52. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  53. package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
  54. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
  55. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
  56. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  59. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  62. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  64. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
  65. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  69. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  71. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  73. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  74. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  75. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  76. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  77. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  78. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  79. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
  80. package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
  81. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  82. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  83. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  84. package/src/runtime/channels/index.mjs +14 -233
  85. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  86. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  87. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  88. package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
  89. package/src/runtime/channels/lib/telegram-format.mjs +19 -22
  90. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  91. package/src/runtime/memory/index.mjs +314 -359
  92. package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
  93. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  94. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  95. package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
  96. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  97. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  98. package/src/runtime/memory/lib/memory.mjs +20 -0
  99. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  100. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  101. package/src/runtime/memory/tool-defs.mjs +4 -4
  102. package/src/runtime/shared/abort-controller.mjs +1 -1
  103. package/src/runtime/shared/background-tasks.mjs +2 -3
  104. package/src/runtime/shared/buffered-appender.mjs +149 -0
  105. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  106. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  107. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  108. package/src/runtime/shared/transcript-writer.mjs +29 -2
  109. package/src/session-runtime/config-helpers.mjs +209 -0
  110. package/src/session-runtime/effort.mjs +128 -0
  111. package/src/session-runtime/fs-utils.mjs +10 -0
  112. package/src/session-runtime/model-capabilities.mjs +130 -0
  113. package/src/session-runtime/output-styles.mjs +124 -0
  114. package/src/session-runtime/plugin-mcp.mjs +114 -0
  115. package/src/session-runtime/session-text.mjs +100 -0
  116. package/src/session-runtime/statusline-route.mjs +35 -0
  117. package/src/session-runtime/tool-catalog.mjs +720 -0
  118. package/src/session-runtime/workflow.mjs +358 -0
  119. package/src/standalone/agent-tool.mjs +49 -10
  120. package/src/standalone/channel-worker.mjs +3 -2
  121. package/src/standalone/explore-tool.mjs +10 -3
  122. package/src/standalone/hook-bus.mjs +165 -8
  123. package/src/standalone/opencode-go-login.mjs +121 -0
  124. package/src/standalone/provider-admin.mjs +14 -3
  125. package/src/tui/App.jsx +884 -143
  126. package/src/tui/components/PromptInput.jsx +272 -15
  127. package/src/tui/components/ToolExecution.jsx +20 -10
  128. package/src/tui/components/tool-output-format.mjs +2 -2
  129. package/src/tui/dist/index.mjs +1771 -1947
  130. package/src/tui/engine/agent-envelope.mjs +296 -0
  131. package/src/tui/engine/boot-profile.mjs +21 -0
  132. package/src/tui/engine/labels.mjs +67 -0
  133. package/src/tui/engine/notice-text.mjs +112 -0
  134. package/src/tui/engine/queue-helpers.mjs +161 -0
  135. package/src/tui/engine/session-stats.mjs +46 -0
  136. package/src/tui/engine/tool-call-fields.mjs +23 -0
  137. package/src/tui/engine/tool-result-text.mjs +126 -0
  138. package/src/tui/engine.mjs +311 -851
  139. package/src/tui/input-editing.mjs +58 -8
  140. package/src/tui/input-editing.selection.test.mjs +75 -0
  141. package/src/tui/keyboard-protocol.mjs +2 -2
  142. package/src/tui/lib/voice-recorder.mjs +35 -19
  143. package/src/tui/markdown/format-token.mjs +7 -8
  144. package/src/tui/markdown/format-token.test.mjs +3 -3
  145. package/src/tui/paste-attachments.mjs +38 -0
  146. package/src/tui/paste-fix.test.mjs +119 -0
  147. package/src/tui/themes/base.mjs +2 -2
  148. package/src/tui/themes/kanagawa.mjs +4 -4
  149. package/src/tui/themes/teal.mjs +4 -5
  150. package/src/tui/themes/utils.mjs +1 -1
  151. package/src/ui/statusline.mjs +49 -0
  152. package/src/workflows/default/WORKFLOW.md +16 -9
  153. package/src/workflows/sequential/WORKFLOW.md +16 -11
  154. 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' || extra.is_enabled !== true) return null;
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. Native-provider convergence (openai-oauth /
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. Mirrors opencode `The arguments provided to the tool are invalid` and
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. Native convergence: return an invalid-args MARKER (not a
89
- // throw) so the dispatch loop feeds the parse error back to the model as
90
- // a tool_result and the model self-corrects in the same turn. Only an
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 parity: on a mid-stream stall, attach the
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
- arguments: item.arguments && typeof item.arguments === 'object'
495
- ? item.arguments
496
- : parseCompletedToolCallArgumentsJson(item.arguments || '{}', label, { id: callId, name: 'tool_search', finishReason: 'done' }),
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 parity: attach streamed partial state so a
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
- arguments: item.arguments && typeof item.arguments === 'object'
1039
- ? item.arguments
1040
- : parseCompletedToolCallArgumentsJson(item.arguments || '{}', label, { id: item.call_id || item.id, name: 'tool_search', finishReason }),
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. This
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
- * References:
13
- * - pi-mono packages/ai/src/providers/openai-codex-responses.ts
14
- * (acquireWebSocket/release, get_incremental_items delta logic).
15
- * - openai/codex codex-rs/core/src/client.rs (turn-state echo header).
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 (matches codex
87
- // responses_websocket.rs timeout(idle_timeout, ws_stream.next()) and mixdog
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
- // Previously 3 — bumped for parity so every provider exhausts the same number
113
- // of transient-5xx attempts before surfacing failure to the caller.
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
- // Port of pi-mono get_incremental_items: if the cached request (sans input)
946
- // matches the current one and the current input starts with the cached input,
947
- // return only the tail. Otherwise return the full input (fresh turn).
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. Native convergence (openai-oauth / anthropic-oauth / opencode): same policy
1218
- // as the function_call_arguments.done path and openai-oauth _parseJsonObject —
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') return value;
1224
+ if (value && typeof value === 'object') {
1225
+ // Reject arrays — the tool_search schema is an object
1226
+ // ({query,select,limit}); an array must never pass through as args.
1227
+ return Array.isArray(value) ? {} : value;
1228
+ }
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 (pi per-event parity): distinct from the inter-chunk
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 parity: attach streamed partial state so
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 (codex parity) — response.created, metadata,
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. Native
1727
- // convergence: surface an invalid-args MARKER (not silent
1728
- // {}) so the dispatch loop returns an is_error tool_result
1729
- // and the model re-issues valid JSON in the same turn.
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 rejects the
60
- // request without it. Keep close to the latest published @openai/codex CLI because
61
- // older versions trigger a visibility-filtered catalog (e.g. only rollout
62
- // models). Bump when the real CLI bumps.
63
- // OpenAI OAuth backend gates new model exposures (e.g. gpt-5.5 only on >= 0.130.0)
64
- // on the client_version header. Resolve dynamically from npm so newly-shipped
65
- // models surface within a day instead of waiting on a hardcoded bump here.
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 pi-mono and the official OpenAI CLI ship so the
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). Match official CLI behavior:
622
- // only send the request value when the model catalog advertises it.
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
- // Native convergence (openai-oauth / anthropic-oauth / opencode): a function_call item
673
- // arrives only on a completion/done signal, so a non-empty-but-malformed
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 parity with anthropic-oauth: attach the
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 that used to be copied per provider: mid-stream
236
- // classifier (WS + SSE), transport fallback predicate, stream-safety stamp
237
- // latches, abort-aware sleep, handshake classifier, and the retry-budget table.
238
- // Provider differences are passed as ARGUMENTS (policy objects), never branched
239
- // on a hardcoded provider name. Behavior is preserved EXACTLY — each path below
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 (mirrors the old
264
- // _allowMidstreamRetry). SSE applies a single top-of-function budget gate and
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
+ }