mixdog 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (468) hide show
  1. package/README.md +8 -4
  2. package/package.json +8 -8
  3. package/scripts/native-release-status.mjs +238 -0
  4. package/scripts/verify-runtime-dependency-versions.mjs +158 -0
  5. package/src/agents/reviewer/AGENT.md +5 -7
  6. package/src/agents/reviewer/agent.json +1 -1
  7. package/src/defaults/shipped-history.json +9 -1
  8. package/src/defaults/skills/browser-use/SKILL.md +6 -3
  9. package/src/defaults/skills/local-provider/SKILL.md +33 -6
  10. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  11. package/src/defaults/skills/setup/SKILL.md +23 -1
  12. package/src/defaults/skills/setup/references/actions.md +50 -3
  13. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  14. package/src/defaults/skills/setup-guide/SKILL.md +7 -3
  15. package/src/defaults/skills/visualize/SKILL.md +9 -6
  16. package/src/defaults/skills/visualize/references/craft.md +8 -1
  17. package/src/help.mjs +1 -1
  18. package/src/lib/keychain-cjs.cjs +4 -1
  19. package/src/lib/rules-builder.cjs +14 -7
  20. package/src/rules/agent-mode/AGENT_MODE.md +8 -5
  21. package/src/rules/agent-mode/MIXY.md +6 -8
  22. package/src/rules/lead/LEAD.md +7 -10
  23. package/src/rules/shared/10-tool-workflow.md +4 -0
  24. package/src/rules/shared/20-research.md +2 -0
  25. package/src/rules/shared/45-destructive.md +5 -1
  26. package/src/rules/shared/60-verification.md +6 -4
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  30. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  31. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  32. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  33. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  35. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  36. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  37. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  38. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +9 -2
  39. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -5
  40. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  41. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  42. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +84 -32
  43. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
  44. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
  45. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +84 -42
  48. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  50. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  51. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +27 -8
  52. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  54. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  55. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
  56. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  57. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  59. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +16 -0
  60. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  62. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  64. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  65. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +2 -1
  68. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  69. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  70. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  72. package/src/runtime/agent/orchestrator/providers/registry.mjs +19 -0
  73. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
  74. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  75. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
  78. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  79. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
  81. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -6
  82. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  83. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +52 -6
  85. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +35 -13
  86. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +7 -6
  87. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  89. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +8 -3
  90. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  92. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  93. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  94. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -2
  95. package/src/runtime/agent/orchestrator/session/agent-room.mjs +196 -45
  96. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  97. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  98. package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
  99. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +45 -19
  100. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
  102. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +185 -37
  103. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +6 -2
  107. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +4 -1
  108. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  110. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +240 -0
  111. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +102 -35
  112. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  113. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +19 -9
  114. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  115. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  116. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  117. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  118. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +199 -109
  119. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
  120. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
  123. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
  124. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  125. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +5 -5
  126. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -0
  127. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  128. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +33 -8
  129. package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
  130. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +27 -0
  131. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  132. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  134. package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
  135. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
  136. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
  137. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  138. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +27 -14
  139. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +14 -0
  140. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  141. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
  142. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  143. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
  144. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +115 -0
  145. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  146. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
  147. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  148. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  149. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  150. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
  151. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  152. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  153. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  154. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
  155. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +55 -27
  156. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +4 -9
  157. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +14 -1
  158. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
  159. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  160. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  161. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  162. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +52 -11
  163. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  164. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +18 -3
  165. package/src/runtime/agent/orchestrator/session/routine-result.mjs +126 -12
  166. package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
  167. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +16 -3
  168. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  169. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  170. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  171. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  172. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  173. package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
  174. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
  175. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
  176. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +15 -5
  177. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +49 -1
  178. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +80 -1
  179. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  180. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  181. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
  182. package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
  183. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
  184. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  185. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  186. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  187. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  188. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +35 -5
  189. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +42 -2
  190. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +47 -14
  191. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  192. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +6 -3
  193. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  194. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  195. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  196. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  197. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
  198. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
  199. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +10 -12
  200. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  201. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  202. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  203. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  204. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  205. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +42 -23
  206. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  207. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  208. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  209. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  210. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  211. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  212. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  213. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  214. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  215. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  216. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  217. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  218. package/src/runtime/attachments/legacy-office.mjs +110 -0
  219. package/src/runtime/attachments/limits.mjs +5 -1
  220. package/src/runtime/attachments/office-extract.mjs +3 -6
  221. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  222. package/src/runtime/attachments/prompt-restore.mjs +70 -0
  223. package/src/runtime/attachments/store.mjs +109 -33
  224. package/src/runtime/browser-bridge/client.mjs +73 -20
  225. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  226. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  227. package/src/runtime/channels/lib/state-file.mjs +15 -15
  228. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  229. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  230. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  231. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  232. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  233. package/src/runtime/effort-judge/judge-client.mjs +66 -16
  234. package/src/runtime/effort-judge/model-install.mjs +47 -7
  235. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  236. package/src/runtime/local-provider/catalog.mjs +5 -2
  237. package/src/runtime/local-provider/registered-models.mjs +64 -27
  238. package/src/runtime/local-provider/server.mjs +2 -3
  239. package/src/runtime/media/adapters/image-api.mjs +89 -0
  240. package/src/runtime/media/auth.mjs +64 -1
  241. package/src/runtime/media/catalog.mjs +66 -4
  242. package/src/runtime/media/jobs.mjs +4 -0
  243. package/src/runtime/media/lanes.mjs +46 -5
  244. package/src/runtime/media/media-usage.mjs +17 -0
  245. package/src/runtime/media/renditions.mjs +15 -15
  246. package/src/runtime/media/store.mjs +36 -35
  247. package/src/runtime/media/tool.mjs +8 -1
  248. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  249. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  250. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  251. package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
  252. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  253. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  254. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  255. package/src/runtime/office/core/journal.mjs +7 -14
  256. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  257. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  258. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  259. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  260. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  261. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  262. package/src/runtime/office/portable/portable-soffice.mjs +135 -29
  263. package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
  264. package/src/runtime/shared/agent-avatar.mjs +3 -2
  265. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  266. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  267. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  268. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  269. package/src/runtime/shared/agent-room-host.d.mts +31 -0
  270. package/src/runtime/shared/agent-room-host.mjs +58 -0
  271. package/src/runtime/shared/agent-store.mjs +24 -7
  272. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  273. package/src/runtime/shared/atomic-file.d.mts +50 -0
  274. package/src/runtime/shared/atomic-file.mjs +394 -132
  275. package/src/runtime/shared/background-tasks.mjs +7 -4
  276. package/src/runtime/shared/bot-hops.mjs +35 -0
  277. package/src/runtime/shared/bot-split-migration.mjs +1 -1
  278. package/src/runtime/shared/bounded-download.mjs +4 -2
  279. package/src/runtime/shared/config.mjs +39 -9
  280. package/src/runtime/shared/defaults-separation.mjs +6 -2
  281. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  282. package/src/runtime/shared/err-text.mjs +4 -0
  283. package/src/runtime/shared/error-presentation.mjs +22 -4
  284. package/src/runtime/shared/file-has-data.mjs +28 -0
  285. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  286. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  287. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  288. package/src/runtime/shared/projects.mjs +50 -41
  289. package/src/runtime/shared/provider-accounts.mjs +17 -12
  290. package/src/runtime/shared/redact-directories.mjs +18 -0
  291. package/src/runtime/shared/remove-directory.mjs +28 -0
  292. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  293. package/src/runtime/shared/routine-trigger.mjs +14 -6
  294. package/src/runtime/shared/run-child.d.mts +3 -0
  295. package/src/runtime/shared/run-child.mjs +24 -3
  296. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  297. package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
  298. package/src/runtime/shared/session-sidebar-host.mjs +16 -3
  299. package/src/runtime/shared/singleton-owner.mjs +7 -6
  300. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  301. package/src/runtime/shared/staged-update.mjs +4 -2
  302. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  303. package/src/runtime/shared/tool-execution-contract.mjs +10 -0
  304. package/src/runtime/shared/tool-work-units.mjs +12 -9
  305. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  306. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  307. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  308. package/src/runtime/shared/turn-snapshot.mjs +277 -124
  309. package/src/runtime/shared/user-cwd.mjs +9 -4
  310. package/src/runtime/shared/user-data-guard.mjs +92 -61
  311. package/src/runtime/shared/utf8-cut.mjs +10 -0
  312. package/src/runtime/shared/worktrees/activity-gate.mjs +251 -61
  313. package/src/runtime/shared/worktrees/contract.mjs +11 -3
  314. package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
  315. package/src/runtime/shared/worktrees/manager.mjs +419 -52
  316. package/src/runtime/shared/worktrees/registry.mjs +29 -27
  317. package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
  318. package/src/runtime/tidy/core-install.mjs +26 -4
  319. package/src/runtime/tidy/install.mjs +91 -23
  320. package/src/runtime/tidy/process.mjs +41 -8
  321. package/src/runtime/tidy/resolve.mjs +10 -7
  322. package/src/runtime/web-search/lib/config.mjs +4 -16
  323. package/src/session-runtime/agent-home-delivery.mjs +542 -204
  324. package/src/session-runtime/agent-profile-changes.mjs +16 -0
  325. package/src/session-runtime/agent-room-delivery.mjs +81 -35
  326. package/src/session-runtime/agent-task-sessions.mjs +811 -179
  327. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  328. package/src/session-runtime/boot/apis.mjs +48 -24
  329. package/src/session-runtime/boot/config.mjs +1 -0
  330. package/src/session-runtime/boot/facade.mjs +16 -7
  331. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  332. package/src/session-runtime/boot/providers.mjs +2 -2
  333. package/src/session-runtime/boot/shared.mjs +12 -6
  334. package/src/session-runtime/boot/tools.mjs +23 -3
  335. package/src/session-runtime/bot-status.mjs +432 -0
  336. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  337. package/src/session-runtime/config-lifecycle.mjs +60 -55
  338. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  339. package/src/session-runtime/held-requests.mjs +31 -33
  340. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  341. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  342. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +24 -26
  343. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  344. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  345. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  346. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  347. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  348. package/src/session-runtime/media-api.mjs +5 -2
  349. package/src/session-runtime/model-recency.mjs +1 -0
  350. package/src/session-runtime/model-route/route-tuning.mjs +4 -1
  351. package/src/session-runtime/model-route/set-route.mjs +61 -1
  352. package/src/session-runtime/owned-automation-run.mjs +8 -1
  353. package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
  354. package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
  355. package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
  356. package/src/session-runtime/route-resolution.mjs +16 -8
  357. package/src/session-runtime/route-state.mjs +1 -59
  358. package/src/session-runtime/routine-home-turn.mjs +5 -3
  359. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  360. package/src/session-runtime/runtime-paths.mjs +11 -5
  361. package/src/session-runtime/runtime-review-api.mjs +4 -1
  362. package/src/session-runtime/runtime-tunables.mjs +6 -0
  363. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  364. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  365. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  366. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  367. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +34 -19
  368. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  369. package/src/session-runtime/services/agent-tool/send-flow.mjs +25 -21
  370. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +7 -0
  371. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  372. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +8 -5
  373. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  374. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  375. package/src/session-runtime/services/agent-tool.mjs +6 -0
  376. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  377. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  378. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  379. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  380. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  381. package/src/session-runtime/services/provider-admin.mjs +26 -6
  382. package/src/session-runtime/services/schedule-access.mjs +33 -4
  383. package/src/session-runtime/services/seeds.mjs +4 -3
  384. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  385. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  386. package/src/session-runtime/services/worktree-access.mjs +27 -0
  387. package/src/session-runtime/session-create/materialize.mjs +2 -3
  388. package/src/session-runtime/session-create/session-options.mjs +15 -2
  389. package/src/session-runtime/session-hooks.mjs +9 -17
  390. package/src/session-runtime/settings-builtin-tools-api.mjs +65 -1
  391. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  392. package/src/session-runtime/setup-tool/executor.mjs +12 -2
  393. package/src/session-runtime/setup-tool/settings-contract.mjs +34 -1
  394. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  395. package/src/session-runtime/skills-api.mjs +12 -5
  396. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +349 -0
  397. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  398. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  399. package/src/session-runtime/tool-policy/policy-stage.mjs +183 -0
  400. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  401. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  402. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  403. package/src/session-runtime/tool-policy-refresh.mjs +48 -369
  404. package/src/session-runtime/tool-surface.mjs +10 -4
  405. package/src/session-runtime/turn/session-ops.mjs +8 -0
  406. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +95 -88
  407. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  408. package/src/session-runtime/turn/turn-run.mjs +7 -2
  409. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  410. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +527 -333
  411. package/src/session-runtime/workflow-agents-api/agent-route.mjs +85 -63
  412. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  413. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  414. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  415. package/src/session-runtime/workspace/transition-steps.mjs +1 -18
  416. package/src/session-runtime/workspace/transitions.mjs +279 -67
  417. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  418. package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
  419. package/src/standalone/daemon-agent-control.mjs +4 -1
  420. package/src/standalone/daemon-lag-profiler.mjs +3 -1
  421. package/src/standalone/daemon-session-control.mjs +173 -15
  422. package/src/standalone/daemon-telemetry.mjs +9 -0
  423. package/src/standalone/daemon.mjs +33 -10
  424. package/src/standalone/local-session-runtime.mjs +33 -16
  425. package/src/standalone/session-client.mjs +7 -2
  426. package/src/standalone/session-protocol.mjs +8 -0
  427. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
  428. package/src/standalone/session-service/agent-route-follow.mjs +69 -0
  429. package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
  430. package/src/standalone/session-service.mjs +21 -0
  431. package/src/standalone/session-state-projection.mjs +19 -9
  432. package/src/standalone/session-tool-output.mjs +36 -9
  433. package/src/standalone/staged-import.mjs +47 -0
  434. package/src/tui/App.jsx +2 -0
  435. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  436. package/src/tui/app/message-selector.mjs +15 -2
  437. package/src/tui/app/model-options.mjs +16 -2
  438. package/src/tui/app/model-picker.mjs +2 -1
  439. package/src/tui/app/route-pickers.mjs +3 -1
  440. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  441. package/src/tui/app/slash-commands.mjs +1 -1
  442. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  443. package/src/tui/dist/index.mjs +617 -369
  444. package/src/tui/paste-attachments.mjs +0 -1
  445. package/src/tui/paste-text-policy.mjs +4 -5
  446. package/src/tui/prompt-history-store.mjs +16 -21
  447. package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
  448. package/src/tui/session/context-state.mjs +4 -0
  449. package/src/tui/session/notice-surface.mjs +3 -0
  450. package/src/tui/session/session-api/intake/abort.mjs +52 -18
  451. package/src/tui/session/session-api/intake/rewind.mjs +39 -4
  452. package/src/tui/session/session-api/media.mjs +2 -1
  453. package/src/tui/session/session-api/routes-agent-definitions.mjs +76 -0
  454. package/src/tui/session/session-api/routes-write.mjs +203 -0
  455. package/src/tui/session/session-api/routes.mjs +23 -247
  456. package/src/tui/session/session-api/settings.mjs +14 -1
  457. package/src/tui/session/session-flow/drain.mjs +28 -3
  458. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  459. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  460. package/src/tui/session/session-flow/steering.mjs +8 -3
  461. package/src/tui/session/tool-approval.mjs +142 -14
  462. package/src/tui/session/transcript-intake.mjs +2 -0
  463. package/src/tui/session/turn.mjs +23 -14
  464. package/src/tui/transcript-tool-failures.mjs +8 -2
  465. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  466. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  467. package/src/rules/agent-mode/ROUTINE.md +0 -6
  468. package/src/session-runtime/room-member-gate.mjs +0 -48
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join, dirname, resolve } from 'node:path';
3
3
  import { explicitSessionCwd } from '../../shared/user-cwd.mjs';
4
+ import { holdsNoData } from '../../shared/atomic-file.mjs';
4
5
 
5
6
  /** @type {Map<string, string|null>} */
6
7
  const cache = new Map();
@@ -51,7 +52,10 @@ export function resolveProjectId(cwd) {
51
52
  const idFile = join(mixdogRoot, '.mixdog', 'project.id');
52
53
  let content;
53
54
  try {
54
- content = readFileSync(idFile, 'utf8').trim();
55
+ const raw = readFileSync(idFile, 'utf8');
56
+ // A crash can leave the marker NUL-filled; trim() keeps NULs, so it would
57
+ // otherwise become the project id. No data means no marker (COMMON).
58
+ content = holdsNoData(raw) ? '' : raw.trim();
55
59
  } catch {
56
60
  // TOCTOU: the file vanished between findProjectIdRoot's existsSync and
57
61
  // this read. Treat as no marker; do not cache so a transient miss can
@@ -34,6 +34,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'nod
34
34
  import { fileURLToPath } from 'node:url';
35
35
  import { runChild } from './pg/run-child.mjs';
36
36
  import { renameWithRetrySync, writeFileAtomicSync, writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
37
+ import { fileHasData } from '../../shared/file-has-data.mjs';
37
38
  import { downloadToFileWithRetry } from '../../shared/bounded-download.mjs';
38
39
  import { platformKey, platformKeyCandidates, verifySha256File } from '../../shared/native-asset.mjs';
39
40
 
@@ -497,8 +498,12 @@ function cachedRuntimeDir(runtimeBaseDir, version, sha256) {
497
498
  }
498
499
 
499
500
  // The asset digest stamped into a complete runtime tree, '' when absent.
501
+ // A tree whose postgres executable holds no data (a crash after the extract
502
+ // left it NUL-filled) is not complete, whatever its stamp says.
500
503
  function readRuntimeStamp(verDir) {
501
504
  try {
505
+ const postgres = join(verDir, 'bin', process.platform === 'win32' ? 'postgres.exe' : 'postgres');
506
+ if (existsSync(postgres) && !fileHasData(postgres)) return '';
502
507
  return readFileSync(join(verDir, '.version-sha256'), 'utf8').trim();
503
508
  } catch {
504
509
  return '';
@@ -588,7 +593,7 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
588
593
  }
589
594
 
590
595
  // Stamp sha256 inside staging dir.
591
- writeFileSync(join(stagingDir, '.version-sha256'), sha256);
596
+ writeFileAtomicSync(join(stagingDir, '.version-sha256'), sha256);
592
597
  normalizeBinExecBit(stagingDir);
593
598
 
594
599
  // Atomic swap:
@@ -1,7 +1,7 @@
1
- import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
- import { randomUUID } from 'node:crypto';
4
+ import { parseJsonText, writeJsonAtomicAsync } from '../../shared/atomic-file.mjs';
5
5
 
6
6
  const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
7
7
 
@@ -18,17 +18,8 @@ function journalPath(id, dataDir) {
18
18
  }
19
19
 
20
20
  export async function writeOfficeJournal(record, dataDir = '') {
21
- const directory = officeJournalDirectory(dataDir);
22
- await mkdir(directory, { recursive: true });
23
21
  const target = journalPath(record.id, dataDir);
24
- const temporary = join(directory, `.${record.id}.${randomUUID()}.tmp`);
25
- await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
26
- try {
27
- await rename(temporary, target);
28
- } catch {
29
- await rm(target, { force: true });
30
- await rename(temporary, target);
31
- }
22
+ await writeJsonAtomicAsync(target, record);
32
23
  return target;
33
24
  }
34
25
 
@@ -38,9 +29,11 @@ export async function removeOfficeJournal(id, dataDir = '') {
38
29
 
39
30
  export async function readOfficeJournal(id, dataDir = '') {
40
31
  try {
41
- return JSON.parse(await readFile(journalPath(id, dataDir), 'utf8'));
32
+ return parseJsonText(await readFile(journalPath(id, dataDir), 'utf8'));
42
33
  } catch (error) {
43
- if (error?.code === 'ENOENT') return null;
34
+ // A journal a crash left empty or NUL-filled records nothing to recover;
35
+ // it stays on disk for the retention sweep and never blocks the caller.
36
+ if (error?.code === 'ENOENT' || error instanceof SyntaxError) return null;
44
37
  throw error;
45
38
  }
46
39
  }
@@ -1,5 +1,5 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
1
+ import { readFile } from 'node:fs/promises';
2
+ import { writeJsonAtomicAsync } from '../../../shared/atomic-file.mjs';
3
3
  import { delimiter, dirname, isAbsolute, join, resolve, win32 } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { fileURLToPath } from 'node:url';
@@ -126,20 +126,8 @@ export async function readJson(path, fallback = null) {
126
126
  }
127
127
 
128
128
  export async function writeJsonAtomic(path, value) {
129
- await mkdir(dirname(path), { recursive: true });
130
- const temporary = join(dirname(path), `.${randomUUID()}.tmp`);
131
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
132
- encoding: 'utf8',
133
- mode: 0o600,
134
- });
135
- try {
136
- await rename(temporary, path);
137
- } catch {
138
- await rm(path, { force: true });
139
- await rename(temporary, path);
140
- } finally {
141
- await rm(temporary, { force: true }).catch(() => {});
142
- }
129
+ // Durable atomic replace; the replaced version stays as <file>.bak.
130
+ await writeJsonAtomicAsync(path, value, { backup: true });
143
131
  }
144
132
 
145
133
  function resolveConfigPath(path, base) {
@@ -0,0 +1,37 @@
1
+ // The OCR engine's own thread. Starting the engine includes the first-use
2
+ // download of each language's data and gives no handle until it finishes, so
3
+ // it runs here, where ending this thread ends the engine and the download with
4
+ // it (ocr-engine.mjs).
5
+ import { createRequire } from 'node:module';
6
+ import { mkdir } from 'node:fs/promises';
7
+ import { parentPort, workerData } from 'node:worker_threads';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const { languages, cachePath, langPath } = workerData;
11
+
12
+ let engine = null;
13
+ try {
14
+ await mkdir(cachePath, { recursive: true });
15
+ const tesseract = require('tesseract.js');
16
+ engine = await tesseract.createWorker(languages, tesseract.OEM.LSTM_ONLY, {
17
+ cachePath,
18
+ gzip: true,
19
+ ...(langPath ? { langPath } : {}),
20
+ });
21
+ } catch (error) {
22
+ parentPort.postMessage({ startError: String(error?.message || error) });
23
+ }
24
+
25
+ if (engine) {
26
+ // One page at a time, in arrival order: the engine itself is serial.
27
+ parentPort.on('message', async ({ id, image, output }) => {
28
+ try {
29
+ const input = typeof image === 'string' ? image : Buffer.from(image.buffer, image.byteOffset, image.byteLength);
30
+ const recognized = await engine.recognize(input, {}, output || { text: true });
31
+ parentPort.postMessage({ id, data: recognized.data });
32
+ } catch (error) {
33
+ parentPort.postMessage({ id, error: String(error?.message || error) });
34
+ }
35
+ });
36
+ parentPort.postMessage({ ready: true });
37
+ }
@@ -0,0 +1,146 @@
1
+ // One OCR engine per language set and cache, on a thread of its own so that a
2
+ // stalled start (the first-use language download) or a cancelled page can be
3
+ // ended at once: ending the thread ends the engine and its download. Callers
4
+ // arriving while it starts share that start; the engine ends when the last
5
+ // caller releases it.
6
+ import { Worker } from 'node:worker_threads';
7
+ import { workerExecArgv } from '../../shared/worker-exec-argv.mjs';
8
+
9
+ const ENGINE_URL = new URL('./ocr-engine-worker.mjs', import.meta.url);
10
+
11
+ /** The start includes the first-use download; without network it fails or hangs. */
12
+ export const OCR_ENGINE_START_TIMEOUT_MS = 90_000;
13
+
14
+ const engines = new Map();
15
+
16
+ const cancelledError = () => new Error('PDF OCR was cancelled');
17
+ const unavailableError = (message) => Object.assign(new Error(message), { code: 'OCR_UNAVAILABLE' });
18
+
19
+ const defaultSpawn = (url, options) => new Worker(url, options);
20
+
21
+ /** How many engine threads are running (for diagnostics and tests). */
22
+ export function liveOcrEngines() {
23
+ return engines.size;
24
+ }
25
+
26
+ // Ends the engine: its thread, its start and every page it still owes.
27
+ function closeEngine(entry, error) {
28
+ if (entry.closed) return;
29
+ entry.closed = true;
30
+ entry.error = error;
31
+ clearTimeout(entry.timer);
32
+ if (engines.get(entry.key) === entry) engines.delete(entry.key);
33
+ entry.failStart(error);
34
+ const jobs = [...entry.jobs.values()];
35
+ entry.jobs.clear();
36
+ for (const job of jobs) job.reject(error);
37
+ void entry.thread.terminate().catch(() => {});
38
+ }
39
+
40
+ function startEngine(key, { languages, cachePath, langPath, timeoutMs, spawn }) {
41
+ const entry = { key, holders: 0, jobs: new Map(), nextJob: 0, closed: false, error: null };
42
+ entry.started = new Promise((resolve, reject) => {
43
+ entry.markStarted = resolve;
44
+ entry.failStart = reject;
45
+ });
46
+ entry.started.catch(() => {});
47
+ entry.thread = spawn(ENGINE_URL, {
48
+ execArgv: workerExecArgv(),
49
+ workerData: { languages, cachePath, langPath: langPath || null },
50
+ });
51
+ entry.timer = setTimeout(
52
+ () => closeEngine(entry, unavailableError('OCR language data did not load in time (it downloads on first use)')),
53
+ timeoutMs
54
+ );
55
+ entry.timer.unref?.();
56
+ entry.thread.on('message', (message) => {
57
+ if (message?.ready) {
58
+ clearTimeout(entry.timer);
59
+ entry.markStarted();
60
+ return;
61
+ }
62
+ if (message?.startError) {
63
+ closeEngine(entry, unavailableError(`OCR could not start: ${message.startError}`));
64
+ return;
65
+ }
66
+ const job = entry.jobs.get(message?.id);
67
+ if (!job) return;
68
+ entry.jobs.delete(message.id);
69
+ if (message.error) job.reject(new Error(`OCR failed: ${message.error}`));
70
+ else job.resolve(message.data);
71
+ });
72
+ entry.thread.on('error', (error) => closeEngine(entry, error));
73
+ entry.thread.on('exit', (code) => closeEngine(entry, new Error(`OCR engine stopped (exit ${code})`)));
74
+ engines.set(key, entry);
75
+ return entry;
76
+ }
77
+
78
+ // `promise`, or a cancellation as soon as `signal` aborts.
79
+ function untilAborted(promise, signal) {
80
+ if (!signal) return promise;
81
+ if (signal.aborted) return Promise.reject(cancelledError());
82
+ return new Promise((resolve, reject) => {
83
+ const onAbort = () => reject(cancelledError());
84
+ signal.addEventListener('abort', onAbort, { once: true });
85
+ promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
86
+ });
87
+ }
88
+
89
+ function recognizeOn(entry, image, { output, signal } = {}) {
90
+ if (entry.closed) return Promise.reject(entry.error);
91
+ if (signal?.aborted) return Promise.reject(cancelledError());
92
+ const id = ++entry.nextJob;
93
+ const answer = new Promise((resolve, reject) => {
94
+ entry.jobs.set(id, { resolve, reject });
95
+ });
96
+ entry.thread.postMessage({ id, image, output: output || null });
97
+ if (!signal) return answer;
98
+ return new Promise((resolve, reject) => {
99
+ const onAbort = () => {
100
+ entry.jobs.delete(id);
101
+ // A page in recognition cannot be stopped inside the engine: the engine
102
+ // ends with it unless another caller still uses it, which then gets the
103
+ // page's (discarded) answer before its own.
104
+ if (entry.holders <= 1) closeEngine(entry, cancelledError());
105
+ reject(cancelledError());
106
+ };
107
+ signal.addEventListener('abort', onAbort, { once: true });
108
+ answer.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
109
+ });
110
+ }
111
+
112
+ /**
113
+ * The engine for `languages` ("eng+kor") with its data cached at `cachePath`,
114
+ * started on first use. Resolves `{ recognize(image, { output, signal }),
115
+ * release() }` once it is ready; the caller releases it when done. Rejects
116
+ * with code OCR_UNAVAILABLE when it does not start within `timeoutMs`, and
117
+ * with a cancellation when `signal` aborts, ending the start if no one else
118
+ * waits on it.
119
+ */
120
+ export async function acquireOcrEngine({
121
+ languages,
122
+ cachePath,
123
+ langPath = null,
124
+ signal = null,
125
+ timeoutMs = OCR_ENGINE_START_TIMEOUT_MS,
126
+ spawn = defaultSpawn,
127
+ }) {
128
+ if (signal?.aborted) throw cancelledError();
129
+ const key = `${cachePath}\0${languages}\0${langPath || ''}`;
130
+ const entry = engines.get(key) || startEngine(key, { languages, cachePath, langPath, timeoutMs, spawn });
131
+ entry.holders += 1;
132
+ let released = false;
133
+ const release = () => {
134
+ if (released) return;
135
+ released = true;
136
+ entry.holders -= 1;
137
+ if (entry.holders <= 0) closeEngine(entry, new Error('OCR engine released'));
138
+ };
139
+ try {
140
+ await untilAborted(entry.started, signal);
141
+ } catch (error) {
142
+ release();
143
+ throw error;
144
+ }
145
+ return { recognize: (image, options) => recognizeOn(entry, image, options), release };
146
+ }
@@ -1,15 +1,18 @@
1
1
  // PDF OCR: tesseract TSV/block parsing, readiness probe and the ocr operation.
2
2
  import { createRequire } from 'node:module';
3
- import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3
+ import { readFile, readdir, rm, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { PDFDocument, rgb } from 'pdf-lib';
6
6
  import { SAVE_OPTIONS } from './pdf-draw.mjs';
7
7
  import { embedDocumentFont, fontCovers } from './pdf-fonts.mjs';
8
- import { renderPdfPages } from './pdf-render.mjs';
8
+ import { PDF_RENDER_MAX_EXPLICIT_PAGES, renderPdfPages } from './pdf-render.mjs';
9
9
  import { selectedPages } from './pdf-document.mjs';
10
+ import { acquireOcrEngine } from './ocr-engine.mjs';
10
11
 
11
12
  const require = createRequire(import.meta.url);
12
13
 
14
+ const DEFAULT_MIN_CONFIDENCE = 40;
15
+
13
16
  // The engine's own column order. Tesseract writes this table with a header
14
17
  // row through its command line and without one through the worker API, so the
15
18
  // reader accepts both: taking the first data row as a header dropped that word
@@ -208,7 +211,7 @@ export async function pdfOcrReadiness(dataDir) {
208
211
 
209
212
  // Renders and recognizes one page; the rendered image path is recorded on
210
213
  // `temporaryImages` for cleanup.
211
- async function recognizePage(worker, path, pageNumber, operation, signal, temporaryImages) {
214
+ async function recognizePage(engine, path, pageNumber, operation, signal, temporaryImages) {
212
215
  const rendered = await renderPdfPages(path, {
213
216
  pages: [pageNumber],
214
217
  maxWidth: Math.max(1200, Math.min(3200, Number(operation.maxWidth) || 2400)),
@@ -216,13 +219,13 @@ async function recognizePage(worker, path, pageNumber, operation, signal, tempor
216
219
  });
217
220
  const image = rendered.images[0];
218
221
  temporaryImages.push(image.path);
219
- const recognized = await worker.recognize(image.path, {}, { text: true, tsv: true, blocks: true });
220
- const lines = ocrTextLines(recognized.data.tsv, recognized.data.text);
221
- const minConfidence = Number(operation.minConfidence ?? 40);
222
- const words = (lines.length ? lines.flatMap((line) => line.words) : parseOcrBlocks(recognized.data.blocks)).filter(
222
+ const recognized = await engine.recognize(image.path, { output: { text: true, tsv: true, blocks: true }, signal });
223
+ const lines = ocrTextLines(recognized.tsv, recognized.text);
224
+ const minConfidence = Number(operation.minConfidence ?? DEFAULT_MIN_CONFIDENCE);
225
+ const words = (lines.length ? lines.flatMap((line) => line.words) : parseOcrBlocks(recognized.blocks)).filter(
223
226
  (word) => word.confidence >= minConfidence
224
227
  );
225
- return { pageNumber, image, lines, words, text: recognized.data.text || '' };
228
+ return { pageNumber, image, lines, words, text: recognized.text || '' };
226
229
  }
227
230
 
228
231
  // One font for all the recognized text: Helvetica when it is Latin, an
@@ -285,18 +288,15 @@ function placeRecognizedText(document, { pageNumber, image, lines, words }, font
285
288
  }
286
289
  }
287
290
 
288
- async function createOcrWorker(operation, dataDir) {
291
+ // The shared engine for the operation's languages (ocr-engine.mjs): it starts
292
+ // on its own thread, so a stalled first-use download or a cancellation ends
293
+ // it at once.
294
+ async function openOcrEngine(operation, dataDir, signal) {
289
295
  const languages = Array.isArray(operation.languages)
290
296
  ? operation.languages.map(String).join('+')
291
297
  : String(operation.languages || 'eng+kor');
292
- const cachePath = ocrCachePath(dataDir);
293
- await mkdir(cachePath, { recursive: true });
294
- const tesseract = require('tesseract.js');
295
- const worker = await tesseract.createWorker(languages, tesseract.OEM.LSTM_ONLY, {
296
- cachePath,
297
- gzip: true,
298
- });
299
- return { worker, languages };
298
+ const engine = await acquireOcrEngine({ languages, cachePath: ocrCachePath(dataDir), signal });
299
+ return { engine, languages };
300
300
  }
301
301
 
302
302
  function ocrResult(operation, { pages, languages, text, tally, fontPath, embedded }) {
@@ -318,6 +318,36 @@ function ocrResult(operation, { pages, languages, text, tally, fontPath, embedde
318
318
  };
319
319
  }
320
320
 
321
+ /**
322
+ * Read-only OCR: the recognized text of `pages` (1-based) of the PDF at
323
+ * `path`, as `[{ page, text }]`. Pages are rendered in memory and nothing is
324
+ * written next to the PDF or into it. Throws when the engine or its language
325
+ * data is unavailable (no network on first use).
326
+ */
327
+ export async function recognizePdfPageTexts(path, pages, { dataDir, signal = null, languages } = {}) {
328
+ const { engine } = await openOcrEngine({ languages }, dataDir, signal);
329
+ const out = [];
330
+ try {
331
+ for (let offset = 0; offset < pages.length; offset += PDF_RENDER_MAX_EXPLICIT_PAGES) {
332
+ if (signal?.aborted) throw new Error('PDF OCR was cancelled');
333
+ const rendered = await renderPdfPages(path, {
334
+ pages: pages.slice(offset, offset + PDF_RENDER_MAX_EXPLICIT_PAGES),
335
+ maxWidth: 2000,
336
+ signal,
337
+ writeImages: false,
338
+ });
339
+ for (const image of rendered.images) {
340
+ if (signal?.aborted) throw new Error('PDF OCR was cancelled');
341
+ const recognized = await engine.recognize(Buffer.from(image.data, 'base64'), { signal });
342
+ out.push({ page: image.page, text: String(recognized?.text || '').trim() });
343
+ }
344
+ }
345
+ return out;
346
+ } finally {
347
+ engine.release();
348
+ }
349
+ }
350
+
321
351
  export async function ocrPdf(path, operation, { dataDir, signal = null } = {}) {
322
352
  const source = await readFile(path);
323
353
  const document = await PDFDocument.load(source, {
@@ -325,7 +355,7 @@ export async function ocrPdf(path, operation, { dataDir, signal = null } = {}) {
325
355
  updateMetadata: false,
326
356
  });
327
357
  const pages = selectedPages(document.getPageCount(), operation.pages || (operation.page ? [operation.page] : null));
328
- const { worker, languages } = await createOcrWorker(operation, dataDir);
358
+ const { engine, languages } = await openOcrEngine(operation, dataDir, signal);
329
359
  const tally = { wordCount: 0, skippedWords: 0, totalConfidence: 0 };
330
360
  let text = '';
331
361
  const temporaryImages = [];
@@ -335,19 +365,19 @@ export async function ocrPdf(path, operation, { dataDir, signal = null } = {}) {
335
365
  // it produced.
336
366
  for (const pageNumber of pages) {
337
367
  if (signal?.aborted) throw new Error('PDF OCR was cancelled');
338
- const recognized = await recognizePage(worker, path, pageNumber, operation, signal, temporaryImages);
368
+ const recognized = await recognizePage(engine, path, pageNumber, operation, signal, temporaryImages);
339
369
  recognizedPages.push(recognized);
340
370
  text += `${text ? '\n\n' : ''}--- Page ${pageNumber} ---\n${recognized.text}`;
341
371
  }
342
372
  const { font, fontPath, embedded } = await selectOcrFont(document, operation, recognizedPages);
343
- const minConfidence = Number(operation.minConfidence ?? 40);
373
+ const minConfidence = Number(operation.minConfidence ?? DEFAULT_MIN_CONFIDENCE);
344
374
  for (const entry of recognizedPages) placeRecognizedText(document, entry, font, minConfidence, tally);
345
375
  if (tally.wordCount > 0) {
346
376
  await writeFile(path, await document.save(SAVE_OPTIONS));
347
377
  }
348
378
  return ocrResult(operation, { pages, languages, text, tally, fontPath, embedded });
349
379
  } finally {
350
- await worker.terminate().catch(() => {});
380
+ engine.release();
351
381
  for (const image of temporaryImages) await rm(image, { force: true }).catch(() => {});
352
382
  }
353
383
  }
@@ -51,8 +51,8 @@ function requestedPages(pageCount, pages) {
51
51
  const selected =
52
52
  Array.isArray(pages) && pages.length ? pages : Array.from({ length: pageCount }, (_, index) => index + 1);
53
53
  const unique = [...new Set(selected.map(Number))];
54
- if (Array.isArray(pages) && pages.length && unique.length > 12) {
55
- throw new Error('render accepts at most 12 explicit pages per call');
54
+ if (Array.isArray(pages) && pages.length && unique.length > PDF_RENDER_MAX_EXPLICIT_PAGES) {
55
+ throw new Error(`render accepts at most ${PDF_RENDER_MAX_EXPLICIT_PAGES} explicit pages per call`);
56
56
  }
57
57
  for (const page of unique) {
58
58
  if (!Number.isInteger(page) || page < 1 || page > pageCount) {
@@ -62,6 +62,8 @@ function requestedPages(pageCount, pages) {
62
62
  return unique;
63
63
  }
64
64
 
65
+ /** The most explicit pages one render call accepts. */
66
+ export const PDF_RENDER_MAX_EXPLICIT_PAGES = 12;
65
67
  const PDF_RENDER_WORKER_KIND = 'mixdog-office-pdf-render';
66
68
  const PDF_RENDER_PAGE_WORKER_KIND = 'mixdog-office-pdf-render-page';
67
69
 
@@ -104,9 +106,21 @@ async function renderPdfPageDirect(path, pageNumber, targetWidth, minimumScale =
104
106
  data: canvas.toBuffer('image/png').toString('base64'),
105
107
  };
106
108
  } finally {
107
- try {
108
- await loadingTask.destroy?.();
109
- } catch {}
109
+ await destroyQuietly(loadingTask);
110
+ }
111
+ }
112
+
113
+ const cancelledError = () => new Error('PDF rendering was cancelled');
114
+
115
+ function throwIfCancelled(signal) {
116
+ if (signal?.aborted) throw cancelledError();
117
+ }
118
+
119
+ async function destroyQuietly(loadingTask) {
120
+ try {
121
+ await loadingTask.destroy?.();
122
+ } catch {
123
+ /* a document that will not close cleanly must not mask the render's own outcome */
110
124
  }
111
125
  }
112
126
 
@@ -125,7 +139,7 @@ async function runPdfRenderWorker(data, signal = null) {
125
139
  };
126
140
  const onAbort = () => {
127
141
  void worker.terminate();
128
- finish(reject, new Error('PDF rendering was cancelled'));
142
+ finish(reject, cancelledError());
129
143
  };
130
144
  signal?.addEventListener('abort', onAbort, { once: true });
131
145
  worker.once('message', (message) => {
@@ -216,7 +230,7 @@ async function renderPdfPagesDirect(
216
230
  path,
217
231
  { pages = null, maxWidth = 1400, maximumScale = 2, signal = null, writeImages = true } = {}
218
232
  ) {
219
- if (signal?.aborted) throw new Error('PDF rendering was cancelled');
233
+ throwIfCancelled(signal);
220
234
  installPdfGlobals();
221
235
  const { getDocument, VerbosityLevel } = await resolvedPdfJs();
222
236
  const pdfData = await readFile(path);
@@ -225,9 +239,7 @@ async function renderPdfPagesDirect(
225
239
  try {
226
240
  return { loadingTask, document: await loadingTask.promise };
227
241
  } catch (error) {
228
- try {
229
- await loadingTask.destroy?.();
230
- } catch {}
242
+ await destroyQuietly(loadingTask);
231
243
  throw error;
232
244
  }
233
245
  };
@@ -237,7 +249,7 @@ async function renderPdfPagesDirect(
237
249
  const stem = basename(path, extname(path));
238
250
  const output = [];
239
251
  const renderPage = async (pageNumber, targetWidth, minimumScale = 0.25) => {
240
- if (signal?.aborted) throw new Error('PDF rendering was cancelled');
252
+ throwIfCancelled(signal);
241
253
  const rendered = await runPdfRenderWorker(
242
254
  {
243
255
  kind: PDF_RENDER_PAGE_WORKER_KIND,
@@ -249,7 +261,7 @@ async function renderPdfPagesDirect(
249
261
  },
250
262
  signal
251
263
  );
252
- if (signal?.aborted) throw new Error('PDF rendering was cancelled');
264
+ throwIfCancelled(signal);
253
265
  return {
254
266
  canvas: { width: rendered.width, height: rendered.height },
255
267
  data: Buffer.from(rendered.data, 'base64'),
@@ -294,18 +306,18 @@ async function renderPdfPagesDirect(
294
306
  },
295
307
  };
296
308
  } finally {
297
- try {
298
- await loadingTask.destroy?.();
299
- } catch {}
309
+ await destroyQuietly(loadingTask);
300
310
  }
301
311
  }
302
312
 
303
313
  export async function renderPdfPages(path, options = {}) {
304
314
  const signal = options.signal || null;
305
- if (signal?.aborted) throw new Error('PDF rendering was cancelled');
315
+ throwIfCancelled(signal);
306
316
  if (!isMainThread) return await renderPdfPagesDirect(path, options);
307
317
  // The worker rasterizes; this thread writes the images, where the call's
308
- // write ledger can see each one before it lands.
318
+ // write ledger can see each one before it lands. `writeImages: false`
319
+ // returns the images in memory only (a reader that must not write next to
320
+ // the user's file).
309
321
  const rendered = await runPdfRenderWorker(
310
322
  {
311
323
  kind: PDF_RENDER_WORKER_KIND,
@@ -319,7 +331,7 @@ export async function renderPdfPages(path, options = {}) {
319
331
  },
320
332
  signal
321
333
  );
322
- await writePageImages(rendered.images);
334
+ if (options.writeImages !== false) await writePageImages(rendered.images);
323
335
  return rendered;
324
336
  }
325
337
 
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { createHash, randomUUID } from 'node:crypto';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
3
  import { access, chmod, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import { gunzipSync } from 'node:zlib';
5
5
  import { join } from 'node:path';
@@ -202,7 +202,7 @@ export async function ensureOoxmlValidator({ dataDir, download = true, signal =
202
202
  }
203
203
 
204
204
  async function installValidator(paths, entry, signal) {
205
- const temporary = join(paths.root, `.${entry.binary}.${randomUUID()}.tmp`);
205
+ const temporary = join(paths.root, `${entry.binary}.mixdog-tmp-${randomBytes(8).toString('hex')}`);
206
206
  try {
207
207
  const archive = await fetchArchive(packageUrl(entry.name), { signal });
208
208
  verifyIntegrity(archive, entry.integrity);