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
@@ -0,0 +1,110 @@
1
+ // Legacy Office (.doc / .xls / .ppt, OLE compound) attachments. Text
2
+ // extraction reads OOXML packages only, so a legacy file is saved as its
3
+ // package by LibreOffice (convertLegacyOffice, the conversion `office open`
4
+ // uses) in a private scratch folder, never beside the user's file.
5
+ import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { basename, extname, join } from 'node:path';
8
+ import { redactDirectories } from '../shared/redact-directories.mjs';
9
+ import { removeDirectoryWithRetry } from '../shared/remove-directory.mjs';
10
+
11
+ // The first eight bytes of every legacy Office (OLE compound) file.
12
+ const OLE_COMPOUND_MAGIC = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]);
13
+
14
+ const PACKAGE_BY_LEGACY_EXTENSION = { doc: 'docx', xls: 'xlsx', ppt: 'pptx' };
15
+ const PACKAGE_BY_LEGACY_MIME = {
16
+ 'application/msword': 'docx',
17
+ 'application/vnd.ms-excel': 'xlsx',
18
+ 'application/vnd.ms-powerpoint': 'pptx',
19
+ };
20
+ const PACKAGE_BY_OOXML_EXTENSION = { docx: 'docx', xlsx: 'xlsx', xlsm: 'xlsx', pptx: 'pptx' };
21
+
22
+ export const OOXML_MIME_BY_PACKAGE = {
23
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
24
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
25
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
26
+ };
27
+ const PACKAGE_BY_OOXML_MIME = {
28
+ ...Object.fromEntries(Object.entries(OOXML_MIME_BY_PACKAGE).map(([kind, mime]) => [mime, kind])),
29
+ 'application/vnd.ms-excel.sheet.macroenabled.12': 'xlsx',
30
+ };
31
+
32
+ export function isLegacyOfficeBuffer(buffer) {
33
+ return buffer.length >= 8 && buffer.subarray(0, 8).equals(OLE_COMPOUND_MAGIC);
34
+ }
35
+
36
+ /** The package extension (`docx`/`xlsx`/`pptx`) an attachment labelled legacy Office
37
+ * (by MIME type or file extension) converts to, else ''. */
38
+ export function legacyOfficeLabelPackage(mimeType, filename) {
39
+ const mime = String(mimeType || '')
40
+ .split(';', 1)[0]
41
+ .trim()
42
+ .toLowerCase();
43
+ const extension = extname(String(filename || ''))
44
+ .slice(1)
45
+ .toLowerCase();
46
+ return PACKAGE_BY_LEGACY_MIME[mime] || PACKAGE_BY_LEGACY_EXTENSION[extension] || '';
47
+ }
48
+
49
+ /** The package an OLE file labelled as an OOXML Office file (renamed, or sent
50
+ * with the OOXML MIME type and no extension) converts to, else ''. */
51
+ export function ooxmlLabelPackage(mimeType, filename) {
52
+ const mime = String(mimeType || '')
53
+ .split(';', 1)[0]
54
+ .trim()
55
+ .toLowerCase();
56
+ const extension = extname(String(filename || ''))
57
+ .slice(1)
58
+ .toLowerCase();
59
+ return PACKAGE_BY_OOXML_MIME[mime] || PACKAGE_BY_OOXML_EXTENSION[extension] || '';
60
+ }
61
+
62
+ async function defaultConvert(path, output, options) {
63
+ const { convertLegacyOffice } = await import('../office/portable/portable-soffice.mjs');
64
+ return convertLegacyOffice(path, output, options);
65
+ }
66
+
67
+ /**
68
+ * Saves the legacy Office `buffer` as the OOXML package `kind` and returns the
69
+ * package bytes and its file name. The scratch folder is always removed, once
70
+ * the converter has returned (it returns only after its process has closed;
71
+ * a removal that still meets a held file is retried, and a final failure is
72
+ * logged). A failure (LibreOffice missing, damaged or protected file, bytes
73
+ * that are not a legacy Office file) throws a message with the scratch path
74
+ * taken out; a cancellation rethrows the abort itself.
75
+ */
76
+ export async function convertLegacyOfficeBuffer(
77
+ buffer,
78
+ filename,
79
+ kind,
80
+ { signal = null, convert = defaultConvert, remove = undefined, log = undefined, retryMs = undefined } = {}
81
+ ) {
82
+ signal?.throwIfAborted();
83
+ if (!isLegacyOfficeBuffer(buffer)) {
84
+ throw new Error(
85
+ `${filename || 'This attachment'} is labelled as a legacy Office file but its content is not one, so it cannot be converted. Open it in Microsoft Office and save it as .${kind}, then attach that copy.`
86
+ );
87
+ }
88
+ const stem = basename(String(filename || 'attachment'), extname(String(filename || ''))) || 'attachment';
89
+ const safeStem = stem.replace(/[^\w.-]+/g, '_').slice(0, 80) || 'attachment';
90
+ const legacyExtension = Object.keys(PACKAGE_BY_LEGACY_EXTENSION).find(
91
+ (ext) => PACKAGE_BY_LEGACY_EXTENSION[ext] === kind
92
+ );
93
+ const dir = await mkdtemp(join(tmpdir(), 'mixdog-attachment-'));
94
+ let converted;
95
+ try {
96
+ const source = join(dir, `${safeStem}.${legacyExtension}`);
97
+ const output = join(dir, `${safeStem}.${kind}`);
98
+ await writeFile(source, buffer);
99
+ await convert(source, output, { signal });
100
+ converted = { buffer: await readFile(output), filename: `${stem}.${kind}` };
101
+ } catch (error) {
102
+ signal?.throwIfAborted();
103
+ throw new Error(redactDirectories(error instanceof Error ? error.message : String(error), [dir]));
104
+ } finally {
105
+ await removeDirectoryWithRetry(dir, { remove, log, retryMs });
106
+ }
107
+ // A cancel that arrived while the folder was being removed still cancels.
108
+ signal?.throwIfAborted();
109
+ return converted;
110
+ }
@@ -16,5 +16,9 @@ const PDF_MAGIC = Buffer.from('%PDF-', 'latin1');
16
16
 
17
17
  /** True when the bytes start with the %PDF- header. */
18
18
  export function isPdfBuffer(buffer) {
19
- return Buffer.isBuffer(buffer) && buffer.length >= PDF_MAGIC.length && buffer.subarray(0, PDF_MAGIC.length).equals(PDF_MAGIC);
19
+ return (
20
+ Buffer.isBuffer(buffer) &&
21
+ buffer.length >= PDF_MAGIC.length &&
22
+ buffer.subarray(0, PDF_MAGIC.length).equals(PDF_MAGIC)
23
+ );
20
24
  }
@@ -6,6 +6,7 @@
6
6
  // No external dependencies.
7
7
  import { readFile, stat } from 'node:fs/promises';
8
8
  import { inflateRawSync } from 'node:zlib';
9
+ import { stripTrailingReplacementChars } from '../shared/utf8-cut.mjs';
9
10
 
10
11
  // Longest run of attributes inside one tag that a pattern will cross. Bounded
11
12
  // so a tag with no closing bracket cannot make a pattern rescan the part.
@@ -675,12 +676,8 @@ function sheetFigures(buf, entries, sheetPart) {
675
676
  function capOutput(text, maxOutputBytes) {
676
677
  const buf = Buffer.from(text, 'utf8');
677
678
  if (buf.length <= maxOutputBytes) return text;
678
- let cut = buf.subarray(0, maxOutputBytes).toString('utf8');
679
- // Drop the replacement characters a split multi-byte sequence leaves (a loop:
680
- // `/\uFFFD+$/` is quadratic on a long run of them).
681
- let end = cut.length;
682
- while (end > 0 && cut[end - 1] === '\uFFFD') end -= 1;
683
- cut = cut.slice(0, end);
679
+ // Drop the replacement characters a split multi-byte sequence leaves.
680
+ const cut = stripTrailingReplacementChars(buf.subarray(0, maxOutputBytes).toString('utf8'));
684
681
  return `${cut}\n... [office text truncated at ${maxOutputBytes} bytes]`;
685
682
  }
686
683
 
@@ -5,19 +5,18 @@
5
5
  * OpenAI-compatible providers without a document contract receive page-ordered
6
6
  * text extracted here, so they never see an unsupported inline Base64 block.
7
7
  */
8
+ import { stripTrailingReplacementChars } from '../shared/utf8-cut.mjs';
8
9
  import { MAX_PDF_PAGES } from './limits.mjs';
9
10
  import { resolvedPdfJs } from './pdfjs-runtime.mjs';
10
11
 
11
12
  const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
12
13
 
13
- function boundedUtf8(value, maxBytes) {
14
+ /** `value` cut to at most `maxBytes` of UTF-8, never inside a multi-byte sequence. */
15
+ export function boundedUtf8(value, maxBytes) {
14
16
  const buffer = Buffer.from(String(value || ''), 'utf8');
15
17
  if (buffer.length <= maxBytes) return { text: buffer.toString('utf8'), bytes: buffer.length, truncated: false };
16
18
  if (maxBytes <= 0) return { text: '', bytes: 0, truncated: true };
17
- const text = buffer
18
- .subarray(0, maxBytes)
19
- .toString('utf8')
20
- .replace(/\uFFFD+$/g, '');
19
+ const text = stripTrailingReplacementChars(buffer.subarray(0, maxBytes).toString('utf8'));
21
20
  return { text, bytes: Buffer.byteLength(text, 'utf8'), truncated: true };
22
21
  }
23
22
 
@@ -74,38 +73,90 @@ function pageBodyText(content) {
74
73
  .trim();
75
74
  }
76
75
 
77
- export async function inspectPdfBuffer(
76
+ /** The deadline prompt intake passes: it runs inside a room's post queue, which
77
+ * a document that never loads would otherwise hold forever. */
78
+ export const PDF_INTAKE_TIMEOUT_MS = 60_000;
79
+
80
+ // A page counts as carrying a text layer when its extracted text holds at
81
+ // least this many non-whitespace characters. A scan often keeps a stamped page
82
+ // number or a few stray glyphs; those alone do not make its content readable
83
+ // as text, so such a page still needs its image (or OCR).
84
+ export const PDF_TEXT_LAYER_MIN_CHARS = 16;
85
+
86
+ /** Whether one page's extracted text is a real text layer (see above). */
87
+ export function pageHasTextLayer(text) {
88
+ return String(text || '').replace(/\s+/g, '').length >= PDF_TEXT_LAYER_MIN_CHARS;
89
+ }
90
+
91
+ function timeoutError(timeoutMs) {
92
+ return Object.assign(new Error(`PDF inspection timed out after ${timeoutMs} ms`), {
93
+ code: 'PDF_INSPECTION_TIMEOUT',
94
+ });
95
+ }
96
+
97
+ /** Inspect a PDF. With `timeoutMs`, a document that has not finished by then
98
+ * is destroyed and the call rejects (`code: 'PDF_INSPECTION_TIMEOUT'`);
99
+ * without it there is no deadline. */
100
+ export async function inspectPdfBuffer(buffer, { timeoutMs = null, ...options } = {}) {
101
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new TypeError('PDF payload is empty');
102
+ const hold = { pdf: null, expired: false };
103
+ if (!(timeoutMs > 0)) return inspectLoaded(buffer, options, hold);
104
+ let timer;
105
+ const deadline = new Promise((_, reject) => {
106
+ timer = setTimeout(() => {
107
+ hold.expired = true;
108
+ // A document still loading is destroyed as soon as it arrives (below).
109
+ Promise.resolve(hold.pdf?.destroy?.()).catch(() => {});
110
+ reject(timeoutError(timeoutMs));
111
+ }, timeoutMs);
112
+ });
113
+ try {
114
+ return await Promise.race([inspectLoaded(buffer, options, hold), deadline]);
115
+ } finally {
116
+ clearTimeout(timer);
117
+ }
118
+ }
119
+
120
+ async function inspectLoaded(
78
121
  buffer,
79
122
  {
80
123
  extractText = false,
124
+ measureText = false,
81
125
  maxPages = MAX_PDF_PAGES,
82
126
  maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
83
127
  pageRange = null,
84
128
  password = '',
85
- } = {}
129
+ },
130
+ hold
86
131
  ) {
87
- if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new TypeError('PDF payload is empty');
88
132
  await resolvedPdfJs();
89
133
  const { getDocumentProxy } = await import('unpdf');
90
134
  const pdf = await getDocumentProxy(new Uint8Array(buffer), password ? { password: String(password) } : {});
135
+ hold.pdf = pdf;
91
136
  try {
137
+ if (hold.expired) throw timeoutError(0);
92
138
  const pageCount = Math.max(0, Number(pdf?.numPages) || 0);
93
139
  const pageLimit = Number.isFinite(Number(maxPages)) ? Math.max(1, Math.floor(Number(maxPages))) : Infinity;
94
140
  if (pageCount > pageLimit) {
95
141
  throw new RangeError(`PDF has ${pageCount} pages; maximum supported attachment is ${pageLimit} pages`);
96
142
  }
143
+ if (measureText) return { pageCount, ...(await measureTextLayer(pdf, pageCount, hold)) };
97
144
  if (!extractText) return { pageCount, text: '', truncated: false };
98
145
 
99
146
  const from = Math.max(1, Number(pageRange?.from) || 1);
100
147
  const to = Math.min(pageCount, Math.max(from, Number(pageRange?.to) || pageCount));
101
148
  const byteLimit = Math.max(1, Number(maxOutputBytes) || DEFAULT_MAX_OUTPUT_BYTES);
102
149
  const chunks = [];
150
+ // Each page's own text (before the output budget cuts it), so a caller can
151
+ // tell text pages from scanned ones.
152
+ const pages = [];
103
153
  let bytes = 0;
104
154
  let truncated = false;
105
155
  for (let pageNumber = from; pageNumber <= to; pageNumber += 1) {
106
156
  const page = await pdf.getPage(pageNumber);
107
157
  try {
108
158
  const body = pageBodyText(await page.getTextContent());
159
+ pages.push({ page: pageNumber, text: body });
109
160
  const block = `--- Page ${pageNumber} ---\n${body || '(no extractable text on this page)'}`;
110
161
  const separatorBytes = chunks.length ? 2 : 0;
111
162
  const remaining = byteLimit - bytes - separatorBytes;
@@ -134,12 +185,35 @@ export async function inspectPdfBuffer(
134
185
  const suffix = '\n\n... [PDF text truncated to the prompt input budget]';
135
186
  const room = Math.max(0, byteLimit - Buffer.byteLength(suffix, 'utf8'));
136
187
  const bounded = boundedUtf8(chunks.join(''), room);
137
- return { pageCount, text: `${bounded.text}${suffix}`, truncated: true };
188
+ return { pageCount, text: `${bounded.text}${suffix}`, truncated: true, pages };
138
189
  }
139
- return { pageCount, text: chunks.join(''), truncated: false };
190
+ return { pageCount, text: chunks.join(''), truncated: false, pages };
140
191
  } finally {
141
192
  try {
142
193
  await pdf.destroy?.();
143
194
  } catch {}
144
195
  }
145
196
  }
197
+
198
+ // How much of the document reads as text: the pages that carry a text layer
199
+ // and the characters they hold. Nothing is kept but the counts.
200
+ async function measureTextLayer(pdf, pageCount, hold) {
201
+ let textPages = 0;
202
+ let textChars = 0;
203
+ for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) {
204
+ if (hold.expired) throw timeoutError(0);
205
+ const page = await pdf.getPage(pageNumber);
206
+ try {
207
+ const body = pageBodyText(await page.getTextContent());
208
+ if (pageHasTextLayer(body)) {
209
+ textPages += 1;
210
+ textChars += body.length;
211
+ }
212
+ } finally {
213
+ try {
214
+ page.cleanup?.();
215
+ } catch {}
216
+ }
217
+ }
218
+ return { textPages, textChars };
219
+ }
@@ -0,0 +1,70 @@
1
+ // Rebuilds the composer's restore payload (`pastedTexts`, `pastedImages`) from a
2
+ // stored, materialized user prompt. The model history holds the EXPANDED prompt:
3
+ // each `[Pasted text #n …]` / `[File #n …]` token became a `textKind` reference
4
+ // part, in token order, and each image an image reference part. The transcript
5
+ // row's own text still spells the tokens, so the n-th reference part belongs to
6
+ // the n-th token; the entries carry references only and are hydrated by the
7
+ // caller (`hydratePastedAttachments`) exactly as the abort/queue restores are.
8
+ import { isAttachmentReference } from './store.mjs';
9
+
10
+ const TEXT_TOKEN_RE = /\[(Pasted text|File) #(\d+)(?:: ([^\]\r\n]+))?[^\]\r\n]*\]/g;
11
+ const IMAGE_TOKEN_RE = /\[Image #(\d+)[^\]\r\n]*\]/g;
12
+
13
+ export function restorableAttachmentsFromPrompt(content, rowText) {
14
+ const parts = Array.isArray(content) ? content : [];
15
+ const text = String(rowText ?? '');
16
+ const textRefs = parts.filter(
17
+ (part) =>
18
+ part?.type === 'text' && isAttachmentReference(part) && (part.textKind === 'paste' || part.textKind === 'file')
19
+ );
20
+ const imageRefs = parts.filter((part) => part?.type === 'image' && isAttachmentReference(part));
21
+
22
+ // A `[File #n: name]` token is also spelled for binary files (PDF/Office),
23
+ // which never became a text reference: a token takes the next text reference
24
+ // only when it is the same kind (and, for files, the same name).
25
+ const pastedTexts = {};
26
+ let next = 0;
27
+ for (const match of text.matchAll(TEXT_TOKEN_RE)) {
28
+ const part = textRefs[next];
29
+ if (!part) break;
30
+ const isFile = match[1] === 'File';
31
+ if (isFile ? part.textKind !== 'file' || match[3] !== part.filename : part.textKind !== 'paste') continue;
32
+ next += 1;
33
+ const id = Number(match[2]);
34
+ if (!id || pastedTexts[id]) continue;
35
+ pastedTexts[id] = {
36
+ id,
37
+ source: part.textKind === 'file' ? 'file' : 'paste',
38
+ attachmentRef: part.attachmentRef,
39
+ sizeBytes: part.sizeBytes,
40
+ ...(part.filename ? { filename: part.filename } : {}),
41
+ ...(part.mimeType ? { mimeType: part.mimeType } : {}),
42
+ };
43
+ }
44
+
45
+ const imageIds = [...text.matchAll(IMAGE_TOKEN_RE)].map((match) => Number(match[1]));
46
+ const usedIds = new Set([...Object.keys(pastedTexts).map(Number), ...imageIds]);
47
+ let nextId = 1;
48
+ const pastedImages = {};
49
+ imageRefs.forEach((part, index) => {
50
+ let id = Number(part.pasteId) || imageIds[index] || 0;
51
+ if (!id || pastedImages[id]) {
52
+ while (usedIds.has(nextId)) nextId += 1;
53
+ id = nextId;
54
+ usedIds.add(id);
55
+ }
56
+ pastedImages[id] = {
57
+ id,
58
+ type: 'image',
59
+ mediaType: String(part.mimeType || part.mediaType || 'image/png'),
60
+ attachmentRef: part.attachmentRef,
61
+ sizeBytes: part.sizeBytes,
62
+ ...(part.filename ? { filename: part.filename } : {}),
63
+ };
64
+ });
65
+
66
+ return {
67
+ pastedTexts: Object.keys(pastedTexts).length ? pastedTexts : null,
68
+ pastedImages: Object.keys(pastedImages).length ? pastedImages : null,
69
+ };
70
+ }
@@ -6,7 +6,7 @@
6
6
  * snapshots carry only a sha256 reference plus byte-free metadata.
7
7
  * Provider lowering resolves the reference at the last possible boundary.
8
8
  */
9
- import { createHash, randomBytes } from 'node:crypto';
9
+ import { createHash } from 'node:crypto';
10
10
  import {
11
11
  closeSync,
12
12
  existsSync,
@@ -15,18 +15,25 @@ import {
15
15
  openSync,
16
16
  readFileSync,
17
17
  readSync,
18
- renameSync,
19
18
  statSync,
20
19
  unlinkSync,
21
20
  utimesSync,
22
21
  writeFileSync,
23
22
  } from 'node:fs';
24
- import { mkdir, readdir, readFile as readFileAsync, rename, stat, unlink, writeFile } from 'node:fs/promises';
23
+ import { readdir, readFile as readFileAsync, stat, unlink } from 'node:fs/promises';
25
24
  import { dirname, join } from 'node:path';
26
25
 
26
+ import { writeFileAtomicSync, writeJsonAtomicAsync } from '../shared/atomic-file.mjs';
27
27
  import { resolvePluginData } from '../shared/plugin-paths.mjs';
28
+ import {
29
+ convertLegacyOfficeBuffer,
30
+ isLegacyOfficeBuffer,
31
+ legacyOfficeLabelPackage,
32
+ OOXML_MIME_BY_PACKAGE,
33
+ ooxmlLabelPackage,
34
+ } from './legacy-office.mjs';
28
35
  import { MAX_ATTACHMENT_BLOB_BYTES, MAX_PDF_BYTES, MAX_PDF_PAGES, isPdfBuffer } from './limits.mjs';
29
- import { inspectPdfBuffer } from './pdf-extract.mjs';
36
+ import { inspectPdfBuffer, PDF_INTAKE_TIMEOUT_MS } from './pdf-extract.mjs';
30
37
 
31
38
  const ATTACHMENT_REF_RE = /^[a-f0-9]{64}$/;
32
39
  const TEXT_TOKEN_RE = /\[(?:Pasted text|File) #(\d+)[^\]\r\n]*\]/g;
@@ -140,17 +147,13 @@ function saveBuffer(buffer) {
140
147
  }
141
148
  if (!existingValid) {
142
149
  mkdirSync(join(attachmentsDir(), attachmentRef.slice(0, 2)), { recursive: true, mode: 0o700 });
143
- const temp = `${target}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
144
150
  try {
145
- writeFileSync(temp, buffer, { mode: 0o600 });
146
- renameSync(temp, target);
151
+ // Durable, so a crash cannot leave this content address holding NUL
152
+ // bytes (a read would then fail its digest check).
153
+ writeFileAtomicSync(target, buffer, { mode: 0o600 });
147
154
  } catch (error) {
148
155
  // A concurrent writer may have published the same content first.
149
156
  if (!storedMatches()) throw error;
150
- } finally {
151
- try {
152
- unlinkSync(temp);
153
- } catch {}
154
157
  }
155
158
  } else {
156
159
  // Reusing old content creates a fresh reference before its queue/session
@@ -172,10 +175,23 @@ export function storeAttachmentBuffer(buffer) {
172
175
  return saveBuffer(buffer);
173
176
  }
174
177
 
178
+ /** Where the store keeps the bytes of `ref` on this machine (no extension:
179
+ * the read tool recognizes PDFs and images there by their content). */
180
+ export function attachmentFilePath(ref) {
181
+ return attachmentPath(ref);
182
+ }
183
+
175
184
  export function isAttachmentReference(value) {
176
185
  return Boolean(value && typeof value === 'object' && ATTACHMENT_REF_RE.test(String(value.attachmentRef || '')));
177
186
  }
178
187
 
188
+ // The bytes of a base64 string, or null when they are empty or would not
189
+ // re-encode to the same string (a stored reference could not replay it exactly).
190
+ function canonicalBase64Buffer(data) {
191
+ const buffer = Buffer.from(data, 'base64');
192
+ return buffer.length === 0 || buffer.toString('base64') !== data ? null : buffer;
193
+ }
194
+
179
195
  /**
180
196
  * Stored-history form of an inline base64 image part (`{ type:'image', data }`
181
197
  * or `{ type:'image', source:{ type:'base64' } }`): the same part as a
@@ -200,8 +216,8 @@ export function storeInlineImagePart(part) {
200
216
  } else {
201
217
  return null;
202
218
  }
203
- const buffer = Buffer.from(data, 'base64');
204
- if (buffer.length === 0 || buffer.toString('base64') !== data) return null;
219
+ const buffer = canonicalBase64Buffer(data);
220
+ if (!buffer) return null;
205
221
  return { ...metadata, ...saveBuffer(buffer) };
206
222
  }
207
223
 
@@ -235,8 +251,8 @@ export function storeInlineDocumentPart(part) {
235
251
  } else {
236
252
  return null;
237
253
  }
238
- const buffer = Buffer.from(data, 'base64');
239
- if (buffer.length === 0 || buffer.toString('base64') !== data) return null;
254
+ const buffer = canonicalBase64Buffer(data);
255
+ if (!buffer) return null;
240
256
  // The magic header decides the kind before the label does (inlineFileKind),
241
257
  // so a mislabelled PDF is stored under its real type: the reloaded part then
242
258
  // estimates and lowers exactly like the live one.
@@ -380,7 +396,7 @@ function materializePastedTexts(pastedTexts) {
380
396
  type: 'text',
381
397
  attachmentRef: raw.attachmentRef,
382
398
  sizeBytes: attachmentBlobSize(raw, raw.sizeBytes),
383
- ...(raw.source === 'file' ? { textKind: 'file' } : {}),
399
+ textKind: raw.source === 'file' ? 'file' : 'paste',
384
400
  ...(raw.filename ? { filename: raw.filename } : {}),
385
401
  ...(raw.mimeType ? { mimeType: raw.mimeType } : {}),
386
402
  };
@@ -621,6 +637,50 @@ export function providerTakesNativePdf(provider) {
621
637
  return provider?.nativePdf !== false;
622
638
  }
623
639
 
640
+ /**
641
+ * A legacy Office attachment (.doc/.xls/.ppt: by label, or an OLE file renamed
642
+ * to an OOXML name) as the OOXML package LibreOffice saves it as, stored under
643
+ * its own reference, so text extraction and every later lowering see an
644
+ * ordinary package. A conversion that cannot run (LibreOffice missing, damaged
645
+ * file, or bytes that are not a legacy file under a legacy label) becomes a text
646
+ * part carrying the converter's message, so the model and the user are told
647
+ * instead of the file vanishing. A cancellation (`signal`) is rethrown, never
648
+ * turned into text. Any other part is returned as is.
649
+ */
650
+ async function packageLegacyOfficePart(part, { convertLegacy, signal }) {
651
+ if (part?.type !== 'file' || !isAttachmentReference(part)) return part;
652
+ const mimeType = part.mimeType || part.mediaType;
653
+ const legacyKind = legacyOfficeLabelPackage(mimeType, part.filename);
654
+ const kind = legacyKind || ooxmlLabelPackage(mimeType, part.filename);
655
+ if (!kind) return part;
656
+ const buffer = readAttachmentBuffer(part);
657
+ // An OOXML-labelled part converts only when its bytes are legacy; a legacy
658
+ // label is always converted (or explained).
659
+ if (!legacyKind && !isLegacyOfficeBuffer(buffer)) return part;
660
+ const name = String(part.filename || '')
661
+ .split(/[\\/]/)
662
+ .pop();
663
+ try {
664
+ const converted = await convertLegacyOfficeBuffer(buffer, part.filename, kind, {
665
+ signal,
666
+ ...(convertLegacy ? { convert: convertLegacy } : {}),
667
+ });
668
+ const { mediaType: _mediaType, ...rest } = part;
669
+ return {
670
+ ...rest,
671
+ ...saveBuffer(converted.buffer),
672
+ mimeType: OOXML_MIME_BY_PACKAGE[kind],
673
+ filename: converted.filename,
674
+ };
675
+ } catch (error) {
676
+ if (signal?.aborted) throw error;
677
+ return {
678
+ type: 'text',
679
+ text: `--- ${name || 'attachment'} ---\n${error instanceof Error ? error.message : String(error)}`,
680
+ };
681
+ }
682
+ }
683
+
624
684
  /**
625
685
  * PDF intake validation. Every PDF keeps its content-addressed reference — the
626
686
  * one conversion to text happens at provider lowering — so this only checks what
@@ -629,13 +689,19 @@ export function providerTakesNativePdf(provider) {
629
689
  * PDF as text reads its first MAX_PDF_PAGES pages, so a longer PDF is accepted.
630
690
  * The page count is recorded on the part for estimates.
631
691
  */
632
- export async function preparePromptSubmissionForProvider(intake, provider) {
692
+ export async function preparePromptSubmissionForProvider(
693
+ intake,
694
+ provider,
695
+ { convertLegacy = undefined, signal = null } = {}
696
+ ) {
633
697
  const prompt = intake?.prompt;
634
698
  if (!Array.isArray(prompt)) return intake;
635
699
  const nativePdf = providerTakesNativePdf(provider);
636
700
  let changed = false;
637
701
  const prepared = [];
638
- for (const part of prompt) {
702
+ for (const candidate of prompt) {
703
+ const part = await packageLegacyOfficePart(candidate, { convertLegacy, signal });
704
+ if (part !== candidate) changed = true;
639
705
  const mimeType = String(part?.mimeType || part?.mediaType || '').toLowerCase();
640
706
  if (part?.type !== 'file' || mimeType !== 'application/pdf' || !isAttachmentReference(part)) {
641
707
  prepared.push(part);
@@ -645,11 +711,21 @@ export async function preparePromptSubmissionForProvider(intake, provider) {
645
711
  if (bytes > MAX_PDF_BYTES) {
646
712
  throw new RangeError(`PDF exceeds the ${MAX_PDF_BYTES / 1024 / 1024} MiB input limit`);
647
713
  }
714
+ // The text-layer counts price the PDF (a scanned page costs its page image,
715
+ // a text page its text as well) and tell the model, when the PDF is sent
716
+ // as a reference, which pages read as text.
648
717
  const inspected = await inspectPdfBuffer(readAttachmentBuffer(part), {
649
718
  maxPages: nativePdf ? MAX_PDF_PAGES : Infinity,
719
+ measureText: true,
720
+ timeoutMs: PDF_INTAKE_TIMEOUT_MS,
650
721
  });
651
722
  changed = true;
652
- prepared.push({ ...part, pageCount: inspected.pageCount });
723
+ prepared.push({
724
+ ...part,
725
+ pageCount: inspected.pageCount,
726
+ textPages: inspected.textPages,
727
+ textChars: inspected.textChars,
728
+ });
653
729
  }
654
730
  if (!changed) return intake;
655
731
  return { ...intake, prompt: prepared };
@@ -739,8 +815,8 @@ function dropCachedAttachment(path) {
739
815
  async function persistedAttachmentReferencePaths() {
740
816
  const root = resolvePluginData();
741
817
  const paths = [];
742
- for (const dirname of ['sessions', 'turn-checkpoints', 'session-pending']) {
743
- const dir = join(root, dirname);
818
+ for (const subdir of ['sessions', 'turn-checkpoints', 'session-pending']) {
819
+ const dir = join(root, subdir);
744
820
  let entries;
745
821
  try {
746
822
  entries = await readdir(dir, { withFileTypes: true });
@@ -752,7 +828,7 @@ async function persistedAttachmentReferencePaths() {
752
828
  if (!entry.isFile()) continue;
753
829
  // The turn-checkpoint journal appends `<session>.jsonl` records that
754
830
  // carry attachment refs of in-flight turns.
755
- if (entry.name.endsWith('.json') || (dirname === 'turn-checkpoints' && entry.name.endsWith('.jsonl'))) {
831
+ if (entry.name.endsWith('.json') || (subdir === 'turn-checkpoints' && entry.name.endsWith('.jsonl'))) {
756
832
  paths.push(join(dir, entry.name));
757
833
  }
758
834
  }
@@ -819,16 +895,14 @@ async function loadReferenceCache(root) {
819
895
  async function saveReferenceCache(root, files) {
820
896
  referenceCache = { root, files };
821
897
  const target = attachmentReferenceCachePath();
822
- const temp = `${target}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
823
898
  try {
824
- await mkdir(dirname(target), { recursive: true });
825
- await writeFile(temp, JSON.stringify({ version: 1, files: Object.fromEntries(files) }));
826
- await rename(temp, target);
827
- } catch {
828
- try {
829
- await unlink(temp);
830
- } catch {}
831
- }
899
+ // An unusable cache only costs a full re-read (loadReferenceCache), so no fsync.
900
+ await writeJsonAtomicAsync(
901
+ target,
902
+ { version: 1, files: Object.fromEntries(files) },
903
+ { compact: true, fsync: false }
904
+ );
905
+ } catch {}
832
906
  }
833
907
 
834
908
  export async function collectPromptAttachments({ now = Date.now(), minAgeMs = ATTACHMENT_GC_MIN_AGE_MS } = {}) {
@@ -885,8 +959,10 @@ export async function collectPromptAttachments({ now = Date.now(), minAgeMs = AT
885
959
  continue;
886
960
  }
887
961
  for (const entry of entries) {
888
- // A writer that died between create and rename leaves `<ref>.<pid>.<rand>.tmp`.
889
- if (entry.isFile() && /^[a-f0-9]{64}\..+\.tmp$/.test(entry.name)) {
962
+ // A writer that died between create and rename leaves
963
+ // `<ref>.mixdog-tmp-<rand>` (atomic writer), or the older
964
+ // `.<ref>.<rand>.tmp` / `<ref>.<pid>.<rand>.tmp`.
965
+ if (entry.isFile() && /^(?:\.?[a-f0-9]{64}\..+\.tmp|[a-f0-9]{64}\.mixdog-tmp-[0-9a-f]+)$/.test(entry.name)) {
890
966
  const tempPath = join(dir, entry.name);
891
967
  try {
892
968
  if ((await stat(tempPath)).mtimeMs <= cutoff) await unlink(tempPath);