mixdog 1.2.6 → 1.3.0

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 (246) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  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 +7 -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 +11 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +175 -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/rules/agent/AGENT.md +4 -0
  17. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  18. package/src/rules/agent-mode/MIXY.md +21 -0
  19. package/src/rules/agent-mode/ROUTINE.md +6 -0
  20. package/src/rules/lead/LEAD.md +7 -3
  21. package/src/rules/shared/70-delivery.md +3 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  23. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  24. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  26. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  29. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  30. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  33. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  34. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  36. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  41. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  46. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  47. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
  48. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  50. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  52. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  54. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  56. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  57. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  58. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  59. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  60. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  61. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  62. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  63. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  66. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  67. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  68. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  69. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  70. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  72. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  73. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  74. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  75. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  76. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  83. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  84. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  89. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  92. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  95. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  96. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  97. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  98. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  99. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  100. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  101. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  102. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  105. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  106. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  107. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  108. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  109. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  110. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  111. package/src/runtime/browser-bridge/client.mjs +24 -0
  112. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  113. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  114. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  115. package/src/runtime/channels/lib/webhook.mjs +0 -1
  116. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  117. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  118. package/src/runtime/computer-bridge/client.mjs +28 -1
  119. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  120. package/src/runtime/connectors/google-mcp.mjs +446 -0
  121. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  122. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  123. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  124. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  125. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  126. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  127. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  128. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  129. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  130. package/src/runtime/memory/lib/memory.mjs +7 -1
  131. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  132. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  133. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  134. package/src/runtime/memory/tool-defs.mjs +5 -0
  135. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  136. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  137. package/src/runtime/shared/agent-avatar.mjs +320 -0
  138. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  139. package/src/runtime/shared/agent-route-config.mjs +16 -0
  140. package/src/runtime/shared/atomic-file.mjs +14 -1
  141. package/src/runtime/shared/config.mjs +8 -1
  142. package/src/runtime/shared/connector-accounts.mjs +15 -0
  143. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  144. package/src/runtime/shared/connector-catalog.mjs +230 -0
  145. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  146. package/src/runtime/shared/connector-triggers.mjs +43 -0
  147. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  148. package/src/runtime/shared/held-request-host.d.mts +12 -0
  149. package/src/runtime/shared/held-request-host.mjs +71 -0
  150. package/src/runtime/shared/plain-preview.d.mts +1 -0
  151. package/src/runtime/shared/plain-preview.mjs +28 -0
  152. package/src/runtime/shared/routine-limits.d.mts +11 -0
  153. package/src/runtime/shared/routine-limits.mjs +21 -0
  154. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  155. package/src/runtime/shared/routine-trigger.mjs +44 -0
  156. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  157. package/src/runtime/shared/slow-span.mjs +4 -12
  158. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  159. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  160. package/src/runtime/shared/turn-changes.mjs +0 -8
  161. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  162. package/src/runtime/shared/user-data-guard.mjs +114 -36
  163. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  164. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  165. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  166. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  167. package/src/session-runtime/boot/apis.mjs +50 -18
  168. package/src/session-runtime/boot/facade.mjs +5 -8
  169. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  170. package/src/session-runtime/boot/tools.mjs +3 -0
  171. package/src/session-runtime/channel-config-api.mjs +53 -5
  172. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  173. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  174. package/src/session-runtime/held-requests.mjs +228 -0
  175. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  176. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  177. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  178. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  179. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  180. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  181. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  182. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  184. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  185. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  186. package/src/session-runtime/model-route/set-route.mjs +14 -3
  187. package/src/session-runtime/owned-automation-run.mjs +229 -0
  188. package/src/session-runtime/resource-api.mjs +5 -0
  189. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  190. package/src/session-runtime/resource-skills-api.mjs +12 -0
  191. package/src/session-runtime/room-member-gate.mjs +48 -0
  192. package/src/session-runtime/routine-home-turn.mjs +156 -0
  193. package/src/session-runtime/routine-owner.mjs +48 -0
  194. package/src/session-runtime/schedule-session-run.mjs +30 -64
  195. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  196. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  197. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  198. package/src/session-runtime/services/channel-admin.mjs +93 -24
  199. package/src/session-runtime/services/memory-access.mjs +37 -0
  200. package/src/session-runtime/services/provider-admin.mjs +74 -39
  201. package/src/session-runtime/services/schedule-access.mjs +91 -0
  202. package/src/session-runtime/session-create/session-options.mjs +34 -7
  203. package/src/session-runtime/session-hooks.mjs +16 -8
  204. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  205. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  206. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  207. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  208. package/src/session-runtime/skills-api.mjs +16 -1
  209. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  210. package/src/session-runtime/turn/session-ops.mjs +8 -1
  211. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  212. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  213. package/src/session-runtime/webhook-session-run.mjs +49 -67
  214. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  215. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  216. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  217. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  218. package/src/standalone/daemon-agent-control.mjs +1 -8
  219. package/src/standalone/daemon-session-control.mjs +11 -2
  220. package/src/standalone/daemon.mjs +18 -4
  221. package/src/standalone/session-protocol.mjs +27 -0
  222. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  223. package/src/standalone/session-service.mjs +19 -0
  224. package/src/tui/app/slash-commands.mjs +20 -0
  225. package/src/tui/dist/index.mjs +164 -86
  226. package/src/tui/session/context-state.mjs +6 -1
  227. package/src/tui/session/notice-surface.mjs +35 -4
  228. package/src/tui/session/queue-helpers.mjs +2 -1
  229. package/src/tui/session/session-api/extensions.mjs +4 -0
  230. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  231. package/src/tui/session/session-api/integrations.mjs +8 -0
  232. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  233. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  234. package/src/tui/session/session-api/routes.mjs +48 -3
  235. package/src/tui/session/session-flow/drain.mjs +12 -3
  236. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  237. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  238. package/src/tui/session/session-flow/steering.mjs +58 -24
  239. package/src/tui/session/tool-approval.mjs +26 -5
  240. package/src/tui/session/transcript-intake.mjs +6 -2
  241. package/src/tui/session/transcript-spill.mjs +5 -1
  242. package/src/tui/session/tui-steering-persist.mjs +65 -42
  243. package/src/tui/session/turn-live-call.mjs +56 -0
  244. package/src/tui/session/turn.mjs +22 -6
  245. package/src/session-runtime/automation-agents.mjs +0 -94
  246. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -0,0 +1,230 @@
1
+ // The built-in connector catalog: services a user connects with one click
2
+ // from Desktop (Extensions → 커넥터) or from a chat card the setup tool's
3
+ // `connect_connector` action surfaces. Each entry declares its backend
4
+ // explicitly:
5
+ // oauth — a remote MCP server that signs in with MCP OAuth;
6
+ // apiKey — an MCP server that takes one key (an HTTP header or a stdio
7
+ // environment variable);
8
+ // builtin — a Mixdog built-in feature (GitHub is the GitHub CLI of the
9
+ // Git & GitHub built-in, never a GitHub MCP);
10
+ // A connected catalog service is an ordinary user MCP server whose config
11
+ // carries `connector: <id>`. An existing custom server is never adopted into
12
+ // the catalog row.
13
+
14
+ import { connectorTrigger } from './connector-triggers.mjs';
15
+
16
+ // `oauthClient` names a pre-registered client the runtime supplies
17
+ // (mcp/oauth-client.mjs: 'google') in place of dynamic client registration;
18
+ // `scopes` are then the ones requested.
19
+ const oauth = (url, client = {}) => Object.freeze({ kind: 'oauth', url, ...client });
20
+
21
+ const GOOGLE_SCOPE = 'https://www.googleapis.com/auth';
22
+ // The Google access each built-in connector asks for: the one place to widen
23
+ // it (e.g. gmail.readonly). openid + email name the signed-in account.
24
+ export const GOOGLE_SCOPES = Object.freeze({
25
+ gmail: Object.freeze(['openid', 'email', `${GOOGLE_SCOPE}/gmail.send`]),
26
+ calendar: Object.freeze(['openid', 'email', `${GOOGLE_SCOPE}/calendar.events`]),
27
+ drive: Object.freeze(['openid', 'email', `${GOOGLE_SCOPE}/drive.file`]),
28
+ });
29
+ // Gmail / Calendar / Drive are Mixdog's built-in Google connectors
30
+ // (runtime/connectors/google-mcp.mjs), answered in-process at a reserved
31
+ // address and signed in with Mixdog's Google client. `askTools` ask before
32
+ // they run unless the agent set them.
33
+ const google = (service, scopes, askTools) =>
34
+ oauth(`https://${service}.mixdog.invalid/mcp`, { oauthClient: 'google', scopes, askTools });
35
+ // Gmail can also connect with a Google app password instead of the sign-in:
36
+ // IMAP and SMTP over TLS (runtime/connectors/gmail-app-password.mjs), answered
37
+ // in-process at its own reserved address. Its server is still the Gmail
38
+ // connector (`connector: 'gmail'`, so Gmail's askTools apply to its tools of
39
+ // the same names); the account rides in a header, the app password lives only
40
+ // in the keychain.
41
+ export const GMAIL_APP_PASSWORD = Object.freeze({
42
+ url: 'https://gmail-imap.mixdog.invalid/mcp',
43
+ accountHeader: 'X-Mixdog-Gmail-Account',
44
+ helpUrl: 'https://myaccount.google.com/apppasswords',
45
+ });
46
+ const apiKey = (spec) => Object.freeze({ kind: 'apiKey', ...spec });
47
+
48
+ export const CONNECTOR_CATALOG = Object.freeze(
49
+ [
50
+ { id: 'airtable', name: 'Airtable', backend: oauth('https://mcp.airtable.com/mcp') },
51
+ { id: 'confluence', name: 'Confluence', backend: oauth('https://mcp.atlassian.com/v1/mcp/authv2') },
52
+ { id: 'dropbox', name: 'Dropbox', backend: oauth('https://mcp.dropbox.com/mcp') },
53
+ { id: 'github', name: 'GitHub', backend: Object.freeze({ kind: 'builtin', feature: 'git' }) },
54
+ {
55
+ id: 'gmail',
56
+ name: 'Gmail',
57
+ backend: google('gmail', GOOGLE_SCOPES.gmail, ['send_email']),
58
+ },
59
+ // Calendar and Drive are offered only to a user who entered their own
60
+ // Google OAuth client (Connectors → add custom → 내 Google 키): `ownKey`.
61
+ // Existing connections keep working; Mixy and the chat cards never offer them.
62
+ {
63
+ id: 'google-calendar',
64
+ name: 'Google Calendar',
65
+ ownKey: true,
66
+ backend: google('calendar', GOOGLE_SCOPES.calendar, ['delete_event']),
67
+ },
68
+ {
69
+ id: 'google-drive',
70
+ name: 'Google Drive',
71
+ ownKey: true,
72
+ backend: google('drive', GOOGLE_SCOPES.drive, []),
73
+ },
74
+ {
75
+ id: 'hugging-face',
76
+ name: 'Hugging Face',
77
+ backend: apiKey({
78
+ url: 'https://huggingface.co/mcp',
79
+ header: 'Authorization',
80
+ prefix: 'Bearer ',
81
+ keyUrl: 'https://huggingface.co/settings/tokens',
82
+ }),
83
+ },
84
+ { id: 'jira', name: 'Jira', backend: oauth('https://mcp.atlassian.com/v1/mcp/authv2') },
85
+ { id: 'linear', name: 'Linear', backend: oauth('https://mcp.linear.app/mcp') },
86
+ { id: 'notion', name: 'Notion', backend: oauth('https://mcp.notion.com/mcp') },
87
+ {
88
+ id: 'postgres',
89
+ name: 'Postgres',
90
+ backend: apiKey({
91
+ command: 'npx',
92
+ args: ['-y', '@microsoft/postgres-mcp@0.2.0', 'run'],
93
+ env: 'POSTGRES_MCP_CONNECTION_STRING',
94
+ keyUrl: 'https://github.com/microsoft/postgres-mcp/blob/main/USAGE.md',
95
+ placeholder: 'postgresql://user:password@host:5432/db',
96
+ }),
97
+ },
98
+ { id: 'sentry', name: 'Sentry', backend: oauth('https://mcp.sentry.dev/mcp') },
99
+ {
100
+ id: 'stripe',
101
+ name: 'Stripe',
102
+ backend: apiKey({
103
+ url: 'https://mcp.stripe.com',
104
+ header: 'Authorization',
105
+ prefix: 'Bearer ',
106
+ keyUrl: 'https://dashboard.stripe.com/apikeys',
107
+ placeholder: 'rk_live_…',
108
+ }),
109
+ },
110
+ { id: 'supabase', name: 'Supabase', backend: oauth('https://mcp.supabase.com/mcp') },
111
+ { id: 'trello', name: 'Trello', backend: oauth('https://mcp.trello.com/v1') },
112
+ { id: 'vercel', name: 'Vercel', backend: oauth('https://mcp.vercel.com') },
113
+ // The ids are the deferred-tool manifest's curated keys: a server named by
114
+ // its id gets that one-line trigger (connector-triggers.mjs).
115
+ ].map((entry) => Object.freeze({ ...entry, trigger: connectorTrigger(entry.id) }))
116
+ );
117
+
118
+ /** The ids offered to everyone (the setup tool's `connect_connector`): the
119
+ * catalog minus the own-key-only Google services. */
120
+ export const CONNECTOR_IDS = Object.freeze(CONNECTOR_CATALOG.filter((entry) => !entry.ownKey).map((entry) => entry.id));
121
+
122
+ /** The catalog entry for `id`, or null. */
123
+ export function connectorById(id) {
124
+ return CONNECTOR_CATALOG.find((entry) => entry.id === id) || null;
125
+ }
126
+
127
+ const urlKey = (url) =>
128
+ String(url || '')
129
+ .trim()
130
+ .replace(/\/+$/, '')
131
+ .toLowerCase();
132
+
133
+ /** The catalog id a configured MCP server connects: its `connector` marker,
134
+ * '' for none (or a marker the catalog no longer knows). */
135
+ export function connectorOfServer(cfg) {
136
+ const marker = typeof cfg?.connector === 'string' ? cfg.connector : '';
137
+ return connectorById(marker) ? marker : '';
138
+ }
139
+
140
+ /** The permission a catalog connector's tool has when nothing set its own:
141
+ * `needs_approval` for the high-risk tools it names, else `auto`. */
142
+ export function connectorToolDefault(connectorId, tool) {
143
+ const backend = connectorById(connectorId)?.backend;
144
+ return backend?.askTools?.includes(tool) ? 'needs_approval' : 'auto';
145
+ }
146
+
147
+ const EMAIL = /^[^\s@<>(),;:"]+@[^\s@<>(),;:"]+\.[^\s@<>(),;:".]+$/;
148
+
149
+ /** A Gmail address as typed (trimmed, lowercased), or '' when it is none. */
150
+ export function gmailAddress(value) {
151
+ const address = String(value ?? '')
152
+ .trim()
153
+ .toLowerCase();
154
+ return EMAIL.test(address) ? address : '';
155
+ }
156
+
157
+ /** A Google app password as typed, its spaces removed; '' unless it then
158
+ * has the 16 characters Google issues. */
159
+ export function gmailAppPasswordValue(value) {
160
+ const password = String(value ?? '').replace(/\s+/g, '');
161
+ return password.length === 16 ? password : '';
162
+ }
163
+
164
+ /** The account of a Gmail app-password server config, '' for any other. */
165
+ export function gmailAppPasswordAccount(cfg) {
166
+ if (urlKey(cfg?.url) !== urlKey(GMAIL_APP_PASSWORD.url)) return '';
167
+ const headers = cfg?.headers && typeof cfg.headers === 'object' ? cfg.headers : {};
168
+ const header = GMAIL_APP_PASSWORD.accountHeader.toLowerCase();
169
+ const key = Object.keys(headers).find((name) => name.toLowerCase() === header);
170
+ return key ? gmailAddress(headers[key]) : '';
171
+ }
172
+
173
+ /** The MCP server input that connects Gmail under `name` as `email` with an
174
+ * app password. `appPassword` only travels to the host, which checks it
175
+ * with Gmail and keeps it in the keychain; it never reaches the config. */
176
+ export function gmailAppPasswordServerInput(name, email, password) {
177
+ const address = gmailAddress(email);
178
+ if (!address) throw new Error('GMAIL_APP_PASSWORD_ADDRESS: Enter a Gmail address.');
179
+ const appPassword = gmailAppPasswordValue(password);
180
+ if (!appPassword) throw new Error('GMAIL_APP_PASSWORD_FORMAT: An app password has 16 characters.');
181
+ return {
182
+ name,
183
+ type: 'http',
184
+ url: GMAIL_APP_PASSWORD.url,
185
+ headers: { [GMAIL_APP_PASSWORD.accountHeader]: address },
186
+ connector: 'gmail',
187
+ appPassword,
188
+ };
189
+ }
190
+
191
+ /** Whether several accounts of catalog connector `entry` can be connected at
192
+ * once: Gmail (also by app password), Calendar and Drive with the Google
193
+ * sign-in. Other remote servers keep one (their tokens and URL are shared). */
194
+ export function connectorMultiAccount(entry) {
195
+ return entry?.backend?.kind === 'oauth' && entry.backend.oauthClient === 'google';
196
+ }
197
+
198
+ /** The MCP server input that connects catalog entry `entry` under `name`
199
+ * (oauth / apiKey); `key` fills an apiKey backend; `extraAccount`
200
+ * makes it one more account of a multi-account connector. */
201
+ export function connectorServerInput(entry, name, key = '', { extraAccount = false } = {}) {
202
+ const { backend } = entry;
203
+ if (backend.kind === 'oauth') {
204
+ // Tokens are stored per exact server URL, so another account of the same
205
+ // connector needs its own URL; the in-process Google servers ignore the query.
206
+ const url =
207
+ extraAccount && connectorMultiAccount(entry) ? `${backend.url}?account=${encodeURIComponent(name)}` : backend.url;
208
+ return { name, type: 'http', url, connector: entry.id };
209
+ }
210
+ if (backend.kind !== 'apiKey') throw new Error(`${entry.name} is not an MCP connector`);
211
+ const value = String(key || '').trim();
212
+ if (!value) throw new Error(`${entry.name} needs a key`);
213
+ if (backend.url) {
214
+ return {
215
+ name,
216
+ type: 'http',
217
+ url: backend.url,
218
+ headers: { [backend.header]: `${backend.prefix || ''}${value}` },
219
+ connector: entry.id,
220
+ };
221
+ }
222
+ return {
223
+ name,
224
+ type: 'stdio',
225
+ command: backend.command,
226
+ args: [...backend.args],
227
+ env: { [backend.env]: value },
228
+ connector: entry.id,
229
+ };
230
+ }
@@ -0,0 +1,3 @@
1
+ export declare const CONNECTOR_TRIGGER_MAX_CHARS: number;
2
+ export declare const CONNECTOR_TRIGGERS: Readonly<Record<string, string>>;
3
+ export declare function connectorTrigger(serverId: string): string;
@@ -0,0 +1,43 @@
1
+ // Curated selection triggers for catalog connectors (MCP servers). A catalog
2
+ // entry carries its line as `trigger`; the deferred-tool manifest lists each
3
+ // connected server once as `- mcp__<server id>: <trigger>`, so one line must
4
+ // say what the connector is for (plus a close negative where useful), in
5
+ // English, at most CONNECTOR_TRIGGER_MAX_CHARS characters.
6
+ export const CONNECTOR_TRIGGER_MAX_CHARS = 100;
7
+
8
+ export const CONNECTOR_TRIGGERS = Object.freeze({
9
+ github: 'GitHub repos, issues, pull requests, Actions and code search; not local Git history or diffs.',
10
+ notion: 'Notion pages, databases and workspace search; create or update team docs and notes.',
11
+ linear: 'Linear issues, projects, cycles and comments; create, triage or update tickets.',
12
+ sentry: 'Sentry issues, events, stack traces and releases; triage production errors.',
13
+ gmail: 'Gmail: send email; with an app password also search, read, archive, label, star, trash.',
14
+ 'google-calendar': 'Google Calendar events on the primary calendar; list, search, create, update or delete.',
15
+ 'google-drive': 'Google Drive files Mixdog created or was given; search, read or create; not local files.',
16
+ jira: 'Jira issues, projects, sprints and comments; JQL search, create or transition tickets.',
17
+ confluence: 'Confluence spaces and pages; search, read or edit the team wiki; not Jira tickets.',
18
+ dropbox: 'Dropbox files and folders; search, read, upload or share cloud files; not local files.',
19
+ trello: 'Trello boards, lists and cards; create, move or update cards.',
20
+ airtable: 'Airtable bases, tables and records; query, create or update rows.',
21
+ stripe: 'Stripe customers, payments, invoices, subscriptions and refunds; billing data and actions.',
22
+ vercel: 'Vercel projects, deployments, domains and logs; deploy status and build errors.',
23
+ supabase: 'Supabase projects: Postgres tables and SQL, auth, storage and edge functions.',
24
+ postgres: 'PostgreSQL schemas, tables and SQL queries on a connected database; not local files.',
25
+ 'hugging-face': 'Hugging Face Hub models, datasets, Spaces and papers; not local GGUF model setup.',
26
+ });
27
+
28
+ // Server ids differ only in separators and case (google_calendar, huggingface).
29
+ const triggerKey = (id) =>
30
+ String(id || '')
31
+ .toLowerCase()
32
+ .replace(/[^a-z0-9]/g, '');
33
+ const TRIGGERS_BY_KEY = new Map(Object.entries(CONNECTOR_TRIGGERS).map(([id, line]) => [triggerKey(id), line]));
34
+
35
+ /** The curated trigger of the catalog connector `serverId`, or ''. */
36
+ export function connectorTrigger(serverId) {
37
+ // Another account of a connector is named `<id>-2`, `<id>-3`…
38
+ return (
39
+ TRIGGERS_BY_KEY.get(triggerKey(serverId)) ||
40
+ TRIGGERS_BY_KEY.get(triggerKey(String(serverId || '').replace(/[-_]\d+$/, ''))) ||
41
+ ''
42
+ );
43
+ }
@@ -0,0 +1,24 @@
1
+ // Mixdog's own Google OAuth client (a "Desktop app" client, marked "Use this
2
+ // client for an AI-powered agent"), used by the Gmail, Google Calendar and
3
+ // Google Drive catalog connectors instead of dynamic client registration,
4
+ // which Google does not offer. Google treats desktop clients as public: the
5
+ // id and its non-confidential secret ship with the app, and every sign-in
6
+ // uses PKCE (S256), state and a loopback redirect
7
+ // (developers.google.com/identity/protocols/oauth2/native-app).
8
+ //
9
+ // The values come only from the environment; the source ships none, so a
10
+ // build without them offers no built-in Google sign-in (the user's own
11
+ // Google client still works).
12
+
13
+ const value = (name) => String(process.env[name] || '').trim();
14
+
15
+ /** `{ clientId, clientSecret }` of Mixdog's Google client, or null when this
16
+ * build has none. */
17
+ export function googleOAuthClient() {
18
+ const clientId = value('MIXDOG_GOOGLE_OAUTH_CLIENT_ID');
19
+ if (!clientId) return null;
20
+ return { clientId, clientSecret: value('MIXDOG_GOOGLE_OAUTH_CLIENT_SECRET') };
21
+ }
22
+
23
+ /** Google issues a refresh token only for `access_type=offline`. */
24
+ export const GOOGLE_AUTH_PARAMS = Object.freeze({ access_type: 'offline' });
@@ -0,0 +1,12 @@
1
+ export interface HeldRequestPushEvent {
2
+ sessionId: string;
3
+ agentId: string;
4
+ agentName: string;
5
+ requestId: string;
6
+ }
7
+ export function registerHeldRequestResolver(resolver: (id: string, decision: unknown) => boolean): () => void;
8
+ export function resolveHeldRequest(id: string, decision: unknown): boolean;
9
+ export function registerHeldRequestNotifier(notifier: (event: HeldRequestPushEvent) => void): () => void;
10
+ export function notifyHeldRequest(event: Partial<HeldRequestPushEvent>): void;
11
+ export function recordRunOwner(sessionId: string, agentId: string): void;
12
+ export function runOwnerOf(sessionId: string): string;
@@ -0,0 +1,71 @@
1
+ // Runtime ↔ desktop-service channel for held requests of unattended runs
2
+ // (session-runtime/held-requests.mjs). The daemon's held-request registry
3
+ // registers the resolver; the desktop service hosted in this daemon registers
4
+ // a notifier that sends the sanitized phone push. The session API answers a
5
+ // held request through resolveHeldRequest, whatever session it was sent from.
6
+ //
7
+ // Deliberately dependency-free: the desktop service bundle imports this file
8
+ // too, and both copies share one registry through a global symbol.
9
+
10
+ const KEY = Symbol.for('mixdog.heldRequestHost.v1');
11
+
12
+ function registry() {
13
+ globalThis[KEY] ??= { resolver: null, notifiers: [], runOwners: new Map() };
14
+ return globalThis[KEY];
15
+ }
16
+
17
+ const RUN_OWNER_LIMIT = 500;
18
+ /** Remember which agent an unattended run session belongs to, so its pushes
19
+ * follow that agent's notification mute after the run ends (bounded, newest kept). */
20
+ export function recordRunOwner(sessionId, agentId) {
21
+ const owners = registry().runOwners;
22
+ owners.delete(sessionId);
23
+ owners.set(String(sessionId), String(agentId));
24
+ while (owners.size > RUN_OWNER_LIMIT) owners.delete(owners.keys().next().value);
25
+ }
26
+
27
+ export function runOwnerOf(sessionId) {
28
+ return registry().runOwners.get(String(sessionId || '')) || '';
29
+ }
30
+
31
+ /** `resolver(id, decision) → boolean` (true when `id` was a pending held request). */
32
+ export function registerHeldRequestResolver(resolver) {
33
+ registry().resolver = resolver;
34
+ return () => {
35
+ if (registry().resolver === resolver) registry().resolver = null;
36
+ };
37
+ }
38
+
39
+ export function resolveHeldRequest(id, decision) {
40
+ const { resolver } = registry();
41
+ return typeof resolver === 'function' && String(id || '').startsWith('held_')
42
+ ? resolver(id, decision) === true
43
+ : false;
44
+ }
45
+
46
+ /** `notifier({ sessionId, agentId, agentName, requestId })`: one per desktop service. */
47
+ export function registerHeldRequestNotifier(notifier) {
48
+ const entry = { notifier };
49
+ registry().notifiers.push(entry);
50
+ return () => {
51
+ const list = registry().notifiers;
52
+ const index = list.indexOf(entry);
53
+ if (index >= 0) list.splice(index, 1);
54
+ };
55
+ }
56
+
57
+ /** Hand one held request to every registered notifier. Carries ids and the
58
+ * agent's name only: never the tool, its arguments or any text. */
59
+ export function notifyHeldRequest({ sessionId, agentId, agentName, requestId }) {
60
+ const event = {
61
+ sessionId: String(sessionId || ''),
62
+ agentId: String(agentId || ''),
63
+ agentName: String(agentName || ''),
64
+ requestId: String(requestId || ''),
65
+ };
66
+ for (const { notifier } of registry().notifiers) {
67
+ try {
68
+ notifier(event);
69
+ } catch {}
70
+ }
71
+ }
@@ -0,0 +1 @@
1
+ export function plainPreviewText(text: unknown): string;
@@ -0,0 +1,28 @@
1
+ // A messenger preview line reads as plain text, as KakaoTalk shows it: the
2
+ // markdown a reply was written in (bold, headings, list markers, quotes,
3
+ // code fences and spans, links, images) is dropped, its words kept.
4
+
5
+ /** `text` without markdown syntax, whitespace folded to single spaces. */
6
+ export function plainPreviewText(text) {
7
+ // HTML comments never show in markdown (a greeting's suggestions ride one).
8
+ return String(text ?? '')
9
+ .replace(/<!--[\s\S]*?(?:-->|$)/g, ' ')
10
+ .replace(/^[ \t]*(`{3,}|~{3,})[^\n]*$/gm, ' ')
11
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
12
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
13
+ .replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, '')
14
+ .replace(/^[ \t]*>[ \t]?/gm, '')
15
+ .replace(/^[ \t]*(?:[-*+]|\d+[.)])[ \t]+(?:\[[ xX]\][ \t]+)?/gm, '')
16
+ .replace(/^[ \t]*(?:[-*_][ \t]*){3,}$/gm, ' ')
17
+ .replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '$2')
18
+ .replace(/(^|[^\w*])\*(?=\S)([^*\n]*?\S)\*(?!\w)/g, '$1$2')
19
+ .replace(/(^|[^\w_])_(?=\S)([^_\n]*?\S)_(?!\w)/g, '$1$2')
20
+ .replace(/~~(?=\S)([\s\S]*?\S)~~/g, '$1')
21
+ .replace(/`+([^`\n]*)`+/g, '$1')
22
+ .replace(/\s+/g, ' ')
23
+ .trim()
24
+ .slice(0, PREVIEW_CAP);
25
+ }
26
+
27
+ /** A safety cap only: the row clamps the shown text to two lines. */
28
+ const PREVIEW_CAP = 200;
@@ -0,0 +1,11 @@
1
+ export declare const ROUTINE_LIMITS: Readonly<{
2
+ backoffBaseMs: number;
3
+ pauseAfterFailures: number;
4
+ homeQueueLimit: number;
5
+ }>;
6
+
7
+ export declare function routineBackoffRemaining(
8
+ state: { consecutiveFailures?: unknown; lastFailedAt?: unknown } | null | undefined,
9
+ now?: number,
10
+ limits?: typeof ROUTINE_LIMITS
11
+ ): number;
@@ -0,0 +1,21 @@
1
+ // Safeguards of owned routines (schedules and webhook endpoints, see
2
+ // session-runtime/owned-automation-run.mjs): after a failure the next
3
+ // unattended fire waits `backoffBaseMs * 2^(n-1)` (n consecutive failures)
4
+ // and the routine switches itself off at `pauseAfterFailures`; a home thread
5
+ // holds at most `homeQueueLimit` routine turns waiting (and as many queued
6
+ // rows posted from outside its turns). Runs themselves are
7
+ // unlimited: no concurrency cap and no deadline.
8
+ export const ROUTINE_LIMITS = Object.freeze({
9
+ backoffBaseMs: 60_000,
10
+ pauseAfterFailures: 5,
11
+ homeQueueLimit: 200,
12
+ });
13
+
14
+ /** How long (ms) an unattended fire still waits after the routine's
15
+ * `consecutiveFailures`, the latest at `lastFailedAt`; 0 when it may run. */
16
+ export function routineBackoffRemaining(state, now = Date.now(), limits = ROUTINE_LIMITS) {
17
+ const failures = Number(state?.consecutiveFailures) || 0;
18
+ const at = state?.lastFailedAt ? new Date(state.lastFailedAt).getTime() : Number.NaN;
19
+ if (failures < 1 || !Number.isFinite(at)) return 0;
20
+ return Math.max(0, at + limits.backoffBaseMs * 2 ** (failures - 1) - now);
21
+ }
@@ -0,0 +1,10 @@
1
+ export interface RoutineTrigger {
2
+ kind: 'schedule' | 'webhook';
3
+ name: string;
4
+ event?: string;
5
+ }
6
+
7
+ export declare function routineTriggerHeader(input: { kind: string; name: string; project?: string }): string;
8
+ export declare function stripRoutineTriggerHeader(text: unknown): string;
9
+ export declare function normalizeRoutineTrigger(value: unknown): RoutineTrigger | null;
10
+ export declare function routineTriggerField(value: unknown): { routineTrigger?: RoutineTrigger };
@@ -0,0 +1,44 @@
1
+ // A routine fire delivered into its bot's home thread as a turn
2
+ // (session-runtime/routine-home-turn.mjs): the model-visible header line that
3
+ // opens the turn's prompt, and the `routineTrigger` marker its user row
4
+ // carries (live, persisted in meta.transcript, restored) so a chat paints it
5
+ // as a routine marker instead of a user bubble.
6
+
7
+ const HEADER_PREFIX = '[Routine trigger]';
8
+ const KINDS = new Set(['schedule', 'webhook']);
9
+
10
+ /** The first line of a routine turn's prompt: which routine fired and how to
11
+ * treat the turn. Only the turn's content carries it, never the system
12
+ * prompt, so the thread's prompt cache is untouched. */
13
+ export function routineTriggerHeader({ kind, name, project = '' }) {
14
+ const what = kind === 'webhook' ? 'webhook' : 'schedule';
15
+ return [
16
+ `${HEADER_PREFIX} Your ${what} "${String(name)}" fired.`,
17
+ 'Carry out its instructions below now, yourself or by delegating; nobody is watching this turn, so approvals wait for the user.',
18
+ 'Reply in this chat with the result.',
19
+ ...(project ? [`Project folder: ${project}`] : []),
20
+ ].join(' ');
21
+ }
22
+
23
+ /** `text` without the header line a routine turn opens with. */
24
+ export function stripRoutineTriggerHeader(text) {
25
+ const value = String(text ?? '');
26
+ if (!value.startsWith(HEADER_PREFIX)) return value;
27
+ const end = value.indexOf('\n');
28
+ return end < 0 ? '' : value.slice(end + 1).replace(/^\n+/, '');
29
+ }
30
+
31
+ /** A well-formed `{ kind, name, event? }` marker, or null. */
32
+ export function normalizeRoutineTrigger(value) {
33
+ if (!value || typeof value !== 'object') return null;
34
+ const name = typeof value.name === 'string' ? value.name.trim().slice(0, 200) : '';
35
+ if (!name || !KINDS.has(value.kind)) return null;
36
+ const event = typeof value.event === 'string' ? value.event.trim().slice(0, 200) : '';
37
+ return { kind: value.kind, name, ...(event ? { event } : {}) };
38
+ }
39
+
40
+ /** The `routineTrigger` field of a transcript row (absent when none). */
41
+ export function routineTriggerField(value) {
42
+ const trigger = normalizeRoutineTrigger(value?.routineTrigger);
43
+ return trigger ? { routineTrigger: trigger } : {};
44
+ }
@@ -1,13 +1,12 @@
1
- // schedule.model wire format shared by the channels-worker scheduler and the
2
- // engine-side run-now dispatch: either a config.presets id/name (legacy) or a
3
- // direct "provider/model[@effort][+fast][+noauto][?parameter=value]" route string
4
- // written by the desktop schedule editor (`+noauto`: Auto effort off for the
5
- // route; absent means on). Slash-form values become direct route
6
- // objects, which agent-dispatch consumes without a presets lookup.
7
- // The optional route fields an automation run carries, from a preset, a
8
- // maintenance slot or a parsed route ref. One shape so schedule and webhook
9
- // runs cannot drift when a field is added. modelParameters is copied so a
10
- // stored preset can never be mutated through the run.
1
+ // Model route-ref wire format (task sessions, provider model caches): either a
2
+ // config.presets id/name (legacy) or a direct
3
+ // "provider/model[@effort][+fast][+noauto][?parameter=value]" route string
4
+ // (`+noauto`: Auto effort off for the route; absent means on). Slash-form
5
+ // values become direct route objects, which agent-dispatch consumes without a
6
+ // presets lookup.
7
+ // The optional route fields a run carries, from a preset, a maintenance slot
8
+ // or a parsed route ref. modelParameters is copied so a stored preset can
9
+ // never be mutated through the run.
11
10
  export function modelRouteFields(slot = {}) {
12
11
  return {
13
12
  provider: slot.provider,
@@ -3,8 +3,8 @@
3
3
  // monotonic clock the owner heartbeat uses (performance.now), completed spans
4
4
  // over `minMs` land in a bounded ring, and spans over `slowMs` are logged as
5
5
  // `[slow-op] <label> <ms>`. When the event loop stalls, `describe(from, to)`
6
- // names the spans that overlapped the stall window. Spans carry labels, phase
7
- // durations and sizes only — never payloads.
6
+ // names the spans that overlapped the stall window. Spans carry labels and
7
+ // phase durations only — never payloads.
8
8
 
9
9
  import { AsyncLocalStorage } from 'node:async_hooks';
10
10
  import { performance } from 'node:perf_hooks';
@@ -36,18 +36,11 @@ export function markPhase(name) {
36
36
  span.mark = t;
37
37
  }
38
38
 
39
- /** Records a size (count/bytes) on the caller's span. */
40
- export function noteSpanSize(size) {
41
- const span = current.getStore();
42
- if (span && Number.isFinite(size)) span.size = size;
43
- }
44
-
45
39
  const round = (ms) => Math.round(ms);
46
40
 
47
41
  function formatSpan(span) {
48
42
  const phases = span.phases.map(([name, ms]) => `${name}=${round(ms)}`).join(' ');
49
- const size = span.size === undefined ? '' : ` size=${span.size}`;
50
- return `${span.label} ${round(span.ms)}ms${phases ? ` ${phases}` : ''}${size}`;
43
+ return `${span.label} ${round(span.ms)}ms${phases ? ` ${phases}` : ''}`;
51
44
  }
52
45
 
53
46
  export function createSlowSpans({
@@ -70,7 +63,6 @@ export function createSlowSpans({
70
63
  end: span.end,
71
64
  ms: span.ms,
72
65
  phases: span.phases,
73
- size: span.size,
74
66
  });
75
67
  if (ring.length > capacity) ring.shift();
76
68
  }
@@ -90,7 +82,7 @@ export function createSlowSpans({
90
82
  /** Runs `fn` inside a span; markPhase/annotateSpan inside it attach to this span. */
91
83
  async trace(label, fn) {
92
84
  const start = now();
93
- const span = { label: String(label), start, mark: start, now, phases: [], size: undefined };
85
+ const span = { label: String(label), start, mark: start, now, phases: [] };
94
86
  try {
95
87
  return await current.run(span, fn);
96
88
  } finally {
@@ -200,7 +200,8 @@ async function refreshShellJobsStatus(ownerPid) {
200
200
  // its owner retires the record. Its root pid may be gone while a
201
201
  // descendant lives, so the owner's liveness decides, not the root's.
202
202
  const unconfirmed = detail?.terminationUnconfirmed === true;
203
- if (unconfirmed ? !isPidAlive(ownerByJob.get(id) || 0) : !(await isShellJobAlive(detail, p, dir, id))) continue;
203
+ if (unconfirmed ? !isHeldShellJobAlive(detail, ownerByJob.get(id)) : !(await isShellJobAlive(detail, p, dir, id)))
204
+ continue;
204
205
  // A foreground command is awaited in its own tool card; listing it here
205
206
  // made every short command flash in and out of the shell list. Only
206
207
  // backgrounded jobs (including promoted ones) are background work.
@@ -326,6 +327,17 @@ function summarizeShellJobs({ count, oldestMs, jobs, bySession }) {
326
327
  return { count, elapsedLabel, jobs, sessions };
327
328
  }
328
329
 
330
+ // A held record lives while its owner process does. Once its owner session
331
+ // has closed, it also needs a process left to hold: the root or a survivor
332
+ // its termination watch last reported. Without one it is an orphan whose
333
+ // processes are gone, and it resolves instead of staying unconfirmed.
334
+ function isHeldShellJobAlive(detail, ownerPid) {
335
+ if (!isPidAlive(ownerPid || 0)) return false;
336
+ if (detail?.ownerSessionClosed !== true) return true;
337
+ const survivors = Array.isArray(detail?.survivorPids) ? detail.survivorPids : [];
338
+ return [detail?.pid, ...survivors].some((pid) => isPidAlive(positiveInt(pid)));
339
+ }
340
+
329
341
  async function isShellJobAlive(detail, detailPath, dir, id) {
330
342
  const pid = positiveInt(detail?.pid);
331
343
  if (!pid) return false;
@@ -2,6 +2,7 @@
2
2
  // These pure helpers keep live rows, persisted messages and restored rows on
3
3
  // the same field contract without rewriting older display-only transcripts.
4
4
  import { displayModelName } from './model-display.mjs';
5
+ import { routineTriggerField } from './routine-trigger.mjs';
5
6
 
6
7
  export function transcriptRouteMetadataFields(value) {
7
8
  if (!value || typeof value !== 'object') return {};
@@ -76,10 +77,19 @@ export function persistedUserTranscriptMetadata(value) {
76
77
  // The turn's change-record key (the outer prompt id), so a restored user
77
78
  // row still addresses its turn's Undo/Reapply.
78
79
  ...(typeof value.checkpointId === 'string' && value.checkpointId ? { checkpointId: value.checkpointId } : {}),
80
+ // Every submission a merged queued batch delivered as this turn.
81
+ ...submissionIdsField(value.submissionIds),
79
82
  ...attachmentsField(value.attachments),
83
+ // A routine fire delivered as this turn (routine-trigger.mjs).
84
+ ...routineTriggerField(value),
80
85
  };
81
86
  }
82
87
 
88
+ function submissionIdsField(value) {
89
+ const ids = Array.isArray(value) ? value.filter((id) => typeof id === 'string' && id) : [];
90
+ return ids.length ? { submissionIds: ids } : {};
91
+ }
92
+
83
93
  function attachmentsField(value) {
84
94
  const attachments = normalizeMessageAttachments(value);
85
95
  return attachments.length ? { attachments } : {};