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
@@ -7,6 +7,7 @@ import {
7
7
  openSync,
8
8
  readFileSync,
9
9
  renameSync,
10
+ statSync,
10
11
  unlinkSync,
11
12
  writeFileSync,
12
13
  } from 'node:fs';
@@ -17,7 +18,6 @@ import {
17
18
  readFile as readFileAsync,
18
19
  rename as renameAsync,
19
20
  unlink as unlinkAsync,
20
- writeFile as writeFileAsync,
21
21
  } from 'node:fs/promises';
22
22
  import { tmpdir } from 'node:os';
23
23
  import { dirname, basename, join, relative, isAbsolute, resolve } from 'node:path';
@@ -82,10 +82,23 @@ function renameBackoffs(opts) {
82
82
  return Array.isArray(opts.backoffs) && opts.backoffs.length > 0 ? opts.backoffs : DEFAULT_BACKOFFS_MS;
83
83
  }
84
84
 
85
+ // Windows answers EPERM/EACCES both for a sharing violation (AV/indexer holds
86
+ // the destination: clears in moments) and for renaming over a read-only file
87
+ // (never clears). Only the first is worth waiting for.
88
+ function isReadOnlyFile(path) {
89
+ try {
90
+ const stat = statSync(path);
91
+ return stat.isFile() && (stat.mode & 0o200) === 0;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
85
97
  // Jittered delay before the next rename attempt, or null when this error or
86
98
  // attempt count ends the loop. Shared so the sync and async paths cannot drift.
87
- function nextRenameDelayMs(err, attempt, backoffs) {
99
+ function nextRenameDelayMs(err, attempt, backoffs, dst) {
88
100
  if (!RETRY_CODES.has(err?.code) || attempt >= backoffs.length) return null;
101
+ if ((err.code === 'EPERM' || err.code === 'EACCES') && isReadOnlyFile(dst)) return null;
89
102
  return backoffs[attempt] + Math.floor(Math.random() * Math.min(50, Math.max(1, backoffs[attempt])));
90
103
  }
91
104
 
@@ -101,7 +114,7 @@ export function renameAttempt(src, dst, attempt, opts = {}) {
101
114
  renameSync(src, dst);
102
115
  return null;
103
116
  } catch (err) {
104
- const delayMs = nextRenameDelayMs(err, attempt, renameBackoffs(opts));
117
+ const delayMs = nextRenameDelayMs(err, attempt, renameBackoffs(opts), dst);
105
118
  if (delayMs === null) throw err;
106
119
  return delayMs;
107
120
  }
@@ -124,7 +137,7 @@ async function renameWithRetry(src, dst, opts = {}) {
124
137
  return true;
125
138
  } catch (err) {
126
139
  lastErr = err;
127
- const delayMs = nextRenameDelayMs(err, attempt, backoffs);
140
+ const delayMs = nextRenameDelayMs(err, attempt, backoffs, dst);
128
141
  if (delayMs === null) break;
129
142
  await sleep(Math.max(1, Number(delayMs) || 1));
130
143
  }
@@ -137,6 +150,14 @@ async function renameWithRetry(src, dst, opts = {}) {
137
150
  const FILE_FSYNC_TOLERATED = ['EPERM', 'ENOTSUP', 'EINVAL'];
138
151
  const DIR_FSYNC_TOLERATED = ['EPERM', 'ENOTSUP', 'EINVAL', 'EACCES'];
139
152
 
153
+ /** True when `err` is a filesystem refusing fsync on a file (network and
154
+ * cloud-sync drives), not a real I/O error: durability is best-effort there. */
155
+ export function isFileFsyncUnsupported(err) {
156
+ return FILE_FSYNC_TOLERATED.includes(err?.code);
157
+ }
158
+
159
+ // Directory fsync (POSIX durability of the rename). Windows refuses it with
160
+ // EPERM, which is tolerated.
140
161
  function fsyncPathSync(path, tolerated) {
141
162
  let fd = null;
142
163
  try {
@@ -165,6 +186,81 @@ async function fsyncPathAsync(path, tolerated) {
165
186
  }
166
187
  }
167
188
 
189
+ // Writes a file and flushes it through the handle that wrote it. Reopening to
190
+ // flush does not work: Windows refuses fsync on a read-only handle (EPERM) and
191
+ // may refuse a read-write reopen while AV scans the new file, and a 0o400 file
192
+ // cannot be reopened for writing on POSIX. Unflushed, a crash can publish the
193
+ // renamed file with its size committed but every byte NUL.
194
+ function writeFileDurableSync(path, data, opts) {
195
+ const fd = openSync(path, opts.flag || 'wx', opts.mode !== undefined ? opts.mode : 0o600);
196
+ try {
197
+ writeFileSync(fd, data, { encoding: opts.encoding || 'utf8' });
198
+ if (opts.fsync !== false) {
199
+ try {
200
+ fsyncSync(fd);
201
+ } catch (err) {
202
+ if (!isFileFsyncUnsupported(err)) throw err;
203
+ }
204
+ }
205
+ } finally {
206
+ closeSync(fd);
207
+ }
208
+ }
209
+
210
+ async function writeFileDurableAsync(path, data, opts) {
211
+ const handle = await openAsync(path, opts.flag || 'wx', opts.mode !== undefined ? opts.mode : 0o600);
212
+ try {
213
+ await handle.writeFile(data, { encoding: opts.encoding || 'utf8' });
214
+ if (opts.fsync !== false) {
215
+ try {
216
+ await handle.sync();
217
+ } catch (err) {
218
+ if (!isFileFsyncUnsupported(err)) throw err;
219
+ }
220
+ }
221
+ } finally {
222
+ await handle.close();
223
+ }
224
+ }
225
+
226
+ /** True when `content` (string or Buffer) holds nothing but NUL bytes and
227
+ * whitespace — what a crash leaves of a file whose data never reached disk. */
228
+ export function holdsNoData(content) {
229
+ if (typeof content === 'string') return /^[\0\s]*$/.test(content);
230
+ for (const byte of content) {
231
+ if (byte !== 0 && byte !== 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d) return false;
232
+ }
233
+ return true;
234
+ }
235
+
236
+ // `backup: true` keeps the version a write replaces as `<file>.bak`, the last
237
+ // good copy readJsonState* restores when the file is later found damaged.
238
+ // Damaged bytes are never promoted to the backup.
239
+ const keepsAsBackup = (opts, content) => (opts.backupAccepts ? opts.backupAccepts(content) : !holdsNoData(content));
240
+ const backupWriteOpts = (opts) => ({ mode: opts.mode, secret: opts.secret, fsync: opts.fsync });
241
+
242
+ function keepBackupSync(filePath, opts) {
243
+ let previous;
244
+ try {
245
+ previous = readFileSync(filePath);
246
+ } catch (err) {
247
+ if (err?.code === 'ENOENT') return;
248
+ throw err;
249
+ }
250
+ if (keepsAsBackup(opts, previous)) publishFileSync(`${filePath}.bak`, previous, backupWriteOpts(opts));
251
+ }
252
+
253
+ async function keepBackupAsync(filePath, opts) {
254
+ let previous;
255
+ try {
256
+ previous = await readFileAsync(filePath);
257
+ } catch (err) {
258
+ if (err?.code === 'ENOENT') return;
259
+ throw err;
260
+ }
261
+ if (keepsAsBackup(opts, previous)) await publishFileAsync(`${filePath}.bak`, previous, backupWriteOpts(opts));
262
+ }
263
+
168
264
  // Node sets NODE_TEST_CONTEXT in every `--test` process. A secret (credential)
169
265
  // write from a test must land under the temp dir; anything else is a fixture
170
266
  // about to overwrite a real profile, so fail loudly instead.
@@ -177,88 +273,95 @@ function assertTestSecretWriteSafe(filePath, opts) {
177
273
 
178
274
  export function writeFileAtomicSync(filePath, data, opts = {}) {
179
275
  assertTestSecretWriteSafe(filePath, opts);
180
- const run = () => {
181
- const dir = dirname(filePath);
182
- mkdirSync(dir, { recursive: true });
183
- const tmp = join(dir, `.${basename(filePath)}.${randomBytes(12).toString('hex')}.tmp`);
184
- try {
185
- const writeOpts = {
186
- encoding: opts.encoding || 'utf8',
187
- flag: 'wx',
188
- mode: opts.mode !== undefined ? opts.mode : 0o600,
189
- };
190
- writeFileSync(tmp, data, writeOpts);
191
- // Secure the file before publication. Rename and hard links retain this
192
- // file's ACL; resetting permissions afterward would expose a published
193
- // secret if the subsequent owner grant failed.
194
- if (opts.secret === true) _enforceOwnerOnlyAclWin32(tmp, { fresh: true });
195
- if (opts.fsync !== false) fsyncPathSync(tmp, FILE_FSYNC_TOLERATED);
196
- if (opts.createOnly === true) {
197
- // Atomic create-if-absent: linkSync fails with EEXIST if the
198
- // target already exists at link time, so a concurrent
199
- // non-locking writer (user/editor) that creates the file
200
- // between any prior existsSync gate and this call still
201
- // wins — we drop the temp and report `false` so the caller
202
- // can record the path as skipped rather than overwriting.
276
+ const run = () => publishFileSync(filePath, data, opts);
277
+ if (opts.lock === true) {
278
+ return withFileLockSync(`${filePath}.lock`, run, opts);
279
+ }
280
+ return run();
281
+ }
282
+
283
+ // Windows cloud-sync drives (Naver MYBOX, #138) refuse to create a file whose
284
+ // name starts with '.', so neither a temp file nor a quarantined copy may
285
+ // carry one. The temp shape is the edit tool's `<name>.mixdog-tmp-<hex>`.
286
+ function publishTempPath(filePath) {
287
+ return join(dirname(filePath), `${basename(filePath)}.mixdog-tmp-${randomBytes(8).toString('hex')}`);
288
+ }
289
+
290
+ function publishFileSync(filePath, data, opts) {
291
+ const dir = dirname(filePath);
292
+ mkdirSync(dir, { recursive: true });
293
+ const tmp = publishTempPath(filePath);
294
+ try {
295
+ writeFileDurableSync(tmp, data, { ...opts, flag: 'wx' });
296
+ // Secure the file before publication. Rename and hard links retain this
297
+ // file's ACL; resetting permissions afterward would expose a published
298
+ // secret if the subsequent owner grant failed.
299
+ if (opts.secret === true) _enforceOwnerOnlyAclWin32(tmp, { fresh: true });
300
+ if (opts.backup === true && opts.createOnly !== true) keepBackupSync(filePath, opts);
301
+ if (opts.createOnly === true) {
302
+ // Atomic create-if-absent: linkSync fails with EEXIST if the
303
+ // target already exists at link time, so a concurrent
304
+ // non-locking writer (user/editor) that creates the file
305
+ // between any prior existsSync gate and this call still
306
+ // wins — we drop the temp and report `false` so the caller
307
+ // can record the path as skipped rather than overwriting.
308
+ try {
309
+ linkSync(tmp, filePath);
310
+ } catch (err) {
203
311
  try {
204
- linkSync(tmp, filePath);
205
- } catch (err) {
312
+ unlinkSync(tmp);
313
+ } catch {}
314
+ if (err?.code === 'EEXIST') return false;
315
+ throw err;
316
+ }
317
+ try {
318
+ unlinkSync(tmp);
319
+ } catch {}
320
+ } else {
321
+ try {
322
+ renameWithRetrySync(tmp, filePath, opts);
323
+ } catch (err) {
324
+ // Opt-in fallback for callers whose target is a rename-lock hot
325
+ // spot on win32 (e.g. AV/indexer briefly holding the destination
326
+ // handle open), where rename-over keeps failing with EPERM/EACCES
327
+ // /EBUSY even after renameWithRetrySync's backoff loop. The write
328
+ // is already serialized by the caller's own withFileLockSync
329
+ // (opts.lock / updateJsonAtomicSync's lock), so a direct
330
+ // truncate+write of the target here is still race-safe against
331
+ // other writers using the same lock path; it just loses the
332
+ // rename's fully-atomic-swap guarantee for THIS write. Default
333
+ // behavior (throw) is unchanged for every other caller.
334
+ if (
335
+ opts.renameFallback === 'truncate' &&
336
+ opts.secret !== true &&
337
+ process.platform === 'win32' &&
338
+ RETRY_CODES.has(err?.code)
339
+ ) {
340
+ writeFileDurableSync(filePath, readFileSync(tmp), { ...opts, flag: 'w', encoding: undefined });
206
341
  try {
207
342
  unlinkSync(tmp);
208
343
  } catch {}
209
- if (err?.code === 'EEXIST') return false;
344
+ } else {
210
345
  throw err;
211
346
  }
212
- try {
213
- unlinkSync(tmp);
214
- } catch {}
215
- } else {
216
- try {
217
- renameWithRetrySync(tmp, filePath, opts);
218
- } catch (err) {
219
- // Opt-in fallback for callers whose target is a rename-lock hot
220
- // spot on win32 (e.g. AV/indexer briefly holding the destination
221
- // handle open), where rename-over keeps failing with EPERM/EACCES
222
- // /EBUSY even after renameWithRetrySync's backoff loop. The write
223
- // is already serialized by the caller's own withFileLockSync
224
- // (opts.lock / updateJsonAtomicSync's lock), so a direct
225
- // truncate+write of the target here is still race-safe against
226
- // other writers using the same lock path; it just loses the
227
- // rename's fully-atomic-swap guarantee for THIS write. Default
228
- // behavior (throw) is unchanged for every other caller.
229
- if (
230
- opts.renameFallback === 'truncate' &&
231
- opts.secret !== true &&
232
- process.platform === 'win32' &&
233
- RETRY_CODES.has(err?.code)
234
- ) {
235
- const data = readFileSync(tmp);
236
- writeFileSync(filePath, data, { mode: opts.mode !== undefined ? opts.mode : 0o600 });
237
- try {
238
- unlinkSync(tmp);
239
- } catch {}
240
- } else {
241
- throw err;
242
- }
243
- }
244
347
  }
245
- if (opts.fsyncDir === true) fsyncPathSync(dir, DIR_FSYNC_TOLERATED);
246
- return true;
247
- } catch (err) {
248
- try {
249
- if (existsSync(tmp)) unlinkSync(tmp);
250
- } catch {}
251
- throw err;
252
348
  }
253
- };
254
- if (opts.lock === true) {
255
- return withFileLockSync(`${filePath}.lock`, run, opts);
349
+ if (opts.fsyncDir === true) fsyncPathSync(dir, DIR_FSYNC_TOLERATED);
350
+ return true;
351
+ } catch (err) {
352
+ try {
353
+ if (existsSync(tmp)) unlinkSync(tmp);
354
+ } catch {}
355
+ throw err;
256
356
  }
257
- return run();
258
357
  }
259
358
 
359
+ // A JSON write's backup takes only a replaced version that still parses.
360
+ const jsonBackupOpts = (opts) =>
361
+ opts.backup === true && !opts.backupAccepts ? { ...opts, backupAccepts: isJsonContent } : opts;
362
+
260
363
  export function writeJsonAtomicSync(filePath, value, opts = {}) {
261
- return writeFileAtomicSync(filePath, `${JSON.stringify(value, null, opts.compact ? 0 : 2)}\n`, opts);
364
+ return writeFileAtomicSync(filePath, `${JSON.stringify(value, null, opts.compact ? 0 : 2)}\n`, jsonBackupOpts(opts));
262
365
  }
263
366
 
264
367
  // Windows editors and PowerShell 5 save UTF-8 with a leading byte order mark,
@@ -267,12 +370,182 @@ export function parseJsonText(text) {
267
370
  return JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
268
371
  }
269
372
 
270
- function recoverJsonMutationRead(error) {
271
- // Missing/malformed JSON retains the existing initializer contract. An I/O
272
- // or permission failure is not an empty document and must never authorize
273
- // replacing unreadable user state with a newly initialized value.
274
- if (error?.code === 'ENOENT' || error instanceof SyntaxError) return null;
275
- throw error;
373
+ function isJsonContent(content) {
374
+ try {
375
+ parseJsonText(String(content));
376
+ return true;
377
+ } catch {
378
+ return false;
379
+ }
380
+ }
381
+
382
+ // ── Damaged JSON state ──────────────────────────────────────────────
383
+ // A state file whose text does not parse, or that the caller's `validate`
384
+ // rejects (throws), is damaged: empty or NUL-filled after a crash, truncated,
385
+ // or broken by hand. Its feature must keep working, so the damaged file is
386
+ // moved aside to `<file>.corrupt-<ts>` (its bytes are kept, never overwritten)
387
+ // and the last good copy `<file>.bak` is restored when there is one. A file
388
+ // that cannot be READ (EACCES, EBUSY…) is not damaged and still throws.
389
+ // `decode` turns the file's bytes into JSON text (e.g. unsealing an encrypted
390
+ // file); throwing there marks the file damaged too.
391
+ // The rename that moves a damaged file aside replaces an existing destination,
392
+ // so the name is the first `<file>.corrupt-<ms>` that does not exist yet: two
393
+ // recoveries in one millisecond (or after a clock rollback) never overwrite a
394
+ // kept copy, and the name is no longer than the original shape. Recovery runs
395
+ // under the file's lock, so nothing else creates a quarantine name between
396
+ // this check and the rename.
397
+ export const quarantinePath = (filePath) => {
398
+ let stamp = Date.now();
399
+ while (existsSync(`${filePath}.corrupt-${stamp}`)) stamp += 1;
400
+ return `${filePath}.corrupt-${stamp}`;
401
+ };
402
+ const LOCK_BUSY_CODES = new Set(['ELOCKCONTENDED', 'ELOCKTIMEOUT']);
403
+ const STATE_UNSUPPORTED = 'ESTATEUNSUPPORTED';
404
+ const acceptAny = (value) => value;
405
+ const utf8Text = (content) => content.toString('utf8');
406
+
407
+ /** For `validate`: the file is intact but in a format a newer build wrote.
408
+ * It is not damage — the file stays in place, untouched, and the read
409
+ * answers the fallback, so a downgrade never discards a newer build's data. */
410
+ export function unsupportedState(message) {
411
+ return Object.assign(new Error(message), { code: STATE_UNSUPPORTED });
412
+ }
413
+
414
+ const stateParser =
415
+ (validate = acceptAny, decode = utf8Text) =>
416
+ (content) => {
417
+ if (content === null) return { state: 'absent' };
418
+ try {
419
+ return { state: 'ok', value: validate(parseJsonText(decode(content))), content };
420
+ } catch (error) {
421
+ return { state: error?.code === STATE_UNSUPPORTED ? 'unsupported' : 'damaged', error };
422
+ }
423
+ };
424
+
425
+ function readContentOrNullSync(filePath) {
426
+ try {
427
+ return readFileSync(filePath);
428
+ } catch (err) {
429
+ if (err?.code === 'ENOENT') return null;
430
+ throw err;
431
+ }
432
+ }
433
+
434
+ async function readContentOrNullAsync(filePath) {
435
+ try {
436
+ return await readFileAsync(filePath);
437
+ } catch (err) {
438
+ if (err?.code === 'ENOENT') return null;
439
+ throw err;
440
+ }
441
+ }
442
+
443
+ function reportDamaged(filePath, error, outcome) {
444
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: escapes C0 control characters (U+0000-U+001F) so a damaged-file message stays on one log line.
445
+ const reason = String(error?.message || error).replace(/[\0-\x1f]/g, (c) => `\\x${c.charCodeAt(0).toString(16)}`);
446
+ process.stderr.write(`[state] ${filePath} is damaged (${reason}); ${outcome}\n`);
447
+ }
448
+
449
+ const restoreOpts = (opts) => ({ mode: opts.mode, secret: opts.secret });
450
+ const KEPT_IN_PLACE = 'left in place: no good copy to restore';
451
+
452
+ const NO_BACKUP = { state: 'absent' };
453
+
454
+ // Caller holds `${filePath}.lock`. The file is re-read under the lock, so a
455
+ // file another process has repaired meanwhile is kept. `keepWithoutBackup` leaves a
456
+ // damaged file in place when there is no good copy: for a file other readers
457
+ // treat "missing" as permissive (e.g. a deny list), moving it would fail open.
458
+ // `restoreBackup: false` never restores `.bak` — for files where an older copy
459
+ // is worse than none (e.g. permission grants a later revocation removed).
460
+ function recoverDamagedSync(filePath, parse, opts) {
461
+ const current = parse(readContentOrNullSync(filePath));
462
+ if (current.state !== 'damaged') return current;
463
+ const backup = opts.restoreBackup === false ? NO_BACKUP : parse(readContentOrNullSync(`${filePath}.bak`));
464
+ if (backup.state !== 'ok' && opts.keepWithoutBackup) {
465
+ reportDamaged(filePath, current.error, KEPT_IN_PLACE);
466
+ return { state: 'absent' };
467
+ }
468
+ const aside = quarantinePath(filePath);
469
+ renameWithRetrySync(filePath, aside, opts);
470
+ if (backup.state === 'ok') publishFileSync(filePath, backup.content, restoreOpts(opts));
471
+ reportDamaged(filePath, current.error, recoveredOutcome(aside, backup));
472
+ return backup.state === 'ok' ? backup : { state: 'absent' };
473
+ }
474
+
475
+ async function recoverDamagedAsync(filePath, parse, opts) {
476
+ const current = parse(await readContentOrNullAsync(filePath));
477
+ if (current.state !== 'damaged') return current;
478
+ const backup = opts.restoreBackup === false ? NO_BACKUP : parse(await readContentOrNullAsync(`${filePath}.bak`));
479
+ if (backup.state !== 'ok' && opts.keepWithoutBackup) {
480
+ reportDamaged(filePath, current.error, KEPT_IN_PLACE);
481
+ return { state: 'absent' };
482
+ }
483
+ const aside = quarantinePath(filePath);
484
+ await renameWithRetry(filePath, aside, opts);
485
+ if (backup.state === 'ok') await publishFileAsync(filePath, backup.content, restoreOpts(opts));
486
+ reportDamaged(filePath, current.error, recoveredOutcome(aside, backup));
487
+ return backup.state === 'ok' ? backup : { state: 'absent' };
488
+ }
489
+
490
+ const recoveredOutcome = (aside, backup) =>
491
+ `kept as ${aside}${backup.state === 'ok' ? '; restored the last good copy' : '; starting empty'}`;
492
+
493
+ /** Reads a JSON state file that must never block its feature. Returns the
494
+ * validated value, or `fallback` (as is) when the file is missing or damaged
495
+ * and has no good backup. `validate` maps the parsed value and throws to mark
496
+ * it damaged; `decode` maps the raw bytes to JSON text. Other opts (`secret`,
497
+ * `mode`, lock timing) apply to the lock and to a restored file. */
498
+ export function readJsonStateSync(filePath, { validate, decode, fallback = null, ...opts } = {}) {
499
+ const parse = stateParser(validate, decode);
500
+ const first = parse(readContentOrNullSync(filePath));
501
+ if (first.state === 'ok') return first.value;
502
+ if (first.state !== 'damaged') return fallback;
503
+ let recovered;
504
+ try {
505
+ recovered = withFileLockSync(`${filePath}.lock`, () => recoverDamagedSync(filePath, parse, opts), opts);
506
+ } catch (err) {
507
+ if (!LOCK_BUSY_CODES.has(err?.code)) throw err;
508
+ reportDamaged(filePath, first.error, 'left in place while another writer holds its lock');
509
+ return fallback;
510
+ }
511
+ return recovered.state === 'ok' ? recovered.value : fallback;
512
+ }
513
+
514
+ export async function readJsonState(filePath, { validate, decode, fallback = null, ...opts } = {}) {
515
+ const parse = stateParser(validate, decode);
516
+ const first = parse(await readContentOrNullAsync(filePath));
517
+ if (first.state === 'ok') return first.value;
518
+ if (first.state !== 'damaged') return fallback;
519
+ let recovered;
520
+ try {
521
+ recovered = await withFileLock(`${filePath}.lock`, () => recoverDamagedAsync(filePath, parse, opts), opts);
522
+ } catch (err) {
523
+ if (!LOCK_BUSY_CODES.has(err?.code)) throw err;
524
+ reportDamaged(filePath, first.error, 'left in place while another writer holds its lock');
525
+ return fallback;
526
+ }
527
+ return recovered.state === 'ok' ? recovered.value : fallback;
528
+ }
529
+
530
+ // Read-modify-write start: a missing file is null; a damaged one is kept
531
+ // aside (never silently overwritten) and its last good copy, if any, is the
532
+ // mutator's input. I/O failures throw — an unreadable file is not empty.
533
+ const parseAnyJson = stateParser();
534
+
535
+ function readForMutationSync(filePath, opts) {
536
+ const current = parseAnyJson(readContentOrNullSync(filePath));
537
+ if (current.state === 'ok') return current.value;
538
+ if (current.state === 'absent') return null;
539
+ const recovered = recoverDamagedSync(filePath, parseAnyJson, opts);
540
+ return recovered.state === 'ok' ? recovered.value : null;
541
+ }
542
+
543
+ async function readForMutationAsync(filePath, opts) {
544
+ const current = parseAnyJson(await readContentOrNullAsync(filePath));
545
+ if (current.state === 'ok') return current.value;
546
+ if (current.state === 'absent') return null;
547
+ const recovered = await recoverDamagedAsync(filePath, parseAnyJson, opts);
548
+ return recovered.state === 'ok' ? recovered.value : null;
276
549
  }
277
550
 
278
551
  export function updateJsonAtomicSync(filePath, mutator, opts = {}) {
@@ -280,12 +553,7 @@ export function updateJsonAtomicSync(filePath, mutator, opts = {}) {
280
553
  return withFileLockSync(
281
554
  `${filePath}.lock`,
282
555
  () => {
283
- let cur = null;
284
- try {
285
- cur = parseJsonText(readFileSync(filePath, 'utf8'));
286
- } catch (error) {
287
- cur = recoverJsonMutationRead(error);
288
- }
556
+ const cur = readForMutationSync(filePath, writeOpts);
289
557
  const next = mutator(cur);
290
558
  if (next === undefined) return cur;
291
559
  writeJsonAtomicSync(filePath, next, { ...writeOpts, lock: false });
@@ -302,45 +570,7 @@ export function updateJsonAtomicSync(filePath, mutator, opts = {}) {
302
570
  // Async writes retain atomic rename publication; truncate recovery is sync-only.
303
571
  export async function writeFileAtomicAsync(filePath, data, opts = {}) {
304
572
  assertTestSecretWriteSafe(filePath, opts);
305
- const run = async () => {
306
- const dir = dirname(filePath);
307
- await mkdirAsync(dir, { recursive: true });
308
- const tmp = join(dir, `.${basename(filePath)}.${randomBytes(12).toString('hex')}.tmp`);
309
- try {
310
- const writeOpts = {
311
- encoding: opts.encoding || 'utf8',
312
- flag: 'wx',
313
- mode: opts.mode !== undefined ? opts.mode : 0o600,
314
- };
315
- await writeFileAsync(tmp, data, writeOpts);
316
- // Both publication paths retain the secured file's existing ACL.
317
- if (opts.secret === true) await _enforceOwnerOnlyAclWin32Async(tmp, { fresh: true });
318
- if (opts.fsync !== false) await fsyncPathAsync(tmp, FILE_FSYNC_TOLERATED);
319
- if (opts.createOnly === true) {
320
- try {
321
- await linkAsync(tmp, filePath);
322
- } catch (err) {
323
- try {
324
- await unlinkAsync(tmp);
325
- } catch {}
326
- if (err?.code === 'EEXIST') return false;
327
- throw err;
328
- }
329
- try {
330
- await unlinkAsync(tmp);
331
- } catch {}
332
- } else {
333
- await renameWithRetry(tmp, filePath, opts);
334
- }
335
- if (opts.fsyncDir === true) await fsyncPathAsync(dir, DIR_FSYNC_TOLERATED);
336
- return true;
337
- } catch (err) {
338
- try {
339
- await unlinkAsync(tmp);
340
- } catch {}
341
- throw err;
342
- }
343
- };
573
+ const run = () => publishFileAsync(filePath, data, opts);
344
574
  // Acquire the file lock before occupying an I/O slot. Otherwise a waiter can
345
575
  // exhaust the gate while the current lock owner needs it to finish its write.
346
576
  if (opts.lock !== true) return _runAdaptiveFileIo(run);
@@ -369,8 +599,45 @@ export async function writeFileAtomicAsync(filePath, data, opts = {}) {
369
599
  );
370
600
  }
371
601
 
602
+ // Never re-enters the I/O gate: a backup or restore runs inside a slot the
603
+ // caller already holds.
604
+ async function publishFileAsync(filePath, data, opts) {
605
+ const dir = dirname(filePath);
606
+ await mkdirAsync(dir, { recursive: true });
607
+ const tmp = publishTempPath(filePath);
608
+ try {
609
+ await writeFileDurableAsync(tmp, data, { ...opts, flag: 'wx' });
610
+ // Both publication paths retain the secured file's existing ACL.
611
+ if (opts.secret === true) await _enforceOwnerOnlyAclWin32Async(tmp, { fresh: true });
612
+ if (opts.backup === true && opts.createOnly !== true) await keepBackupAsync(filePath, opts);
613
+ if (opts.createOnly === true) {
614
+ try {
615
+ await linkAsync(tmp, filePath);
616
+ } catch (err) {
617
+ try {
618
+ await unlinkAsync(tmp);
619
+ } catch {}
620
+ if (err?.code === 'EEXIST') return false;
621
+ throw err;
622
+ }
623
+ try {
624
+ await unlinkAsync(tmp);
625
+ } catch {}
626
+ } else {
627
+ await renameWithRetry(tmp, filePath, opts);
628
+ }
629
+ if (opts.fsyncDir === true) await fsyncPathAsync(dir, DIR_FSYNC_TOLERATED);
630
+ return true;
631
+ } catch (err) {
632
+ try {
633
+ await unlinkAsync(tmp);
634
+ } catch {}
635
+ throw err;
636
+ }
637
+ }
638
+
372
639
  export function writeJsonAtomicAsync(filePath, value, opts = {}) {
373
- return writeFileAtomicAsync(filePath, `${JSON.stringify(value, null, opts.compact ? 0 : 2)}\n`, opts);
640
+ return writeFileAtomicAsync(filePath, `${JSON.stringify(value, null, opts.compact ? 0 : 2)}\n`, jsonBackupOpts(opts));
374
641
  }
375
642
 
376
643
  // Async read-modify-write. Same lock path (`${filePath}.lock`) and protocol
@@ -383,12 +650,7 @@ export async function updateJsonAtomic(filePath, mutator, opts = {}) {
383
650
  return withFileLock(
384
651
  `${filePath}.lock`,
385
652
  async () => {
386
- let cur = null;
387
- try {
388
- cur = parseJsonText(await readFileAsync(filePath, 'utf8'));
389
- } catch (error) {
390
- cur = recoverJsonMutationRead(error);
391
- }
653
+ const cur = await readForMutationAsync(filePath, writeOpts);
392
654
  const next = mutator(cur);
393
655
  if (next === undefined) return cur;
394
656
  await writeJsonAtomicAsync(filePath, next, { ...writeOpts, lock: false });
@@ -306,12 +306,12 @@ export function cleanupBackgroundTasks(options = {}) {
306
306
  // `notify`: true notifies immediately, false never notifies (owner teardown).
307
307
  // Left unset, a `notifyOnCancel` task defers its single `cancelled`
308
308
  // notification until its run settles (see settleCancelledBackgroundTask); any
309
- // other task stays silent.
310
- function cancelRunningTask(task, reason, notify) {
309
+ // other task stays silent. `terminalReason` records why it was cancelled.
310
+ function cancelRunningTask(task, reason, notify, terminalReason = null) {
311
311
  try {
312
312
  task.cancel?.();
313
313
  } catch {}
314
- completeBackgroundTask(task.taskId, { status: 'cancelled', error: reason, notify: notify === true });
314
+ completeBackgroundTask(task.taskId, { status: 'cancelled', error: reason, notify: notify === true, terminalReason });
315
315
  if (task.notifyOnCancel !== true || notify === true || task.status !== 'cancelled' || task.cancelNotify) return;
316
316
  if (notify === false) {
317
317
  task.cancelNotify = 'silent';
@@ -362,8 +362,11 @@ export function settleCancelledBackgroundTask(taskId, { result } = {}) {
362
362
  return task;
363
363
  }
364
364
 
365
+ /** Cancel the running tasks `options` selects. `terminalReason` (optional) is
366
+ * stamped on each one cancelled, so its run's settlement can tell why. */
365
367
  export function cancelBackgroundTasks(options = {}) {
366
368
  const reason = clean(options.reason) || 'cancelled';
369
+ const terminalReason = clean(options.terminalReason) || null;
367
370
  const wanted = clean(options.surface);
368
371
  const executionSessionId = clean(options.executionSessionId);
369
372
  const scope = normalizeTaskScope(options);
@@ -382,7 +385,7 @@ export function cancelBackgroundTasks(options = {}) {
382
385
  if (options.notify === false) silenceCancelledNotification(task);
383
386
  continue;
384
387
  }
385
- cancelRunningTask(task, reason, options.notify);
388
+ cancelRunningTask(task, reason, options.notify, terminalReason);
386
389
  cancelled += 1;
387
390
  }
388
391
  return { cancelled };