mixdog 0.9.147 → 0.9.149

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 (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -26,6 +26,14 @@ export const SPAWN_PLATFORMS = {
26
26
  'win32-x64': 'mixdog-spawn-win32-x64.exe',
27
27
  };
28
28
 
29
+ export const TOKEN_PLATFORMS = {
30
+ 'darwin-arm64': 'mixdog-token-darwin-arm64.node',
31
+ 'darwin-x64': 'mixdog-token-darwin-x64.node',
32
+ 'linux-arm64': 'mixdog-token-linux-arm64.node',
33
+ 'linux-x64': 'mixdog-token-linux-x64.node',
34
+ 'win32-x64': 'mixdog-token-win32-x64.node',
35
+ };
36
+
29
37
  const RUNTIME_PLATFORMS = [
30
38
  'linux-x64',
31
39
  'linux-arm64',
@@ -43,6 +51,8 @@ const RUNTIME_MANIFEST_PATH = 'src/runtime/memory/data/runtime-manifest.json';
43
51
  const GRAPH_MANIFEST_PATH = 'src/runtime/agent/orchestrator/tools/graph-manifest.json';
44
52
  const SPAWN_MANIFEST_PATH = 'src/runtime/agent/orchestrator/tools/spawn-manifest.json';
45
53
  const SPAWN_CARGO_PATH = 'native/mixdog-spawn/Cargo.toml';
54
+ const TOKEN_MANIFEST_PATH = 'src/runtime/agent/orchestrator/tools/token-manifest.json';
55
+ const TOKEN_CARGO_PATH = 'native/mixdog-token/Cargo.toml';
46
56
  const PACKAGE_PATH = 'package.json';
47
57
 
48
58
  function assertPlainObject(value, label) {
@@ -125,14 +135,43 @@ export function validatePatchManifest(manifest, cargoToml) {
125
135
 
126
136
  export function validateRuntimeManifest(manifest) {
127
137
  assertPlainObject(manifest, 'Runtime manifest');
128
- if (!manifest.release_tag || !manifest.assets || typeof manifest.assets !== 'object') {
129
- throw new Error(`${RUNTIME_MANIFEST_PATH} is missing release_tag or assets`);
138
+ assertExactKeys(
139
+ manifest,
140
+ ['schema_version', 'generated_at', 'release_tag', 'pg', 'pgvector', 'assets'],
141
+ 'Runtime manifest',
142
+ );
143
+ if (manifest.schema_version !== 1) {
144
+ throw new Error('Runtime manifest schema_version must be 1');
145
+ }
146
+ if (typeof manifest.generated_at !== 'string' || !manifest.generated_at) {
147
+ throw new Error('Runtime manifest generated_at must be a non-empty string');
148
+ }
149
+ if (typeof manifest.release_tag !== 'string' || !/^runtime-v\d+\.\d+\.\d+$/.test(manifest.release_tag)) {
150
+ throw new Error(`Runtime manifest release_tag is invalid: ${manifest.release_tag}`);
151
+ }
152
+ assertPlainObject(manifest.pg, 'Runtime manifest pg');
153
+ assertExactKeys(manifest.pg, ['major', 'minor'], 'Runtime manifest pg');
154
+ if (!Number.isSafeInteger(manifest.pg.major) || manifest.pg.major < 1) {
155
+ throw new Error('Runtime manifest pg.major is invalid');
156
+ }
157
+ if (!Number.isSafeInteger(manifest.pg.minor) || manifest.pg.minor < 0) {
158
+ throw new Error('Runtime manifest pg.minor is invalid');
159
+ }
160
+ assertPlainObject(manifest.pgvector, 'Runtime manifest pgvector');
161
+ assertExactKeys(manifest.pgvector, ['version'], 'Runtime manifest pgvector');
162
+ if (typeof manifest.pgvector.version !== 'string' || !STRICT_VERSION.test(manifest.pgvector.version)) {
163
+ throw new Error(`Runtime manifest pgvector.version is invalid: ${manifest.pgvector.version}`);
130
164
  }
131
165
  assertPlainObject(manifest.assets, 'Runtime manifest assets');
132
166
  assertExactKeys(manifest.assets, RUNTIME_PLATFORMS, 'Runtime manifest assets');
133
- for (const [platform, asset] of Object.entries(manifest.assets)) {
134
- if (typeof asset?.url !== 'string' || new URL(asset.url).protocol !== 'https:') {
135
- throw new Error(`${platform}: invalid HTTPS asset URL`);
167
+ for (const platform of RUNTIME_PLATFORMS) {
168
+ const asset = manifest.assets[platform];
169
+ assertPlainObject(asset, `Runtime asset ${platform}`);
170
+ assertExactKeys(asset, ['url', 'sha256', 'size'], `Runtime asset ${platform}`);
171
+ const filename = `mixdog-runtime-${platform}-pg${manifest.pg.major}.${manifest.pg.minor}-pgvector${manifest.pgvector.version}.tar.gz`;
172
+ const expected = `https://github.com/tribgames/mixdog/releases/download/${manifest.release_tag}/${filename}`;
173
+ if (asset.url !== expected) {
174
+ throw new Error(`${platform}: runtime asset URL must be ${expected}`);
136
175
  }
137
176
  if (typeof asset.sha256 !== 'string' || !SHA256.test(asset.sha256)) {
138
177
  throw new Error(`${platform}: invalid sha256`);
@@ -219,6 +258,50 @@ export function validateSpawnManifest(manifest, cargoToml) {
219
258
  return manifest;
220
259
  }
221
260
 
261
+ export function validateTokenManifest(manifest, cargoToml) {
262
+ assertPlainObject(manifest, 'Token manifest');
263
+ assertExactKeys(manifest, ['version', '_comment', 'assets'], 'Token manifest');
264
+ if (typeof manifest.version !== 'string' || !STRICT_VERSION.test(manifest.version)) {
265
+ throw new Error(`Token manifest version is not strict MAJOR.MINOR.PATCH: ${manifest.version}`);
266
+ }
267
+ if (typeof manifest._comment !== 'string' || !manifest._comment) {
268
+ throw new Error('Token manifest _comment must be a non-empty string');
269
+ }
270
+ let inPackage = false;
271
+ let cargoVersion;
272
+ for (const line of String(cargoToml).split(/\r?\n/)) {
273
+ const section = line.match(/^\s*\[([^\]]+)\]\s*$/)?.[1];
274
+ if (section) {
275
+ inPackage = section === 'package';
276
+ continue;
277
+ }
278
+ if (inPackage) {
279
+ const version = line.match(/^\s*version\s*=\s*"([^"]+)"\s*$/)?.[1];
280
+ if (version) {
281
+ cargoVersion = version;
282
+ break;
283
+ }
284
+ }
285
+ }
286
+ if (!cargoVersion) throw new Error('Could not read [package] version from native/mixdog-token/Cargo.toml');
287
+ if (cargoVersion !== manifest.version) {
288
+ throw new Error(`Token Cargo version ${cargoVersion} does not match manifest version ${manifest.version}`);
289
+ }
290
+ assertPlainObject(manifest.assets, 'Token manifest assets');
291
+ assertExactKeys(manifest.assets, Object.keys(TOKEN_PLATFORMS), 'Token manifest assets');
292
+ for (const [platform, filename] of Object.entries(TOKEN_PLATFORMS)) {
293
+ const asset = manifest.assets[platform];
294
+ assertPlainObject(asset, `Token asset ${platform}`);
295
+ assertExactKeys(asset, ['url', 'sha256'], `Token asset ${platform}`);
296
+ const expected = `https://github.com/tribgames/mixdog/releases/download/token-v${manifest.version}/${filename}`;
297
+ if (asset.url !== expected) throw new Error(`${platform}: token asset URL must be ${expected}`);
298
+ if (typeof asset.sha256 !== 'string' || !SHA256.test(asset.sha256)) {
299
+ throw new Error(`${platform}: invalid token asset sha256`);
300
+ }
301
+ }
302
+ return manifest;
303
+ }
304
+
222
305
  async function downloadAndVerify(label, asset, fetchImpl, timeoutMs, maxAssetBytes) {
223
306
  console.log(`Downloading and verifying ${label}`);
224
307
  const controller = new AbortController();
@@ -307,22 +390,37 @@ export async function verifyReleaseAssets({
307
390
  graphManifestPath = GRAPH_MANIFEST_PATH,
308
391
  spawnManifestPath = SPAWN_MANIFEST_PATH,
309
392
  spawnCargoPath = SPAWN_CARGO_PATH,
393
+ tokenManifestPath = TOKEN_MANIFEST_PATH,
394
+ tokenCargoPath = TOKEN_CARGO_PATH,
310
395
  packagePath = PACKAGE_PATH,
311
396
  downloadOptions,
312
397
  } = {}) {
313
- const [patchSource, cargoToml, runtimeSource, graphSource, spawnSource, spawnCargo, packageSource] = await Promise.all([
398
+ const [
399
+ patchSource,
400
+ cargoToml,
401
+ runtimeSource,
402
+ graphSource,
403
+ spawnSource,
404
+ spawnCargo,
405
+ tokenSource,
406
+ tokenCargo,
407
+ packageSource,
408
+ ] = await Promise.all([
314
409
  readFile(patchManifestPath, 'utf8'),
315
410
  readFile(cargoPath, 'utf8'),
316
411
  readFile(runtimeManifestPath, 'utf8'),
317
412
  readFile(graphManifestPath, 'utf8'),
318
413
  readFile(spawnManifestPath, 'utf8'),
319
414
  readFile(spawnCargoPath, 'utf8'),
415
+ readFile(tokenManifestPath, 'utf8'),
416
+ readFile(tokenCargoPath, 'utf8'),
320
417
  readFile(packagePath, 'utf8'),
321
418
  ]);
322
419
  const patchManifest = validatePatchManifest(JSON.parse(patchSource), cargoToml);
323
420
  const runtimeManifest = validateRuntimeManifest(JSON.parse(runtimeSource));
324
421
  const graphManifest = validateGraphManifest(JSON.parse(graphSource), JSON.parse(packageSource));
325
422
  const spawnManifest = validateSpawnManifest(JSON.parse(spawnSource), spawnCargo);
423
+ const tokenManifest = validateTokenManifest(JSON.parse(tokenSource), tokenCargo);
326
424
  await verifyAssetDownloads(patchManifest.assets, downloadOptions);
327
425
  console.log(`Verified all bundled patch assets for patch-v${patchManifest.version}.`);
328
426
  await verifyAssetDownloads(runtimeManifest.assets, downloadOptions);
@@ -331,6 +429,8 @@ export async function verifyReleaseAssets({
331
429
  console.log(`Verified all bundled graph assets for graph-v${graphManifest.version}.`);
332
430
  await verifyAssetDownloads(spawnManifest.assets, downloadOptions);
333
431
  console.log(`Verified all bundled spawn assets for spawn-v${spawnManifest.version}.`);
432
+ await verifyAssetDownloads(tokenManifest.assets, downloadOptions);
433
+ console.log(`Verified all bundled token assets for token-v${tokenManifest.version}.`);
334
434
  }
335
435
 
336
436
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -0,0 +1,89 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readFile, stat } from 'node:fs/promises'
3
+ import { resolve } from 'node:path'
4
+
5
+ import {
6
+ VOICE_RUNTIME_CONFIG,
7
+ VOICE_RUNTIME_TAG,
8
+ assertVoiceRuntimeManifestIdentity,
9
+ buildVoiceRuntimePlatforms,
10
+ downloadReleaseAsset,
11
+ fetchGithubRelease,
12
+ releaseAssetSha256,
13
+ } from './generate-voice-runtime-manifest.mjs'
14
+
15
+ const delay = (ms) => new Promise((resolveDelay) => setTimeout(resolveDelay, ms))
16
+
17
+ async function verifyOnce(manifestPath, repository, token) {
18
+ const release = await fetchGithubRelease(repository, VOICE_RUNTIME_TAG, token)
19
+ if (!release.draft) throw new Error(`${VOICE_RUNTIME_TAG} must remain draft until verification passes`)
20
+
21
+ const manifestBytes = await readFile(manifestPath)
22
+ const manifest = JSON.parse(manifestBytes.toString('utf8'))
23
+ await assertVoiceRuntimeManifestIdentity(manifest, { repository })
24
+
25
+ const expectedNames = new Set(['voice-runtime-manifest.json'])
26
+ for (const platform of VOICE_RUNTIME_CONFIG.platforms) {
27
+ expectedNames.add(platform.asset)
28
+ expectedNames.add(`${platform.asset}.sha256`)
29
+ }
30
+ const voiceAssets = (release.assets || []).filter((asset) =>
31
+ asset.name === 'voice-runtime-manifest.json' || asset.name.startsWith('whisper-server-'))
32
+ const unexpected = voiceAssets.filter((asset) => !expectedNames.has(asset.name))
33
+ if (unexpected.length > 0) {
34
+ throw new Error(`release has unexpected voice assets: ${unexpected.map((asset) => asset.name).join(', ')}`)
35
+ }
36
+ for (const name of expectedNames) {
37
+ if (!voiceAssets.some((asset) => asset.name === name)) {
38
+ throw new Error(`release is missing required asset ${name}`)
39
+ }
40
+ }
41
+
42
+ const generatedPlatforms = buildVoiceRuntimePlatforms(release.assets, {
43
+ repository,
44
+ tag: VOICE_RUNTIME_TAG,
45
+ })
46
+ if (JSON.stringify(generatedPlatforms) !== JSON.stringify(manifest.platforms)) {
47
+ throw new Error('released runtime assets do not match the generated manifest')
48
+ }
49
+
50
+ const manifestAsset = voiceAssets.find((asset) => asset.name === 'voice-runtime-manifest.json')
51
+ const localManifestHash = createHash('sha256').update(manifestBytes).digest('hex')
52
+ const localManifestSize = (await stat(manifestPath)).size
53
+ if (manifestAsset.size !== localManifestSize || releaseAssetSha256(manifestAsset) !== localManifestHash) {
54
+ throw new Error('uploaded voice-runtime-manifest.json does not match the verified local manifest')
55
+ }
56
+
57
+ for (const platform of VOICE_RUNTIME_CONFIG.platforms) {
58
+ const archive = voiceAssets.find((asset) => asset.name === platform.asset)
59
+ const sidecar = voiceAssets.find((asset) => asset.name === `${platform.asset}.sha256`)
60
+ const sidecarText = (await downloadReleaseAsset(sidecar, token)).toString('utf8').trim()
61
+ const declaredHash = /^([a-f0-9]{64})\s+/i.exec(sidecarText)?.[1]?.toLowerCase()
62
+ const archiveHash = releaseAssetSha256(archive)
63
+ if (declaredHash !== archiveHash) {
64
+ throw new Error(`${sidecar.name} does not match ${archive.name}`)
65
+ }
66
+ }
67
+ }
68
+
69
+ async function main() {
70
+ const manifestPath = resolve(process.argv[2] || 'src/runtime/channels/data/voice-runtime-manifest.json')
71
+ const repository = String(process.env.GITHUB_REPOSITORY || 'tribgames/mixdog')
72
+ let lastError
73
+ for (let attempt = 1; attempt <= 5; attempt++) {
74
+ try {
75
+ await verifyOnce(manifestPath, repository, process.env.GITHUB_TOKEN)
76
+ console.log(`verified complete draft release ${VOICE_RUNTIME_TAG}`)
77
+ return
78
+ } catch (error) {
79
+ lastError = error
80
+ if (attempt < 5) await delay(attempt * 2_000)
81
+ }
82
+ }
83
+ throw lastError
84
+ }
85
+
86
+ main().catch((error) => {
87
+ console.error(error?.stack || error)
88
+ process.exitCode = 1
89
+ })
@@ -0,0 +1,52 @@
1
+ {
2
+ "runtimeVersion": "1.9.2",
3
+ "whisperCommit": "306c88f4d1286aec1bf96e544632897886af5501",
4
+ "vulkanSdkVersion": "1.4.309.0",
5
+ "platforms": [
6
+ {
7
+ "key": "win32-x64",
8
+ "os": "win32",
9
+ "arch": "x64",
10
+ "backend": "vulkan",
11
+ "runner": "windows-2022",
12
+ "asset": "whisper-server-win32-x64-vulkan.zip",
13
+ "executable": "whisper-server.exe"
14
+ },
15
+ {
16
+ "key": "linux-x64",
17
+ "os": "linux",
18
+ "arch": "x64",
19
+ "backend": "vulkan",
20
+ "runner": "ubuntu-24.04",
21
+ "asset": "whisper-server-linux-x64-vulkan.zip",
22
+ "executable": "whisper-server"
23
+ },
24
+ {
25
+ "key": "linux-arm64",
26
+ "os": "linux",
27
+ "arch": "arm64",
28
+ "backend": "vulkan",
29
+ "runner": "ubuntu-24.04-arm",
30
+ "asset": "whisper-server-linux-arm64-vulkan.zip",
31
+ "executable": "whisper-server"
32
+ },
33
+ {
34
+ "key": "darwin-arm64",
35
+ "os": "darwin",
36
+ "arch": "arm64",
37
+ "backend": "metal",
38
+ "runner": "macos-14",
39
+ "asset": "whisper-server-darwin-arm64-metal.zip",
40
+ "executable": "whisper-server"
41
+ },
42
+ {
43
+ "key": "darwin-x64",
44
+ "os": "darwin",
45
+ "arch": "x64",
46
+ "backend": "cpu",
47
+ "runner": "macos-15-intel",
48
+ "asset": "whisper-server-darwin-x64-cpu.zip",
49
+ "executable": "whisper-server"
50
+ }
51
+ ]
52
+ }
package/src/app.mjs CHANGED
@@ -120,17 +120,54 @@ export async function run(argv = [], classifiedInvocation = null) {
120
120
  // in src. Sample a few hot files — a newer source than the bundle means the
121
121
  // running behavior will NOT match the tree, so warn loudly.
122
122
  try {
123
- const { statSync } = await import('node:fs');
123
+ const { readdirSync, statSync } = await import('node:fs');
124
124
  const bundleMtime = statSync(bundle).mtimeMs;
125
125
  const hotSources = [
126
126
  join(__dirname, 'runtime', 'agent', 'orchestrator', 'session', 'agent-loop.mjs'),
127
127
  join(__dirname, 'runtime', 'agent', 'orchestrator', 'session', 'loop', 'stored-tool-args.mjs'),
128
- join(__dirname, 'tui', 'engine', 'session-api-ext.mjs'),
129
128
  join(__dirname, 'standalone', 'agent-tool.mjs'),
130
129
  ];
131
- const stale = hotSources.some((file) => {
132
- try { return statSync(file).mtimeMs > bundleMtime + 1_000; } catch { return false; }
133
- });
130
+ // The TUI half of the guard used to sample ONE path that does not exist
131
+ // (tui/engine/session-api-ext.mjs), so every real src/tui edit ran silently
132
+ // against an old bundle. Walk the actual bundled tree instead — dist (the
133
+ // build output) and dev (excluded from the package) are not inputs.
134
+ const tuiSourceNewestMtime = (() => {
135
+ const skipDirs = new Set(['dist', 'dev', 'node_modules']);
136
+ // Ceilings, not expectations: the bundled tree is currently 2 levels
137
+ // deep with ~150 files, so depth 12 reaches every source that can end up
138
+ // in the bundle (the old depth-6 cut would have started missing files as
139
+ // soon as the tree grew), while the visit budget keeps this boot-time
140
+ // walk bounded on any pathological tree. The guard is advisory, so a
141
+ // budget-truncated walk still reports the newest mtime it did see.
142
+ const MAX_DEPTH = 12;
143
+ const MAX_VISITS = 5000;
144
+ let visits = 0;
145
+ let newest = 0;
146
+ const walk = (dir, depth) => {
147
+ if (depth > MAX_DEPTH || visits >= MAX_VISITS) return;
148
+ let entries;
149
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
150
+ for (const entry of entries) {
151
+ if (visits >= MAX_VISITS) return;
152
+ visits += 1;
153
+ if (entry.isDirectory()) {
154
+ if (!skipDirs.has(entry.name)) walk(join(dir, entry.name), depth + 1);
155
+ continue;
156
+ }
157
+ if (!/\.(mjs|jsx|js)$/.test(entry.name)) continue;
158
+ try {
159
+ const mtime = statSync(join(dir, entry.name)).mtimeMs;
160
+ if (mtime > newest) newest = mtime;
161
+ } catch { /* unreadable file is not evidence of staleness */ }
162
+ }
163
+ };
164
+ walk(join(__dirname, 'tui'), 0);
165
+ return newest;
166
+ })();
167
+ const stale = tuiSourceNewestMtime > bundleMtime + 1_000
168
+ || hotSources.some((file) => {
169
+ try { return statSync(file).mtimeMs > bundleMtime + 1_000; } catch { return false; }
170
+ });
134
171
  if (stale) {
135
172
  process.stderr.write(
136
173
  'mixdog: TUI bundle is OLDER than runtime sources — behavior will not match the tree.\n'
@@ -13,7 +13,6 @@ import {
13
13
  } from './runtime/shared/pristine-execution.mjs';
14
14
  import { hasActiveBackgroundTasks } from './runtime/shared/background-tasks.mjs';
15
15
  import { installProcessSignalCleanup } from './runtime/shared/process-shutdown.mjs';
16
- import { sleep } from './runtime/shared/sleep.mjs';
17
16
  import { stopStandaloneMemoryRuntimesForProcess } from './standalone/memory-runtime-proxy.mjs';
18
17
  import { applyUsageDelta, createSessionStats } from './ui/session-stats.mjs';
19
18
 
@@ -21,6 +20,21 @@ function clean(value) {
21
20
  return String(value ?? '').trim();
22
21
  }
23
22
 
23
+ export async function prewarmHeadlessSearch(cwd, {
24
+ loadNativeSearch = () => import('./runtime/agent/orchestrator/tools/builtin/native-search-client.mjs'),
25
+ loadListTool = () => import('./runtime/agent/orchestrator/tools/builtin/list-tool.mjs'),
26
+ } = {}) {
27
+ const root = clean(cwd) || process.cwd();
28
+ const [nativeSearch, listTool] = await Promise.all([
29
+ loadNativeSearch(),
30
+ loadListTool(),
31
+ ]);
32
+ await Promise.all([
33
+ nativeSearch.warmNativeSearchServer(),
34
+ listTool.prewarmFindEnumeration(root),
35
+ ]);
36
+ }
37
+
24
38
  function nonNegativeNumber(value) {
25
39
  const number = Number(value);
26
40
  return Number.isFinite(number) ? Math.max(0, number) : 0;
@@ -424,6 +438,8 @@ function createJsonLifecycle({
424
438
  }
425
439
  const durationMs = Math.max(0, completedAt - turnStartedAt);
426
440
  const usage = usageSummary(stats, toolCallCount);
441
+ const terminationReason = clean(result?.terminationReason) || null;
442
+ const isApiError = terminationReason === 'refusal';
427
443
  emit({
428
444
  type: 'turn.completed',
429
445
  thread_id: threadId,
@@ -441,7 +457,7 @@ function createJsonLifecycle({
441
457
  turn_id: turnId,
442
458
  session_id: threadId,
443
459
  model: resolvedModel,
444
- is_error: false,
460
+ is_error: isApiError,
445
461
  duration_ms: durationMs,
446
462
  duration_api_ms: providerDurationMs,
447
463
  num_turns: 1,
@@ -449,6 +465,7 @@ function createJsonLifecycle({
449
465
  tool_calls: toolCallCount,
450
466
  result: finalText,
451
467
  stop_reason: result?.stopReason ?? result?.stop_reason ?? null,
468
+ termination_reason: terminationReason,
452
469
  usage,
453
470
  }, completedAt);
454
471
  },
@@ -491,28 +508,17 @@ function createJsonLifecycle({
491
508
  };
492
509
  }
493
510
 
494
- export async function waitForTrackedTasks({
495
- sessionId,
496
- clientHostPid = process.pid,
497
- hasActiveTasks = hasActiveBackgroundTasks,
498
- pollMs = 100,
499
- } = {}) {
500
- const scope = {
501
- ...(clean(sessionId) ? { callerSessionId: clean(sessionId) } : {}),
502
- clientHostPid,
503
- };
504
- while (hasActiveTasks(scope)) {
505
- await sleep(Math.max(10, Number(pollMs) || 100));
506
- }
507
- }
508
-
509
- function writeUsageDocument(path, stats, runtime, toolCallCount = 0) {
511
+ function writeUsageDocument(path, stats, runtime, toolCallCount = 0, observedModels = []) {
510
512
  const target = clean(path);
511
513
  if (!target) return;
514
+ const models = [
515
+ clean(runtime?.model),
516
+ ...Array.from(observedModels || [], clean),
517
+ ].filter((value, index, values) => value && values.indexOf(value) === index);
512
518
  const session = {
513
519
  sessionId: clean(runtime?.id),
514
520
  agentRole: 'primary',
515
- models: [clean(runtime?.model)].filter(Boolean),
521
+ models,
516
522
  inputTokens: stats.inputTokens,
517
523
  cacheTokens: stats.cachedTokens,
518
524
  cacheWriteTokens: stats.cacheWriteTokens,
@@ -554,7 +560,6 @@ export async function runHeadlessExec({
554
560
  memoryRuntimeCleanup = stopStandaloneMemoryRuntimesForProcess,
555
561
  hasActiveTasks = hasActiveBackgroundTasks,
556
562
  installSignalCleanupFn = installProcessSignalCleanup,
557
- idlePollMs = 100,
558
563
  } = {}) {
559
564
  const prompt = clean(message);
560
565
  if (!prompt) {
@@ -568,6 +573,7 @@ export async function runHeadlessExec({
568
573
  }
569
574
 
570
575
  const stats = createSessionStats();
576
+ const observedModels = new Set();
571
577
  const lifecycle = json ? createJsonLifecycle({
572
578
  write,
573
579
  stats,
@@ -589,11 +595,24 @@ export async function runHeadlessExec({
589
595
  let resultText = '';
590
596
  let executionError = null;
591
597
  let code = 1;
598
+ const taskScopeFor = (session) => ({
599
+ ...(clean(session?.id) ? { callerSessionId: clean(session.id) } : {}),
600
+ clientHostPid: session?.clientHostPid,
601
+ });
592
602
  const cleanup = (reason = 'exec-exit') => {
593
603
  cleanupPromise ??= (async () => {
594
604
  const errors = [];
595
605
  try {
596
- if (runtime) await runtime.close(reason);
606
+ // Background jobs the model left running ON PURPOSE (a task's required
607
+ // server) must outlive this process: reaping them at exit destroys the
608
+ // very artifact the caller asked for. Detach instead of reap. cli.mjs
609
+ // exits explicitly, so surviving children cannot hold the process open.
610
+ if (runtime) {
611
+ await runtime.close(
612
+ reason,
613
+ hasActiveTasks(taskScopeFor(runtime)) ? { keepBackgroundWork: true } : {},
614
+ );
615
+ }
597
616
  } catch (error) {
598
617
  errors.push(error);
599
618
  }
@@ -624,6 +643,13 @@ export async function runHeadlessExec({
624
643
 
625
644
  try {
626
645
  boundary = boundaryFactory({ provider, model, effort, fast });
646
+ // Fire-and-forget search prewarm, matching the long-lived host. Warm both
647
+ // the resident server and this Project's complete path inventory so the
648
+ // first normal find/glob does real indexed work instead of paying startup
649
+ // or a cold tree walk. Non-fatal by construction.
650
+ if (!/^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_DISABLE_TOOL_PREWARM || '').trim())) {
651
+ void prewarmHeadlessSearch(cwd).catch(() => {});
652
+ }
627
653
  signalCleanup = installSignalCleanupFn({
628
654
  name: 'mixdog-exec',
629
655
  timeoutMs: 20_000,
@@ -666,6 +692,8 @@ export async function runHeadlessExec({
666
692
  const askOptions = {
667
693
  onTextReset: () => true,
668
694
  onUsageDelta: (delta) => {
695
+ const observedModel = clean(delta?.model);
696
+ if (observedModel) observedModels.add(observedModel);
669
697
  applyUsageDelta(stats, delta);
670
698
  lifecycle?.onUsageDelta(delta);
671
699
  },
@@ -682,17 +710,14 @@ export async function runHeadlessExec({
682
710
  } : {}),
683
711
  };
684
712
  ({ result } = await runtime.ask(prompt, askOptions));
685
- const taskScope = {
686
- ...(clean(runtime.id) ? { callerSessionId: clean(runtime.id) } : {}),
687
- clientHostPid: runtime.clientHostPid,
688
- };
689
- while (completionPending || hasActiveTasks(taskScope)) {
690
- await waitForTrackedTasks({
691
- sessionId: runtime.id,
692
- clientHostPid: runtime.clientHostPid,
693
- hasActiveTasks,
694
- pollMs: idlePollMs,
695
- });
713
+ // Exit NEVER waits on running background work. A job the model left
714
+ // running on purpose (a task's server) has no end, so waiting for it spent
715
+ // the whole remaining budget on an already-finished turn — 2026-08-23 full
716
+ // run: two trials, ~85s of real work each, 900s burned. Claude Code and
717
+ // Codex both leave the moment the turn ends. Completions that ALREADY
718
+ // arrived still get their follow-up turn below: that is a queued message,
719
+ // not a wait, so no result the model can act on is dropped.
720
+ while (completionPending) {
696
721
  completionPending = false;
697
722
  ({ result } = await runtime.ask('', askOptions));
698
723
  }
@@ -716,6 +741,7 @@ export async function runHeadlessExec({
716
741
  stats,
717
742
  runtime,
718
743
  lifecycle?.toolCallCount || 0,
744
+ observedModels,
719
745
  );
720
746
  } catch (error) {
721
747
  writeErr(`mixdog: usage log write failed: ${error?.message || error}\n`);