mixdog 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/README.md +8 -4
  2. package/package.json +2 -2
  3. package/scripts/native-release-status.mjs +238 -0
  4. package/src/defaults/shipped-history.json +5 -1
  5. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  6. package/src/defaults/skills/setup/SKILL.md +23 -1
  7. package/src/defaults/skills/setup/references/actions.md +50 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  9. package/src/defaults/skills/setup-guide/SKILL.md +7 -3
  10. package/src/defaults/skills/visualize/SKILL.md +9 -6
  11. package/src/defaults/skills/visualize/references/craft.md +8 -1
  12. package/src/help.mjs +1 -1
  13. package/src/lib/keychain-cjs.cjs +4 -1
  14. package/src/lib/rules-builder.cjs +14 -7
  15. package/src/rules/agent-mode/AGENT_MODE.md +8 -5
  16. package/src/rules/agent-mode/MIXY.md +6 -8
  17. package/src/rules/lead/LEAD.md +2 -2
  18. package/src/rules/shared/10-tool-workflow.md +4 -0
  19. package/src/rules/shared/20-research.md +2 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  21. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  24. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  25. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  27. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  28. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  30. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  31. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -5
  33. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  34. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  36. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  39. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +27 -8
  42. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +16 -0
  47. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  48. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  49. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  50. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +2 -1
  55. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  58. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  59. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  61. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  62. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  64. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -6
  66. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  67. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  68. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  69. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +6 -5
  70. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +7 -6
  71. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  73. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  75. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  76. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  77. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +17 -2
  78. package/src/runtime/agent/orchestrator/session/agent-room.mjs +133 -38
  79. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  80. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  82. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  83. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  84. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  85. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  87. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  88. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  91. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  92. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +136 -32
  93. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  94. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  95. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  96. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  97. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  98. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  99. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  100. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  101. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +5 -5
  102. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  103. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  104. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  105. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  106. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  107. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  108. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  109. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  110. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  111. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  112. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  113. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  114. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  115. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  116. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  117. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  121. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +55 -27
  123. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -8
  124. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  125. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  126. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  127. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  128. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  129. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  130. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +18 -3
  131. package/src/runtime/agent/orchestrator/session/routine-result.mjs +120 -9
  132. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  133. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  135. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  136. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  137. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  138. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  139. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  140. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  141. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  142. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  143. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  144. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  145. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  146. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +35 -5
  147. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +42 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  149. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  150. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  151. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  152. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  154. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  155. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  156. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  157. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  158. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  159. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  160. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  161. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  162. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  163. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  164. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  165. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  166. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  167. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  168. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  169. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  170. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  171. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  173. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  174. package/src/runtime/attachments/limits.mjs +5 -1
  175. package/src/runtime/attachments/office-extract.mjs +3 -6
  176. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  177. package/src/runtime/attachments/store.mjs +49 -30
  178. package/src/runtime/browser-bridge/client.mjs +73 -20
  179. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  180. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  181. package/src/runtime/channels/lib/state-file.mjs +15 -15
  182. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  183. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  184. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  185. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  186. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  187. package/src/runtime/effort-judge/judge-client.mjs +5 -2
  188. package/src/runtime/effort-judge/model-install.mjs +16 -8
  189. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  190. package/src/runtime/local-provider/catalog.mjs +5 -2
  191. package/src/runtime/local-provider/registered-models.mjs +64 -27
  192. package/src/runtime/local-provider/server.mjs +2 -3
  193. package/src/runtime/media/adapters/image-api.mjs +89 -0
  194. package/src/runtime/media/auth.mjs +64 -1
  195. package/src/runtime/media/catalog.mjs +66 -4
  196. package/src/runtime/media/jobs.mjs +4 -0
  197. package/src/runtime/media/lanes.mjs +46 -5
  198. package/src/runtime/media/media-usage.mjs +17 -0
  199. package/src/runtime/media/renditions.mjs +15 -15
  200. package/src/runtime/media/store.mjs +36 -35
  201. package/src/runtime/media/tool.mjs +8 -1
  202. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  203. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  204. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  205. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  206. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  207. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  208. package/src/runtime/office/core/journal.mjs +7 -14
  209. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  210. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  211. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  212. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  213. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  214. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  215. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  216. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  217. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  218. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  219. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  220. package/src/runtime/shared/agent-store.mjs +24 -7
  221. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  222. package/src/runtime/shared/atomic-file.d.mts +50 -0
  223. package/src/runtime/shared/atomic-file.mjs +394 -132
  224. package/src/runtime/shared/background-tasks.mjs +7 -4
  225. package/src/runtime/shared/bot-hops.mjs +30 -0
  226. package/src/runtime/shared/bot-split-migration.mjs +1 -1
  227. package/src/runtime/shared/bounded-download.mjs +4 -2
  228. package/src/runtime/shared/config.mjs +39 -9
  229. package/src/runtime/shared/defaults-separation.mjs +6 -2
  230. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  231. package/src/runtime/shared/err-text.mjs +4 -0
  232. package/src/runtime/shared/error-presentation.mjs +22 -4
  233. package/src/runtime/shared/file-has-data.mjs +28 -0
  234. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  235. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  236. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  237. package/src/runtime/shared/projects.mjs +50 -41
  238. package/src/runtime/shared/provider-accounts.mjs +17 -12
  239. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  240. package/src/runtime/shared/routine-trigger.mjs +14 -6
  241. package/src/runtime/shared/run-child.d.mts +3 -0
  242. package/src/runtime/shared/run-child.mjs +24 -3
  243. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  244. package/src/runtime/shared/singleton-owner.mjs +7 -6
  245. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  246. package/src/runtime/shared/staged-update.mjs +4 -2
  247. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  248. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  249. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  250. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  251. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  252. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  253. package/src/runtime/shared/user-cwd.mjs +9 -4
  254. package/src/runtime/shared/user-data-guard.mjs +41 -42
  255. package/src/runtime/shared/utf8-cut.mjs +10 -0
  256. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  257. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  258. package/src/runtime/tidy/core-install.mjs +26 -4
  259. package/src/runtime/tidy/install.mjs +91 -23
  260. package/src/runtime/web-search/lib/config.mjs +4 -16
  261. package/src/session-runtime/agent-home-delivery.mjs +555 -199
  262. package/src/session-runtime/agent-room-delivery.mjs +143 -36
  263. package/src/session-runtime/agent-task-sessions.mjs +667 -165
  264. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  265. package/src/session-runtime/boot/apis.mjs +45 -24
  266. package/src/session-runtime/boot/config.mjs +1 -0
  267. package/src/session-runtime/boot/facade.mjs +11 -1
  268. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  269. package/src/session-runtime/boot/providers.mjs +2 -2
  270. package/src/session-runtime/boot/shared.mjs +12 -6
  271. package/src/session-runtime/boot/tools.mjs +2 -2
  272. package/src/session-runtime/bot-status.mjs +432 -0
  273. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  274. package/src/session-runtime/config-lifecycle.mjs +60 -55
  275. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  276. package/src/session-runtime/held-requests.mjs +36 -33
  277. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  278. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  279. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +19 -4
  280. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  281. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  282. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  283. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  284. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  285. package/src/session-runtime/media-api.mjs +5 -2
  286. package/src/session-runtime/model-route/route-tuning.mjs +4 -1
  287. package/src/session-runtime/model-route/set-route.mjs +61 -1
  288. package/src/session-runtime/owned-automation-run.mjs +8 -1
  289. package/src/session-runtime/room-member-gate.mjs +16 -3
  290. package/src/session-runtime/route-resolution.mjs +16 -8
  291. package/src/session-runtime/route-state.mjs +1 -59
  292. package/src/session-runtime/routine-home-turn.mjs +5 -3
  293. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  294. package/src/session-runtime/runtime-paths.mjs +11 -5
  295. package/src/session-runtime/runtime-tunables.mjs +6 -0
  296. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  297. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  298. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  299. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  300. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  301. package/src/session-runtime/services/agent-tool/send-flow.mjs +25 -21
  302. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  303. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  304. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +8 -5
  305. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  306. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  307. package/src/session-runtime/services/agent-tool.mjs +6 -0
  308. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  309. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  310. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  311. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  312. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  313. package/src/session-runtime/services/provider-admin.mjs +14 -4
  314. package/src/session-runtime/services/schedule-access.mjs +33 -4
  315. package/src/session-runtime/services/seeds.mjs +4 -3
  316. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  317. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  318. package/src/session-runtime/session-create/materialize.mjs +2 -3
  319. package/src/session-runtime/session-create/session-options.mjs +15 -2
  320. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  321. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  322. package/src/session-runtime/setup-tool/executor.mjs +8 -2
  323. package/src/session-runtime/setup-tool/settings-contract.mjs +31 -1
  324. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  325. package/src/session-runtime/skills-api.mjs +12 -5
  326. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  327. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  328. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  329. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  330. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  331. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  332. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  333. package/src/session-runtime/tool-policy-refresh.mjs +47 -369
  334. package/src/session-runtime/tool-surface.mjs +5 -3
  335. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +95 -88
  336. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  337. package/src/session-runtime/turn/turn-run.mjs +2 -1
  338. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  339. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +519 -333
  340. package/src/session-runtime/workflow-agents-api/agent-route.mjs +46 -46
  341. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  342. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  343. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  344. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  345. package/src/standalone/daemon-agent-control.mjs +4 -1
  346. package/src/standalone/daemon-session-control.mjs +51 -10
  347. package/src/standalone/daemon.mjs +28 -10
  348. package/src/standalone/local-session-runtime.mjs +22 -14
  349. package/src/standalone/session-client.mjs +7 -2
  350. package/src/standalone/session-protocol.mjs +7 -0
  351. package/src/standalone/session-service.mjs +11 -0
  352. package/src/standalone/session-state-projection.mjs +19 -9
  353. package/src/standalone/session-tool-output.mjs +35 -9
  354. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  355. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  356. package/src/tui/app/slash-commands.mjs +1 -1
  357. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  358. package/src/tui/dist/index.mjs +529 -333
  359. package/src/tui/prompt-history-store.mjs +16 -21
  360. package/src/tui/session/context-state.mjs +4 -0
  361. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  362. package/src/tui/session/session-api/media.mjs +2 -1
  363. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  364. package/src/tui/session/session-api/routes-write.mjs +203 -0
  365. package/src/tui/session/session-api/routes.mjs +23 -247
  366. package/src/tui/session/session-api/settings.mjs +14 -1
  367. package/src/tui/session/session-flow/drain.mjs +28 -3
  368. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  369. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  370. package/src/tui/session/session-flow/steering.mjs +8 -3
  371. package/src/tui/session/tool-approval.mjs +185 -20
  372. package/src/tui/session/transcript-intake.mjs +2 -0
  373. package/src/tui/session/turn.mjs +23 -14
  374. package/src/tui/transcript-tool-failures.mjs +8 -2
  375. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  376. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  377. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -1,10 +1,12 @@
1
1
  // Embedding model files in the transformers.js cache layout
2
2
  // (<cache>/<model id>/<path>), so copies that library already downloaded are
3
3
  // reused; a missing file is fetched from the Hugging Face Hub once.
4
- import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
4
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { Readable } from 'node:stream';
7
7
  import { pipeline } from 'node:stream/promises';
8
+ import { parseJsonText } from '../../shared/atomic-file.mjs';
9
+ import { fileHasData } from '../../shared/file-has-data.mjs';
8
10
 
9
11
  const HUB = 'https://huggingface.co';
10
12
 
@@ -17,7 +19,8 @@ async function download(modelId, file, target, { optional = false } = {}) {
17
19
  mkdirSync(dirname(target), { recursive: true });
18
20
  const partial = `${target}.${process.pid}.part`;
19
21
  try {
20
- await pipeline(Readable.fromWeb(res.body), createWriteStream(partial));
22
+ // flush: fsync before close, so the renamed file cannot be all NUL after a crash.
23
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(partial, { flush: true }));
21
24
  renameSync(partial, target);
22
25
  } catch (error) {
23
26
  rmSync(partial, { force: true });
@@ -26,19 +29,39 @@ async function download(modelId, file, target, { optional = false } = {}) {
26
29
  return true;
27
30
  }
28
31
 
29
- /** Local path of `file`, downloading it first when absent. */
32
+ // A cached file that holds no data (empty, NUL-filled by a crash) or, for
33
+ // JSON, does not parse is not present: it is fetched again.
34
+ function cachedFileUsable(target) {
35
+ if (!fileHasData(target)) return false;
36
+ if (!target.endsWith('.json')) return true;
37
+ try {
38
+ parseJsonText(readFileSync(target, 'utf8'));
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /** Local path of `file`, downloading it first when absent or damaged. */
30
46
  export async function ensureModelFile(cacheDir, modelId, file) {
31
47
  const target = join(cacheDir, ...modelId.split('/'), ...file.split('/'));
32
- if (!existsSync(target)) await download(modelId, file, target);
48
+ if (!cachedFileUsable(target)) await download(modelId, file, target);
33
49
  return target;
34
50
  }
35
51
 
36
52
  /** Local path of an ONNX graph; a graph published with external weights gets them alongside. */
37
53
  export async function ensureModelGraph(cacheDir, modelId, file) {
38
54
  const target = join(cacheDir, ...modelId.split('/'), ...file.split('/'));
39
- if (!existsSync(target)) {
40
- await download(modelId, file, target);
41
- await download(modelId, `${file}_data`, `${target}_data`, { optional: true });
55
+ const weights = `${target}_data`;
56
+ const graphPresent = fileHasData(target);
57
+ // The weights are checked on their own: an external-weights file a crash
58
+ // left empty or NUL-filled is fetched again even when the graph is intact.
59
+ // They are fetched before the graph, so a cached graph implies its weights
60
+ // finished downloading; a weights file that is absent stays absent (graphs
61
+ // without external data have none).
62
+ if (!graphPresent || (existsSync(weights) && !fileHasData(weights))) {
63
+ await download(modelId, `${file}_data`, weights, { optional: true });
42
64
  }
65
+ if (!graphPresent) await download(modelId, file, target);
43
66
  return target;
44
67
  }
@@ -2,10 +2,10 @@
2
2
  // One incremental read of a transcript file: the bytes past the persisted
3
3
  // offset, split into complete JSONL lines, stored as conversation rows. The
4
4
  // persisted boundary only advances past lines that were fully consumed (parsed
5
- // + inserted or intentionally skipped); a malformed trailing line (mid-write
6
- // partial) or a transient insert error stops the walk and leaves the boundary
7
- // untouched so the next sweep retries from the same position instead of
8
- // silently consuming the line forever.
5
+ // + inserted or intentionally skipped); a transient insert error stops the walk
6
+ // and leaves the boundary untouched so the next sweep retries from the same
7
+ // position instead of silently consuming the line forever. A complete line
8
+ // that is not JSON (crash damage) is skipped, never a permanent stop.
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { cwdFromTranscriptPath, transcriptRowFromLine } from './transcript-rows.mjs';
@@ -73,8 +73,12 @@ export async function ingestTranscriptTail({ db, transcriptPath, cwd, offsets, r
73
73
  const boundary = { bytes: tail.bytes, lineIndex: tail.lineIndex, generation: tail.generation };
74
74
  let index = tail.lineIndex;
75
75
  let count = 0;
76
- for (const { line, consumedBytes } of completeLines(tail.text)) {
77
- if (!line) {
76
+ for (const { line: rawLine, consumedBytes } of completeLines(tail.text)) {
77
+ // A crash leaves a run of NUL bytes where data never reached disk, and the
78
+ // resumed writer appends after it. A raw NUL is never valid JSON, so drop
79
+ // them and keep the record that follows.
80
+ const line = rawLine.includes('\0') ? rawLine.replaceAll('\0', '') : rawLine;
81
+ if (!line.trim()) {
78
82
  boundary.bytes += consumedBytes;
79
83
  continue;
80
84
  }
@@ -82,7 +86,12 @@ export async function ingestTranscriptTail({ db, transcriptPath, cwd, offsets, r
82
86
  try {
83
87
  parsed = JSON.parse(line);
84
88
  } catch {
85
- break; // malformed line: do not advance past it; retry on next sweep
89
+ // The line is complete (a half-written one has no newline yet and is not
90
+ // yielded), so it can never become valid: skip it for good, or it would
91
+ // block every line after it.
92
+ log(`[transcript-watch] skipped a damaged line in ${path.basename(transcriptPath)}\n`);
93
+ boundary.bytes += consumedBytes;
94
+ continue;
86
95
  }
87
96
  index += 1;
88
97
  const row = transcriptRowFromLine(parsed);
@@ -26,6 +26,7 @@ import { readdir, readFile, rm, stat } from 'node:fs/promises';
26
26
  import { join } from 'node:path';
27
27
  import { execFile, spawn, spawnSync } from 'node:child_process';
28
28
  import { createConnection, createServer } from 'node:net';
29
+ import { holdsNoData, writeFileAtomicSync } from '../../../shared/atomic-file.mjs';
29
30
  import { isPidAlive } from '../../../shared/pid-liveness.mjs';
30
31
  import { runChild } from './run-child.mjs';
31
32
 
@@ -180,9 +181,12 @@ function stripPlatformInvalidLines(conf) {
180
181
  function ensureConfV2(pgdataDir) {
181
182
  const confPath = join(pgdataDir, 'postgresql.conf');
182
183
  if (!existsSync(confPath)) return false;
183
- const original = readFileSync(confPath, 'utf8');
184
+ // A conf a crash left empty or NUL-filled cannot be parsed by PostgreSQL; the
185
+ // defaults plus the overrides appended below are a working config.
186
+ const text = readFileSync(confPath, 'utf8');
187
+ const original = holdsNoData(text) ? '' : text;
184
188
  let conf = stripPlatformInvalidLines(original);
185
- let changed = conf !== original;
189
+ let changed = conf !== text;
186
190
  if (!conf.includes(MIXDOG_CONF_V2_MARKER)) {
187
191
  conf += buildV2Block();
188
192
  changed = true;
@@ -196,7 +200,7 @@ function ensureConfV2(pgdataDir) {
196
200
  changed = true;
197
201
  }
198
202
  if (!changed) return false;
199
- writeFileSync(confPath, conf);
203
+ writeFileAtomicSync(confPath, conf, { backup: true });
200
204
  return true;
201
205
  }
202
206
 
@@ -350,7 +354,7 @@ async function initClusterIfNeeded({ runtimeDir, pgdataDir, env }) {
350
354
  const confPath = join(pgdataDir, 'postgresql.conf');
351
355
  try {
352
356
  const existing = readFileSync(confPath, 'utf8');
353
- writeFileSync(confPath, existing + freshClusterConfAppend());
357
+ writeFileAtomicSync(confPath, existing + freshClusterConfAppend());
354
358
  } catch (e) {
355
359
  __mixdogMemoryLog(`[pg-process] postgresql.conf append failed: ${e?.message}\n`);
356
360
  }
@@ -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
  }