mixdog 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/README.md +8 -4
  2. package/package.json +2 -2
  3. package/scripts/native-release-status.mjs +238 -0
  4. package/src/defaults/shipped-history.json +5 -1
  5. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  6. package/src/defaults/skills/setup/SKILL.md +23 -1
  7. package/src/defaults/skills/setup/references/actions.md +50 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  9. package/src/defaults/skills/setup-guide/SKILL.md +7 -3
  10. package/src/defaults/skills/visualize/SKILL.md +9 -6
  11. package/src/defaults/skills/visualize/references/craft.md +8 -1
  12. package/src/help.mjs +1 -1
  13. package/src/lib/keychain-cjs.cjs +4 -1
  14. package/src/lib/rules-builder.cjs +14 -7
  15. package/src/rules/agent-mode/AGENT_MODE.md +8 -5
  16. package/src/rules/agent-mode/MIXY.md +6 -8
  17. package/src/rules/lead/LEAD.md +2 -2
  18. package/src/rules/shared/10-tool-workflow.md +4 -0
  19. package/src/rules/shared/20-research.md +2 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  21. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  24. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  25. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  27. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  28. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  30. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  31. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -5
  33. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  34. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  36. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  39. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +27 -8
  42. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +16 -0
  47. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  48. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  49. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  50. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +2 -1
  55. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  58. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  59. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  61. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  62. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  64. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -6
  66. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  67. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  68. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  69. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +6 -5
  70. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +7 -6
  71. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  73. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  75. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  76. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  77. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +17 -2
  78. package/src/runtime/agent/orchestrator/session/agent-room.mjs +133 -38
  79. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  80. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  82. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  83. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  84. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  85. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  87. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  88. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  91. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  92. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +136 -32
  93. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  94. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  95. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  96. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  97. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  98. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  99. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  100. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  101. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +5 -5
  102. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  103. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  104. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  105. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  106. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  107. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  108. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  109. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  110. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  111. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  112. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  113. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  114. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  115. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  116. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  117. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  121. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +55 -27
  123. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -8
  124. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  125. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  126. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  127. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  128. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  129. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  130. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +18 -3
  131. package/src/runtime/agent/orchestrator/session/routine-result.mjs +120 -9
  132. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  133. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  135. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  136. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  137. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  138. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  139. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  140. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  141. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  142. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  143. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  144. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  145. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  146. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +35 -5
  147. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +42 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  149. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  150. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  151. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  152. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  154. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  155. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  156. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  157. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  158. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  159. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  160. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  161. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  162. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  163. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  164. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  165. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  166. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  167. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  168. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  169. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  170. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  171. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  173. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  174. package/src/runtime/attachments/limits.mjs +5 -1
  175. package/src/runtime/attachments/office-extract.mjs +3 -6
  176. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  177. package/src/runtime/attachments/store.mjs +49 -30
  178. package/src/runtime/browser-bridge/client.mjs +73 -20
  179. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  180. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  181. package/src/runtime/channels/lib/state-file.mjs +15 -15
  182. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  183. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  184. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  185. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  186. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  187. package/src/runtime/effort-judge/judge-client.mjs +5 -2
  188. package/src/runtime/effort-judge/model-install.mjs +16 -8
  189. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  190. package/src/runtime/local-provider/catalog.mjs +5 -2
  191. package/src/runtime/local-provider/registered-models.mjs +64 -27
  192. package/src/runtime/local-provider/server.mjs +2 -3
  193. package/src/runtime/media/adapters/image-api.mjs +89 -0
  194. package/src/runtime/media/auth.mjs +64 -1
  195. package/src/runtime/media/catalog.mjs +66 -4
  196. package/src/runtime/media/jobs.mjs +4 -0
  197. package/src/runtime/media/lanes.mjs +46 -5
  198. package/src/runtime/media/media-usage.mjs +17 -0
  199. package/src/runtime/media/renditions.mjs +15 -15
  200. package/src/runtime/media/store.mjs +36 -35
  201. package/src/runtime/media/tool.mjs +8 -1
  202. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  203. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  204. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  205. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  206. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  207. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  208. package/src/runtime/office/core/journal.mjs +7 -14
  209. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  210. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  211. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  212. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  213. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  214. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  215. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  216. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  217. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  218. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  219. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  220. package/src/runtime/shared/agent-store.mjs +24 -7
  221. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  222. package/src/runtime/shared/atomic-file.d.mts +50 -0
  223. package/src/runtime/shared/atomic-file.mjs +394 -132
  224. package/src/runtime/shared/background-tasks.mjs +7 -4
  225. package/src/runtime/shared/bot-hops.mjs +30 -0
  226. package/src/runtime/shared/bot-split-migration.mjs +1 -1
  227. package/src/runtime/shared/bounded-download.mjs +4 -2
  228. package/src/runtime/shared/config.mjs +39 -9
  229. package/src/runtime/shared/defaults-separation.mjs +6 -2
  230. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  231. package/src/runtime/shared/err-text.mjs +4 -0
  232. package/src/runtime/shared/error-presentation.mjs +22 -4
  233. package/src/runtime/shared/file-has-data.mjs +28 -0
  234. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  235. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  236. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  237. package/src/runtime/shared/projects.mjs +50 -41
  238. package/src/runtime/shared/provider-accounts.mjs +17 -12
  239. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  240. package/src/runtime/shared/routine-trigger.mjs +14 -6
  241. package/src/runtime/shared/run-child.d.mts +3 -0
  242. package/src/runtime/shared/run-child.mjs +24 -3
  243. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  244. package/src/runtime/shared/singleton-owner.mjs +7 -6
  245. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  246. package/src/runtime/shared/staged-update.mjs +4 -2
  247. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  248. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  249. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  250. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  251. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  252. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  253. package/src/runtime/shared/user-cwd.mjs +9 -4
  254. package/src/runtime/shared/user-data-guard.mjs +41 -42
  255. package/src/runtime/shared/utf8-cut.mjs +10 -0
  256. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  257. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  258. package/src/runtime/tidy/core-install.mjs +26 -4
  259. package/src/runtime/tidy/install.mjs +91 -23
  260. package/src/runtime/web-search/lib/config.mjs +4 -16
  261. package/src/session-runtime/agent-home-delivery.mjs +555 -199
  262. package/src/session-runtime/agent-room-delivery.mjs +143 -36
  263. package/src/session-runtime/agent-task-sessions.mjs +667 -165
  264. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  265. package/src/session-runtime/boot/apis.mjs +45 -24
  266. package/src/session-runtime/boot/config.mjs +1 -0
  267. package/src/session-runtime/boot/facade.mjs +11 -1
  268. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  269. package/src/session-runtime/boot/providers.mjs +2 -2
  270. package/src/session-runtime/boot/shared.mjs +12 -6
  271. package/src/session-runtime/boot/tools.mjs +2 -2
  272. package/src/session-runtime/bot-status.mjs +432 -0
  273. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  274. package/src/session-runtime/config-lifecycle.mjs +60 -55
  275. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  276. package/src/session-runtime/held-requests.mjs +36 -33
  277. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  278. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  279. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +19 -4
  280. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  281. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  282. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  283. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  284. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  285. package/src/session-runtime/media-api.mjs +5 -2
  286. package/src/session-runtime/model-route/route-tuning.mjs +4 -1
  287. package/src/session-runtime/model-route/set-route.mjs +61 -1
  288. package/src/session-runtime/owned-automation-run.mjs +8 -1
  289. package/src/session-runtime/room-member-gate.mjs +16 -3
  290. package/src/session-runtime/route-resolution.mjs +16 -8
  291. package/src/session-runtime/route-state.mjs +1 -59
  292. package/src/session-runtime/routine-home-turn.mjs +5 -3
  293. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  294. package/src/session-runtime/runtime-paths.mjs +11 -5
  295. package/src/session-runtime/runtime-tunables.mjs +6 -0
  296. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  297. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  298. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  299. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  300. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  301. package/src/session-runtime/services/agent-tool/send-flow.mjs +25 -21
  302. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  303. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  304. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +8 -5
  305. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  306. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  307. package/src/session-runtime/services/agent-tool.mjs +6 -0
  308. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  309. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  310. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  311. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  312. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  313. package/src/session-runtime/services/provider-admin.mjs +14 -4
  314. package/src/session-runtime/services/schedule-access.mjs +33 -4
  315. package/src/session-runtime/services/seeds.mjs +4 -3
  316. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  317. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  318. package/src/session-runtime/session-create/materialize.mjs +2 -3
  319. package/src/session-runtime/session-create/session-options.mjs +15 -2
  320. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  321. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  322. package/src/session-runtime/setup-tool/executor.mjs +8 -2
  323. package/src/session-runtime/setup-tool/settings-contract.mjs +31 -1
  324. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  325. package/src/session-runtime/skills-api.mjs +12 -5
  326. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  327. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  328. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  329. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  330. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  331. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  332. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  333. package/src/session-runtime/tool-policy-refresh.mjs +47 -369
  334. package/src/session-runtime/tool-surface.mjs +5 -3
  335. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +95 -88
  336. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  337. package/src/session-runtime/turn/turn-run.mjs +2 -1
  338. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  339. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +519 -333
  340. package/src/session-runtime/workflow-agents-api/agent-route.mjs +46 -46
  341. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  342. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  343. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  344. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  345. package/src/standalone/daemon-agent-control.mjs +4 -1
  346. package/src/standalone/daemon-session-control.mjs +51 -10
  347. package/src/standalone/daemon.mjs +28 -10
  348. package/src/standalone/local-session-runtime.mjs +22 -14
  349. package/src/standalone/session-client.mjs +7 -2
  350. package/src/standalone/session-protocol.mjs +7 -0
  351. package/src/standalone/session-service.mjs +11 -0
  352. package/src/standalone/session-state-projection.mjs +19 -9
  353. package/src/standalone/session-tool-output.mjs +35 -9
  354. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  355. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  356. package/src/tui/app/slash-commands.mjs +1 -1
  357. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  358. package/src/tui/dist/index.mjs +529 -333
  359. package/src/tui/prompt-history-store.mjs +16 -21
  360. package/src/tui/session/context-state.mjs +4 -0
  361. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  362. package/src/tui/session/session-api/media.mjs +2 -1
  363. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  364. package/src/tui/session/session-api/routes-write.mjs +203 -0
  365. package/src/tui/session/session-api/routes.mjs +23 -247
  366. package/src/tui/session/session-api/settings.mjs +14 -1
  367. package/src/tui/session/session-flow/drain.mjs +28 -3
  368. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  369. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  370. package/src/tui/session/session-flow/steering.mjs +8 -3
  371. package/src/tui/session/tool-approval.mjs +185 -20
  372. package/src/tui/session/transcript-intake.mjs +2 -0
  373. package/src/tui/session/turn.mjs +23 -14
  374. package/src/tui/transcript-tool-failures.mjs +8 -2
  375. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  376. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  377. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -17,10 +17,21 @@
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
+ * go to the room instead: each line lands in the member's own session (never
23
+ * waking it: the room host drives its turns), and a blocked or ended turn
24
+ * also lands in the room's log as that member's speech row
25
+ * (agent-room-delivery.mjs appendTaskReports), where the room's next exchange
26
+ * sees it. Both ride this same durable queue, keyed by those sessions.
20
27
  */
21
28
  import { randomUUID } from 'node:crypto';
22
- import { readFile } from 'node:fs/promises';
29
+ import { readFileSync } from 'node:fs';
23
30
  import { join } from 'node:path';
31
+ import { readAgentDefinitionKind } from '../runtime/shared/agent-kind.mjs';
32
+ import { BOT_AGENT_KIND, WORKFLOW_AGENT_KIND, isContactAgentId } from '../runtime/shared/agent-route-config.mjs';
33
+ import { botStoreActive, isStoreBot } from '../runtime/shared/agent-store.mjs';
34
+ import { readMarkdownDocument } from '../runtime/shared/markdown-frontmatter.mjs';
24
35
  import {
25
36
  MIXY_AGENT_ID,
26
37
  agentHomeSessionId,
@@ -31,20 +42,23 @@ import {
31
42
  isRoomThreadSessionId,
32
43
  roomIdOfSession,
33
44
  roomMemberOfSession,
45
+ roomSessionId,
34
46
  } from '../runtime/agent/orchestrator/session/agent-room.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,12 @@ 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
+ // A task report's output kept for its room row (newlines kept).
151
+ const ROOM_REPORT_TEXT_LIMIT = 4000;
152
+ // Waits before the 2nd, 3rd and 4th try of a relay (wake) turn that did not finish.
153
+ export const WAKE_RETRY_DELAYS_MS = Object.freeze([30_000, 120_000, 600_000]);
154
+ // Refused wake submissions of one batch, a minute apart, before it is given up.
155
+ export const WAKE_REFUSAL_LIMIT = 10;
103
156
 
104
157
  /** `homeId`'s wake obligations in `wakes`: posted task results not in a batch
105
158
  * yet (`owed`), those past the cap collapsed into one marker (`overflow`:
@@ -127,6 +180,83 @@ function withWakeState(doc, homeId, { owed, batches, overflow }) {
127
180
  return { ...doc, wakes };
128
181
  }
129
182
 
183
+ /** `own`'s per-target counts (`{ n, at }`) used within ROUND_TTL_MS of
184
+ * `nowMs`, the `cap` most recently counted. */
185
+ function freshRoundCounts(own, nowMs, cap) {
186
+ return Object.fromEntries(
187
+ Object.entries(own && typeof own === 'object' ? own : {})
188
+ .filter(([, count]) => count && typeof count === 'object' && nowMs - (Number(count.at) || 0) < ROUND_TTL_MS)
189
+ .sort(([, a], [, b]) => (Number(b.at) || 0) - (Number(a.at) || 0))
190
+ .slice(0, cap)
191
+ );
192
+ }
193
+
194
+ /** `doc` with `items` appended to `homeId`'s list, minus run ids it holds or
195
+ * posted; past `limit` the oldest drop, counted under `omitted`. Null when
196
+ * nothing changes. */
197
+ function appendedItems(doc, homeId, items, limit, omittedCount) {
198
+ const list = doc.homes[homeId] || [];
199
+ const seen = new Set([...list.map((row) => row.runId), ...(doc.delivered[homeId] || [])]);
200
+ const fresh = items.filter((item) => !seen.has(item.runId) && seen.add(item.runId));
201
+ if (!fresh.length && !omittedCount) return null;
202
+ const next = [...list, ...fresh];
203
+ const drop = Math.max(0, next.length - limit);
204
+ const omitted = { ...doc.omitted };
205
+ const count = drop + omittedCount;
206
+ if (count) {
207
+ const previous = omitted[homeId];
208
+ omitted[homeId] = { id: previous?.id || `omit_${next[0].runId}`, count: (previous?.count || 0) + count };
209
+ }
210
+ return { ...doc, homes: { ...doc.homes, [homeId]: next.slice(drop) }, omitted };
211
+ }
212
+
213
+ /** `doc.wakes` with the run ids `wake` owed a wake by `homeId`, at most
214
+ * `cap` per home: past it the oldest owed collapse into the overflow marker
215
+ * (`overflowed` counts them). Stored batches never change. */
216
+ function owedWakes(doc, homeId, wake, cap) {
217
+ const state = wakeState(doc.wakes, homeId);
218
+ const batched = state.batches.flatMap((batch) => batch.runIds);
219
+ const known = new Set([
220
+ ...state.owed,
221
+ ...batched,
222
+ ...[state.overflow?.oldest, state.overflow?.newest].filter(Boolean),
223
+ ]);
224
+ const added = wake.filter((runId) => !known.has(runId));
225
+ if (!added.length) return { wakes: doc.wakes, overflowed: 0 };
226
+ const owed = [...state.owed, ...added];
227
+ const room = Math.max(0, cap - batched.length);
228
+ const excess = owed.slice(0, Math.max(0, owed.length - room));
229
+ const overflow = excess.length
230
+ ? {
231
+ count: (state.overflow?.count || 0) + excess.length,
232
+ oldest: state.overflow?.oldest || excess[0],
233
+ newest: excess.at(-1),
234
+ }
235
+ : state.overflow;
236
+ const { wakes } = withWakeState(doc, homeId, { ...state, owed: owed.slice(excess.length), overflow });
237
+ return { wakes, overflowed: excess.length };
238
+ }
239
+
240
+ /** `doc` with the posted `runIds` of `homeId` dequeued into its posted
241
+ * ledger, and the posted omission notice `omitted` retired. */
242
+ function withDequeued(doc, homeId, runIds, omitted) {
243
+ const homes = { ...doc.homes };
244
+ const rest = (homes[homeId] || []).filter((row) => !runIds.includes(row.runId));
245
+ if (rest.length) homes[homeId] = rest;
246
+ else delete homes[homeId];
247
+ const delivered = { ...doc.delivered };
248
+ if (runIds.length) delivered[homeId] = [...(delivered[homeId] || []), ...runIds].slice(-DELIVERED_LEDGER);
249
+ const omittedNow = { ...doc.omitted };
250
+ const current = omittedNow[homeId];
251
+ if (omitted && current?.id === omitted.id) {
252
+ // More dropped while this pass posted: they get a notice of their own.
253
+ const left = current.count - omitted.count;
254
+ if (left > 0) omittedNow[homeId] = { id: `${current.id}.${omitted.count}`, count: left };
255
+ else delete omittedNow[homeId];
256
+ }
257
+ return { ...doc, homes, omitted: omittedNow, delivered };
258
+ }
259
+
130
260
  /** The persisted queue at `filePath`: `{ homes: { [homeSessionId]: item[] },
131
261
  * omitted: { [homeSessionId]: { id, count } }, delivered: { [homeSessionId]:
132
262
  * runId[] } }`. A home keeps at most `limit` items: past it the oldest drop,
@@ -136,15 +266,7 @@ export function createRoutineResultQueue(
136
266
  { limit = ROUTINE_LIMITS.homeQueueLimit, now = Date.now, caps = QUEUE_CAPS } = {}
137
267
  ) {
138
268
  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
- );
269
+ const freshRounds = (own) => freshRoundCounts(own, now(), caps.roundTargets);
148
270
  const tidyRounds = (rounds) =>
149
271
  Object.fromEntries(
150
272
  Object.entries(rounds || {})
@@ -162,39 +284,26 @@ export function createRoutineResultQueue(
162
284
  // Posted task results whose wake turn was not taken yet, per home.
163
285
  wakes: section(doc, 'wakes'),
164
286
  });
287
+ // A damaged queue file (empty, NUL-filled after a crash, malformed) is kept
288
+ // aside and its last good copy restored (readJsonState); it never blocks
289
+ // delivery. I/O errors still throw.
165
290
  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
- }
291
+ return parts(await readJsonState(filePath));
172
292
  }
173
293
  /** Read-modify-write under the file lock (`mutator(doc)` returns the next
174
294
  * doc, or nothing to keep the file); every write also retires stale and
175
295
  * surplus round counts. */
176
296
  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 };
297
+ return updateJsonAtomic(
298
+ filePath,
299
+ (raw) => {
300
+ const next = mutator(parts(raw));
301
+ return next ? { ...next, rounds: tidyRounds(next.rounds) } : undefined;
302
+ },
303
+ { backup: true }
304
+ );
197
305
  }
306
+ const appended = (doc, homeId, items, omittedCount = 0) => appendedItems(doc, homeId, items, limit, omittedCount);
198
307
  return {
199
308
  async pendingHomes() {
200
309
  const { homes, omitted, wakes } = await read();
@@ -237,6 +346,31 @@ export function createRoutineResultQueue(
237
346
  return withWakeState(doc, homeId, { ...state, batches });
238
347
  });
239
348
  },
349
+ /** Batch `batchId`'s relay turn did not finish: it is owed again under a
350
+ * fresh wake id (the old one names the failed turn), as try `tries`, not
351
+ * before `notBefore`. Returns the new batch; null when it is gone. */
352
+ async rewake(homeId, batchId, { tries, notBefore }) {
353
+ let next = null;
354
+ await update((doc) => {
355
+ next = null;
356
+ const state = wakeState(doc.wakes, homeId);
357
+ const index = state.batches.findIndex((batch) => batch.id === batchId);
358
+ if (index < 0) return undefined;
359
+ next = { ...state.batches[index], 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) =>
@@ -314,7 +448,9 @@ export function createRoutineResultQueue(
314
448
  (kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId
315
449
  );
316
450
  if (own.length >= caps.watches) {
317
- throw new Error(`${watch.homeSessionId} already waits on ${own.length} session reports`);
451
+ throw Object.assign(new Error(`${watch.homeSessionId} already waits on ${own.length} session reports`), {
452
+ code: 'REPORT_WATCH_CAP',
453
+ });
318
454
  }
319
455
  return { ...doc, watches: { ...watches, [watch.runId]: watch } };
320
456
  });
@@ -363,45 +499,9 @@ export function createRoutineResultQueue(
363
499
  async remove(homeId, runIds, omitted = null, { wake = [] } = {}) {
364
500
  let overflowed = 0;
365
501
  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 };
502
+ let wakes;
503
+ ({ wakes, overflowed } = owedWakes(doc, homeId, wake, caps.wakeRunIds));
504
+ return { ...withDequeued(doc, homeId, runIds, omitted), wakes };
405
505
  });
406
506
  return { overflowed };
407
507
  },
@@ -410,6 +510,281 @@ export function createRoutineResultQueue(
410
510
 
411
511
  const messageIdOf = (runId) => `routine_${runId}`;
412
512
 
513
+ /** Show `notice` live in `homeId` through the `live()` surface: its answer,
514
+ * or false (logged) when the surface threw. */
515
+ function noticeLive(live, log, homeId, notice) {
516
+ try {
517
+ return live()?.notice?.(homeId, notice);
518
+ } catch (error) {
519
+ log(`live notice in ${homeId} failed: ${error?.message || error}`);
520
+ return false;
521
+ }
522
+ }
523
+
524
+ /** Dequeue every queued item (and the omission) of `homeId` unposted, and
525
+ * its wake obligations. */
526
+ async function dropQueued(queue, homeId, items, omitted) {
527
+ await queue.remove(
528
+ homeId,
529
+ items.map((item) => item.runId),
530
+ omitted
531
+ );
532
+ await queue.dropWakes(homeId);
533
+ }
534
+
535
+ /** The queued item of `result` ({ runId, kind, ok, text, runSessionId }) of
536
+ * a schedule or webhook `row` (enqueue). */
537
+ function routineResultItem(row, result) {
538
+ return {
539
+ runId: String(result.runId),
540
+ kind: result.kind === 'webhook' ? 'webhook' : 'schedule',
541
+ name: row.name,
542
+ ok: result.ok === true,
543
+ text: routineSummary(result.text),
544
+ runSessionId: result.runSessionId || null,
545
+ ...(result.reason ? { reason: String(result.reason) } : {}),
546
+ ...(result.paused === true ? { paused: true } : {}),
547
+ at: Date.now(),
548
+ };
549
+ }
550
+
551
+ /** The run ids of `watches` a home at its watch `cap` gives up before
552
+ * storing `watch`: its oldest whose target sessions are proven deleted. */
553
+ function watchesToDrop(watches, watch, cap, sessionDeleted) {
554
+ const own = watches.filter((kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId);
555
+ const over = own.length + 1 - cap;
556
+ return over > 0
557
+ ? own
558
+ .filter((kept) => sessionDeleted(kept.taskSessionId))
559
+ .slice(0, over)
560
+ .map((kept) => kept.runId)
561
+ : [];
562
+ }
563
+
564
+ /** A bookkept home proven deleted: a bot's home whose definition is gone,
565
+ * or a room thread whose room is. Never Mixy's home, never on a missing
566
+ * thread record, never on an unknown answer. */
567
+ function homeProvenDeleted(homeId, agentPresence, roomPresence) {
568
+ const agentId = agentIdOfHomeSession(homeId);
569
+ if (agentId) return agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted';
570
+ const roomId = roomIdOfSession(homeId) || roomMemberOfSession(homeId)?.roomId || '';
571
+ return Boolean(roomId) && roomPresence(roomId) === 'deleted';
572
+ }
573
+
574
+ /** The queued item of one task report (enqueueTask). */
575
+ function taskReportItem({ runId, taskSessionId, title, status, text = '', target, files = [], omitted = 0, hop = 0 }) {
576
+ // The model-facing reply keeps its lines up to REPLY_LIMIT; the row's
577
+ // one-line summary is derived from it (agentTaskMessage).
578
+ const reply = boundedReply(text);
579
+ return {
580
+ runId: String(runId),
581
+ kind: 'agent-task',
582
+ taskSessionId,
583
+ title,
584
+ status,
585
+ text: reply.text,
586
+ // The UI row's one line comes from the whole reply, not its bounded part.
587
+ summary: routineSummary(text),
588
+ ...(reply.chars > reply.text.length ? { textChars: reply.chars } : {}),
589
+ // Any session other than a task session the agent created.
590
+ ...(target === 'session' ? { target } : {}),
591
+ // The files the turn produced (handoff-attachments.mjs collectTurnArtifacts).
592
+ ...(Array.isArray(files) && files.length ? { files, ...(omitted > 0 ? { omitted } : {}) } : {}),
593
+ roundTrip: status !== 'started' && status !== 'blocked',
594
+ ...(hop > 0 ? { hop } : {}),
595
+ at: Date.now(),
596
+ };
597
+ }
598
+
599
+ /** The room-log item of task report `item` by member `agentId` (`text`: the
600
+ * report's whole output, newlines kept). */
601
+ function roomReportItem(item, agentId, text) {
602
+ return {
603
+ runId: item.runId,
604
+ kind: 'room-report',
605
+ speaker: agentId,
606
+ taskSessionId: item.taskSessionId,
607
+ title: item.title,
608
+ status: item.status,
609
+ text: String(text ?? '')
610
+ .trim()
611
+ .slice(0, ROOM_REPORT_TEXT_LIMIT),
612
+ ...(item.target ? { target: item.target } : {}),
613
+ ...(item.files ? { files: item.files } : {}),
614
+ at: item.at,
615
+ };
616
+ }
617
+
618
+ /** Queue a room member's task report: the member's own session (`from`) gets
619
+ * the line without a wake; a blocked or ended turn (`toRoom`) is the
620
+ * member's row in the room log too. The watch drops with the last write, so
621
+ * a restart in between re-queues both (deduplicated). */
622
+ async function queueRoomTaskReport(queue, { from, roomLogId, toRoom }, item, agentId, text, unwatch) {
623
+ await queue.add(from, { ...item, roundTrip: false }, { unwatch: toRoom ? '' : unwatch });
624
+ if (toRoom) {
625
+ await queue.add(roomLogId, roomReportItem(item, agentId, text), { unwatch });
626
+ }
627
+ }
628
+
629
+ /** Append to `session.messages` the omission notice and the queued `items`
630
+ * not posted before. `fresh`: the rows newly posted; `toWake`: the run ids
631
+ * owed a wake (every dequeued task result, posted now or before). */
632
+ function postQueuedRows(session, items, omitted) {
633
+ const posted = new Set(session.messages.map((message) => message?.meta?.transcript?.messageId).filter(Boolean));
634
+ const fresh = [];
635
+ const omittedId = omitted ? messageIdOf(omitted.id) : '';
636
+ if (omitted && !posted.has(omittedId)) {
637
+ const message = routineOmittedMessage({ count: omitted.count, messageId: omittedId });
638
+ session.messages.push(message);
639
+ fresh.push({ message, notice: routineResultNotice(message) });
640
+ }
641
+ const toWake = [];
642
+ for (const item of items) {
643
+ const row = deliveredRow(item, messageIdOf(item.runId));
644
+ if (row.wake) toWake.push(item.runId);
645
+ if (posted.has(messageIdOf(item.runId))) continue;
646
+ session.messages.push(row.message);
647
+ fresh.push(row);
648
+ }
649
+ return { fresh, toWake };
650
+ }
651
+
652
+ /** The relay of posted task results: owed results wake the agent with one
653
+ * hidden turn per batch. A batch is stored with its wake id BEFORE the wake
654
+ * is submitted and dropped once its relay turn settled done, so a restart in
655
+ * between re-checks that exact id; a relay turn that did not finish is owed
656
+ * again under a fresh id (retryRelay). Results owed later form a new batch.
657
+ * A replayed run id is never queued again. */
658
+ function createHomeRelay({
659
+ queue,
660
+ saveSession,
661
+ live,
662
+ busy,
663
+ later,
664
+ drain,
665
+ log,
666
+ wakeRetryDelaysMs,
667
+ wakeRefusalLimit,
668
+ busyRetryMs,
669
+ missingRetryMs,
670
+ }) {
671
+ // Wake batch id → 'running' while this process's relay turn runs, then
672
+ // 'done' or 'failed' once it settled (until the batch is retired).
673
+ const relays = new Map();
674
+
675
+ /** How batch `batchId`'s relay turn went: '' when no turn took it, else
676
+ * 'running', 'done' or 'failed'. A turn an earlier process took decides by
677
+ * its recorded end; one with none was cut off by the restart. */
678
+ function relayOf(batchId, messages, taken) {
679
+ const local = relays.get(batchId);
680
+ if (local) return local;
681
+ if (!taken.has(batchId)) return '';
682
+ const outcome = submissionOutcome(messages, batchId);
683
+ return outcome && outcome !== 'open' && outcome.status === 'completed' ? 'done' : 'failed';
684
+ }
685
+
686
+ /** The relay turn of `batch` did not finish: owe it again after a backoff,
687
+ * or, past the last try, leave a note in the thread and wake nobody. */
688
+ async function retryRelay(homeId, session, batch) {
689
+ const tries = (Number(batch.tries) || 0) + 1;
690
+ if (tries <= wakeRetryDelaysMs.length) {
691
+ log(`the relay turn in ${homeId} did not finish (try ${tries}); waking it again later`);
692
+ await queue.rewake(homeId, batch.id, { tries, notBefore: Date.now() + wakeRetryDelaysMs[tries - 1] });
693
+ return true;
694
+ }
695
+ return abandonRelay(homeId, session, batch, `did not finish after ${tries} tries`, tries);
696
+ }
697
+
698
+ /** Give up on `batch`'s relay: a note in the thread (once), nobody woken.
699
+ * False while the thread is busy (try again once it is idle). */
700
+ async function abandonRelay(homeId, session, batch, why, tries) {
701
+ if (busy(homeId)) return false;
702
+ log(`the relay turn in ${homeId} ${why}; its results stay posted unrelayed`);
703
+ const messageId = messageIdOf(`relay_${batch.id}`);
704
+ if (!session.messages.some((message) => message?.meta?.transcript?.messageId === messageId)) {
705
+ const message = agentTaskRelayFailedMessage({
706
+ count: batch.runIds.length + (batch.overflow?.count || 0),
707
+ tries,
708
+ messageId,
709
+ });
710
+ session.messages.push(message);
711
+ session.updatedAt = Date.now();
712
+ await saveSession(session);
713
+ noticeLive(live, log, homeId, { ...routineResultNotice(message), messageId });
714
+ }
715
+ await queue.woke(homeId, batch.id);
716
+ return true;
717
+ }
718
+
719
+ /** Wake `homeId` (its idle `session`, just posted to) for its next owed batch. */
720
+ async function wakeOwed(homeId, session) {
721
+ const byId = new Map(session.messages.map((message) => [message?.meta?.transcript?.messageId, message]));
722
+ const taken = new Set(session.messages.flatMap((message) => submissionIdsOf(message)));
723
+ let batch = null;
724
+ let woken = [];
725
+ let more = 0;
726
+ for (;;) {
727
+ batch = (await queue.wakes(homeId)).batches[0] || (await queue.batchWake(homeId));
728
+ if (!batch) return;
729
+ const relay = relayOf(batch.id, session.messages, taken);
730
+ // Its settle drains again.
731
+ if (relay === 'running') return;
732
+ if (relay) {
733
+ relays.delete(batch.id);
734
+ if (relay === 'done') await queue.woke(homeId, batch.id);
735
+ else if (!(await retryRelay(homeId, session, batch))) return later(homeId, busyRetryMs);
736
+ continue;
737
+ }
738
+ const wait = (Number(batch.notBefore) || 0) - Date.now();
739
+ if (wait > 0) return later(homeId, wait);
740
+ woken = batch.runIds.map((runId) => byId.get(messageIdOf(runId))).filter(Boolean);
741
+ // Results past the wake cap ride this wake as a count of rows to read.
742
+ more = byId.has(messageIdOf(batch.overflow?.newest)) ? batch.overflow.count : 0;
743
+ if (woken.length || more) break;
744
+ // Its rows are gone with a thread reset.
745
+ await queue.woke(homeId, batch.id);
746
+ }
747
+ const surface = live();
748
+ if (typeof surface?.wake !== 'function') return later(homeId, busyRetryMs);
749
+ const batchId = batch.id;
750
+ let turn;
751
+ relays.set(batchId, 'running');
752
+ try {
753
+ // The turn descends from the bot-to-bot sends it reports: it carries their highest hop.
754
+ const hop = Math.max(0, ...woken.map((message) => Number(message?.meta?.routine?.hop) || 0));
755
+ turn = await surface.wake(homeId, agentTaskWakePrompt(woken, { more }), { id: batchId, hop });
756
+ if (turn === false) throw new Error('the wake turn was refused');
757
+ } catch (error) {
758
+ relays.delete(batchId);
759
+ // A wake refused (or failing) every time is retried a bounded number of times.
760
+ const refusals = (Number(batch.refusals) || 0) + 1;
761
+ if (refusals >= wakeRefusalLimit) {
762
+ if (await abandonRelay(homeId, session, batch, `could not start (${refusals} refusals)`, refusals)) return;
763
+ return later(homeId, busyRetryMs);
764
+ }
765
+ await queue.markBatch(homeId, batchId, { refusals });
766
+ log(`waking ${homeId} failed (${refusals}/${wakeRefusalLimit}), retrying: ${error?.message || error}`);
767
+ return later(homeId, missingRetryMs);
768
+ }
769
+ // `settled` resolves the relay turn's end status: only 'done' retires the batch.
770
+ if (typeof turn?.settled?.then === 'function') {
771
+ const settled = (status) => {
772
+ relays.set(batchId, status === 'done' ? 'done' : 'failed');
773
+ void drain(homeId);
774
+ };
775
+ turn.settled.then(settled, () => settled('failed'));
776
+ return;
777
+ }
778
+ // A surface that cannot report the turn's end: taking it is relaying it.
779
+ relays.delete(batchId);
780
+ await queue.woke(homeId, batchId);
781
+ // Another batch left by an earlier process wakes once this turn is done.
782
+ if (owesWake(await queue.wakes(homeId))) later(homeId, busyRetryMs);
783
+ }
784
+
785
+ return { wakeOwed };
786
+ }
787
+
413
788
  export function createAgentHomeDelivery({
414
789
  loadSession,
415
790
  saveSession,
@@ -427,6 +802,19 @@ export function createAgentHomeDelivery({
427
802
  // True only on positive evidence that session `id` was deleted; an
428
803
  // unreadable or unknown record is not deleted.
429
804
  sessionDeleted = () => false,
805
+ // 'bot' | 'none' | 'unknown' per agent id: whether it can have a home chat
806
+ // at all. 'none' (proven: a workflow agent such as Reviewer) drops the rows
807
+ // queued for its missing home instead of waiting on it forever.
808
+ homeAgent = () => 'unknown',
809
+ // Waits before each new try of a relay turn that did not finish; past the
810
+ // last one the results stay posted with a note and nobody is woken.
811
+ wakeRetryDelaysMs = WAKE_RETRY_DELAYS_MS,
812
+ // Refused or failing wake submissions of one batch (missingRetryMs apart)
813
+ // before it is given up the same way.
814
+ wakeRefusalLimit = WAKE_REFUSAL_LIMIT,
815
+ // Post queued room task reports into a room log (agent-room-delivery.mjs).
816
+ postRoomReports = async (roomLogId, items) =>
817
+ (await import('./agent-room-delivery.mjs')).appendRoomTaskReports(roomLogId, items),
430
818
  log = (line) => process.stderr.write(`mixdog routines: ${line}\n`),
431
819
  }) {
432
820
  const busy = (sessionId) => {
@@ -441,6 +829,20 @@ export function createAgentHomeDelivery({
441
829
  const again = new Set(); // homes enqueued to while their pass ran
442
830
  const waiting = new Set(); // homes with a retry timer armed
443
831
  const shownLive = new Set(); // held-request message ids shown live in a busy thread
832
+ const waitLogged = new Set(); // missing homes whose wait was logged
833
+ const relay = createHomeRelay({
834
+ queue,
835
+ saveSession,
836
+ live,
837
+ busy,
838
+ later,
839
+ drain,
840
+ log,
841
+ wakeRetryDelaysMs,
842
+ wakeRefusalLimit,
843
+ busyRetryMs,
844
+ missingRetryMs,
845
+ });
444
846
 
445
847
  /** A held request waits on the user, possibly raised by the very turn that
446
848
  * keeps its thread busy (a routine turn): show it live at once; the stored
@@ -451,11 +853,7 @@ export function createAgentHomeDelivery({
451
853
  const messageId = messageIdOf(item.runId);
452
854
  if (shownLive.has(messageId)) continue;
453
855
  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
- }
856
+ noticeLive(live, log, homeId, { ...deliveredRow(item, messageId).notice, messageId });
459
857
  }
460
858
  }
461
859
 
@@ -468,8 +866,48 @@ export function createAgentHomeDelivery({
468
866
  }, ms);
469
867
  }
470
868
 
869
+ /** A room thread proven gone: its queued rows are dropped, never retried. */
870
+ async function dropGoneRoomThread(homeId, items, omitted) {
871
+ if (!isRoomThreadSessionId(homeId) || !(homeDeleted(homeId) || sessionDeleted(homeId))) return false;
872
+ log(`${items.length} task report(s) for the deleted ${homeId} are dropped`);
873
+ await dropQueued(queue, homeId, items, omitted);
874
+ return true;
875
+ }
876
+
877
+ /** A missing home of an agent that can never have one (no bot): its queued
878
+ * rows are dropped with a log line, never retried. */
879
+ async function dropHomeless(homeId, items, omitted) {
880
+ const agentId = agentIdOfHomeSession(homeId);
881
+ if (!agentId || agentId === MIXY_AGENT_ID || homeAgent(agentId) !== 'none') return false;
882
+ log(`${items.length} result(s) for ${homeId} are dropped: ${agentId} is not a bot, so it has no chat`);
883
+ await dropQueued(queue, homeId, items, omitted);
884
+ return true;
885
+ }
886
+
887
+ /** Post every queued task report of the room log `roomLogId` as member rows. */
888
+ async function drainRoomLog(roomLogId) {
889
+ const items = await queue.items(roomLogId);
890
+ const omitted = await queue.omitted(roomLogId);
891
+ if (!items.length && !omitted) return;
892
+ const session = loadSession(roomLogId);
893
+ if (!session || session.closed === true) {
894
+ if (await dropGoneRoomThread(roomLogId, items, omitted)) return;
895
+ log(`${items.length} task report(s) wait for the room log ${roomLogId}`);
896
+ return later(roomLogId, missingRetryMs);
897
+ }
898
+ // A room log has no notice row: the dropped count is only logged.
899
+ if (omitted) log(`${omitted.count} older task report(s) for ${roomLogId} were dropped: its queue was full`);
900
+ if (items.length) await postRoomReports(roomLogId, items);
901
+ await queue.remove(
902
+ roomLogId,
903
+ items.map((item) => item.runId),
904
+ omitted
905
+ );
906
+ }
907
+
471
908
  /** Post every queued result of `homeId` if its thread is idle; else retry later. */
472
909
  async function drainOnce(homeId) {
910
+ if (roomIdOfSession(homeId)) return drainRoomLog(homeId);
473
911
  const items = await queue.items(homeId);
474
912
  const omitted = await queue.omitted(homeId);
475
913
  const owed = owesWake(await queue.wakes(homeId));
@@ -491,37 +929,23 @@ export function createAgentHomeDelivery({
491
929
  // From here to the push nothing awaits, so no turn can start in between.
492
930
  const session = loadSession(homeId);
493
931
  if (!session || session.closed === true) {
494
- log(`${items.length} result(s) wait for the home thread ${homeId}`);
932
+ if (await dropGoneRoomThread(homeId, items, omitted)) return;
933
+ if (await dropHomeless(homeId, items, omitted)) return;
934
+ // Kept (a bot, or an agent whose kind cannot be read): logged once per process.
935
+ if (!waitLogged.has(homeId)) {
936
+ waitLogged.add(homeId);
937
+ log(`${items.length} result(s) wait for the home thread ${homeId}`);
938
+ }
495
939
  return later(homeId, missingRetryMs);
496
940
  }
497
941
  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
- }
942
+ const { fresh, toWake } = postQueuedRows(session, items, omitted);
515
943
  for (const item of items) shownLive.delete(messageIdOf(item.runId));
516
944
  if (fresh.length) {
517
945
  session.updatedAt = Date.now();
518
946
  await saveSession(session);
519
947
  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
- }
948
+ noticeLive(live, log, homeId, { ...notice, messageId: message.meta.transcript.messageId });
525
949
  }
526
950
  }
527
951
  // Dequeuing and owing the wake are one write: no restart between posting
@@ -533,39 +957,7 @@ export function createAgentHomeDelivery({
533
957
  { wake: toWake }
534
958
  );
535
959
  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);
960
+ await relay.wakeOwed(homeId, session);
569
961
  }
570
962
 
571
963
  /** The single drainer of `homeId`; a call during a pass runs one more pass. */
@@ -598,17 +990,7 @@ export function createAgentHomeDelivery({
598
990
  async function enqueue(row, result) {
599
991
  const homeId = agentHomeSessionId(scheduleOwnerAgentId(row));
600
992
  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
- });
993
+ await queue.add(homeId, routineResultItem(row, result));
612
994
  void drain(homeId);
613
995
  return homeId;
614
996
  }
@@ -631,30 +1013,22 @@ export function createAgentHomeDelivery({
631
1013
  /** Persist one report of a coding task session `agentId` created (session
632
1014
  * create) for its home thread, then start draining it. `status` 'started'
633
1015
  * 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
- ) {
1016
+ async function enqueueTask(agentId, report) {
1017
+ const { runId, taskSessionId, status, text = '', watchId = '', from = '' } = report;
638
1018
  const homeId = agentHomeSessionId(agentId) || agentHomeSessionId(MIXY_AGENT_ID);
639
1019
  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
- );
1020
+ const item = taskReportItem(report);
1021
+ const unwatch = watchId ? String(watchId) : '';
1022
+ const member = roomMemberOfSession(from);
1023
+ if (member?.agentId === agentId) {
1024
+ const roomLogId = roomSessionId(member.roomId);
1025
+ const toRoom = status !== 'started';
1026
+ await queueRoomTaskReport(queue, { from, roomLogId, toRoom }, item, agentId, text, unwatch);
1027
+ void drain(from);
1028
+ if (toRoom) void drain(roomLogId);
1029
+ return toRoom ? roomLogId : from;
1030
+ }
1031
+ await queue.add(homeId, item, { unwatch });
658
1032
  void drain(homeId);
659
1033
  return homeId;
660
1034
  }
@@ -667,23 +1041,10 @@ export function createAgentHomeDelivery({
667
1041
  const homeId = agentHomeSessionId(scheduleOwnerAgentId(row));
668
1042
  if (!run?.runId || busy(homeId)) return false;
669
1043
  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
- }
1044
+ return noticeLive(live, log, homeId, notice) === true;
676
1045
  }
677
1046
 
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
- }
1047
+ const homeDeleted = (homeId) => homeProvenDeleted(homeId, agentPresence, roomPresence);
687
1048
 
688
1049
  /** Retire the bookkeeping of deleted homes (and stale round counts). */
689
1050
  async function prune() {
@@ -694,17 +1055,7 @@ export function createAgentHomeDelivery({
694
1055
  * whose target sessions are proven deleted, and refuses the watch (so the
695
1056
  * send claims no report) when that frees too few. */
696
1057
  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
- : [];
1058
+ const drop = watchesToDrop(await queue.watches(), watch, queue.watchCap, sessionDeleted);
708
1059
  if (drop.length) {
709
1060
  log(`${watch.homeSessionId} is at its watch cap: dropped ${drop.length} watch(es) of deleted sessions`);
710
1061
  }
@@ -748,6 +1099,7 @@ async function processDelivery() {
748
1099
  { createAgentEditorApi },
749
1100
  { probePath, PROBE_ABSENT },
750
1101
  { sessionPath },
1102
+ { mixdogRoot },
751
1103
  ] = await Promise.all([
752
1104
  import('../runtime/agent/orchestrator/session/store.mjs'),
753
1105
  import('../runtime/agent/orchestrator/session/manager/runtime-liveness.mjs'),
@@ -755,6 +1107,7 @@ async function processDelivery() {
755
1107
  import('./workflow-agents-api/agent-editor.mjs'),
756
1108
  import('../runtime/agent/orchestrator/session/store/fs-probe.mjs'),
757
1109
  import('../runtime/agent/orchestrator/session/store/session-paths.mjs'),
1110
+ import('../runtime/shared/plugin-paths.mjs'),
758
1111
  ]);
759
1112
  // The editor's per-id definition probe: 'deleted' only when proven gone.
760
1113
  const { agentDefinitionPresence } = createAgentEditorApi({ cfgMod: { getPluginData } });
@@ -764,6 +1117,9 @@ async function processDelivery() {
764
1117
  turnInFlight: isSessionTurnInFlight,
765
1118
  queue: createRoutineResultQueue(join(getPluginData(), 'routine-results.json')),
766
1119
  agentPresence: (agentId) => agentDefinitionPresence([agentId])[agentId],
1120
+ // A home chat exists only for bots; only a definition read successfully
1121
+ // can prove an agent is none (readHomeAgent).
1122
+ homeAgent: (agentId) => readHomeAgent({ dataDir: getPluginData(), shippedRoot: mixdogRoot(), id: agentId }),
767
1123
  // Deleting a session unlinks its record (session-delete.mjs) and leaves
768
1124
  // no tombstone: only an absent file with no in-memory copy proves it.
769
1125
  // An unreadable file probes 'unreadable' and stays.