mixdog 1.3.0 → 1.3.2

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 (423) hide show
  1. package/NOTICE.md +52 -9
  2. package/README.md +8 -4
  3. package/package.json +2 -2
  4. package/scripts/native-release-status.mjs +238 -0
  5. package/src/defaults/shipped-history.json +8 -1
  6. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  7. package/src/defaults/skills/setup/SKILL.md +23 -1
  8. package/src/defaults/skills/setup/references/actions.md +54 -4
  9. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  10. package/src/defaults/skills/setup-guide/SKILL.md +17 -10
  11. package/src/defaults/skills/visualize/SKILL.md +9 -6
  12. package/src/defaults/skills/visualize/references/craft.md +8 -1
  13. package/src/help.mjs +1 -1
  14. package/src/lib/keychain-cjs.cjs +4 -1
  15. package/src/lib/rules-builder.cjs +14 -7
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent-mode/AGENT_MODE.md +9 -6
  18. package/src/rules/agent-mode/MIXY.md +11 -6
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/rules/shared/10-tool-workflow.md +4 -0
  21. package/src/rules/shared/20-research.md +2 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  25. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  26. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  27. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  29. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  32. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +81 -16
  34. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -4
  35. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  36. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  37. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  38. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  43. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  44. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +34 -16
  45. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  46. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  47. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  49. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -1
  50. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  51. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  52. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  53. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  54. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +10 -2
  60. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  65. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  68. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  69. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  72. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +5 -5
  74. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  75. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +17 -6
  78. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +41 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +6 -4
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  83. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  84. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  85. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +21 -2
  86. package/src/runtime/agent/orchestrator/session/agent-room.mjs +149 -45
  87. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  88. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  90. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  91. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  92. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  93. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  94. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  96. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  97. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  99. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  100. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +156 -25
  101. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  102. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  103. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  104. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  105. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  106. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  107. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  108. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  110. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +111 -9
  111. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -14
  112. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  116. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
  119. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  120. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  124. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  125. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  126. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  128. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  130. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  131. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  132. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  134. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +62 -47
  135. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +6 -8
  136. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  137. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  138. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  139. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  140. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
  141. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  142. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
  143. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  144. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  145. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +35 -4
  146. package/src/runtime/agent/orchestrator/session/routine-result.mjs +179 -16
  147. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  148. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  149. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  150. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  151. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  152. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  153. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  154. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  155. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  156. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  157. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  158. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  159. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  160. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  161. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +49 -14
  162. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +134 -17
  163. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  164. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  165. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  166. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  167. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  168. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  169. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  170. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  171. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  175. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  176. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  177. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  178. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  179. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  180. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  181. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  182. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  183. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  184. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  185. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  186. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  187. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  188. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  189. package/src/runtime/attachments/limits.mjs +5 -1
  190. package/src/runtime/attachments/office-extract.mjs +3 -6
  191. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  192. package/src/runtime/attachments/store.mjs +55 -30
  193. package/src/runtime/browser-bridge/client.mjs +73 -20
  194. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  195. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  196. package/src/runtime/channels/lib/state-file.mjs +15 -15
  197. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  198. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  199. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  200. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  201. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  202. package/src/runtime/effort-judge/judge-client.mjs +6 -3
  203. package/src/runtime/effort-judge/model-install.mjs +16 -8
  204. package/src/runtime/effort-judge/model-manifest.json +7 -7
  205. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  206. package/src/runtime/local-provider/catalog.mjs +5 -2
  207. package/src/runtime/local-provider/registered-models.mjs +64 -27
  208. package/src/runtime/local-provider/server.mjs +2 -3
  209. package/src/runtime/media/adapters/image-api.mjs +89 -0
  210. package/src/runtime/media/auth.mjs +64 -1
  211. package/src/runtime/media/catalog.mjs +66 -4
  212. package/src/runtime/media/jobs.mjs +4 -0
  213. package/src/runtime/media/lanes.mjs +46 -5
  214. package/src/runtime/media/media-usage.mjs +17 -0
  215. package/src/runtime/media/renditions.mjs +15 -15
  216. package/src/runtime/media/store.mjs +36 -35
  217. package/src/runtime/media/tool.mjs +8 -1
  218. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  219. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  220. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  221. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  222. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  223. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  224. package/src/runtime/office/core/journal.mjs +7 -14
  225. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  226. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  227. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  228. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  229. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  230. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  231. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  232. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  233. package/src/runtime/shared/agent-kind.mjs +152 -0
  234. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  235. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  236. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  237. package/src/runtime/shared/agent-route-config.d.mts +1 -0
  238. package/src/runtime/shared/agent-route-config.mjs +22 -8
  239. package/src/runtime/shared/agent-store.mjs +149 -0
  240. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  241. package/src/runtime/shared/atomic-file.d.mts +50 -0
  242. package/src/runtime/shared/atomic-file.mjs +394 -132
  243. package/src/runtime/shared/background-tasks.mjs +7 -4
  244. package/src/runtime/shared/bot-hops.mjs +30 -0
  245. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  246. package/src/runtime/shared/bounded-download.mjs +4 -2
  247. package/src/runtime/shared/config-patch.mjs +20 -1
  248. package/src/runtime/shared/config.mjs +39 -9
  249. package/src/runtime/shared/connector-catalog.d.mts +1 -2
  250. package/src/runtime/shared/connector-catalog.mjs +2 -4
  251. package/src/runtime/shared/connector-triggers.mjs +2 -0
  252. package/src/runtime/shared/defaults-separation.mjs +6 -2
  253. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  254. package/src/runtime/shared/err-text.mjs +7 -1
  255. package/src/runtime/shared/error-presentation.mjs +113 -10
  256. package/src/runtime/shared/file-has-data.mjs +28 -0
  257. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  258. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  259. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  260. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  261. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  262. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  263. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  264. package/src/runtime/shared/plain-preview.mjs +22 -17
  265. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  266. package/src/runtime/shared/projects.mjs +50 -41
  267. package/src/runtime/shared/provider-accounts.mjs +17 -12
  268. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  269. package/src/runtime/shared/routine-trigger.mjs +14 -6
  270. package/src/runtime/shared/run-child.d.mts +3 -0
  271. package/src/runtime/shared/run-child.mjs +24 -3
  272. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  273. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  274. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  275. package/src/runtime/shared/singleton-owner.mjs +7 -6
  276. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  277. package/src/runtime/shared/staged-update.mjs +4 -2
  278. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  279. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  280. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  281. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  282. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  283. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  284. package/src/runtime/shared/usage-readout-totals.mjs +15 -13
  285. package/src/runtime/shared/user-cwd.mjs +9 -4
  286. package/src/runtime/shared/user-data-guard.mjs +46 -44
  287. package/src/runtime/shared/utf8-cut.mjs +10 -0
  288. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  289. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  290. package/src/runtime/tidy/core-install.mjs +26 -4
  291. package/src/runtime/tidy/install.mjs +91 -23
  292. package/src/runtime/web-search/lib/config.mjs +4 -16
  293. package/src/session-runtime/agent-home-delivery.mjs +859 -136
  294. package/src/session-runtime/agent-room-delivery.mjs +167 -19
  295. package/src/session-runtime/agent-task-sessions.mjs +831 -69
  296. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  297. package/src/session-runtime/boot/apis.mjs +58 -24
  298. package/src/session-runtime/boot/config.mjs +1 -0
  299. package/src/session-runtime/boot/facade.mjs +11 -1
  300. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  301. package/src/session-runtime/boot/providers.mjs +16 -6
  302. package/src/session-runtime/boot/shared.mjs +12 -6
  303. package/src/session-runtime/boot/tools.mjs +2 -2
  304. package/src/session-runtime/bot-status.mjs +432 -0
  305. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  306. package/src/session-runtime/config-lifecycle.mjs +78 -32
  307. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  308. package/src/session-runtime/handoff-attachments.mjs +593 -0
  309. package/src/session-runtime/held-requests.mjs +36 -33
  310. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  311. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  312. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +45 -6
  313. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  314. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  315. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  316. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  317. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  318. package/src/session-runtime/media-api.mjs +5 -2
  319. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  320. package/src/session-runtime/model-route/route-tuning.mjs +11 -2
  321. package/src/session-runtime/model-route/set-route.mjs +82 -2
  322. package/src/session-runtime/native-web-search.mjs +2 -2
  323. package/src/session-runtime/new-session-config.mjs +6 -1
  324. package/src/session-runtime/owned-automation-run.mjs +8 -1
  325. package/src/session-runtime/room-member-gate.mjs +16 -3
  326. package/src/session-runtime/route-resolution.mjs +21 -19
  327. package/src/session-runtime/route-state.mjs +1 -59
  328. package/src/session-runtime/routine-home-turn.mjs +5 -3
  329. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  330. package/src/session-runtime/runtime-paths.mjs +11 -5
  331. package/src/session-runtime/runtime-tunables.mjs +6 -0
  332. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  333. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  334. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  335. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  336. package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
  337. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  338. package/src/session-runtime/services/agent-tool/send-flow.mjs +36 -24
  339. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  340. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  341. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +12 -6
  342. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  343. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  344. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  345. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  346. package/src/session-runtime/services/agent-tool.mjs +6 -0
  347. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  348. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  349. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  350. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  351. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  352. package/src/session-runtime/services/provider-admin.mjs +14 -4
  353. package/src/session-runtime/services/schedule-access.mjs +33 -4
  354. package/src/session-runtime/services/seeds.mjs +4 -3
  355. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  356. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  357. package/src/session-runtime/session-create/materialize.mjs +2 -3
  358. package/src/session-runtime/session-create/session-options.mjs +15 -2
  359. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  360. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  361. package/src/session-runtime/setup-tool/executor.mjs +20 -3
  362. package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
  363. package/src/session-runtime/setup-tool/settings-contract.mjs +37 -1
  364. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  365. package/src/session-runtime/skills-api.mjs +12 -5
  366. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  367. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  368. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  369. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  370. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  371. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  372. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  373. package/src/session-runtime/tool-policy-refresh.mjs +47 -358
  374. package/src/session-runtime/tool-surface.mjs +5 -3
  375. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +99 -81
  376. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  377. package/src/session-runtime/turn/turn-run.mjs +2 -1
  378. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  379. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +578 -301
  380. package/src/session-runtime/workflow-agents-api/agent-route.mjs +113 -53
  381. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  382. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  383. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  384. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  385. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  386. package/src/standalone/daemon-agent-control.mjs +4 -1
  387. package/src/standalone/daemon-session-control.mjs +60 -11
  388. package/src/standalone/daemon.mjs +34 -4
  389. package/src/standalone/local-session-runtime.mjs +22 -14
  390. package/src/standalone/session-client.mjs +7 -2
  391. package/src/standalone/session-protocol.mjs +7 -0
  392. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  393. package/src/standalone/session-service/agent-tree.mjs +4 -3
  394. package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
  395. package/src/standalone/session-service.mjs +11 -0
  396. package/src/standalone/session-state-projection.mjs +19 -9
  397. package/src/standalone/session-tool-output.mjs +35 -9
  398. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  399. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  400. package/src/tui/app/slash-commands.mjs +1 -1
  401. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  402. package/src/tui/dist/index.mjs +599 -342
  403. package/src/tui/prompt-history-store.mjs +16 -21
  404. package/src/tui/session/context-state.mjs +4 -0
  405. package/src/tui/session/notice-surface.mjs +3 -4
  406. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  407. package/src/tui/session/session-api/media.mjs +2 -1
  408. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  409. package/src/tui/session/session-api/routes-write.mjs +203 -0
  410. package/src/tui/session/session-api/routes.mjs +23 -236
  411. package/src/tui/session/session-api/settings.mjs +14 -1
  412. package/src/tui/session/session-flow/drain.mjs +28 -3
  413. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  414. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  415. package/src/tui/session/session-flow/steering.mjs +8 -3
  416. package/src/tui/session/tool-approval.mjs +185 -20
  417. package/src/tui/session/transcript-intake.mjs +4 -0
  418. package/src/tui/session/turn-live-call.mjs +11 -3
  419. package/src/tui/session/turn.mjs +36 -17
  420. package/src/tui/transcript-tool-failures.mjs +8 -2
  421. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  422. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  423. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -20,9 +20,10 @@
20
20
  */
21
21
 
22
22
  import { AsyncLocalStorage } from 'node:async_hooks';
23
- import { readFileSync, statSync, writeFileSync } from 'node:fs';
23
+ import { readFileSync, statSync } from 'node:fs';
24
24
  import { join, resolve } from 'node:path';
25
25
  import { homedir } from 'node:os';
26
+ import { holdsNoData, writeFileAtomicSync } from './atomic-file.mjs';
26
27
  import { resolvePluginData, mixdogRoot } from './plugin-paths.mjs';
27
28
 
28
29
  const _cwdOverride = new AsyncLocalStorage();
@@ -112,7 +113,10 @@ export function explicitSessionCwd() {
112
113
  if (sessionDir) return sessionDir;
113
114
  }
114
115
  try {
115
- const txt = readFileSync(_dataFile('user-cwd.txt'), 'utf8').trim();
116
+ const raw = readFileSync(_dataFile('user-cwd.txt'), 'utf8');
117
+ // A crash can leave NUL bytes in place of the path: that is no path.
118
+ if (holdsNoData(raw)) return null;
119
+ const txt = raw.trim();
116
120
  return (txt && _normalizePlatformCwd(txt)) || null;
117
121
  } catch {
118
122
  return null;
@@ -173,7 +177,7 @@ function _lastSessionCwdFile(keyPid) {
173
177
  */
174
178
  export function writeLastSessionCwd(cwd, keyPid) {
175
179
  try {
176
- writeFileSync(_lastSessionCwdFile(keyPid), String(cwd));
180
+ writeFileAtomicSync(_lastSessionCwdFile(keyPid), String(cwd));
177
181
  } catch {
178
182
  /* best-effort */
179
183
  }
@@ -188,7 +192,8 @@ export function writeLastSessionCwd(cwd, keyPid) {
188
192
  */
189
193
  export function readLastSessionCwd(keyPid) {
190
194
  try {
191
- return _existingDirectory(readFileSync(_lastSessionCwdFile(keyPid), 'utf8').trim());
195
+ const raw = readFileSync(_lastSessionCwdFile(keyPid), 'utf8');
196
+ return holdsNoData(raw) ? null : _existingDirectory(raw.trim());
192
197
  } catch {
193
198
  return null;
194
199
  }
@@ -1,23 +1,15 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
1
2
  import {
2
- copyFileSync,
3
- existsSync,
4
- mkdirSync,
5
- readdirSync,
6
- readFileSync,
7
- rmSync,
8
- statSync,
9
- writeFileSync,
10
- } from 'node:fs';
11
- import {
12
- copyFile as copyFileP,
13
3
  mkdir as mkdirP,
4
+ readFile as readFileP,
14
5
  readdir as readdirP,
15
6
  rm as rmP,
16
7
  stat as statP,
17
8
  } from 'node:fs/promises';
18
- import { basename, dirname, join, resolve } from 'node:path';
9
+ import { basename, join, resolve } from 'node:path';
19
10
  import { homedir } from 'node:os';
20
11
  import { createHash, randomUUID } from 'node:crypto';
12
+ import { writeFileAtomicAsync, writeFileAtomicSync, writeJsonAtomicSync } from './atomic-file.mjs';
21
13
  import { isPlainObject } from './object.mjs';
22
14
  import { mixdogHome } from './plugin-paths.mjs';
23
15
 
@@ -155,15 +147,27 @@ function safeReason(reason) {
155
147
  );
156
148
  }
157
149
 
158
- /** instanceId of this data dir's init marker; null when absent or unreadable. */
150
+ /** instanceId of this data dir's init marker; null only when absent. */
159
151
  function readInitInstanceId(dataDir) {
160
152
  if (!dataDir) return null;
153
+ const path = join(dataDir, INIT_MARKER);
154
+ let raw;
161
155
  try {
162
- const marker = JSON.parse(readFileSync(join(dataDir, INIT_MARKER), 'utf8'));
163
- return typeof marker?.instanceId === 'string' && marker.instanceId ? marker.instanceId : null;
164
- } catch {
165
- return null;
156
+ raw = readFileSync(path, 'utf8');
157
+ } catch (error) {
158
+ if (error?.code === 'ENOENT') return null;
159
+ throw error;
160
+ }
161
+ let marker;
162
+ try {
163
+ marker = JSON.parse(raw);
164
+ } catch (error) {
165
+ throw new SyntaxError(`[user-data] invalid initialization marker at ${path}`, { cause: error });
166
+ }
167
+ if (typeof marker?.instanceId !== 'string' || !marker.instanceId) {
168
+ throw new SyntaxError(`[user-data] initialization marker at ${path} must have a non-empty instanceId`);
166
169
  }
170
+ return marker.instanceId;
167
171
  }
168
172
 
169
173
  export function hasUserDataInitMarker(dataDir) {
@@ -379,8 +383,9 @@ function copyTree(src, dst, copied) {
379
383
  return;
380
384
  }
381
385
  if (!st.isFile()) return;
382
- mkdirSync(dirname(dst), { recursive: true });
383
- copyFileSync(src, dst);
386
+ // A copy goes through an fsynced handle: a plain copy can leave a backup that
387
+ // is NUL exactly when a crash makes it needed.
388
+ writeFileAtomicSync(dst, readFileSync(src), { mode: st.mode & 0o777 });
384
389
  copied.push(dst);
385
390
  }
386
391
 
@@ -394,22 +399,20 @@ function pruneBackups(scopeDir, names, own) {
394
399
  }
395
400
 
396
401
  /**
397
- * Create the data dir's init marker unless one exists; returns its instanceId
398
- * (null when the data dir is gone or unwritable). Never creates the data dir.
402
+ * Create a missing init marker; return its instanceId, or null if the data dir
403
+ * is gone or the missing marker cannot be created. Never creates the data dir.
404
+ * Read/validation failures throw; an existing marker is never replaced.
399
405
  */
400
406
  export function markUserDataInitialized(dataDir) {
401
407
  const existing = readInitInstanceId(dataDir);
402
408
  if (existing || !dataDir || !existsSync(dataDir)) return existing;
403
409
  try {
404
- writeFileSync(
405
- join(dataDir, INIT_MARKER),
406
- `${JSON.stringify(
407
- { instanceId: randomUUID(), dataDir: resolve(dataDir), createdAt: new Date().toISOString() },
408
- null,
409
- 2
410
- )}\n`,
411
- // An unreadable marker is replaced; a concurrent creator's valid one wins.
412
- { encoding: 'utf8', flag: existsSync(join(dataDir, INIT_MARKER)) ? 'w' : 'wx' }
410
+ const marker = join(dataDir, INIT_MARKER);
411
+ writeJsonAtomicSync(
412
+ marker,
413
+ { instanceId: randomUUID(), dataDir: resolve(dataDir), createdAt: new Date().toISOString() },
414
+ // Another creator may have published since the absent read; it wins.
415
+ { createOnly: true }
413
416
  );
414
417
  } catch {}
415
418
  return readInitInstanceId(dataDir);
@@ -427,8 +430,8 @@ export function ensureUserDataInitialized(dataDir) {
427
430
  if (!dataDir) return;
428
431
  const key = normalizeDataDir(dataDir);
429
432
  if (ensuredDataDirs.has(key)) return;
430
- ensuredDataDirs.add(key);
431
433
  if (!hasUserDataInitMarker(dataDir)) backupUserData(dataDir, 'initial-snapshot');
434
+ ensuredDataDirs.add(key);
432
435
  }
433
436
 
434
437
  function skipBackups() {
@@ -439,20 +442,20 @@ function skipBackups() {
439
442
  // manifest goes last, so an interrupted copy is never restorable.
440
443
  function finishBackup(dataDir, backupDir, reason) {
441
444
  const instanceId = markUserDataInitialized(dataDir);
442
- writeFileSync(
443
- join(backupDir, BACKUP_MANIFEST),
444
- `${JSON.stringify(
445
- { dataDir: resolve(dataDir), instanceId, reason, createdAt: new Date().toISOString() },
446
- null,
447
- 2
448
- )}\n`,
449
- 'utf8'
450
- );
445
+ writeJsonAtomicSync(join(backupDir, BACKUP_MANIFEST), {
446
+ dataDir: resolve(dataDir),
447
+ instanceId,
448
+ reason,
449
+ createdAt: new Date().toISOString(),
450
+ });
451
451
  restoreLookupCache.delete(normalizeDataDir(dataDir));
452
452
  legacyEvidenceCache.delete(normalizeDataDir(dataDir));
453
453
  }
454
454
 
455
- export function backupUserData(dataDir, reason = 'snapshot') {
455
+ /** `dirs`: more data-dir folders this one snapshot also carries (a one-time
456
+ * migration's, e.g. agents/ and bots/); routine snapshots keep to
457
+ * USER_DATA_DIRS so the ring stays small. */
458
+ export function backupUserData(dataDir, reason = 'snapshot', { dirs = [] } = {}) {
456
459
  if (skipBackups()) return { dir: null, copied: [] };
457
460
  if (!dataDir || !existsSync(dataDir)) return { dir: null, copied: [] };
458
461
  const scopeDir = scopeDirFor(dataDir);
@@ -462,7 +465,7 @@ export function backupUserData(dataDir, reason = 'snapshot') {
462
465
  const src = join(dataDir, rel);
463
466
  if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
464
467
  }
465
- for (const rel of USER_DATA_DIRS) {
468
+ for (const rel of [...USER_DATA_DIRS, ...dirs]) {
466
469
  const src = join(dataDir, rel);
467
470
  if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
468
471
  }
@@ -491,8 +494,7 @@ async function copyTreeAsync(src, dst, copied) {
491
494
  return;
492
495
  }
493
496
  if (!st.isFile()) return;
494
- await mkdirP(dirname(dst), { recursive: true });
495
- await copyFileP(src, dst);
497
+ await writeFileAtomicAsync(dst, await readFileP(src), { mode: st.mode & 0o777 });
496
498
  copied.push(dst);
497
499
  }
498
500
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `text` without its trailing U+FFFD characters: what a byte cut through a
3
+ * multi-byte sequence leaves behind. A loop, not `/\uFFFD+$/`, which is
4
+ * quadratic on a long run of them.
5
+ */
6
+ export function stripTrailingReplacementChars(text) {
7
+ let end = text.length;
8
+ while (end > 0 && text[end - 1] === '\uFFFD') end -= 1;
9
+ return end === text.length ? text : text.slice(0, end);
10
+ }
@@ -1,13 +1,21 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
2
3
  import { setTimeout as delay } from 'node:timers/promises';
3
- import { readFileSync, renameSync } from 'node:fs';
4
4
  import { isAbsolute, join } from 'node:path';
5
- import { withFileLockSync, writeJsonAtomicSync } from '../atomic-file.mjs';
5
+ import {
6
+ parseJsonText,
7
+ quarantinePath,
8
+ readJsonStateSync,
9
+ unsupportedState,
10
+ updateJsonAtomicSync,
11
+ writeFileAtomicSync,
12
+ writeJsonAtomicSync,
13
+ } from '../atomic-file.mjs';
6
14
  import { isPidAliveOrUnknown } from '../pid-liveness.mjs';
7
15
  import { findGitLayout } from './git-head.mjs';
8
16
  import { canonicalPath, isWithin, samePath } from './paths.mjs';
9
17
  import { worktreeError, worktreeRefusal } from './contract.mjs';
10
- import { registryPath } from './registry.mjs';
18
+ import { readRegistrySync } from './registry.mjs';
11
19
 
12
20
  const FILE = 'mixdog-workspace-admission.json';
13
21
  const overlaps = (a, b) => isWithin(a, b) || isWithin(b, a);
@@ -24,91 +32,273 @@ export function isExternalCheckout(folder) {
24
32
  if (!layout || samePath(layout.gitDir, layout.commonDir)) return false;
25
33
  let entries;
26
34
  try {
27
- entries = JSON.parse(readFileSync(registryPath(), 'utf8'))?.entries ?? {};
28
- } catch (error) {
29
- if (error?.code !== 'ENOENT') throw worktreeRefusal('workspace-admission-unreadable');
30
- entries = {};
35
+ entries = readRegistrySync().entries;
36
+ } catch {
37
+ // A damaged registry is kept aside and reads as a restored or empty one;
38
+ // only a real I/O failure (access denied, in use) lands here.
39
+ throw worktreeRefusal('workspace-admission-unreadable');
31
40
  }
32
41
  return !Object.values(entries).some(
33
42
  (record) => typeof record?.path === 'string' && samePath(record.path, layout.topLevel)
34
43
  );
35
44
  }
36
45
 
46
+ const VERSION = 1;
47
+ const UNRECORDED = Symbol('unrecorded');
48
+ const emptyLedger = () => ({ version: VERSION, leases: [] });
49
+ const isNewer = (state) => Number.isSafeInteger(state?.version) && state.version > VERSION;
50
+ const isLease = (lease) =>
51
+ !!lease &&
52
+ ['turn', 'command', 'management'].includes(lease.kind) &&
53
+ typeof lease.token === 'string' &&
54
+ Number.isSafeInteger(lease.pid) &&
55
+ lease.pid > 0 &&
56
+ typeof lease.path === 'string' &&
57
+ isAbsolute(lease.path);
58
+ const hasLedgerShape = (state) => state?.version === VERSION && Array.isArray(state.leases);
59
+
60
+ // The whole-file check: a newer build's ledger is intact data in a format this
61
+ // build cannot honour (left in place), anything else off-shape is damage. One
62
+ // bad lease record is not a damaged file; mutate() drops just that record.
63
+ function validateLedger(state) {
64
+ if (isNewer(state)) throw unsupportedState('Workspace admission ledger was written by a newer build.');
65
+ if (!hasLedgerShape(state)) throw new TypeError('Workspace admission ledger is malformed.');
66
+ return state;
67
+ }
68
+
69
+ const refuseUnreadable = () => {
70
+ throw worktreeRefusal('workspace-admission-unreadable');
71
+ };
72
+ const envMs = (name, fallback) => {
73
+ const value = Number(process.env[name]);
74
+ return Number.isFinite(value) && value > 0 ? value : fallback;
75
+ };
76
+ // How long the leases of a recovered ledger stay unknown. Every process
77
+ // holding a lease republishes it within one heal period, so after this window
78
+ // the ledger is whole again and destructive operations resume on their own.
79
+ const recoveryWindowMs = () => envMs('MIXDOG_ADMISSION_RECOVERY_MS', 15_000);
80
+ const healIntervalMs = () => envMs('MIXDOG_ADMISSION_HEAL_MS', 2_000);
81
+
82
+ // A path that cannot be read at all (access denied, in use, a directory) is an
83
+ // I/O failure, not damage: it refuses everything and is never touched.
84
+ function assertLedgerReadable(path) {
85
+ try {
86
+ readFileSync(path);
87
+ } catch (error) {
88
+ if (error?.code !== 'ENOENT') throw worktreeRefusal('workspace-admission-unreadable');
89
+ }
90
+ }
91
+
37
92
  /**
38
- * Reads the ledger under its lock. A missing ledger is empty. A ledger that is
39
- * not JSON is a torn write (a crash or power loss after the rename committed
40
- * the file's size but not its bytes leaves it NUL-filled on NTFS): nothing in
41
- * it can be recovered and every lease it held belongs to a process that died
42
- * with it, so refusing forever would only lock the project out. It is moved
43
- * aside as `<FILE>.corrupt-<time>` for inspection and admission starts over
44
- * empty. A ledger that cannot be read at all (permissions, I/O, a directory in
45
- * its place) is not an empty one and still refuses, as does one that moves
46
- * aside unsuccessfully: an unreadable file is never overwritten.
93
+ * A damaged ledger (NUL, truncated, malformed) must not block every prompt and
94
+ * shell command of the workspace, nor may it make a destructive operation fail
95
+ * open. Called under the ledger lock: its bytes are COPIED aside as
96
+ * `<ledger>.corrupt-<ts>` and the same locked update replaces it with a ledger
97
+ * carrying `recoveredAt`. The ledger is never moved away: a missing ledger
98
+ * reads as "no leases", so any gap between the move and the publish (another
99
+ * process taking the lock, or a crash) would admit management beside a live
100
+ * foreign turn. A crash before the publish leaves the damaged file in place,
101
+ * which the next writer recovers the same way. If the copy cannot be kept the
102
+ * ledger is left untouched and everything refuses.
47
103
  */
48
- function readLedger(path) {
49
- let text;
104
+ function keepDamagedAside(path) {
105
+ const aside = quarantinePath(path);
50
106
  try {
51
- text = readFileSync(path, 'utf8');
52
- } catch (error) {
53
- if (error?.code === 'ENOENT') return { version: 1, leases: [] };
107
+ writeFileAtomicSync(aside, readFileSync(path));
108
+ } catch {
54
109
  throw worktreeRefusal('workspace-admission-unreadable');
55
110
  }
111
+ process.stderr.write(`[state] ${path} is damaged; kept as ${aside}\n`);
112
+ }
113
+
114
+ function keepAside(path, value) {
115
+ const aside = quarantinePath(path);
116
+ try {
117
+ writeFileAtomicSync(aside, JSON.stringify(value));
118
+ process.stderr.write(`[state] ${path} held invalid lease records; kept as ${aside}\n`);
119
+ } catch {
120
+ // Best effort: the valid records carry on either way.
121
+ }
122
+ }
123
+
124
+ const aliveLease = (lease) => isPidAliveOrUnknown(lease.pid);
125
+
126
+ // Management leases live in a mirror file as well as the ledger: damage to
127
+ // one never hides an operation in progress from the readers.
128
+ function readMirror(mirror) {
56
129
  try {
57
- return JSON.parse(text);
130
+ const state = readJsonStateSync(mirror, { validate: validateLedger, strictOwner: true });
131
+ return (state?.leases ?? []).filter((lease) => isLease(lease) && lease.kind === 'management' && aliveLease(lease));
58
132
  } catch {
133
+ return [];
134
+ }
135
+ }
136
+
137
+ function writeMirror(mirror, leases) {
138
+ try {
139
+ writeJsonAtomicSync(mirror, { version: VERSION, leases }, { compact: true });
140
+ } catch (error) {
141
+ process.stderr.write(`[state] ${mirror} could not be written (${error?.message})\n`);
142
+ }
143
+ }
144
+
145
+ // Leases this process holds, republished into any ledger that lost them.
146
+ const held = new Map();
147
+ let healTimer = null;
148
+
149
+ // A management hold shares one token across its folders, so a lease is
150
+ // identified by token and path.
151
+ const leaseKey = (lease) => `${lease.token}\0${lease.path}`;
152
+
153
+ function track(commonDir, lease) {
154
+ held.set(leaseKey(lease), { commonDir, lease });
155
+ healTimer ??= setInterval(healHeld, healIntervalMs());
156
+ healTimer.unref?.();
157
+ }
158
+
159
+ function untrack(token) {
160
+ for (const [key, { lease }] of held) if (lease.token === token) held.delete(key);
161
+ if (!held.size && healTimer) {
162
+ clearInterval(healTimer);
163
+ healTimer = null;
164
+ }
165
+ }
166
+
167
+ function ledgerLacks(commonDir, keys) {
168
+ let state;
169
+ try {
170
+ state = parseJsonText(readFileSync(join(commonDir, FILE), 'utf8'));
171
+ } catch (error) {
172
+ return error?.code === 'ENOENT' || error instanceof SyntaxError;
173
+ }
174
+ if (isNewer(state)) return false;
175
+ if (!hasLedgerShape(state)) return true;
176
+ const stored = new Set(state.leases.filter(Boolean).map(leaseKey));
177
+ return keys.some((key) => !stored.has(key));
178
+ }
179
+
180
+ function healHeld() {
181
+ const byDir = new Map();
182
+ for (const [key, { commonDir }] of held) byDir.set(commonDir, [...(byDir.get(commonDir) ?? []), key]);
183
+ for (const [commonDir, keys] of byDir) {
184
+ if (!ledgerLacks(commonDir, keys)) continue;
59
185
  try {
60
- renameSync(path, `${path}.corrupt-${Date.now()}`);
186
+ mutate(
187
+ commonDir,
188
+ () => {},
189
+ () => {}
190
+ );
61
191
  } catch {
62
- throw worktreeRefusal('workspace-admission-unreadable');
192
+ // Retried on the next tick.
63
193
  }
64
- return { version: 1, leases: [] };
65
194
  }
66
195
  }
67
196
 
68
- function mutate(commonDir, update) {
197
+ /**
198
+ * Runs `update(leases, { unknown })` under the ledger lock. `unknown`: the
199
+ * ledger was recovered recently and may be missing other processes' leases.
200
+ * `onNewer` answers for a ledger of a newer build, which is left untouched:
201
+ * it refuses by default; readers pass an answer that admits them unrecorded.
202
+ */
203
+ function mutate(commonDir, update, onNewer = refuseUnreadable) {
69
204
  const path = join(commonDir, FILE);
70
- return withFileLockSync(
71
- `${path}.lock`,
72
- () => {
73
- const state = readLedger(path);
74
- if (
75
- state?.version !== 1 ||
76
- !Array.isArray(state.leases) ||
77
- state.leases.some(
78
- (lease) =>
79
- !lease ||
80
- !['turn', 'command', 'management'].includes(lease.kind) ||
81
- typeof lease.token !== 'string' ||
82
- !Number.isSafeInteger(lease.pid) ||
83
- lease.pid <= 0 ||
84
- typeof lease.path !== 'string' ||
85
- !isAbsolute(lease.path)
86
- )
87
- ) {
88
- throw worktreeRefusal('workspace-admission-unreadable');
205
+ const mirror = `${path}.management`;
206
+ assertLedgerReadable(path);
207
+ let result;
208
+ let refusal = null;
209
+ updateJsonAtomicSync(
210
+ path,
211
+ (current) => {
212
+ if (isNewer(current)) {
213
+ result = onNewer();
214
+ return undefined;
215
+ }
216
+ const now = Date.now();
217
+ let state;
218
+ let recovered = false;
219
+ if (current === null) {
220
+ state = emptyLedger();
221
+ // `keepWithoutBackup` leaves a ledger whose text does not parse in
222
+ // place, so null with a file present is damage; no file is a new ledger.
223
+ if (existsSync(path)) {
224
+ keepDamagedAside(path);
225
+ recovered = true;
226
+ }
227
+ } else if (!hasLedgerShape(current)) {
228
+ keepAside(path, current);
229
+ state = emptyLedger();
230
+ recovered = true;
231
+ } else {
232
+ state = { ...current, leases: current.leases.filter(isLease) };
233
+ if (state.leases.length !== current.leases.length) {
234
+ keepAside(path, current);
235
+ recovered = true;
236
+ }
89
237
  }
238
+ if (recovered) state.recoveredAt = now;
90
239
  // Only a pid PROVEN dead (ESRCH) drops its lease, whatever its kind; an
91
240
  // unprobeable pid still counts as running. A live runtime keeps a command
92
241
  // whose tree it cannot verify reserved under its own (live) pid until the
93
242
  // tree is confirmed gone, so a lease that outlives every pid belongs to a
94
243
  // restarted runtime that can no longer release it.
95
- state.leases = state.leases.filter((lease) => isPidAliveOrUnknown(lease.pid));
96
- const result = update(state.leases);
97
- writeJsonAtomicSync(path, state, { lock: false });
98
- return result;
244
+ state.leases = state.leases.filter(aliveLease);
245
+ const present = new Set(state.leases.map(leaseKey));
246
+ const restore = [
247
+ ...(recovered ? readMirror(mirror) : []),
248
+ ...[...held.values()].filter((own) => own.commonDir === commonDir).map((own) => own.lease),
249
+ ];
250
+ for (const lease of restore) {
251
+ if (present.has(leaseKey(lease)) || !aliveLease(lease)) continue;
252
+ present.add(leaseKey(lease));
253
+ state.leases.push(lease);
254
+ }
255
+ const unknown = Number.isFinite(state.recoveredAt) && now - state.recoveredAt < recoveryWindowMs();
256
+ if (!unknown) delete state.recoveredAt;
257
+ const managementTokens = () =>
258
+ state.leases
259
+ .filter((lease) => lease.kind === 'management')
260
+ .map((lease) => lease.token)
261
+ .join();
262
+ const before = managementTokens();
263
+ const unchanged = [...state.leases];
264
+ try {
265
+ result = update(state.leases, { unknown });
266
+ } catch (error) {
267
+ // A refusal still publishes the recovery: dropping the write would
268
+ // leave the damaged file, and every retry would recover it again and
269
+ // restart the window, so management would never resume.
270
+ if (!recovered) throw error;
271
+ refusal = error;
272
+ state.leases = unchanged;
273
+ }
274
+ if (recovered || managementTokens() !== before) {
275
+ writeMirror(
276
+ mirror,
277
+ state.leases.filter((lease) => lease.kind === 'management')
278
+ );
279
+ }
280
+ return state;
99
281
  },
100
- { strictOwner: true }
282
+ // No backup restore: an older ledger would resurrect leases long released.
283
+ { strictOwner: true, keepWithoutBackup: true, restoreBackup: false }
101
284
  );
285
+ if (refusal) throw refusal;
286
+ return result;
102
287
  }
103
288
 
104
289
  function release(commonDir, token) {
105
290
  let done = false;
106
291
  return () => {
107
292
  if (done) return;
108
- mutate(commonDir, (leases) => {
109
- for (let i = leases.length - 1; i >= 0; i--) if (leases[i].token === token) leases.splice(i, 1);
110
- });
293
+ mutate(
294
+ commonDir,
295
+ (leases) => {
296
+ for (let i = leases.length - 1; i >= 0; i--) if (leases[i].token === token) leases.splice(i, 1);
297
+ },
298
+ () => {}
299
+ );
111
300
  done = true;
301
+ untrack(token);
112
302
  };
113
303
  }
114
304
 
@@ -123,12 +313,22 @@ export function tryAdmitWorkspaceActivity(sessionId, cwd, { pid = process.pid, k
123
313
  const external = isExternalCheckout(cwd);
124
314
  const path = canonicalPath(cwd);
125
315
  const token = randomUUID();
126
- const admitted = mutate(layout.commonDir, (leases) => {
127
- if (!external && leases.some((lease) => lease.kind === 'management' && overlaps(lease.path, path))) return false;
128
- leases.push({ kind, sessionId, pid, token, path });
129
- return true;
130
- });
131
- return admitted ? release(layout.commonDir, token) : null;
316
+ const lease = { kind, sessionId, pid, token, path };
317
+ const admitted = mutate(
318
+ layout.commonDir,
319
+ (leases) => {
320
+ if (!external && leases.some((other) => other.kind === 'management' && overlaps(other.path, path))) return false;
321
+ leases.push(lease);
322
+ return true;
323
+ },
324
+ // A newer build's ledger cannot be written: the reader goes unrecorded
325
+ // (prompts never wait on it) while management refuses on it.
326
+ () => UNRECORDED
327
+ );
328
+ if (admitted === UNRECORDED) return () => {};
329
+ if (!admitted) return null;
330
+ track(layout.commonDir, lease);
331
+ return release(layout.commonDir, token);
132
332
  }
133
333
 
134
334
  export async function admitWorkspaceActivity(sessionId, cwd, signal, options) {
@@ -166,17 +366,25 @@ export function holdWorkspaceActivity(folders, { sessionId = null } = {}) {
166
366
  try {
167
367
  for (const [commonDir, paths] of [...groups].sort(([a], [b]) => a.localeCompare(b))) {
168
368
  const token = randomUUID();
169
- mutate(commonDir, (leases) => {
369
+ const taken = [];
370
+ mutate(commonDir, (leases, { unknown }) => {
170
371
  const conflicts = leases.filter(
171
372
  (lease) =>
172
373
  !(sessionId && lease.sessionId === sessionId && lease.pid === process.pid) &&
173
374
  paths.some(({ path }) => overlaps(path, lease.path))
174
375
  );
175
376
  if (conflicts.length) throw worktreeRefusal('folder-in-use');
377
+ // A recovered ledger may be missing a live foreign turn: wait for the
378
+ // holders to republish (bounded by the recovery window).
379
+ if (unknown) throw worktreeRefusal('workspace-admission-unreadable');
176
380
  for (const { path, external } of paths) {
177
- if (!external) leases.push({ kind: 'management', pid: process.pid, token, path });
381
+ if (external) continue;
382
+ const lease = { kind: 'management', pid: process.pid, token, path };
383
+ leases.push(lease);
384
+ taken.push(lease);
178
385
  }
179
386
  });
387
+ for (const lease of taken) track(commonDir, lease);
180
388
  releases.push(release(commonDir, token));
181
389
  }
182
390
  } catch (error) {