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
@@ -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;
166
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`);
169
+ }
170
+ return marker.instanceId;
167
171
  }
168
172
 
169
173
  export function hasUserDataInitMarker(dataDir) {
@@ -379,14 +383,26 @@ 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
 
387
- /** Prune one data dir's scope only; other scopes and legacy entries stay. */
392
+ // Only a published manifest makes a backup count toward retention.
393
+ const manifestPath = (scopeDir, name) => join(scopeDir, name, BACKUP_MANIFEST);
394
+
395
+ /** Prune one data dir's scope only; other scopes, legacy entries and
396
+ * unfinished reservations stay. */
388
397
  function pruneBackups(scopeDir, names, own) {
389
- for (const name of backupsToPrune(names, own)) {
398
+ const completed = names.filter((name) => {
399
+ try {
400
+ return statSync(manifestPath(scopeDir, name)).isFile();
401
+ } catch {
402
+ return false;
403
+ }
404
+ });
405
+ for (const name of backupsToPrune(completed, own)) {
390
406
  try {
391
407
  rmSync(join(scopeDir, name), { recursive: true, force: true });
392
408
  } catch {}
@@ -394,22 +410,20 @@ function pruneBackups(scopeDir, names, own) {
394
410
  }
395
411
 
396
412
  /**
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.
413
+ * Create a missing init marker; return its instanceId, or null if the data dir
414
+ * is gone or the missing marker cannot be created. Never creates the data dir.
415
+ * Read/validation failures throw; an existing marker is never replaced.
399
416
  */
400
417
  export function markUserDataInitialized(dataDir) {
401
418
  const existing = readInitInstanceId(dataDir);
402
419
  if (existing || !dataDir || !existsSync(dataDir)) return existing;
403
420
  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' }
421
+ const marker = join(dataDir, INIT_MARKER);
422
+ writeJsonAtomicSync(
423
+ marker,
424
+ { instanceId: randomUUID(), dataDir: resolve(dataDir), createdAt: new Date().toISOString() },
425
+ // Another creator may have published since the absent read; it wins.
426
+ { createOnly: true }
413
427
  );
414
428
  } catch {}
415
429
  return readInitInstanceId(dataDir);
@@ -427,8 +441,8 @@ export function ensureUserDataInitialized(dataDir) {
427
441
  if (!dataDir) return;
428
442
  const key = normalizeDataDir(dataDir);
429
443
  if (ensuredDataDirs.has(key)) return;
430
- ensuredDataDirs.add(key);
431
444
  if (!hasUserDataInitMarker(dataDir)) backupUserData(dataDir, 'initial-snapshot');
445
+ ensuredDataDirs.add(key);
432
446
  }
433
447
 
434
448
  function skipBackups() {
@@ -439,15 +453,12 @@ function skipBackups() {
439
453
  // manifest goes last, so an interrupted copy is never restorable.
440
454
  function finishBackup(dataDir, backupDir, reason) {
441
455
  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
- );
456
+ writeJsonAtomicSync(join(backupDir, BACKUP_MANIFEST), {
457
+ dataDir: resolve(dataDir),
458
+ instanceId,
459
+ reason,
460
+ createdAt: new Date().toISOString(),
461
+ });
451
462
  restoreLookupCache.delete(normalizeDataDir(dataDir));
452
463
  legacyEvidenceCache.delete(normalizeDataDir(dataDir));
453
464
  }
@@ -461,16 +472,24 @@ export function backupUserData(dataDir, reason = 'snapshot', { dirs = [] } = {})
461
472
  const scopeDir = scopeDirFor(dataDir);
462
473
  const { backupDir, names } = reserveBackupDir(scopeDir, reason);
463
474
  const copied = [];
464
- for (const rel of USER_DATA_FILES) {
465
- const src = join(dataDir, rel);
466
- if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
467
- }
468
- for (const rel of [...USER_DATA_DIRS, ...dirs]) {
469
- const src = join(dataDir, rel);
470
- if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
475
+ try {
476
+ for (const rel of USER_DATA_FILES) {
477
+ const src = join(dataDir, rel);
478
+ if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
479
+ }
480
+ for (const rel of [...USER_DATA_DIRS, ...dirs]) {
481
+ const src = join(dataDir, rel);
482
+ if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
483
+ }
484
+ if (copied.length > 0) finishBackup(dataDir, backupDir, reason);
485
+ } catch (error) {
486
+ // Drop only this call's reservation; a failed cleanup must not mask the cause.
487
+ try {
488
+ rmSync(backupDir, { recursive: true, force: true });
489
+ } catch {}
490
+ throw error;
471
491
  }
472
492
  if (copied.length > 0) {
473
- finishBackup(dataDir, backupDir, reason);
474
493
  pruneBackups(scopeDir, names, basename(backupDir));
475
494
  if (process.env.MIXDOG_SETUP_QUIET !== '1') {
476
495
  process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
@@ -494,13 +513,18 @@ async function copyTreeAsync(src, dst, copied) {
494
513
  return;
495
514
  }
496
515
  if (!st.isFile()) return;
497
- await mkdirP(dirname(dst), { recursive: true });
498
- await copyFileP(src, dst);
516
+ await writeFileAtomicAsync(dst, await readFileP(src), { mode: st.mode & 0o777 });
499
517
  copied.push(dst);
500
518
  }
501
519
 
502
520
  async function pruneBackupsAsync(scopeDir, names, own) {
503
- for (const name of backupsToPrune(names, own)) {
521
+ const completed = [];
522
+ for (const name of names) {
523
+ try {
524
+ if ((await statP(manifestPath(scopeDir, name))).isFile()) completed.push(name);
525
+ } catch {}
526
+ }
527
+ for (const name of backupsToPrune(completed, own)) {
504
528
  try {
505
529
  await rmP(join(scopeDir, name), { recursive: true, force: true });
506
530
  } catch {}
@@ -513,16 +537,23 @@ export async function backupUserDataAsync(dataDir, reason = 'snapshot') {
513
537
  const scopeDir = scopeDirFor(dataDir);
514
538
  const { backupDir, names } = await reserveBackupDirAsync(scopeDir, reason);
515
539
  const copied = [];
516
- for (const rel of USER_DATA_FILES) {
517
- const src = join(dataDir, rel);
518
- if (existsSync(src)) await copyTreeAsync(src, join(backupDir, rel), copied);
519
- }
520
- for (const rel of USER_DATA_DIRS) {
521
- const src = join(dataDir, rel);
522
- if (existsSync(src)) await copyTreeAsync(src, join(backupDir, rel), copied);
540
+ try {
541
+ for (const rel of USER_DATA_FILES) {
542
+ const src = join(dataDir, rel);
543
+ if (existsSync(src)) await copyTreeAsync(src, join(backupDir, rel), copied);
544
+ }
545
+ for (const rel of USER_DATA_DIRS) {
546
+ const src = join(dataDir, rel);
547
+ if (existsSync(src)) await copyTreeAsync(src, join(backupDir, rel), copied);
548
+ }
549
+ if (copied.length > 0) finishBackup(dataDir, backupDir, reason);
550
+ } catch (error) {
551
+ try {
552
+ await rmP(backupDir, { recursive: true, force: true });
553
+ } catch {}
554
+ throw error;
523
555
  }
524
556
  if (copied.length > 0) {
525
- finishBackup(dataDir, backupDir, reason);
526
557
  await pruneBackupsAsync(scopeDir, names, basename(backupDir));
527
558
  if (process.env.MIXDOG_SETUP_QUIET !== '1') {
528
559
  process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
@@ -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,13 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { 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 { parseJsonText, quarantinePath, withFileLockSync, writeFileAtomicSync } from '../atomic-file.mjs';
6
6
  import { isPidAliveOrUnknown } from '../pid-liveness.mjs';
7
7
  import { findGitLayout } from './git-head.mjs';
8
8
  import { canonicalPath, isWithin, samePath } from './paths.mjs';
9
9
  import { worktreeError, worktreeRefusal } from './contract.mjs';
10
- import { registryPath } from './registry.mjs';
10
+ import { readRegistrySync } from './registry.mjs';
11
11
 
12
12
  const FILE = 'mixdog-workspace-admission.json';
13
13
  const overlaps = (a, b) => isWithin(a, b) || isWithin(b, a);
@@ -24,97 +24,277 @@ export function isExternalCheckout(folder) {
24
24
  if (!layout || samePath(layout.gitDir, layout.commonDir)) return false;
25
25
  let entries;
26
26
  try {
27
- entries = JSON.parse(readFileSync(registryPath(), 'utf8'))?.entries ?? {};
28
- } catch (error) {
29
- if (error?.code !== 'ENOENT') throw worktreeRefusal('workspace-admission-unreadable');
30
- entries = {};
27
+ entries = readRegistrySync().entries;
28
+ } catch {
29
+ // A damaged registry is kept aside and reads as a restored or empty one;
30
+ // only a real I/O failure (access denied, in use) lands here.
31
+ throw worktreeRefusal('workspace-admission-unreadable');
31
32
  }
32
33
  return !Object.values(entries).some(
33
34
  (record) => typeof record?.path === 'string' && samePath(record.path, layout.topLevel)
34
35
  );
35
36
  }
36
37
 
38
+ /*
39
+ * The admission ledger lists every turn, command and management lease of a
40
+ * repository. It is kept in two copies with the same content in the Git
41
+ * common directory: `mixdog-workspace-admission.json` and its `.management`
42
+ * mirror. A grant returns its release only once both copies hold it, and a
43
+ * release is forgotten only once both copies dropped it, so damage to either
44
+ * copy alone never loses an acknowledged lease. `recoveredAt` marks a ledger
45
+ * that may be missing leases because no complete copy survived: readers still
46
+ * run, management refuses, and only a quiescent reset that removes both
47
+ * copies clears it (docs/git-github-integration.md). Time, healing and
48
+ * releases never do.
49
+ */
50
+ const VERSION = 2;
51
+ const isLease = (lease) =>
52
+ !!lease &&
53
+ ['turn', 'command', 'management'].includes(lease.kind) &&
54
+ typeof lease.token === 'string' &&
55
+ Number.isSafeInteger(lease.pid) &&
56
+ lease.pid > 0 &&
57
+ typeof lease.path === 'string' &&
58
+ isAbsolute(lease.path);
59
+ // Only a pid PROVEN dead (ESRCH) drops its lease, whatever its kind; an
60
+ // unprobeable pid still counts as running. A live runtime keeps a command
61
+ // whose tree it cannot verify reserved under its own (live) pid until the
62
+ // tree is confirmed gone, so a lease that outlives every pid belongs to a
63
+ // restarted runtime that can no longer release it.
64
+ const aliveLease = (lease) => isPidAliveOrUnknown(lease.pid);
65
+ // A management hold shares one token across its folders, so a lease is
66
+ // identified by token and path.
67
+ const leaseKey = (lease) => `${lease.token}\0${lease.path}`;
68
+ const unverifiable = () => worktreeRefusal('workspace-admission-unreadable');
69
+ const copyPaths = (commonDir) => {
70
+ const primary = join(commonDir, FILE);
71
+ return [primary, `${primary}.management`];
72
+ };
73
+ const envMs = (name, fallback) => {
74
+ const value = Number(process.env[name]);
75
+ return Number.isFinite(value) && value > 0 ? value : fallback;
76
+ };
77
+ const healIntervalMs = () => envMs('MIXDOG_ADMISSION_HEAL_MS', 2_000);
78
+
79
+ // The leases this process holds, put back into a ledger that lost them, and
80
+ // the tokens it released or failed to grant whose removal from both copies is
81
+ // not confirmed yet (token -> common dir), retried until it is.
82
+ const held = new Map();
83
+ const pending = new Map();
84
+ let healTimer = null;
85
+
37
86
  /**
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.
87
+ * One copy as read under the ledger lock. A copy that cannot be read (access
88
+ * denied, in use, a directory) or that a newer build wrote refuses everything
89
+ * and is never written: neither may pass for an empty ledger. Anything else
90
+ * that is not a ledger, or holds an invalid lease record, is damaged; its
91
+ * valid records still count. A copy is complete when it accounts for every
92
+ * acknowledged lease: intact, not uncertain, and version 2 — or the version 1
93
+ * primary, since the version 1 mirror held management leases only.
47
94
  */
48
- function readLedger(path) {
49
- let text;
95
+ function readCopy(path, primary) {
96
+ let bytes;
50
97
  try {
51
- text = readFileSync(path, 'utf8');
98
+ bytes = readFileSync(path);
52
99
  } catch (error) {
53
- if (error?.code === 'ENOENT') return { version: 1, leases: [] };
54
- throw worktreeRefusal('workspace-admission-unreadable');
100
+ if (error?.code === 'ENOENT') return { path, leases: [] };
101
+ throw unverifiable();
55
102
  }
103
+ let ledger = null;
56
104
  try {
57
- return JSON.parse(text);
105
+ ledger = parseJsonText(bytes.toString('utf8'));
58
106
  } catch {
107
+ // Unparseable: damaged below.
108
+ }
109
+ if (Number.isSafeInteger(ledger?.version) && ledger.version > VERSION) throw unverifiable();
110
+ if (![1, VERSION].includes(ledger?.version) || !Array.isArray(ledger.leases)) {
111
+ return { path, bytes, leases: [], damaged: true };
112
+ }
113
+ const leases = ledger.leases.filter(isLease);
114
+ const damaged = leases.length !== ledger.leases.length;
115
+ const uncertain = Object.hasOwn(ledger, 'recoveredAt');
116
+ const complete = !damaged && !uncertain && (ledger.version === VERSION || primary);
117
+ return { path, bytes, leases, damaged, uncertain, complete, recoveredAt: ledger.recoveredAt };
118
+ }
119
+
120
+ /**
121
+ * The ledger recovered from both copies: the union of their valid lease
122
+ * records (one copy lacking a lease never drops it), less proven-dead pids
123
+ * and this process's pending removals, plus the leases this process holds.
124
+ * It stays uncertain when either copy is, and becomes so when a copy exists
125
+ * but none is complete. Both copies missing reads as a new repository.
126
+ */
127
+ function recover(commonDir, copies) {
128
+ const own = [...held.values()].filter((entry) => entry.commonDir === commonDir).map((entry) => entry.lease);
129
+ const leases = new Map();
130
+ for (const lease of [...copies.flatMap((copy) => copy.leases), ...own]) {
131
+ const key = leaseKey(lease);
132
+ if (!leases.has(key) && pending.get(lease.token) !== commonDir && aliveLease(lease)) leases.set(key, lease);
133
+ }
134
+ const ledger = { version: VERSION, leases: [...leases.values()] };
135
+ const uncertain =
136
+ copies.some((copy) => copy.uncertain) ||
137
+ (copies.some((copy) => copy.bytes) && !copies.some((copy) => copy.complete));
138
+ if (uncertain) ledger.recoveredAt = copies.map((copy) => copy.recoveredAt).find(Number.isFinite) ?? Date.now();
139
+ return ledger;
140
+ }
141
+
142
+ /**
143
+ * #117: the bytes of a damaged copy are copied aside as `<copy>.corrupt-<ts>`
144
+ * before it is rewritten. If they cannot be kept, nothing is written and
145
+ * everything refuses.
146
+ */
147
+ function keepDamagedAside(copies) {
148
+ for (const copy of copies.filter((each) => each.damaged)) {
149
+ const aside = quarantinePath(copy.path);
59
150
  try {
60
- renameSync(path, `${path}.corrupt-${Date.now()}`);
151
+ writeFileAtomicSync(aside, copy.bytes);
61
152
  } catch {
62
- throw worktreeRefusal('workspace-admission-unreadable');
153
+ throw unverifiable();
63
154
  }
64
- return { version: 1, leases: [] };
155
+ process.stderr.write(`[state] ${copy.path} is damaged; kept as ${aside}\n`);
65
156
  }
66
157
  }
67
158
 
68
- function mutate(commonDir, update) {
69
- const path = join(commonDir, FILE);
159
+ /** Writes the primary, then the mirror; a copy that already holds the ledger is left alone. */
160
+ function publish(copies, ledger) {
161
+ const text = Buffer.from(`${JSON.stringify(ledger, null, 2)}\n`);
162
+ for (const copy of copies) {
163
+ if (copy.bytes?.equals(text)) continue;
164
+ copy.bytes = null; // Unknown until the write lands.
165
+ writeFileAtomicSync(copy.path, text);
166
+ copy.bytes = text;
167
+ }
168
+ }
169
+
170
+ function settle(tokens) {
171
+ for (const token of tokens) pending.delete(token);
172
+ syncHealTimer();
173
+ }
174
+
175
+ /**
176
+ * Runs `update(leases, { unknown })` on the recovered ledger under the
177
+ * primary copy's lock and publishes the result to both copies before
178
+ * returning. `unknown`: the ledger may be missing leases. A refused update
179
+ * still publishes the repaired ledger. A grant that cannot be published is
180
+ * refused: its leases become pending removals and the ledger without them is
181
+ * published again; whatever that leaves behind is withdrawn by the next
182
+ * mutation or heal tick, which also blocks every later grant until it lands.
183
+ */
184
+ function mutate(commonDir, update = () => {}) {
185
+ const paths = copyPaths(commonDir);
70
186
  return withFileLockSync(
71
- `${path}.lock`,
187
+ `${paths[0]}.lock`,
72
188
  () => {
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');
189
+ const copies = paths.map((path, index) => readCopy(path, index === 0));
190
+ keepDamagedAside(copies);
191
+ const withdrawn = [...pending].filter(([, dir]) => dir === commonDir).map(([token]) => token);
192
+ const base = recover(commonDir, copies);
193
+ const leases = [...base.leases];
194
+ let result;
195
+ let refusal = null;
196
+ try {
197
+ result = update(leases, { unknown: Object.hasOwn(base, 'recoveredAt') });
198
+ } catch (error) {
199
+ refusal = error;
200
+ }
201
+ const granted = refusal ? [] : leases.filter((lease) => !base.leases.includes(lease));
202
+ try {
203
+ publish(copies, refusal ? base : { ...base, leases });
204
+ settle(withdrawn);
205
+ } catch (error) {
206
+ process.stderr.write(`[state] ${paths[0]} could not be published (${error?.message})\n`);
207
+ if (refusal) throw refusal;
208
+ for (const lease of granted) pending.set(lease.token, commonDir);
209
+ if (granted.length) {
210
+ try {
211
+ publish(copies, base);
212
+ settle([...withdrawn, ...granted.map((lease) => lease.token)]);
213
+ } catch {
214
+ syncHealTimer();
215
+ }
216
+ }
217
+ throw unverifiable();
89
218
  }
90
- // Only a pid PROVEN dead (ESRCH) drops its lease, whatever its kind; an
91
- // unprobeable pid still counts as running. A live runtime keeps a command
92
- // whose tree it cannot verify reserved under its own (live) pid until the
93
- // tree is confirmed gone, so a lease that outlives every pid belongs to a
94
- // 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 });
219
+ if (refusal) throw refusal;
98
220
  return result;
99
221
  },
100
222
  { strictOwner: true }
101
223
  );
102
224
  }
103
225
 
226
+ // Without the lock, a cheap check that both copies are the same current
227
+ // ledger holding every live lease this process holds there and none of the
228
+ // tokens it is withdrawing. Repair, never proof that the ledger is complete.
229
+ function inSync(commonDir) {
230
+ let ledger;
231
+ try {
232
+ const [primary, mirror] = copyPaths(commonDir).map((path) => readFileSync(path));
233
+ if (!primary.equals(mirror)) return false;
234
+ ledger = parseJsonText(primary.toString('utf8'));
235
+ } catch {
236
+ return false;
237
+ }
238
+ if (ledger?.version !== VERSION || !Array.isArray(ledger.leases)) return false;
239
+ const stored = new Set(ledger.leases.filter(isLease).map(leaseKey));
240
+ return (
241
+ [...held].every(([key, entry]) => entry.commonDir !== commonDir || stored.has(key) || !aliveLease(entry.lease)) &&
242
+ !ledger.leases.some((lease) => pending.get(lease?.token) === commonDir)
243
+ );
244
+ }
245
+
246
+ function healHeld() {
247
+ const dirs = new Set([...[...held.values()].map((entry) => entry.commonDir), ...pending.values()]);
248
+ for (const commonDir of dirs) {
249
+ if (inSync(commonDir)) continue;
250
+ try {
251
+ mutate(commonDir);
252
+ } catch {
253
+ // Retried on the next tick.
254
+ }
255
+ }
256
+ syncHealTimer();
257
+ }
258
+
259
+ function syncHealTimer() {
260
+ if (held.size || pending.size) {
261
+ healTimer ??= setInterval(healHeld, healIntervalMs());
262
+ healTimer.unref?.();
263
+ } else if (healTimer) {
264
+ clearInterval(healTimer);
265
+ healTimer = null;
266
+ }
267
+ }
268
+
269
+ function track(commonDir, lease) {
270
+ held.set(leaseKey(lease), { commonDir, lease });
271
+ syncHealTimer();
272
+ }
273
+
274
+ // Forgotten as held first, so nothing puts the lease back; its token stays
275
+ // pending until both copies dropped it.
104
276
  function release(commonDir, token) {
105
277
  let done = false;
106
278
  return () => {
107
279
  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
- });
111
280
  done = true;
281
+ for (const [key, entry] of held) if (entry.lease.token === token) held.delete(key);
282
+ pending.set(token, commonDir);
283
+ try {
284
+ mutate(commonDir);
285
+ } catch {
286
+ // Retried by the next mutation or heal tick.
287
+ }
288
+ syncHealTimer();
112
289
  };
113
290
  }
114
291
 
115
292
  /**
116
293
  * Readers share a checkout freely. Null means a management writer currently
117
- * owns it; a reader in an external checkout is never held back.
294
+ * owns it; a reader in an external checkout is never held back. Throws
295
+ * `workspace-admission-unreadable` when the ledger cannot be read, was
296
+ * written by a newer build, or the grant could not be made durable in both
297
+ * copies: no work may run unrecorded.
118
298
  */
119
299
  export function tryAdmitWorkspaceActivity(sessionId, cwd, { pid = process.pid, kind = 'turn' } = {}) {
120
300
  if (!['turn', 'command'].includes(kind)) throw new TypeError('Invalid workspace reader kind.');
@@ -123,12 +303,15 @@ export function tryAdmitWorkspaceActivity(sessionId, cwd, { pid = process.pid, k
123
303
  const external = isExternalCheckout(cwd);
124
304
  const path = canonicalPath(cwd);
125
305
  const token = randomUUID();
306
+ const lease = { kind, sessionId, pid, token, path };
126
307
  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 });
308
+ if (!external && leases.some((other) => other.kind === 'management' && overlaps(other.path, path))) return false;
309
+ leases.push(lease);
129
310
  return true;
130
311
  });
131
- return admitted ? release(layout.commonDir, token) : null;
312
+ if (!admitted) return null;
313
+ track(layout.commonDir, lease);
314
+ return release(layout.commonDir, token);
132
315
  }
133
316
 
134
317
  export async function admitWorkspaceActivity(sessionId, cwd, signal, options) {
@@ -166,17 +349,24 @@ export function holdWorkspaceActivity(folders, { sessionId = null } = {}) {
166
349
  try {
167
350
  for (const [commonDir, paths] of [...groups].sort(([a], [b]) => a.localeCompare(b))) {
168
351
  const token = randomUUID();
169
- mutate(commonDir, (leases) => {
352
+ const taken = [];
353
+ mutate(commonDir, (leases, { unknown }) => {
170
354
  const conflicts = leases.filter(
171
355
  (lease) =>
172
356
  !(sessionId && lease.sessionId === sessionId && lease.pid === process.pid) &&
173
357
  paths.some(({ path }) => overlaps(path, lease.path))
174
358
  );
175
359
  if (conflicts.length) throw worktreeRefusal('folder-in-use');
360
+ // The ledger may be missing a live foreign lease: refused until reset.
361
+ if (unknown) throw unverifiable();
176
362
  for (const { path, external } of paths) {
177
- if (!external) leases.push({ kind: 'management', pid: process.pid, token, path });
363
+ if (external) continue;
364
+ const lease = { kind: 'management', pid: process.pid, token, path };
365
+ leases.push(lease);
366
+ taken.push(lease);
178
367
  }
179
368
  });
369
+ for (const lease of taken) track(commonDir, lease);
180
370
  releases.push(release(commonDir, token));
181
371
  }
182
372
  } catch (error) {