@vellumai/assistant 0.3.4 → 0.3.6

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 (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -0,0 +1,126 @@
1
+ import { beforeEach, describe, expect, mock, test } from 'bun:test';
2
+ import type { SmsSendResult } from '../messaging/providers/sms/client.js';
3
+
4
+ const sendSmsMock = mock(async (..._args: unknown[]): Promise<SmsSendResult> => ({ messageSid: 'SM-mock-sid', status: 'queued' }));
5
+ const getOrCreateConversationMock = mock((_key: string) => ({ conversationId: 'conv-1' }));
6
+ const upsertOutboundBindingMock = mock((_input: Record<string, unknown>) => {});
7
+
8
+ let secureKeys: Record<string, string | undefined> = {
9
+ 'credential:twilio:account_sid': 'AC1234567890',
10
+ 'credential:twilio:auth_token': 'auth-token',
11
+ };
12
+
13
+ let configState: {
14
+ sms?: {
15
+ phoneNumber?: string;
16
+ assistantPhoneNumbers?: Record<string, string>;
17
+ };
18
+ } = {
19
+ sms: {},
20
+ };
21
+
22
+ mock.module('../security/secure-keys.js', () => ({
23
+ getSecureKey: (key: string) => secureKeys[key],
24
+ }));
25
+
26
+ mock.module('../util/platform.js', () => ({
27
+ readHttpToken: () => 'runtime-token',
28
+ }));
29
+
30
+ mock.module('../config/loader.js', () => ({
31
+ loadConfig: () => configState,
32
+ }));
33
+
34
+ mock.module('../memory/conversation-key-store.js', () => ({
35
+ getOrCreateConversation: (key: string) => getOrCreateConversationMock(key),
36
+ }));
37
+
38
+ mock.module('../memory/external-conversation-store.js', () => ({
39
+ upsertOutboundBinding: (input: Record<string, unknown>) => upsertOutboundBindingMock(input),
40
+ }));
41
+
42
+ mock.module('../messaging/providers/sms/client.js', () => ({
43
+ sendMessage: (
44
+ gatewayUrl: string,
45
+ bearerToken: string,
46
+ to: string,
47
+ text: string,
48
+ assistantId?: string,
49
+ ) => sendSmsMock(gatewayUrl, bearerToken, to, text, assistantId),
50
+ }));
51
+
52
+ import { smsMessagingProvider } from '../messaging/providers/sms/adapter.js';
53
+
54
+ describe('smsMessagingProvider', () => {
55
+ beforeEach(() => {
56
+ sendSmsMock.mockClear();
57
+ getOrCreateConversationMock.mockClear();
58
+ upsertOutboundBindingMock.mockClear();
59
+ secureKeys = {
60
+ 'credential:twilio:account_sid': 'AC1234567890',
61
+ 'credential:twilio:auth_token': 'auth-token',
62
+ };
63
+ configState = { sms: {} };
64
+ delete process.env.TWILIO_PHONE_NUMBER;
65
+ delete process.env.GATEWAY_INTERNAL_BASE_URL;
66
+ delete process.env.GATEWAY_PORT;
67
+ });
68
+
69
+ test('isConnected is true when assistant-scoped numbers exist', () => {
70
+ configState = {
71
+ sms: {
72
+ assistantPhoneNumbers: { 'ast-alpha': '+15550001111' },
73
+ },
74
+ };
75
+
76
+ expect(smsMessagingProvider.isConnected?.()).toBe(true);
77
+ });
78
+
79
+ test('sendMessage forwards explicit assistant scope and avoids outbound binding writes for non-self', async () => {
80
+ await smsMessagingProvider.sendMessage('', '+15550002222', 'hi', {
81
+ assistantId: 'ast-alpha',
82
+ });
83
+
84
+ expect(sendSmsMock).toHaveBeenCalledWith(
85
+ 'http://127.0.0.1:7830',
86
+ 'runtime-token',
87
+ '+15550002222',
88
+ 'hi',
89
+ 'ast-alpha',
90
+ );
91
+ expect(getOrCreateConversationMock).toHaveBeenCalledWith('asst:ast-alpha:sms:+15550002222');
92
+ expect(upsertOutboundBindingMock).not.toHaveBeenCalled();
93
+ });
94
+
95
+ test('sendMessage uses messageSid from gateway response as result ID', async () => {
96
+ sendSmsMock.mockImplementation(async () => ({ messageSid: 'SM-test-12345', status: 'queued' }));
97
+ const result = await smsMessagingProvider.sendMessage('', '+15550009999', 'sid test', {
98
+ assistantId: 'self',
99
+ });
100
+ expect(result.id).toBe('SM-test-12345');
101
+ });
102
+
103
+ test('sendMessage falls back to timestamp-based ID when messageSid is absent', async () => {
104
+ sendSmsMock.mockImplementation(async () => ({}));
105
+ const before = Date.now();
106
+ const result = await smsMessagingProvider.sendMessage('', '+15550009999', 'no sid', {
107
+ assistantId: 'self',
108
+ });
109
+ expect(result.id).toMatch(/^sms-\d+$/);
110
+ const ts = parseInt(result.id.replace('sms-', ''), 10);
111
+ expect(ts).toBeGreaterThanOrEqual(before);
112
+ });
113
+
114
+ test('sendMessage uses canonical self key and writes outbound binding for self scope', async () => {
115
+ await smsMessagingProvider.sendMessage('', '+15550003333', 'hello', {
116
+ assistantId: 'self',
117
+ });
118
+
119
+ expect(getOrCreateConversationMock).toHaveBeenCalledWith('sms:+15550003333');
120
+ expect(upsertOutboundBindingMock).toHaveBeenCalledWith({
121
+ conversationId: 'conv-1',
122
+ sourceChannel: 'sms',
123
+ externalChatId: '+15550003333',
124
+ });
125
+ });
126
+ });
@@ -30,6 +30,7 @@ mock.module('../home-base/prebuilt/seed.js', () => ({
30
30
  import {
31
31
  handleSurfaceAction,
32
32
  surfaceProxyResolver,
33
+ createSurfaceMutex,
33
34
  type SurfaceSessionContext,
34
35
  } from '../daemon/session-surfaces.js';
35
36
 
@@ -49,6 +50,7 @@ function makeContext(): SurfaceSessionContext {
49
50
  enqueueMessage: () => ({ queued: false, requestId: 'req-1' }),
50
51
  getQueueDepth: () => 0,
51
52
  processMessage: async () => 'ok',
53
+ withSurface: createSurfaceMutex(),
52
54
  };
53
55
  }
54
56
 
@@ -0,0 +1,535 @@
1
+ /**
2
+ * Integration tests for swarm DAG scheduling under pathological graph shapes.
3
+ *
4
+ * These tests exercise the orchestrator and plan validator together to verify
5
+ * correct behaviour when the dependency graph is adversarial:
6
+ * 1. Deep dependency chains (20+ sequential tasks)
7
+ * 2. Near-circular dependencies (partial cycles that stop just short of
8
+ * forming a full cycle, plus detection of actual cycles)
9
+ * 3. Very wide fan-outs (one root task with 50+ immediate dependents)
10
+ */
11
+
12
+ import { describe, test, expect } from 'bun:test';
13
+ import { executeSwarm } from '../swarm/orchestrator.js';
14
+ import type { OrchestratorEvent } from '../swarm/orchestrator.js';
15
+ import {
16
+ validateAndNormalizePlan,
17
+ SwarmPlanValidationError,
18
+ } from '../swarm/plan-validator.js';
19
+ import type { SwarmPlan, SwarmTaskNode } from '../swarm/types.js';
20
+ import type { SwarmWorkerBackend } from '../swarm/worker-backend.js';
21
+ import { resolveSwarmLimits } from '../swarm/limits.js';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Shared helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const SUCCESS_OUTPUT =
28
+ '```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```';
29
+
30
+ function makeBackend(overrides?: Partial<SwarmWorkerBackend>): SwarmWorkerBackend {
31
+ return {
32
+ name: 'test-backend',
33
+ isAvailable: () => true,
34
+ runTask: async () => ({
35
+ success: true,
36
+ output: SUCCESS_OUTPUT,
37
+ durationMs: 5,
38
+ }),
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ /** Limits generous enough for large pathological plans. */
44
+ const LARGE_LIMITS = {
45
+ maxWorkers: 6,
46
+ // Bypass the maxTasks hard-ceiling inside resolveSwarmLimits by using a
47
+ // value at the ceiling (20). For the deep-chain test we build exactly 20
48
+ // tasks, and for the fan-out we pass the oversized plan directly to
49
+ // executeSwarm (skipping validation) to test the orchestrator in isolation.
50
+ maxTasks: 20,
51
+ maxRetriesPerTask: 0,
52
+ workerTimeoutSec: 30,
53
+ };
54
+
55
+ const RESOLVED_LIMITS = resolveSwarmLimits(LARGE_LIMITS);
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Helpers for building specific graph shapes
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /** Build a linear chain: t0 -> t1 -> t2 -> ... -> t(n-1) */
62
+ function buildChain(n: number): SwarmTaskNode[] {
63
+ return Array.from({ length: n }, (_, i) => ({
64
+ id: `t${i}`,
65
+ role: 'coder' as const,
66
+ objective: `Task ${i}`,
67
+ dependencies: i === 0 ? [] : [`t${i - 1}`],
68
+ }));
69
+ }
70
+
71
+ /** Build a star: one root + n leaf tasks that all depend on the root. */
72
+ function buildStar(leafCount: number): SwarmTaskNode[] {
73
+ const root: SwarmTaskNode = {
74
+ id: 'root',
75
+ role: 'coder',
76
+ objective: 'Root task',
77
+ dependencies: [],
78
+ };
79
+ const leaves: SwarmTaskNode[] = Array.from({ length: leafCount }, (_, i) => ({
80
+ id: `leaf${i}`,
81
+ role: 'coder' as const,
82
+ objective: `Leaf ${i}`,
83
+ dependencies: ['root'],
84
+ }));
85
+ return [root, ...leaves];
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // 1. Deep dependency chains
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe('deep dependency chains', () => {
93
+ test('validates a 20-task linear chain without errors', () => {
94
+ const tasks = buildChain(20);
95
+ const plan: SwarmPlan = { objective: 'Deep chain', tasks };
96
+ // Should not throw — a pure chain is a valid DAG with no cycles.
97
+ const validated = validateAndNormalizePlan(plan, RESOLVED_LIMITS);
98
+ expect(validated.tasks).toHaveLength(20);
99
+ });
100
+
101
+ test('executes a 20-task linear chain completing all tasks in order', async () => {
102
+ const tasks = buildChain(20);
103
+ // Drive through executeSwarm directly — bypassing the maxTasks truncation
104
+ // inside the validator so we can test the orchestrator with 20 tasks.
105
+ const plan: SwarmPlan = { objective: 'Deep chain', tasks };
106
+
107
+ const completionOrder: string[] = [];
108
+ const backend = makeBackend({
109
+ runTask: async (input) => {
110
+ // Extract task id from prompt — the prompt embeds the task objective.
111
+ const match = input.prompt.match(/Task (\d+)/);
112
+ if (match) completionOrder.push(`t${match[1]}`);
113
+ return { success: true, output: SUCCESS_OUTPUT, durationMs: 2 };
114
+ },
115
+ });
116
+
117
+ const limits = resolveSwarmLimits({ ...LARGE_LIMITS, maxWorkers: 2 });
118
+ const summary = await executeSwarm({
119
+ plan,
120
+ limits,
121
+ backend,
122
+ workingDir: '/tmp',
123
+ });
124
+
125
+ expect(summary.stats.totalTasks).toBe(20);
126
+ expect(summary.stats.completed).toBe(20);
127
+ expect(summary.stats.failed).toBe(0);
128
+ expect(summary.stats.blocked).toBe(0);
129
+ });
130
+
131
+ test('blocks the entire tail when the head of a deep chain fails', async () => {
132
+ const tasks = buildChain(20);
133
+ const plan: SwarmPlan = { objective: 'Deep chain failure', tasks };
134
+
135
+ // Only the very first task (t0) fails; all subsequent ones should be blocked.
136
+ const backend = makeBackend({
137
+ runTask: async (input) => {
138
+ if (input.prompt.includes('Task 0')) {
139
+ return { success: false, output: 'fail', failureReason: 'timeout' as const, durationMs: 2 };
140
+ }
141
+ return { success: true, output: SUCCESS_OUTPUT, durationMs: 2 };
142
+ },
143
+ });
144
+
145
+ const summary = await executeSwarm({
146
+ plan,
147
+ limits: resolveSwarmLimits({ ...LARGE_LIMITS, maxRetriesPerTask: 0 }),
148
+ backend,
149
+ workingDir: '/tmp',
150
+ });
151
+
152
+ expect(summary.stats.failed).toBe(1);
153
+ // Every downstream task (t1..t19) must be blocked, not completed.
154
+ expect(summary.stats.blocked).toBe(19);
155
+ expect(summary.stats.completed).toBe(0);
156
+ });
157
+
158
+ test('passes dependency output down the full chain', async () => {
159
+ const tasks = buildChain(5);
160
+ const plan: SwarmPlan = { objective: 'Context propagation', tasks };
161
+
162
+ // Track how many dependency-output entries each task sees.
163
+ // buildWorkerPrompt uses the section header "Outputs from prerequisite tasks:"
164
+ // followed by "- [taskId]: summary" lines (one per upstream dependency).
165
+ const depCounts: Record<string, number> = {};
166
+ const backend = makeBackend({
167
+ runTask: async (input) => {
168
+ // Count how many "- [<id>]:" lines appear in the upstream-output section.
169
+ const prereqSection = input.prompt.match(
170
+ /Outputs from prerequisite tasks:([\s\S]*?)(?:\n\n|$)/,
171
+ );
172
+ const count = prereqSection
173
+ ? (prereqSection[1].match(/^- \[/gm) ?? []).length
174
+ : 0;
175
+ // The prompt embeds the objective ("Task N") verbatim.
176
+ const match = input.prompt.match(/Task (\d+)/);
177
+ if (match) depCounts[`t${match[1]}`] = count;
178
+ return { success: true, output: SUCCESS_OUTPUT, durationMs: 2 };
179
+ },
180
+ });
181
+
182
+ await executeSwarm({
183
+ plan,
184
+ limits: resolveSwarmLimits({ ...LARGE_LIMITS, maxWorkers: 1 }),
185
+ backend,
186
+ workingDir: '/tmp',
187
+ });
188
+
189
+ // t0 has no dependencies; each subsequent task has exactly one upstream dep.
190
+ expect(depCounts['t0'] ?? 0).toBe(0);
191
+ expect(depCounts['t1']).toBe(1);
192
+ expect(depCounts['t2']).toBe(1);
193
+ expect(depCounts['t3']).toBe(1);
194
+ expect(depCounts['t4']).toBe(1);
195
+ });
196
+
197
+ test('emits task events for every node in a deep chain', async () => {
198
+ const tasks = buildChain(10);
199
+ const plan: SwarmPlan = { objective: 'Event coverage', tasks };
200
+
201
+ const events: OrchestratorEvent[] = [];
202
+ await executeSwarm({
203
+ plan,
204
+ limits: resolveSwarmLimits({ ...LARGE_LIMITS, maxWorkers: 2 }),
205
+ backend: makeBackend(),
206
+ workingDir: '/tmp',
207
+ onStatus: (e) => events.push(e),
208
+ });
209
+
210
+ const startedIds = events.filter((e) => e.kind === 'task_started').map((e) => e.taskId);
211
+ const completedIds = events.filter((e) => e.kind === 'task_completed').map((e) => e.taskId);
212
+
213
+ // Every task must have been started and completed exactly once.
214
+ expect(new Set(startedIds).size).toBe(10);
215
+ expect(new Set(completedIds).size).toBe(10);
216
+ });
217
+ });
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // 2. Near-circular dependencies
221
+ // ---------------------------------------------------------------------------
222
+
223
+ describe('near-circular dependencies', () => {
224
+ /**
225
+ * "Near-circular" is a chain where the last node almost loops back to the
226
+ * first but instead stops one edge short. These are valid DAGs and must
227
+ * execute without error. Contrast with actual cycles, which must be
228
+ * rejected.
229
+ */
230
+
231
+ test('validates and executes a near-circular chain (A→B→C, C does NOT link back to A)', async () => {
232
+ // A→B→C is valid; the test confirms it finishes without a cycle error.
233
+ const tasks: SwarmTaskNode[] = [
234
+ { id: 'A', role: 'coder', objective: 'Step A', dependencies: [] },
235
+ { id: 'B', role: 'coder', objective: 'Step B', dependencies: ['A'] },
236
+ { id: 'C', role: 'coder', objective: 'Step C', dependencies: ['B'] },
237
+ ];
238
+ const plan: SwarmPlan = { objective: 'Near-circular chain', tasks };
239
+ expect(() => validateAndNormalizePlan(plan, RESOLVED_LIMITS)).not.toThrow();
240
+
241
+ const summary = await executeSwarm({
242
+ plan,
243
+ limits: RESOLVED_LIMITS,
244
+ backend: makeBackend(),
245
+ workingDir: '/tmp',
246
+ });
247
+ expect(summary.stats.completed).toBe(3);
248
+ expect(summary.stats.blocked).toBe(0);
249
+ });
250
+
251
+ test('rejects a minimal 2-node cycle (A→B→A)', () => {
252
+ const plan: SwarmPlan = {
253
+ objective: 'Two-node cycle',
254
+ tasks: [
255
+ { id: 'A', role: 'coder', objective: 'A', dependencies: ['B'] },
256
+ { id: 'B', role: 'coder', objective: 'B', dependencies: ['A'] },
257
+ ],
258
+ };
259
+ let err: unknown;
260
+ try {
261
+ validateAndNormalizePlan(plan, RESOLVED_LIMITS);
262
+ } catch (e) {
263
+ err = e;
264
+ }
265
+ expect(err).toBeInstanceOf(SwarmPlanValidationError);
266
+ expect((err as SwarmPlanValidationError).issues).toContainEqual(
267
+ expect.stringContaining('cycle'),
268
+ );
269
+ });
270
+
271
+ test('rejects a 3-node cycle (A→B→C→A)', () => {
272
+ const plan: SwarmPlan = {
273
+ objective: 'Three-node cycle',
274
+ tasks: [
275
+ { id: 'A', role: 'coder', objective: 'A', dependencies: ['C'] },
276
+ { id: 'B', role: 'coder', objective: 'B', dependencies: ['A'] },
277
+ { id: 'C', role: 'coder', objective: 'C', dependencies: ['B'] },
278
+ ],
279
+ };
280
+ expect(() => validateAndNormalizePlan(plan, RESOLVED_LIMITS)).toThrow(
281
+ SwarmPlanValidationError,
282
+ );
283
+ });
284
+
285
+ test('rejects a cycle embedded in an otherwise-acyclic graph', () => {
286
+ // D is an independent node; only A/B/C form the cycle.
287
+ const plan: SwarmPlan = {
288
+ objective: 'Mixed graph with embedded cycle',
289
+ tasks: [
290
+ { id: 'D', role: 'coder', objective: 'Independent', dependencies: [] },
291
+ { id: 'A', role: 'coder', objective: 'A', dependencies: ['C'] },
292
+ { id: 'B', role: 'coder', objective: 'B', dependencies: ['A'] },
293
+ { id: 'C', role: 'coder', objective: 'C', dependencies: ['B'] },
294
+ ],
295
+ };
296
+ let err: unknown;
297
+ try {
298
+ validateAndNormalizePlan(plan, RESOLVED_LIMITS);
299
+ } catch (e) {
300
+ err = e;
301
+ }
302
+ expect(err).toBeInstanceOf(SwarmPlanValidationError);
303
+ expect((err as SwarmPlanValidationError).issues).toContainEqual(
304
+ expect.stringContaining('cycle'),
305
+ );
306
+ });
307
+
308
+ test('rejects a self-loop (A depends on itself)', () => {
309
+ const plan: SwarmPlan = {
310
+ objective: 'Self-loop',
311
+ tasks: [
312
+ { id: 'A', role: 'coder', objective: 'A', dependencies: ['A'] },
313
+ ],
314
+ };
315
+ expect(() => validateAndNormalizePlan(plan, RESOLVED_LIMITS)).toThrow(
316
+ SwarmPlanValidationError,
317
+ );
318
+ });
319
+
320
+ test('validates a long chain that almost cycles — last node stops one step short', () => {
321
+ // t0→t1→...→t9; t9 does NOT depend on t0 (so it is NOT a cycle).
322
+ const tasks: SwarmTaskNode[] = Array.from({ length: 10 }, (_, i) => ({
323
+ id: `t${i}`,
324
+ role: 'coder' as const,
325
+ objective: `Step ${i}`,
326
+ dependencies: i === 0 ? [] : [`t${i - 1}`],
327
+ }));
328
+ const plan: SwarmPlan = { objective: 'Long near-circular chain', tasks };
329
+ // Should not throw.
330
+ const validated = validateAndNormalizePlan(plan, RESOLVED_LIMITS);
331
+ expect(validated.tasks).toHaveLength(10);
332
+ });
333
+
334
+ test('rejects a long chain that does complete the cycle — last node links back to first', () => {
335
+ // t0→t1→...→t8→t9, and t0 also depends on t9 — this closes the loop
336
+ // making the entire chain one big cycle.
337
+ const tasks: SwarmTaskNode[] = Array.from({ length: 10 }, (_, i) => ({
338
+ id: `t${i}`,
339
+ role: 'coder' as const,
340
+ objective: `Step ${i}`,
341
+ // t0 depends on t9 (closing the loop); every other node depends on its predecessor.
342
+ dependencies: i === 0 ? ['t9'] : [`t${i - 1}`],
343
+ }));
344
+ const plan: SwarmPlan = { objective: 'Closed long cycle', tasks };
345
+ expect(() => validateAndNormalizePlan(plan, RESOLVED_LIMITS)).toThrow(
346
+ SwarmPlanValidationError,
347
+ );
348
+ });
349
+
350
+ test('error message identifies at least one node involved in the cycle', () => {
351
+ const plan: SwarmPlan = {
352
+ objective: 'Cycle identification',
353
+ tasks: [
354
+ { id: 'X', role: 'coder', objective: 'X', dependencies: ['Z'] },
355
+ { id: 'Y', role: 'coder', objective: 'Y', dependencies: ['X'] },
356
+ { id: 'Z', role: 'coder', objective: 'Z', dependencies: ['Y'] },
357
+ ],
358
+ };
359
+ try {
360
+ validateAndNormalizePlan(plan, RESOLVED_LIMITS);
361
+ expect(true).toBe(false); // unreachable
362
+ } catch (e) {
363
+ const err = e as SwarmPlanValidationError;
364
+ const cycleMessage = err.issues.find((i) => i.includes('cycle')) ?? '';
365
+ // The cycle message must name at least one of the nodes.
366
+ const mentionsANode = ['X', 'Y', 'Z'].some((id) => cycleMessage.includes(id));
367
+ expect(mentionsANode).toBe(true);
368
+ }
369
+ });
370
+ });
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // 3. Very wide fan-outs
374
+ // ---------------------------------------------------------------------------
375
+
376
+ describe('very wide fan-outs', () => {
377
+ /**
378
+ * These tests bypass validateAndNormalizePlan (which caps maxTasks at 20)
379
+ * and drive executeSwarm directly. The orchestrator has no hard cap on
380
+ * tasks — the cap lives only in the validator. Testing the orchestrator
381
+ * directly lets us verify scheduling correctness at scale without needing
382
+ * to change the hard limit constants.
383
+ */
384
+
385
+ test('completes a star graph with 51 leaf tasks (1 root + 50 dependents)', async () => {
386
+ const tasks = buildStar(50);
387
+ const plan: SwarmPlan = { objective: 'Wide fan-out', tasks };
388
+
389
+ const summary = await executeSwarm({
390
+ plan,
391
+ limits: RESOLVED_LIMITS,
392
+ backend: makeBackend(),
393
+ workingDir: '/tmp',
394
+ });
395
+
396
+ expect(summary.stats.totalTasks).toBe(51);
397
+ expect(summary.stats.completed).toBe(51);
398
+ expect(summary.stats.failed).toBe(0);
399
+ expect(summary.stats.blocked).toBe(0);
400
+ });
401
+
402
+ test('no leaf starts before root finishes in a 51-task star', async () => {
403
+ const tasks = buildStar(50);
404
+ const plan: SwarmPlan = { objective: 'Wide fan-out ordering', tasks };
405
+
406
+ const completionOrder: string[] = [];
407
+ const backend = makeBackend({
408
+ runTask: async (input) => {
409
+ const isRoot = input.prompt.includes('Root task');
410
+ if (isRoot) {
411
+ // Introduce a tiny delay so leaves clearly start after root ends.
412
+ await new Promise((r) => setTimeout(r, 10));
413
+ // Record completion only after the delay finishes so completionOrder
414
+ // reflects actual completion time, not when execution started.
415
+ completionOrder.push('root');
416
+ }
417
+ return { success: true, output: SUCCESS_OUTPUT, durationMs: 2 };
418
+ },
419
+ });
420
+
421
+ await executeSwarm({
422
+ plan,
423
+ limits: RESOLVED_LIMITS,
424
+ backend,
425
+ workingDir: '/tmp',
426
+ onStatus: (e) => {
427
+ if (e.kind === 'task_started' && e.taskId !== 'root') {
428
+ // Leaves must only start after root has been recorded as completed.
429
+ expect(completionOrder).toContain('root');
430
+ }
431
+ },
432
+ });
433
+ });
434
+
435
+ test('blocks all 50 leaves when the root task fails', async () => {
436
+ const tasks = buildStar(50);
437
+ const plan: SwarmPlan = { objective: 'Fan-out failure propagation', tasks };
438
+
439
+ const backend = makeBackend({
440
+ runTask: async (input) => {
441
+ if (input.prompt.includes('Root task')) {
442
+ return {
443
+ success: false,
444
+ output: 'root failed',
445
+ failureReason: 'timeout' as const,
446
+ durationMs: 2,
447
+ };
448
+ }
449
+ return { success: true, output: SUCCESS_OUTPUT, durationMs: 2 };
450
+ },
451
+ });
452
+
453
+ const summary = await executeSwarm({
454
+ plan,
455
+ limits: resolveSwarmLimits({ ...LARGE_LIMITS, maxRetriesPerTask: 0 }),
456
+ backend,
457
+ workingDir: '/tmp',
458
+ });
459
+
460
+ expect(summary.stats.failed).toBe(1);
461
+ expect(summary.stats.blocked).toBe(50);
462
+ expect(summary.stats.completed).toBe(0);
463
+ });
464
+
465
+ test('honours maxWorkers concurrency ceiling during a wide fan-out', async () => {
466
+ const leafCount = 20;
467
+ const tasks = buildStar(leafCount);
468
+ const plan: SwarmPlan = { objective: 'Fan-out concurrency', tasks };
469
+
470
+ let peak = 0;
471
+ let current = 0;
472
+ const backend = makeBackend({
473
+ runTask: async () => {
474
+ current++;
475
+ if (current > peak) peak = current;
476
+ // Hold the slot open briefly so concurrency actually builds up.
477
+ await new Promise((r) => setTimeout(r, 5));
478
+ current--;
479
+ return { success: true, output: SUCCESS_OUTPUT, durationMs: 5 };
480
+ },
481
+ });
482
+
483
+ const maxWorkers = 3;
484
+ await executeSwarm({
485
+ plan,
486
+ limits: resolveSwarmLimits({ ...LARGE_LIMITS, maxWorkers }),
487
+ backend,
488
+ workingDir: '/tmp',
489
+ });
490
+
491
+ // Peak concurrency must never exceed the configured worker limit.
492
+ expect(peak).toBeLessThanOrEqual(maxWorkers);
493
+ // At least 2 workers should have run concurrently (shows leaves ran in
494
+ // parallel rather than sequentially).
495
+ expect(peak).toBeGreaterThanOrEqual(2);
496
+ });
497
+
498
+ test('all 50 leaf task_started events are emitted eventually in a wide fan-out', async () => {
499
+ const tasks = buildStar(50);
500
+ const plan: SwarmPlan = { objective: 'Fan-out event coverage', tasks };
501
+
502
+ const startedIds = new Set<string>();
503
+ await executeSwarm({
504
+ plan,
505
+ limits: RESOLVED_LIMITS,
506
+ backend: makeBackend(),
507
+ workingDir: '/tmp',
508
+ onStatus: (e) => {
509
+ if (e.kind === 'task_started' && e.taskId) startedIds.add(e.taskId);
510
+ },
511
+ });
512
+
513
+ // root + 50 leaves = 51 started events
514
+ expect(startedIds.size).toBe(51);
515
+ expect(startedIds.has('root')).toBe(true);
516
+ for (let i = 0; i < 50; i++) {
517
+ expect(startedIds.has(`leaf${i}`)).toBe(true);
518
+ }
519
+ });
520
+
521
+ test('validator correctly validates a 20-task star (within hard limits)', () => {
522
+ const tasks = buildStar(19); // 1 root + 19 leaves = 20 tasks total
523
+ const plan: SwarmPlan = { objective: 'Validated star', tasks };
524
+ const validated = validateAndNormalizePlan(plan, RESOLVED_LIMITS);
525
+ expect(validated.tasks).toHaveLength(20);
526
+ });
527
+
528
+ test('validator truncates a 30-task star to maxTasks=20', () => {
529
+ const tasks = buildStar(29); // 1 root + 29 leaves = 30 tasks total
530
+ const plan: SwarmPlan = { objective: 'Oversized star', tasks };
531
+ const validated = validateAndNormalizePlan(plan, RESOLVED_LIMITS);
532
+ // Truncated to 20; the root will still be present (it is first in the array).
533
+ expect(validated.tasks.length).toBeLessThanOrEqual(20);
534
+ });
535
+ });
@@ -42,6 +42,8 @@ mock.module('../util/logger.js', () => ({
42
42
  getLogger: () => new Proxy({} as Record<string, unknown>, {
43
43
  get: () => () => {},
44
44
  }),
45
+ isDebug: () => false,
46
+ truncateForLog: (v: string) => v,
45
47
  }));
46
48
 
47
49
  mock.module('../config/loader.js', () => ({
@@ -464,8 +464,8 @@ describe('renderTemplate', () => {
464
464
  expect(renderTemplate('Hello {{name}}', { name: 'World' })).toBe('Hello World');
465
465
  });
466
466
 
467
- test('leaves unknown placeholders unchanged', () => {
468
- expect(renderTemplate('{{unknown}} text', {})).toBe('{{unknown}} text');
467
+ test('replaces unknown placeholders with <MISSING: key>', () => {
468
+ expect(renderTemplate('{{unknown}} text', {})).toBe('<MISSING: unknown> text');
469
469
  });
470
470
 
471
471
  test('handles multiple placeholders', () => {