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
@@ -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
+ }
@@ -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,18 @@ 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
28
  import { MAX_ATTACHMENT_BLOB_BYTES, MAX_PDF_BYTES, MAX_PDF_PAGES, isPdfBuffer } from './limits.mjs';
29
- import { inspectPdfBuffer } from './pdf-extract.mjs';
29
+ import { inspectPdfBuffer, PDF_INTAKE_TIMEOUT_MS } from './pdf-extract.mjs';
30
30
 
31
31
  const ATTACHMENT_REF_RE = /^[a-f0-9]{64}$/;
32
32
  const TEXT_TOKEN_RE = /\[(?:Pasted text|File) #(\d+)[^\]\r\n]*\]/g;
@@ -140,17 +140,13 @@ function saveBuffer(buffer) {
140
140
  }
141
141
  if (!existingValid) {
142
142
  mkdirSync(join(attachmentsDir(), attachmentRef.slice(0, 2)), { recursive: true, mode: 0o700 });
143
- const temp = `${target}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
144
143
  try {
145
- writeFileSync(temp, buffer, { mode: 0o600 });
146
- renameSync(temp, target);
144
+ // Durable, so a crash cannot leave this content address holding NUL
145
+ // bytes (a read would then fail its digest check).
146
+ writeFileAtomicSync(target, buffer, { mode: 0o600 });
147
147
  } catch (error) {
148
148
  // A concurrent writer may have published the same content first.
149
149
  if (!storedMatches()) throw error;
150
- } finally {
151
- try {
152
- unlinkSync(temp);
153
- } catch {}
154
150
  }
155
151
  } else {
156
152
  // Reusing old content creates a fresh reference before its queue/session
@@ -172,10 +168,23 @@ export function storeAttachmentBuffer(buffer) {
172
168
  return saveBuffer(buffer);
173
169
  }
174
170
 
171
+ /** Where the store keeps the bytes of `ref` on this machine (no extension:
172
+ * the read tool recognizes PDFs and images there by their content). */
173
+ export function attachmentFilePath(ref) {
174
+ return attachmentPath(ref);
175
+ }
176
+
175
177
  export function isAttachmentReference(value) {
176
178
  return Boolean(value && typeof value === 'object' && ATTACHMENT_REF_RE.test(String(value.attachmentRef || '')));
177
179
  }
178
180
 
181
+ // The bytes of a base64 string, or null when they are empty or would not
182
+ // re-encode to the same string (a stored reference could not replay it exactly).
183
+ function canonicalBase64Buffer(data) {
184
+ const buffer = Buffer.from(data, 'base64');
185
+ return buffer.length === 0 || buffer.toString('base64') !== data ? null : buffer;
186
+ }
187
+
179
188
  /**
180
189
  * Stored-history form of an inline base64 image part (`{ type:'image', data }`
181
190
  * or `{ type:'image', source:{ type:'base64' } }`): the same part as a
@@ -200,8 +209,8 @@ export function storeInlineImagePart(part) {
200
209
  } else {
201
210
  return null;
202
211
  }
203
- const buffer = Buffer.from(data, 'base64');
204
- if (buffer.length === 0 || buffer.toString('base64') !== data) return null;
212
+ const buffer = canonicalBase64Buffer(data);
213
+ if (!buffer) return null;
205
214
  return { ...metadata, ...saveBuffer(buffer) };
206
215
  }
207
216
 
@@ -235,8 +244,8 @@ export function storeInlineDocumentPart(part) {
235
244
  } else {
236
245
  return null;
237
246
  }
238
- const buffer = Buffer.from(data, 'base64');
239
- if (buffer.length === 0 || buffer.toString('base64') !== data) return null;
247
+ const buffer = canonicalBase64Buffer(data);
248
+ if (!buffer) return null;
240
249
  // The magic header decides the kind before the label does (inlineFileKind),
241
250
  // so a mislabelled PDF is stored under its real type: the reloaded part then
242
251
  // estimates and lowers exactly like the live one.
@@ -645,11 +654,21 @@ export async function preparePromptSubmissionForProvider(intake, provider) {
645
654
  if (bytes > MAX_PDF_BYTES) {
646
655
  throw new RangeError(`PDF exceeds the ${MAX_PDF_BYTES / 1024 / 1024} MiB input limit`);
647
656
  }
657
+ // The text-layer counts price the PDF (a scanned page costs its page image,
658
+ // a text page its text as well) and tell the model, when the PDF is sent
659
+ // as a reference, which pages read as text.
648
660
  const inspected = await inspectPdfBuffer(readAttachmentBuffer(part), {
649
661
  maxPages: nativePdf ? MAX_PDF_PAGES : Infinity,
662
+ measureText: true,
663
+ timeoutMs: PDF_INTAKE_TIMEOUT_MS,
650
664
  });
651
665
  changed = true;
652
- prepared.push({ ...part, pageCount: inspected.pageCount });
666
+ prepared.push({
667
+ ...part,
668
+ pageCount: inspected.pageCount,
669
+ textPages: inspected.textPages,
670
+ textChars: inspected.textChars,
671
+ });
653
672
  }
654
673
  if (!changed) return intake;
655
674
  return { ...intake, prompt: prepared };
@@ -739,8 +758,8 @@ function dropCachedAttachment(path) {
739
758
  async function persistedAttachmentReferencePaths() {
740
759
  const root = resolvePluginData();
741
760
  const paths = [];
742
- for (const dirname of ['sessions', 'turn-checkpoints', 'session-pending']) {
743
- const dir = join(root, dirname);
761
+ for (const subdir of ['sessions', 'turn-checkpoints', 'session-pending']) {
762
+ const dir = join(root, subdir);
744
763
  let entries;
745
764
  try {
746
765
  entries = await readdir(dir, { withFileTypes: true });
@@ -752,7 +771,7 @@ async function persistedAttachmentReferencePaths() {
752
771
  if (!entry.isFile()) continue;
753
772
  // The turn-checkpoint journal appends `<session>.jsonl` records that
754
773
  // carry attachment refs of in-flight turns.
755
- if (entry.name.endsWith('.json') || (dirname === 'turn-checkpoints' && entry.name.endsWith('.jsonl'))) {
774
+ if (entry.name.endsWith('.json') || (subdir === 'turn-checkpoints' && entry.name.endsWith('.jsonl'))) {
756
775
  paths.push(join(dir, entry.name));
757
776
  }
758
777
  }
@@ -819,16 +838,14 @@ async function loadReferenceCache(root) {
819
838
  async function saveReferenceCache(root, files) {
820
839
  referenceCache = { root, files };
821
840
  const target = attachmentReferenceCachePath();
822
- const temp = `${target}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
823
841
  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
- }
842
+ // An unusable cache only costs a full re-read (loadReferenceCache), so no fsync.
843
+ await writeJsonAtomicAsync(
844
+ target,
845
+ { version: 1, files: Object.fromEntries(files) },
846
+ { compact: true, fsync: false }
847
+ );
848
+ } catch {}
832
849
  }
833
850
 
834
851
  export async function collectPromptAttachments({ now = Date.now(), minAgeMs = ATTACHMENT_GC_MIN_AGE_MS } = {}) {
@@ -885,8 +902,10 @@ export async function collectPromptAttachments({ now = Date.now(), minAgeMs = AT
885
902
  continue;
886
903
  }
887
904
  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)) {
905
+ // A writer that died between create and rename leaves
906
+ // `<ref>.mixdog-tmp-<rand>` (atomic writer), or the older
907
+ // `.<ref>.<rand>.tmp` / `<ref>.<pid>.<rand>.tmp`.
908
+ if (entry.isFile() && /^(?:\.?[a-f0-9]{64}\..+\.tmp|[a-f0-9]{64}\.mixdog-tmp-[0-9a-f]+)$/.test(entry.name)) {
890
909
  const tempPath = join(dir, entry.name);
891
910
  try {
892
911
  if ((await stat(tempPath)).mtimeMs <= cutoff) await unlink(tempPath);
@@ -14,8 +14,12 @@ import { base64ByteLength, inlineFileKind } from '../shared/inline-file-kind.mjs
14
14
  import { MAX_IMAGE_BYTES, SUPPORTED_IMAGE_MIME_TYPES } from '../attachments/limits.mjs';
15
15
 
16
16
  const DISCOVERY_FILE = 'browser-bridge.json';
17
- /** Ceiling above the bridge's own per-action timeouts (navigation settle,
18
- * surface auto-open), so the bridge's specific error wins over a bare abort. */
17
+ /** How long a request may go without a single byte from the bridge. Above the
18
+ * bridge's own per-action timeouts (navigation settle, surface auto-open), so
19
+ * the bridge's specific error wins over a bare abort. It is a silence limit,
20
+ * not a total: a command that waits on the person (an approval card, which has
21
+ * no limit) is kept alive by the bridge with a space every few seconds
22
+ * (apps/desktop/src/main/browser/bridge-server.ts), and each byte restarts it. */
19
23
  const REQUEST_TIMEOUT_MS = 45_000;
20
24
  const MAX_REQUEST_BYTES = 256 * 1024;
21
25
  const MAX_RESPONSE_BYTES = 150 * 1024 * 1024;
@@ -33,7 +37,7 @@ export async function finishBrowserTurn(sessionId, turnId, { aborted = false } =
33
37
  if (!discovery) return;
34
38
  turns.delete(turnId);
35
39
  if (!turns.size) browserTurns.delete(sessionId);
36
- const result = await requestBridge(
40
+ const result = await requestBridgeWithin(
37
41
  discovery,
38
42
  JSON.stringify({
39
43
  action: 'finish_turn',
@@ -43,7 +47,7 @@ export async function finishBrowserTurn(sessionId, turnId, { aborted = false } =
43
47
  // so its pages stay; only an aborted run reclaims them.
44
48
  aborted: aborted === true,
45
49
  }),
46
- AbortSignal.timeout(REQUEST_TIMEOUT_MS),
50
+ undefined,
47
51
  { sessionId, turnId, action: 'finish_turn' }
48
52
  );
49
53
  if (result.status !== 200 || result.body?.ok === false || result.body?.error) {
@@ -61,12 +65,10 @@ export async function browserSecretInput(sessionId, requestId, { signal } = {})
61
65
  const discovery = readBridgeDiscovery(DISCOVERY_FILE);
62
66
  if (!discovery || !sessionId || !requestId) return 'skipped';
63
67
  try {
64
- const result = await requestBridge(
68
+ const result = await requestBridgeWithin(
65
69
  discovery,
66
70
  JSON.stringify({ action: 'secret_input', session_id: String(sessionId), request_id: String(requestId) }),
67
- signal
68
- ? AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), signal])
69
- : AbortSignal.timeout(REQUEST_TIMEOUT_MS),
71
+ signal,
70
72
  { sessionId, action: 'secret_input' }
71
73
  );
72
74
  return result.status === 200 && result.body?.ok !== false && result.body?.value?.text === 'provided'
@@ -155,8 +157,9 @@ function unavailableMessage() {
155
157
  class BrowserBridgeResponseError extends Error {}
156
158
 
157
159
  // Read the body incrementally so a chunked response (no content-length) still
158
- // honours MAX_RESPONSE_BYTES.
159
- export async function readCappedJson(response, maximum = MAX_RESPONSE_BYTES) {
160
+ // honours MAX_RESPONSE_BYTES. `onChunk` hears every chunk read (the bridge's
161
+ // keep-alive spaces included).
162
+ export async function readCappedJson(response, maximum = MAX_RESPONSE_BYTES, onChunk = undefined) {
160
163
  if (!response.body) throw new Error('empty response');
161
164
  const reader = response.body.getReader();
162
165
  const chunks = [];
@@ -165,6 +168,7 @@ export async function readCappedJson(response, maximum = MAX_RESPONSE_BYTES) {
165
168
  for (;;) {
166
169
  const result = await reader.read();
167
170
  if (result.done) break;
171
+ onChunk?.();
168
172
  bytes += result.value.byteLength;
169
173
  if (bytes > maximum) {
170
174
  await reader.cancel().catch(() => {});
@@ -178,7 +182,50 @@ export async function readCappedJson(response, maximum = MAX_RESPONSE_BYTES) {
178
182
  return JSON.parse(Buffer.concat(chunks, bytes).toString('utf8'));
179
183
  }
180
184
 
181
- async function requestBridge(discovery, encodedPayload, signal, timingContext) {
185
+ /** One bridge request's clock: it times out after `timeoutMs` without a byte
186
+ * from the bridge (`touch` restarts it), with the same TimeoutError an
187
+ * AbortSignal.timeout gives, and ends with `callerSignal` (the caller's own
188
+ * reason). `stop` leaves no timer behind. */
189
+ export function bridgeDeadline(callerSignal, timeoutMs = REQUEST_TIMEOUT_MS) {
190
+ const controller = new AbortController();
191
+ let timer = null;
192
+ const stop = () => {
193
+ if (timer) clearTimeout(timer);
194
+ timer = null;
195
+ };
196
+ const touch = () => {
197
+ stop();
198
+ timer = setTimeout(
199
+ () => controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError')),
200
+ timeoutMs
201
+ );
202
+ timer.unref?.();
203
+ };
204
+ touch();
205
+ return {
206
+ signal: callerSignal ? AbortSignal.any([controller.signal, callerSignal]) : controller.signal,
207
+ touch,
208
+ stop,
209
+ };
210
+ }
211
+
212
+ /** `requestBridge` under the silence limit of `bridgeDeadline`. */
213
+ export async function requestBridgeWithin(
214
+ discovery,
215
+ encodedPayload,
216
+ callerSignal,
217
+ timingContext,
218
+ timeoutMs = REQUEST_TIMEOUT_MS
219
+ ) {
220
+ const deadline = bridgeDeadline(callerSignal, timeoutMs);
221
+ try {
222
+ return await requestBridge(discovery, encodedPayload, deadline.signal, timingContext, deadline.touch);
223
+ } finally {
224
+ deadline.stop();
225
+ }
226
+ }
227
+
228
+ async function requestBridge(discovery, encodedPayload, signal, timingContext, onActivity = undefined) {
182
229
  const started = performance.now();
183
230
  let body;
184
231
  let status;
@@ -192,6 +239,7 @@ async function requestBridge(discovery, encodedPayload, signal, timingContext) {
192
239
  body: encodedPayload,
193
240
  signal,
194
241
  });
242
+ onActivity?.();
195
243
  status = response.status;
196
244
  const contentLength = Number(response.headers.get('content-length'));
197
245
  if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
@@ -199,10 +247,12 @@ async function requestBridge(discovery, encodedPayload, signal, timingContext) {
199
247
  throw new BrowserBridgeResponseError(`browser bridge response exceeds ${MAX_RESPONSE_BYTES} bytes`);
200
248
  }
201
249
  try {
202
- body = await readCappedJson(response);
250
+ body = await readCappedJson(response, MAX_RESPONSE_BYTES, onActivity);
203
251
  return { body, status: response.status };
204
252
  } catch (error) {
205
253
  if (error instanceof BrowserBridgeResponseError) throw error;
254
+ // Silence past the clock, or the caller stopping, is not a bad response.
255
+ if (signal?.aborted) throw signal.reason ?? error;
206
256
  throw new BrowserBridgeResponseError(`browser bridge returned an invalid response (HTTP ${response.status})`);
207
257
  }
208
258
  } finally {
@@ -225,6 +275,12 @@ export async function executeBrowserTool(args, options = {}) {
225
275
  action: validated.action,
226
276
  ...validated.input,
227
277
  session_id: sessionId,
278
+ // Set by the runtime from the session, never from the model's arguments: a
279
+ // schedule/webhook run (or work it started) whose saved routine is the
280
+ // user's grant for submits, posts and uploads (human-gate/run-policy.mjs).
281
+ ...(options.unattendedRun === true
282
+ ? { unattended_run: true, unattended_agent: String(options.unattendedAgent || '') }
283
+ : {}),
228
284
  ...(Number.isFinite(Number(options.turnId)) && Number(options.turnId) > 0
229
285
  ? { turn_id: Math.trunc(Number(options.turnId)) }
230
286
  : {}),
@@ -243,14 +299,11 @@ export async function executeBrowserTool(args, options = {}) {
243
299
  let bridgeResult;
244
300
  for (let attempt = 0; attempt < 2; attempt += 1) {
245
301
  try {
246
- bridgeResult = await requestBridge(
247
- discovery,
248
- encodedPayload,
249
- options.signal
250
- ? AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), options.signal])
251
- : AbortSignal.timeout(REQUEST_TIMEOUT_MS),
252
- { sessionId, turnId: payload.turn_id, action: validated.action }
253
- );
302
+ bridgeResult = await requestBridgeWithin(discovery, encodedPayload, options.signal, {
303
+ sessionId,
304
+ turnId: payload.turn_id,
305
+ action: validated.action,
306
+ });
254
307
  break;
255
308
  } catch (error) {
256
309
  if (error?.name === 'TimeoutError') {
@@ -329,7 +329,7 @@ function cleanupStaleRuntimeFiles(now = Date.now()) {
329
329
  // tighter TTLs keep dead-process residue from accumulating.
330
330
  let ttl = RUNTIME_STALE_TTL;
331
331
  if (/^status-.*\.json$/.test(file)) ttl = STATUS_FILE_TTL;
332
- else if (/\.tmp$/.test(file)) ttl = TMP_FILE_TTL;
332
+ else if (/\.tmp$|\.mixdog-tmp-[0-9a-f]+$/.test(file)) ttl = TMP_FILE_TTL;
333
333
  if (age > ttl) removeFileIfExists(fullPath);
334
334
  } catch {}
335
335
  });
@@ -26,6 +26,19 @@ const SCHEDULE_LOG = join(DATA_DIR, 'schedule.log');
26
26
  // here. The channels worker owns shutdown sequencing (drain queues, persist
27
27
  // baselines, release the scheduler lock, etc.) and a library-level exit(0)
28
28
  // preempts that drain. The buffered appender drains pending log lines on exit.
29
+ function skipReasonText(outcome) {
30
+ switch (outcome.reason) {
31
+ case 'backoff':
32
+ return 'backing off after failures';
33
+ case 'paused':
34
+ return 'switched off';
35
+ case 'owner':
36
+ return `paused: ${outcome.message}`;
37
+ default:
38
+ return 'previous run still in progress';
39
+ }
40
+ }
41
+
29
42
  function logSchedule(msg) {
30
43
  msg = msg.replace(/\n$/, '');
31
44
  process.stderr.write(`mixdog scheduler: ${msg}\n`);
@@ -686,13 +699,7 @@ ${prompt}`;
686
699
  // already persisted the skip.
687
700
  if (outcome?.skipped) {
688
701
  schedule.lastSkippedAt = new Date().toISOString();
689
- const why =
690
- outcome.reason === 'backoff'
691
- ? 'backing off after failures'
692
- : outcome.reason === 'owner'
693
- ? `paused: ${outcome.message}`
694
- : 'previous run still in progress';
695
- logSchedule(`${schedule.name}: skipped — ${why}\n`);
702
+ logSchedule(`${schedule.name}: skipped — ${skipReasonText(outcome)}\n`);
696
703
  return false;
697
704
  }
698
705
  logSchedule(`${schedule.name} done\n`);
@@ -1,5 +1,5 @@
1
1
  import { mkdirSync, readFileSync, unlinkSync } from 'node:fs';
2
- import { writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
2
+ import { readJsonStateSync, updateJsonAtomicSync, writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
3
3
  function ensureDir(dirPath) {
4
4
  mkdirSync(dirPath, { recursive: true });
5
5
  }
@@ -19,10 +19,6 @@ function readJsonFile(filePath, fallback) {
19
19
  return fallback;
20
20
  }
21
21
  }
22
- function readJsonFileStrict(filePath) {
23
- const raw = readFileSync(filePath, 'utf8');
24
- return JSON.parse(raw);
25
- }
26
22
  function writeJsonFile(filePath, value) {
27
23
  writeJsonAtomicSync(filePath, value, { compact: true, lock: true, fsync: false, fsyncDir: false });
28
24
  }
@@ -38,19 +34,23 @@ class JsonStateFile {
38
34
  writeJsonFile(this.filePath, value);
39
35
  return value;
40
36
  }
37
+ // A damaged file (empty, NUL-filled after a crash) is kept aside as
38
+ // <file>.corrupt-<ts> and the file restarts from the fallback; it is never
39
+ // overwritten in place and never blocks an update. That recovery is why the
40
+ // writes below can skip fsync.
41
41
  ensure() {
42
- writeJsonFile(this.filePath, this.read());
42
+ writeJsonFile(this.filePath, readJsonStateSync(this.filePath, { fallback: this.fallback }));
43
43
  }
44
44
  update(mutator) {
45
- let draft;
46
- try {
47
- draft = readJsonFileStrict(this.filePath);
48
- } catch (err) {
49
- process.stderr.write(`[state-file] REFUSING update: parse error for ${this.filePath}: ${err.message}\n`);
50
- throw err;
51
- }
52
- mutator(draft);
53
- return this.write(draft);
45
+ return updateJsonAtomicSync(
46
+ this.filePath,
47
+ (current) => {
48
+ const draft = current ?? structuredClone(this.fallback);
49
+ mutator(draft);
50
+ return draft;
51
+ },
52
+ { compact: true, fsync: false, fsyncDir: false }
53
+ );
54
54
  }
55
55
  }
56
56
  export { JsonStateFile, ensureDir, readJsonFile, removeFileIfExists, writeJsonFile };