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
@@ -17,10 +17,20 @@
17
17
  * re-drains every queue on start, and
18
18
  * registers the live surface so a thread open in a window shows the notice at
19
19
  * once; a cold thread picks it up from its stored record.
20
+ *
21
+ * A group-room member's task reports (enqueueTask `from` its member session)
22
+ * land in that member session alone, never as rows in the room's log. An
23
+ * ended turn is owed a wake like a home's, but the relay turn runs in the
24
+ * room: the desktop room host (agent-room-host.mjs `relay`) gives the member
25
+ * one turn there to relay the result, with the same batches and retries.
20
26
  */
21
27
  import { randomUUID } from 'node:crypto';
22
- import { readFile } from 'node:fs/promises';
28
+ import { readFileSync } from 'node:fs';
23
29
  import { join } from 'node:path';
30
+ import { readAgentDefinitionKind } from '../runtime/shared/agent-kind.mjs';
31
+ import { BOT_AGENT_KIND, WORKFLOW_AGENT_KIND, isContactAgentId } from '../runtime/shared/agent-route-config.mjs';
32
+ import { botStoreActive, isStoreBot } from '../runtime/shared/agent-store.mjs';
33
+ import { readMarkdownDocument } from '../runtime/shared/markdown-frontmatter.mjs';
24
34
  import {
25
35
  MIXY_AGENT_ID,
26
36
  agentHomeSessionId,
@@ -32,19 +42,23 @@ import {
32
42
  roomIdOfSession,
33
43
  roomMemberOfSession,
34
44
  } from '../runtime/agent/orchestrator/session/agent-room.mjs';
45
+ import { agentRoomHost } from '../runtime/shared/agent-room-host.mjs';
46
+ import { sendDepth } from '../runtime/shared/bot-hops.mjs';
35
47
  import {
36
48
  TASK_LOOP_CAP,
37
49
  agentTaskMessage,
50
+ agentTaskRelayFailedMessage,
38
51
  agentTaskWakePrompt,
52
+ boundedReply,
39
53
  routineOmittedMessage,
40
54
  routineResultMessage,
41
55
  routineResultNotice,
42
56
  routineRunNotice,
43
57
  routineSummary,
44
58
  } from '../runtime/agent/orchestrator/session/routine-result.mjs';
45
- import { parseJsonText, updateJsonAtomic } from '../runtime/shared/atomic-file.mjs';
59
+ import { readJsonState, updateJsonAtomic } from '../runtime/shared/atomic-file.mjs';
46
60
  import { ROUTINE_LIMITS } from '../runtime/shared/routine-limits.mjs';
47
- import { submissionIdsOf } from './agent-task-sessions.mjs';
61
+ import { submissionIdsOf, submissionOutcome } from './agent-task-sessions.mjs';
48
62
  import {
49
63
  heldRequestMessage,
50
64
  heldRequestNotice,
@@ -71,6 +85,38 @@ function deliveredRow(item, messageId) {
71
85
  return { message, notice: routineResultNotice(message) };
72
86
  }
73
87
 
88
+ /** Whether agent `id` can have a home chat, decided only from a definition
89
+ * read successfully: 'bot' (a store bot, or one the kind rule calls a bot),
90
+ * 'none' (read, and a workflow agent: an explicit non-bot `kind`, a shipped
91
+ * role, or an unmarked agents/ copy once bots have their own store), else
92
+ * 'unknown' — an unreadable file, an unparsable one, or an unmarked user
93
+ * copy without bot evidence never proves the agent is no bot. */
94
+ export function readHomeAgent({ dataDir, shippedRoot, id, read = (path) => readFileSync(path, 'utf8') }) {
95
+ if (!dataDir || !id) return 'unknown';
96
+ if (isStoreBot(dataDir, id)) return 'bot';
97
+ const readOrAbsent = (path) => {
98
+ try {
99
+ return { text: read(path) };
100
+ } catch (error) {
101
+ return error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? { absent: true } : { failed: true };
102
+ }
103
+ };
104
+ const user = readOrAbsent(join(dataDir, 'agents', id, 'AGENT.md'));
105
+ if (user.failed) return 'unknown';
106
+ const shipped = shippedRoot ? readOrAbsent(join(shippedRoot, 'agents', id, 'AGENT.md')) : { absent: true };
107
+ const own = user.text ?? (shipped.failed ? null : shipped.text);
108
+ if (typeof own !== 'string') return 'unknown';
109
+ const kind = String(readMarkdownDocument(own).frontmatter?.kind || '')
110
+ .trim()
111
+ .toLowerCase();
112
+ if (kind) {
113
+ if (isContactAgentId(id, kind)) return 'bot';
114
+ return kind === WORKFLOW_AGENT_KIND ? 'none' : 'unknown';
115
+ }
116
+ if (user.text === undefined || typeof shipped.text === 'string' || botStoreActive(dataDir)) return 'none';
117
+ return readAgentDefinitionKind({ dataDir, shippedRoot, id }) === BOT_AGENT_KIND ? 'bot' : 'unknown';
118
+ }
119
+
74
120
  /** The agent a schedule belongs to; ownerless rows belong to Mixy. */
75
121
  export function scheduleOwnerAgentId(schedule) {
76
122
  const owner = typeof schedule?.ownerAgentId === 'string' ? schedule.ownerAgentId.trim() : '';
@@ -81,7 +127,8 @@ let liveSurface = null;
81
127
  /** `{ busy(sessionId), notice(sessionId, notice), wake(sessionId, prompt, { id }),
82
128
  * submit(sessionId, prompt, options), abort(sessionId) }` from the session
83
129
  * service; `wake` starts one hidden turn with submission `id` (resolves
84
- * false when it was refused), `submit` queues a routine turn
130
+ * false when it was refused, else `{ settled }`: a promise of the turn's end
131
+ * status, 'done' when it finished), `submit` queues a routine turn
85
132
  * (resolves whether it was accepted), `abort` stops the running turn. */
86
133
  export function setAgentHomeLiveSurface(surface) {
87
134
  liveSurface = surface || null;
@@ -100,6 +147,10 @@ export const ROUND_TTL_MS = 30 * 24 * 60 * 60 * 1000;
100
147
  // Per home: round-trip targets counted (least recently counted drop first),
101
148
  // reporting turns watched at once, and posted results owed a wake.
102
149
  export const QUEUE_CAPS = Object.freeze({ roundTargets: 100, watches: 200, wakeRunIds: 100 });
150
+ // Waits before the 2nd, 3rd and 4th try of a relay (wake) turn that did not finish.
151
+ export const WAKE_RETRY_DELAYS_MS = Object.freeze([30_000, 120_000, 600_000]);
152
+ // Refused wake submissions of one batch, a minute apart, before it is given up.
153
+ export const WAKE_REFUSAL_LIMIT = 10;
103
154
 
104
155
  /** `homeId`'s wake obligations in `wakes`: posted task results not in a batch
105
156
  * yet (`owed`), those past the cap collapsed into one marker (`overflow`:
@@ -127,6 +178,83 @@ function withWakeState(doc, homeId, { owed, batches, overflow }) {
127
178
  return { ...doc, wakes };
128
179
  }
129
180
 
181
+ /** `own`'s per-target counts (`{ n, at }`) used within ROUND_TTL_MS of
182
+ * `nowMs`, the `cap` most recently counted. */
183
+ function freshRoundCounts(own, nowMs, cap) {
184
+ return Object.fromEntries(
185
+ Object.entries(own && typeof own === 'object' ? own : {})
186
+ .filter(([, count]) => count && typeof count === 'object' && nowMs - (Number(count.at) || 0) < ROUND_TTL_MS)
187
+ .sort(([, a], [, b]) => (Number(b.at) || 0) - (Number(a.at) || 0))
188
+ .slice(0, cap)
189
+ );
190
+ }
191
+
192
+ /** `doc` with `items` appended to `homeId`'s list, minus run ids it holds or
193
+ * posted; past `limit` the oldest drop, counted under `omitted`. Null when
194
+ * nothing changes. */
195
+ function appendedItems(doc, homeId, items, limit, omittedCount) {
196
+ const list = doc.homes[homeId] || [];
197
+ const seen = new Set([...list.map((row) => row.runId), ...(doc.delivered[homeId] || [])]);
198
+ const fresh = items.filter((item) => !seen.has(item.runId) && seen.add(item.runId));
199
+ if (!fresh.length && !omittedCount) return null;
200
+ const next = [...list, ...fresh];
201
+ const drop = Math.max(0, next.length - limit);
202
+ const omitted = { ...doc.omitted };
203
+ const count = drop + omittedCount;
204
+ if (count) {
205
+ const previous = omitted[homeId];
206
+ omitted[homeId] = { id: previous?.id || `omit_${next[0].runId}`, count: (previous?.count || 0) + count };
207
+ }
208
+ return { ...doc, homes: { ...doc.homes, [homeId]: next.slice(drop) }, omitted };
209
+ }
210
+
211
+ /** `doc.wakes` with the run ids `wake` owed a wake by `homeId`, at most
212
+ * `cap` per home: past it the oldest owed collapse into the overflow marker
213
+ * (`overflowed` counts them). Stored batches never change. */
214
+ function owedWakes(doc, homeId, wake, cap) {
215
+ const state = wakeState(doc.wakes, homeId);
216
+ const batched = state.batches.flatMap((batch) => batch.runIds);
217
+ const known = new Set([
218
+ ...state.owed,
219
+ ...batched,
220
+ ...[state.overflow?.oldest, state.overflow?.newest].filter(Boolean),
221
+ ]);
222
+ const added = wake.filter((runId) => !known.has(runId));
223
+ if (!added.length) return { wakes: doc.wakes, overflowed: 0 };
224
+ const owed = [...state.owed, ...added];
225
+ const room = Math.max(0, cap - batched.length);
226
+ const excess = owed.slice(0, Math.max(0, owed.length - room));
227
+ const overflow = excess.length
228
+ ? {
229
+ count: (state.overflow?.count || 0) + excess.length,
230
+ oldest: state.overflow?.oldest || excess[0],
231
+ newest: excess.at(-1),
232
+ }
233
+ : state.overflow;
234
+ const { wakes } = withWakeState(doc, homeId, { ...state, owed: owed.slice(excess.length), overflow });
235
+ return { wakes, overflowed: excess.length };
236
+ }
237
+
238
+ /** `doc` with the posted `runIds` of `homeId` dequeued into its posted
239
+ * ledger, and the posted omission notice `omitted` retired. */
240
+ function withDequeued(doc, homeId, runIds, omitted) {
241
+ const homes = { ...doc.homes };
242
+ const rest = (homes[homeId] || []).filter((row) => !runIds.includes(row.runId));
243
+ if (rest.length) homes[homeId] = rest;
244
+ else delete homes[homeId];
245
+ const delivered = { ...doc.delivered };
246
+ if (runIds.length) delivered[homeId] = [...(delivered[homeId] || []), ...runIds].slice(-DELIVERED_LEDGER);
247
+ const omittedNow = { ...doc.omitted };
248
+ const current = omittedNow[homeId];
249
+ if (omitted && current?.id === omitted.id) {
250
+ // More dropped while this pass posted: they get a notice of their own.
251
+ const left = current.count - omitted.count;
252
+ if (left > 0) omittedNow[homeId] = { id: `${current.id}.${omitted.count}`, count: left };
253
+ else delete omittedNow[homeId];
254
+ }
255
+ return { ...doc, homes, omitted: omittedNow, delivered };
256
+ }
257
+
130
258
  /** The persisted queue at `filePath`: `{ homes: { [homeSessionId]: item[] },
131
259
  * omitted: { [homeSessionId]: { id, count } }, delivered: { [homeSessionId]:
132
260
  * runId[] } }`. A home keeps at most `limit` items: past it the oldest drop,
@@ -136,15 +264,7 @@ export function createRoutineResultQueue(
136
264
  { limit = ROUTINE_LIMITS.homeQueueLimit, now = Date.now, caps = QUEUE_CAPS } = {}
137
265
  ) {
138
266
  const section = (doc, key) => (doc && typeof doc[key] === 'object' && doc[key]) || {};
139
- /** `own`'s per-target counts (`{ n, at }`) used within ROUND_TTL_MS, the
140
- * `caps.roundTargets` most recently counted. */
141
- const freshRounds = (own) =>
142
- Object.fromEntries(
143
- Object.entries(own && typeof own === 'object' ? own : {})
144
- .filter(([, count]) => count && typeof count === 'object' && now() - (Number(count.at) || 0) < ROUND_TTL_MS)
145
- .sort(([, a], [, b]) => (Number(b.at) || 0) - (Number(a.at) || 0))
146
- .slice(0, caps.roundTargets)
147
- );
267
+ const freshRounds = (own) => freshRoundCounts(own, now(), caps.roundTargets);
148
268
  const tidyRounds = (rounds) =>
149
269
  Object.fromEntries(
150
270
  Object.entries(rounds || {})
@@ -162,39 +282,26 @@ export function createRoutineResultQueue(
162
282
  // Posted task results whose wake turn was not taken yet, per home.
163
283
  wakes: section(doc, 'wakes'),
164
284
  });
285
+ // A damaged queue file (empty, NUL-filled after a crash, malformed) is kept
286
+ // aside and its last good copy restored (readJsonState); it never blocks
287
+ // delivery. I/O errors still throw.
165
288
  async function read() {
166
- try {
167
- return parts(parseJsonText(await readFile(filePath, 'utf8')));
168
- } catch (error) {
169
- if (error?.code === 'ENOENT') return parts(null);
170
- throw error;
171
- }
289
+ return parts(await readJsonState(filePath));
172
290
  }
173
291
  /** Read-modify-write under the file lock (`mutator(doc)` returns the next
174
292
  * doc, or nothing to keep the file); every write also retires stale and
175
293
  * surplus round counts. */
176
294
  function update(mutator) {
177
- return updateJsonAtomic(filePath, (raw) => {
178
- const next = mutator(parts(raw));
179
- return next ? { ...next, rounds: tidyRounds(next.rounds) } : undefined;
180
- });
181
- }
182
- /** `doc` with `items` appended to `homeId`'s list, minus run ids it holds or posted. */
183
- function appended(doc, homeId, items, omittedCount = 0) {
184
- const list = doc.homes[homeId] || [];
185
- const seen = new Set([...list.map((row) => row.runId), ...(doc.delivered[homeId] || [])]);
186
- const fresh = items.filter((item) => !seen.has(item.runId) && seen.add(item.runId));
187
- if (!fresh.length && !omittedCount) return null;
188
- const next = [...list, ...fresh];
189
- const drop = Math.max(0, next.length - limit);
190
- const omitted = { ...doc.omitted };
191
- const count = drop + omittedCount;
192
- if (count) {
193
- const previous = omitted[homeId];
194
- omitted[homeId] = { id: previous?.id || `omit_${next[0].runId}`, count: (previous?.count || 0) + count };
195
- }
196
- return { ...doc, homes: { ...doc.homes, [homeId]: next.slice(drop) }, omitted };
295
+ return updateJsonAtomic(
296
+ filePath,
297
+ (raw) => {
298
+ const next = mutator(parts(raw));
299
+ return next ? { ...next, rounds: tidyRounds(next.rounds) } : undefined;
300
+ },
301
+ { backup: true }
302
+ );
197
303
  }
304
+ const appended = (doc, homeId, items, omittedCount = 0) => appendedItems(doc, homeId, items, limit, omittedCount);
198
305
  return {
199
306
  async pendingHomes() {
200
307
  const { homes, omitted, wakes } = await read();
@@ -237,6 +344,33 @@ export function createRoutineResultQueue(
237
344
  return withWakeState(doc, homeId, { ...state, batches });
238
345
  });
239
346
  },
347
+ /** Batch `batchId`'s relay turn did not finish: it is owed again under a
348
+ * fresh wake id (the old one names the failed turn), as try `tries`, not
349
+ * before `notBefore`. Returns the new batch; null when it is gone. */
350
+ async rewake(homeId, batchId, { tries, notBefore }) {
351
+ let next = null;
352
+ await update((doc) => {
353
+ next = null;
354
+ const state = wakeState(doc.wakes, homeId);
355
+ const index = state.batches.findIndex((batch) => batch.id === batchId);
356
+ if (index < 0) return undefined;
357
+ // The new try's turn is its own: it follows no earlier turn.
358
+ const { follows: _follows, ...batch } = state.batches[index];
359
+ next = { ...batch, id: `awake_${randomUUID()}`, tries, notBefore };
360
+ const batches = state.batches.map((batch, i) => (i === index ? next : batch));
361
+ return withWakeState(doc, homeId, { ...state, batches });
362
+ });
363
+ return next;
364
+ },
365
+ /** Store `fields` on batch `batchId` (same wake id). */
366
+ async markBatch(homeId, batchId, fields) {
367
+ await update((doc) => {
368
+ const state = wakeState(doc.wakes, homeId);
369
+ if (!state.batches.some((batch) => batch.id === batchId)) return undefined;
370
+ const batches = state.batches.map((batch) => (batch.id === batchId ? { ...batch, ...fields } : batch));
371
+ return withWakeState(doc, homeId, { ...state, batches });
372
+ });
373
+ },
240
374
  /** Drop every wake obligation of `homeId` (its owner is gone). */
241
375
  async dropWakes(homeId) {
242
376
  await update((doc) =>
@@ -288,11 +422,14 @@ export function createRoutineResultQueue(
288
422
  else {
289
423
  // A task turn's end counts one round trip, once per run id, per home
290
424
  // and target: the user's next message in that home starts over
291
- // (resetRounds), and so does a count idle past ROUND_TTL_MS.
425
+ // (resetRounds), and so does a count idle past ROUND_TTL_MS. Only a
426
+ // send made in a bot-descended turn counts: the user's or a routine's
427
+ // own delegations never use the budget up.
292
428
  const own = freshRounds(doc.rounds[homeId]);
293
- const round = (own[item.taskSessionId]?.n || 0) + 1;
429
+ const counts = sendDepth(item) > 0;
430
+ const round = counts ? (own[item.taskSessionId]?.n || 0) + 1 : 0;
294
431
  next = appended(doc, homeId, [{ ...item, round }]);
295
- if (next) {
432
+ if (next && counts) {
296
433
  const counted = { ...own, [item.taskSessionId]: { n: round, at: now() } };
297
434
  next = { ...next, rounds: { ...doc.rounds, [homeId]: counted } };
298
435
  }
@@ -314,7 +451,9 @@ export function createRoutineResultQueue(
314
451
  (kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId
315
452
  );
316
453
  if (own.length >= caps.watches) {
317
- throw new Error(`${watch.homeSessionId} already waits on ${own.length} session reports`);
454
+ throw Object.assign(new Error(`${watch.homeSessionId} already waits on ${own.length} session reports`), {
455
+ code: 'REPORT_WATCH_CAP',
456
+ });
318
457
  }
319
458
  return { ...doc, watches: { ...watches, [watch.runId]: watch } };
320
459
  });
@@ -324,6 +463,16 @@ export function createRoutineResultQueue(
324
463
  async watches() {
325
464
  return Object.values((await read()).watches);
326
465
  },
466
+ /** The watch of `runId` follows the turn submitted as `submissionId`: a
467
+ * room member's one turn answering several sends, or a fresh turn
468
+ * replacing one cut short (recovery reads that turn's end). */
469
+ async followWatch(runId, submissionId) {
470
+ await update((doc) => {
471
+ const watch = doc.watches[runId];
472
+ if (!watch || watch.follows === submissionId) return undefined;
473
+ return { ...doc, watches: { ...doc.watches, [runId]: { ...watch, follows: submissionId } } };
474
+ });
475
+ },
327
476
  /** Drop the watch of `runId` (its turn was never taken). */
328
477
  async unwatch(runId) {
329
478
  await update((doc) => {
@@ -363,45 +512,9 @@ export function createRoutineResultQueue(
363
512
  async remove(homeId, runIds, omitted = null, { wake = [] } = {}) {
364
513
  let overflowed = 0;
365
514
  await update((doc) => {
366
- overflowed = 0;
367
- const state = wakeState(doc.wakes, homeId);
368
- const batched = state.batches.flatMap((batch) => batch.runIds);
369
- const known = new Set([
370
- ...state.owed,
371
- ...batched,
372
- ...[state.overflow?.oldest, state.overflow?.newest].filter(Boolean),
373
- ]);
374
- const added = wake.filter((runId) => !known.has(runId));
375
- let wakes = doc.wakes;
376
- if (added.length) {
377
- const owed = [...state.owed, ...added];
378
- const room = Math.max(0, caps.wakeRunIds - batched.length);
379
- const excess = owed.slice(0, Math.max(0, owed.length - room));
380
- overflowed = excess.length;
381
- const overflow = excess.length
382
- ? {
383
- count: (state.overflow?.count || 0) + excess.length,
384
- oldest: state.overflow?.oldest || excess[0],
385
- newest: excess.at(-1),
386
- }
387
- : state.overflow;
388
- ({ wakes } = withWakeState(doc, homeId, { ...state, owed: owed.slice(excess.length), overflow }));
389
- }
390
- const homes = { ...doc.homes };
391
- const rest = (homes[homeId] || []).filter((row) => !runIds.includes(row.runId));
392
- if (rest.length) homes[homeId] = rest;
393
- else delete homes[homeId];
394
- const delivered = { ...doc.delivered };
395
- if (runIds.length) delivered[homeId] = [...(delivered[homeId] || []), ...runIds].slice(-DELIVERED_LEDGER);
396
- const omittedNow = { ...doc.omitted };
397
- const current = omittedNow[homeId];
398
- if (omitted && current?.id === omitted.id) {
399
- // More dropped while this pass posted: they get a notice of their own.
400
- const left = current.count - omitted.count;
401
- if (left > 0) omittedNow[homeId] = { id: `${current.id}.${omitted.count}`, count: left };
402
- else delete omittedNow[homeId];
403
- }
404
- return { ...doc, homes, omitted: omittedNow, delivered, wakes };
515
+ let wakes;
516
+ ({ wakes, overflowed } = owedWakes(doc, homeId, wake, caps.wakeRunIds));
517
+ return { ...withDequeued(doc, homeId, runIds, omitted), wakes };
405
518
  });
406
519
  return { overflowed };
407
520
  },
@@ -410,6 +523,263 @@ export function createRoutineResultQueue(
410
523
 
411
524
  const messageIdOf = (runId) => `routine_${runId}`;
412
525
 
526
+ /** Show `notice` live in `homeId` through the `live()` surface: its answer,
527
+ * or false (logged) when the surface threw. */
528
+ function noticeLive(live, log, homeId, notice) {
529
+ try {
530
+ return live()?.notice?.(homeId, notice);
531
+ } catch (error) {
532
+ log(`live notice in ${homeId} failed: ${error?.message || error}`);
533
+ return false;
534
+ }
535
+ }
536
+
537
+ /** Dequeue every queued item (and the omission) of `homeId` unposted, and
538
+ * its wake obligations. */
539
+ async function dropQueued(queue, homeId, items, omitted) {
540
+ await queue.remove(
541
+ homeId,
542
+ items.map((item) => item.runId),
543
+ omitted
544
+ );
545
+ await queue.dropWakes(homeId);
546
+ }
547
+
548
+ /** The queued item of `result` ({ runId, kind, ok, text, runSessionId }) of
549
+ * a schedule or webhook `row` (enqueue). */
550
+ function routineResultItem(row, result) {
551
+ return {
552
+ runId: String(result.runId),
553
+ kind: result.kind === 'webhook' ? 'webhook' : 'schedule',
554
+ name: row.name,
555
+ ok: result.ok === true,
556
+ text: routineSummary(result.text),
557
+ runSessionId: result.runSessionId || null,
558
+ ...(result.reason ? { reason: String(result.reason) } : {}),
559
+ ...(result.paused === true ? { paused: true } : {}),
560
+ at: Date.now(),
561
+ };
562
+ }
563
+
564
+ /** The run ids of `watches` a home at its watch `cap` gives up before
565
+ * storing `watch`: its oldest whose target sessions are proven deleted. */
566
+ function watchesToDrop(watches, watch, cap, sessionDeleted) {
567
+ const own = watches.filter((kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId);
568
+ const over = own.length + 1 - cap;
569
+ return over > 0
570
+ ? own
571
+ .filter((kept) => sessionDeleted(kept.taskSessionId))
572
+ .slice(0, over)
573
+ .map((kept) => kept.runId)
574
+ : [];
575
+ }
576
+
577
+ /** A bookkept home proven deleted: a bot's home whose definition is gone,
578
+ * or a room thread whose room is. Never Mixy's home, never on a missing
579
+ * thread record, never on an unknown answer. */
580
+ function homeProvenDeleted(homeId, agentPresence, roomPresence) {
581
+ const agentId = agentIdOfHomeSession(homeId);
582
+ if (agentId) return agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted';
583
+ const roomId = roomIdOfSession(homeId) || roomMemberOfSession(homeId)?.roomId || '';
584
+ return Boolean(roomId) && roomPresence(roomId) === 'deleted';
585
+ }
586
+
587
+ /** The queued item of one task report (enqueueTask). */
588
+ function taskReportItem({ runId, taskSessionId, title, status, text = '', target, files = [], omitted = 0, hop = 0 }) {
589
+ // The model-facing reply keeps its lines up to REPLY_LIMIT; the row's
590
+ // one-line summary is derived from it (agentTaskMessage).
591
+ const reply = boundedReply(text);
592
+ return {
593
+ runId: String(runId),
594
+ kind: 'agent-task',
595
+ taskSessionId,
596
+ title,
597
+ status,
598
+ text: reply.text,
599
+ // The UI row's one line comes from the whole reply, not its bounded part.
600
+ summary: routineSummary(text),
601
+ ...(reply.chars > reply.text.length ? { textChars: reply.chars } : {}),
602
+ // Any session other than a task session the agent created.
603
+ ...(target === 'session' ? { target } : {}),
604
+ // The files the turn produced (handoff-attachments.mjs collectTurnArtifacts).
605
+ ...(Array.isArray(files) && files.length ? { files, ...(omitted > 0 ? { omitted } : {}) } : {}),
606
+ roundTrip: status !== 'started' && status !== 'blocked',
607
+ ...(hop > 0 ? { hop } : {}),
608
+ at: Date.now(),
609
+ };
610
+ }
611
+
612
+ /** Append to `session.messages` the omission notice and the queued `items`
613
+ * not posted before. `fresh`: the rows newly posted; `toWake`: the run ids
614
+ * owed a wake (every dequeued task result, posted now or before). */
615
+ function postQueuedRows(session, items, omitted) {
616
+ const posted = new Set(session.messages.map((message) => message?.meta?.transcript?.messageId).filter(Boolean));
617
+ const fresh = [];
618
+ const omittedId = omitted ? messageIdOf(omitted.id) : '';
619
+ if (omitted && !posted.has(omittedId)) {
620
+ const message = routineOmittedMessage({ count: omitted.count, messageId: omittedId });
621
+ session.messages.push(message);
622
+ fresh.push({ message, notice: routineResultNotice(message) });
623
+ }
624
+ const toWake = [];
625
+ for (const item of items) {
626
+ const row = deliveredRow(item, messageIdOf(item.runId));
627
+ if (row.wake) toWake.push(item.runId);
628
+ if (posted.has(messageIdOf(item.runId))) continue;
629
+ session.messages.push(row.message);
630
+ fresh.push(row);
631
+ }
632
+ return { fresh, toWake };
633
+ }
634
+
635
+ /** The relay of posted task results: owed results wake the agent with one
636
+ * hidden turn per batch. A batch is stored with its wake id BEFORE the wake
637
+ * is submitted and dropped once its relay turn settled done, so a restart in
638
+ * between re-checks that exact id; a relay turn that did not finish is owed
639
+ * again under a fresh id (retryRelay). Results owed later form a new batch.
640
+ * A replayed run id is never queued again. */
641
+ function createHomeRelay({
642
+ queue,
643
+ saveSession,
644
+ live,
645
+ roomRelay,
646
+ busy,
647
+ later,
648
+ drain,
649
+ log,
650
+ wakeRetryDelaysMs,
651
+ wakeRefusalLimit,
652
+ busyRetryMs,
653
+ missingRetryMs,
654
+ }) {
655
+ // Wake batch id → 'running' while this process's relay turn runs, then
656
+ // 'done' or 'failed' once it settled (until the batch is retired).
657
+ const relays = new Map();
658
+
659
+ /** How `batch`'s relay turn went: '' when no turn took it, else
660
+ * 'running', 'done' or 'failed'. A turn an earlier process took decides by
661
+ * its recorded end; one with none was cut off by the restart. A room
662
+ * member's batch may be answered by a turn under another submission id
663
+ * (`follows`, recorded before that turn was submitted). */
664
+ function relayOf(batch, messages, taken) {
665
+ const local = relays.get(batch.id);
666
+ if (local) return local;
667
+ const turnId = batch.follows || batch.id;
668
+ if (!taken.has(turnId)) return '';
669
+ const outcome = submissionOutcome(messages, turnId);
670
+ return outcome && outcome !== 'open' && outcome.status === 'completed' ? 'done' : 'failed';
671
+ }
672
+
673
+ /** The relay turn of `batch` did not finish: owe it again after a backoff,
674
+ * or, past the last try, leave a note in the thread and wake nobody. */
675
+ async function retryRelay(homeId, session, batch) {
676
+ const tries = (Number(batch.tries) || 0) + 1;
677
+ if (tries <= wakeRetryDelaysMs.length) {
678
+ log(`the relay turn in ${homeId} did not finish (try ${tries}); waking it again later`);
679
+ await queue.rewake(homeId, batch.id, { tries, notBefore: Date.now() + wakeRetryDelaysMs[tries - 1] });
680
+ return true;
681
+ }
682
+ return abandonRelay(homeId, session, batch, `did not finish after ${tries} tries`, tries);
683
+ }
684
+
685
+ /** Give up on `batch`'s relay: a note in the thread (once), nobody woken.
686
+ * False while the thread is busy (try again once it is idle). */
687
+ async function abandonRelay(homeId, session, batch, why, tries) {
688
+ if (busy(homeId)) return false;
689
+ log(`the relay turn in ${homeId} ${why}; its results stay posted unrelayed`);
690
+ const messageId = messageIdOf(`relay_${batch.id}`);
691
+ if (!session.messages.some((message) => message?.meta?.transcript?.messageId === messageId)) {
692
+ const message = agentTaskRelayFailedMessage({
693
+ count: batch.runIds.length + (batch.overflow?.count || 0),
694
+ tries,
695
+ messageId,
696
+ });
697
+ session.messages.push(message);
698
+ session.updatedAt = Date.now();
699
+ await saveSession(session);
700
+ noticeLive(live, log, homeId, { ...routineResultNotice(message), messageId });
701
+ }
702
+ await queue.woke(homeId, batch.id);
703
+ return true;
704
+ }
705
+
706
+ /** Wake `homeId` (its idle `session`, just posted to) for its next owed batch. */
707
+ async function wakeOwed(homeId, session) {
708
+ const byId = new Map(session.messages.map((message) => [message?.meta?.transcript?.messageId, message]));
709
+ const taken = new Set(session.messages.flatMap((message) => submissionIdsOf(message)));
710
+ let batch = null;
711
+ let woken = [];
712
+ let more = 0;
713
+ for (;;) {
714
+ batch = (await queue.wakes(homeId)).batches[0] || (await queue.batchWake(homeId));
715
+ if (!batch) return;
716
+ const relay = relayOf(batch, session.messages, taken);
717
+ // Its settle drains again.
718
+ if (relay === 'running') return;
719
+ if (relay) {
720
+ relays.delete(batch.id);
721
+ if (relay === 'done') await queue.woke(homeId, batch.id);
722
+ else if (!(await retryRelay(homeId, session, batch))) return later(homeId, busyRetryMs);
723
+ continue;
724
+ }
725
+ const wait = (Number(batch.notBefore) || 0) - Date.now();
726
+ if (wait > 0) return later(homeId, wait);
727
+ woken = batch.runIds.map((runId) => byId.get(messageIdOf(runId))).filter(Boolean);
728
+ // Results past the wake cap ride this wake as a count of rows to read.
729
+ more = byId.has(messageIdOf(batch.overflow?.newest)) ? batch.overflow.count : 0;
730
+ if (woken.length || more) break;
731
+ // Its rows are gone with a thread reset.
732
+ await queue.woke(homeId, batch.id);
733
+ }
734
+ // A room member relays in its room (the room host runs that turn), any
735
+ // other thread in a hidden turn of its own.
736
+ const surface = live();
737
+ const wake = roomMemberOfSession(homeId) ? roomRelay : surface?.wake?.bind(surface);
738
+ if (typeof wake !== 'function') return later(homeId, busyRetryMs);
739
+ const batchId = batch.id;
740
+ let turn;
741
+ relays.set(batchId, 'running');
742
+ try {
743
+ // The reports answer sends this home made itself: the wake turn resumes
744
+ // at the depth of the turns that sent them (bot-hops.mjs), 0 for work the
745
+ // user or a routine started, so only bot-descended chains keep counting.
746
+ const hop = Math.max(0, ...woken.map((message) => sendDepth(message?.meta?.routine)));
747
+ // A room relay may be answered under another turn's submission id: the
748
+ // batch records it before that turn is submitted.
749
+ const follow = (submissionId) => queue.markBatch(homeId, batchId, { follows: submissionId });
750
+ turn = await wake(homeId, agentTaskWakePrompt(woken, { more }), { id: batchId, hop, follow });
751
+ if (turn === false) throw new Error('the wake turn was refused');
752
+ } catch (error) {
753
+ relays.delete(batchId);
754
+ // A wake refused (or failing) every time is retried a bounded number of times.
755
+ const refusals = (Number(batch.refusals) || 0) + 1;
756
+ if (refusals >= wakeRefusalLimit) {
757
+ if (await abandonRelay(homeId, session, batch, `could not start (${refusals} refusals)`, refusals)) return;
758
+ return later(homeId, busyRetryMs);
759
+ }
760
+ await queue.markBatch(homeId, batchId, { refusals });
761
+ log(`waking ${homeId} failed (${refusals}/${wakeRefusalLimit}), retrying: ${error?.message || error}`);
762
+ return later(homeId, missingRetryMs);
763
+ }
764
+ // `settled` resolves the relay turn's end status: only 'done' retires the batch.
765
+ if (typeof turn?.settled?.then === 'function') {
766
+ const settled = (status) => {
767
+ relays.set(batchId, status === 'done' ? 'done' : 'failed');
768
+ void drain(homeId);
769
+ };
770
+ turn.settled.then(settled, () => settled('failed'));
771
+ return;
772
+ }
773
+ // A surface that cannot report the turn's end: taking it is relaying it.
774
+ relays.delete(batchId);
775
+ await queue.woke(homeId, batchId);
776
+ // Another batch left by an earlier process wakes once this turn is done.
777
+ if (owesWake(await queue.wakes(homeId))) later(homeId, busyRetryMs);
778
+ }
779
+
780
+ return { wakeOwed };
781
+ }
782
+
413
783
  export function createAgentHomeDelivery({
414
784
  loadSession,
415
785
  saveSession,
@@ -427,6 +797,20 @@ export function createAgentHomeDelivery({
427
797
  // True only on positive evidence that session `id` was deleted; an
428
798
  // unreadable or unknown record is not deleted.
429
799
  sessionDeleted = () => false,
800
+ // 'bot' | 'none' | 'unknown' per agent id: whether it can have a home chat
801
+ // at all. 'none' (proven: a workflow agent such as Reviewer) drops the rows
802
+ // queued for its missing home instead of waiting on it forever.
803
+ homeAgent = () => 'unknown',
804
+ // Waits before each new try of a relay turn that did not finish; past the
805
+ // last one the results stay posted with a note and nobody is woken.
806
+ wakeRetryDelaysMs = WAKE_RETRY_DELAYS_MS,
807
+ // Refused or failing wake submissions of one batch (missingRetryMs apart)
808
+ // before it is given up the same way.
809
+ wakeRefusalLimit = WAKE_REFUSAL_LIMIT,
810
+ // A room member's relay turn (wake of a member session): its turn in the
811
+ // room, through the desktop room host (agent-room-host.mjs `relay`), with
812
+ // the live surface's `wake` contract.
813
+ roomRelay = (sessionId, prompt, options) => agentRoomHost().relay(sessionId, prompt, options),
430
814
  log = (line) => process.stderr.write(`mixdog routines: ${line}\n`),
431
815
  }) {
432
816
  const busy = (sessionId) => {
@@ -441,6 +825,21 @@ export function createAgentHomeDelivery({
441
825
  const again = new Set(); // homes enqueued to while their pass ran
442
826
  const waiting = new Set(); // homes with a retry timer armed
443
827
  const shownLive = new Set(); // held-request message ids shown live in a busy thread
828
+ const waitLogged = new Set(); // missing homes whose wait was logged
829
+ const relay = createHomeRelay({
830
+ queue,
831
+ saveSession,
832
+ live,
833
+ roomRelay,
834
+ busy,
835
+ later,
836
+ drain,
837
+ log,
838
+ wakeRetryDelaysMs,
839
+ wakeRefusalLimit,
840
+ busyRetryMs,
841
+ missingRetryMs,
842
+ });
444
843
 
445
844
  /** A held request waits on the user, possibly raised by the very turn that
446
845
  * keeps its thread busy (a routine turn): show it live at once; the stored
@@ -451,11 +850,7 @@ export function createAgentHomeDelivery({
451
850
  const messageId = messageIdOf(item.runId);
452
851
  if (shownLive.has(messageId)) continue;
453
852
  shownLive.add(messageId);
454
- try {
455
- live()?.notice?.(homeId, { ...deliveredRow(item, messageId).notice, messageId });
456
- } catch (error) {
457
- log(`live notice in ${homeId} failed: ${error?.message || error}`);
458
- }
853
+ noticeLive(live, log, homeId, { ...deliveredRow(item, messageId).notice, messageId });
459
854
  }
460
855
  }
461
856
 
@@ -468,12 +863,38 @@ export function createAgentHomeDelivery({
468
863
  }, ms);
469
864
  }
470
865
 
866
+ /** A room thread proven gone: its queued rows are dropped, never retried. */
867
+ async function dropGoneRoomThread(homeId, items, omitted) {
868
+ if (!isRoomThreadSessionId(homeId) || !(homeDeleted(homeId) || sessionDeleted(homeId))) return false;
869
+ log(`${items.length} task report(s) for the deleted ${homeId} are dropped`);
870
+ await dropQueued(queue, homeId, items, omitted);
871
+ return true;
872
+ }
873
+
874
+ /** A room log's queued rows: task reports go to member sessions now, so what
875
+ * an earlier version left under the log's id is dropped, never posted. */
876
+ async function dropRoomLogQueue(logId, items, omitted) {
877
+ log(`${items.length} legacy task report(s) queued for the room log ${logId} are dropped`);
878
+ await dropQueued(queue, logId, items, omitted);
879
+ }
880
+
881
+ /** A missing home of an agent that can never have one (no bot): its queued
882
+ * rows are dropped with a log line, never retried. */
883
+ async function dropHomeless(homeId, items, omitted) {
884
+ const agentId = agentIdOfHomeSession(homeId);
885
+ if (!agentId || agentId === MIXY_AGENT_ID || homeAgent(agentId) !== 'none') return false;
886
+ log(`${items.length} result(s) for ${homeId} are dropped: ${agentId} is not a bot, so it has no chat`);
887
+ await dropQueued(queue, homeId, items, omitted);
888
+ return true;
889
+ }
890
+
471
891
  /** Post every queued result of `homeId` if its thread is idle; else retry later. */
472
892
  async function drainOnce(homeId) {
473
893
  const items = await queue.items(homeId);
474
894
  const omitted = await queue.omitted(homeId);
475
895
  const owed = owesWake(await queue.wakes(homeId));
476
896
  if (!items.length && !omitted && !owed) return;
897
+ if (roomIdOfSession(homeId)) return dropRoomLogQueue(homeId, items, omitted);
477
898
  const agentId = agentIdOfHomeSession(homeId);
478
899
  if (agentId && agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted') {
479
900
  // The owner is gone (its home archived): Mixy, the default owner, gets them.
@@ -491,37 +912,23 @@ export function createAgentHomeDelivery({
491
912
  // From here to the push nothing awaits, so no turn can start in between.
492
913
  const session = loadSession(homeId);
493
914
  if (!session || session.closed === true) {
494
- log(`${items.length} result(s) wait for the home thread ${homeId}`);
915
+ if (await dropGoneRoomThread(homeId, items, omitted)) return;
916
+ if (await dropHomeless(homeId, items, omitted)) return;
917
+ // Kept (a bot, or an agent whose kind cannot be read): logged once per process.
918
+ if (!waitLogged.has(homeId)) {
919
+ waitLogged.add(homeId);
920
+ log(`${items.length} result(s) wait for the home thread ${homeId}`);
921
+ }
495
922
  return later(homeId, missingRetryMs);
496
923
  }
497
924
  if (!Array.isArray(session.messages)) session.messages = [];
498
- const posted = new Set(session.messages.map((message) => message?.meta?.transcript?.messageId).filter(Boolean));
499
- const fresh = [];
500
- const omittedId = omitted ? messageIdOf(omitted.id) : '';
501
- if (omitted && !posted.has(omittedId)) {
502
- const message = routineOmittedMessage({ count: omitted.count, messageId: omittedId });
503
- session.messages.push(message);
504
- fresh.push(message);
505
- }
506
- const toWake = [];
507
- for (const item of items) {
508
- const row = deliveredRow(item, messageIdOf(item.runId));
509
- // Every dequeued task result is owed one wake, posted in this pass or before.
510
- if (row.wake) toWake.push(item.runId);
511
- if (posted.has(messageIdOf(item.runId))) continue;
512
- session.messages.push(row.message);
513
- fresh.push(row);
514
- }
925
+ const { fresh, toWake } = postQueuedRows(session, items, omitted);
515
926
  for (const item of items) shownLive.delete(messageIdOf(item.runId));
516
927
  if (fresh.length) {
517
928
  session.updatedAt = Date.now();
518
929
  await saveSession(session);
519
930
  for (const { message, notice } of fresh) {
520
- try {
521
- live()?.notice?.(homeId, { ...notice, messageId: message.meta.transcript.messageId });
522
- } catch (error) {
523
- log(`live notice in ${homeId} failed: ${error?.message || error}`);
524
- }
931
+ noticeLive(live, log, homeId, { ...notice, messageId: message.meta.transcript.messageId });
525
932
  }
526
933
  }
527
934
  // Dequeuing and owing the wake are one write: no restart between posting
@@ -533,39 +940,7 @@ export function createAgentHomeDelivery({
533
940
  { wake: toWake }
534
941
  );
535
942
  if (overflowed) log(`${homeId} is at its wake cap: ${overflowed} result(s) ride the next wake as a count`);
536
- // Owed task results wake the agent with one hidden turn per batch. A batch
537
- // is stored with its wake id BEFORE the wake is submitted and dropped once
538
- // it is taken, so a restart in between re-checks that exact id; results
539
- // owed later form a new batch. A replayed run id is never queued again.
540
- const byId = new Map(session.messages.map((message) => [message?.meta?.transcript?.messageId, message]));
541
- const taken = new Set(session.messages.flatMap((message) => submissionIdsOf(message)));
542
- let batch = null;
543
- let woken = [];
544
- let more = 0;
545
- for (;;) {
546
- batch = (await queue.wakes(homeId)).batches[0] || (await queue.batchWake(homeId));
547
- if (!batch) return;
548
- const stale = taken.has(batch.id);
549
- woken = stale ? [] : batch.runIds.map((runId) => byId.get(messageIdOf(runId))).filter(Boolean);
550
- // Results past the wake cap ride this wake as a count of rows to read.
551
- more = !stale && byId.has(messageIdOf(batch.overflow?.newest)) ? batch.overflow.count : 0;
552
- if (woken.length || more) break;
553
- // Taken before a restart, or its rows gone with a thread reset.
554
- await queue.woke(homeId, batch.id);
555
- }
556
- const surface = live();
557
- if (typeof surface?.wake !== 'function') return later(homeId, busyRetryMs);
558
- try {
559
- if ((await surface.wake(homeId, agentTaskWakePrompt(woken, { more }), { id: batch.id })) === false) {
560
- throw new Error('the wake turn was refused');
561
- }
562
- } catch (error) {
563
- log(`waking ${homeId} failed, retrying: ${error?.message || error}`);
564
- return later(homeId, missingRetryMs);
565
- }
566
- await queue.woke(homeId, batch.id);
567
- // Another batch left by an earlier process wakes once this turn is done.
568
- if (owesWake(await queue.wakes(homeId))) later(homeId, busyRetryMs);
943
+ await relay.wakeOwed(homeId, session);
569
944
  }
570
945
 
571
946
  /** The single drainer of `homeId`; a call during a pass runs one more pass. */
@@ -598,17 +973,7 @@ export function createAgentHomeDelivery({
598
973
  async function enqueue(row, result) {
599
974
  const homeId = agentHomeSessionId(scheduleOwnerAgentId(row));
600
975
  if (!result?.runId) throw new Error('routine result: runId required');
601
- await queue.add(homeId, {
602
- runId: String(result.runId),
603
- kind: result.kind === 'webhook' ? 'webhook' : 'schedule',
604
- name: row.name,
605
- ok: result.ok === true,
606
- text: routineSummary(result.text),
607
- runSessionId: result.runSessionId || null,
608
- ...(result.reason ? { reason: String(result.reason) } : {}),
609
- ...(result.paused === true ? { paused: true } : {}),
610
- at: Date.now(),
611
- });
976
+ await queue.add(homeId, routineResultItem(row, result));
612
977
  void drain(homeId);
613
978
  return homeId;
614
979
  }
@@ -631,32 +996,17 @@ export function createAgentHomeDelivery({
631
996
  /** Persist one report of a coding task session `agentId` created (session
632
997
  * create) for its home thread, then start draining it. `status` 'started'
633
998
  * and 'blocked' only post a line; a turn's end also wakes the agent. */
634
- async function enqueueTask(
635
- agentId,
636
- { runId, taskSessionId, title, status, text = '', target, watchId = '', files = [], omitted = 0 }
637
- ) {
999
+ async function enqueueTask(agentId, report) {
1000
+ const { runId, taskSessionId, watchId = '', from = '' } = report;
638
1001
  const homeId = agentHomeSessionId(agentId) || agentHomeSessionId(MIXY_AGENT_ID);
639
1002
  if (!runId || !taskSessionId) throw new Error('task report: runId and taskSessionId required');
640
- await queue.add(
641
- homeId,
642
- {
643
- runId: String(runId),
644
- kind: 'agent-task',
645
- taskSessionId,
646
- title,
647
- status,
648
- text: routineSummary(text),
649
- // Any session other than a task session the agent created.
650
- ...(target === 'session' ? { target } : {}),
651
- // The files the turn produced (handoff-attachments.mjs collectTurnArtifacts).
652
- ...(Array.isArray(files) && files.length ? { files, ...(omitted > 0 ? { omitted } : {}) } : {}),
653
- roundTrip: status !== 'started' && status !== 'blocked',
654
- at: Date.now(),
655
- },
656
- { unwatch: watchId ? String(watchId) : '' }
657
- );
658
- void drain(homeId);
659
- return homeId;
1003
+ const item = taskReportItem(report);
1004
+ const unwatch = watchId ? String(watchId) : '';
1005
+ // A room member's own work reports to its member session, never its home.
1006
+ const target = roomMemberOfSession(from)?.agentId === agentId ? from : homeId;
1007
+ await queue.add(target, item, { unwatch });
1008
+ void drain(target);
1009
+ return target;
660
1010
  }
661
1011
 
662
1012
  /** Show `run`'s live-only row (routineRunNotice: `running`, or a result whose
@@ -667,23 +1017,10 @@ export function createAgentHomeDelivery({
667
1017
  const homeId = agentHomeSessionId(scheduleOwnerAgentId(row));
668
1018
  if (!run?.runId || busy(homeId)) return false;
669
1019
  const notice = routineRunNotice({ ...run, name: row.name, messageId: messageIdOf(run.runId) });
670
- try {
671
- return live()?.notice?.(homeId, notice) === true;
672
- } catch (error) {
673
- log(`live notice in ${homeId} failed: ${error?.message || error}`);
674
- return false;
675
- }
1020
+ return noticeLive(live, log, homeId, notice) === true;
676
1021
  }
677
1022
 
678
- /** A bookkept home proven deleted: a bot's home whose definition is gone,
679
- * or a room thread whose room is. Never Mixy's home, never on a missing
680
- * thread record, never on an unknown answer. */
681
- function homeDeleted(homeId) {
682
- const agentId = agentIdOfHomeSession(homeId);
683
- if (agentId) return agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted';
684
- const roomId = roomIdOfSession(homeId) || roomMemberOfSession(homeId)?.roomId || '';
685
- return Boolean(roomId) && roomPresence(roomId) === 'deleted';
686
- }
1023
+ const homeDeleted = (homeId) => homeProvenDeleted(homeId, agentPresence, roomPresence);
687
1024
 
688
1025
  /** Retire the bookkeeping of deleted homes (and stale round counts). */
689
1026
  async function prune() {
@@ -694,17 +1031,7 @@ export function createAgentHomeDelivery({
694
1031
  * whose target sessions are proven deleted, and refuses the watch (so the
695
1032
  * send claims no report) when that frees too few. */
696
1033
  async function watchTask(watch) {
697
- const own = (await queue.watches()).filter(
698
- (kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId
699
- );
700
- const over = own.length + 1 - queue.watchCap;
701
- const drop =
702
- over > 0
703
- ? own
704
- .filter((kept) => sessionDeleted(kept.taskSessionId))
705
- .slice(0, over)
706
- .map((kept) => kept.runId)
707
- : [];
1034
+ const drop = watchesToDrop(await queue.watches(), watch, queue.watchCap, sessionDeleted);
708
1035
  if (drop.length) {
709
1036
  log(`${watch.homeSessionId} is at its watch cap: dropped ${drop.length} watch(es) of deleted sessions`);
710
1037
  }
@@ -732,6 +1059,7 @@ export function createAgentHomeDelivery({
732
1059
  recover,
733
1060
  watchTask,
734
1061
  unwatchTask: (runId) => queue.unwatch(runId),
1062
+ followTask: (runId, submissionId) => queue.followWatch(runId, submissionId),
735
1063
  taskWatches: () => queue.watches(),
736
1064
  resetRounds: (homeId) => queue.resetRounds(homeId),
737
1065
  };
@@ -748,6 +1076,7 @@ async function processDelivery() {
748
1076
  { createAgentEditorApi },
749
1077
  { probePath, PROBE_ABSENT },
750
1078
  { sessionPath },
1079
+ { mixdogRoot },
751
1080
  ] = await Promise.all([
752
1081
  import('../runtime/agent/orchestrator/session/store.mjs'),
753
1082
  import('../runtime/agent/orchestrator/session/manager/runtime-liveness.mjs'),
@@ -755,6 +1084,7 @@ async function processDelivery() {
755
1084
  import('./workflow-agents-api/agent-editor.mjs'),
756
1085
  import('../runtime/agent/orchestrator/session/store/fs-probe.mjs'),
757
1086
  import('../runtime/agent/orchestrator/session/store/session-paths.mjs'),
1087
+ import('../runtime/shared/plugin-paths.mjs'),
758
1088
  ]);
759
1089
  // The editor's per-id definition probe: 'deleted' only when proven gone.
760
1090
  const { agentDefinitionPresence } = createAgentEditorApi({ cfgMod: { getPluginData } });
@@ -764,6 +1094,9 @@ async function processDelivery() {
764
1094
  turnInFlight: isSessionTurnInFlight,
765
1095
  queue: createRoutineResultQueue(join(getPluginData(), 'routine-results.json')),
766
1096
  agentPresence: (agentId) => agentDefinitionPresence([agentId])[agentId],
1097
+ // A home chat exists only for bots; only a definition read successfully
1098
+ // can prove an agent is none (readHomeAgent).
1099
+ homeAgent: (agentId) => readHomeAgent({ dataDir: getPluginData(), shippedRoot: mixdogRoot(), id: agentId }),
767
1100
  // Deleting a session unlinks its record (session-delete.mjs) and leaves
768
1101
  // no tombstone: only an absent file with no in-memory copy proves it.
769
1102
  // An unreadable file probes 'unreadable' and stays.
@@ -810,6 +1143,11 @@ export async function unwatchAgentTask(runId) {
810
1143
  return (await processDelivery()).unwatchTask(runId);
811
1144
  }
812
1145
 
1146
+ /** One reporting turn's watch follows the turn submitted as `submissionId`. */
1147
+ export async function followAgentTask(runId, submissionId) {
1148
+ return (await processDelivery()).followTask(runId, submissionId);
1149
+ }
1150
+
813
1151
  /** A user's own message in `homeId`: its round trips with every target restart. */
814
1152
  export async function resetAgentTaskRounds(homeId) {
815
1153
  return (await processDelivery()).resetRounds(homeId);