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
@@ -104,6 +104,9 @@ export function createShellRun({ abortSignal, resolve, jobId = null }) {
104
104
  // `settled`: whichever transition wins first wins for good.
105
105
  autoBackgrounded: false,
106
106
  autoBackgroundJobId: null,
107
+ // [exitCode, signal] of a 'close' that arrived while promotion was in
108
+ // flight; a promotion that then fails settles the run with it.
109
+ promotionClose: null,
107
110
  autoBgTimer: null,
108
111
  killUnconfirmed: false,
109
112
  child: null,
@@ -288,6 +288,7 @@ async function settleQueuedPromotion(jobId) {
288
288
  function queueSaturatedAsTask(run, saturation, spec) {
289
289
  const queuedAbort = new AbortController();
290
290
  const taskId = run.foregroundRecordId;
291
+ let promotedJobId = null;
291
292
  let task;
292
293
  try {
293
294
  task = startBackgroundTask({
@@ -301,10 +302,15 @@ function queueSaturatedAsTask(run, saturation, spec) {
301
302
  input: { command: spec.command, cwd: spec.cwd },
302
303
  meta: { task_id: taskId, stdout: null, stderr: null, cwd: spec.cwd, timeoutMs: spec.backgroundDeadlineMs || 0 },
303
304
  resultType: 'shell_task_result',
305
+ // While the command runs in the foreground, its abort handler snapshots
306
+ // the tree before it kills. A native kill sent here as well would end
307
+ // the root first: the snapshot then misses the tree and the workspace
308
+ // stays held as unconfirmed forever. Only a promoted job is killed here.
304
309
  cancel: () => {
305
310
  queuedAbort.abort();
311
+ if (!promotedJobId) return;
306
312
  try {
307
- killShellJob(taskId);
313
+ killShellJob(promotedJobId);
308
314
  } catch {}
309
315
  },
310
316
  run: async () => {
@@ -319,7 +325,17 @@ function queueSaturatedAsTask(run, saturation, spec) {
319
325
  onProgress: null,
320
326
  onOutputTail: null,
321
327
  });
322
- if (result.backgrounded && result.jobId) return settleQueuedPromotion(result.jobId);
328
+ if (result.backgrounded && result.jobId) {
329
+ promotedJobId = result.jobId;
330
+ // Promotion detached the abort handler: a cancel that landed during
331
+ // the handoff reaches the promoted job here.
332
+ if (queuedAbort.signal.aborted) {
333
+ try {
334
+ killShellJob(promotedJobId);
335
+ } catch {}
336
+ }
337
+ return settleQueuedPromotion(promotedJobId);
338
+ }
323
339
  const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
324
340
  if (result.exitCode !== 0) {
325
341
  throw new Error(`exit code ${result.exitCode ?? result.signal ?? 'unknown'}${output ? `\n${output}` : ''}`);
@@ -573,65 +589,67 @@ export function execShellCommand(options) {
573
589
  // The user approved this call's elevation 'ask' decision (runtime-set).
574
590
  elevationApproved = false,
575
591
  } = options;
576
- return new Promise(async (resolve) => {
592
+ return new Promise((resolve) => {
577
593
  const run = createShellRun({ abortSignal, resolve, jobId });
578
- // An async executor's throw is NOT routed to this Promise — it becomes an
579
- // unhandled rejection of the executor's own promise while the awaiting
580
- // caller hangs forever. Every path below therefore has to settle the run
581
- // itself, including the unexpected one.
582
- try {
583
- const spawned = await admitAndSpawn(
584
- run,
585
- {
586
- admission,
587
- shell,
588
- shellArg,
589
- shellArgs,
594
+ // A throw inside this async body is NOT routed to the Promise — it becomes
595
+ // an unhandled rejection while the awaiting caller hangs forever. Every
596
+ // path below therefore has to settle the run itself, including the
597
+ // unexpected one.
598
+ void (async () => {
599
+ try {
600
+ const spawned = await admitAndSpawn(
601
+ run,
602
+ {
603
+ admission,
604
+ shell,
605
+ shellArg,
606
+ shellArgs,
607
+ command,
608
+ env,
609
+ cwd,
610
+ abortSignal,
611
+ directArgv,
612
+ execScript,
613
+ elevationApproved,
614
+ ownerSessionId,
615
+ clientHostPid,
616
+ },
617
+ {
618
+ admissionWaitMs,
619
+ onSaturated: (err) => backgroundOnTimeout && queueSaturatedAsTask(run, err, options),
620
+ }
621
+ );
622
+ if (!spawned) return;
623
+
624
+ // Pre-aborted signal: kill immediately if the abort already fired
625
+ // before spawn returned (synchronous reentry from a parent abort), so
626
+ // the child doesn't run for the full timeoutMs window.
627
+ if (abortSignal?.aborted) {
628
+ treeKillForceSettle(run, 'cancellation');
629
+ }
630
+ attachOutputCapture(run);
631
+ const releasePromotedCapture = createPromotedCaptureRelease(run);
632
+ run.settle = createSettle(run, releasePromotedCapture);
633
+ attachChildExit(run, releasePromotedCapture);
634
+ const fireAutoBackground = createAutoBackground({
635
+ run,
590
636
  command,
591
- env,
592
637
  cwd,
593
- abortSignal,
594
- directArgv,
595
- execScript,
596
- elevationApproved,
597
- ownerSessionId,
598
638
  clientHostPid,
599
- },
600
- {
601
- admissionWaitMs,
602
- onSaturated: (err) => backgroundOnTimeout && queueSaturatedAsTask(run, err, options),
603
- }
604
- );
605
- if (!spawned) return;
606
-
607
- // Pre-aborted signal: kill immediately if the abort already fired
608
- // before spawn returned (synchronous reentry from a parent abort), so
609
- // the child doesn't run for the full timeoutMs window.
610
- if (abortSignal?.aborted) {
611
- treeKillForceSettle(run, 'cancellation');
639
+ ownerSessionId,
640
+ promotedTimeoutMs,
641
+ backgroundDeadlineMs,
642
+ });
643
+ armDeadlines(
644
+ run,
645
+ { timeoutMs, autoBackgroundMs, autoBackgroundReason, backgroundOnTimeout, abortSignal },
646
+ fireAutoBackground
647
+ );
648
+ armInputWake(run, wakeSignal, fireAutoBackground);
649
+ armProgressTimers(run, { onProgress, onOutputTail });
650
+ } catch (err) {
651
+ settleSetupFailure(run, err);
612
652
  }
613
- attachOutputCapture(run);
614
- const releasePromotedCapture = createPromotedCaptureRelease(run);
615
- run.settle = createSettle(run, releasePromotedCapture);
616
- attachChildExit(run, releasePromotedCapture);
617
- const fireAutoBackground = createAutoBackground({
618
- run,
619
- command,
620
- cwd,
621
- clientHostPid,
622
- ownerSessionId,
623
- promotedTimeoutMs,
624
- backgroundDeadlineMs,
625
- });
626
- armDeadlines(
627
- run,
628
- { timeoutMs, autoBackgroundMs, autoBackgroundReason, backgroundOnTimeout, abortSignal },
629
- fireAutoBackground
630
- );
631
- armInputWake(run, wakeSignal, fireAutoBackground);
632
- armProgressTimers(run, { onProgress, onOutputTail });
633
- } catch (err) {
634
- settleSetupFailure(run, err);
635
- }
653
+ })();
636
654
  });
637
655
  }
@@ -166,6 +166,12 @@ function saveBuffer(buffer) {
166
166
  return { attachmentRef, sizeBytes: buffer.length };
167
167
  }
168
168
 
169
+ /** Store `buffer` (a file a bot or agent hands on, session-runtime/
170
+ * handoff-attachments.mjs) under its content address: `{ attachmentRef, sizeBytes }`. */
171
+ export function storeAttachmentBuffer(buffer) {
172
+ return saveBuffer(buffer);
173
+ }
174
+
169
175
  export function isAttachmentReference(value) {
170
176
  return Boolean(value && typeof value === 'object' && ATTACHMENT_REF_RE.test(String(value.attachmentRef || '')));
171
177
  }
@@ -53,6 +53,30 @@ export async function finishBrowserTurn(sessionId, turnId, { aborted = false } =
53
53
  }
54
54
  }
55
55
 
56
+ /** Runtime-only: fill the secret the user handed the desktop for `requestId`
57
+ * into the page's focused password field through the host's isolated-world
58
+ * fill. The value never passes through here; the host answers only
59
+ * 'provided' or 'skipped'. */
60
+ export async function browserSecretInput(sessionId, requestId, { signal } = {}) {
61
+ const discovery = readBridgeDiscovery(DISCOVERY_FILE);
62
+ if (!discovery || !sessionId || !requestId) return 'skipped';
63
+ try {
64
+ const result = await requestBridge(
65
+ discovery,
66
+ JSON.stringify({ action: 'secret_input', session_id: String(sessionId), request_id: String(requestId) }),
67
+ signal
68
+ ? AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), signal])
69
+ : AbortSignal.timeout(REQUEST_TIMEOUT_MS),
70
+ { sessionId, action: 'secret_input' }
71
+ );
72
+ return result.status === 200 && result.body?.ok !== false && result.body?.value?.text === 'provided'
73
+ ? 'provided'
74
+ : 'skipped';
75
+ } catch {
76
+ return 'skipped';
77
+ }
78
+ }
79
+
56
80
  const BRIDGE_UNAVAILABLE_MESSAGE = 'browser use is unavailable; open the Mixdog desktop app and enable Browser Use';
57
81
 
58
82
  /** Sync gate for the session tool surface (featureDisallowedTools). */
@@ -12,6 +12,7 @@ import {
12
12
  markFired,
13
13
  markDone,
14
14
  markScheduleFailure,
15
+ markScheduleSkipped,
15
16
  markScheduleSuccess,
16
17
  setDeferred,
17
18
  setNextFire,
@@ -61,6 +62,11 @@ function isCronExpression(time) {
61
62
  return false;
62
63
  }
63
64
  }
65
+ // True when this fire (started at `since`) ended in an overlap skip.
66
+ function skippedSince(schedule, since) {
67
+ const at = schedule.lastSkippedAt ? new Date(schedule.lastSkippedAt).getTime() : 0;
68
+ return at >= since.getTime();
69
+ }
64
70
  // Records a one-shot fire that did not complete; the entry stays pending.
65
71
  async function markOneShotFailed(schedule) {
66
72
  schedule.lastFailedAt = new Date().toISOString();
@@ -407,6 +413,8 @@ ${prompt}`;
407
413
  schedule.lastFiredAt = startedAt.toISOString();
408
414
  this.lastFired.set(schedule.name, startedAt.toISOString());
409
415
  const fired = await this.fireTimed(schedule, type, { awaitDispatch: true });
416
+ // An overlap skip is recorded as a skip, never as a failed run.
417
+ if (!fired && skippedSince(schedule, startedAt)) return;
410
418
  if (!fired) {
411
419
  schedule.lastFailedAt = new Date().toISOString();
412
420
  await markScheduleFailure(schedule.name, new Date());
@@ -498,7 +506,7 @@ ${prompt}`;
498
506
  return;
499
507
  }
500
508
  // Mark done ONLY after a successful fire. A false/throwing fireTimed
501
- // (missing prompt/model, running guard, dispatch error) must leave the
509
+ // (missing prompt, running guard, paused owner, dispatch error) must leave the
502
510
  // one-shot pending so a reload/restart can retry it — never retire it.
503
511
  try {
504
512
  // awaitDispatch: for a one-shot we must wait for the actual LLM/relay
@@ -506,6 +514,10 @@ ${prompt}`;
506
514
  // contract) would retire the entry even when schedulerLlm() later
507
515
  // rejects. Only a resolved dispatch counts as a real fire here.
508
516
  const fired = await this.fireTimed(schedule, type, { awaitDispatch: true });
517
+ if (!fired && skippedSince(schedule, now)) {
518
+ logSchedule(`one-shot "${schedule.name}" skipped (previous run in progress) — leaving pending for retry\n`);
519
+ return;
520
+ }
509
521
  if (!fired) {
510
522
  await markOneShotFailed(schedule);
511
523
  logSchedule(`one-shot "${schedule.name}" did not fire (skipped/guarded) — leaving pending for retry\n`);
@@ -625,20 +637,11 @@ ${prompt}`;
625
637
  async fireTimedPrompt(schedule, type, prompt, { awaitDispatch = false } = {}) {
626
638
  logSchedule(`firing ${schedule.name} (${type})\n`);
627
639
  if (this.running.has(schedule.name)) {
628
- // Not silent anymore: overlap skips were invisible and read as the
629
- // scheduler "ignoring" a fire.
630
- logSchedule(`${schedule.name}: skipped — previous run still in progress\n`);
640
+ this.recordSkip(schedule);
631
641
  return false;
632
642
  }
633
643
  if (type === 'interactive' && this.injectIntoLead(schedule, type, prompt)) return true;
634
644
  this.running.add(schedule.name);
635
- const presetId = schedule.model;
636
- if (!presetId) {
637
- this.running.delete(schedule.name);
638
- logSchedule(`${schedule.name}: missing required "model" in schedule config — dispatch rejected\n`);
639
- this.notifyFailure(schedule, 'missing "model" in schedule config — dispatch rejected');
640
- return false;
641
- }
642
645
  // Cron (recurring) keeps the fire-and-forget contract: return truthy now
643
646
  // and swallow async failures. A one-shot (awaitDispatch) instead awaits
644
647
  // the dispatch so a rejected run propagates and the caller leaves the
@@ -676,8 +679,22 @@ ${prompt}`;
676
679
  ? this.scheduleRunner(schedule, { prompt })
677
680
  : Promise.reject(new Error('schedule runner not configured'));
678
681
  return run
679
- .then(({ result }) => {
682
+ .then((outcome) => {
680
683
  this.running.delete(schedule.name);
684
+ // The run path found this schedule's previous run still going (a
685
+ // "run now" from the app) or backing off after failures in a row; it
686
+ // already persisted the skip.
687
+ if (outcome?.skipped) {
688
+ schedule.lastSkippedAt = new Date().toISOString();
689
+ const why =
690
+ outcome.reason === 'backoff'
691
+ ? 'backing off after failures'
692
+ : outcome.reason === 'owner'
693
+ ? `paused: ${outcome.message}`
694
+ : 'previous run still in progress';
695
+ logSchedule(`${schedule.name}: skipped — ${why}\n`);
696
+ return false;
697
+ }
681
698
  logSchedule(`${schedule.name} done\n`);
682
699
  return true;
683
700
  })
@@ -693,6 +710,17 @@ ${prompt}`;
693
710
  });
694
711
  }
695
712
  // ── Helpers ─────────────────────────────────────────────────────────
713
+ /** Overlap skip: the previous run of this schedule is still in progress.
714
+ * Logged and persisted (last_skipped_at) so a skipped fire is never
715
+ * invisible; `persistSkip` is injectable for tests. */
716
+ recordSkip(schedule) {
717
+ const at = new Date();
718
+ schedule.lastSkippedAt = at.toISOString();
719
+ logSchedule(`${schedule.name}: skipped — previous run still in progress\n`);
720
+ void Promise.resolve()
721
+ .then(() => (this.persistSkip ?? markScheduleSkipped)(schedule.name, at))
722
+ .catch((err) => process.stderr.write(`mixdog scheduler: ${schedule.name} skip record failed: ${err}\n`));
723
+ }
696
724
  /** Failure notice: logged so scheduled fires never fail silently. */
697
725
  notifyFailure(schedule, reason) {
698
726
  logSchedule(`schedule "${schedule?.name}": ${reason}\n`);
@@ -14,6 +14,7 @@
14
14
  import { ensurePgInstance, withSchemaBootstrapLock } from '../../memory/lib/pg/adapter.mjs';
15
15
  import { createPgSchemaDb } from '../../shared/pg-schema-db.mjs';
16
16
  import { resolvePluginData } from '../../shared/plugin-paths.mjs';
17
+ import { ROUTINE_LIMITS } from '../../shared/routine-limits.mjs';
17
18
 
18
19
  const SCHEMA = 'scheduler';
19
20
 
@@ -54,6 +55,18 @@ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_scheduled_at times
54
55
  ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_started_at timestamptz;
55
56
  ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_success_at timestamptz;
56
57
  ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_failed_at timestamptz;
58
+ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_skipped_at timestamptz;
59
+ -- The agent a schedule belongs to (its results land in that agent's home
60
+ -- thread). Rows from before ownership existed fill with Mixy ('lead').
61
+ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS owner_agent_id text NOT NULL DEFAULT 'lead';
62
+ -- Legacy columns model, workflow and run_as_agent are no longer read or written (kept, not dropped, to avoid a risky migration).
63
+ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS run_as_agent boolean NOT NULL DEFAULT false;
64
+ -- Failed runs in a row: a success or switching it back on resets it; at
65
+ -- ROUTINE_LIMITS.pauseAfterFailures the schedule switches itself off.
66
+ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS consecutive_failures integer NOT NULL DEFAULT 0;
67
+ -- Results of finished runs not yet queued for the owner's home thread, by run
68
+ -- id ({ state: 'pending' | 'failed', error?, at, result }).
69
+ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS result_delivery jsonb;
57
70
  `;
58
71
 
59
72
  const getDb = createPgSchemaDb({
@@ -78,9 +91,7 @@ function rowToDef(row) {
78
91
  timezone: row.timezone,
79
92
  target: row.target,
80
93
  channelId: row.channel_id,
81
- model: row.model,
82
94
  cwd: row.cwd,
83
- workflow: row.workflow,
84
95
  attachments: row.attachments || null,
85
96
  delivery: row.delivery || null,
86
97
  prompt: row.prompt,
@@ -91,6 +102,10 @@ function rowToDef(row) {
91
102
  lastStartedAt: row.last_started_at,
92
103
  lastSuccessAt: row.last_success_at,
93
104
  lastFailedAt: row.last_failed_at,
105
+ lastSkippedAt: row.last_skipped_at,
106
+ ownerAgentId: row.owner_agent_id || 'lead',
107
+ consecutiveFailures: Number(row.consecutive_failures) || 0,
108
+ resultDelivery: row.result_delivery || null,
94
109
  nextFireAt: row.next_fire_at,
95
110
  deferredUntil: row.deferred_until,
96
111
  skippedUntil: row.skipped_until,
@@ -100,7 +115,7 @@ function rowToDef(row) {
100
115
  }
101
116
 
102
117
  const COLS =
103
- 'name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, workflow, attachments, delivery, prompt, enabled, status, last_fired_at, last_scheduled_at, last_started_at, last_success_at, last_failed_at, next_fire_at, deferred_until, skipped_until, created_at, updated_at';
118
+ 'name, description, when_at, when_cron, timezone, target, channel_id, cwd, attachments, delivery, prompt, enabled, status, last_fired_at, last_scheduled_at, last_started_at, last_success_at, last_failed_at, last_skipped_at, owner_agent_id, consecutive_failures, result_delivery, next_fire_at, deferred_until, skipped_until, created_at, updated_at';
104
119
 
105
120
  // ---------------------------------------------------------------------------
106
121
  // Public API
@@ -134,30 +149,28 @@ export async function upsertSchedule(def, { dataDir } = {}) {
134
149
  def.timezone ?? null,
135
150
  def.target,
136
151
  def.channelId ?? null,
137
- def.model ?? null,
138
152
  def.cwd ?? null,
139
- def.workflow ?? null,
140
153
  def.attachments ? JSON.stringify(def.attachments) : null,
141
154
  def.delivery ?? null,
142
155
  def.prompt,
143
156
  def.enabled ?? true,
144
157
  def.status ?? 'active',
145
158
  def.nextFireAt ?? null,
159
+ def.ownerAgentId || 'lead',
146
160
  ];
147
161
  const { rows } = await db.query(
148
162
  `INSERT INTO scheduler.schedules
149
- (name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, workflow, attachments, delivery, prompt, enabled, status, next_fire_at)
150
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
163
+ (name, description, when_at, when_cron, timezone, target, channel_id, cwd, attachments, delivery, prompt, enabled, status, next_fire_at, owner_agent_id)
164
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
151
165
  ON CONFLICT (name) DO UPDATE SET
166
+ owner_agent_id = EXCLUDED.owner_agent_id,
152
167
  description = EXCLUDED.description,
153
168
  when_at = EXCLUDED.when_at,
154
169
  when_cron = EXCLUDED.when_cron,
155
170
  timezone = EXCLUDED.timezone,
156
171
  target = EXCLUDED.target,
157
172
  channel_id = EXCLUDED.channel_id,
158
- model = EXCLUDED.model,
159
173
  cwd = EXCLUDED.cwd,
160
- workflow = EXCLUDED.workflow,
161
174
  attachments = EXCLUDED.attachments,
162
175
  delivery = EXCLUDED.delivery,
163
176
  prompt = EXCLUDED.prompt,
@@ -173,6 +186,8 @@ export async function upsertSchedule(def, { dataDir } = {}) {
173
186
  last_started_at = NULL,
174
187
  last_success_at = NULL,
175
188
  last_failed_at = NULL,
189
+ last_skipped_at = NULL,
190
+ consecutive_failures = 0,
176
191
  updated_at = now()
177
192
  RETURNING ${COLS}`,
178
193
  params
@@ -198,8 +213,16 @@ async function updateScheduleColumn(name, column, value, dataDir) {
198
213
  return rowToDef(rows[0]);
199
214
  }
200
215
 
216
+ /** Switching a schedule back on clears its failure streak. */
201
217
  export async function setEnabled(name, enabled, { dataDir } = {}) {
202
- return updateScheduleColumn(name, 'enabled', !!enabled, dataDir);
218
+ const db = await getDb(dataDir);
219
+ const { rows } = await db.query(
220
+ `UPDATE scheduler.schedules
221
+ SET enabled = $2, consecutive_failures = CASE WHEN $2 THEN 0 ELSE consecutive_failures END, updated_at = now()
222
+ WHERE name = $1 RETURNING ${COLS}`,
223
+ [name, !!enabled]
224
+ );
225
+ return rowToDef(rows[0]);
203
226
  }
204
227
 
205
228
  export async function markFired(name, ts = new Date(), { dataDir } = {}) {
@@ -240,11 +263,47 @@ export async function claimScheduleRun(name, scheduledAt, startedAt, nextFireAt,
240
263
  }
241
264
 
242
265
  export async function markScheduleSuccess(name, ts = new Date(), { dataDir } = {}) {
243
- return updateScheduleColumn(name, 'last_success_at', ts, dataDir);
266
+ const db = await getDb(dataDir);
267
+ const { rows } = await db.query(
268
+ `UPDATE scheduler.schedules SET last_success_at = $2, consecutive_failures = 0, updated_at = now()
269
+ WHERE name = $1 RETURNING ${COLS}`,
270
+ [name, ts]
271
+ );
272
+ return rowToDef(rows[0]);
244
273
  }
245
274
 
275
+ /** One more failure in a row; the one reaching the pause limit switches the
276
+ * schedule off (claimScheduleRun admits only enabled rows). */
246
277
  export async function markScheduleFailure(name, ts = new Date(), { dataDir } = {}) {
247
- return updateScheduleColumn(name, 'last_failed_at', ts, dataDir);
278
+ const db = await getDb(dataDir);
279
+ const { rows } = await db.query(
280
+ `UPDATE scheduler.schedules
281
+ SET last_failed_at = $2, consecutive_failures = consecutive_failures + 1,
282
+ enabled = enabled AND consecutive_failures + 1 < $3, updated_at = now()
283
+ WHERE name = $1 RETURNING ${COLS}`,
284
+ [name, ts, ROUTINE_LIMITS.pauseAfterFailures]
285
+ );
286
+ return rowToDef(rows[0]);
287
+ }
288
+
289
+ /** Set (`entry`) or clear (null) the undelivered result of run `runId`. */
290
+ export async function recordScheduleDelivery(name, runId, entry, { dataDir } = {}) {
291
+ const db = await getDb(dataDir);
292
+ await db.query(
293
+ entry
294
+ ? `UPDATE scheduler.schedules
295
+ SET result_delivery = COALESCE(result_delivery, '{}'::jsonb) || jsonb_build_object($2::text, $3::jsonb)
296
+ WHERE name = $1`
297
+ : `UPDATE scheduler.schedules
298
+ SET result_delivery = NULLIF(COALESCE(result_delivery, '{}'::jsonb) - $2::text, '{}'::jsonb)
299
+ WHERE name = $1`,
300
+ entry ? [name, runId, JSON.stringify(entry)] : [name, runId]
301
+ );
302
+ }
303
+
304
+ /** A fire skipped because the previous run of the same schedule still runs. */
305
+ export async function markScheduleSkipped(name, ts = new Date(), { dataDir } = {}) {
306
+ return updateScheduleColumn(name, 'last_skipped_at', ts, dataDir);
248
307
  }
249
308
 
250
309
  export async function markDone(name, { dataDir } = {}) {
@@ -1,9 +1,14 @@
1
1
  import { logWebhook } from './log.mjs';
2
- import { updateDeliveryStatus } from '../webhooks-db.mjs';
2
+ import { recordEndpointEvent, updateDeliveryStatus } from '../webhooks-db.mjs';
3
3
  import { UNTRUSTED_MARKER, scrubFenceMarker } from './untrusted-fence.mjs';
4
4
 
5
5
  const DISPATCH_TIMEOUT_MS = 10 * 60 * 1000;
6
6
 
7
+ /** The headers a run's prompt shows (and a stored event keeps). */
8
+ function promptHeaders(headers) {
9
+ return Object.fromEntries(Object.entries(headers || {}).filter(([k]) => k.startsWith('x-') || k === 'content-type'));
10
+ }
11
+
7
12
  function buildFencedPayload(body, headers) {
8
13
  // Trust boundary: webhook body + headers are external input and must be
9
14
  // treated as DATA, never instructions. The directive line gives the
@@ -11,8 +16,7 @@ function buildFencedPayload(body, headers) {
11
16
  const _UNTRUSTED = UNTRUSTED_MARKER;
12
17
  const payload = scrubFenceMarker(JSON.stringify(body, null, 2));
13
18
  const headersSummary = scrubFenceMarker(
14
- Object.entries(headers)
15
- .filter(([k]) => k.startsWith('x-') || k === 'content-type')
19
+ Object.entries(promptHeaders(headers))
16
20
  .map(([k, v]) => `${k}: ${v}`)
17
21
  .join('\n')
18
22
  );
@@ -35,6 +39,14 @@ function webhookRunPrompt(instructions, body, headers) {
35
39
  return { prompt: `${trusted}\n\n${buildFencedPayload(body, headers)}`, skillSelection: trusted };
36
40
  }
37
41
 
42
+ /** A short label of the event for the routine marker in the bot's chat
43
+ * (display only, never model-visible): the sender's event type and action. */
44
+ function webhookEventSummary(headers, body) {
45
+ const field = (value) => (typeof value === 'string' ? value.trim() : '');
46
+ const type = field(headers?.['x-github-event']) || field(headers?.['sentry-hook-resource']) || field(body?.type);
47
+ return [type, field(body?.action)].filter(Boolean).join(' · ').slice(0, 120);
48
+ }
49
+
38
50
  function trackDispatch({ name, deliveryId, dispatchP, controller }) {
39
51
  let settled = false;
40
52
  const timeoutHandle = setTimeout(
@@ -63,7 +75,10 @@ function trackDispatch({ name, deliveryId, dispatchP, controller }) {
63
75
  { once: true }
64
76
  );
65
77
  dispatchP.then(
66
- () => finish('done', {}, `${name}: webhook session run dispatched (id=${deliveryId})`),
78
+ (run) =>
79
+ run?.skipped
80
+ ? finish('skipped', {}, `${name}: previous run still in progress; skipped (id=${deliveryId})`)
81
+ : finish('done', {}, `${name}: webhook session run dispatched (id=${deliveryId})`),
67
82
  (err) =>
68
83
  finish(
69
84
  'failed',
@@ -73,8 +88,8 @@ function trackDispatch({ name, deliveryId, dispatchP, controller }) {
73
88
  );
74
89
  }
75
90
 
76
- function createWebhookSessionDispatcher({ getConfig, getBridgeDispatch }) {
77
- async function dispatchSessionRun(name, model, fullPrompt, headers, deliveryId, res, extra = {}) {
91
+ function createWebhookSessionDispatcher({ getBridgeDispatch }) {
92
+ async function dispatchSessionRun(name, fullPrompt, deliveryId, res, extra = {}) {
78
93
  await updateDeliveryStatus(name, deliveryId, 'processing');
79
94
  // Session dispatch must not be allowed to hang forever — without a
80
95
  // ceiling a stuck LLM call leaves the delivery in `processing`
@@ -88,13 +103,10 @@ function createWebhookSessionDispatcher({ getConfig, getBridgeDispatch }) {
88
103
  const controller = new AbortController();
89
104
  const dispatchP = Promise.resolve(
90
105
  getBridgeDispatch()({
91
- model: model || null,
92
106
  prompt: fullPrompt,
93
107
  signal: controller.signal,
94
- // Endpoint-scoped project/workflow (New-task parity): the webhook row's
95
- // cwd/workflow define the created session, not the worker's global cwd.
96
- cwd: extra.cwd || getConfig()?.cwd || null,
97
- workflow: extra.workflow || null,
108
+ // The endpoint's project folder, named to the bot in the turn.
109
+ cwd: extra.cwd || null,
98
110
  attachments: extra.attachments || null,
99
111
  delivery: extra.delivery || null,
100
112
  // Trusted skill-selection input: only the endpoint-authored
@@ -104,8 +116,10 @@ function createWebhookSessionDispatcher({ getConfig, getBridgeDispatch }) {
104
116
  context: {
105
117
  source: 'webhook',
106
118
  endpoint: name,
119
+ // The bot whose chat the run is a turn in.
120
+ owner: extra.owner || null,
107
121
  deliveryId,
108
- event: headers['x-github-event'] || null,
122
+ event: extra.event || null,
109
123
  },
110
124
  })
111
125
  );
@@ -116,12 +130,18 @@ function createWebhookSessionDispatcher({ getConfig, getBridgeDispatch }) {
116
130
 
117
131
  async function dispatchEndpoint({ name, body, headers, res, deliveryId, endpoint }) {
118
132
  try {
119
- const { model, instructions, cwd, workflow, attachments, delivery } = endpoint;
133
+ const { instructions, cwd, attachments, delivery, ownerAgentId } = endpoint;
120
134
  const run = webhookRunPrompt(instructions, body, headers);
121
135
  if (!getBridgeDispatch()) throw new Error(`[webhook] session dispatch requires bridgeDispatch`);
122
- await dispatchSessionRun(name, model || null, run.prompt, headers, deliveryId, res, {
136
+ // Kept for the app's "Run now", which re-runs the latest event. Losing
137
+ // the record never blocks the delivery itself.
138
+ await recordEndpointEvent(name, { body, headers: promptHeaders(headers) }).catch((e) =>
139
+ logWebhook(`${name}: event record failed: ${e?.message || e}`)
140
+ );
141
+ await dispatchSessionRun(name, run.prompt, deliveryId, res, {
142
+ owner: ownerAgentId || null,
123
143
  cwd: cwd || null,
124
- workflow: workflow || null,
144
+ event: webhookEventSummary(headers, body),
125
145
  attachments: attachments || null,
126
146
  delivery: delivery || null,
127
147
  skillSelection: run.skillSelection,
@@ -137,4 +157,4 @@ function createWebhookSessionDispatcher({ getConfig, getBridgeDispatch }) {
137
157
  return { dispatchEndpoint };
138
158
  }
139
159
 
140
- export { buildFencedPayload, createWebhookSessionDispatcher, webhookRunPrompt };
160
+ export { buildFencedPayload, createWebhookSessionDispatcher, promptHeaders, webhookEventSummary, webhookRunPrompt };
@@ -23,7 +23,6 @@ class WebhookServer {
23
23
  },
24
24
  });
25
25
  this.sessionDispatcher = createWebhookSessionDispatcher({
26
- getConfig: () => this.config,
27
26
  getBridgeDispatch: () => this.bridgeDispatch,
28
27
  });
29
28
  this.requestHandler = createWebhookRequestHandler({