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
@@ -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 };
@@ -43,7 +43,8 @@ import { fileURLToPath } from 'node:url';
43
43
  import { pipeline } from 'node:stream/promises';
44
44
  import { spawnSync } from 'node:child_process';
45
45
  import { createGunzip } from 'node:zlib';
46
- import { renameWithRetrySync, writeFileAtomicSync } from '../../shared/atomic-file.mjs';
46
+ import { renameWithRetrySync, writeFileAtomicSync, writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
47
+ import { fileHasData } from '../../shared/file-has-data.mjs';
47
48
  import { streamResponseToFile } from '../../shared/bounded-download.mjs';
48
49
  import { platformEntryKey, sha256File, verifySha256File } from '../../shared/native-asset.mjs';
49
50
  import { windowsProgramRoots, windowsSystemRoot } from '../../agent/orchestrator/tools/builtin/windows-roots.mjs';
@@ -264,7 +265,7 @@ async function loadManifest(dataDir) {
264
265
  if (!res.ok) throw new Error(`[voice-runtime] manifest fetch failed: ${res.status} ${res.statusText}`);
265
266
  const manifest = await res.json();
266
267
  mkdirSync(join(dataDir, 'voice-runtime'), { recursive: true });
267
- writeFileSync(cachedPath, JSON.stringify(manifest, null, 2));
268
+ writeJsonAtomicSync(cachedPath, manifest);
268
269
  return manifest;
269
270
  }
270
271
 
@@ -439,7 +440,7 @@ function gcPublishedRuntimeSiblings(rootDir, prefix) {
439
440
  }
440
441
 
441
442
  async function extractGz(gzPath, destPath) {
442
- await pipeline(createReadStream(gzPath), createGunzip(), createWriteStream(destPath));
443
+ await pipeline(createReadStream(gzPath), createGunzip(), createWriteStream(destPath, { flush: true }));
443
444
  }
444
445
 
445
446
  // Sweep .staging-* partials left by killed/crashed install attempts.
@@ -541,13 +542,13 @@ export async function ensureWhisperRuntime(dataDir, onProgress = null) {
541
542
  const activeDir = join(rootDir, activeName);
542
543
  const whisperCmd = join(activeDir, variant.executable);
543
544
 
544
- if (existsSync(whisperCmd)) {
545
+ if (fileHasData(whisperCmd)) {
545
546
  gcStaleVersions(rootDir, activeName, 'whisper-');
546
547
  return { whisperCmd, version: ver, variantId: variant.id };
547
548
  }
548
549
 
549
550
  return _withInstallLock(rootDir, 'install', async () => {
550
- if (existsSync(whisperCmd)) {
551
+ if (fileHasData(whisperCmd)) {
551
552
  gcStaleVersions(rootDir, activeName, 'whisper-');
552
553
  return { whisperCmd, version: ver, variantId: variant.id };
553
554
  }
@@ -641,14 +642,14 @@ function resolveManagedWhisperCmd(dataDir) {
641
642
  const variant = variants.find((v) => v.id === variantId);
642
643
  if (variant?.executable) {
643
644
  const p = join(activeDir, variant.executable);
644
- if (existsSync(p)) return p;
645
+ if (fileHasData(p)) return p;
645
646
  }
646
647
  }
647
648
  } catch {}
648
649
  }
649
650
  for (const c of ['Release/whisper-cli.exe', 'Release/whisper-cli']) {
650
651
  const p = join(activeDir, c);
651
- if (existsSync(p)) return p;
652
+ if (fileHasData(p)) return p;
652
653
  }
653
654
  return null;
654
655
  }
@@ -659,7 +660,7 @@ function resolveManagedWhisperModelById(dataDir, modelId = 'standard') {
659
660
  if (!entry?.filename) return null;
660
661
  const modelDir = join(dataDir, 'voice', 'models');
661
662
  const p = join(modelDir, entry.filename);
662
- return existsSync(p) ? p : null;
663
+ return fileHasData(p) ? p : null;
663
664
  }
664
665
 
665
666
  // Clean-install policy: the models dir holds ONLY filenames declared by the
@@ -712,13 +713,13 @@ export async function ensureFfmpegRuntime(dataDir, onProgress = null) {
712
713
  const activeDir = join(rootDir, activeName);
713
714
  const ffmpegPath = join(activeDir, platformEntry.executable);
714
715
 
715
- if (existsSync(ffmpegPath)) {
716
+ if (fileHasData(ffmpegPath)) {
716
717
  gcStaleVersions(rootDir, activeName, 'ffmpeg-');
717
718
  return { ffmpegPath, version: ver };
718
719
  }
719
720
 
720
721
  return _withInstallLock(rootDir, 'install', async () => {
721
- if (existsSync(ffmpegPath)) {
722
+ if (fileHasData(ffmpegPath)) {
722
723
  gcStaleVersions(rootDir, activeName, 'ffmpeg-');
723
724
  return { ffmpegPath, version: ver };
724
725
  }
@@ -783,13 +784,13 @@ function resolveManagedFfmpegPath(dataDir) {
783
784
  const platformEntry = manifest.ffmpeg?.platforms?.[key];
784
785
  if (platformEntry?.executable) {
785
786
  const p = join(activeDir, platformEntry.executable);
786
- if (existsSync(p)) return p;
787
+ if (fileHasData(p)) return p;
787
788
  }
788
789
  } catch {}
789
790
  }
790
791
  for (const c of ['ffmpeg.exe', 'ffmpeg']) {
791
792
  const p = join(activeDir, c);
792
- if (existsSync(p)) return p;
793
+ if (fileHasData(p)) return p;
793
794
  }
794
795
  return null;
795
796
  }
@@ -835,7 +836,7 @@ export function resolveVoiceRuntime(dataDir, { modelId = 'standard', cleanup = t
835
836
  const managedFfmpegPath = resolveManagedFfmpegPath(dataDir);
836
837
  const ext = process.platform === 'win32' ? '.exe' : '';
837
838
  const managedServerCmd = managedWhisperCmd ? join(dirname(managedWhisperCmd), `whisper-server${ext}`) : null;
838
- const serverCmd = managedServerCmd && existsSync(managedServerCmd) ? managedServerCmd : null;
839
+ const serverCmd = managedServerCmd && fileHasData(managedServerCmd) ? managedServerCmd : null;
839
840
  return {
840
841
  kind: 'managed-whisper.cpp',
841
842
  label: 'whisper.cpp',
@@ -8,8 +8,9 @@
8
8
  // verification stays local — the relay never inspects payloads. Works out
9
9
  // of the box: no binary, no authtoken, no reserved domain.
10
10
  import { randomBytes, randomUUID } from 'node:crypto';
11
- import { readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
11
+ import { readFileSync, chmodSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
+ import { readJsonStateSync, writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
13
14
  import { DATA_DIR } from '../config.mjs';
14
15
  import { logWebhook } from './log.mjs';
15
16
  import { MAX_HOOK_FRAME_BYTES, MAX_HOOK_HEADER_BYTES, MAX_TUNNEL_BODY_BYTES } from './relay-tunnel/limits.mjs';
@@ -55,19 +56,28 @@ function hookIdentityPath() {
55
56
  return join(DATA_DIR, 'relay-hook-device.json');
56
57
  }
57
58
 
59
+ function validHookIdentity(parsed) {
60
+ if (
61
+ typeof parsed?.deviceId !== 'string' ||
62
+ !/^[0-9a-f-]{8,64}$/.test(parsed.deviceId) ||
63
+ typeof parsed.deviceSecret !== 'string' ||
64
+ parsed.deviceSecret.length < 16
65
+ ) {
66
+ throw new Error('invalid hook tunnel identity');
67
+ }
68
+ return { deviceId: parsed.deviceId, deviceSecret: parsed.deviceSecret };
69
+ }
70
+
58
71
  // Stable per-install identity (trust-on-first-use at the relay, mirroring
59
72
  // the desktop leg). The secret never leaves this machine except toward the
60
73
  // relay; the deviceId doubles as the public URL path segment.
61
74
  function loadOrCreateHookIdentity() {
62
75
  const path = hookIdentityPath();
63
76
  try {
64
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
65
- if (
66
- typeof parsed.deviceId === 'string' &&
67
- /^[0-9a-f-]{8,64}$/.test(parsed.deviceId) &&
68
- typeof parsed.deviceSecret === 'string' &&
69
- parsed.deviceSecret.length >= 16
70
- ) {
77
+ // A damaged identity (empty, NUL-filled, malformed) is kept aside and the
78
+ // last good copy restored; only when there is none is a new identity made.
79
+ const stored = readJsonStateSync(path, { validate: validHookIdentity, secret: true, fallback: null });
80
+ if (stored) {
71
81
  // The secret authenticates the webhook tunnel leg (whoever holds it can
72
82
  // take over inbound deliveries), so clamp legacy world-readable files.
73
83
  try {
@@ -75,20 +85,14 @@ function loadOrCreateHookIdentity() {
75
85
  } catch {
76
86
  /* windows/fs quirk */
77
87
  }
78
- return { deviceId: parsed.deviceId, deviceSecret: parsed.deviceSecret };
88
+ return stored;
79
89
  }
80
- } catch {
81
- /* first run */
90
+ } catch (err) {
91
+ logWebhook(`hook tunnel: identity read failed — ${err?.message || err}`);
82
92
  }
83
93
  const identity = { deviceId: randomUUID(), deviceSecret: randomBytes(24).toString('hex') };
84
94
  try {
85
- mkdirSync(DATA_DIR, { recursive: true });
86
- writeFileSync(path, JSON.stringify(identity, null, 2), { encoding: 'utf8', mode: 0o600 });
87
- try {
88
- chmodSync(path, 0o600);
89
- } catch {
90
- /* windows/fs quirk */
91
- }
95
+ writeJsonAtomicSync(path, identity, { secret: true, backup: true });
92
96
  } catch (err) {
93
97
  logWebhook(`hook tunnel: identity persist failed — ${err?.message || err}`);
94
98
  }
@@ -2,8 +2,6 @@ import { logWebhook } from './log.mjs';
2
2
  import { recordEndpointEvent, updateDeliveryStatus } from '../webhooks-db.mjs';
3
3
  import { UNTRUSTED_MARKER, scrubFenceMarker } from './untrusted-fence.mjs';
4
4
 
5
- const DISPATCH_TIMEOUT_MS = 10 * 60 * 1000;
6
-
7
5
  /** The headers a run's prompt shows (and a stored event keeps). */
8
6
  function promptHeaders(headers) {
9
7
  return Object.fromEntries(Object.entries(headers || {}).filter(([k]) => k.startsWith('x-') || k === 'content-type'));
@@ -47,33 +45,16 @@ function webhookEventSummary(headers, body) {
47
45
  return [type, field(body?.action)].filter(Boolean).join(' · ').slice(0, 120);
48
46
  }
49
47
 
50
- function trackDispatch({ name, deliveryId, dispatchP, controller }) {
48
+ function trackDispatch({ name, deliveryId, dispatchP, updateDeliveryStatus }) {
51
49
  let settled = false;
52
- const timeoutHandle = setTimeout(
53
- () => controller.abort(new Error(`bridge dispatch timed out after ${DISPATCH_TIMEOUT_MS}ms`)),
54
- DISPATCH_TIMEOUT_MS
55
- );
56
50
  const finish = (status, fields, message) => {
57
51
  if (settled) return;
58
52
  settled = true;
59
- clearTimeout(timeoutHandle);
60
53
  void updateDeliveryStatus(name, deliveryId, status, fields).catch((e) =>
61
54
  logWebhook(`${name}: delivery status update failed: ${e?.message || e}`)
62
55
  );
63
56
  logWebhook(message);
64
57
  };
65
- controller.signal.addEventListener(
66
- 'abort',
67
- () => {
68
- const reason = controller.signal.reason;
69
- finish(
70
- 'failed',
71
- { error: String(reason?.message || reason) },
72
- `${name}: webhook session run aborted: ${reason?.message || reason}`
73
- );
74
- },
75
- { once: true }
76
- );
77
58
  dispatchP.then(
78
59
  (run) =>
79
60
  run?.skipped
@@ -88,23 +69,20 @@ function trackDispatch({ name, deliveryId, dispatchP, controller }) {
88
69
  );
89
70
  }
90
71
 
91
- function createWebhookSessionDispatcher({ getBridgeDispatch }) {
72
+ // `store` is the delivery record (injectable for tests).
73
+ function createWebhookSessionDispatcher({ getBridgeDispatch, store = { recordEndpointEvent, updateDeliveryStatus } }) {
74
+ const { recordEndpointEvent: recordEvent, updateDeliveryStatus: setStatus } = store;
92
75
  async function dispatchSessionRun(name, fullPrompt, deliveryId, res, extra = {}) {
93
- await updateDeliveryStatus(name, deliveryId, 'processing');
94
- // Session dispatch must not be allowed to hang forever — without a
95
- // ceiling a stuck LLM call leaves the delivery in `processing`
96
- // for the lifetime of the process and dedup keeps re-running
97
- // forever. 10 minutes covers the slowest handler run we ship.
98
- // Racing a bare timer against the dispatch promise reported failure while
99
- // the run itself kept going: the delivery row was already `failed` when the
100
- // late result arrived and flipped it again. The timeout now aborts the
101
- // dispatch through a signal the dispatcher receives, and the first outcome
102
- // to land is the only one that writes a terminal status.
103
- const controller = new AbortController();
76
+ await setStatus(name, deliveryId, 'processing');
77
+ // The HTTP caller is answered 202 at once; the run is a turn in the bot's
78
+ // chat and has no deadline (a run may wait on the user's approval for as
79
+ // long as it takes), so nothing here cancels it. The delivery stays
80
+ // `processing` until the run ends, then `done` / `skipped` / `failed`; a
81
+ // second delivery meanwhile is skipped by the run's own overlap rule.
104
82
  const dispatchP = Promise.resolve(
105
83
  getBridgeDispatch()({
106
84
  prompt: fullPrompt,
107
- signal: controller.signal,
85
+ signal: null,
108
86
  // The endpoint's project folder, named to the bot in the turn.
109
87
  cwd: extra.cwd || null,
110
88
  attachments: extra.attachments || null,
@@ -123,7 +101,7 @@ function createWebhookSessionDispatcher({ getBridgeDispatch }) {
123
101
  },
124
102
  })
125
103
  );
126
- trackDispatch({ name, deliveryId, dispatchP, controller });
104
+ trackDispatch({ name, deliveryId, dispatchP, updateDeliveryStatus: setStatus });
127
105
  res.writeHead(202, { 'Content-Type': 'application/json' });
128
106
  res.end(JSON.stringify({ status: 'accepted', handler: 'session', id: deliveryId }));
129
107
  }
@@ -135,7 +113,7 @@ function createWebhookSessionDispatcher({ getBridgeDispatch }) {
135
113
  if (!getBridgeDispatch()) throw new Error(`[webhook] session dispatch requires bridgeDispatch`);
136
114
  // Kept for the app's "Run now", which re-runs the latest event. Losing
137
115
  // the record never blocks the delivery itself.
138
- await recordEndpointEvent(name, { body, headers: promptHeaders(headers) }).catch((e) =>
116
+ await recordEvent(name, { body, headers: promptHeaders(headers) }).catch((e) =>
139
117
  logWebhook(`${name}: event record failed: ${e?.message || e}`)
140
118
  );
141
119
  await dispatchSessionRun(name, run.prompt, deliveryId, res, {
@@ -148,7 +126,7 @@ function createWebhookSessionDispatcher({ getBridgeDispatch }) {
148
126
  });
149
127
  return true;
150
128
  } catch (err) {
151
- await updateDeliveryStatus(name, deliveryId, 'failed', { error: String(err?.message || err) });
129
+ await setStatus(name, deliveryId, 'failed', { error: String(err?.message || err) });
152
130
  logWebhook(`${name}: folder handler error: ${err}`);
153
131
  return false;
154
132
  }
@@ -10,6 +10,7 @@ import { Scheduler } from './scheduler.mjs';
10
10
  import { hasPending as dispatchHasPending } from '../../agent/orchestrator/dispatch-persist.mjs';
11
11
  import { stripSoftWarns } from '../../agent/orchestrator/tool-loop-guard.mjs';
12
12
  import { JsonStateFile } from './state-file.mjs';
13
+ import { writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
13
14
  import {
14
15
  makeInstanceId,
15
16
  getStatusPath,
@@ -115,7 +116,7 @@ function writeBridgeState(active) {
115
116
  try {
116
117
  const stateFile = path.join(ensurePrivateRuntimeRoot(resolveRuntimeRoot()), 'bridge-state.json');
117
118
  fs.mkdirSync(path.dirname(stateFile), { recursive: true });
118
- fs.writeFileSync(stateFile, JSON.stringify({ active, ts: Date.now() }));
119
+ writeJsonAtomicSync(stateFile, { active, ts: Date.now() }, { compact: true });
119
120
  } catch {}
120
121
  }
121
122
  function isChannelBridgeActive() {
@@ -225,7 +226,7 @@ function wireWebhookHandlers() {
225
226
  cwd: cwd || null,
226
227
  attachments: attachments || null,
227
228
  event: context?.event || '',
228
- // Dispatch-timeout cancellation only works if the signal reaches the run.
229
+ // The dispatch has no deadline; a caller-supplied signal still reaches the run.
229
230
  signal: signal || null,
230
231
  prompt,
231
232
  skillSelection,
@@ -7,8 +7,9 @@
7
7
  // arrays (~30 MB). The compact form is built once from tokenizer.json and
8
8
  // cached next to it as tokenizer.bin, keyed by the source file's size and
9
9
  // modification time.
10
- import { readFileSync, statSync, writeFileSync } from 'node:fs';
10
+ import { readFileSync, statSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
+ import { writeFileAtomicSync } from '../shared/atomic-file.mjs';
12
13
 
13
14
  const MAGIC = 0x4d585442; // "MXTB"
14
15
  const VERSION = 3;
@@ -93,6 +94,9 @@ function readCached(dir) {
93
94
  const h = new Int32Array(buf.buffer, buf.byteOffset, 7);
94
95
  const [size, mtime] = sourceStamp(dir);
95
96
  if (h[0] !== MAGIC || h[1] !== VERSION || h[2] !== size || h[3] !== mtime % 2 ** 31) return null;
97
+ // header (28) + byte table (1024) + char pairs + merges + meta; a shorter
98
+ // file is torn and is rebuilt.
99
+ if (buf.length < 1052 + h[4] * 8 + h[5] * 12 + h[6]) return null;
96
100
  return buf;
97
101
  } catch {
98
102
  return null;
@@ -104,7 +108,9 @@ export function loadCompactTokenizer(dir) {
104
108
  let buf = readCached(dir);
105
109
  if (!buf) {
106
110
  buf = build(dir);
107
- writeFileSync(join(dir, 'tokenizer.bin'), buf);
111
+ // Atomic: a torn cache (valid header, short body) would pass readCached's
112
+ // header check and then fail on every load.
113
+ writeFileAtomicSync(join(dir, 'tokenizer.bin'), buf);
108
114
  }
109
115
  // Copy into an aligned buffer so typed-array views are valid.
110
116
  const ab = new ArrayBuffer(buf.length);