mixdog 0.9.93 → 0.9.95

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 (186) hide show
  1. package/NOTICE.md +72 -0
  2. package/package.json +16 -6
  3. package/scripts/lib/isolated-root-cleanup.mjs +19 -0
  4. package/scripts/run-suite.mjs +100 -0
  5. package/src/lib/rules-builder.cjs +4 -3
  6. package/src/output-styles/detailed.md +13 -15
  7. package/src/output-styles/extreme-minimal.md +7 -11
  8. package/src/output-styles/minimal.md +4 -7
  9. package/src/output-styles/simple.md +11 -13
  10. package/src/rules/agent/30-explorer.md +22 -16
  11. package/src/rules/lead/01-general.md +1 -2
  12. package/src/rules/lead/lead-brief.md +4 -5
  13. package/src/rules/lead/lead-tool.md +3 -2
  14. package/src/rules/shared/01-tool.md +28 -29
  15. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  16. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +23 -7
  17. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
  18. package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
  19. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
  21. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
  22. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
  26. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
  27. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
  28. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
  29. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
  30. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
  32. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
  33. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
  34. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
  35. package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
  36. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
  37. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
  38. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
  39. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
  40. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
  41. package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
  42. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
  43. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
  44. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
  47. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
  50. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
  51. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
  52. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
  54. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +103 -92
  55. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
  56. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
  57. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
  58. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +25 -3
  59. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  60. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  61. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
  62. package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
  64. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
  65. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
  66. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +20 -1
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
  69. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
  70. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
  71. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
  72. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
  73. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
  75. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
  76. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
  77. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +546 -23
  78. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +8 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
  83. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
  84. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
  86. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +7 -3
  87. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +104 -14
  88. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
  89. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
  90. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
  91. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
  93. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +5 -5
  94. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
  96. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +151 -18
  97. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
  99. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
  100. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
  101. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
  102. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
  103. package/src/runtime/channels/backends/discord.mjs +5 -14
  104. package/src/runtime/channels/backends/telegram.mjs +0 -5
  105. package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
  106. package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
  107. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  108. package/src/runtime/channels/lib/worker-main.mjs +1 -1
  109. package/src/runtime/media/renditions.mjs +21 -2
  110. package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
  111. package/src/runtime/memory/tool-defs.mjs +6 -6
  112. package/src/runtime/shared/atomic-file.mjs +53 -0
  113. package/src/runtime/shared/background-tasks.mjs +16 -6
  114. package/src/runtime/shared/child-spawn-gate.mjs +50 -26
  115. package/src/runtime/shared/resource-admission.mjs +40 -1
  116. package/src/runtime/shared/task-notification-envelope.mjs +11 -2
  117. package/src/runtime/shared/tool-card-model.mjs +6 -2
  118. package/src/runtime/shared/tool-status.mjs +10 -1
  119. package/src/runtime/shared/tool-surface.mjs +5 -0
  120. package/src/runtime/shared/turn-snapshot.mjs +385 -21
  121. package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
  122. package/src/session-runtime/context-status.mjs +9 -3
  123. package/src/session-runtime/lifecycle-api.mjs +35 -5
  124. package/src/session-runtime/mcp-glue.mjs +11 -6
  125. package/src/session-runtime/provider-models.mjs +94 -43
  126. package/src/session-runtime/provider-usage.mjs +26 -2
  127. package/src/session-runtime/runtime-core.mjs +46 -8
  128. package/src/session-runtime/runtime-tunables.mjs +4 -0
  129. package/src/session-runtime/self-update.mjs +33 -4
  130. package/src/session-runtime/session-lifecycle.mjs +2 -0
  131. package/src/session-runtime/session-text.mjs +2 -1
  132. package/src/session-runtime/session-turn-api.mjs +106 -22
  133. package/src/session-runtime/workflow.mjs +11 -4
  134. package/src/standalone/agent-tool.mjs +4 -4
  135. package/src/standalone/backend-daemon.mjs +570 -0
  136. package/src/standalone/channel-daemon-transport.mjs +141 -1
  137. package/src/standalone/channel-worker.mjs +3 -2
  138. package/src/standalone/engine-daemon-client.mjs +894 -0
  139. package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
  140. package/src/standalone/engine-daemon-protocol.mjs +33 -0
  141. package/src/standalone/engine-daemon-service.mjs +864 -0
  142. package/src/standalone/engine-daemon-transport.mjs +603 -0
  143. package/src/standalone/explore-tool.mjs +1 -1
  144. package/src/tui/App.jsx +62 -47
  145. package/src/tui/app/app-format.mjs +4 -2
  146. package/src/tui/app/app-view.jsx +5 -0
  147. package/src/tui/app/channel-pickers.mjs +7 -6
  148. package/src/tui/app/core-memory-picker.mjs +4 -4
  149. package/src/tui/app/extension-pickers.mjs +20 -18
  150. package/src/tui/app/maintenance-pickers.mjs +27 -27
  151. package/src/tui/app/onboarding-steps.mjs +23 -18
  152. package/src/tui/app/prompt-submit.mjs +13 -2
  153. package/src/tui/app/route-pickers.mjs +13 -8
  154. package/src/tui/app/settings-picker.mjs +55 -58
  155. package/src/tui/app/slash-dispatch.mjs +32 -28
  156. package/src/tui/app/transcript-window.mjs +19 -0
  157. package/src/tui/app/usage-context-panels.mjs +22 -7
  158. package/src/tui/app/use-mouse-input.mjs +25 -3
  159. package/src/tui/app/use-prompt-queue-history.mjs +31 -16
  160. package/src/tui/app/use-transcript-scroll.mjs +30 -8
  161. package/src/tui/app/use-transcript-window.mjs +22 -1
  162. package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
  163. package/src/tui/components/PromptInput.jsx +10 -0
  164. package/src/tui/components/Spinner.jsx +89 -86
  165. package/src/tui/components/TextEntryPanel.jsx +14 -0
  166. package/src/tui/components/ToolExecution.jsx +2 -2
  167. package/src/tui/dist/index.mjs +1537 -9718
  168. package/src/tui/engine/agent-job-feed.mjs +2 -2
  169. package/src/tui/engine/live-share.mjs +97 -4
  170. package/src/tui/engine/session-api-ext.mjs +23 -1
  171. package/src/tui/engine/session-api.mjs +88 -41
  172. package/src/tui/engine/session-flow.mjs +43 -4
  173. package/src/tui/engine/tool-card-results.mjs +6 -0
  174. package/src/tui/engine/turn.mjs +84 -4
  175. package/src/tui/engine-local-session.mjs +1108 -0
  176. package/src/tui/engine.mjs +16 -1057
  177. package/src/tui/index.jsx +47 -2
  178. package/src/tui/markdown/stream-fence.mjs +1 -1
  179. package/src/tui/spinner-meta.mjs +80 -0
  180. package/src/tui/spinner-verbs.mjs +35 -0
  181. package/src/ui/statusline-segments.mjs +43 -10
  182. package/src/ui/statusline.mjs +10 -1
  183. package/scripts/tmp-cdp-errors.mjs +0 -41
  184. package/scripts/tmp-cdp-inspect.mjs +0 -41
  185. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
  186. package/src/standalone/channel-daemon.mjs +0 -226
@@ -5,6 +5,7 @@
5
5
  import { join, resolve as _pathResolve, isAbsolute as _pathIsAbs, normalize as _pathNorm } from 'node:path';
6
6
  import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
7
7
  import { _normalizeCacheKey } from './util.mjs';
8
+ import { GREP_AUTO_CONTEXT_LINES } from '../../tools/builtin/path-utils.mjs';
8
9
 
9
10
  const MAX_PER_SESSION = 100;
10
11
 
@@ -43,6 +44,22 @@ function _firstArg(args, names) {
43
44
  return undefined;
44
45
  }
45
46
 
47
+ const _GREP_CONTEXT_GROUPS = [
48
+ ['-A', ['-A', 'A', 'after', 'after_context', 'afterContext', '--after-context', 'after-context', 'afterLines', 'after_lines']],
49
+ ['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext', '--before-context', 'before-context', 'beforeLines', 'before_lines']],
50
+ ['context', ['context', '-C', 'C', 'context_lines', 'contextLines', '--context', 'contextN', 'around', 'surrounding']],
51
+ ];
52
+
53
+ function _canonicalizeGrepContextArgs(args) {
54
+ for (const [canonical, aliases] of _GREP_CONTEXT_GROUPS) {
55
+ const value = _firstArg(args, aliases);
56
+ for (const alias of aliases) delete args[alias];
57
+ if (value === undefined) continue;
58
+ const numeric = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
59
+ args[canonical] = Number.isFinite(numeric) && Number.isInteger(numeric) ? numeric : value;
60
+ }
61
+ }
62
+
46
63
  function _canonicalToolArgs(toolName, args) {
47
64
  if (!args || typeof args !== 'object') return args;
48
65
  const next = { ...args };
@@ -59,12 +76,35 @@ function _canonicalToolArgs(toolName, args) {
59
76
  const alias = _firstArg(next, ['root', 'directory', 'dir']);
60
77
  if (alias !== undefined) next.path = alias;
61
78
  }
79
+ _canonicalizeGrepContextArgs(next);
62
80
  if ((next.output_mode === undefined || next.output_mode === null || next.output_mode === '') && typeof next.mode === 'string') {
63
81
  const mode = next.mode.trim();
64
- if (['files_with_matches', 'content', 'count'].includes(mode)) next.output_mode = mode;
82
+ if (['files_with_matches', 'content', 'content_with_context', 'count'].includes(mode)) next.output_mode = mode;
65
83
  }
66
84
  for (const k of ['query', 'regex', 'regexp', 'needle', 'search', 'literal', 'file_pattern', 'filePattern', 'include', 'includes', 'files', 'root', 'directory', 'dir']) delete next[k];
67
- if (next.output_mode && next.mode === next.output_mode) delete next.mode;
85
+ delete next.mode;
86
+
87
+ // Canonicalize by execution semantics, not caller spelling:
88
+ // omitted/content_with_context => content + automatic 25-line context;
89
+ // context:0 => bare content; context flags are ignored in count/files.
90
+ const requestedMode = typeof next.output_mode === 'string' ? next.output_mode.trim() : '';
91
+ if (requestedMode === 'files_with_matches' || requestedMode === 'count') {
92
+ next.output_mode = requestedMode;
93
+ delete next['-A'];
94
+ delete next['-B'];
95
+ delete next.context;
96
+ } else {
97
+ const hasExplicitContext = ['-A', '-B', 'context']
98
+ .some((key) => Object.prototype.hasOwnProperty.call(next, key));
99
+ next.output_mode = 'content';
100
+ if ((requestedMode === '' || requestedMode === 'content_with_context') && !hasExplicitContext) {
101
+ next.context = GREP_AUTO_CONTEXT_LINES;
102
+ }
103
+ if (next.context === 0 && !Object.prototype.hasOwnProperty.call(next, '-A')
104
+ && !Object.prototype.hasOwnProperty.call(next, '-B')) {
105
+ delete next.context;
106
+ }
107
+ }
68
108
  } else if (toolName === 'glob') {
69
109
  if (next.pattern === undefined || next.pattern === null || next.pattern === '') {
70
110
  const alias = _firstArg(next, ['glob', 'file_pattern', 'filePattern', 'name', 'include', 'includes', 'files']);
@@ -2,7 +2,9 @@ import { isAgentOwner } from '../agent-owner.mjs';
2
2
 
3
3
  export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
4
4
  export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
5
- export const DEFAULT_MAIN_COMPACTION_BUFFER_RATIO = 0.05;
5
+ // Main/user sessions default to full-window trigger (buffer 0): RFT compact
6
+ // runs at the boundary. Agent sessions keep DEFAULT_COMPACTION_BUFFER_RATIO.
7
+ export const DEFAULT_MAIN_COMPACTION_BUFFER_RATIO = 0;
6
8
  export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
7
9
  const MAX_BUFFER_INPUT_RATIO = 0.999_999;
8
10
  export const DEFAULT_COMPACTION_KEEP_TOKENS = 8_000;
@@ -16,9 +18,14 @@ function envTokenInt(name) {
16
18
  return positiveTokenInt(process.env[name]);
17
19
  }
18
20
  export function normalizeCompactionBufferRatio(value, fallback = DEFAULT_COMPACTION_BUFFER_RATIO) {
21
+ // Explicit 0 is a valid buffer (main full-window trigger). Only nullish /
22
+ // empty / non-finite / negative values fall back. Do not treat Number(null)
23
+ // as zero here — null must mean "unset".
24
+ if (value == null || value === '') return fallback;
19
25
  const n = Number(value);
20
- if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
21
- return fallback;
26
+ if (!Number.isFinite(n) || n < 0) return fallback;
27
+ if (n === 0) return 0;
28
+ return n > 1 ? n / 100 : n;
22
29
  }
23
30
  export function resolveBufferRatioCandidate(percentInputs = [], ratioInputs = []) {
24
31
  for (const raw of percentInputs) {
@@ -160,11 +160,10 @@ export function providerTokenCalibration(provider) {
160
160
  if (p.startsWith('gemini') || p.startsWith('google')) return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_GEMINI') ?? 1.15;
161
161
  return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_DEFAULT') ?? 1.0;
162
162
  }
163
- // Claude Code parity (services/tokenEstimation.ts): images/documents count a
164
- // flat 2000 tokens when dimensions are unknown — the conservative constant CC
165
- // shares with microCompact's IMAGE_MAX_TOKEN_SIZE. Known dimensions may only
166
- // RAISE the allowance via Anthropic's real vision formula (w*h/750), capped at
167
- // the 2000x2000 resize ceiling (5333 tokens).
163
+ // Images/documents count a flat 2000 tokens when dimensions are unknown —
164
+ // the conservative constant also used by the micro-compaction path. Known
165
+ // dimensions may only RAISE the allowance via Anthropic's real vision
166
+ // formula (w*h/750), capped at the 2000x2000 resize ceiling (5333 tokens).
168
167
  export const IMAGE_VISUAL_TOKEN_ALLOWANCE = 2_000;
169
168
  const IMAGE_MAX_TOKEN_ALLOWANCE = 5_333;
170
169
 
@@ -249,8 +248,8 @@ function legacyEstimateTokens(s) {
249
248
  // for those runs without penalizing ordinary spaced prose.
250
249
  let denseAsciiFloor = 0;
251
250
  for (const match of s.matchAll(/[\x21-\x7e]{16,}/g)) {
252
- // Claude Code prices dense JSON/JSONL at chars/2 (bytesPerTokenForFileType);
253
- // 0.5/char keeps that parity for long unmerged printable runs.
251
+ // Dense JSON/JSONL prices at chars/2; 0.5/char keeps that ratio for
252
+ // long unmerged printable runs.
254
253
  denseAsciiFloor += match[0].length * 0.5;
255
254
  }
256
255
  const encodedWords = s.match(/\b(?=[A-Za-z0-9]{8,}\b)(?=[A-Za-z0-9]*[A-Za-z])(?=[A-Za-z0-9]*\d)[A-Za-z0-9]+\b/g) || [];
@@ -272,7 +271,7 @@ function legacyEstimateTokens(s) {
272
271
  && (jsonLikeLines >= Math.ceil(lines.length / 2) || structural / nonWhitespace >= 0.12)) {
273
272
  // JSONL, compact tables and generated line protocols can consist
274
273
  // entirely of short runs while still tokenizing like minified data.
275
- // chars/2 on the dense payload chars (Claude Code JSON parity).
274
+ // chars/2 on the dense payload chars (dense-JSON pricing).
276
275
  denseAsciiFloor = Math.max(denseAsciiFloor, (nonWhitespace * 0.5) + ((s.length - nonWhitespace) * 0.25));
277
276
  }
278
277
  const asciiFloor = s.length / 4; // never below the legacy chars/4 lower bound
@@ -355,8 +354,8 @@ function messageEstimateText(m) {
355
354
  function imageDescriptorAllowance(descriptor) {
356
355
  if (descriptor.width && descriptor.height) {
357
356
  // Anthropic vision cost: tokens = (width * height) / 750, with images
358
- // resized down to at most 2000x2000 (5333 tokens) — Claude Code uses
359
- // the same formula/cap. Caller-supplied dimensions may RAISE the
357
+ // resized down to at most 2000x2000 (5333 tokens). Caller-supplied
358
+ // dimensions may RAISE the
360
359
  // allowance above the unknown-image floor but never lower it (the
361
360
  // provider normalizer may not preserve caller metadata).
362
361
  const formula = Math.ceil((descriptor.width * descriptor.height) / 750);
@@ -365,7 +364,7 @@ function imageDescriptorAllowance(descriptor) {
365
364
  Math.max(IMAGE_VISUAL_TOKEN_ALLOWANCE, formula),
366
365
  );
367
366
  }
368
- // Unknown-size images: flat conservative allowance (Claude Code parity).
367
+ // Unknown-size images: flat conservative allowance.
369
368
  return IMAGE_VISUAL_TOKEN_ALLOWANCE;
370
369
  }
371
370
  function messageImageDescriptors(m) {
@@ -100,6 +100,7 @@ export function createEagerDispatcher({
100
100
  if (_dedupEligible) _eagerInFlightSigs.set(_sig, call.id);
101
101
  entry.promise = (async () => {
102
102
  try {
103
+ await opts.beforeToolExecution?.();
103
104
  return { ok: true, value: await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: getNextIteration(), deferShellCwdCommit: true }) };
104
105
  } catch (error) {
105
106
  return { ok: false, error };
@@ -112,8 +112,8 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
112
112
  if (!boundaryTokens) return null;
113
113
  const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
114
114
  // Shared session-compaction policy (context-utils): agent semantic keeps the
115
- // default early-trigger buffer (90%); main/user use their independently
116
- // configurable 25% default headroom (75%);
115
+ // default early-trigger buffer (90%); main/user default to full-window
116
+ // trigger (buffer 0 / 100%), still overridable via mainBuffer*;
117
117
  // a truly-explicit sub-boundary limit wins. explicitAutoCompactTokenLimit
118
118
  // is the sanitized (null when legacy full-window) value so telemetry never
119
119
  // re-persists a boundary-collapsing limit.
@@ -216,7 +216,7 @@ function providerPressureTokens(sessionRef, usage) {
216
216
  /**
217
217
  * Align an authoritative provider usage snapshot to the message prefix it
218
218
  * covers. Later pressure checks add estimates only for messages after this
219
- * baseline, matching Claude Code's actual-usage-plus-growth accounting.
219
+ * baseline: actual-usage-plus-growth accounting.
220
220
  */
221
221
  export function recordProviderContextBaseline(sessionRef, messages, usage, {
222
222
  boundary = 'complete',
@@ -38,20 +38,6 @@ export function isEditProgressTool(name, isEager) {
38
38
  return !NON_PROGRESS_TOOLS.has(bare);
39
39
  }
40
40
 
41
- // Step 1 — level-2 escalation steering. N = cumulative level-1 fires.
42
- // `readOnlyRole` swaps the edit-oriented directive for a report-oriented one:
43
- // read-permission sessions (reviewer-style) cannot apply_patch, so telling
44
- // them to edit is self-contradictory and pushes premature termination.
45
- const LEVEL2_DIMINISHING_RETURNS = ' Further reads add cost, not evidence.';
46
-
47
- export function level2SteerMessage(n, readOnlyRole = false, level2Fires = 1) {
48
- const tail = Number(level2Fires) >= 2 ? LEVEL2_DIMINISHING_RETURNS : '';
49
- if (readOnlyRole) {
50
- return `<system-reminder>\nEnough evidence gathered — report findings now; name any gaps.${tail}\n</system-reminder>`;
51
- }
52
- return `<system-reminder>\nStop exploring — apply the edit you know, or report blocked with what's missing.${tail}\n</system-reminder>`;
53
- }
54
-
55
41
  // Step 2 — cross-turn dedup stub. `stuck` appends the escalation tail at the
56
42
  // 5th+ dedup stub in the session.
57
43
  export function crossTurnDedupStub(name, firstIteration, stuck) {
@@ -1,11 +1,12 @@
1
- // Codex-style turn stop hook (refs/codex core/src/session/turn.rs:372-404).
1
+ // Turn stop hook.
2
2
  //
3
- // Codex semantics: a sampling request that needs no follow-up ends the turn.
4
- // Before breaking, the turn runs its stop hooks; a hook may block ONCE by
5
- // returning continuation fragments, which are recorded as a prompt message
6
- // before sampling resumes (`stop_hook_active = true`, never cleared inside the
7
- // turn). Nothing else can force another turn: no mandatory terminal tool, no
8
- // completion strikes, no lexical inspection of the assistant text.
3
+ // Termination semantics: a sampling request that needs no follow-up ends the
4
+ // turn. Before breaking, the turn runs its stop hooks; a hook may block ONCE
5
+ // by returning continuation fragments, which are recorded as a prompt message
6
+ // before sampling resumes (the hook stays armed for the rest of the turn and
7
+ // is never cleared inside it). Nothing else can force another turn: no
8
+ // mandatory terminal tool, no completion strikes, no lexical inspection of
9
+ // the assistant text.
9
10
  //
10
11
  // The single hook implemented here preserves the original failed-apply_patch
11
12
  // protection: while a real tool failure is unresolved, the first terminal
@@ -41,7 +42,7 @@ export function toolFailureContinuationPrompt(failedTool) {
41
42
  * success clears it.
42
43
  * - takeContinuationPrompt(): returns the continuation prompt the FIRST time
43
44
  * a terminal assistant message arrives while a failure is unresolved, then
44
- * stays silent for the rest of the turn (Codex `stop_hook_active`).
45
+ * stays silent for the rest of the turn.
45
46
  */
46
47
  export function createToolFailureStopHook() {
47
48
  let unresolvedFailure = false;
@@ -672,11 +672,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
672
672
  },
673
673
  onToolResult: _trackToolResult,
674
674
  onToolApproval: typeof askOpts?.onToolApproval === 'function' ? askOpts.onToolApproval : undefined,
675
+ beforeToolExecution: typeof askOpts?.beforeToolExecution === 'function'
676
+ ? askOpts.beforeToolExecution
677
+ : undefined,
675
678
  onCompactEvent: (event) => {
676
679
  _scheduleTurnCheckpoint(true);
677
680
  try { askOpts?.onCompactEvent?.(event); } catch {}
678
681
  },
679
- // Claude Code parity: mid-chain queued prompt/notification
682
+ // Mid-chain queued prompt/notification
680
683
  // drain is owned by agentLoop at provider-continuation
681
684
  // boundaries (after a tool batch, before the next send).
682
685
  // The post-loop _pendingTail drain below still handles
@@ -692,9 +695,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
692
695
  // Manager/pending-messages entries carry no
693
696
  // mode/priority/slash metadata, so they stay OUT of the
694
697
  // mid-chain (post-tool-batch) drain — that would bypass
695
- // Claude Code's queued_command filters. At the TERMINAL
696
- // boundary they are exactly Codex `has_pending_input`
697
- // (turn.rs:304-318): an `agent type=send` queued while
698
+ // the queued-command filters. At the TERMINAL boundary
699
+ // they are exactly pending input: an `agent type=send`
700
+ // queued while
698
701
  // the terminal sample was in flight must be folded into
699
702
  // THIS turn before any stop hook runs, instead of losing
700
703
  // its slot to a synthetic continuation prompt. The mutex
@@ -210,7 +210,7 @@ export function resolveSessionContextMeta(provider, model, seed = {}) {
210
210
  }
211
211
  export function compactTriggerForSession(session, boundaryTokens) {
212
212
  // Delegates to the shared session-compaction policy (context-utils):
213
- // agent semantic -> 90% (default buffer), main/user -> 75% (default),
213
+ // agent semantic -> 90% (default buffer), main/user -> 100% (buffer 0),
214
214
  // truly-explicit sub-boundary limit wins.
215
215
  return resolveSessionCompactPolicy(session, boundaryTokens).triggerTokens;
216
216
  }
@@ -150,7 +150,7 @@ export function _runCleanupCycle() {
150
150
  // Drain every settled runtime entry on each pass, not just the one or two
151
151
  // sessions whose on-disk idle TTL happened to expire in this interval.
152
152
  _sweepTerminalSessionRuntimes();
153
- sweepOrphanedPendingMessages();
153
+ await sweepOrphanedPendingMessages();
154
154
  await sweepIdleSessions({ includeTombstones: true });
155
155
  // Reclaim same-process session snapshots whose state is durable on disk
156
156
  // (memory-leak guard: _liveSessions used to grow for process lifetime).
@@ -4,7 +4,7 @@ import { join } from 'path';
4
4
  import { readFileSync, statSync } from 'fs';
5
5
  import { createHash, randomBytes } from 'crypto';
6
6
  import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
7
- import { updateJsonAtomicSync, updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
7
+ import { updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
8
8
  import { promptContentText, isInternalRuntimeNotificationText } from './prompt-utils.mjs';
9
9
  import { loadSession, readSessionLifecycleStateFromDisk, saveSessionAsync } from '../store.mjs';
10
10
  import { isDeliveredCompletion, logDuplicateSkip } from './delivered-completions.mjs';
@@ -942,11 +942,11 @@ function shouldEvictPendingSession(sessionId, ttlMs, entryTouchedAt, now = Date.
942
942
  return entryTouch > 0 && (now - entryTouch) > PENDING_ORPHAN_GRACE_MS;
943
943
  }
944
944
 
945
- export function sweepOrphanedPendingMessages({ ttlMs = PENDING_ORPHAN_TTL_MS } = {}) {
945
+ export async function sweepOrphanedPendingMessages({ ttlMs = PENDING_ORPHAN_TTL_MS } = {}) {
946
946
  const now = Date.now();
947
947
  const removed = [];
948
948
  try {
949
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
949
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
950
950
  const next = normalizePendingStore(raw);
951
951
  const ids = Object.keys(next.sessions);
952
952
  if (ids.length === 0) return undefined;
@@ -988,9 +988,9 @@ function modelVisiblePendingMessages(messages) {
988
988
  }
989
989
 
990
990
  export function _mergePendingMessageEntries(entries) {
991
- // CC-parity delivery priority (reference messageQueueManager.ts: user
992
- // input enqueues at 'next', task notifications at 'later', and dequeue
993
- // always serves 'next' first). Our single merged turn message is the
991
+ // Delivery priority: user input enqueues at 'next', task notifications at
992
+ // 'later', and dequeue always serves 'next' first. Our single merged turn
993
+ // message is the
994
994
  // analogue of that dequeue order: genuine user/steering entries are
995
995
  // merged BEFORE deferred completion notifications so queued user input
996
996
  // is never buried under system notification text. FIFO is preserved
@@ -1074,7 +1074,16 @@ export function enqueueRemotePendingMessage(sessionId, message) {
1074
1074
  // spool lock (see persistPendingMessages).
1075
1075
  runPendingTestHook('enqueue:beforePublish', { sessionId });
1076
1076
  if (pendingLifecycleInvalidated(sessionId, token)) return 0;
1077
- const remoteEntry = stampLifecycleToken({ ...normalized, id: newPendingMessageId() }, token);
1077
+ // Preserve the viewer's submission id end-to-end. Minting a fresh id here
1078
+ // made pipe-delivered + spool-fallback twins look like two messages
1079
+ // (owner accepted the pipe copy, then re-took the spool copy).
1080
+ const preservedId = pendingMessageId(normalized) || pendingMessageId(message);
1081
+ const remoteEntry = stampLifecycleToken({
1082
+ ...normalized,
1083
+ id: (typeof preservedId === 'string' && preservedId.trim())
1084
+ ? preservedId.trim()
1085
+ : newPendingMessageId(),
1086
+ }, token);
1078
1087
  return persistPendingMessages(sessionId, [remoteEntry]);
1079
1088
  }
1080
1089
 
@@ -1104,7 +1113,7 @@ function _rememberForeignSpoolScan(sessionId, mtime) {
1104
1113
  * internal-notification entries are left untouched for the normal
1105
1114
  * askSession hydrate path.
1106
1115
  */
1107
- export function drainForeignUserInjections(sessionId) {
1116
+ export async function drainForeignUserInjections(sessionId) {
1108
1117
  if (!isValidPendingSessionId(sessionId)) return [];
1109
1118
  // DELIVERY path, not cleanup: this takes rows out of the spool and hands
1110
1119
  // them to a live turn. A tombstoned (or otherwise invalidated) session may
@@ -1147,7 +1156,13 @@ export function drainForeignUserInjections(sessionId) {
1147
1156
  // spool immediately pollable again.
1148
1157
  let lifecycleDecided = false;
1149
1158
  try {
1150
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
1159
+ // ASYNC spool lock. The sync variant parked the owning process's event
1160
+ // loop in Atomics.wait for the whole timeout: in the daemon (PTYs,
1161
+ // engines, IPC and SSE fan-out share one loop) every poll that lost the
1162
+ // race froze the entire app, and it could never win against an async
1163
+ // holder in the SAME process. Waiting off the loop also lets the
1164
+ // in-process lock queue serialize sibling sessions for free.
1165
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
1151
1166
  // Same authority re-read INSIDE the spool lock: a close/detach
1152
1167
  // landing in this window must neither deliver to the old owner nor
1153
1168
  // remove the reopened owner's rows.
@@ -1172,8 +1187,10 @@ export function drainForeignUserInjections(sessionId) {
1172
1187
  // late-delivery header instead of being silently removed
1173
1188
  // (user report: remote sends silently discarded around owner
1174
1189
  // restarts).
1175
- if (foreignUser && isStaleUserInjection(entry)) taken.push(lateDeliveryText(text, entry));
1176
- else if (foreignUser) taken.push(text);
1190
+ if (foreignUser && isStaleUserInjection(entry)) {
1191
+ taken.push({ text: lateDeliveryText(text, entry), id });
1192
+ }
1193
+ else if (foreignUser) taken.push({ text, id });
1177
1194
  else kept.push(entry);
1178
1195
  }
1179
1196
  if (taken.length === 0) return undefined;
@@ -1184,8 +1201,18 @@ export function drainForeignUserInjections(sessionId) {
1184
1201
  }
1185
1202
  next.updatedAt = Date.now();
1186
1203
  return next;
1187
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
1204
+ }, {
1205
+ compact: true,
1206
+ lock: true,
1207
+ mode: PENDING_MESSAGES_MODE,
1208
+ fsync: false,
1209
+ // Delivery polling must never wait on an async spool writer in the
1210
+ // same daemon process: waiting synchronously prevents that writer
1211
+ // from ever reaching its finally/release path.
1212
+ timeoutMs: 0,
1213
+ });
1188
1214
  } catch (err) {
1215
+ if (err?.code === 'ELOCKCONTENDED') return [];
1189
1216
  try { process.stderr.write(`[session] foreign-injection drain failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
1190
1217
  return [];
1191
1218
  }
@@ -1422,5 +1449,5 @@ setImmediate(() => {
1422
1449
  // the cross-process lock against the lead's writes for zero
1423
1450
  // benefit — the lead already sweeps.
1424
1451
  if (process.env.MIXDOG_WORKER_MODE === '1') return;
1425
- try { sweepOrphanedPendingMessages(); } catch { /* ignore */ }
1452
+ void sweepOrphanedPendingMessages().catch(() => {});
1426
1453
  });
@@ -12,6 +12,7 @@ import { _dropPendingMessageState } from './pending-messages.mjs';
12
12
  import { _stopToolActivityHeartbeat, _getRuntimeEntry, _clearSessionRuntime } from './runtime-liveness.mjs';
13
13
  import { _closeBashSessionLazy } from './runtime-loaders.mjs';
14
14
  import { clearTurnCheckpoint } from './turn-checkpoint.mjs';
15
+ import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs';
15
16
 
16
17
  /**
17
18
  * Close a session. Plants a `closed=true` tombstone on disk with a bumped
@@ -137,6 +138,7 @@ export function closeSession(id, reason = 'manual', opts = {}) {
137
138
  // Drop session-scoped read dedup cache so the Map doesn't accumulate
138
139
  // entries across mcp-server lifetime.
139
140
  try { clearReadDedupSession(id); } catch { /* ignore */ }
141
+ try { releaseReadSnapshotScope(id); } catch { /* ignore */ }
140
142
  // Drop offload sidecars + module-level counter for this session so a
141
143
  // long-running mcp-server doesn't leak disk (tool-results/<id>/*.txt)
142
144
  // or Map entries across session lifetime. Fire-and-forget — close path
@@ -226,6 +228,7 @@ export function unloadSessionRuntime(id, reason = 'runtime-unload') {
226
228
  try { globalThis.__mixdogCloseProviderConnectionsForSession?.(id, `runtime-unload:${reason}`); } catch { /* ignore */ }
227
229
  // Pure caches — a resumed turn rebuilds them from the transcript/disk.
228
230
  try { clearReadDedupSession(id); } catch { /* ignore */ }
231
+ try { releaseReadSnapshotScope(id); } catch { /* ignore */ }
229
232
  _clearSessionRuntime(id);
230
233
  if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
231
234
  try { process.stderr.write(`[agent-unload] session=${id} reason=${reason} shells=${allBashIds.length}\n`); } catch { /* best-effort */ }
@@ -169,7 +169,14 @@ export function createSession(opts) {
169
169
  const provider = getProvider(providerName);
170
170
  if (!provider)
171
171
  throw new Error(`Provider "${providerName}" not found or not enabled`);
172
- const id = mintSessionId();
172
+ const requestedId = String(opts.id || '').trim();
173
+ if (requestedId && !/^[A-Za-z0-9_-]+$/.test(requestedId)) {
174
+ throw new Error('createSession: id is invalid');
175
+ }
176
+ // The daemon may reserve the durable address before provider/session
177
+ // materialization. Supplying that reservation here keeps the address
178
+ // stable across intake -> queued turn -> provider execution.
179
+ const id = requestedId || mintSessionId();
173
180
  // Provider cache strategy — agentRuntime.resolveSync() above is a
174
181
  // best-effort injection point (setAgentRuntime() has no live caller
175
182
  // today, so that branch never fires); build it directly here so every
@@ -7,10 +7,10 @@ import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
7
7
 
8
8
  const INTERRUPT_MESSAGE = '[Request interrupted by user]';
9
9
  const INTERRUPT_MESSAGE_FOR_TOOL_USE = '[Request interrupted by user for tool use]';
10
- const STREAMING_INTERRUPTED_TOOL_RESULT = 'Interrupted by user';
11
10
  const PROCESS_RESTART_INTERRUPT_MESSAGE = '[Request interrupted by process restart]';
12
- const PROCESS_RESTART_TOOL_RESULT = 'Interrupted by process restart';
13
- const TOOL_USE_REJECT_RESULT = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
11
+ // Short tool_result body for any unfinished call closed by
12
+ // cancel/crash. UI maps this (and legacy long reject bodies) to Cancelled.
13
+ const INTERRUPTED_TOOL_RESULT = 'Cancelled';
14
14
 
15
15
  // Abort reasons that represent an EXPLICIT user cancellation of this turn.
16
16
  // Only these rewind a not-yet-answered user turn out of history — the TUI/
@@ -136,11 +136,7 @@ function finalizeInterruptedTurn({
136
136
  const executionStarted = phase === 'tools' || observed?.eagerStarted === true;
137
137
  messages.push({
138
138
  role: 'tool',
139
- content: abortReason === 'process-crash'
140
- ? PROCESS_RESTART_TOOL_RESULT
141
- : (executionStarted
142
- ? TOOL_USE_REJECT_RESULT
143
- : STREAMING_INTERRUPTED_TOOL_RESULT),
139
+ content: INTERRUPTED_TOOL_RESULT,
144
140
  toolCallId: callId,
145
141
  toolKind: 'error',
146
142
  });
@@ -148,7 +144,7 @@ function finalizeInterruptedTurn({
148
144
  }
149
145
 
150
146
  const pairedMessages = sanitizeToolPairs(messages);
151
- // Claude Code omits the synthetic marker when a queued user submission
147
+ // The synthetic marker is omitted when a queued user submission
152
148
  // interrupted the active request; that queued message is the boundary.
153
149
  if (abortReason !== 'interrupt' && abortReason !== 'provider-error') {
154
150
  pairedMessages.push({
@@ -36,7 +36,7 @@ function normalizedIncompleteUsage(raw) {
36
36
  };
37
37
  }
38
38
 
39
- // Loop-level transport replay (codex retry_transport parity). The provider
39
+ // Loop-level transport replay. The provider
40
40
  // layer already retries transient failures with a ~15s total envelope
41
41
  // (PROVIDER_RETRY_BACKOFF_MS); a real network blip (router/VPN flap — the
42
42
  // 2026-08-02 17:37 incident also dropped the Discord gateway) outlasts it and
@@ -192,8 +192,8 @@ export async function sendWithRecovery(ctx) {
192
192
  // have been mid-sentence, and there is no completion signal that
193
193
  // says the work is done. Promoting it to a normal terminal response
194
194
  // silently reported truncated/aborted turns as finished. Keep it an
195
- // explicit failure (like codex's no-completed stream handling and
196
- // opencode's persisted partial-failure): the already-streamed text
195
+ // explicit failure (no completion signal means no completed turn,
196
+ // and the partial is persisted): the already-streamed text
197
197
  // is preserved by the interruption/error persistence path
198
198
  // (turn-interruption.mjs records the deltas and commits them as the
199
199
  // partial assistant message alongside the error), so nothing visible