mixdog 1.3.1 → 1.3.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 (468) hide show
  1. package/README.md +8 -4
  2. package/package.json +8 -8
  3. package/scripts/native-release-status.mjs +238 -0
  4. package/scripts/verify-runtime-dependency-versions.mjs +158 -0
  5. package/src/agents/reviewer/AGENT.md +5 -7
  6. package/src/agents/reviewer/agent.json +1 -1
  7. package/src/defaults/shipped-history.json +9 -1
  8. package/src/defaults/skills/browser-use/SKILL.md +6 -3
  9. package/src/defaults/skills/local-provider/SKILL.md +33 -6
  10. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  11. package/src/defaults/skills/setup/SKILL.md +23 -1
  12. package/src/defaults/skills/setup/references/actions.md +50 -3
  13. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  14. package/src/defaults/skills/setup-guide/SKILL.md +7 -3
  15. package/src/defaults/skills/visualize/SKILL.md +9 -6
  16. package/src/defaults/skills/visualize/references/craft.md +8 -1
  17. package/src/help.mjs +1 -1
  18. package/src/lib/keychain-cjs.cjs +4 -1
  19. package/src/lib/rules-builder.cjs +14 -7
  20. package/src/rules/agent-mode/AGENT_MODE.md +8 -5
  21. package/src/rules/agent-mode/MIXY.md +6 -8
  22. package/src/rules/lead/LEAD.md +7 -10
  23. package/src/rules/shared/10-tool-workflow.md +4 -0
  24. package/src/rules/shared/20-research.md +2 -0
  25. package/src/rules/shared/45-destructive.md +5 -1
  26. package/src/rules/shared/60-verification.md +6 -4
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  30. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  31. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  32. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  33. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  35. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  36. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  37. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  38. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +9 -2
  39. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -5
  40. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  41. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  42. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +84 -32
  43. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
  44. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
  45. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +84 -42
  48. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  50. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  51. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +27 -8
  52. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  54. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  55. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
  56. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  57. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  59. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +16 -0
  60. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  62. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  64. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  65. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +2 -1
  68. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  69. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  70. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  72. package/src/runtime/agent/orchestrator/providers/registry.mjs +19 -0
  73. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
  74. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  75. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
  78. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  79. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
  81. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -6
  82. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  83. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +52 -6
  85. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +35 -13
  86. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +7 -6
  87. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  89. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +8 -3
  90. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  92. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  93. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  94. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -2
  95. package/src/runtime/agent/orchestrator/session/agent-room.mjs +196 -45
  96. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  97. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  98. package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
  99. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +45 -19
  100. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
  102. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +185 -37
  103. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +6 -2
  107. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +4 -1
  108. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  110. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +240 -0
  111. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +102 -35
  112. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  113. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +19 -9
  114. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  115. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  116. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  117. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  118. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +199 -109
  119. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
  120. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
  123. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
  124. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  125. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +5 -5
  126. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -0
  127. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  128. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +33 -8
  129. package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
  130. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +27 -0
  131. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  132. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  134. package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
  135. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
  136. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
  137. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  138. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +27 -14
  139. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +14 -0
  140. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  141. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
  142. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  143. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
  144. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +115 -0
  145. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  146. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
  147. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  148. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  149. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  150. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
  151. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  152. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  153. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  154. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
  155. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +55 -27
  156. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +4 -9
  157. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +14 -1
  158. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
  159. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  160. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  161. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  162. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +52 -11
  163. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  164. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +18 -3
  165. package/src/runtime/agent/orchestrator/session/routine-result.mjs +126 -12
  166. package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
  167. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +16 -3
  168. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  169. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  170. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  171. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  172. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  173. package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
  174. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
  175. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
  176. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +15 -5
  177. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +49 -1
  178. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +80 -1
  179. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  180. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  181. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
  182. package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
  183. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
  184. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  185. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  186. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  187. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  188. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +35 -5
  189. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +42 -2
  190. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +47 -14
  191. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  192. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +6 -3
  193. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  194. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  195. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  196. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  197. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
  198. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
  199. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +10 -12
  200. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  201. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  202. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  203. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  204. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  205. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +42 -23
  206. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  207. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  208. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  209. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  210. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  211. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  212. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  213. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  214. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  215. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  216. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  217. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  218. package/src/runtime/attachments/legacy-office.mjs +110 -0
  219. package/src/runtime/attachments/limits.mjs +5 -1
  220. package/src/runtime/attachments/office-extract.mjs +3 -6
  221. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  222. package/src/runtime/attachments/prompt-restore.mjs +70 -0
  223. package/src/runtime/attachments/store.mjs +109 -33
  224. package/src/runtime/browser-bridge/client.mjs +73 -20
  225. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  226. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  227. package/src/runtime/channels/lib/state-file.mjs +15 -15
  228. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  229. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  230. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  231. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  232. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  233. package/src/runtime/effort-judge/judge-client.mjs +66 -16
  234. package/src/runtime/effort-judge/model-install.mjs +47 -7
  235. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  236. package/src/runtime/local-provider/catalog.mjs +5 -2
  237. package/src/runtime/local-provider/registered-models.mjs +64 -27
  238. package/src/runtime/local-provider/server.mjs +2 -3
  239. package/src/runtime/media/adapters/image-api.mjs +89 -0
  240. package/src/runtime/media/auth.mjs +64 -1
  241. package/src/runtime/media/catalog.mjs +66 -4
  242. package/src/runtime/media/jobs.mjs +4 -0
  243. package/src/runtime/media/lanes.mjs +46 -5
  244. package/src/runtime/media/media-usage.mjs +17 -0
  245. package/src/runtime/media/renditions.mjs +15 -15
  246. package/src/runtime/media/store.mjs +36 -35
  247. package/src/runtime/media/tool.mjs +8 -1
  248. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  249. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  250. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  251. package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
  252. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  253. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  254. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  255. package/src/runtime/office/core/journal.mjs +7 -14
  256. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  257. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  258. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  259. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  260. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  261. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  262. package/src/runtime/office/portable/portable-soffice.mjs +135 -29
  263. package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
  264. package/src/runtime/shared/agent-avatar.mjs +3 -2
  265. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  266. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  267. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  268. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  269. package/src/runtime/shared/agent-room-host.d.mts +31 -0
  270. package/src/runtime/shared/agent-room-host.mjs +58 -0
  271. package/src/runtime/shared/agent-store.mjs +24 -7
  272. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  273. package/src/runtime/shared/atomic-file.d.mts +50 -0
  274. package/src/runtime/shared/atomic-file.mjs +394 -132
  275. package/src/runtime/shared/background-tasks.mjs +7 -4
  276. package/src/runtime/shared/bot-hops.mjs +35 -0
  277. package/src/runtime/shared/bot-split-migration.mjs +1 -1
  278. package/src/runtime/shared/bounded-download.mjs +4 -2
  279. package/src/runtime/shared/config.mjs +39 -9
  280. package/src/runtime/shared/defaults-separation.mjs +6 -2
  281. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  282. package/src/runtime/shared/err-text.mjs +4 -0
  283. package/src/runtime/shared/error-presentation.mjs +22 -4
  284. package/src/runtime/shared/file-has-data.mjs +28 -0
  285. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  286. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  287. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  288. package/src/runtime/shared/projects.mjs +50 -41
  289. package/src/runtime/shared/provider-accounts.mjs +17 -12
  290. package/src/runtime/shared/redact-directories.mjs +18 -0
  291. package/src/runtime/shared/remove-directory.mjs +28 -0
  292. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  293. package/src/runtime/shared/routine-trigger.mjs +14 -6
  294. package/src/runtime/shared/run-child.d.mts +3 -0
  295. package/src/runtime/shared/run-child.mjs +24 -3
  296. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  297. package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
  298. package/src/runtime/shared/session-sidebar-host.mjs +16 -3
  299. package/src/runtime/shared/singleton-owner.mjs +7 -6
  300. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  301. package/src/runtime/shared/staged-update.mjs +4 -2
  302. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  303. package/src/runtime/shared/tool-execution-contract.mjs +10 -0
  304. package/src/runtime/shared/tool-work-units.mjs +12 -9
  305. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  306. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  307. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  308. package/src/runtime/shared/turn-snapshot.mjs +277 -124
  309. package/src/runtime/shared/user-cwd.mjs +9 -4
  310. package/src/runtime/shared/user-data-guard.mjs +92 -61
  311. package/src/runtime/shared/utf8-cut.mjs +10 -0
  312. package/src/runtime/shared/worktrees/activity-gate.mjs +251 -61
  313. package/src/runtime/shared/worktrees/contract.mjs +11 -3
  314. package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
  315. package/src/runtime/shared/worktrees/manager.mjs +419 -52
  316. package/src/runtime/shared/worktrees/registry.mjs +29 -27
  317. package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
  318. package/src/runtime/tidy/core-install.mjs +26 -4
  319. package/src/runtime/tidy/install.mjs +91 -23
  320. package/src/runtime/tidy/process.mjs +41 -8
  321. package/src/runtime/tidy/resolve.mjs +10 -7
  322. package/src/runtime/web-search/lib/config.mjs +4 -16
  323. package/src/session-runtime/agent-home-delivery.mjs +542 -204
  324. package/src/session-runtime/agent-profile-changes.mjs +16 -0
  325. package/src/session-runtime/agent-room-delivery.mjs +81 -35
  326. package/src/session-runtime/agent-task-sessions.mjs +811 -179
  327. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  328. package/src/session-runtime/boot/apis.mjs +48 -24
  329. package/src/session-runtime/boot/config.mjs +1 -0
  330. package/src/session-runtime/boot/facade.mjs +16 -7
  331. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  332. package/src/session-runtime/boot/providers.mjs +2 -2
  333. package/src/session-runtime/boot/shared.mjs +12 -6
  334. package/src/session-runtime/boot/tools.mjs +23 -3
  335. package/src/session-runtime/bot-status.mjs +432 -0
  336. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  337. package/src/session-runtime/config-lifecycle.mjs +60 -55
  338. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  339. package/src/session-runtime/held-requests.mjs +31 -33
  340. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  341. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  342. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +24 -26
  343. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  344. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  345. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  346. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  347. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  348. package/src/session-runtime/media-api.mjs +5 -2
  349. package/src/session-runtime/model-recency.mjs +1 -0
  350. package/src/session-runtime/model-route/route-tuning.mjs +4 -1
  351. package/src/session-runtime/model-route/set-route.mjs +61 -1
  352. package/src/session-runtime/owned-automation-run.mjs +8 -1
  353. package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
  354. package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
  355. package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
  356. package/src/session-runtime/route-resolution.mjs +16 -8
  357. package/src/session-runtime/route-state.mjs +1 -59
  358. package/src/session-runtime/routine-home-turn.mjs +5 -3
  359. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  360. package/src/session-runtime/runtime-paths.mjs +11 -5
  361. package/src/session-runtime/runtime-review-api.mjs +4 -1
  362. package/src/session-runtime/runtime-tunables.mjs +6 -0
  363. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  364. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  365. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  366. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  367. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +34 -19
  368. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  369. package/src/session-runtime/services/agent-tool/send-flow.mjs +25 -21
  370. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +7 -0
  371. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  372. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +8 -5
  373. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  374. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  375. package/src/session-runtime/services/agent-tool.mjs +6 -0
  376. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  377. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  378. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  379. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  380. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  381. package/src/session-runtime/services/provider-admin.mjs +26 -6
  382. package/src/session-runtime/services/schedule-access.mjs +33 -4
  383. package/src/session-runtime/services/seeds.mjs +4 -3
  384. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  385. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  386. package/src/session-runtime/services/worktree-access.mjs +27 -0
  387. package/src/session-runtime/session-create/materialize.mjs +2 -3
  388. package/src/session-runtime/session-create/session-options.mjs +15 -2
  389. package/src/session-runtime/session-hooks.mjs +9 -17
  390. package/src/session-runtime/settings-builtin-tools-api.mjs +65 -1
  391. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  392. package/src/session-runtime/setup-tool/executor.mjs +12 -2
  393. package/src/session-runtime/setup-tool/settings-contract.mjs +34 -1
  394. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  395. package/src/session-runtime/skills-api.mjs +12 -5
  396. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +349 -0
  397. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  398. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  399. package/src/session-runtime/tool-policy/policy-stage.mjs +183 -0
  400. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  401. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  402. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  403. package/src/session-runtime/tool-policy-refresh.mjs +48 -369
  404. package/src/session-runtime/tool-surface.mjs +10 -4
  405. package/src/session-runtime/turn/session-ops.mjs +8 -0
  406. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +95 -88
  407. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  408. package/src/session-runtime/turn/turn-run.mjs +7 -2
  409. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  410. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +527 -333
  411. package/src/session-runtime/workflow-agents-api/agent-route.mjs +85 -63
  412. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  413. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  414. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  415. package/src/session-runtime/workspace/transition-steps.mjs +1 -18
  416. package/src/session-runtime/workspace/transitions.mjs +279 -67
  417. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  418. package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
  419. package/src/standalone/daemon-agent-control.mjs +4 -1
  420. package/src/standalone/daemon-lag-profiler.mjs +3 -1
  421. package/src/standalone/daemon-session-control.mjs +173 -15
  422. package/src/standalone/daemon-telemetry.mjs +9 -0
  423. package/src/standalone/daemon.mjs +33 -10
  424. package/src/standalone/local-session-runtime.mjs +33 -16
  425. package/src/standalone/session-client.mjs +7 -2
  426. package/src/standalone/session-protocol.mjs +8 -0
  427. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
  428. package/src/standalone/session-service/agent-route-follow.mjs +69 -0
  429. package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
  430. package/src/standalone/session-service.mjs +21 -0
  431. package/src/standalone/session-state-projection.mjs +19 -9
  432. package/src/standalone/session-tool-output.mjs +36 -9
  433. package/src/standalone/staged-import.mjs +47 -0
  434. package/src/tui/App.jsx +2 -0
  435. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  436. package/src/tui/app/message-selector.mjs +15 -2
  437. package/src/tui/app/model-options.mjs +16 -2
  438. package/src/tui/app/model-picker.mjs +2 -1
  439. package/src/tui/app/route-pickers.mjs +3 -1
  440. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  441. package/src/tui/app/slash-commands.mjs +1 -1
  442. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  443. package/src/tui/dist/index.mjs +617 -369
  444. package/src/tui/paste-attachments.mjs +0 -1
  445. package/src/tui/paste-text-policy.mjs +4 -5
  446. package/src/tui/prompt-history-store.mjs +16 -21
  447. package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
  448. package/src/tui/session/context-state.mjs +4 -0
  449. package/src/tui/session/notice-surface.mjs +3 -0
  450. package/src/tui/session/session-api/intake/abort.mjs +52 -18
  451. package/src/tui/session/session-api/intake/rewind.mjs +39 -4
  452. package/src/tui/session/session-api/media.mjs +2 -1
  453. package/src/tui/session/session-api/routes-agent-definitions.mjs +76 -0
  454. package/src/tui/session/session-api/routes-write.mjs +203 -0
  455. package/src/tui/session/session-api/routes.mjs +23 -247
  456. package/src/tui/session/session-api/settings.mjs +14 -1
  457. package/src/tui/session/session-flow/drain.mjs +28 -3
  458. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  459. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  460. package/src/tui/session/session-flow/steering.mjs +8 -3
  461. package/src/tui/session/tool-approval.mjs +142 -14
  462. package/src/tui/session/transcript-intake.mjs +2 -0
  463. package/src/tui/session/turn.mjs +23 -14
  464. package/src/tui/transcript-tool-failures.mjs +8 -2
  465. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  466. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  467. package/src/rules/agent-mode/ROUTINE.md +0 -6
  468. package/src/session-runtime/room-member-gate.mjs +0 -48
@@ -6,15 +6,18 @@
6
6
  * transaction ledger that makes creation rollback and deletion resumable:
7
7
  * which resources this transaction created ('pending' is written before a
8
8
  * resource is made, 'created' after), the deletion decision and any recovery
9
- * diagnostics. Writes are atomic (temp file + rename) under a file lock; an
10
- * unreadable registry is an error, never silently replaced.
9
+ * diagnostics. Writes are atomic (temp file + rename) under a file lock and keep
10
+ * the replaced version as registry.json.bak. A damaged registry (NUL, truncated,
11
+ * malformed) never blocks worktree operations: its bytes are kept aside as
12
+ * registry.json.corrupt-<ts>, the last good copy is restored when there is one,
13
+ * and otherwise the registry starts empty (worktree folders on disk are never
14
+ * touched by that).
11
15
  */
12
- import { readFile } from 'node:fs/promises';
13
16
  import { join } from 'node:path';
14
- import { parseJsonText, withFileLock, writeJsonAtomicAsync } from '../atomic-file.mjs';
17
+ import { readJsonState, readJsonStateSync, updateJsonAtomic } from '../atomic-file.mjs';
15
18
  import { isPlainObject } from '../object.mjs';
16
19
  import { resolvePluginData } from '../plugin-paths.mjs';
17
- import { normalizeWorktreeEntry, WORKTREE_STORE_DIR } from './contract.mjs';
20
+ import { normalizeWorktreeEntry, WORKTREE_SNAPSHOT_KIND, WORKTREE_STORE_DIR } from './contract.mjs';
18
21
 
19
22
  const REGISTRY_VERSION = 1;
20
23
 
@@ -30,24 +33,21 @@ function emptyDoc() {
30
33
  return { version: REGISTRY_VERSION, entries: {} };
31
34
  }
32
35
 
33
- async function readDoc(path) {
34
- let text;
35
- try {
36
- text = await readFile(path, 'utf8');
37
- } catch (error) {
38
- if (error?.code === 'ENOENT') return emptyDoc();
39
- throw error;
40
- }
41
- const value = parseJsonText(text);
42
- if (!isPlainObject(value) || !isPlainObject(value.entries)) {
43
- throw new Error(`Worktree registry ${path} is malformed.`);
44
- }
36
+ const isRegistryDoc = (value) => isPlainObject(value) && isPlainObject(value.entries);
37
+
38
+ function validateDoc(value) {
39
+ if (!isRegistryDoc(value)) throw new Error('Worktree registry is malformed.');
45
40
  return value;
46
41
  }
47
42
 
48
43
  /** Snapshot of every record (plain clones). */
49
44
  export async function readRegistry() {
50
- return readDoc(registryPath());
45
+ return (await readJsonState(registryPath(), { validate: validateDoc })) ?? emptyDoc();
46
+ }
47
+
48
+ /** readRegistry for synchronous callers (the workspace admission gate). */
49
+ export function readRegistrySync() {
50
+ return readJsonStateSync(registryPath(), { validate: validateDoc }) ?? emptyDoc();
51
51
  }
52
52
 
53
53
  /**
@@ -58,17 +58,17 @@ export async function readRegistry() {
58
58
  * @returns {Promise<T>}
59
59
  */
60
60
  export async function mutateRegistry(mutate) {
61
- const path = registryPath();
62
- return withFileLock(
63
- `${path}.lock`,
64
- async () => {
65
- const doc = await readDoc(path);
66
- const result = mutate(doc);
67
- await writeJsonAtomicAsync(path, doc, { lock: false });
68
- return result;
61
+ let result;
62
+ await updateJsonAtomic(
63
+ registryPath(),
64
+ (current) => {
65
+ const doc = isRegistryDoc(current) ? current : emptyDoc();
66
+ result = mutate(doc);
67
+ return doc;
69
68
  },
70
- { timeoutMs: 15_000 }
69
+ { timeoutMs: 15_000, backup: true }
71
70
  );
71
+ return result;
72
72
  }
73
73
 
74
74
  export async function getRecord(id) {
@@ -106,5 +106,7 @@ export function entryView(record) {
106
106
  mergeTarget: record.mergeTarget ?? null,
107
107
  warnings: Array.isArray(record.warnings) ? [...record.warnings] : [],
108
108
  disposable: record.disposable === true,
109
+ // The session whose turn made a snapshot (it is removed when that turn settles).
110
+ ...(record.kind === WORKTREE_SNAPSHOT_KIND ? { holderSessionId: record.holderSessionId ?? null } : {}),
109
111
  };
110
112
  }
@@ -0,0 +1,39 @@
1
+ // Agent tool → session runtime channel for managed-worktree operations. The
2
+ // `git` tool (a builtin with no workspace authority of its own) reaches the
3
+ // runtime that hosts the calling Lead session through the host that runtime
4
+ // registers here; a caller no host claims (a delegated worker, a bare tool
5
+ // run) is no Lead and gets no worktree authority.
6
+ //
7
+ // Dependency-free, and shared through a global symbol like the sidebar host,
8
+ // so every bundle copy of this module sees one registry.
9
+
10
+ const HOSTS_KEY = Symbol.for('mixdog.worktreeToolHosts.v1');
11
+
12
+ function hosts() {
13
+ globalThis[HOSTS_KEY] ??= [];
14
+ return globalThis[HOSTS_KEY];
15
+ }
16
+
17
+ /**
18
+ * Register a runtime's host: `sessionId()` is the Lead session it hosts now
19
+ * (null before one exists); `run(request, context)` performs one operation
20
+ * (session-runtime/workspace/worktree-tool.mjs).
21
+ * @param {{ sessionId: () => string|null, run: (request: object, context: object) => Promise<object> }} host
22
+ * @returns {() => void} Unregister.
23
+ */
24
+ export function registerWorktreeToolHost(host) {
25
+ const entry = { sessionId: host.sessionId, run: host.run };
26
+ hosts().push(entry);
27
+ return () => {
28
+ const list = hosts();
29
+ const index = list.indexOf(entry);
30
+ if (index >= 0) list.splice(index, 1);
31
+ };
32
+ }
33
+
34
+ /** The host of Lead session `sessionId` (the most recently registered), or null. */
35
+ export function worktreeToolHost(sessionId) {
36
+ const id = String(sessionId || '').trim();
37
+ if (!id) return null;
38
+ return hosts().findLast((entry) => entry.sessionId() === id) ?? null;
39
+ }
@@ -76,6 +76,11 @@ function powerShellHost(env) {
76
76
  return false;
77
77
  }
78
78
 
79
+ function coreInstallable(id, manifest, env) {
80
+ if (!TIDY_CORE_ENGINE_IDS.includes(id) || !manifestAsset(manifest, id)) return false;
81
+ return id !== 'psscriptanalyzer' || powerShellHost(env);
82
+ }
83
+
79
84
  let installJob = null;
80
85
  let installInFlight = null;
81
86
  // Bumped on every job mutation and every refreshed host-module probe, so the
@@ -118,6 +123,14 @@ async function cachedHostModuleProbe(engine) {
118
123
  return entry?.version || '';
119
124
  }
120
125
 
126
+ /** A host-module engine (PSScriptAnalyzer) whose host exists but whose first
127
+ * probe has not answered yet. Until then "module missing" only means "not
128
+ * looked at yet", so the status reports it as pending instead. */
129
+ function hostModuleProbing(catalog, engine) {
130
+ if (!catalog.managedModule || engine.source !== 'missing' || !engine.command) return false;
131
+ return (hostModuleCache.get(String(engine.command))?.at || 0) === 0;
132
+ }
133
+
121
134
  function clampPercent(value) {
122
135
  return Math.max(0, Math.min(100, Math.round(value)));
123
136
  }
@@ -222,14 +235,17 @@ async function runCoreInstall({
222
235
  signal = null,
223
236
  installEngines = installEnginesImpl,
224
237
  resolveEngines = resolveEnginesImpl,
238
+ engines: only = null,
225
239
  } = {}) {
226
240
  const loaded = manifest || readEnginesManifest();
227
241
  const toolsDir = pluginData ? managedToolsDir(pluginData) : '';
228
- const job = startJob(TIDY_CORE_ENGINE_IDS);
242
+ // A retry for named engines installs just those; the default is the set.
243
+ const wanted = Array.isArray(only) ? TIDY_CORE_ENGINE_IDS.filter((id) => only.includes(id)) : TIDY_CORE_ENGINE_IDS;
244
+ const job = startJob(wanted);
229
245
  try {
230
246
  const resolution = await resolveEngines({
231
247
  cwd: resolutionScope(toolsDir),
232
- engineIds: [...TIDY_CORE_ENGINE_IDS],
248
+ engineIds: [...wanted],
233
249
  pluginData,
234
250
  manifest: loaded,
235
251
  env,
@@ -238,7 +254,7 @@ async function runCoreInstall({
238
254
  });
239
255
  const resolved = new Map((resolution.engines || []).map((engine) => [engine.id, engine]));
240
256
  const pending = [];
241
- for (const id of TIDY_CORE_ENGINE_IDS) {
257
+ for (const id of wanted) {
242
258
  const catalog = ENGINE_CATALOG[id];
243
259
  const engine = resolved.get(id);
244
260
  if (id === 'psscriptanalyzer' && !powerShellHost(env)) {
@@ -400,6 +416,7 @@ function withInstallStatus(inventory) {
400
416
  toolsDir: inventory.toolsDir,
401
417
  core: [...inventory.core],
402
418
  engines: inventory.engines.map((engine) => ({ ...engine })),
419
+ probing: inventory.probing,
403
420
  installing: tidyInstallStatus(),
404
421
  };
405
422
  }
@@ -424,6 +441,7 @@ async function buildEngineInventory({ pluginData, manifest, env, probeVersions,
424
441
  const source = statusSource(engine.source);
425
442
  const version = engine.version || '';
426
443
  const installHint = engine.installHint || catalog.installHint || '';
444
+ const probing = hostModuleProbing(catalog, engine);
427
445
  return {
428
446
  id,
429
447
  title: catalog.title || id,
@@ -437,7 +455,11 @@ async function buildEngineInventory({ pluginData, manifest, env, probeVersions,
437
455
  // Managed engines carry their on-disk cost; a host binary is the user's.
438
456
  ...(source === 'managed' ? { bytes: managedEngineBytes(pluginData, id, version) } : {}),
439
457
  ...(source === 'missing' && installHint ? { installHint } : {}),
458
+ // The host probe has not answered: pending, neither installed nor missing.
459
+ ...(probing ? { probing: true } : {}),
460
+ // A missing core engine the Install/Retry control can fetch right now.
461
+ ...(source === 'missing' && !probing && coreInstallable(id, loaded, env) ? { installable: true } : {}),
440
462
  };
441
463
  });
442
- return { toolsDir, core: [...TIDY_CORE_ENGINE_IDS], engines };
464
+ return { toolsDir, core: [...TIDY_CORE_ENGINE_IDS], engines, probing: engines.some((engine) => engine.probing) };
443
465
  }
@@ -4,11 +4,12 @@
4
4
  // Policy gating happens BEFORE any network call: 'never' refuses, 'ask' returns
5
5
  // needsApproval unless the call carried approveDownloads:true, 'auto' proceeds.
6
6
  // Toolchain engines (rustfmt, gofmt, zig fmt, ...) are never downloadable.
7
- import { chmodSync, existsSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs';
7
+ import { chmodSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { join } from 'node:path';
10
10
  import { MAX_NATIVE_BINARY_DOWNLOAD_BYTES, streamResponseToFile } from '../shared/bounded-download.mjs';
11
11
  import { platformEntryKey, platformKey, sha256File } from '../shared/native-asset.mjs';
12
+ import { fileHasData } from '../shared/file-has-data.mjs';
12
13
  import { readJsonSafe } from '../shared/json-file.mjs';
13
14
  import { ENGINE_CATALOG } from './engines.mjs';
14
15
  import { extractArchive } from './extract.mjs';
@@ -51,7 +52,7 @@ export function managedEngineBinary({ id, manifest, pluginData }) {
51
52
  const binPath = String(found.asset.binPath || '');
52
53
  if (!binPath) return null;
53
54
  const full = join(managedEngineDir(pluginData, id, found.version), binPath);
54
- return existsSync(full) ? { path: full, version: found.version } : null;
55
+ return fileHasData(full) ? { path: full, version: found.version } : null;
55
56
  }
56
57
 
57
58
  /** Throws unless `filePath` hashes to `expected`. */
@@ -148,44 +149,109 @@ export function planInstall({ ids = [], manifest, pluginData = '', policy = 'aut
148
149
  return { targets, present, errors, needsApproval: null };
149
150
  }
150
151
 
151
- async function downloadAsset(target, tmpDir, fetchFn, signal, onProgress) {
152
- const archiveFile = join(tmpDir, `asset-${target.id}`);
153
- const response = await fetchFn(target.url, { signal, redirect: 'follow' });
154
- if (!response?.ok) {
155
- throw new Error(`${LABEL} ${target.id}: HTTP ${response?.status || 0} for ${target.url}`);
152
+ // A network switch can leave a socket open that never delivers another byte;
153
+ // without a deadline the install (and the single in-flight job behind it)
154
+ // would wait forever.
155
+ const DEFAULT_STALL_TIMEOUT_MS = 60_000;
156
+ // A `.tmp-<pid>-<ms>-…` staging dir whose owner crashed is never cleaned by
157
+ // that owner; an hour is far beyond any live download.
158
+ const STALE_TMP_AGE_MS = 60 * 60 * 1000;
159
+
160
+ function pidAlive(pid) {
161
+ try {
162
+ process.kill(pid, 0);
163
+ return true;
164
+ } catch (error) {
165
+ return error?.code === 'EPERM';
166
+ }
167
+ }
168
+
169
+ /** Remove staging dirs a killed install left behind (dead owner, or ancient). */
170
+ function reclaimStaleStaging(engineRoot) {
171
+ let names;
172
+ try {
173
+ names = readdirSync(engineRoot);
174
+ } catch {
175
+ return;
176
+ }
177
+ for (const name of names) {
178
+ const match = /^\.tmp-(\d+)-(\d+)-/.exec(name);
179
+ if (!match) continue;
180
+ const pid = Number(match[1]);
181
+ const ownerGone = pid !== process.pid && !pidAlive(pid);
182
+ if (!ownerGone && Date.now() - Number(match[2]) < STALE_TMP_AGE_MS) continue;
183
+ try {
184
+ rmSync(join(engineRoot, name), { recursive: true, force: true });
185
+ } catch {
186
+ /* best-effort */
187
+ }
156
188
  }
157
- await streamResponseToFile(response, archiveFile, {
158
- maxBytes: MAX_NATIVE_BINARY_DOWNLOAD_BYTES,
159
- label: `${LABEL} ${target.id}`,
160
- // Byte progress per engine: the desktop card renders it live, so the
161
- // callback carries the engine id the bytes belong to.
162
- ...(typeof onProgress === 'function'
163
- ? {
164
- onProgress: ({ downloaded, total }) => {
165
- onProgress({ id: target.id, receivedBytes: downloaded, totalBytes: total });
166
- },
189
+ }
190
+
191
+ async function downloadAsset(target, tmpDir, fetchFn, signal, onProgress, stallTimeoutMs) {
192
+ const archiveFile = join(tmpDir, `asset-${target.id}`);
193
+ const stalled = new AbortController();
194
+ const merged = signal ? AbortSignal.any([signal, stalled.signal]) : stalled.signal;
195
+ let timer;
196
+ const arm = () => {
197
+ clearTimeout(timer);
198
+ timer = setTimeout(() => stalled.abort(), stallTimeoutMs);
199
+ };
200
+ arm();
201
+ try {
202
+ const response = await fetchFn(target.url, { signal: merged, redirect: 'follow' });
203
+ if (!response?.ok) {
204
+ throw new Error(`${LABEL} ${target.id}: HTTP ${response?.status || 0} for ${target.url}`);
205
+ }
206
+ await streamResponseToFile(response, archiveFile, {
207
+ maxBytes: MAX_NATIVE_BINARY_DOWNLOAD_BYTES,
208
+ label: `${LABEL} ${target.id}`,
209
+ signal: merged,
210
+ // Byte progress per engine: the desktop card renders it live, so the
211
+ // callback carries the engine id the bytes belong to. Any bytes also
212
+ // prove the connection is alive.
213
+ onProgress: ({ downloaded, total }) => {
214
+ arm();
215
+ if (typeof onProgress === 'function') {
216
+ onProgress({ id: target.id, receivedBytes: downloaded, totalBytes: total });
167
217
  }
168
- : {}),
169
- });
218
+ },
219
+ });
220
+ } catch (error) {
221
+ if (stalled.signal.aborted && !signal?.aborted) {
222
+ throw new Error(`${LABEL} ${target.id}: download stalled (no data for ${stallTimeoutMs / 1000}s)`);
223
+ }
224
+ throw error;
225
+ } finally {
226
+ clearTimeout(timer);
227
+ }
170
228
  return archiveFile;
171
229
  }
172
230
 
173
231
  /** Download + verify + extract one engine into its versioned managed dir. */
174
232
  export async function installEngine(
175
233
  target,
176
- { pluginData, fetchFn = globalThis.fetch, signal = null, onProgress = null } = {}
234
+ {
235
+ pluginData,
236
+ fetchFn = globalThis.fetch,
237
+ signal = null,
238
+ onProgress = null,
239
+ stallTimeoutMs = DEFAULT_STALL_TIMEOUT_MS,
240
+ } = {}
177
241
  ) {
178
242
  const engineRoot = join(managedToolsDir(pluginData), target.id);
179
243
  const finalDir = managedEngineDir(pluginData, target.id, target.version);
180
244
  const binFull = join(finalDir, target.binPath);
181
- if (existsSync(binFull)) {
245
+ // An engine binary a crash left NUL-filled is not present: it is installed again.
246
+ if (fileHasData(binFull)) {
182
247
  return { id: target.id, version: target.version, path: binFull, bytes: 0, status: 'present' };
183
248
  }
184
249
  mkdirSync(engineRoot, { recursive: true });
250
+ reclaimStaleStaging(engineRoot);
185
251
  const tmpDir = join(engineRoot, `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
186
252
  mkdirSync(tmpDir, { recursive: true });
187
253
  try {
188
- const archiveFile = await downloadAsset(target, tmpDir, fetchFn, signal, onProgress);
254
+ const archiveFile = await downloadAsset(target, tmpDir, fetchFn, signal, onProgress, stallTimeoutMs);
189
255
  await verifyDownloadDigest(archiveFile, target.sha256);
190
256
  const bytes = statSync(archiveFile).size;
191
257
  const stageDir = join(tmpDir, 'stage');
@@ -207,6 +273,7 @@ export async function installEngine(
207
273
  }
208
274
  }
209
275
  try {
276
+ if (existsSync(finalDir) && !fileHasData(binFull)) rmSync(finalDir, { recursive: true, force: true });
210
277
  renameSync(stageDir, finalDir);
211
278
  } catch (error) {
212
279
  // A concurrent install won the race; its verified copy is equivalent.
@@ -235,6 +302,7 @@ export async function installEngines({
235
302
  fetchFn = globalThis.fetch,
236
303
  signal = null,
237
304
  onProgress = null,
305
+ stallTimeoutMs = undefined,
238
306
  } = {}) {
239
307
  const loaded = manifest || readEnginesManifest();
240
308
  const plan = planInstall({ ids, manifest: loaded, pluginData, policy, approveDownloads });
@@ -249,7 +317,7 @@ export async function installEngines({
249
317
  }
250
318
  for (const target of plan.targets) {
251
319
  try {
252
- installed.push(await installEngine(target, { pluginData, fetchFn, signal, onProgress }));
320
+ installed.push(await installEngine(target, { pluginData, fetchFn, signal, onProgress, stallTimeoutMs }));
253
321
  } catch (error) {
254
322
  errors.push({ id: target.id, error: error?.message || String(error) });
255
323
  }
@@ -4,7 +4,7 @@
4
4
  // binaries and file paths are untrusted input, so no value ever reaches a shell
5
5
  // parser.
6
6
  import { spawn } from 'node:child_process';
7
- import { existsSync, statSync } from 'node:fs';
7
+ import { existsSync, readdirSync, statSync } from 'node:fs';
8
8
  import { delimiter, extname, isAbsolute, join } from 'node:path';
9
9
 
10
10
  const DEFAULT_PROCESS_TIMEOUT_MS = 90_000;
@@ -36,19 +36,52 @@ function isExecutableFile(candidate) {
36
36
  }
37
37
  }
38
38
 
39
- /** First match for `name` on PATH, or null. Pure filesystem lookup, no spawn. */
40
- export function which(name, { env = process.env } = {}) {
39
+ const pathDirs = (env) =>
40
+ String(env.PATH || env.Path || '')
41
+ .split(delimiter)
42
+ .map((value) => value.trim())
43
+ .filter(Boolean);
44
+
45
+ const nameKey = (value) => (process.platform === 'win32' ? value.toLowerCase() : value);
46
+
47
+ /**
48
+ * One PATH's directory listings, read once per directory on first use, for a
49
+ * batch of `which` lookups. Resolving the whole engine catalog probed every
50
+ * PATH dir × PATHEXT extension × engine name with its own statSync (≈17k
51
+ * synchronous stats on a 65-entry PATH); with an index a name that is on no
52
+ * directory costs no filesystem call at all.
53
+ */
54
+ export function createPathIndex(env = process.env) {
55
+ const listings = new Map(); // dir -> Set of entry names, or null (unlistable)
56
+ return {
57
+ dirs: pathDirs(env),
58
+ has(dir, candidate) {
59
+ if (!listings.has(dir)) {
60
+ let names = null;
61
+ try {
62
+ names = new Set(readdirSync(dir).map(nameKey));
63
+ } catch {
64
+ // Unlistable (no list permission, gone): probe it per candidate.
65
+ }
66
+ listings.set(dir, names);
67
+ }
68
+ const names = listings.get(dir);
69
+ return names ? names.has(nameKey(candidate)) : true;
70
+ },
71
+ };
72
+ }
73
+
74
+ /** First match for `name` on PATH, or null. Pure filesystem lookup, no spawn.
75
+ * `index` (createPathIndex over the same env) answers from directory listings. */
76
+ export function which(name, { env = process.env, index = null } = {}) {
41
77
  const bin = String(name || '').trim();
42
78
  if (!bin) return null;
43
79
  if (isAbsolute(bin) || bin.includes('/') || bin.includes('\\')) {
44
80
  return existsSync(bin) ? bin : null;
45
81
  }
46
- const dirs = String(env.PATH || env.Path || '')
47
- .split(delimiter)
48
- .map((value) => value.trim())
49
- .filter(Boolean);
50
- for (const dir of dirs) {
82
+ for (const dir of index ? index.dirs : pathDirs(env)) {
51
83
  for (const candidate of executableCandidates(bin)) {
84
+ if (index && !index.has(dir, candidate)) continue;
52
85
  const full = join(dir, candidate);
53
86
  if (isExecutableFile(full)) return full;
54
87
  }
@@ -7,7 +7,7 @@
7
7
  import { existsSync, readFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import { ENGINE_CATALOG, ENGINE_IDS, engineEntry, enginesForLanguages } from './engines.mjs';
10
- import { mapLimit, runProcess, which } from './process.mjs';
10
+ import { createPathIndex, mapLimit, runProcess, which } from './process.mjs';
11
11
  import { managedEngineBinary, manifestAsset, readEnginesManifest } from './install.mjs';
12
12
 
13
13
  const DOWNLOAD_POLICIES = Object.freeze(['ask', 'auto', 'never']);
@@ -113,9 +113,9 @@ export function projectConfigFileFor(cwd, entry) {
113
113
  return null;
114
114
  }
115
115
 
116
- function pathBinary(entry, env) {
116
+ function pathBinary(entry, env, index) {
117
117
  for (const name of [entry.bin, ...(entry.altBins || [])]) {
118
- const found = which(name, { env });
118
+ const found = which(name, { env, index });
119
119
  if (found) return { path: found, bin: name };
120
120
  }
121
121
  return null;
@@ -135,7 +135,7 @@ function installableFlag(entry, manifest) {
135
135
  return entry.managed && manifestAsset(manifest, entry.id) ? { installable: true } : {};
136
136
  }
137
137
 
138
- function resolveOne({ cwd, entry, config, env, manifest, pluginData }) {
138
+ function resolveOne({ cwd, entry, config, env, manifest, pluginData, pathIndex }) {
139
139
  const base = {
140
140
  id: entry.id,
141
141
  kind: entry.kind,
@@ -159,7 +159,7 @@ function resolveOne({ cwd, entry, config, env, manifest, pluginData }) {
159
159
  return { ...base, source: 'missing', path: '', missing: true };
160
160
  }
161
161
  if (entry.managedModule) {
162
- const host = pathBinary(entry, env);
162
+ const host = pathBinary(entry, env, pathIndex);
163
163
  if (!host) {
164
164
  return {
165
165
  ...base,
@@ -191,7 +191,7 @@ function resolveOne({ cwd, entry, config, env, manifest, pluginData }) {
191
191
  ...installableFlag(entry, manifest),
192
192
  };
193
193
  }
194
- const onPath = pathBinary(entry, env);
194
+ const onPath = pathBinary(entry, env, pathIndex);
195
195
  if (onPath) return { ...base, source: 'path', path: onPath.path, ...commandFor(entry, onPath.path) };
196
196
  if (entry.managed && pluginData) {
197
197
  const managed = managedEngineBinary({ id: entry.id, manifest, pluginData });
@@ -282,9 +282,12 @@ export async function resolveEngines({
282
282
  else if (languages.length > 0) requested = enginesForLanguages(languages);
283
283
  const unknown = engineIds.filter((id) => !ENGINE_IDS.includes(id));
284
284
 
285
+ // One listing per PATH dir for the whole batch: this runs synchronously on
286
+ // the host loop (the desktop's Code Tidy status read at startup).
287
+ const pathIndex = createPathIndex(env);
285
288
  const resolved = requested.map((id) => {
286
289
  const entry = ENGINE_CATALOG[id];
287
- const result = resolveOne({ cwd, entry, config, env, manifest: loadedManifest, pluginData });
290
+ const result = resolveOne({ cwd, entry, config, env, manifest: loadedManifest, pluginData, pathIndex });
288
291
  const configFile = projectConfigFileFor(cwd, entry);
289
292
  if (configFile) result.configFile = configFile;
290
293
  if (entry.requiresConfig && !configFile && result.source !== 'project-config') {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { resolvePluginData } from '../../shared/plugin-paths.mjs';
4
- import { renameWithRetrySync, writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
4
+ import { readJsonStateSync, writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
5
5
 
6
6
  // Unified mode: web search shares the plugin data dir with the rest of mixdog.
7
7
  const SHARED_DATA_DIR = resolvePluginData();
@@ -19,22 +19,10 @@ export function ensureDataDir() {
19
19
  fs.mkdirSync(DATA_DIR, { recursive: true });
20
20
  }
21
21
 
22
+ // usage/cache state: a damaged file (empty, NUL-filled, truncated) is kept as
23
+ // <file>.corrupt-<ts> and reads as `fallback`, so search/fetch keep working.
22
24
  export function readJson(filePath, fallback) {
23
- try {
24
- const raw = fs.readFileSync(filePath, 'utf8');
25
- try {
26
- return JSON.parse(raw);
27
- } catch (parseErr) {
28
- try {
29
- renameWithRetrySync(filePath, `${filePath}.corrupt.${Date.now()}`);
30
- } catch {}
31
- process.stderr.write(`[web-search-config] corrupt JSON backed up: ${filePath}\n`);
32
- throw parseErr;
33
- }
34
- } catch (err) {
35
- if (err.code === 'ENOENT') return fallback;
36
- throw err;
37
- }
25
+ return readJsonStateSync(filePath, { fallback });
38
26
  }
39
27
 
40
28
  export function writeJson(filePath, value) {