mixdog 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/README.md +8 -4
  2. package/package.json +2 -2
  3. package/scripts/native-release-status.mjs +238 -0
  4. package/src/defaults/shipped-history.json +5 -1
  5. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  6. package/src/defaults/skills/setup/SKILL.md +23 -1
  7. package/src/defaults/skills/setup/references/actions.md +50 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  9. package/src/defaults/skills/setup-guide/SKILL.md +7 -3
  10. package/src/defaults/skills/visualize/SKILL.md +9 -6
  11. package/src/defaults/skills/visualize/references/craft.md +8 -1
  12. package/src/help.mjs +1 -1
  13. package/src/lib/keychain-cjs.cjs +4 -1
  14. package/src/lib/rules-builder.cjs +14 -7
  15. package/src/rules/agent-mode/AGENT_MODE.md +8 -5
  16. package/src/rules/agent-mode/MIXY.md +6 -8
  17. package/src/rules/lead/LEAD.md +2 -2
  18. package/src/rules/shared/10-tool-workflow.md +4 -0
  19. package/src/rules/shared/20-research.md +2 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  21. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  24. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  25. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  27. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  28. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  30. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  31. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -5
  33. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  34. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  36. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  39. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +27 -8
  42. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +16 -0
  47. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  48. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  49. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  50. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +2 -1
  55. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  58. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  59. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  61. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  62. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  64. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -6
  66. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  67. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  68. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  69. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +6 -5
  70. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +7 -6
  71. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  73. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  75. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  76. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  77. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +17 -2
  78. package/src/runtime/agent/orchestrator/session/agent-room.mjs +133 -38
  79. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  80. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  82. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  83. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  84. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  85. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  87. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  88. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  91. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  92. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +136 -32
  93. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  94. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  95. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  96. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  97. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  98. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  99. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  100. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  101. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +5 -5
  102. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  103. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  104. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  105. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  106. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  107. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  108. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  109. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  110. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  111. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  112. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  113. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  114. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  115. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  116. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  117. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  121. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +55 -27
  123. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -8
  124. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  125. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  126. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  127. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  128. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  129. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  130. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +18 -3
  131. package/src/runtime/agent/orchestrator/session/routine-result.mjs +120 -9
  132. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  133. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  135. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  136. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  137. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  138. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  139. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  140. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  141. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  142. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  143. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  144. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  145. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  146. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +35 -5
  147. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +42 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  149. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  150. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  151. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  152. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  154. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  155. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  156. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  157. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  158. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  159. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  160. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  161. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  162. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  163. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  164. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  165. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  166. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  167. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  168. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  169. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  170. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  171. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  173. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  174. package/src/runtime/attachments/limits.mjs +5 -1
  175. package/src/runtime/attachments/office-extract.mjs +3 -6
  176. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  177. package/src/runtime/attachments/store.mjs +49 -30
  178. package/src/runtime/browser-bridge/client.mjs +73 -20
  179. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  180. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  181. package/src/runtime/channels/lib/state-file.mjs +15 -15
  182. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  183. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  184. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  185. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  186. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  187. package/src/runtime/effort-judge/judge-client.mjs +5 -2
  188. package/src/runtime/effort-judge/model-install.mjs +16 -8
  189. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  190. package/src/runtime/local-provider/catalog.mjs +5 -2
  191. package/src/runtime/local-provider/registered-models.mjs +64 -27
  192. package/src/runtime/local-provider/server.mjs +2 -3
  193. package/src/runtime/media/adapters/image-api.mjs +89 -0
  194. package/src/runtime/media/auth.mjs +64 -1
  195. package/src/runtime/media/catalog.mjs +66 -4
  196. package/src/runtime/media/jobs.mjs +4 -0
  197. package/src/runtime/media/lanes.mjs +46 -5
  198. package/src/runtime/media/media-usage.mjs +17 -0
  199. package/src/runtime/media/renditions.mjs +15 -15
  200. package/src/runtime/media/store.mjs +36 -35
  201. package/src/runtime/media/tool.mjs +8 -1
  202. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  203. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  204. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  205. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  206. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  207. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  208. package/src/runtime/office/core/journal.mjs +7 -14
  209. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  210. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  211. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  212. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  213. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  214. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  215. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  216. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  217. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  218. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  219. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  220. package/src/runtime/shared/agent-store.mjs +24 -7
  221. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  222. package/src/runtime/shared/atomic-file.d.mts +50 -0
  223. package/src/runtime/shared/atomic-file.mjs +394 -132
  224. package/src/runtime/shared/background-tasks.mjs +7 -4
  225. package/src/runtime/shared/bot-hops.mjs +30 -0
  226. package/src/runtime/shared/bot-split-migration.mjs +1 -1
  227. package/src/runtime/shared/bounded-download.mjs +4 -2
  228. package/src/runtime/shared/config.mjs +39 -9
  229. package/src/runtime/shared/defaults-separation.mjs +6 -2
  230. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  231. package/src/runtime/shared/err-text.mjs +4 -0
  232. package/src/runtime/shared/error-presentation.mjs +22 -4
  233. package/src/runtime/shared/file-has-data.mjs +28 -0
  234. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  235. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  236. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  237. package/src/runtime/shared/projects.mjs +50 -41
  238. package/src/runtime/shared/provider-accounts.mjs +17 -12
  239. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  240. package/src/runtime/shared/routine-trigger.mjs +14 -6
  241. package/src/runtime/shared/run-child.d.mts +3 -0
  242. package/src/runtime/shared/run-child.mjs +24 -3
  243. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  244. package/src/runtime/shared/singleton-owner.mjs +7 -6
  245. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  246. package/src/runtime/shared/staged-update.mjs +4 -2
  247. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  248. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  249. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  250. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  251. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  252. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  253. package/src/runtime/shared/user-cwd.mjs +9 -4
  254. package/src/runtime/shared/user-data-guard.mjs +41 -42
  255. package/src/runtime/shared/utf8-cut.mjs +10 -0
  256. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  257. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  258. package/src/runtime/tidy/core-install.mjs +26 -4
  259. package/src/runtime/tidy/install.mjs +91 -23
  260. package/src/runtime/web-search/lib/config.mjs +4 -16
  261. package/src/session-runtime/agent-home-delivery.mjs +555 -199
  262. package/src/session-runtime/agent-room-delivery.mjs +143 -36
  263. package/src/session-runtime/agent-task-sessions.mjs +667 -165
  264. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  265. package/src/session-runtime/boot/apis.mjs +45 -24
  266. package/src/session-runtime/boot/config.mjs +1 -0
  267. package/src/session-runtime/boot/facade.mjs +11 -1
  268. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  269. package/src/session-runtime/boot/providers.mjs +2 -2
  270. package/src/session-runtime/boot/shared.mjs +12 -6
  271. package/src/session-runtime/boot/tools.mjs +2 -2
  272. package/src/session-runtime/bot-status.mjs +432 -0
  273. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  274. package/src/session-runtime/config-lifecycle.mjs +60 -55
  275. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  276. package/src/session-runtime/held-requests.mjs +36 -33
  277. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  278. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  279. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +19 -4
  280. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  281. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  282. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  283. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  284. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  285. package/src/session-runtime/media-api.mjs +5 -2
  286. package/src/session-runtime/model-route/route-tuning.mjs +4 -1
  287. package/src/session-runtime/model-route/set-route.mjs +61 -1
  288. package/src/session-runtime/owned-automation-run.mjs +8 -1
  289. package/src/session-runtime/room-member-gate.mjs +16 -3
  290. package/src/session-runtime/route-resolution.mjs +16 -8
  291. package/src/session-runtime/route-state.mjs +1 -59
  292. package/src/session-runtime/routine-home-turn.mjs +5 -3
  293. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  294. package/src/session-runtime/runtime-paths.mjs +11 -5
  295. package/src/session-runtime/runtime-tunables.mjs +6 -0
  296. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  297. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  298. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  299. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  300. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  301. package/src/session-runtime/services/agent-tool/send-flow.mjs +25 -21
  302. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  303. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  304. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +8 -5
  305. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  306. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  307. package/src/session-runtime/services/agent-tool.mjs +6 -0
  308. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  309. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  310. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  311. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  312. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  313. package/src/session-runtime/services/provider-admin.mjs +14 -4
  314. package/src/session-runtime/services/schedule-access.mjs +33 -4
  315. package/src/session-runtime/services/seeds.mjs +4 -3
  316. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  317. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  318. package/src/session-runtime/session-create/materialize.mjs +2 -3
  319. package/src/session-runtime/session-create/session-options.mjs +15 -2
  320. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  321. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  322. package/src/session-runtime/setup-tool/executor.mjs +8 -2
  323. package/src/session-runtime/setup-tool/settings-contract.mjs +31 -1
  324. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  325. package/src/session-runtime/skills-api.mjs +12 -5
  326. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  327. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  328. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  329. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  330. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  331. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  332. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  333. package/src/session-runtime/tool-policy-refresh.mjs +47 -369
  334. package/src/session-runtime/tool-surface.mjs +5 -3
  335. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +95 -88
  336. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  337. package/src/session-runtime/turn/turn-run.mjs +2 -1
  338. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  339. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +519 -333
  340. package/src/session-runtime/workflow-agents-api/agent-route.mjs +46 -46
  341. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  342. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  343. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  344. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  345. package/src/standalone/daemon-agent-control.mjs +4 -1
  346. package/src/standalone/daemon-session-control.mjs +51 -10
  347. package/src/standalone/daemon.mjs +28 -10
  348. package/src/standalone/local-session-runtime.mjs +22 -14
  349. package/src/standalone/session-client.mjs +7 -2
  350. package/src/standalone/session-protocol.mjs +7 -0
  351. package/src/standalone/session-service.mjs +11 -0
  352. package/src/standalone/session-state-projection.mjs +19 -9
  353. package/src/standalone/session-tool-output.mjs +35 -9
  354. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  355. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  356. package/src/tui/app/slash-commands.mjs +1 -1
  357. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  358. package/src/tui/dist/index.mjs +529 -333
  359. package/src/tui/prompt-history-store.mjs +16 -21
  360. package/src/tui/session/context-state.mjs +4 -0
  361. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  362. package/src/tui/session/session-api/media.mjs +2 -1
  363. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  364. package/src/tui/session/session-api/routes-write.mjs +203 -0
  365. package/src/tui/session/session-api/routes.mjs +23 -247
  366. package/src/tui/session/session-api/settings.mjs +14 -1
  367. package/src/tui/session/session-flow/drain.mjs +28 -3
  368. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  369. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  370. package/src/tui/session/session-flow/steering.mjs +8 -3
  371. package/src/tui/session/tool-approval.mjs +185 -20
  372. package/src/tui/session/transcript-intake.mjs +2 -0
  373. package/src/tui/session/turn.mjs +23 -14
  374. package/src/tui/transcript-tool-failures.mjs +8 -2
  375. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  376. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  377. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -8,9 +8,9 @@
8
8
  * Data shape: { projects: [{ name, path, addedAt, lastSelectedAt? }] }
9
9
  */
10
10
  import { homedir } from 'node:os';
11
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
11
+ import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
12
12
  import { basename, isAbsolute, join, resolve } from 'node:path';
13
- import { withFileLockSync, writeJsonAtomicSync } from './atomic-file.mjs';
13
+ import { holdsNoData, readJsonStateSync, updateJsonAtomicSync, writeFileAtomicSync } from './atomic-file.mjs';
14
14
 
15
15
  const MIXDOG_HOME = process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
16
16
  const PROJECTS_FILE = process.env.MIXDOG_PROJECTS_FILE
@@ -30,47 +30,57 @@ function normalizeKey(absPath) {
30
30
  return process.platform === 'win32' ? text.replace(/[\\/]+$/, '').toLowerCase() : text.replace(/\/+$/, '');
31
31
  }
32
32
 
33
+ // The registry document as a store. A strict read (permission checks and
34
+ // mutations) drops entries without an absolute path: they can never grant
35
+ // anything, and the replaced file survives as projects.json.bak.
36
+ function normalizeStore(parsed, strict) {
37
+ const projects = Array.isArray(parsed?.projects) ? parsed.projects : [];
38
+ return {
39
+ projects: projects
40
+ .filter(
41
+ (entry) => entry && typeof entry.path === 'string' && entry.path.trim() && (!strict || isAbsolute(entry.path))
42
+ )
43
+ .map((entry) => ({
44
+ name: String(entry.name || basename(entry.path) || entry.path),
45
+ path: String(entry.path),
46
+ addedAt: Number(entry.addedAt) || 0,
47
+ ...(Number(entry.lastSelectedAt) > 0 ? { lastSelectedAt: Number(entry.lastSelectedAt) } : {}),
48
+ })),
49
+ };
50
+ }
51
+
52
+ // A damaged registry file (NUL, truncated, malformed) is kept aside as
53
+ // projects.json.corrupt-<ts> and the last good projects.json.bak is restored
54
+ // when there is one, else the registry starts empty: a damaged file never
55
+ // blocks adding, removing or selecting a project. Strict callers still see a
56
+ // real I/O failure (EACCES, EIO) as an error — an unreadable registry is not
57
+ // an empty one; the tolerant listing reads it as empty.
33
58
  function readStore(strict = false) {
34
59
  try {
35
- if (!strict && !existsSync(PROJECTS_FILE)) return { projects: [] };
36
- const raw = readFileSync(PROJECTS_FILE, 'utf8');
37
- const parsed = JSON.parse(raw);
38
- if (
39
- strict &&
40
- (!Array.isArray(parsed?.projects) ||
41
- parsed.projects.some(
42
- (entry) => !entry || typeof entry.path !== 'string' || !entry.path.trim() || !isAbsolute(entry.path)
43
- ))
44
- )
45
- throw new TypeError('Project registry is invalid.');
46
- const projects = Array.isArray(parsed?.projects) ? parsed.projects : [];
47
- return {
48
- projects: projects
49
- .filter((entry) => entry && typeof entry.path === 'string' && entry.path.trim())
50
- .map((entry) => ({
51
- name: String(entry.name || basename(entry.path) || entry.path),
52
- path: String(entry.path),
53
- addedAt: Number(entry.addedAt) || 0,
54
- ...(Number(entry.lastSelectedAt) > 0 ? { lastSelectedAt: Number(entry.lastSelectedAt) } : {}),
55
- })),
56
- };
60
+ return normalizeStore(readJsonStateSync(PROJECTS_FILE), strict);
57
61
  } catch (error) {
58
- if (strict && error?.code !== 'ENOENT') throw error;
62
+ if (strict) throw error;
59
63
  return { projects: [] };
60
64
  }
61
65
  }
62
66
 
63
67
  // Serialize the entire read/modify/write across cooperating processes, not
64
- // just publication. Unlike the generic JSON updater, a corrupt registry must
65
- // never be repaired into an empty document. Preserve synchronous return values
66
- // and no-op behavior; failed reads, lock acquisition and writes propagate.
68
+ // just publication: the file is read under the registry lock. Preserve
69
+ // synchronous return values and no-op behavior (an unchanged store is not
70
+ // written); failed reads, lock acquisition and writes propagate. Each write
71
+ // keeps the replaced version as projects.json.bak.
67
72
  function mutateStore(mutator) {
68
- return withFileLockSync(`${PROJECTS_FILE}.lock`, () => {
69
- const store = readStore(true);
70
- const { value, changed } = mutator(store);
71
- if (changed) writeJsonAtomicSync(PROJECTS_FILE, store, { lock: false });
72
- return value;
73
- });
73
+ let outcome;
74
+ updateJsonAtomicSync(
75
+ PROJECTS_FILE,
76
+ (current) => {
77
+ const store = normalizeStore(current, true);
78
+ outcome = mutator(store);
79
+ return outcome.changed ? store : undefined;
80
+ },
81
+ { backup: true }
82
+ );
83
+ return outcome.value;
74
84
  }
75
85
 
76
86
  /**
@@ -78,7 +88,8 @@ function mutateStore(mutator) {
78
88
  * selection store and memory project_id classification (resolveProjectId)
79
89
  * agree. Rules:
80
90
  * - Never overwrite an existing marker (user may have set 'common' or a
81
- * custom id — respect it).
91
+ * custom id — respect it). A marker holding no data (empty or NUL, what a
92
+ * crash leaves) is no id: it is rewritten.
82
93
  * - Only act when absPath is a real directory on disk; never create the
83
94
  * project folder itself (addProject does NOT create the directory).
84
95
  * - Marker value = provided name, else basename(absPath). Skip when the
@@ -97,14 +108,12 @@ function ensureProjectIdMarker(absPath, name) {
97
108
  return; // missing / unstattable → silently skip
98
109
  }
99
110
  const markerPath = join(absPath, '.mixdog', 'project.id');
100
- if (existsSync(markerPath)) return; // preserve existing value
111
+ const present = existsSync(markerPath);
112
+ if (present && !holdsNoData(readFileSync(markerPath))) return; // preserve existing value
101
113
  const value = String(name || basename(absPath) || '').trim();
102
114
  if (!value || value.toLowerCase() === 'common') return;
103
- const markerDir = join(absPath, '.mixdog');
104
- mkdirSync(markerDir, { recursive: true });
105
- const tmp = `${markerPath}.${process.pid}.tmp`;
106
- writeFileSync(tmp, `${value}\n`, 'utf8');
107
- renameSync(tmp, markerPath);
115
+ // A missing marker is created only if still missing (a concurrent creator's wins).
116
+ writeFileAtomicSync(markerPath, `${value}\n`, { createOnly: !present });
108
117
  } catch {
109
118
  /* best-effort marker; never throw */
110
119
  }
@@ -1,7 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
2
  import { join } from 'node:path';
4
- import { parseJsonText, updateJsonAtomic } from './atomic-file.mjs';
3
+ import { readJsonStateSync, unsupportedState, updateJsonAtomic } from './atomic-file.mjs';
5
4
  import { resolvePluginData } from './plugin-paths.mjs';
6
5
 
7
6
  export const ACCOUNT_PROVIDERS = Object.freeze([
@@ -34,12 +33,19 @@ function requireAccountId(id) {
34
33
 
35
34
  function validate(raw) {
36
35
  if (raw == null) return { version: 1, providers: {} };
36
+ // A newer build's roster is left in place: reads see no accounts and writes
37
+ // refuse, so a downgrade never discards it.
38
+ if (Number.isSafeInteger(raw.version) && raw.version > 1) {
39
+ throw unsupportedState(`Provider account settings version ${raw.version} is newer than this build.`);
40
+ }
37
41
  if (raw.version !== 1 || !raw.providers || typeof raw.providers !== 'object') {
38
42
  throw new Error('Provider account settings are unreadable.');
39
43
  }
40
44
  for (const [provider, pool] of Object.entries(raw.providers)) {
41
- requireAccountProvider(provider);
42
- if (!Array.isArray(pool.accounts) || pool.accounts.length > MAX_ACCOUNTS) {
45
+ // A provider a newer build added is carried as is: this build never reads
46
+ // or writes its pool, and treating it as damage would discard it.
47
+ if (!ACCOUNT_PROVIDERS.includes(provider)) continue;
48
+ if (!Array.isArray(pool?.accounts) || pool.accounts.length > MAX_ACCOUNTS) {
43
49
  throw new Error('Invalid provider account list.');
44
50
  }
45
51
  const ids = pool.accounts.map((row) => requireAccountId(row.id));
@@ -48,13 +54,11 @@ function validate(raw) {
48
54
  return raw;
49
55
  }
50
56
 
57
+ // Every OAuth provider's init reads the roster, so a damaged one (crash
58
+ // leftovers, truncation, a shape this build rejects) must never throw: it is
59
+ // kept aside and the last good copy, which every write keeps, is restored.
51
60
  function read(dataDir) {
52
- try {
53
- return validate(parseJsonText(readFileSync(file(dataDir), 'utf8')));
54
- } catch (error) {
55
- if (error.code === 'ENOENT') return validate(null);
56
- throw error;
57
- }
61
+ return readJsonStateSync(file(dataDir), { validate, fallback: validate(null), secret: true });
58
62
  }
59
63
 
60
64
  // Labels an earlier build assigned automatically. They read as a different
@@ -106,7 +110,8 @@ export function readProviderAccountPool(provider, dataDir = resolvePluginData())
106
110
 
107
111
  async function update(provider, mutate) {
108
112
  requireAccountProvider(provider);
109
- // Read failures must not be interpreted as an empty account roster.
113
+ // A roster that parses yet fails validation is recovered here first; the
114
+ // mutator's validate would otherwise refuse every write. I/O failures throw.
110
115
  read();
111
116
  const result = await updateJsonAtomic(
112
117
  file(),
@@ -117,7 +122,7 @@ async function update(provider, mutate) {
117
122
  state.providers[provider] = pool;
118
123
  return validate(state);
119
124
  },
120
- { mode: 0o600, secret: true, fsyncDir: true }
125
+ { mode: 0o600, secret: true, fsyncDir: true, backup: true }
121
126
  );
122
127
  return normalizePool(result.providers[provider]);
123
128
  }
@@ -4,7 +4,12 @@ export interface RoutineTrigger {
4
4
  event?: string;
5
5
  }
6
6
 
7
- export declare function routineTriggerHeader(input: { kind: string; name: string; project?: string }): string;
7
+ export declare function routineTriggerHeader(input: {
8
+ kind: string;
9
+ name: string;
10
+ project?: string;
11
+ now?: Date | number;
12
+ }): string;
8
13
  export declare function stripRoutineTriggerHeader(text: unknown): string;
9
14
  export declare function normalizeRoutineTrigger(value: unknown): RoutineTrigger | null;
10
15
  export declare function routineTriggerField(value: unknown): { routineTrigger?: RoutineTrigger };
@@ -4,18 +4,26 @@
4
4
  // carries (live, persisted in meta.transcript, restored) so a chat paints it
5
5
  // as a routine marker instead of a user bubble.
6
6
 
7
+ import { localTimestampParts } from './time-format.mjs';
8
+
7
9
  const HEADER_PREFIX = '[Routine trigger]';
8
10
  const KINDS = new Set(['schedule', 'webhook']);
9
11
 
10
- /** The first line of a routine turn's prompt: which routine fired and how to
11
- * treat the turn. Only the turn's content carries it, never the system
12
- * prompt, so the thread's prompt cache is untouched. */
13
- export function routineTriggerHeader({ kind, name, project = '' }) {
12
+ /** The first line of a routine turn's prompt: which routine fired, when (local
13
+ * time and zone: the turn gets no other date), and how to treat the turn.
14
+ * It is the routine's only conduct text. Only the turn's content carries it,
15
+ * never the system prompt, so the thread's prompt cache is untouched. */
16
+ export function routineTriggerHeader({ kind, name, project = '', now = new Date() }) {
14
17
  const what = kind === 'webhook' ? 'webhook' : 'schedule';
18
+ const at = localTimestampParts(now);
15
19
  return [
16
20
  `${HEADER_PREFIX} Your ${what} "${String(name)}" fired.`,
17
- 'Carry out its instructions below now, yourself or by delegating; nobody is watching this turn, so approvals wait for the user.',
18
- 'Reply in this chat with the result.',
21
+ ...(at ? [`Local time: ${at.date} ${at.time.slice(0, 5)} ${at.timeZone} (${at.offset}).`] : []),
22
+ 'Carry out its instructions below now, yourself or by delegating.',
23
+ 'Nobody is watching this turn and nothing asks for approval: act within your permissions.',
24
+ 'Payments and permanent deletes are refused here: report that and suggest doing it from the chat.',
25
+ 'If a sign-in, code or CAPTCHA blocks you, stop and say which site needs the user.',
26
+ 'Reply in this chat in plain words, without tool names; if you handed work off, say it started — its result arrives here later.',
19
27
  ...(project ? [`Project folder: ${project}`] : []),
20
28
  ].join(' ');
21
29
  }
@@ -0,0 +1,3 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+
3
+ export function killChildTree(child: ChildProcess): void;
@@ -8,6 +8,29 @@
8
8
  // maxBytes, or the spawn error itself.
9
9
  import { spawn } from 'node:child_process';
10
10
 
11
+ /** Kills `child` with everything it started. On Windows a plain kill ends only
12
+ * the child itself: `git` there is a launcher whose git.exe (and the alias,
13
+ * hook or filter it runs) would live on, still holding the output pipes. */
14
+ export function killChildTree(child) {
15
+ if (process.platform === 'win32' && child.pid && child.exitCode === null) {
16
+ const taskkill = `${process.env.SystemRoot || 'C:\\Windows'}\\System32\\taskkill.exe`;
17
+ const fallback = () => {
18
+ try {
19
+ child.kill();
20
+ } catch {}
21
+ };
22
+ spawn(taskkill, ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true })
23
+ .once('error', fallback)
24
+ .once('exit', (code) => {
25
+ if (code !== 0) fallback();
26
+ });
27
+ return;
28
+ }
29
+ try {
30
+ child.kill();
31
+ } catch {}
32
+ }
33
+
11
34
  export function runChild({ command, args, cwd, input, env, timeoutMs, maxBytes, stdoutEncoding = 'utf8' }) {
12
35
  return new Promise((resolvePromise, rejectPromise) => {
13
36
  const hasInput = (typeof input === 'string' || ArrayBuffer.isView(input)) && input.length > 0;
@@ -35,9 +58,7 @@ export function runChild({ command, args, cwd, input, env, timeoutMs, maxBytes,
35
58
  ? setTimeout(() => {
36
59
  const error = new Error(`${command} ${args.join(' ')} timed out after ${timeoutMs}ms`);
37
60
  error.code = 'ETIMEDOUT';
38
- try {
39
- child.kill();
40
- } catch {}
61
+ killChildTree(child);
41
62
  finish(error);
42
63
  }, timeoutMs)
43
64
  : null;
@@ -809,8 +809,14 @@ export function _unsavedBytesForTest(sessionId) {
809
809
  async function readBlob(sessionId, entry) {
810
810
  if (!entry?.blob) return undefined;
811
811
  try {
812
- const bytes = await readFile(join(blobDir(sessionId), entry.blob));
813
- return digest(bytes) === entry.blob ? bytes : undefined;
812
+ const path = join(blobDir(sessionId), entry.blob);
813
+ const bytes = await readFile(path);
814
+ if (digest(bytes) === entry.blob) return bytes;
815
+ // A torn blob (unflushed write cut by a crash). Blobs are published whole
816
+ // and never replaced, so a mismatch is damage: drop it, or the create-only
817
+ // write of a later capture of the same content would skip over it.
818
+ await rm(path, { force: true });
819
+ return undefined;
814
820
  } catch {
815
821
  return undefined;
816
822
  }
@@ -1,6 +1,6 @@
1
- import { mkdirSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
1
+ import { rmdirSync, rmSync, statSync } from 'node:fs';
2
2
  import { readFile, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
3
- import { dirname } from 'node:path';
3
+ import { writeJsonAtomicSync } from './atomic-file.mjs';
4
4
  import { withFileLock, withFileLockSync } from './file-lock.mjs';
5
5
  import { readJsonSafe } from './json-file.mjs';
6
6
  import { isPidAlive } from './pid-liveness.mjs';
@@ -19,11 +19,12 @@ const CLAIM_LOCK_STALE_MS = 5000;
19
19
  const CLAIM_LOCK_OPTS = Object.freeze({ timeoutMs: CLAIM_LOCK_TIMEOUT_MS, staleMs: CLAIM_LOCK_STALE_MS });
20
20
  const BUSY = Symbol('claim lock busy');
21
21
 
22
+ // A claim or handoff is flushed before it is published. The heartbeat's async
23
+ // refresh below is not: it rewrites the record every few seconds, and a record
24
+ // a crash leaves NUL reads as "no owner" (readJsonSafe), so the next claim
25
+ // simply takes over.
22
26
  function writeJsonAtomic(path, value) {
23
- mkdirSync(dirname(path), { recursive: true });
24
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
25
- writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
26
- renameSync(tmp, path);
27
+ writeJsonAtomicSync(path, value);
27
28
  }
28
29
 
29
30
  // Held by another live process or this one's async holder ('busy'). Any other
@@ -1,8 +1,9 @@
1
1
  // Explicit skill dependencies. Imported manifests are read, never rewritten.
2
- import { createHash, randomUUID } from 'node:crypto';
3
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, readFileSync, rmSync, statSync } from 'node:fs';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { parseDocument } from 'yaml';
6
+ import { writeFileAtomicSync } from './atomic-file.mjs';
6
7
  import { resolvePluginData } from './plugin-paths.mjs';
7
8
 
8
9
  const MAX_METADATA_BYTES = 64 * 1024;
@@ -119,12 +120,5 @@ export function saveSkillToolDependencies(filePath, dependencies) {
119
120
  const tools = normalizeSkillToolDependencies(dependencies, { strict: true });
120
121
  const content = `${JSON.stringify({ dependencies: { tools } }, null, 2)}\n`;
121
122
  if (Buffer.byteLength(content) > MAX_METADATA_BYTES) throw new Error('Skill dependency metadata is too large.');
122
- mkdirSync(dirname(path), { recursive: true });
123
- const temporary = `${path}.${randomUUID()}.tmp`;
124
- try {
125
- writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
126
- renameSync(temporary, path);
127
- } finally {
128
- rmSync(temporary, { force: true });
129
- }
123
+ writeFileAtomicSync(path, content);
130
124
  }
@@ -50,7 +50,7 @@ import { resolvePluginData } from './plugin-paths.mjs';
50
50
  import { isPidAlive as pidAlive } from './pid-liveness.mjs';
51
51
  import { sleepSync } from './sleep.mjs';
52
52
  import { detachedSpawnOpts, hiddenSpawnOpts } from './spawn-flags.mjs';
53
- import { renameWithRetrySync } from './atomic-file.mjs';
53
+ import { renameWithRetrySync, writeJsonAtomicSync } from './atomic-file.mjs';
54
54
  import {
55
55
  isDevInstall,
56
56
  localPackageVersion,
@@ -295,7 +295,9 @@ export async function runStagedInstall(version) {
295
295
 
296
296
  if (!existsSync(join(pkgDir, 'src', 'cli.mjs'))) return { ok: false, error: 'staged cli.mjs missing' };
297
297
  // Marker written LAST — its presence is the completion signal for the swap.
298
- writeFileSync(markerPath(verDir), JSON.stringify({ version: v, pkgDir, stagedAt: Date.now() }), 'utf8');
298
+ // Published whole and flushed: a NUL marker would read as "not staged" and
299
+ // force a full re-install.
300
+ writeJsonAtomicSync(markerPath(verDir), { version: v, pkgDir, stagedAt: Date.now() }, { compact: true });
299
301
  return { ok: true, version: v, dir: pkgDir };
300
302
  } catch (err) {
301
303
  return { ok: false, error: err?.message || String(err) };
@@ -197,11 +197,11 @@ async function refreshShellJobsStatus(ownerPid) {
197
197
  }
198
198
  // A killed command whose processes are not confirmed gone has already
199
199
  // reported its result, so its tool card is closed: it is listed until
200
- // its owner retires the record. Its root pid may be gone while a
201
- // descendant lives, so the owner's liveness decides, not the root's.
200
+ // its owner retires the record. An incomplete observation can leave no
201
+ // known survivors even after the owner session closes; only the owning
202
+ // runtime can confirm termination, so its liveness decides, not the root's.
202
203
  const unconfirmed = detail?.terminationUnconfirmed === true;
203
- if (unconfirmed ? !isHeldShellJobAlive(detail, ownerByJob.get(id)) : !(await isShellJobAlive(detail, p, dir, id)))
204
- continue;
204
+ if (unconfirmed ? !isPidAlive(ownerByJob.get(id) || 0) : !(await isShellJobAlive(detail, p, dir, id))) continue;
205
205
  // A foreground command is awaited in its own tool card; listing it here
206
206
  // made every short command flash in and out of the shell list. Only
207
207
  // backgrounded jobs (including promoted ones) are background work.
@@ -327,17 +327,6 @@ function summarizeShellJobs({ count, oldestMs, jobs, bySession }) {
327
327
  return { count, elapsedLabel, jobs, sessions };
328
328
  }
329
329
 
330
- // A held record lives while its owner process does. Once its owner session
331
- // has closed, it also needs a process left to hold: the root or a survivor
332
- // its termination watch last reported. Without one it is an orphan whose
333
- // processes are gone, and it resolves instead of staying unconfirmed.
334
- function isHeldShellJobAlive(detail, ownerPid) {
335
- if (!isPidAlive(ownerPid || 0)) return false;
336
- if (detail?.ownerSessionClosed !== true) return true;
337
- const survivors = Array.isArray(detail?.survivorPids) ? detail.survivorPids : [];
338
- return [detail?.pid, ...survivors].some((pid) => isPidAlive(positiveInt(pid)));
339
- }
340
-
341
330
  async function isShellJobAlive(detail, detailPath, dir, id) {
342
331
  const pid = positiveInt(detail?.pid);
343
332
  if (!pid) return false;
@@ -236,6 +236,12 @@ export function isTranscriptHiddenToolItem(item) {
236
236
  // stopped, and the restored transcript must keep that row
237
237
  // (scripts/turn-checkpoint-crash-test.mjs).
238
238
  const INTERNAL_TRANSCRIPT_INTERRUPT_RE = /^\[request interrupted by user(?: for tool use)?\]$/i;
239
+ // Every runtime control row written as a user turn when a turn is cut short
240
+ // (including the bare "[Request interrupted]" a kept Stop prompt gets). One
241
+ // pattern for "not real conversation": realTurnCount and the preview/title
242
+ // noise filter both use it.
243
+ export const CONTROL_MARKER_RE =
244
+ /^\[(?:truncated|request interrupted(?: by user(?: for tool use)?| by process restart)?)\]$/i;
239
245
  const TRANSCRIPT_CANCELLED_STATUS_RE = /^\[request interrupted(?: by process restart)?\]$/i;
240
246
 
241
247
  // Crash/implicit interruption markers remain in model-visible history for
@@ -53,6 +53,20 @@ export function normalizeMessageAttachments(value) {
53
53
  return out;
54
54
  }
55
55
 
56
+ /**
57
+ * Who a user-kind row came from, as the row's own fields: the sender, the
58
+ * submitting device and the bot that sent it. One definition for the delivered
59
+ * steering row and the still-queued one, so both render the same.
60
+ */
61
+ export function transcriptSourceFields(value) {
62
+ if (!value || typeof value !== 'object') return {};
63
+ return {
64
+ ...(typeof value.sender === 'string' && value.sender ? { sender: value.sender } : {}),
65
+ ...(typeof value.device === 'string' && value.device ? { device: value.device } : {}),
66
+ ...(typeof value.fromAgent === 'string' && value.fromAgent ? { fromAgent: value.fromAgent } : {}),
67
+ };
68
+ }
69
+
56
70
  /** A new stable message id (`meta.transcript.messageId`). */
57
71
  export function newTranscriptMessageId() {
58
72
  return `msg_${globalThis.crypto.randomUUID().replaceAll('-', '')}`;
@@ -71,14 +85,19 @@ export function persistedUserTranscriptMetadata(value) {
71
85
  ...(typeof value.attemptId === 'string' && value.attemptId ? { attemptId: value.attemptId } : {}),
72
86
  ...(Number.isFinite(Number(value.at)) ? { at: Number(value.at) } : {}),
73
87
  ...transcriptRouteMetadataFields(value),
74
- ...(typeof value.sender === 'string' && value.sender ? { sender: value.sender } : {}),
75
- // The device that submitted the prompt (host-stamped display name).
76
- ...(typeof value.device === 'string' && value.device ? { device: value.device } : {}),
88
+ // Sender, the submitting device (host-stamped display name) and the bot
89
+ // that sent this message to this session (agent-task-sessions.mjs).
90
+ ...transcriptSourceFields(value),
91
+ // Bot-to-bot chain position of a bot's message (session-runtime/bot-hops.mjs).
92
+ ...(Number.isInteger(value.hop) && value.hop > 0 ? { hop: value.hop } : {}),
77
93
  // The turn's change-record key (the outer prompt id), so a restored user
78
94
  // row still addresses its turn's Undo/Reapply.
79
95
  ...(typeof value.checkpointId === 'string' && value.checkpointId ? { checkpointId: value.checkpointId } : {}),
80
96
  // Every submission a merged queued batch delivered as this turn.
81
97
  ...submissionIdsField(value.submissionIds),
98
+ // The executions a woken batch delivered (session-flow/drain.mjs): the
99
+ // provenance a task session's report follows (agent-task-sessions.mjs).
100
+ ...consumedExecutionIdsField(value.consumedExecutionIds),
82
101
  ...attachmentsField(value.attachments),
83
102
  // A routine fire delivered as this turn (routine-trigger.mjs).
84
103
  ...routineTriggerField(value),
@@ -90,6 +109,12 @@ function submissionIdsField(value) {
90
109
  return ids.length ? { submissionIds: ids } : {};
91
110
  }
92
111
 
112
+ // Positions are kept: a null stands for a delivered entry that named no execution.
113
+ function consumedExecutionIdsField(value) {
114
+ if (!Array.isArray(value) || !value.length) return {};
115
+ return { consumedExecutionIds: value.map((id) => (typeof id === 'string' && id ? id : null)) };
116
+ }
117
+
93
118
  function attachmentsField(value) {
94
119
  const attachments = normalizeMessageAttachments(value);
95
120
  return attachments.length ? { attachments } : {};
@@ -3,8 +3,7 @@
3
3
  * record that names this session's transcript for discovery, plus the
4
4
  * first-occurrence failure logger every transcript writer shares.
5
5
  */
6
- import { mkdirSync, writeFileSync } from 'node:fs';
7
- import { dirname } from 'node:path';
6
+ import { writeJsonAtomicSync } from '../atomic-file.mjs';
8
7
 
9
8
  // Repeated-failure guard: log the first time each distinct message is seen,
10
9
  // then stay quiet so a persistently-broken path does not flood stderr.
@@ -27,10 +26,11 @@ export function createSessionRecord({ sessionRecordPath, sessionId, resolvedCwd,
27
26
 
28
27
  function write() {
29
28
  try {
30
- mkdirSync(dirname(sessionRecordPath), { recursive: true });
31
- writeFileSync(
29
+ // Published whole and flushed: a discovery reader never meets a torn or
30
+ // NUL record, and every refresh rewrites it anyway.
31
+ writeJsonAtomicSync(
32
32
  sessionRecordPath,
33
- JSON.stringify({
33
+ {
34
34
  sessionId,
35
35
  cwd: resolvedCwd,
36
36
  transcriptPath,
@@ -38,7 +38,8 @@ export function createSessionRecord({ sessionRecordPath, sessionId, resolvedCwd,
38
38
  updatedAt: Date.now(),
39
39
  kind: 'interactive',
40
40
  entrypoint: 'cli',
41
- })
41
+ },
42
+ { compact: true }
42
43
  );
43
44
  } catch (err) {
44
45
  logOnce(err);
@@ -13,8 +13,9 @@
13
13
  // list is what lets a SHARED worktree still revert: the baseline describes the
14
14
  // whole tree, but only these paths may be restored from it.
15
15
  import { createHash } from 'node:crypto';
16
- import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
16
+ import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises';
17
17
  import { join, resolve } from 'node:path';
18
+ import { writeFileAtomicAsync } from './atomic-file.mjs';
18
19
  import { cleanString as clean } from './clean.mjs';
19
20
  import { resolvePluginData } from './plugin-paths.mjs';
20
21
 
@@ -173,10 +174,10 @@ async function sweepRecords() {
173
174
  await sweepDirectory(recordRoot(), RECORD_TTL_MS, MAX_RECORD_FILES);
174
175
  }
175
176
 
177
+ // Flushed before it is published: a crash must not leave a record that reads as
178
+ // NUL (every reader below answers a damaged record as "no record").
176
179
  async function writeJsonAtomically(path, payload) {
177
- const temporary = `${path}.${process.pid}.tmp`;
178
- await writeFile(temporary, JSON.stringify(payload), 'utf8');
179
- await rename(temporary, path);
180
+ await writeFileAtomicAsync(path, JSON.stringify(payload));
180
181
  }
181
182
 
182
183
  /**
@@ -54,6 +54,7 @@ import {
54
54
  reviewSectionTruncated,
55
55
  } from './review-diff.mjs';
56
56
  import { PATCH_EDIT_MAX_BYTES } from './bounded-file-read.mjs';
57
+ import { runGitOffThread } from './git-runner.mjs';
57
58
  import { cleanString as clean } from './clean.mjs';
58
59
  import { createLineDiffBudget, lineDiffHunks, unifiedHunksBody } from './bounded-line-diff.mjs';
59
60
 
@@ -1718,13 +1719,52 @@ export async function getTurnChanges(sessionId, options = {}) {
1718
1719
  let status = 'missing';
1719
1720
  if (ownerSessionId && turnChangesPending(ownerSessionId, checkpointId)) status = 'pending';
1720
1721
  else if (record) status = record.files?.length || record.outside?.length ? 'ready' : 'none';
1721
- return turnChangesReply({
1722
+ const reply = turnChangesReply({
1722
1723
  status,
1723
1724
  checkpointId,
1724
1725
  record,
1725
1726
  patch: options?.patch === true,
1726
1727
  file: clean(options?.file) || null,
1727
1728
  });
1729
+ return status === 'ready' ? withIgnoredChangeFiles(ownerSessionId, reply) : reply;
1730
+ }
1731
+
1732
+ // The gitignored files of each answered record revision (build output a file
1733
+ // tool wrote): flagged `ignored` so a viewer can leave them out of its turn
1734
+ // previews. Asked of Git once per revision; bounded.
1735
+ const IGNORED_CHANGE_CACHE_LIMIT = 200;
1736
+ const _ignoredChangeFiles = new Map();
1737
+
1738
+ async function ignoredChangePaths(key, root, paths) {
1739
+ const cached = _ignoredChangeFiles.get(key);
1740
+ if (cached) return cached;
1741
+ let ignored = new Set();
1742
+ try {
1743
+ const result = await runGitOffThread(['check-ignore', '-z', '--stdin'], {
1744
+ cwd: root,
1745
+ input: `${paths.join('\0')}\0`,
1746
+ timeoutMs: 10_000,
1747
+ maxBytes: 4 * 1024 * 1024,
1748
+ });
1749
+ // 0: some are ignored, 1: none; anything else (no repository) flags none.
1750
+ if (result.code === 0) ignored = new Set(result.stdout.split('\0').filter(Boolean));
1751
+ } catch {
1752
+ // No Git to ask: nothing is flagged.
1753
+ }
1754
+ _ignoredChangeFiles.set(key, ignored);
1755
+ while (_ignoredChangeFiles.size > IGNORED_CHANGE_CACHE_LIMIT) {
1756
+ _ignoredChangeFiles.delete(_ignoredChangeFiles.keys().next().value);
1757
+ }
1758
+ return ignored;
1759
+ }
1760
+
1761
+ async function withIgnoredChangeFiles(ownerSessionId, reply) {
1762
+ const paths = reply.files.map((file) => file.path).filter((path) => path && !isAbsolute(path));
1763
+ if (!reply.root || paths.length === 0) return reply;
1764
+ const key = `${ownerSessionId}\0${reply.checkpointId}\0${reply.revision}\0${reply.root}`;
1765
+ const ignored = await ignoredChangePaths(key, reply.root, paths);
1766
+ if (ignored.size === 0) return reply;
1767
+ return { ...reply, files: reply.files.map((file) => (ignored.has(file.path) ? { ...file, ignored: true } : file)) };
1728
1768
  }
1729
1769
 
1730
1770
  function serializeChangeMutation(ownerSessionId, task) {
@@ -1871,11 +1911,13 @@ async function mutateTurnChanges(sessionId, checkpointIdValue, fileValue, revisi
1871
1911
  if (targets.length === 0) throw new Error(`this turn has no changes to be ${verb}`);
1872
1912
  }
1873
1913
  const updated = await mutateRecordFiles(ownerSessionId, record, targets, reverse);
1874
- return turnChangesReply({
1914
+ const reply = turnChangesReply({
1875
1915
  status: updated ? 'ready' : 'missing',
1876
1916
  checkpointId,
1877
1917
  record: withUnknownChangeStates(ownerSessionId, updated),
1878
1918
  });
1919
+ // The viewer caches this reply as it caches a read: the same `ignored` flags.
1920
+ return updated ? withIgnoredChangeFiles(ownerSessionId, reply) : reply;
1879
1921
  });
1880
1922
  }
1881
1923