mixdog 1.2.6 → 1.3.1

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 (294) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +71 -9
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +14 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +178 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent/AGENT.md +4 -0
  18. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  19. package/src/rules/agent-mode/MIXY.md +28 -0
  20. package/src/rules/agent-mode/ROUTINE.md +6 -0
  21. package/src/rules/lead/LEAD.md +7 -3
  22. package/src/rules/shared/70-delivery.md +3 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  24. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  25. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
  26. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  27. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
  28. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  29. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  30. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  31. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  34. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  35. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  36. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  37. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  44. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  46. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  52. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  53. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  54. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
  58. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  61. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
  63. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
  64. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  66. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  67. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  68. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  69. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  70. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
  71. package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
  72. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  73. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  75. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
  76. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  77. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  78. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  79. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  80. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  81. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  82. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  83. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  84. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
  85. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  86. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  87. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  88. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  89. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
  90. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  91. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  92. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  93. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  94. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  96. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  97. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  98. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  99. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  100. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
  101. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
  102. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  103. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  104. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  105. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  106. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
  108. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  109. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
  110. package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
  111. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  112. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  114. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  115. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  116. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
  118. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  119. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  121. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  123. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  124. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  125. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  127. package/src/runtime/attachments/store.mjs +6 -0
  128. package/src/runtime/browser-bridge/client.mjs +24 -0
  129. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  130. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  131. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  132. package/src/runtime/channels/lib/webhook.mjs +0 -1
  133. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  134. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  135. package/src/runtime/computer-bridge/client.mjs +28 -1
  136. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  137. package/src/runtime/connectors/google-mcp.mjs +446 -0
  138. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  139. package/src/runtime/effort-judge/model-manifest.json +7 -7
  140. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  141. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  142. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  143. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  144. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  145. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  146. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  147. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  148. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  149. package/src/runtime/memory/lib/memory.mjs +7 -1
  150. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  151. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  152. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  153. package/src/runtime/memory/tool-defs.mjs +5 -0
  154. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  155. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  156. package/src/runtime/shared/agent-avatar.mjs +320 -0
  157. package/src/runtime/shared/agent-kind.mjs +152 -0
  158. package/src/runtime/shared/agent-route-config.d.mts +4 -0
  159. package/src/runtime/shared/agent-route-config.mjs +30 -0
  160. package/src/runtime/shared/agent-store.mjs +132 -0
  161. package/src/runtime/shared/atomic-file.mjs +14 -1
  162. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  163. package/src/runtime/shared/config-patch.mjs +20 -1
  164. package/src/runtime/shared/config.mjs +8 -1
  165. package/src/runtime/shared/connector-accounts.mjs +15 -0
  166. package/src/runtime/shared/connector-catalog.d.mts +51 -0
  167. package/src/runtime/shared/connector-catalog.mjs +228 -0
  168. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  169. package/src/runtime/shared/connector-triggers.mjs +45 -0
  170. package/src/runtime/shared/err-text.mjs +3 -1
  171. package/src/runtime/shared/error-presentation.mjs +91 -6
  172. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  173. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  174. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  175. package/src/runtime/shared/held-request-host.d.mts +12 -0
  176. package/src/runtime/shared/held-request-host.mjs +71 -0
  177. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  178. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  179. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  180. package/src/runtime/shared/plain-preview.d.mts +1 -0
  181. package/src/runtime/shared/plain-preview.mjs +33 -0
  182. package/src/runtime/shared/routine-limits.d.mts +11 -0
  183. package/src/runtime/shared/routine-limits.mjs +21 -0
  184. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  185. package/src/runtime/shared/routine-trigger.mjs +44 -0
  186. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  187. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  188. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  189. package/src/runtime/shared/slow-span.mjs +4 -12
  190. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  191. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  192. package/src/runtime/shared/turn-changes.mjs +0 -8
  193. package/src/runtime/shared/usage-readout-totals.mjs +58 -0
  194. package/src/runtime/shared/user-data-guard.mjs +119 -38
  195. package/src/session-runtime/agent-home-delivery.mjs +826 -0
  196. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  197. package/src/session-runtime/agent-room-delivery.mjs +133 -0
  198. package/src/session-runtime/agent-task-sessions.mjs +460 -0
  199. package/src/session-runtime/boot/apis.mjs +63 -18
  200. package/src/session-runtime/boot/facade.mjs +5 -8
  201. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  202. package/src/session-runtime/boot/providers.mjs +14 -4
  203. package/src/session-runtime/boot/tools.mjs +3 -0
  204. package/src/session-runtime/channel-config-api.mjs +53 -5
  205. package/src/session-runtime/config-lifecycle.mjs +43 -2
  206. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  207. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  208. package/src/session-runtime/handoff-attachments.mjs +593 -0
  209. package/src/session-runtime/held-requests.mjs +228 -0
  210. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  211. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  212. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  213. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
  214. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  215. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  216. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  217. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  218. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  219. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  220. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  221. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  222. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  223. package/src/session-runtime/model-route/set-route.mjs +35 -4
  224. package/src/session-runtime/native-web-search.mjs +2 -2
  225. package/src/session-runtime/new-session-config.mjs +6 -1
  226. package/src/session-runtime/owned-automation-run.mjs +229 -0
  227. package/src/session-runtime/resource-api.mjs +5 -0
  228. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  229. package/src/session-runtime/resource-skills-api.mjs +12 -0
  230. package/src/session-runtime/room-member-gate.mjs +48 -0
  231. package/src/session-runtime/route-resolution.mjs +5 -11
  232. package/src/session-runtime/routine-home-turn.mjs +156 -0
  233. package/src/session-runtime/routine-owner.mjs +48 -0
  234. package/src/session-runtime/schedule-session-run.mjs +30 -64
  235. package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
  236. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  237. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  238. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  239. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  240. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  241. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  242. package/src/session-runtime/services/channel-admin.mjs +93 -24
  243. package/src/session-runtime/services/memory-access.mjs +37 -0
  244. package/src/session-runtime/services/provider-admin.mjs +74 -39
  245. package/src/session-runtime/services/schedule-access.mjs +91 -0
  246. package/src/session-runtime/session-create/session-options.mjs +34 -7
  247. package/src/session-runtime/session-hooks.mjs +16 -8
  248. package/src/session-runtime/setup-tool/executor.mjs +79 -6
  249. package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
  250. package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
  251. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  252. package/src/session-runtime/skills-api.mjs +16 -1
  253. package/src/session-runtime/tool-policy-refresh.mjs +249 -29
  254. package/src/session-runtime/turn/session-ops.mjs +8 -1
  255. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  256. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
  257. package/src/session-runtime/webhook-session-run.mjs +49 -67
  258. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  259. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
  260. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  261. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  262. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  263. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  264. package/src/standalone/daemon-agent-control.mjs +1 -8
  265. package/src/standalone/daemon-session-control.mjs +21 -4
  266. package/src/standalone/daemon.mjs +31 -5
  267. package/src/standalone/session-protocol.mjs +27 -0
  268. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  269. package/src/standalone/session-service/agent-tree.mjs +4 -3
  270. package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
  271. package/src/standalone/session-service.mjs +19 -0
  272. package/src/tui/app/slash-commands.mjs +20 -0
  273. package/src/tui/dist/index.mjs +234 -95
  274. package/src/tui/session/context-state.mjs +6 -1
  275. package/src/tui/session/notice-surface.mjs +34 -4
  276. package/src/tui/session/queue-helpers.mjs +2 -1
  277. package/src/tui/session/session-api/extensions.mjs +4 -0
  278. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  279. package/src/tui/session/session-api/integrations.mjs +8 -0
  280. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  281. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  282. package/src/tui/session/session-api/routes.mjs +61 -5
  283. package/src/tui/session/session-flow/drain.mjs +12 -3
  284. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  285. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  286. package/src/tui/session/session-flow/steering.mjs +58 -24
  287. package/src/tui/session/tool-approval.mjs +26 -5
  288. package/src/tui/session/transcript-intake.mjs +8 -2
  289. package/src/tui/session/transcript-spill.mjs +5 -1
  290. package/src/tui/session/tui-steering-persist.mjs +65 -42
  291. package/src/tui/session/turn-live-call.mjs +64 -0
  292. package/src/tui/session/turn.mjs +34 -8
  293. package/src/session-runtime/automation-agents.mjs +0 -94
  294. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -0,0 +1,826 @@
1
+ /**
2
+ * Rows posted into an agent's home thread (`sess_home_<owner>`) from outside
3
+ * its turns: held requests of unattended runs, reports of task sessions the
4
+ * agent created, and routine results left by runs from before routines ran as
5
+ * turns in the thread itself (routine-home-turn.mjs).
6
+ *
7
+ * Delivery is durable. The finished run is first ENQUEUED in a persisted
8
+ * per-home queue (`routine-results.json`), keyed by the run id, so a retried
9
+ * enqueue adds nothing and a crash between posting and dequeuing never posts
10
+ * twice (the posted message id is derived from the run id). One drainer per
11
+ * home appends every queued result FIFO once the thread is idle; posting never
12
+ * starts a turn. A home thread that does not exist yet keeps its results
13
+ * queued until it does; a deleted owner's results move to Mixy's home. Each
14
+ * home keeps at most ROUTINE_LIMITS.homeQueueLimit results (the oldest drop
15
+ * behind one "n omitted" notice), and posted run ids stay in a ledger so a
16
+ * replay never posts twice, even after the thread was reset. The daemon
17
+ * re-drains every queue on start, and
18
+ * registers the live surface so a thread open in a window shows the notice at
19
+ * once; a cold thread picks it up from its stored record.
20
+ */
21
+ import { randomUUID } from 'node:crypto';
22
+ import { readFile } from 'node:fs/promises';
23
+ import { join } from 'node:path';
24
+ import {
25
+ MIXY_AGENT_ID,
26
+ agentHomeSessionId,
27
+ agentIdOfHomeSession,
28
+ isAgentHomeSessionId,
29
+ } from '../runtime/agent/orchestrator/session/agent-home.mjs';
30
+ import {
31
+ isRoomThreadSessionId,
32
+ roomIdOfSession,
33
+ roomMemberOfSession,
34
+ } from '../runtime/agent/orchestrator/session/agent-room.mjs';
35
+ import {
36
+ TASK_LOOP_CAP,
37
+ agentTaskMessage,
38
+ agentTaskWakePrompt,
39
+ routineOmittedMessage,
40
+ routineResultMessage,
41
+ routineResultNotice,
42
+ routineRunNotice,
43
+ routineSummary,
44
+ } from '../runtime/agent/orchestrator/session/routine-result.mjs';
45
+ import { parseJsonText, updateJsonAtomic } from '../runtime/shared/atomic-file.mjs';
46
+ import { ROUTINE_LIMITS } from '../runtime/shared/routine-limits.mjs';
47
+ import { submissionIdsOf } from './agent-task-sessions.mjs';
48
+ import {
49
+ heldRequestMessage,
50
+ heldRequestNotice,
51
+ } from '../runtime/agent/orchestrator/session/human-gate/held-message.mjs';
52
+
53
+ /** A queued item's stored message and live notice: a routine result, or a
54
+ * held request of an unattended run (session-runtime/held-requests.mjs). */
55
+ function deliveredRow(item, messageId) {
56
+ if (item.kind === 'held-request') {
57
+ const message = heldRequestMessage({ ...item.held, at: item.at, messageId });
58
+ return { message, notice: heldRequestNotice(message) };
59
+ }
60
+ if (item.kind === 'agent-task') {
61
+ // A turn's end (not its start or a wait for the user) wakes the agent —
62
+ // except past the round-trip cap with another agent's own thread, where
63
+ // each wake could answer the other agent again without end.
64
+ const capped =
65
+ (isAgentHomeSessionId(item.taskSessionId) || isRoomThreadSessionId(item.taskSessionId)) &&
66
+ (item.round || 0) > TASK_LOOP_CAP;
67
+ const message = agentTaskMessage({ ...item, messageId, ...(capped ? { woken: false } : {}) });
68
+ return { message, notice: routineResultNotice(message), wake: item.roundTrip === true && !capped };
69
+ }
70
+ const message = routineResultMessage({ ...item, messageId });
71
+ return { message, notice: routineResultNotice(message) };
72
+ }
73
+
74
+ /** The agent a schedule belongs to; ownerless rows belong to Mixy. */
75
+ export function scheduleOwnerAgentId(schedule) {
76
+ const owner = typeof schedule?.ownerAgentId === 'string' ? schedule.ownerAgentId.trim() : '';
77
+ return owner && agentHomeSessionId(owner) ? owner : MIXY_AGENT_ID;
78
+ }
79
+
80
+ let liveSurface = null;
81
+ /** `{ busy(sessionId), notice(sessionId, notice), wake(sessionId, prompt, { id }),
82
+ * submit(sessionId, prompt, options), abort(sessionId) }` from the session
83
+ * service; `wake` starts one hidden turn with submission `id` (resolves
84
+ * false when it was refused), `submit` queues a routine turn
85
+ * (resolves whether it was accepted), `abort` stops the running turn. */
86
+ export function setAgentHomeLiveSurface(surface) {
87
+ liveSurface = surface || null;
88
+ }
89
+
90
+ /** The registered live surface (group-room rows post through it too). */
91
+ export function agentHomeLiveSurface() {
92
+ return liveSurface;
93
+ }
94
+
95
+ // Run ids already posted, kept per home so a replayed run never posts twice,
96
+ // even into a thread that was reset ("대화 새로 시작") since.
97
+ const DELIVERED_LEDGER = 500;
98
+ // A target's round-trip count left idle this long starts over.
99
+ export const ROUND_TTL_MS = 30 * 24 * 60 * 60 * 1000;
100
+ // Per home: round-trip targets counted (least recently counted drop first),
101
+ // reporting turns watched at once, and posted results owed a wake.
102
+ export const QUEUE_CAPS = Object.freeze({ roundTargets: 100, watches: 200, wakeRunIds: 100 });
103
+
104
+ /** `homeId`'s wake obligations in `wakes`: posted task results not in a batch
105
+ * yet (`owed`), those past the cap collapsed into one marker (`overflow`:
106
+ * `{ count, oldest, newest }` run ids) the next batch takes over, and
107
+ * batches (`{ id, runIds, overflow? }`) stored before their wake turn was
108
+ * submitted under that id. */
109
+ function wakeState(wakes, homeId) {
110
+ const raw = wakes[homeId];
111
+ if (Array.isArray(raw)) return { owed: raw, batches: [], overflow: null };
112
+ return {
113
+ owed: Array.isArray(raw?.owed) ? raw.owed : [],
114
+ batches: Array.isArray(raw?.batches)
115
+ ? raw.batches.filter((batch) => typeof batch?.id === 'string' && Array.isArray(batch.runIds))
116
+ : [],
117
+ overflow: raw?.overflow?.count > 0 ? raw.overflow : null,
118
+ };
119
+ }
120
+
121
+ const owesWake = (state) => state.owed.length > 0 || state.batches.length > 0 || Boolean(state.overflow);
122
+
123
+ function withWakeState(doc, homeId, { owed, batches, overflow }) {
124
+ const wakes = { ...doc.wakes };
125
+ if (owesWake({ owed, batches, overflow })) wakes[homeId] = { owed, batches, ...(overflow ? { overflow } : {}) };
126
+ else delete wakes[homeId];
127
+ return { ...doc, wakes };
128
+ }
129
+
130
+ /** The persisted queue at `filePath`: `{ homes: { [homeSessionId]: item[] },
131
+ * omitted: { [homeSessionId]: { id, count } }, delivered: { [homeSessionId]:
132
+ * runId[] } }`. A home keeps at most `limit` items: past it the oldest drop,
133
+ * counted under `omitted` so the thread gets one "n omitted" notice. */
134
+ export function createRoutineResultQueue(
135
+ filePath,
136
+ { limit = ROUTINE_LIMITS.homeQueueLimit, now = Date.now, caps = QUEUE_CAPS } = {}
137
+ ) {
138
+ 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
+ );
148
+ const tidyRounds = (rounds) =>
149
+ Object.fromEntries(
150
+ Object.entries(rounds || {})
151
+ .map(([homeId, own]) => [homeId, freshRounds(own)])
152
+ .filter(([, own]) => Object.keys(own).length)
153
+ );
154
+ const parts = (doc) => ({
155
+ homes: section(doc, 'homes'),
156
+ omitted: section(doc, 'omitted'),
157
+ delivered: section(doc, 'delivered'),
158
+ // Agent-initiated round trips per task session (session create).
159
+ rounds: section(doc, 'rounds'),
160
+ // Reporting turns not yet queued, by submission id (agent-task-sessions.mjs).
161
+ watches: section(doc, 'watches'),
162
+ // Posted task results whose wake turn was not taken yet, per home.
163
+ wakes: section(doc, 'wakes'),
164
+ });
165
+ 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
+ }
172
+ }
173
+ /** Read-modify-write under the file lock (`mutator(doc)` returns the next
174
+ * doc, or nothing to keep the file); every write also retires stale and
175
+ * surplus round counts. */
176
+ 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 };
197
+ }
198
+ return {
199
+ async pendingHomes() {
200
+ const { homes, omitted, wakes } = await read();
201
+ return [
202
+ ...new Set([
203
+ ...Object.keys(homes).filter((id) => homes[id]?.length),
204
+ ...Object.keys(omitted),
205
+ ...Object.keys(wakes).filter((id) => owesWake(wakeState(wakes, id))),
206
+ ]),
207
+ ];
208
+ },
209
+ /** `homeId`'s wake obligations: `{ owed, batches, overflow }` (wakeState). */
210
+ async wakes(homeId) {
211
+ return wakeState((await read()).wakes, homeId);
212
+ },
213
+ /** Store the owed results (and overflow marker) of `homeId` as one new
214
+ * batch under a fresh wake id, before its wake is submitted; null when
215
+ * none are owed. */
216
+ async batchWake(homeId) {
217
+ let batch = null;
218
+ await update((doc) => {
219
+ batch = null;
220
+ const state = wakeState(doc.wakes, homeId);
221
+ if (!state.owed.length && !state.overflow) return undefined;
222
+ batch = {
223
+ id: `awake_${randomUUID()}`,
224
+ runIds: state.owed,
225
+ ...(state.overflow ? { overflow: state.overflow } : {}),
226
+ };
227
+ return withWakeState(doc, homeId, { owed: [], batches: [...state.batches, batch], overflow: null });
228
+ });
229
+ return batch;
230
+ },
231
+ /** The wake of batch `batchId` was taken (or is moot): it is owed no more. */
232
+ async woke(homeId, batchId) {
233
+ await update((doc) => {
234
+ const state = wakeState(doc.wakes, homeId);
235
+ const batches = state.batches.filter((batch) => batch.id !== batchId);
236
+ if (batches.length === state.batches.length) return undefined;
237
+ return withWakeState(doc, homeId, { ...state, batches });
238
+ });
239
+ },
240
+ /** Drop every wake obligation of `homeId` (its owner is gone). */
241
+ async dropWakes(homeId) {
242
+ await update((doc) =>
243
+ doc.wakes[homeId] ? withWakeState(doc, homeId, { owed: [], batches: [], overflow: null }) : undefined
244
+ );
245
+ },
246
+ /** Every home the rounds, wakes, posted ledger or watches name. */
247
+ async bookkeptHomes() {
248
+ const doc = await read();
249
+ const watched = Object.values(doc.watches).map((watch) => watch?.homeSessionId);
250
+ return [
251
+ ...new Set(
252
+ [...Object.keys(doc.rounds), ...Object.keys(doc.wakes), ...Object.keys(doc.delivered), ...watched].filter(
253
+ (id) => typeof id === 'string' && id
254
+ )
255
+ ),
256
+ ];
257
+ },
258
+ /** Retire the rounds, wakes, posted ledger and watches of the `gone`
259
+ * homes (and, as every write does, stale round counts). */
260
+ async prune(gone) {
261
+ await update((doc) => {
262
+ const kept = (part) => Object.fromEntries(Object.entries(part).filter(([homeId]) => !gone.includes(homeId)));
263
+ const next = {
264
+ ...doc,
265
+ rounds: tidyRounds(kept(doc.rounds)),
266
+ wakes: kept(doc.wakes),
267
+ delivered: kept(doc.delivered),
268
+ watches: Object.fromEntries(
269
+ Object.entries(doc.watches).filter(([, watch]) => !gone.includes(watch?.homeSessionId))
270
+ ),
271
+ };
272
+ return JSON.stringify(next) === JSON.stringify(doc) ? undefined : next;
273
+ });
274
+ },
275
+ async items(homeId) {
276
+ return (await read()).homes[homeId] || [];
277
+ },
278
+ /** `{ id, count }` of the results dropped for `homeId`; null when none. */
279
+ async omitted(homeId) {
280
+ return (await read()).omitted[homeId] || null;
281
+ },
282
+ /** Queue `item` for `homeId`; `unwatch` drops that watch in the same write,
283
+ * also when the item was queued or posted before. */
284
+ async add(homeId, item, { unwatch = '' } = {}) {
285
+ await update((doc) => {
286
+ let next = null;
287
+ if (item.roundTrip !== true) next = appended(doc, homeId, [item]);
288
+ else {
289
+ // A task turn's end counts one round trip, once per run id, per home
290
+ // and target: the user's next message in that home starts over
291
+ // (resetRounds), and so does a count idle past ROUND_TTL_MS.
292
+ const own = freshRounds(doc.rounds[homeId]);
293
+ const round = (own[item.taskSessionId]?.n || 0) + 1;
294
+ next = appended(doc, homeId, [{ ...item, round }]);
295
+ if (next) {
296
+ const counted = { ...own, [item.taskSessionId]: { n: round, at: now() } };
297
+ next = { ...next, rounds: { ...doc.rounds, [homeId]: counted } };
298
+ }
299
+ }
300
+ if (!unwatch || !doc.watches[unwatch]) return next ?? undefined;
301
+ const watches = { ...doc.watches };
302
+ delete watches[unwatch];
303
+ return { ...(next || doc), watches };
304
+ });
305
+ },
306
+ /** Keep `watch` ({ runId, agentId, homeSessionId, taskSessionId, title,
307
+ * target? }) until its report is queued, dropping the watches `drop` in
308
+ * the same write. Throws when its home already keeps `caps.watches`. */
309
+ async watch(watch, { drop = [] } = {}) {
310
+ await update((doc) => {
311
+ const watches = { ...doc.watches };
312
+ for (const runId of drop) delete watches[runId];
313
+ const own = Object.values(watches).filter(
314
+ (kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId
315
+ );
316
+ if (own.length >= caps.watches) {
317
+ throw new Error(`${watch.homeSessionId} already waits on ${own.length} session reports`);
318
+ }
319
+ return { ...doc, watches: { ...watches, [watch.runId]: watch } };
320
+ });
321
+ },
322
+ watchCap: caps.watches,
323
+ /** Every stored watch, oldest first. */
324
+ async watches() {
325
+ return Object.values((await read()).watches);
326
+ },
327
+ /** Drop the watch of `runId` (its turn was never taken). */
328
+ async unwatch(runId) {
329
+ await update((doc) => {
330
+ if (!doc.watches[runId]) return undefined;
331
+ const watches = { ...doc.watches };
332
+ delete watches[runId];
333
+ return { ...doc, watches };
334
+ });
335
+ },
336
+ /** The user spoke in `homeId`: its round trips count from zero again. */
337
+ async resetRounds(homeId) {
338
+ await update((doc) => {
339
+ if (!doc.rounds[homeId]) return undefined;
340
+ const rounds = { ...doc.rounds };
341
+ delete rounds[homeId];
342
+ return { ...doc, rounds };
343
+ });
344
+ },
345
+ /** Hand every queued item (and omission) of `fromHome` to `toHome`. */
346
+ async move(fromHome, toHome) {
347
+ await update((doc) => {
348
+ const items = doc.homes[fromHome] || [];
349
+ const dropped = doc.omitted[fromHome]?.count || 0;
350
+ const next = appended(doc, toHome, items, dropped) || doc;
351
+ const homes = { ...next.homes };
352
+ const omitted = { ...next.omitted };
353
+ delete homes[fromHome];
354
+ delete omitted[fromHome];
355
+ return { ...next, homes, omitted };
356
+ });
357
+ },
358
+ /** Dequeue the posted `runIds` (and the posted omission notice `omitted`);
359
+ * those in `wake` stay owed a wake (`wakes`) in the same write, at most
360
+ * `caps.wakeRunIds` run ids per home: past it the oldest owed collapse
361
+ * into the overflow marker the next batch takes over (`overflowed`
362
+ * counts them). Stored batches never change. */
363
+ async remove(homeId, runIds, omitted = null, { wake = [] } = {}) {
364
+ let overflowed = 0;
365
+ 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 };
405
+ });
406
+ return { overflowed };
407
+ },
408
+ };
409
+ }
410
+
411
+ const messageIdOf = (runId) => `routine_${runId}`;
412
+
413
+ export function createAgentHomeDelivery({
414
+ loadSession,
415
+ saveSession,
416
+ turnInFlight,
417
+ queue,
418
+ live = () => liveSurface,
419
+ retryLater = (fn, ms) => setTimeout(fn, ms).unref?.(),
420
+ busyRetryMs = 1000,
421
+ missingRetryMs = 60_000,
422
+ // 'present' | 'deleted' | 'unknown' per agent id: a deleted owner's results go to Mixy.
423
+ agentPresence = () => 'unknown',
424
+ // The same per room id. The daemon has no room store (rooms live in the
425
+ // desktop host), so by default no room counts as deleted.
426
+ roomPresence = () => 'unknown',
427
+ // True only on positive evidence that session `id` was deleted; an
428
+ // unreadable or unknown record is not deleted.
429
+ sessionDeleted = () => false,
430
+ log = (line) => process.stderr.write(`mixdog routines: ${line}\n`),
431
+ }) {
432
+ const busy = (sessionId) => {
433
+ if (turnInFlight(sessionId)) return true;
434
+ try {
435
+ return live()?.busy?.(sessionId) === true;
436
+ } catch {
437
+ return false;
438
+ }
439
+ };
440
+ const draining = new Map(); // home → the running pass
441
+ const again = new Set(); // homes enqueued to while their pass ran
442
+ const waiting = new Set(); // homes with a retry timer armed
443
+ const shownLive = new Set(); // held-request message ids shown live in a busy thread
444
+
445
+ /** A held request waits on the user, possibly raised by the very turn that
446
+ * keeps its thread busy (a routine turn): show it live at once; the stored
447
+ * row (same messageId) replaces it once the thread is idle. */
448
+ function showHeldLive(homeId, items) {
449
+ for (const item of items) {
450
+ if (item.kind !== 'held-request') continue;
451
+ const messageId = messageIdOf(item.runId);
452
+ if (shownLive.has(messageId)) continue;
453
+ 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
+ }
459
+ }
460
+ }
461
+
462
+ function later(homeId, ms) {
463
+ if (waiting.has(homeId)) return;
464
+ waiting.add(homeId);
465
+ retryLater(() => {
466
+ waiting.delete(homeId);
467
+ void drain(homeId);
468
+ }, ms);
469
+ }
470
+
471
+ /** Post every queued result of `homeId` if its thread is idle; else retry later. */
472
+ async function drainOnce(homeId) {
473
+ const items = await queue.items(homeId);
474
+ const omitted = await queue.omitted(homeId);
475
+ const owed = owesWake(await queue.wakes(homeId));
476
+ if (!items.length && !omitted && !owed) return;
477
+ const agentId = agentIdOfHomeSession(homeId);
478
+ if (agentId && agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted') {
479
+ // The owner is gone (its home archived): Mixy, the default owner, gets them.
480
+ const mixyHome = agentHomeSessionId(MIXY_AGENT_ID);
481
+ log(`${items.length} result(s) of the deleted agent ${agentId} go to ${mixyHome}`);
482
+ await queue.move(homeId, mixyHome);
483
+ if (owed) await queue.dropWakes(homeId);
484
+ void drain(mixyHome);
485
+ return;
486
+ }
487
+ if (busy(homeId)) {
488
+ showHeldLive(homeId, items);
489
+ return later(homeId, busyRetryMs);
490
+ }
491
+ // From here to the push nothing awaits, so no turn can start in between.
492
+ const session = loadSession(homeId);
493
+ if (!session || session.closed === true) {
494
+ log(`${items.length} result(s) wait for the home thread ${homeId}`);
495
+ return later(homeId, missingRetryMs);
496
+ }
497
+ 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
+ }
515
+ for (const item of items) shownLive.delete(messageIdOf(item.runId));
516
+ if (fresh.length) {
517
+ session.updatedAt = Date.now();
518
+ await saveSession(session);
519
+ 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
+ }
525
+ }
526
+ }
527
+ // Dequeuing and owing the wake are one write: no restart between posting
528
+ // and waking loses the wake.
529
+ const { overflowed } = await queue.remove(
530
+ homeId,
531
+ items.map((item) => item.runId),
532
+ omitted,
533
+ { wake: toWake }
534
+ );
535
+ 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);
569
+ }
570
+
571
+ /** The single drainer of `homeId`; a call during a pass runs one more pass. */
572
+ function drain(homeId) {
573
+ const running = draining.get(homeId);
574
+ if (running) {
575
+ again.add(homeId);
576
+ return running;
577
+ }
578
+ const pass = (async () => {
579
+ try {
580
+ do {
581
+ again.delete(homeId);
582
+ await drainOnce(homeId);
583
+ } while (again.has(homeId));
584
+ } catch (error) {
585
+ log(`delivery into ${homeId} failed, retrying: ${error?.message || error}`);
586
+ later(homeId, busyRetryMs);
587
+ } finally {
588
+ draining.delete(homeId);
589
+ }
590
+ })();
591
+ draining.set(homeId, pass);
592
+ return pass;
593
+ }
594
+
595
+ /** Persist `result` ({ runId, kind, ok, text, runSessionId }) of a schedule
596
+ * or webhook `row` for its owner's home, then start draining it. Resolves
597
+ * once it is queued. */
598
+ async function enqueue(row, result) {
599
+ const homeId = agentHomeSessionId(scheduleOwnerAgentId(row));
600
+ 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
+ });
612
+ void drain(homeId);
613
+ return homeId;
614
+ }
615
+
616
+ /** Persist one held-request row (`held`: normalizeHeldRequest fields) for
617
+ * `agentId`'s home thread, then start draining it. Each status posts once. */
618
+ async function enqueueHeld(agentId, held) {
619
+ const homeId = agentHomeSessionId(agentId) || agentHomeSessionId(MIXY_AGENT_ID);
620
+ if (!held?.id) throw new Error('held request: id required');
621
+ await queue.add(homeId, {
622
+ runId: `${held.id}_${held.status || 'pending'}`,
623
+ kind: 'held-request',
624
+ held,
625
+ at: Date.now(),
626
+ });
627
+ void drain(homeId);
628
+ return homeId;
629
+ }
630
+
631
+ /** Persist one report of a coding task session `agentId` created (session
632
+ * create) for its home thread, then start draining it. `status` 'started'
633
+ * 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
+ ) {
638
+ const homeId = agentHomeSessionId(agentId) || agentHomeSessionId(MIXY_AGENT_ID);
639
+ 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;
660
+ }
661
+
662
+ /** Show `run`'s live-only row (routineRunNotice: `running`, or a result whose
663
+ * delivery failed) in `row`'s owner home when it is open and idle. Nothing
664
+ * is stored and no turn or unread mark follows; the posted result (same
665
+ * messageId) replaces it. False when no window shows it. */
666
+ function announce(row, run) {
667
+ const homeId = agentHomeSessionId(scheduleOwnerAgentId(row));
668
+ if (!run?.runId || busy(homeId)) return false;
669
+ 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
+ }
676
+ }
677
+
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
+ }
687
+
688
+ /** Retire the bookkeeping of deleted homes (and stale round counts). */
689
+ async function prune() {
690
+ await queue.prune((await queue.bookkeptHomes()).filter(homeDeleted));
691
+ }
692
+
693
+ /** Store `watch`; a home at its watch cap first gives up its oldest watches
694
+ * whose target sessions are proven deleted, and refuses the watch (so the
695
+ * send claims no report) when that frees too few. */
696
+ 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
+ : [];
708
+ if (drop.length) {
709
+ log(`${watch.homeSessionId} is at its watch cap: dropped ${drop.length} watch(es) of deleted sessions`);
710
+ }
711
+ await queue.watch(watch, { drop });
712
+ }
713
+
714
+ /** Drain every queue left by an earlier process, after retiring stale bookkeeping. */
715
+ async function recover() {
716
+ try {
717
+ await prune();
718
+ } catch (error) {
719
+ log(`pruning the routine results failed: ${error?.message || error}`);
720
+ }
721
+ const homes = await queue.pendingHomes();
722
+ for (const homeId of homes) void drain(homeId);
723
+ return homes;
724
+ }
725
+
726
+ return {
727
+ enqueue,
728
+ enqueueHeld,
729
+ enqueueTask,
730
+ announce,
731
+ drain,
732
+ recover,
733
+ watchTask,
734
+ unwatchTask: (runId) => queue.unwatch(runId),
735
+ taskWatches: () => queue.watches(),
736
+ resetRounds: (homeId) => queue.resetRounds(homeId),
737
+ };
738
+ }
739
+
740
+ let defaultDelivery = null;
741
+ /** The process delivery over the session store and data dir; built on first use. */
742
+ async function processDelivery() {
743
+ if (!defaultDelivery) {
744
+ const [
745
+ { loadSession, saveSessionAsync },
746
+ { isSessionTurnInFlight },
747
+ { getPluginData },
748
+ { createAgentEditorApi },
749
+ { probePath, PROBE_ABSENT },
750
+ { sessionPath },
751
+ ] = await Promise.all([
752
+ import('../runtime/agent/orchestrator/session/store.mjs'),
753
+ import('../runtime/agent/orchestrator/session/manager/runtime-liveness.mjs'),
754
+ import('../runtime/agent/orchestrator/config.mjs'),
755
+ import('./workflow-agents-api/agent-editor.mjs'),
756
+ import('../runtime/agent/orchestrator/session/store/fs-probe.mjs'),
757
+ import('../runtime/agent/orchestrator/session/store/session-paths.mjs'),
758
+ ]);
759
+ // The editor's per-id definition probe: 'deleted' only when proven gone.
760
+ const { agentDefinitionPresence } = createAgentEditorApi({ cfgMod: { getPluginData } });
761
+ defaultDelivery = createAgentHomeDelivery({
762
+ loadSession,
763
+ saveSession: (session) => saveSessionAsync(session, { expectedGeneration: session.generation }),
764
+ turnInFlight: isSessionTurnInFlight,
765
+ queue: createRoutineResultQueue(join(getPluginData(), 'routine-results.json')),
766
+ agentPresence: (agentId) => agentDefinitionPresence([agentId])[agentId],
767
+ // Deleting a session unlinks its record (session-delete.mjs) and leaves
768
+ // no tombstone: only an absent file with no in-memory copy proves it.
769
+ // An unreadable file probes 'unreadable' and stays.
770
+ sessionDeleted: (sessionId) => {
771
+ try {
772
+ return probePath(sessionPath(sessionId)).state === PROBE_ABSENT && !loadSession(sessionId);
773
+ } catch {
774
+ return false;
775
+ }
776
+ },
777
+ });
778
+ }
779
+ return defaultDelivery;
780
+ }
781
+
782
+ /** Queue one run's result for its owner's home thread. */
783
+ export async function deliverScheduleResult(schedule, result) {
784
+ return (await processDelivery()).enqueue(schedule, result);
785
+ }
786
+
787
+ /** Show a run's live-only row (running / delivery failed) in its owner's home. */
788
+ export async function announceRoutineRun(row, run) {
789
+ if (!liveSurface) return false;
790
+ return (await processDelivery()).announce(row, run);
791
+ }
792
+
793
+ /** Queue one held-request row for `agentId`'s home thread. */
794
+ export async function deliverHeldRequest(agentId, held) {
795
+ return (await processDelivery()).enqueueHeld(agentId, held);
796
+ }
797
+
798
+ /** Queue one report of a task session for `agentId`'s home thread. */
799
+ export async function deliverAgentTask(agentId, report) {
800
+ return (await processDelivery()).enqueueTask(agentId, report);
801
+ }
802
+
803
+ /** Keep one reporting turn's watch until its report is queued. */
804
+ export async function watchAgentTask(watch) {
805
+ return (await processDelivery()).watchTask(watch);
806
+ }
807
+
808
+ /** Drop one reporting turn's watch (its turn was never taken). */
809
+ export async function unwatchAgentTask(runId) {
810
+ return (await processDelivery()).unwatchTask(runId);
811
+ }
812
+
813
+ /** A user's own message in `homeId`: its round trips with every target restart. */
814
+ export async function resetAgentTaskRounds(homeId) {
815
+ return (await processDelivery()).resetRounds(homeId);
816
+ }
817
+
818
+ /** The reporting turns whose report was not queued yet. */
819
+ export async function agentTaskWatches() {
820
+ return (await processDelivery()).taskWatches();
821
+ }
822
+
823
+ /** Daemon start: post the results an earlier process left queued. */
824
+ export async function recoverScheduleResults() {
825
+ return (await processDelivery()).recover();
826
+ }