mixdog 1.4.2 → 1.4.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 (417) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-shipped-history.mjs +15 -5
  3. package/scripts/gate-local.mjs +65 -19
  4. package/scripts/graph-parity.mjs +8 -3
  5. package/scripts/lib/gate-scheduler.mjs +2 -1
  6. package/scripts/lib/run-node-tests.mjs +17 -5
  7. package/scripts/lib/test-failure-summary.mjs +1 -8
  8. package/scripts/lib/test-spawn-ledger.mjs +33 -2
  9. package/scripts/post-release-check.mjs +26 -16
  10. package/scripts/provider-pingpong-live.mjs +1 -1
  11. package/scripts/prune-embedding-runtime.mjs +3 -1
  12. package/scripts/release-paths.mjs +7 -1
  13. package/src/defaults/shipped-history.json +20 -0
  14. package/src/defaults/skills/setup/SKILL.md +2 -2
  15. package/src/defaults/skills/setup/references/actions.md +16 -3
  16. package/src/defaults/skills/setup/references/surfaces.md +3 -0
  17. package/src/defaults/skills/setup-guide/SKILL.md +6 -1
  18. package/src/lib/instruction-text.cjs +36 -0
  19. package/src/lib/rules-builder.cjs +34 -20
  20. package/src/orchestration/balanced.md +23 -0
  21. package/src/orchestration/focused.md +22 -0
  22. package/src/orchestration/none.md +5 -0
  23. package/src/orchestration/swarm.md +24 -0
  24. package/src/rules/agent-mode/AGENT_MODE.md +3 -1
  25. package/src/rules/agent-mode/MIXY.md +2 -2
  26. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +3 -3
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -2
  29. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +0 -9
  30. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +67 -0
  32. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
  33. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -4
  34. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -1
  35. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +199 -46
  36. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +3 -5
  37. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +9 -3
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -5
  39. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +8 -2
  41. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +14 -0
  43. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-stop.mjs +3 -3
  46. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +4 -6
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-head-parity.json +2784 -2872
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +5 -8
  51. package/src/runtime/agent/orchestrator/providers/openai-responses-payload-parity.fixture.json +288 -300
  52. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +10 -1
  53. package/src/runtime/agent/orchestrator/providers/xai-tool-search-head-parity.json +10 -30
  54. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +30 -0
  55. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +0 -11
  56. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-availability.mjs +8 -0
  57. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +17 -3
  58. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +47 -27
  59. package/src/runtime/agent/orchestrator/runtime-core/goal-deadlines.mjs +141 -10
  60. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +26 -63
  61. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +28 -3
  62. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +36 -11
  63. package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +88 -56
  64. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +4 -1
  65. package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +51 -32
  66. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +45 -145
  67. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -98
  68. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +32 -99
  69. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +23 -78
  70. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +69 -48
  71. package/src/runtime/agent/orchestrator/runtime-core/todo-list.mjs +367 -0
  72. package/src/runtime/agent/orchestrator/runtime-core/todo-tool-defs.mjs +47 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/tool-availability.mjs +155 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +9 -0
  75. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +119 -35
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  78. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -22
  79. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +30 -0
  80. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +1 -1
  81. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +2 -2
  83. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/session/context-utils.mjs +8 -12
  88. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +7 -3
  92. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +1 -1
  93. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +1 -1
  96. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +11 -1
  97. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  99. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/todo-check.mjs +196 -0
  100. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +116 -50
  101. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +8 -1
  102. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +1 -1
  104. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -1
  105. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +5 -6
  106. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +6 -1
  107. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  108. package/src/runtime/agent/orchestrator/session/loop/turn-report.mjs +35 -0
  109. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +57 -26
  111. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +9 -7
  112. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +21 -3
  113. package/src/runtime/agent/orchestrator/session/manager/ask-turn-failure.mjs +27 -11
  114. package/src/runtime/agent/orchestrator/session/manager/ask-turn-open.mjs +37 -34
  115. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +15 -0
  116. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +25 -15
  117. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +79 -24
  118. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +3 -0
  119. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +12 -1
  120. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  121. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +6 -1
  122. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +9 -1
  123. package/src/runtime/agent/orchestrator/session/manager/pending-spool-path.mjs +2 -2
  124. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +1 -1
  125. package/src/runtime/agent/orchestrator/session/manager/prompt-settings.mjs +598 -0
  126. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +19 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +5 -0
  129. package/src/runtime/agent/orchestrator/session/manager/session-fallback-title.mjs +1 -1
  130. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +31 -0
  131. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +31 -0
  132. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +42 -6
  133. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +2 -0
  134. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +11 -1
  135. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +8 -9
  136. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +339 -88
  137. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-recovery.mjs +135 -11
  138. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +377 -104
  139. package/src/runtime/agent/orchestrator/session/manager/turn-durability.mjs +144 -0
  140. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +1 -1
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +4 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +6 -0
  143. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +1 -1
  144. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +8 -1
  145. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +5 -1
  146. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +1 -1
  147. package/src/runtime/agent/orchestrator/session/store/liveness-veto.mjs +2 -2
  148. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +23 -2
  149. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +3 -3
  150. package/src/runtime/agent/orchestrator/session/store/transcript-durability.mjs +88 -0
  151. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +5 -0
  152. package/src/runtime/agent/orchestrator/session/store-summary-log.mjs +1 -1
  153. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +14 -5
  154. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +2 -2
  155. package/src/runtime/agent/orchestrator/session/store.mjs +5 -0
  156. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +7 -1
  157. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +9 -1
  158. package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
  159. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +3 -1
  160. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +2 -2
  161. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -5
  162. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -1
  163. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +21 -3
  164. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  165. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/worktree-add-guard.mjs +77 -0
  166. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -1
  167. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +7 -3
  168. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +27 -9
  169. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +2 -2
  170. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +1 -1
  171. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +0 -1
  172. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +0 -10
  173. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1 -2
  174. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -24
  175. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary/child-kill.mjs +4 -3
  176. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +1 -2
  177. package/src/runtime/agent/orchestrator/tools/code-graph/modes/calls.mjs +1 -1
  178. package/src/runtime/agent/orchestrator/tools/code-graph/modes/symbols.mjs +2 -2
  179. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +4 -5
  180. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +3 -4
  181. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4 -22
  182. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +2 -2
  183. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +1 -1
  184. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +0 -4
  185. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +0 -1
  186. package/src/runtime/attachments/attachment-reference.mjs +1 -1
  187. package/src/runtime/attachments/pdf-extract.mjs +1 -1
  188. package/src/runtime/attachments/store.d.mts +1 -0
  189. package/src/runtime/attachments/store.mjs +2 -2
  190. package/src/runtime/channels/lib/config-reload-watch.mjs +42 -0
  191. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +29 -9
  192. package/src/runtime/channels/lib/scheduler.mjs +33 -0
  193. package/src/runtime/channels/lib/schedules-db.mjs +11 -4
  194. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +4 -1
  195. package/src/runtime/channels/lib/webhooks-db.mjs +9 -3
  196. package/src/runtime/channels/lib/whisper-server.mjs +12 -12
  197. package/src/runtime/channels/lib/worker-main.mjs +9 -31
  198. package/src/runtime/computer-bridge/linux-readiness.mjs +10 -14
  199. package/src/runtime/connectors/connector-accounts-mcp.mjs +169 -33
  200. package/src/runtime/connectors/gmail-app-password.mjs +2 -2
  201. package/src/runtime/connectors/google-mcp.mjs +1 -1
  202. package/src/runtime/effort-judge/judge-client.mjs +2 -2
  203. package/src/runtime/effort-judge/judge-input.mjs +4 -4
  204. package/src/runtime/explore/explorer.mjs +0 -5
  205. package/src/runtime/explore/tool.mjs +3 -3
  206. package/src/runtime/local-provider/hardware.mjs +1 -1
  207. package/src/runtime/media/adapters/codex-image.mjs +3 -3
  208. package/src/runtime/media/adapters/gemini-video.mjs +1 -1
  209. package/src/runtime/media/media-usage.mjs +1 -1
  210. package/src/runtime/media/store.mjs +1 -1
  211. package/src/runtime/memory/index.mjs +11 -1
  212. package/src/runtime/memory/lib/embedding-reindex.mjs +27 -0
  213. package/src/runtime/memory/lib/memory-embed.mjs +83 -14
  214. package/src/runtime/memory/lib/memory-process-lock.mjs +159 -36
  215. package/src/runtime/memory/lib/memory-text-utils.mjs +6 -1
  216. package/src/runtime/memory/lib/memory.mjs +17 -0
  217. package/src/runtime/memory/lib/pg/process.mjs +1 -1
  218. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -96
  219. package/src/runtime/memory/lib/recall-hybrid/hybrid-sql.mjs +12 -3
  220. package/src/runtime/office/authoring/docx-html-runner.mjs +1 -1
  221. package/src/runtime/office/authoring/pptx-author-action.mjs +1 -1
  222. package/src/runtime/office/authoring/xlsx-html-build.mjs +1 -1
  223. package/src/runtime/office/authoring/xlsx-html-runner.mjs +1 -1
  224. package/src/runtime/office/capabilities.mjs +1 -1
  225. package/src/runtime/office/core/office-core.mjs +1 -1
  226. package/src/runtime/office/design/content-model.mjs +3 -3
  227. package/src/runtime/office/design/docx/design-docx/section.mjs +6 -6
  228. package/src/runtime/office/design/xlsx/design-xlsx-components.mjs +1 -1
  229. package/src/runtime/office/design/xlsx/design-xlsx.mjs +1 -1
  230. package/src/runtime/office/pdf/ocr-engine.mjs +1 -1
  231. package/src/runtime/office/pdf/pdf-writer.mjs +4 -4
  232. package/src/runtime/office/portable/portable-docx-operations.mjs +2 -2
  233. package/src/runtime/office/portable/portable-soffice.mjs +8 -6
  234. package/src/runtime/office/portable/xlsx-contract.mjs +1 -1
  235. package/src/runtime/shared/atomic-file.d.mts +8 -0
  236. package/src/runtime/shared/background-tasks.mjs +22 -4
  237. package/src/runtime/shared/background-wait.mjs +66 -0
  238. package/src/runtime/shared/child-guardian.mjs +17 -7
  239. package/src/runtime/shared/config.mjs +313 -16
  240. package/src/runtime/shared/connector-catalog.d.mts +4 -5
  241. package/src/runtime/shared/connector-catalog.mjs +11 -1
  242. package/src/runtime/shared/file-lock.mjs +8 -3
  243. package/src/runtime/shared/git-runner.mjs +7 -2
  244. package/src/runtime/shared/idle-gc.mjs +30 -7
  245. package/src/runtime/shared/llm/http-agent.mjs +8 -5
  246. package/src/runtime/shared/llm/system-proxy.mjs +2 -1
  247. package/src/runtime/shared/llm/usage-ledger-quota.mjs +31 -12
  248. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +84 -8
  249. package/src/runtime/shared/llm/usage-ledger.mjs +3 -0
  250. package/src/runtime/shared/llm/usage-rollup.mjs +48 -0
  251. package/src/runtime/shared/maintenance-schedule.mjs +109 -0
  252. package/src/runtime/shared/process-runner.mjs +8 -4
  253. package/src/runtime/shared/routine-trigger.d.mts +13 -0
  254. package/src/runtime/shared/routine-trigger.mjs +34 -0
  255. package/src/runtime/shared/run-child.d.mts +2 -1
  256. package/src/runtime/shared/run-child.mjs +70 -18
  257. package/src/runtime/shared/session-scratch.mjs +14 -2
  258. package/src/runtime/shared/shipped-definitions.mjs +7 -1
  259. package/src/runtime/shared/tool-card-model.golden.json +3 -12
  260. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  261. package/src/runtime/shared/tool-work-units.mjs +31 -6
  262. package/src/runtime/shared/turn-snapshot.mjs +26 -4
  263. package/src/runtime/shared/turn-worktree-snapshot.mjs +6 -11
  264. package/src/runtime/shared/worker-requests.mjs +17 -3
  265. package/src/runtime/shared/worktrees/branch-management.mjs +1 -5
  266. package/src/runtime/shared/worktrees/contract.mjs +16 -6
  267. package/src/runtime/shared/worktrees/dependencies.mjs +127 -89
  268. package/src/runtime/shared/worktrees/disk.mjs +79 -2
  269. package/src/runtime/shared/worktrees/git.mjs +44 -11
  270. package/src/runtime/shared/worktrees/include.mjs +6 -4
  271. package/src/runtime/shared/worktrees/maintenance.mjs +422 -30
  272. package/src/runtime/shared/worktrees/manager.mjs +772 -75
  273. package/src/runtime/shared/worktrees/settings.mjs +33 -7
  274. package/src/runtime/shared/worktrees/test-support.mjs +5 -1
  275. package/src/runtime/shared/worktrees/worker-snapshots.mjs +82 -0
  276. package/src/runtime/tidy/process.mjs +2 -1
  277. package/src/runtime/web-search/index.mjs +3 -22
  278. package/src/runtime/web-search/lib/web-tools.mjs +2 -2
  279. package/src/runtime/web-search/tool-defs.mjs +17 -26
  280. package/src/session-runtime/agent-profile-changes.mjs +55 -4
  281. package/src/session-runtime/boot/apis.mjs +27 -4
  282. package/src/session-runtime/boot/begin.mjs +2 -0
  283. package/src/session-runtime/boot/config.mjs +10 -1
  284. package/src/session-runtime/boot/facade.mjs +1 -0
  285. package/src/session-runtime/boot/providers.mjs +70 -3
  286. package/src/session-runtime/boot/session.mjs +2 -0
  287. package/src/session-runtime/boot/tools.mjs +69 -25
  288. package/src/session-runtime/channel-config-api.mjs +1 -0
  289. package/src/session-runtime/config-change-effects.mjs +179 -0
  290. package/src/session-runtime/config-lifecycle/config-writers.mjs +1 -0
  291. package/src/session-runtime/config-lifecycle.mjs +160 -24
  292. package/src/session-runtime/conversation-import/discovery.mjs +86 -7
  293. package/src/session-runtime/conversation-import/service.mjs +3 -2
  294. package/src/session-runtime/goal-facade-api.mjs +49 -3
  295. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +3 -0
  296. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +3 -2
  297. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +3 -11
  298. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +18 -4
  299. package/src/session-runtime/internal-tool-executor.mjs +12 -19
  300. package/src/session-runtime/lifecycle/cold-heir.mjs +7 -0
  301. package/src/session-runtime/lifecycle/memory-ingest.mjs +3 -3
  302. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +53 -5
  303. package/src/session-runtime/lifecycle/session-switch.mjs +13 -2
  304. package/src/session-runtime/lifecycle/teardown/session-close.mjs +4 -4
  305. package/src/session-runtime/lifecycle-api.mjs +1 -0
  306. package/src/session-runtime/mcp-glue/server-catalog.mjs +14 -3
  307. package/src/session-runtime/mcp-glue/server-connection.mjs +34 -8
  308. package/src/session-runtime/model-route/set-route.mjs +40 -7
  309. package/src/session-runtime/native-web-search/routes.mjs +3 -3
  310. package/src/session-runtime/provider-auth/refresh.mjs +6 -3
  311. package/src/session-runtime/provider-auth/setup.mjs +1 -1
  312. package/src/session-runtime/provider-readiness.mjs +53 -4
  313. package/src/session-runtime/resource-api.mjs +1 -1
  314. package/src/session-runtime/resource-extension-sync.mjs +4 -8
  315. package/src/session-runtime/resource-mcp-connector-accounts.mjs +5 -1
  316. package/src/session-runtime/resource-mcp-secrets.mjs +3 -3
  317. package/src/session-runtime/resource-plugins-api.mjs +4 -0
  318. package/src/session-runtime/routine-executor.mjs +152 -0
  319. package/src/session-runtime/routine-home-turn.mjs +46 -5
  320. package/src/session-runtime/runtime-facade.mjs +5 -0
  321. package/src/session-runtime/runtime-feature-gates.mjs +2 -11
  322. package/src/session-runtime/schedule-session-run.mjs +1 -0
  323. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +6 -1
  324. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +7 -0
  325. package/src/session-runtime/services/agent-tool/send-flow.mjs +110 -6
  326. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +12 -0
  327. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +5 -0
  328. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +24 -6
  329. package/src/session-runtime/services/agent-tool.mjs +51 -1
  330. package/src/session-runtime/services/channel-admin.mjs +18 -0
  331. package/src/session-runtime/services/hook-bus/command-handler.mjs +7 -11
  332. package/src/session-runtime/services/plugin-admin.mjs +4 -0
  333. package/src/session-runtime/services/seeds.mjs +7 -0
  334. package/src/session-runtime/services/usage-stats-model.mjs +32 -11
  335. package/src/session-runtime/session-create/materialize.mjs +4 -2
  336. package/src/session-runtime/session-create/session-options.mjs +2 -0
  337. package/src/session-runtime/settings-builtin-tools-api.mjs +1 -9
  338. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +1 -1
  339. package/src/session-runtime/setup-tool/executor.mjs +8 -1
  340. package/src/session-runtime/setup-tool/extended-actions.mjs +2 -0
  341. package/src/session-runtime/setup-tool/settings-contract.mjs +28 -0
  342. package/src/session-runtime/setup-tool/tool-defs.mjs +26 -1
  343. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +40 -43
  344. package/src/session-runtime/tool-policy/mode-switch.mjs +1 -1
  345. package/src/session-runtime/tool-policy/policy-stage.mjs +106 -8
  346. package/src/session-runtime/tool-policy/workflow-overlay.mjs +2 -1
  347. package/src/session-runtime/tool-policy-refresh.mjs +182 -5
  348. package/src/session-runtime/tool-surface.mjs +38 -1
  349. package/src/session-runtime/turn/agent-ops.mjs +10 -2
  350. package/src/session-runtime/turn/session-ops.mjs +2 -0
  351. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +5 -0
  352. package/src/session-runtime/turn/turn-run.mjs +1 -1
  353. package/src/session-runtime/webhook-session-run.mjs +2 -0
  354. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +1 -4
  355. package/src/session-runtime/workflow-agents-api/agent-route.mjs +3 -10
  356. package/src/session-runtime/workflow-agents-api/instruction-files.mjs +165 -0
  357. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +5 -4
  358. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +2 -3
  359. package/src/session-runtime/workflow-agents-api.mjs +2 -0
  360. package/src/session-runtime/workspace/project-branches.mjs +1 -1
  361. package/src/session-runtime/workspace/session-view.mjs +1 -1
  362. package/src/session-runtime/workspace/transitions.mjs +20 -4
  363. package/src/session-runtime/workspace/worktree-maintenance.mjs +126 -6
  364. package/src/session-runtime/workspace/worktree-setup.mjs +13 -1
  365. package/src/session-runtime/workspace/worktree-tool.mjs +16 -0
  366. package/src/standalone/daemon-agent-control.mjs +25 -8
  367. package/src/standalone/daemon-desktop-runtime.mjs +6 -2
  368. package/src/standalone/daemon-lag-profiler.mjs +7 -7
  369. package/src/standalone/daemon.mjs +54 -2
  370. package/src/standalone/local-session-runtime.mjs +16 -0
  371. package/src/standalone/session-client/daemon-unavailable.mjs +1 -1
  372. package/src/standalone/session-protocol.mjs +11 -0
  373. package/src/standalone/session-service/agent-route-follow.mjs +23 -20
  374. package/src/standalone/session-service/agent-tree/agent-turns.mjs +18 -6
  375. package/src/standalone/session-service/quota-resume.mjs +9 -0
  376. package/src/standalone/session-service/session-calls/session-action.mjs +51 -1
  377. package/src/standalone/session-service/session-calls/session-turns.mjs +37 -18
  378. package/src/standalone/session-service/session-calls/turn-gate.mjs +34 -0
  379. package/src/standalone/session-service/session-calls.mjs +2 -1
  380. package/src/tui/App.jsx +8 -0
  381. package/src/tui/app/clipboard.mjs +1 -1
  382. package/src/tui/app/create-app-pickers.mjs +2 -0
  383. package/src/tui/app/doctor.mjs +2 -2
  384. package/src/tui/app/panel-signature.mjs +1 -1
  385. package/src/tui/app/settings-picker.mjs +53 -8
  386. package/src/tui/app/shell-layout/row-budgets.mjs +2 -2
  387. package/src/tui/app/slash-commands.mjs +1 -1
  388. package/src/tui/app/slash-dispatch/route-commands.mjs +1 -1
  389. package/src/tui/app/slash-dispatch/session-commands.mjs +1 -1
  390. package/src/tui/app/use-prompt-queue-history.mjs +2 -2
  391. package/src/tui/app/use-transcript-activity.mjs +1 -1
  392. package/src/tui/app/use-transcript-window/geometry.mjs +1 -1
  393. package/src/tui/components/prompt-input/escape-policy.d.mts +0 -1
  394. package/src/tui/components/prompt-input/escape-policy.mjs +1 -1
  395. package/src/tui/components/question-panel-model.mjs +4 -4
  396. package/src/tui/components/tool-execution/standard-card.jsx +3 -4
  397. package/src/tui/components/user-message-skills.mjs +1 -1
  398. package/src/tui/dist/index.mjs +309 -30
  399. package/src/tui/markdown/render-ansi.mjs +1 -1
  400. package/src/tui/session/agent-job-feed/notification-router.mjs +10 -0
  401. package/src/tui/session/agent-job-feed.mjs +2 -0
  402. package/src/tui/session/context-state.mjs +3 -0
  403. package/src/tui/session/quota-resume.mjs +1 -1
  404. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  405. package/src/tui/session/session-api/lifecycle/session-reset.mjs +7 -0
  406. package/src/tui/session/session-api/routes-agent-definitions.mjs +5 -0
  407. package/src/tui/session/session-api/worktree.mjs +13 -1
  408. package/src/tui/session/session-flow/drain.mjs +11 -14
  409. package/src/tui/session/tool-card-results/grouped-result.mjs +2 -2
  410. package/src/tui/session/turn-quiet.mjs +1 -1
  411. package/src/tui/session/turn.mjs +3 -0
  412. package/src/tui/session/worktree-setup-notice.mjs +1 -1
  413. package/src/tui/spinner-meta.mjs +3 -3
  414. package/src/tui/spinner-verbs.mjs +2 -2
  415. package/src/defaults/skills/goal-management/SKILL.md +0 -169
  416. package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +0 -156
  417. package/src/session-runtime/goal-tasks.mjs +0 -1
@@ -1,12 +1,12 @@
1
1
  // manager/turn-checkpoint-journal.mjs
2
2
  // Bounded-cost, crash-safe turn checkpointing.
3
3
  //
4
- // PROBLEM. The turn checkpoint used to be a single full-snapshot file that was
5
- // rewritten on every flush (up to ~6/s per live session): the WHOLE growing
4
+ // PROBLEM. A single full-snapshot checkpoint file rewritten on every flush (up
5
+ // to ~6/s per live session) would re-sanitize and re-serialize the WHOLE growing
6
6
  // turn — every committed message, every buffered partial character, every
7
- // observed tool call — was re-sanitized and re-serialized on the engine thread.
8
- // Cost per flush grew with the turn (O(turn^2) over a long tool-heavy turn) and
9
- // at maximum agent fan-out every live session paid it on the SAME event loop.
7
+ // observed tool call — on the engine thread. Cost per flush would grow with the
8
+ // turn (O(turn^2) over a long tool-heavy turn) and at maximum agent fan-out
9
+ // every live session would pay it on the SAME event loop.
10
10
  //
11
11
  // DESIGN. Split the checkpoint into a write-once header plus an append-only
12
12
  // delta journal:
@@ -15,9 +15,11 @@
15
15
  // before the provider ever runs, and store-summary-reader
16
16
  // keeps probing this exact path to detect a live turn.
17
17
  // <session>.jsonl append-only JSONL deltas — NEW/CHANGED turn messages (by
18
- // turn-relative index), a one-time full replacement when
19
- // compaction removes the opening prompt, APPENDED partial
20
- // text/reasoning and small interruption-state transitions.
18
+ // index relative to the header's message list), APPENDED
19
+ // partial text/reasoning, small interruption-state
20
+ // transitions and the terminal seal. A history rewrite is
21
+ // never a delta: the recorder rewrites the header with
22
+ // the whole resulting context (turn-checkpoint.mjs).
21
23
  // A normal flush costs O(new bytes), never O(turn).
22
24
  // Appends ride a bounded coalescing lane (one in-flight write, one pending
23
25
  // buffer, synchronous drain above the byte ceiling) so the engine thread never
@@ -25,23 +27,32 @@
25
27
  //
26
28
  // Change detection is reference identity on message objects — the same contract
27
29
  // the session store's delta saves already rely on ("Replace, never mutate":
28
- // manager/prompt-utils.mjs, manager/session-lifecycle.mjs). A wholesale
29
- // transcript rewrite (image-strip splice, pre-send compaction) replaces the
30
- // refs, so it journals as changed indices plus a truncation record.
30
+ // manager/prompt-utils.mjs, manager/session-lifecycle.mjs). Messages the turn
31
+ // itself replaced journal as changed indices plus a truncation record.
31
32
  //
32
33
  // Reads replay header+journal into the EXACT object shape the full-snapshot
33
34
  // writer produced, so recovery/projection semantics are unchanged. A crash-torn
34
35
  // trailing line is discarded: replay stops at the first unparsable record and
35
36
  // keeps the durable prefix.
36
- import { appendFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
37
+ import { createHash } from 'node:crypto';
38
+ import {
39
+ appendFileSync,
40
+ existsSync,
41
+ mkdirSync,
42
+ readdirSync,
43
+ readFileSync,
44
+ renameSync,
45
+ unlinkSync,
46
+ writeFileSync,
47
+ } from 'node:fs';
37
48
  import { appendFile, unlink, writeFile } from 'node:fs/promises';
38
49
  import { join } from 'node:path';
39
50
  import { getPluginData } from '../../config.mjs';
40
51
  import { writeFileAtomicSync } from '../../../../shared/atomic-file.mjs';
52
+ import { withFileLockSync } from '../../../../shared/file-lock.mjs';
41
53
  import { sanitizeContentForStoredHistory } from '../../providers/media-normalization.mjs';
42
54
  import { redactSettledText, sensitiveValuesVersion } from '../sensitive-values.mjs';
43
55
  import { redactSensitiveContent, redactSensitiveMessage, redactSensitiveToolCall } from '../sensitive-projection.mjs';
44
- import { promptContentText } from './prompt-utils.mjs';
45
56
 
46
57
  const TURN_CHECKPOINT_VERSION = 1;
47
58
  const SESSION_ID = /^[A-Za-z0-9_-]+$/;
@@ -98,30 +109,6 @@ export function checkpointMessage(message) {
98
109
  return content === message.content ? message : { ...message, content };
99
110
  }
100
111
 
101
- function matchingUserMessage(message, currentUserContent) {
102
- return (
103
- message?.role === 'user' &&
104
- (message.content === currentUserContent ||
105
- promptContentText(message.content) === promptContentText(currentUserContent))
106
- );
107
- }
108
-
109
- /** Index of the user message that opened this turn, or -1. */
110
- export function findTurnStart(messages, currentUserContent) {
111
- const source = Array.isArray(messages) ? messages : [];
112
- for (let index = source.length - 1; index >= 0; index -= 1) {
113
- if (matchingUserMessage(source[index], currentUserContent)) return index;
114
- }
115
- return -1;
116
- }
117
-
118
- export function turnMessagesForCheckpoint(messages, currentUserContent) {
119
- const source = Array.isArray(messages) ? messages : [];
120
- const start = findTurnStart(source, currentUserContent);
121
- const turn = start >= 0 ? source.slice(start) : [];
122
- return turn.map(checkpointMessage);
123
- }
124
-
125
112
  export function emptyInterruptionSnapshot() {
126
113
  return {
127
114
  responseStarted: false,
@@ -168,6 +155,10 @@ function redactInterruptionSnapshot(sessionId, snapshot) {
168
155
 
169
156
  export function writeCheckpointHeader(checkpoint) {
170
157
  const { sessionId } = checkpoint;
158
+ // A half-moved checkpoint is finished before anything replaces its files.
159
+ if (finishPendingSetAside(sessionId).pending) {
160
+ throw new Error(`[turn-checkpoint] session ${sessionId}: an unrecovered checkpoint is still being set aside`);
161
+ }
171
162
  const target = turnCheckpointPath(sessionId);
172
163
  const payload = {
173
164
  ...checkpoint,
@@ -196,8 +187,11 @@ export function readTurnCheckpointHeader(sessionId) {
196
187
  }
197
188
 
198
189
  /** `present` is false only when the header file does not exist (ENOENT);
199
- * an unreadable or invalid header is present with a null `header`. */
190
+ * an unreadable or invalid header is present with a null `header`, and so
191
+ * are the files of an unfinished set-aside (`settingAside`), which are never
192
+ * read as a checkpoint. */
200
193
  export function readTurnCheckpointHeaderState(sessionId) {
194
+ if (finishPendingSetAside(sessionId).pending) return { present: true, header: null, settingAside: true };
201
195
  let text;
202
196
  try {
203
197
  text = readFileSync(turnCheckpointReadPath(sessionId), 'utf8');
@@ -207,6 +201,164 @@ export function readTurnCheckpointHeaderState(sessionId) {
207
201
  return { present: true, header: parseTurnCheckpointHeader(text, sessionId) };
208
202
  }
209
203
 
204
+ // ── Unrecovered checkpoints, set aside ──────────────────────────────────────
205
+ // A checkpoint recovery cannot apply (it names no base revision, or the record
206
+ // is not of its base) must never be overwritten by the session's next turn.
207
+ // It is renamed, header and journal as they are, to
208
+ // `<session>.unrecovered-<key>.json(l)`, `key` a digest of the header bytes.
209
+ //
210
+ // The move is several filesystem steps, so it is made one recoverable unit by
211
+ // an intent record, `<session>.setaside` ({ key }), written durably FIRST and
212
+ // removed LAST:
213
+ // 1. intent 2. journal → <stem>.jsonl 3. header → <stem>.json 4. drop intent
214
+ // While the intent exists the primary files are never read as a checkpoint,
215
+ // cleared, or written over (readTurnCheckpointHeaderState, clearTurnCheckpoint
216
+ // in turn-checkpoint.mjs, writeCheckpointHeader): every access first finishes
217
+ // the move, and when that is still refused it reports the set-aside as
218
+ // pending instead. A crash or a refused rename after any step therefore
219
+ // resolves only to finishing it. A moved journal is never overwritten: a
220
+ // primary journal found once `<stem>.jsonl` exists is not this checkpoint's.
221
+ const UNRECOVERED = '.unrecovered-';
222
+ const SET_ASIDE_KEY = /^[0-9a-f]{16}$/;
223
+
224
+ // Test seam: called with the step name ('intent' | 'journal' | 'header' |
225
+ // 'done') before each step; throwing simulates a crash or a refused rename there.
226
+ let _setAsideFault = null;
227
+ export function _setSetAsideFaultForTest(fault) {
228
+ _setAsideFault = typeof fault === 'function' ? fault : null;
229
+ }
230
+
231
+ function setAsideIntentPath(sessionId) {
232
+ return checkpointFilePath(sessionId, '.setaside', { ensureDir: false });
233
+ }
234
+
235
+ function unrecoveredStem(sessionId, key) {
236
+ return join(checkpointDirPath(), `${sessionId}${UNRECOVERED}${key}`);
237
+ }
238
+
239
+ const headerKey = (text) => createHash('sha256').update(text).digest('hex').slice(0, 16);
240
+
241
+ function readFileOrNull(path) {
242
+ try {
243
+ return readFileSync(path, 'utf8');
244
+ } catch (error) {
245
+ if (error?.code === 'ENOENT') return null;
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ /** The key of the set-aside in progress for `sessionId`, or null. */
251
+ function pendingSetAsideKey(sessionId) {
252
+ const text = readFileOrNull(setAsideIntentPath(sessionId));
253
+ if (text === null) return null;
254
+ try {
255
+ const key = JSON.parse(text)?.key;
256
+ if (typeof key === 'string' && SET_ASIDE_KEY.test(key)) return key;
257
+ } catch {
258
+ /* an intent is written atomically; fall back to the header's own key */
259
+ }
260
+ const header = readFileOrNull(turnCheckpointReadPath(sessionId));
261
+ return header === null ? '' : headerKey(header);
262
+ }
263
+
264
+ function renameIfPresent(from, to) {
265
+ try {
266
+ renameSync(from, to);
267
+ } catch (error) {
268
+ if (error?.code !== 'ENOENT') throw error;
269
+ }
270
+ }
271
+
272
+ /** Steps 2-4 for `key` ('' when no primary header is left to name it). */
273
+ function completeSetAside(sessionId, key) {
274
+ if (key) {
275
+ const stem = unrecoveredStem(sessionId, key);
276
+ _setAsideFault?.('journal');
277
+ if (!existsSync(`${stem}.jsonl`)) renameIfPresent(turnJournalReadPath(sessionId), `${stem}.jsonl`);
278
+ _setAsideFault?.('header');
279
+ renameIfPresent(turnCheckpointReadPath(sessionId), `${stem}.json`);
280
+ }
281
+ _setAsideFault?.('done');
282
+ try {
283
+ unlinkSync(setAsideIntentPath(sessionId));
284
+ } catch (error) {
285
+ if (error?.code !== 'ENOENT') throw error;
286
+ }
287
+ }
288
+
289
+ /** Finish an interrupted set-aside of `sessionId`. Returns `{ pending }`:
290
+ * true when one is still unfinished (its files must not be touched);
291
+ * `key` names the one it finished. */
292
+ export function finishPendingSetAside(sessionId) {
293
+ let key;
294
+ try {
295
+ key = pendingSetAsideKey(sessionId);
296
+ if (key === null) return { pending: false, key: null };
297
+ completeSetAside(sessionId, key);
298
+ return { pending: false, key: key || null };
299
+ } catch {
300
+ return { pending: true, key: key || null };
301
+ }
302
+ }
303
+
304
+ /** Move `sessionId`'s current checkpoint aside (finishing an interrupted move
305
+ * first). Returns the key of what it moved, or null when there was nothing.
306
+ * Throws when the files cannot be moved: the caller must not then write a
307
+ * new checkpoint, and every reader keeps treating them as being set aside. */
308
+ export function setAsideTurnCheckpoint(sessionId) {
309
+ const pending = pendingSetAsideKey(sessionId);
310
+ if (pending !== null) completeSetAside(sessionId, pending);
311
+ const text = readFileOrNull(turnCheckpointReadPath(sessionId));
312
+ if (text === null) return pending || null;
313
+ const key = headerKey(text);
314
+ checkpointDir();
315
+ _setAsideFault?.('intent');
316
+ writeFileAtomicSync(setAsideIntentPath(sessionId), JSON.stringify({ key }));
317
+ completeSetAside(sessionId, key);
318
+ return key;
319
+ }
320
+
321
+ /** `[{ key, checkpoint, pending? }]` for every checkpoint of `sessionId` set
322
+ * aside; `checkpoint` is null when its header is unreadable or its move is
323
+ * still unfinished (`pending`). */
324
+ export function listUnrecoveredTurnCheckpoints(sessionId) {
325
+ const prefix = `${assertSessionId(sessionId)}${UNRECOVERED}`;
326
+ const unfinished = finishPendingSetAside(sessionId);
327
+ let names;
328
+ try {
329
+ names = readdirSync(checkpointDirPath());
330
+ } catch {
331
+ names = [];
332
+ }
333
+ const found = unfinished.pending ? [{ key: unfinished.key, checkpoint: null, pending: true }] : [];
334
+ for (const name of names) {
335
+ if (!name.startsWith(prefix) || !name.endsWith('.json')) continue;
336
+ if (unfinished.pending && name === `${prefix}${unfinished.key}.json`) continue;
337
+ const stem = join(checkpointDirPath(), name.slice(0, -'.json'.length));
338
+ let checkpoint = null;
339
+ try {
340
+ const header = parseTurnCheckpointHeader(readFileSync(`${stem}.json`, 'utf8'), sessionId);
341
+ checkpoint = header ? replayTurnJournal(header, `${stem}.jsonl`) : null;
342
+ } catch {
343
+ checkpoint = null;
344
+ }
345
+ found.push({ key: name.slice(prefix.length, -'.json'.length), checkpoint });
346
+ }
347
+ return found;
348
+ }
349
+
350
+ /** Delete one set-aside checkpoint (only on evidence that disk covers it). */
351
+ export function removeUnrecoveredTurnCheckpoint(sessionId, key) {
352
+ const stem = unrecoveredStem(assertSessionId(sessionId), key);
353
+ for (const path of [`${stem}.json`, `${stem}.jsonl`]) {
354
+ try {
355
+ unlinkSync(path);
356
+ } catch (error) {
357
+ if (error?.code !== 'ENOENT') throw error;
358
+ }
359
+ }
360
+ }
361
+
210
362
  function parseTurnCheckpointHeader(text, sessionId) {
211
363
  try {
212
364
  const value = JSON.parse(text);
@@ -293,11 +445,14 @@ function _drainJournalOpsSync(sessionId, entry) {
293
445
  const ops = entry.ops;
294
446
  entry.ops = [];
295
447
  entry.bytes = 0;
296
- for (const op of ops) {
448
+ for (const [index, op] of ops.entries()) {
297
449
  try {
298
450
  _runJournalOpSync(op);
451
+ op.settle?.(true);
299
452
  } catch (error) {
300
453
  _warnJournalOnce(sessionId, error);
454
+ // The rest of the queue is dropped: a removal among it did not happen.
455
+ for (const dropped of ops.slice(index)) dropped.settle?.(false);
301
456
  break;
302
457
  }
303
458
  }
@@ -323,9 +478,13 @@ function _pumpJournal(sessionId) {
323
478
  entry.writing = true;
324
479
  entry.inFlight = op;
325
480
  _runJournalOpAsync(op)
326
- .catch((error) => {
327
- _warnJournalOnce(sessionId, error);
328
- })
481
+ .then(
482
+ () => op.settle?.(true),
483
+ (error) => {
484
+ _warnJournalOnce(sessionId, error);
485
+ op.settle?.(false);
486
+ }
487
+ )
329
488
  .finally(() => {
330
489
  entry.writing = false;
331
490
  entry.inFlight = null;
@@ -361,13 +520,21 @@ export function cancelJournalWrites(sessionId) {
361
520
  _releaseIdleEntry(sessionId, entry);
362
521
  }
363
522
 
364
- export function appendJournalLines(sessionId, lines) {
523
+ /** `immediate`: written on the calling thread when nothing is queued or in
524
+ * flight (the terminal seal), otherwise queued in order like any append. */
525
+ export function appendJournalLines(sessionId, lines, { immediate = false } = {}) {
365
526
  if (!Array.isArray(lines) || lines.length === 0) return false;
366
- _enqueueJournalOp(sessionId, {
367
- kind: 'append',
368
- path: turnJournalPath(sessionId),
369
- data: lines.join(''),
370
- });
527
+ const op = { kind: 'append', path: turnJournalPath(sessionId), data: lines.join('') };
528
+ if (immediate && !_journalWrites.has(sessionId)) {
529
+ try {
530
+ _runJournalOpSync(op);
531
+ return true;
532
+ } catch (error) {
533
+ _warnJournalOnce(sessionId, error);
534
+ return false;
535
+ }
536
+ }
537
+ _enqueueJournalOp(sessionId, op);
371
538
  return true;
372
539
  }
373
540
 
@@ -398,10 +565,102 @@ export function openJournalForTurn(sessionId, openLines) {
398
565
  * the file behind it) and before any later turn's open, which the lane fences
399
566
  * behind it. A crash before it lands leaves a journal whose header is already
400
567
  * gone, which replay never reads; process exit re-runs it synchronously.
568
+ * Resolves true once the file is gone, false when the removal failed (or was
569
+ * dropped behind a failed older write) — it never rejects.
401
570
  */
402
571
  export function removeTurnJournal(sessionId) {
403
572
  cancelJournalWrites(sessionId);
404
- _enqueueJournalOp(sessionId, { kind: 'remove', path: turnJournalReadPath(sessionId) });
573
+ return new Promise((settle) => {
574
+ _enqueueJournalOp(sessionId, { kind: 'remove', path: turnJournalReadPath(sessionId), settle });
575
+ });
576
+ }
577
+
578
+ // ── Header publish + journal open vs. the orphan sweep ──────────────────────
579
+ // A journal is only read through its header, so one without a header is
580
+ // removable. The sweep checks "no header" and then unlinks; a turn in another
581
+ // process publishing its header and opening its journal in between would
582
+ // lose the opening record, and its later appends would never replay. Both are
583
+ // therefore done under one per-session cross-process lock (file-lock.mjs).
584
+ const CHECKPOINT_LOCK_WAIT_MS = 200;
585
+
586
+ export function checkpointLockPath(sessionId) {
587
+ return join(checkpointDir(), `${assertSessionId(sessionId)}.lock`);
588
+ }
589
+
590
+ /** Run `fn` (a header publish plus journal open) under the session's
591
+ * checkpoint lock. A turn never fails or stalls on the lock: it waits at most
592
+ * CHECKPOINT_LOCK_WAIT_MS (holders keep it for one existence check and one
593
+ * unlink), and when the lock cannot be taken at all `fn` runs without it —
594
+ * its own errors still propagate. */
595
+ export function withCheckpointOpenLock(sessionId, fn) {
596
+ let ran = false;
597
+ try {
598
+ return withFileLockSync(
599
+ checkpointLockPath(sessionId),
600
+ () => {
601
+ ran = true;
602
+ return fn();
603
+ },
604
+ { timeoutMs: CHECKPOINT_LOCK_WAIT_MS }
605
+ );
606
+ } catch (error) {
607
+ if (ran) throw error;
608
+ return fn();
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Remove `sessionId`'s journal when no header stands behind it. A journal is
614
+ * only ever read through its header, so one without it can never be
615
+ * recovered; it is a removal that failed once (or was cut short by a restart)
616
+ * and still holds whatever it held. The header check and the unlink run under
617
+ * the session's checkpoint lock, so a turn cannot publish its header and open
618
+ * its journal between them (withCheckpointOpenLock). A busy lock skips the
619
+ * session instead of waiting, and so does a write lane still active for it
620
+ * (that process is removing or recording it itself). A header being set aside
621
+ * counts as present (readTurnCheckpointHeaderState). True when it removed one.
622
+ */
623
+ export function removeOrphanTurnJournal(sessionId) {
624
+ const journalPath = turnJournalReadPath(sessionId);
625
+ if (!existsSync(journalPath)) return false;
626
+ try {
627
+ return withFileLockSync(
628
+ checkpointLockPath(sessionId),
629
+ () => {
630
+ if (readTurnCheckpointHeaderState(sessionId).present) return false;
631
+ if (_journalWrites.has(sessionId)) return false;
632
+ try {
633
+ unlinkSync(journalPath);
634
+ } catch (error) {
635
+ if (error?.code === 'ENOENT') return false;
636
+ throw error;
637
+ }
638
+ return true;
639
+ },
640
+ { timeoutMs: 0 }
641
+ );
642
+ } catch {
643
+ // Contended or unremovable: the next recovery or start tries again.
644
+ return false;
645
+ }
646
+ }
647
+
648
+ /** removeOrphanTurnJournal for every primary journal in the checkpoint
649
+ * directory (set-aside journals, `<id>.unrecovered-<key>.jsonl`, are not
650
+ * primary). Returns how many were removed. */
651
+ export function removeOrphanTurnJournals() {
652
+ let names;
653
+ try {
654
+ names = readdirSync(checkpointDirPath());
655
+ } catch {
656
+ return 0;
657
+ }
658
+ let removed = 0;
659
+ for (const name of names) {
660
+ const match = /^([A-Za-z0-9_-]+)\.jsonl$/.exec(name);
661
+ if (match && removeOrphanTurnJournal(match[1])) removed += 1;
662
+ }
663
+ return removed;
405
664
  }
406
665
 
407
666
  /** Test/shutdown helper: resolve once the write lane for a session is idle. */
@@ -492,15 +751,16 @@ function applyInterruptionDelta(state, delta) {
492
751
  /** Merge the append-only journal onto its header. Returns the header unchanged
493
752
  * when no journal exists, when the journal belongs to another turn, or when it
494
753
  * has no usable head record. */
495
- export function replayTurnJournal(header) {
754
+ export function replayTurnJournal(header, journalPath = header ? turnJournalReadPath(header.sessionId) : null) {
496
755
  if (!header) return null;
497
756
  let raw;
498
757
  try {
499
- raw = readFileSync(turnJournalReadPath(header.sessionId), 'utf8');
758
+ raw = readFileSync(journalPath, 'utf8');
500
759
  } catch {
501
760
  return header;
502
761
  }
503
762
  let messages = header.turnMessages.slice();
763
+ let sealed = header.sealed === true;
504
764
  let fullTranscript = header.fullTranscript === true;
505
765
  let contextState = header.contextState ?? null;
506
766
  const state = interruptionStateFromSnapshot(header.interruption);
@@ -545,7 +805,11 @@ export function replayTurnJournal(header) {
545
805
  if (Number.isInteger(index) && index >= 0 && index <= messages.length) {
546
806
  messages[index] = record.m;
547
807
  }
808
+ } else if (record.t === 'seal') {
809
+ sealed = true;
548
810
  } else if (record.t === 'full' && Array.isArray(record.ms)) {
811
+ // Legacy journals (before checkpoints named their base) replaced the
812
+ // whole context this way; read them so they are not misread as a turn.
549
813
  messages = record.ms.slice();
550
814
  fullTranscript = true;
551
815
  } else if (record.t === 'len') {
@@ -565,6 +829,7 @@ export function replayTurnJournal(header) {
565
829
  return {
566
830
  ...header,
567
831
  turnMessages: messages,
832
+ ...(sealed ? { sealed: true } : {}),
568
833
  ...(fullTranscript ? { fullTranscript: true } : {}),
569
834
  interruption: interruptionSnapshotFromState(state),
570
835
  contextState,
@@ -573,13 +838,14 @@ export function replayTurnJournal(header) {
573
838
  }
574
839
 
575
840
  /**
576
- * Per-turn delta encoder. Holds the message refs already journaled (by
577
- * turn-relative index) plus an opaque interruption cursor, so each encode()
578
- * touches only what changed since the previous flush.
841
+ * Per-turn delta encoder. Holds the message refs already journaled (by index
842
+ * relative to the header's first message, `offset` in the working transcript)
843
+ * plus an opaque interruption cursor, so each encode() touches only what
844
+ * changed since the previous flush.
579
845
  */
580
846
  export function createTurnJournalEncoder({ turnToken, sessionId = null } = {}) {
581
847
  let journaled = [];
582
- let fullTranscript = false;
848
+ let offset = 0;
583
849
  let cursor = null;
584
850
  let contextStateKey = null;
585
851
  // Per-turn record sequence. It is assigned where records are produced, so
@@ -620,16 +886,15 @@ export function createTurnJournalEncoder({ turnToken, sessionId = null } = {}) {
620
886
  return out;
621
887
  };
622
888
  return {
623
- /** Seed from the turn's opening state; returns the header's messages
889
+ /** Seed from `turnOutgoing[offset..]`; returns the header's messages
624
890
  * and the journal's opening lines (head record, plus — defensively —
625
891
  * the delta that makes the journal authoritative if the tracker already
626
892
  * carried state at open time). `snapshot` is the tracker snapshot the
627
893
  * header stores; `epoch` names the journal a rewritten header starts. */
628
- seed(turnOutgoing, currentUserContent, interruption, contextState = null, { snapshot = null, epoch = 0 } = {}) {
894
+ seed(turnOutgoing, interruption, contextState = null, { offset: from = 0, snapshot = null, epoch = 0 } = {}) {
629
895
  const source = Array.isArray(turnOutgoing) ? turnOutgoing : [];
630
- const start = findTurnStart(source, currentUserContent);
631
- fullTranscript = start < 0;
632
- journaled = fullTranscript ? source.slice() : source.slice(start);
896
+ offset = from;
897
+ journaled = source.slice(offset);
633
898
  mirror = sensitiveValuesVersion(sessionId)
634
899
  ? {
635
900
  raw: interruptionStateFromSnapshot(snapshot),
@@ -646,40 +911,25 @@ export function createTurnJournalEncoder({ turnToken, sessionId = null } = {}) {
646
911
  openLines.push(line({ t: 'x', d: seededDelta, at: Date.now() }));
647
912
  return {
648
913
  turnMessages: journaled.map(checkpointMessage),
649
- fullTranscript,
650
914
  contextState,
651
915
  openLines,
652
916
  };
653
917
  },
654
- encode({ turnOutgoing, currentUserContent, interruption, contextState = null }) {
918
+ /** Lines for what changed since the previous flush; `sealed` adds the
919
+ * record that marks the turn finished. */
920
+ encode({ turnOutgoing, interruption, contextState = null, sealed = false }) {
655
921
  const source = Array.isArray(turnOutgoing) ? turnOutgoing : [];
656
- const start = findTurnStart(source, currentUserContent);
657
922
  const records = [];
658
- if (!fullTranscript && start < 0) {
659
- // Pre-send compaction may replace the whole history, including
660
- // the user message that defined our turn-relative origin. The
661
- // replacement is normally small; persist it once so reconnect
662
- // and crash recovery never splice an empty turn onto the stale
663
- // pre-compaction prefix.
664
- fullTranscript = true;
665
- journaled = source.slice();
666
- records.push({
667
- t: 'full',
668
- ms: journaled.map(stored),
669
- });
670
- } else {
671
- const offset = fullTranscript ? 0 : start;
672
- const length = offset >= 0 ? source.length - offset : 0;
673
- for (let index = 0; index < length; index += 1) {
674
- const raw = source[offset + index];
675
- if (journaled[index] === raw) continue;
676
- journaled[index] = raw;
677
- records.push({ t: 'm', i: index, m: stored(raw) });
678
- }
679
- if (journaled.length > length) {
680
- journaled.length = length;
681
- records.push({ t: 'len', n: length });
682
- }
923
+ const length = Math.max(0, source.length - offset);
924
+ for (let index = 0; index < length; index += 1) {
925
+ const raw = source[offset + index];
926
+ if (journaled[index] === raw) continue;
927
+ journaled[index] = raw;
928
+ records.push({ t: 'm', i: index, m: stored(raw) });
929
+ }
930
+ if (journaled.length > length) {
931
+ journaled.length = length;
932
+ records.push({ t: 'len', n: length });
683
933
  }
684
934
  if (typeof interruption?.journalDelta === 'function') {
685
935
  const next = interruption.journalDelta(cursor);
@@ -692,6 +942,7 @@ export function createTurnJournalEncoder({ turnToken, sessionId = null } = {}) {
692
942
  contextStateKey = nextContextStateKey;
693
943
  records.push({ t: 'c', c: contextState });
694
944
  }
945
+ if (sealed) records.push({ t: 'seal' });
695
946
  if (records.length === 0) return [];
696
947
  // One timestamp per batch keeps `updatedAt` fresh for the
697
948
  // newer-than-session recovery guard without a per-record cost.