mixdog 1.3.0 → 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 (423) hide show
  1. package/NOTICE.md +52 -9
  2. package/README.md +8 -4
  3. package/package.json +2 -2
  4. package/scripts/native-release-status.mjs +238 -0
  5. package/src/defaults/shipped-history.json +8 -1
  6. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  7. package/src/defaults/skills/setup/SKILL.md +23 -1
  8. package/src/defaults/skills/setup/references/actions.md +54 -4
  9. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  10. package/src/defaults/skills/setup-guide/SKILL.md +17 -10
  11. package/src/defaults/skills/visualize/SKILL.md +9 -6
  12. package/src/defaults/skills/visualize/references/craft.md +8 -1
  13. package/src/help.mjs +1 -1
  14. package/src/lib/keychain-cjs.cjs +4 -1
  15. package/src/lib/rules-builder.cjs +14 -7
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent-mode/AGENT_MODE.md +9 -6
  18. package/src/rules/agent-mode/MIXY.md +11 -6
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/rules/shared/10-tool-workflow.md +4 -0
  21. package/src/rules/shared/20-research.md +2 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  25. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  26. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  27. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  29. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  32. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +81 -16
  34. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -4
  35. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  36. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  37. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  38. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  43. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  44. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +34 -16
  45. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  46. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  47. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  49. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -1
  50. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  51. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  52. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  53. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  54. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +10 -2
  60. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  65. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  68. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  69. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  72. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +5 -5
  74. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  75. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +17 -6
  78. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +41 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +6 -4
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  83. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  84. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  85. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +21 -2
  86. package/src/runtime/agent/orchestrator/session/agent-room.mjs +149 -45
  87. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  88. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  90. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  91. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  92. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  93. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  94. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  96. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  97. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  99. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  100. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +156 -25
  101. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  102. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  103. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  104. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  105. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  106. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  107. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  108. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  110. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +111 -9
  111. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -14
  112. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  116. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
  119. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  120. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  124. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  125. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  126. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  128. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  130. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  131. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  132. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  134. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +62 -47
  135. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +6 -8
  136. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  137. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  138. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  139. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  140. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
  141. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  142. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
  143. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  144. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  145. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +35 -4
  146. package/src/runtime/agent/orchestrator/session/routine-result.mjs +179 -16
  147. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  148. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  149. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  150. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  151. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  152. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  153. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  154. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  155. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  156. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  157. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  158. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  159. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  160. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  161. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +49 -14
  162. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +134 -17
  163. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  164. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  165. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  166. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  167. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  168. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  169. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  170. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  171. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  175. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  176. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  177. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  178. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  179. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  180. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  181. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  182. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  183. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  184. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  185. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  186. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  187. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  188. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  189. package/src/runtime/attachments/limits.mjs +5 -1
  190. package/src/runtime/attachments/office-extract.mjs +3 -6
  191. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  192. package/src/runtime/attachments/store.mjs +55 -30
  193. package/src/runtime/browser-bridge/client.mjs +73 -20
  194. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  195. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  196. package/src/runtime/channels/lib/state-file.mjs +15 -15
  197. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  198. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  199. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  200. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  201. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  202. package/src/runtime/effort-judge/judge-client.mjs +6 -3
  203. package/src/runtime/effort-judge/model-install.mjs +16 -8
  204. package/src/runtime/effort-judge/model-manifest.json +7 -7
  205. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  206. package/src/runtime/local-provider/catalog.mjs +5 -2
  207. package/src/runtime/local-provider/registered-models.mjs +64 -27
  208. package/src/runtime/local-provider/server.mjs +2 -3
  209. package/src/runtime/media/adapters/image-api.mjs +89 -0
  210. package/src/runtime/media/auth.mjs +64 -1
  211. package/src/runtime/media/catalog.mjs +66 -4
  212. package/src/runtime/media/jobs.mjs +4 -0
  213. package/src/runtime/media/lanes.mjs +46 -5
  214. package/src/runtime/media/media-usage.mjs +17 -0
  215. package/src/runtime/media/renditions.mjs +15 -15
  216. package/src/runtime/media/store.mjs +36 -35
  217. package/src/runtime/media/tool.mjs +8 -1
  218. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  219. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  220. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  221. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  222. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  223. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  224. package/src/runtime/office/core/journal.mjs +7 -14
  225. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  226. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  227. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  228. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  229. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  230. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  231. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  232. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  233. package/src/runtime/shared/agent-kind.mjs +152 -0
  234. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  235. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  236. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  237. package/src/runtime/shared/agent-route-config.d.mts +1 -0
  238. package/src/runtime/shared/agent-route-config.mjs +22 -8
  239. package/src/runtime/shared/agent-store.mjs +149 -0
  240. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  241. package/src/runtime/shared/atomic-file.d.mts +50 -0
  242. package/src/runtime/shared/atomic-file.mjs +394 -132
  243. package/src/runtime/shared/background-tasks.mjs +7 -4
  244. package/src/runtime/shared/bot-hops.mjs +30 -0
  245. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  246. package/src/runtime/shared/bounded-download.mjs +4 -2
  247. package/src/runtime/shared/config-patch.mjs +20 -1
  248. package/src/runtime/shared/config.mjs +39 -9
  249. package/src/runtime/shared/connector-catalog.d.mts +1 -2
  250. package/src/runtime/shared/connector-catalog.mjs +2 -4
  251. package/src/runtime/shared/connector-triggers.mjs +2 -0
  252. package/src/runtime/shared/defaults-separation.mjs +6 -2
  253. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  254. package/src/runtime/shared/err-text.mjs +7 -1
  255. package/src/runtime/shared/error-presentation.mjs +113 -10
  256. package/src/runtime/shared/file-has-data.mjs +28 -0
  257. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  258. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  259. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  260. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  261. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  262. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  263. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  264. package/src/runtime/shared/plain-preview.mjs +22 -17
  265. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  266. package/src/runtime/shared/projects.mjs +50 -41
  267. package/src/runtime/shared/provider-accounts.mjs +17 -12
  268. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  269. package/src/runtime/shared/routine-trigger.mjs +14 -6
  270. package/src/runtime/shared/run-child.d.mts +3 -0
  271. package/src/runtime/shared/run-child.mjs +24 -3
  272. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  273. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  274. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  275. package/src/runtime/shared/singleton-owner.mjs +7 -6
  276. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  277. package/src/runtime/shared/staged-update.mjs +4 -2
  278. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  279. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  280. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  281. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  282. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  283. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  284. package/src/runtime/shared/usage-readout-totals.mjs +15 -13
  285. package/src/runtime/shared/user-cwd.mjs +9 -4
  286. package/src/runtime/shared/user-data-guard.mjs +46 -44
  287. package/src/runtime/shared/utf8-cut.mjs +10 -0
  288. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  289. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  290. package/src/runtime/tidy/core-install.mjs +26 -4
  291. package/src/runtime/tidy/install.mjs +91 -23
  292. package/src/runtime/web-search/lib/config.mjs +4 -16
  293. package/src/session-runtime/agent-home-delivery.mjs +859 -136
  294. package/src/session-runtime/agent-room-delivery.mjs +167 -19
  295. package/src/session-runtime/agent-task-sessions.mjs +831 -69
  296. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  297. package/src/session-runtime/boot/apis.mjs +58 -24
  298. package/src/session-runtime/boot/config.mjs +1 -0
  299. package/src/session-runtime/boot/facade.mjs +11 -1
  300. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  301. package/src/session-runtime/boot/providers.mjs +16 -6
  302. package/src/session-runtime/boot/shared.mjs +12 -6
  303. package/src/session-runtime/boot/tools.mjs +2 -2
  304. package/src/session-runtime/bot-status.mjs +432 -0
  305. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  306. package/src/session-runtime/config-lifecycle.mjs +78 -32
  307. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  308. package/src/session-runtime/handoff-attachments.mjs +593 -0
  309. package/src/session-runtime/held-requests.mjs +36 -33
  310. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  311. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  312. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +45 -6
  313. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  314. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  315. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  316. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  317. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  318. package/src/session-runtime/media-api.mjs +5 -2
  319. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  320. package/src/session-runtime/model-route/route-tuning.mjs +11 -2
  321. package/src/session-runtime/model-route/set-route.mjs +82 -2
  322. package/src/session-runtime/native-web-search.mjs +2 -2
  323. package/src/session-runtime/new-session-config.mjs +6 -1
  324. package/src/session-runtime/owned-automation-run.mjs +8 -1
  325. package/src/session-runtime/room-member-gate.mjs +16 -3
  326. package/src/session-runtime/route-resolution.mjs +21 -19
  327. package/src/session-runtime/route-state.mjs +1 -59
  328. package/src/session-runtime/routine-home-turn.mjs +5 -3
  329. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  330. package/src/session-runtime/runtime-paths.mjs +11 -5
  331. package/src/session-runtime/runtime-tunables.mjs +6 -0
  332. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  333. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  334. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  335. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  336. package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
  337. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  338. package/src/session-runtime/services/agent-tool/send-flow.mjs +36 -24
  339. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  340. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  341. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +12 -6
  342. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  343. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  344. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  345. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  346. package/src/session-runtime/services/agent-tool.mjs +6 -0
  347. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  348. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  349. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  350. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  351. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  352. package/src/session-runtime/services/provider-admin.mjs +14 -4
  353. package/src/session-runtime/services/schedule-access.mjs +33 -4
  354. package/src/session-runtime/services/seeds.mjs +4 -3
  355. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  356. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  357. package/src/session-runtime/session-create/materialize.mjs +2 -3
  358. package/src/session-runtime/session-create/session-options.mjs +15 -2
  359. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  360. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  361. package/src/session-runtime/setup-tool/executor.mjs +20 -3
  362. package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
  363. package/src/session-runtime/setup-tool/settings-contract.mjs +37 -1
  364. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  365. package/src/session-runtime/skills-api.mjs +12 -5
  366. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  367. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  368. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  369. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  370. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  371. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  372. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  373. package/src/session-runtime/tool-policy-refresh.mjs +47 -358
  374. package/src/session-runtime/tool-surface.mjs +5 -3
  375. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +99 -81
  376. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  377. package/src/session-runtime/turn/turn-run.mjs +2 -1
  378. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  379. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +578 -301
  380. package/src/session-runtime/workflow-agents-api/agent-route.mjs +113 -53
  381. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  382. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  383. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  384. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  385. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  386. package/src/standalone/daemon-agent-control.mjs +4 -1
  387. package/src/standalone/daemon-session-control.mjs +60 -11
  388. package/src/standalone/daemon.mjs +34 -4
  389. package/src/standalone/local-session-runtime.mjs +22 -14
  390. package/src/standalone/session-client.mjs +7 -2
  391. package/src/standalone/session-protocol.mjs +7 -0
  392. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  393. package/src/standalone/session-service/agent-tree.mjs +4 -3
  394. package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
  395. package/src/standalone/session-service.mjs +11 -0
  396. package/src/standalone/session-state-projection.mjs +19 -9
  397. package/src/standalone/session-tool-output.mjs +35 -9
  398. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  399. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  400. package/src/tui/app/slash-commands.mjs +1 -1
  401. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  402. package/src/tui/dist/index.mjs +599 -342
  403. package/src/tui/prompt-history-store.mjs +16 -21
  404. package/src/tui/session/context-state.mjs +4 -0
  405. package/src/tui/session/notice-surface.mjs +3 -4
  406. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  407. package/src/tui/session/session-api/media.mjs +2 -1
  408. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  409. package/src/tui/session/session-api/routes-write.mjs +203 -0
  410. package/src/tui/session/session-api/routes.mjs +23 -236
  411. package/src/tui/session/session-api/settings.mjs +14 -1
  412. package/src/tui/session/session-flow/drain.mjs +28 -3
  413. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  414. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  415. package/src/tui/session/session-flow/steering.mjs +8 -3
  416. package/src/tui/session/tool-approval.mjs +185 -20
  417. package/src/tui/session/transcript-intake.mjs +4 -0
  418. package/src/tui/session/turn-live-call.mjs +11 -3
  419. package/src/tui/session/turn.mjs +36 -17
  420. package/src/tui/transcript-tool-failures.mjs +8 -2
  421. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  422. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  423. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -17,25 +17,48 @@
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
- import { readFile } from 'node:fs/promises';
28
+ import { randomUUID } from 'node:crypto';
29
+ import { readFileSync } from 'node:fs';
22
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';
23
35
  import {
24
36
  MIXY_AGENT_ID,
25
37
  agentHomeSessionId,
26
38
  agentIdOfHomeSession,
39
+ isAgentHomeSessionId,
27
40
  } from '../runtime/agent/orchestrator/session/agent-home.mjs';
28
41
  import {
42
+ isRoomThreadSessionId,
43
+ roomIdOfSession,
44
+ roomMemberOfSession,
45
+ roomSessionId,
46
+ } from '../runtime/agent/orchestrator/session/agent-room.mjs';
47
+ import {
48
+ TASK_LOOP_CAP,
29
49
  agentTaskMessage,
50
+ agentTaskRelayFailedMessage,
30
51
  agentTaskWakePrompt,
52
+ boundedReply,
31
53
  routineOmittedMessage,
32
54
  routineResultMessage,
33
55
  routineResultNotice,
34
56
  routineRunNotice,
35
57
  routineSummary,
36
58
  } from '../runtime/agent/orchestrator/session/routine-result.mjs';
37
- import { parseJsonText, updateJsonAtomic } from '../runtime/shared/atomic-file.mjs';
59
+ import { readJsonState, updateJsonAtomic } from '../runtime/shared/atomic-file.mjs';
38
60
  import { ROUTINE_LIMITS } from '../runtime/shared/routine-limits.mjs';
61
+ import { submissionIdsOf, submissionOutcome } from './agent-task-sessions.mjs';
39
62
  import {
40
63
  heldRequestMessage,
41
64
  heldRequestNotice,
@@ -49,14 +72,51 @@ function deliveredRow(item, messageId) {
49
72
  return { message, notice: heldRequestNotice(message) };
50
73
  }
51
74
  if (item.kind === 'agent-task') {
52
- // A turn's end (not its start or a wait for the user) wakes the agent.
53
- const message = agentTaskMessage({ ...item, messageId });
54
- return { message, notice: routineResultNotice(message), wake: item.roundTrip === true };
75
+ // A turn's end (not its start or a wait for the user) wakes the agent —
76
+ // except past the round-trip cap with another agent's own thread, where
77
+ // each wake could answer the other agent again without end.
78
+ const capped =
79
+ (isAgentHomeSessionId(item.taskSessionId) || isRoomThreadSessionId(item.taskSessionId)) &&
80
+ (item.round || 0) > TASK_LOOP_CAP;
81
+ const message = agentTaskMessage({ ...item, messageId, ...(capped ? { woken: false } : {}) });
82
+ return { message, notice: routineResultNotice(message), wake: item.roundTrip === true && !capped };
55
83
  }
56
84
  const message = routineResultMessage({ ...item, messageId });
57
85
  return { message, notice: routineResultNotice(message) };
58
86
  }
59
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
+
60
120
  /** The agent a schedule belongs to; ownerless rows belong to Mixy. */
61
121
  export function scheduleOwnerAgentId(schedule) {
62
122
  const owner = typeof schedule?.ownerAgentId === 'string' ? schedule.ownerAgentId.trim() : '';
@@ -64,9 +124,11 @@ export function scheduleOwnerAgentId(schedule) {
64
124
  }
65
125
 
66
126
  let liveSurface = null;
67
- /** `{ busy(sessionId), notice(sessionId, notice), wake(sessionId, prompt),
127
+ /** `{ busy(sessionId), notice(sessionId, notice), wake(sessionId, prompt, { id }),
68
128
  * submit(sessionId, prompt, options), abort(sessionId) }` from the session
69
- * service; `wake` starts one hidden turn, `submit` queues a routine turn
129
+ * service; `wake` starts one hidden turn with submission `id` (resolves
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
70
132
  * (resolves whether it was accepted), `abort` stops the running turn. */
71
133
  export function setAgentHomeLiveSurface(surface) {
72
134
  liveSurface = surface || null;
@@ -80,48 +142,269 @@ export function agentHomeLiveSurface() {
80
142
  // Run ids already posted, kept per home so a replayed run never posts twice,
81
143
  // even into a thread that was reset ("대화 새로 시작") since.
82
144
  const DELIVERED_LEDGER = 500;
145
+ // A target's round-trip count left idle this long starts over.
146
+ export const ROUND_TTL_MS = 30 * 24 * 60 * 60 * 1000;
147
+ // Per home: round-trip targets counted (least recently counted drop first),
148
+ // reporting turns watched at once, and posted results owed a wake.
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;
156
+
157
+ /** `homeId`'s wake obligations in `wakes`: posted task results not in a batch
158
+ * yet (`owed`), those past the cap collapsed into one marker (`overflow`:
159
+ * `{ count, oldest, newest }` run ids) the next batch takes over, and
160
+ * batches (`{ id, runIds, overflow? }`) stored before their wake turn was
161
+ * submitted under that id. */
162
+ function wakeState(wakes, homeId) {
163
+ const raw = wakes[homeId];
164
+ if (Array.isArray(raw)) return { owed: raw, batches: [], overflow: null };
165
+ return {
166
+ owed: Array.isArray(raw?.owed) ? raw.owed : [],
167
+ batches: Array.isArray(raw?.batches)
168
+ ? raw.batches.filter((batch) => typeof batch?.id === 'string' && Array.isArray(batch.runIds))
169
+ : [],
170
+ overflow: raw?.overflow?.count > 0 ? raw.overflow : null,
171
+ };
172
+ }
173
+
174
+ const owesWake = (state) => state.owed.length > 0 || state.batches.length > 0 || Boolean(state.overflow);
175
+
176
+ function withWakeState(doc, homeId, { owed, batches, overflow }) {
177
+ const wakes = { ...doc.wakes };
178
+ if (owesWake({ owed, batches, overflow })) wakes[homeId] = { owed, batches, ...(overflow ? { overflow } : {}) };
179
+ else delete wakes[homeId];
180
+ return { ...doc, wakes };
181
+ }
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
+ }
83
259
 
84
260
  /** The persisted queue at `filePath`: `{ homes: { [homeSessionId]: item[] },
85
261
  * omitted: { [homeSessionId]: { id, count } }, delivered: { [homeSessionId]:
86
262
  * runId[] } }`. A home keeps at most `limit` items: past it the oldest drop,
87
263
  * counted under `omitted` so the thread gets one "n omitted" notice. */
88
- export function createRoutineResultQueue(filePath, { limit = ROUTINE_LIMITS.homeQueueLimit } = {}) {
264
+ export function createRoutineResultQueue(
265
+ filePath,
266
+ { limit = ROUTINE_LIMITS.homeQueueLimit, now = Date.now, caps = QUEUE_CAPS } = {}
267
+ ) {
89
268
  const section = (doc, key) => (doc && typeof doc[key] === 'object' && doc[key]) || {};
269
+ const freshRounds = (own) => freshRoundCounts(own, now(), caps.roundTargets);
270
+ const tidyRounds = (rounds) =>
271
+ Object.fromEntries(
272
+ Object.entries(rounds || {})
273
+ .map(([homeId, own]) => [homeId, freshRounds(own)])
274
+ .filter(([, own]) => Object.keys(own).length)
275
+ );
90
276
  const parts = (doc) => ({
91
277
  homes: section(doc, 'homes'),
92
278
  omitted: section(doc, 'omitted'),
93
279
  delivered: section(doc, 'delivered'),
94
280
  // Agent-initiated round trips per task session (session create).
95
281
  rounds: section(doc, 'rounds'),
282
+ // Reporting turns not yet queued, by submission id (agent-task-sessions.mjs).
283
+ watches: section(doc, 'watches'),
284
+ // Posted task results whose wake turn was not taken yet, per home.
285
+ wakes: section(doc, 'wakes'),
96
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.
97
290
  async function read() {
98
- try {
99
- return parts(parseJsonText(await readFile(filePath, 'utf8')));
100
- } catch (error) {
101
- if (error?.code === 'ENOENT') return parts(null);
102
- throw error;
103
- }
291
+ return parts(await readJsonState(filePath));
104
292
  }
105
- /** `doc` with `items` appended to `homeId`'s list, minus run ids it holds or posted. */
106
- function appended(doc, homeId, items, omittedCount = 0) {
107
- const list = doc.homes[homeId] || [];
108
- const seen = new Set([...list.map((row) => row.runId), ...(doc.delivered[homeId] || [])]);
109
- const fresh = items.filter((item) => !seen.has(item.runId) && seen.add(item.runId));
110
- if (!fresh.length && !omittedCount) return null;
111
- const next = [...list, ...fresh];
112
- const drop = Math.max(0, next.length - limit);
113
- const omitted = { ...doc.omitted };
114
- const count = drop + omittedCount;
115
- if (count) {
116
- const previous = omitted[homeId];
117
- omitted[homeId] = { id: previous?.id || `omit_${next[0].runId}`, count: (previous?.count || 0) + count };
118
- }
119
- return { ...doc, homes: { ...doc.homes, [homeId]: next.slice(drop) }, omitted };
293
+ /** Read-modify-write under the file lock (`mutator(doc)` returns the next
294
+ * doc, or nothing to keep the file); every write also retires stale and
295
+ * surplus round counts. */
296
+ function update(mutator) {
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
+ );
120
305
  }
306
+ const appended = (doc, homeId, items, omittedCount = 0) => appendedItems(doc, homeId, items, limit, omittedCount);
121
307
  return {
122
308
  async pendingHomes() {
123
- const { homes, omitted } = await read();
124
- return [...new Set([...Object.keys(homes).filter((id) => homes[id]?.length), ...Object.keys(omitted)])];
309
+ const { homes, omitted, wakes } = await read();
310
+ return [
311
+ ...new Set([
312
+ ...Object.keys(homes).filter((id) => homes[id]?.length),
313
+ ...Object.keys(omitted),
314
+ ...Object.keys(wakes).filter((id) => owesWake(wakeState(wakes, id))),
315
+ ]),
316
+ ];
317
+ },
318
+ /** `homeId`'s wake obligations: `{ owed, batches, overflow }` (wakeState). */
319
+ async wakes(homeId) {
320
+ return wakeState((await read()).wakes, homeId);
321
+ },
322
+ /** Store the owed results (and overflow marker) of `homeId` as one new
323
+ * batch under a fresh wake id, before its wake is submitted; null when
324
+ * none are owed. */
325
+ async batchWake(homeId) {
326
+ let batch = null;
327
+ await update((doc) => {
328
+ batch = null;
329
+ const state = wakeState(doc.wakes, homeId);
330
+ if (!state.owed.length && !state.overflow) return undefined;
331
+ batch = {
332
+ id: `awake_${randomUUID()}`,
333
+ runIds: state.owed,
334
+ ...(state.overflow ? { overflow: state.overflow } : {}),
335
+ };
336
+ return withWakeState(doc, homeId, { owed: [], batches: [...state.batches, batch], overflow: null });
337
+ });
338
+ return batch;
339
+ },
340
+ /** The wake of batch `batchId` was taken (or is moot): it is owed no more. */
341
+ async woke(homeId, batchId) {
342
+ await update((doc) => {
343
+ const state = wakeState(doc.wakes, homeId);
344
+ const batches = state.batches.filter((batch) => batch.id !== batchId);
345
+ if (batches.length === state.batches.length) return undefined;
346
+ return withWakeState(doc, homeId, { ...state, batches });
347
+ });
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
+ },
374
+ /** Drop every wake obligation of `homeId` (its owner is gone). */
375
+ async dropWakes(homeId) {
376
+ await update((doc) =>
377
+ doc.wakes[homeId] ? withWakeState(doc, homeId, { owed: [], batches: [], overflow: null }) : undefined
378
+ );
379
+ },
380
+ /** Every home the rounds, wakes, posted ledger or watches name. */
381
+ async bookkeptHomes() {
382
+ const doc = await read();
383
+ const watched = Object.values(doc.watches).map((watch) => watch?.homeSessionId);
384
+ return [
385
+ ...new Set(
386
+ [...Object.keys(doc.rounds), ...Object.keys(doc.wakes), ...Object.keys(doc.delivered), ...watched].filter(
387
+ (id) => typeof id === 'string' && id
388
+ )
389
+ ),
390
+ ];
391
+ },
392
+ /** Retire the rounds, wakes, posted ledger and watches of the `gone`
393
+ * homes (and, as every write does, stale round counts). */
394
+ async prune(gone) {
395
+ await update((doc) => {
396
+ const kept = (part) => Object.fromEntries(Object.entries(part).filter(([homeId]) => !gone.includes(homeId)));
397
+ const next = {
398
+ ...doc,
399
+ rounds: tidyRounds(kept(doc.rounds)),
400
+ wakes: kept(doc.wakes),
401
+ delivered: kept(doc.delivered),
402
+ watches: Object.fromEntries(
403
+ Object.entries(doc.watches).filter(([, watch]) => !gone.includes(watch?.homeSessionId))
404
+ ),
405
+ };
406
+ return JSON.stringify(next) === JSON.stringify(doc) ? undefined : next;
407
+ });
125
408
  },
126
409
  async items(homeId) {
127
410
  return (await read()).homes[homeId] || [];
@@ -130,20 +413,74 @@ export function createRoutineResultQueue(filePath, { limit = ROUTINE_LIMITS.home
130
413
  async omitted(homeId) {
131
414
  return (await read()).omitted[homeId] || null;
132
415
  },
133
- async add(homeId, item) {
134
- await updateJsonAtomic(filePath, (raw) => {
135
- const doc = parts(raw);
136
- if (item.roundTrip !== true) return appended(doc, homeId, [item]) ?? undefined;
137
- // A task turn's end counts one round trip, once per run id.
138
- const round = (doc.rounds[item.taskSessionId] || 0) + 1;
139
- const next = appended(doc, homeId, [{ ...item, round }]);
140
- return next ? { ...next, rounds: { ...doc.rounds, [item.taskSessionId]: round } } : undefined;
416
+ /** Queue `item` for `homeId`; `unwatch` drops that watch in the same write,
417
+ * also when the item was queued or posted before. */
418
+ async add(homeId, item, { unwatch = '' } = {}) {
419
+ await update((doc) => {
420
+ let next = null;
421
+ if (item.roundTrip !== true) next = appended(doc, homeId, [item]);
422
+ else {
423
+ // A task turn's end counts one round trip, once per run id, per home
424
+ // and target: the user's next message in that home starts over
425
+ // (resetRounds), and so does a count idle past ROUND_TTL_MS.
426
+ const own = freshRounds(doc.rounds[homeId]);
427
+ const round = (own[item.taskSessionId]?.n || 0) + 1;
428
+ next = appended(doc, homeId, [{ ...item, round }]);
429
+ if (next) {
430
+ const counted = { ...own, [item.taskSessionId]: { n: round, at: now() } };
431
+ next = { ...next, rounds: { ...doc.rounds, [homeId]: counted } };
432
+ }
433
+ }
434
+ if (!unwatch || !doc.watches[unwatch]) return next ?? undefined;
435
+ const watches = { ...doc.watches };
436
+ delete watches[unwatch];
437
+ return { ...(next || doc), watches };
438
+ });
439
+ },
440
+ /** Keep `watch` ({ runId, agentId, homeSessionId, taskSessionId, title,
441
+ * target? }) until its report is queued, dropping the watches `drop` in
442
+ * the same write. Throws when its home already keeps `caps.watches`. */
443
+ async watch(watch, { drop = [] } = {}) {
444
+ await update((doc) => {
445
+ const watches = { ...doc.watches };
446
+ for (const runId of drop) delete watches[runId];
447
+ const own = Object.values(watches).filter(
448
+ (kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId
449
+ );
450
+ if (own.length >= caps.watches) {
451
+ throw Object.assign(new Error(`${watch.homeSessionId} already waits on ${own.length} session reports`), {
452
+ code: 'REPORT_WATCH_CAP',
453
+ });
454
+ }
455
+ return { ...doc, watches: { ...watches, [watch.runId]: watch } };
456
+ });
457
+ },
458
+ watchCap: caps.watches,
459
+ /** Every stored watch, oldest first. */
460
+ async watches() {
461
+ return Object.values((await read()).watches);
462
+ },
463
+ /** Drop the watch of `runId` (its turn was never taken). */
464
+ async unwatch(runId) {
465
+ await update((doc) => {
466
+ if (!doc.watches[runId]) return undefined;
467
+ const watches = { ...doc.watches };
468
+ delete watches[runId];
469
+ return { ...doc, watches };
470
+ });
471
+ },
472
+ /** The user spoke in `homeId`: its round trips count from zero again. */
473
+ async resetRounds(homeId) {
474
+ await update((doc) => {
475
+ if (!doc.rounds[homeId]) return undefined;
476
+ const rounds = { ...doc.rounds };
477
+ delete rounds[homeId];
478
+ return { ...doc, rounds };
141
479
  });
142
480
  },
143
481
  /** Hand every queued item (and omission) of `fromHome` to `toHome`. */
144
482
  async move(fromHome, toHome) {
145
- await updateJsonAtomic(filePath, (raw) => {
146
- const doc = parts(raw);
483
+ await update((doc) => {
147
484
  const items = doc.homes[fromHome] || [];
148
485
  const dropped = doc.omitted[fromHome]?.count || 0;
149
486
  const next = appended(doc, toHome, items, dropped) || doc;
@@ -154,32 +491,300 @@ export function createRoutineResultQueue(filePath, { limit = ROUTINE_LIMITS.home
154
491
  return { ...next, homes, omitted };
155
492
  });
156
493
  },
157
- /** Dequeue the posted `runIds` (and the posted omission notice `omitted`). */
158
- async remove(homeId, runIds, omitted = null) {
159
- await updateJsonAtomic(filePath, (raw) => {
160
- const doc = parts(raw);
161
- const homes = { ...doc.homes };
162
- const rest = (homes[homeId] || []).filter((row) => !runIds.includes(row.runId));
163
- if (rest.length) homes[homeId] = rest;
164
- else delete homes[homeId];
165
- const delivered = { ...doc.delivered };
166
- if (runIds.length) delivered[homeId] = [...(delivered[homeId] || []), ...runIds].slice(-DELIVERED_LEDGER);
167
- const omittedNow = { ...doc.omitted };
168
- const current = omittedNow[homeId];
169
- if (omitted && current?.id === omitted.id) {
170
- // More dropped while this pass posted: they get a notice of their own.
171
- const left = current.count - omitted.count;
172
- if (left > 0) omittedNow[homeId] = { id: `${current.id}.${omitted.count}`, count: left };
173
- else delete omittedNow[homeId];
174
- }
175
- return { ...doc, homes, omitted: omittedNow, delivered };
494
+ /** Dequeue the posted `runIds` (and the posted omission notice `omitted`);
495
+ * those in `wake` stay owed a wake (`wakes`) in the same write, at most
496
+ * `caps.wakeRunIds` run ids per home: past it the oldest owed collapse
497
+ * into the overflow marker the next batch takes over (`overflowed`
498
+ * counts them). Stored batches never change. */
499
+ async remove(homeId, runIds, omitted = null, { wake = [] } = {}) {
500
+ let overflowed = 0;
501
+ await update((doc) => {
502
+ let wakes;
503
+ ({ wakes, overflowed } = owedWakes(doc, homeId, wake, caps.wakeRunIds));
504
+ return { ...withDequeued(doc, homeId, runIds, omitted), wakes };
176
505
  });
506
+ return { overflowed };
177
507
  },
178
508
  };
179
509
  }
180
510
 
181
511
  const messageIdOf = (runId) => `routine_${runId}`;
182
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
+
183
788
  export function createAgentHomeDelivery({
184
789
  loadSession,
185
790
  saveSession,
@@ -191,6 +796,25 @@ export function createAgentHomeDelivery({
191
796
  missingRetryMs = 60_000,
192
797
  // 'present' | 'deleted' | 'unknown' per agent id: a deleted owner's results go to Mixy.
193
798
  agentPresence = () => 'unknown',
799
+ // The same per room id. The daemon has no room store (rooms live in the
800
+ // desktop host), so by default no room counts as deleted.
801
+ roomPresence = () => 'unknown',
802
+ // True only on positive evidence that session `id` was deleted; an
803
+ // unreadable or unknown record is not deleted.
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),
194
818
  log = (line) => process.stderr.write(`mixdog routines: ${line}\n`),
195
819
  }) {
196
820
  const busy = (sessionId) => {
@@ -205,6 +829,20 @@ export function createAgentHomeDelivery({
205
829
  const again = new Set(); // homes enqueued to while their pass ran
206
830
  const waiting = new Set(); // homes with a retry timer armed
207
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
+ });
208
846
 
209
847
  /** A held request waits on the user, possibly raised by the very turn that
210
848
  * keeps its thread busy (a routine turn): show it live at once; the stored
@@ -215,11 +853,7 @@ export function createAgentHomeDelivery({
215
853
  const messageId = messageIdOf(item.runId);
216
854
  if (shownLive.has(messageId)) continue;
217
855
  shownLive.add(messageId);
218
- try {
219
- live()?.notice?.(homeId, { ...deliveredRow(item, messageId).notice, messageId });
220
- } catch (error) {
221
- log(`live notice in ${homeId} failed: ${error?.message || error}`);
222
- }
856
+ noticeLive(live, log, homeId, { ...deliveredRow(item, messageId).notice, messageId });
223
857
  }
224
858
  }
225
859
 
@@ -232,17 +866,59 @@ export function createAgentHomeDelivery({
232
866
  }, ms);
233
867
  }
234
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
+
235
908
  /** Post every queued result of `homeId` if its thread is idle; else retry later. */
236
909
  async function drainOnce(homeId) {
910
+ if (roomIdOfSession(homeId)) return drainRoomLog(homeId);
237
911
  const items = await queue.items(homeId);
238
912
  const omitted = await queue.omitted(homeId);
239
- if (!items.length && !omitted) return;
913
+ const owed = owesWake(await queue.wakes(homeId));
914
+ if (!items.length && !omitted && !owed) return;
240
915
  const agentId = agentIdOfHomeSession(homeId);
241
916
  if (agentId && agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted') {
242
917
  // The owner is gone (its home archived): Mixy, the default owner, gets them.
243
918
  const mixyHome = agentHomeSessionId(MIXY_AGENT_ID);
244
919
  log(`${items.length} result(s) of the deleted agent ${agentId} go to ${mixyHome}`);
245
920
  await queue.move(homeId, mixyHome);
921
+ if (owed) await queue.dropWakes(homeId);
246
922
  void drain(mixyHome);
247
923
  return;
248
924
  }
@@ -253,53 +929,35 @@ export function createAgentHomeDelivery({
253
929
  // From here to the push nothing awaits, so no turn can start in between.
254
930
  const session = loadSession(homeId);
255
931
  if (!session || session.closed === true) {
256
- 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
+ }
257
939
  return later(homeId, missingRetryMs);
258
940
  }
259
941
  if (!Array.isArray(session.messages)) session.messages = [];
260
- const posted = new Set(session.messages.map((message) => message?.meta?.transcript?.messageId).filter(Boolean));
261
- const fresh = [];
262
- const omittedId = omitted ? messageIdOf(omitted.id) : '';
263
- if (omitted && !posted.has(omittedId)) {
264
- const message = routineOmittedMessage({ count: omitted.count, messageId: omittedId });
265
- session.messages.push(message);
266
- fresh.push(message);
267
- }
268
- const woken = [];
269
- for (const item of items) {
270
- const row = deliveredRow(item, messageIdOf(item.runId));
271
- // Every dequeued task result wakes once, posted in this pass or before.
272
- if (row.wake) woken.push(row.message);
273
- if (posted.has(messageIdOf(item.runId))) continue;
274
- session.messages.push(row.message);
275
- fresh.push(row);
276
- }
942
+ const { fresh, toWake } = postQueuedRows(session, items, omitted);
277
943
  for (const item of items) shownLive.delete(messageIdOf(item.runId));
278
944
  if (fresh.length) {
279
945
  session.updatedAt = Date.now();
280
946
  await saveSession(session);
281
947
  for (const { message, notice } of fresh) {
282
- try {
283
- live()?.notice?.(homeId, { ...notice, messageId: message.meta.transcript.messageId });
284
- } catch (error) {
285
- log(`live notice in ${homeId} failed: ${error?.message || error}`);
286
- }
948
+ noticeLive(live, log, homeId, { ...notice, messageId: message.meta.transcript.messageId });
287
949
  }
288
950
  }
289
- await queue.remove(
951
+ // Dequeuing and owing the wake are one write: no restart between posting
952
+ // and waking loses the wake.
953
+ const { overflowed } = await queue.remove(
290
954
  homeId,
291
955
  items.map((item) => item.runId),
292
- omitted
956
+ omitted,
957
+ { wake: toWake }
293
958
  );
294
- // Dequeued task results wake the agent with ONE hidden turn to relay them;
295
- // a replayed run id is never queued again, so it wakes nobody.
296
- if (woken.length) {
297
- try {
298
- await live()?.wake?.(homeId, agentTaskWakePrompt(woken));
299
- } catch (error) {
300
- log(`waking ${homeId} failed: ${error?.message || error}`);
301
- }
302
- }
959
+ if (overflowed) log(`${homeId} is at its wake cap: ${overflowed} result(s) ride the next wake as a count`);
960
+ await relay.wakeOwed(homeId, session);
303
961
  }
304
962
 
305
963
  /** The single drainer of `homeId`; a call during a pass runs one more pass. */
@@ -332,17 +990,7 @@ export function createAgentHomeDelivery({
332
990
  async function enqueue(row, result) {
333
991
  const homeId = agentHomeSessionId(scheduleOwnerAgentId(row));
334
992
  if (!result?.runId) throw new Error('routine result: runId required');
335
- await queue.add(homeId, {
336
- runId: String(result.runId),
337
- kind: result.kind === 'webhook' ? 'webhook' : 'schedule',
338
- name: row.name,
339
- ok: result.ok === true,
340
- text: routineSummary(result.text),
341
- runSessionId: result.runSessionId || null,
342
- ...(result.reason ? { reason: String(result.reason) } : {}),
343
- ...(result.paused === true ? { paused: true } : {}),
344
- at: Date.now(),
345
- });
993
+ await queue.add(homeId, routineResultItem(row, result));
346
994
  void drain(homeId);
347
995
  return homeId;
348
996
  }
@@ -365,19 +1013,22 @@ export function createAgentHomeDelivery({
365
1013
  /** Persist one report of a coding task session `agentId` created (session
366
1014
  * create) for its home thread, then start draining it. `status` 'started'
367
1015
  * and 'blocked' only post a line; a turn's end also wakes the agent. */
368
- async function enqueueTask(agentId, { runId, taskSessionId, title, status, text = '' }) {
1016
+ async function enqueueTask(agentId, report) {
1017
+ const { runId, taskSessionId, status, text = '', watchId = '', from = '' } = report;
369
1018
  const homeId = agentHomeSessionId(agentId) || agentHomeSessionId(MIXY_AGENT_ID);
370
1019
  if (!runId || !taskSessionId) throw new Error('task report: runId and taskSessionId required');
371
- await queue.add(homeId, {
372
- runId: String(runId),
373
- kind: 'agent-task',
374
- taskSessionId,
375
- title,
376
- status,
377
- text: routineSummary(text),
378
- roundTrip: status !== 'started' && status !== 'blocked',
379
- at: Date.now(),
380
- });
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 });
381
1032
  void drain(homeId);
382
1033
  return homeId;
383
1034
  }
@@ -390,35 +1041,74 @@ export function createAgentHomeDelivery({
390
1041
  const homeId = agentHomeSessionId(scheduleOwnerAgentId(row));
391
1042
  if (!run?.runId || busy(homeId)) return false;
392
1043
  const notice = routineRunNotice({ ...run, name: row.name, messageId: messageIdOf(run.runId) });
393
- try {
394
- return live()?.notice?.(homeId, notice) === true;
395
- } catch (error) {
396
- log(`live notice in ${homeId} failed: ${error?.message || error}`);
397
- return false;
1044
+ return noticeLive(live, log, homeId, notice) === true;
1045
+ }
1046
+
1047
+ const homeDeleted = (homeId) => homeProvenDeleted(homeId, agentPresence, roomPresence);
1048
+
1049
+ /** Retire the bookkeeping of deleted homes (and stale round counts). */
1050
+ async function prune() {
1051
+ await queue.prune((await queue.bookkeptHomes()).filter(homeDeleted));
1052
+ }
1053
+
1054
+ /** Store `watch`; a home at its watch cap first gives up its oldest watches
1055
+ * whose target sessions are proven deleted, and refuses the watch (so the
1056
+ * send claims no report) when that frees too few. */
1057
+ async function watchTask(watch) {
1058
+ const drop = watchesToDrop(await queue.watches(), watch, queue.watchCap, sessionDeleted);
1059
+ if (drop.length) {
1060
+ log(`${watch.homeSessionId} is at its watch cap: dropped ${drop.length} watch(es) of deleted sessions`);
398
1061
  }
1062
+ await queue.watch(watch, { drop });
399
1063
  }
400
1064
 
401
- /** Drain every queue left by an earlier process. */
1065
+ /** Drain every queue left by an earlier process, after retiring stale bookkeeping. */
402
1066
  async function recover() {
1067
+ try {
1068
+ await prune();
1069
+ } catch (error) {
1070
+ log(`pruning the routine results failed: ${error?.message || error}`);
1071
+ }
403
1072
  const homes = await queue.pendingHomes();
404
1073
  for (const homeId of homes) void drain(homeId);
405
1074
  return homes;
406
1075
  }
407
1076
 
408
- return { enqueue, enqueueHeld, enqueueTask, announce, drain, recover };
1077
+ return {
1078
+ enqueue,
1079
+ enqueueHeld,
1080
+ enqueueTask,
1081
+ announce,
1082
+ drain,
1083
+ recover,
1084
+ watchTask,
1085
+ unwatchTask: (runId) => queue.unwatch(runId),
1086
+ taskWatches: () => queue.watches(),
1087
+ resetRounds: (homeId) => queue.resetRounds(homeId),
1088
+ };
409
1089
  }
410
1090
 
411
1091
  let defaultDelivery = null;
412
1092
  /** The process delivery over the session store and data dir; built on first use. */
413
1093
  async function processDelivery() {
414
1094
  if (!defaultDelivery) {
415
- const [{ loadSession, saveSessionAsync }, { isSessionTurnInFlight }, { getPluginData }, { createAgentEditorApi }] =
416
- await Promise.all([
417
- import('../runtime/agent/orchestrator/session/store.mjs'),
418
- import('../runtime/agent/orchestrator/session/manager/runtime-liveness.mjs'),
419
- import('../runtime/agent/orchestrator/config.mjs'),
420
- import('./workflow-agents-api/agent-editor.mjs'),
421
- ]);
1095
+ const [
1096
+ { loadSession, saveSessionAsync },
1097
+ { isSessionTurnInFlight },
1098
+ { getPluginData },
1099
+ { createAgentEditorApi },
1100
+ { probePath, PROBE_ABSENT },
1101
+ { sessionPath },
1102
+ { mixdogRoot },
1103
+ ] = await Promise.all([
1104
+ import('../runtime/agent/orchestrator/session/store.mjs'),
1105
+ import('../runtime/agent/orchestrator/session/manager/runtime-liveness.mjs'),
1106
+ import('../runtime/agent/orchestrator/config.mjs'),
1107
+ import('./workflow-agents-api/agent-editor.mjs'),
1108
+ import('../runtime/agent/orchestrator/session/store/fs-probe.mjs'),
1109
+ import('../runtime/agent/orchestrator/session/store/session-paths.mjs'),
1110
+ import('../runtime/shared/plugin-paths.mjs'),
1111
+ ]);
422
1112
  // The editor's per-id definition probe: 'deleted' only when proven gone.
423
1113
  const { agentDefinitionPresence } = createAgentEditorApi({ cfgMod: { getPluginData } });
424
1114
  defaultDelivery = createAgentHomeDelivery({
@@ -427,6 +1117,19 @@ async function processDelivery() {
427
1117
  turnInFlight: isSessionTurnInFlight,
428
1118
  queue: createRoutineResultQueue(join(getPluginData(), 'routine-results.json')),
429
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 }),
1123
+ // Deleting a session unlinks its record (session-delete.mjs) and leaves
1124
+ // no tombstone: only an absent file with no in-memory copy proves it.
1125
+ // An unreadable file probes 'unreadable' and stays.
1126
+ sessionDeleted: (sessionId) => {
1127
+ try {
1128
+ return probePath(sessionPath(sessionId)).state === PROBE_ABSENT && !loadSession(sessionId);
1129
+ } catch {
1130
+ return false;
1131
+ }
1132
+ },
430
1133
  });
431
1134
  }
432
1135
  return defaultDelivery;
@@ -453,6 +1156,26 @@ export async function deliverAgentTask(agentId, report) {
453
1156
  return (await processDelivery()).enqueueTask(agentId, report);
454
1157
  }
455
1158
 
1159
+ /** Keep one reporting turn's watch until its report is queued. */
1160
+ export async function watchAgentTask(watch) {
1161
+ return (await processDelivery()).watchTask(watch);
1162
+ }
1163
+
1164
+ /** Drop one reporting turn's watch (its turn was never taken). */
1165
+ export async function unwatchAgentTask(runId) {
1166
+ return (await processDelivery()).unwatchTask(runId);
1167
+ }
1168
+
1169
+ /** A user's own message in `homeId`: its round trips with every target restart. */
1170
+ export async function resetAgentTaskRounds(homeId) {
1171
+ return (await processDelivery()).resetRounds(homeId);
1172
+ }
1173
+
1174
+ /** The reporting turns whose report was not queued yet. */
1175
+ export async function agentTaskWatches() {
1176
+ return (await processDelivery()).taskWatches();
1177
+ }
1178
+
456
1179
  /** Daemon start: post the results an earlier process left queued. */
457
1180
  export async function recoverScheduleResults() {
458
1181
  return (await processDelivery()).recover();