@vellumai/assistant 0.3.5 → 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 (486) hide show
  1. package/README.md +51 -0
  2. package/eslint.config.mjs +31 -0
  3. package/package.json +1 -1
  4. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  5. package/scripts/ipc/generate-swift.ts +18 -2
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +338 -1
  7. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  8. package/src/__tests__/browser-manager.test.ts +1 -0
  9. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  10. package/src/__tests__/call-orchestrator.test.ts +752 -271
  11. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  12. package/src/__tests__/call-recovery.test.ts +3 -0
  13. package/src/__tests__/call-routes-http.test.ts +5 -0
  14. package/src/__tests__/call-store.test.ts +3 -0
  15. package/src/__tests__/channel-approval-routes.test.ts +1260 -85
  16. package/src/__tests__/channel-approval.test.ts +37 -0
  17. package/src/__tests__/channel-approvals.test.ts +4 -65
  18. package/src/__tests__/channel-guardian.test.ts +556 -0
  19. package/src/__tests__/channel-readiness-service.test.ts +74 -7
  20. package/src/__tests__/checker.test.ts +14 -7
  21. package/src/__tests__/clarification-resolver.test.ts +44 -24
  22. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  23. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  24. package/src/__tests__/config-schema.test.ts +12 -7
  25. package/src/__tests__/context-window-manager.test.ts +30 -2
  26. package/src/__tests__/contradiction-checker.test.ts +20 -5
  27. package/src/__tests__/credential-security-invariants.test.ts +6 -2
  28. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  29. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  30. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  31. package/src/__tests__/guardian-action-store.test.ts +123 -0
  32. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  33. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  34. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  35. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  36. package/src/__tests__/handlers-twilio-config.test.ts +126 -0
  37. package/src/__tests__/intent-routing.test.ts +2 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +228 -1
  39. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  40. package/src/__tests__/model-intents.test.ts +96 -0
  41. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  42. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  43. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  44. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  45. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  46. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  47. package/src/__tests__/qdrant-manager.test.ts +27 -20
  48. package/src/__tests__/relay-server.test.ts +779 -40
  49. package/src/__tests__/run-orchestrator-assistant-events.test.ts +2 -0
  50. package/src/__tests__/run-orchestrator.test.ts +20 -4
  51. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  52. package/src/__tests__/runtime-runs.test.ts +16 -0
  53. package/src/__tests__/schedule-store.test.ts +18 -4
  54. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  55. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  56. package/src/__tests__/session-agent-loop.test.ts +857 -0
  57. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  58. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  59. package/src/__tests__/session-profile-injection.test.ts +6 -0
  60. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  61. package/src/__tests__/session-queue.test.ts +6 -0
  62. package/src/__tests__/session-runtime-assembly.test.ts +237 -13
  63. package/src/__tests__/session-slash-known.test.ts +6 -0
  64. package/src/__tests__/session-slash-queue.test.ts +6 -0
  65. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  66. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  67. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  68. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  69. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  70. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  71. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  72. package/src/__tests__/skills.test.ts +2 -0
  73. package/src/__tests__/sms-messaging-provider.test.ts +2 -1
  74. package/src/__tests__/starter-task-flow.test.ts +2 -0
  75. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  76. package/src/__tests__/system-prompt.test.ts +2 -0
  77. package/src/__tests__/task-management-tools.test.ts +2 -2
  78. package/src/__tests__/task-runner.test.ts +14 -4
  79. package/src/__tests__/terminal-tools.test.ts +25 -19
  80. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  81. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  82. package/src/__tests__/tool-executor.test.ts +23 -24
  83. package/src/__tests__/trust-store.test.ts +3 -3
  84. package/src/__tests__/twilio-rest.test.ts +29 -0
  85. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  86. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  87. package/src/__tests__/twilio-routes.test.ts +141 -21
  88. package/src/__tests__/user-reference.test.ts +2 -0
  89. package/src/__tests__/voice-quality.test.ts +222 -0
  90. package/src/__tests__/web-search.test.ts +45 -29
  91. package/src/agent/loop.ts +1 -1
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  93. package/src/amazon/client.ts +1418 -0
  94. package/src/amazon/request-extractor.ts +135 -0
  95. package/src/amazon/session.ts +109 -0
  96. package/src/autonomy/autonomy-store.ts +5 -5
  97. package/src/browser-extension-relay/client.ts +124 -0
  98. package/src/browser-extension-relay/protocol.ts +63 -0
  99. package/src/browser-extension-relay/server.ts +177 -0
  100. package/src/bundler/app-bundler.ts +3 -3
  101. package/src/bundler/bundle-signer.ts +1 -1
  102. package/src/bundler/signature-verifier.ts +1 -1
  103. package/src/calls/call-conversation-messages.ts +33 -0
  104. package/src/calls/call-domain.ts +106 -5
  105. package/src/calls/call-orchestrator.ts +252 -54
  106. package/src/calls/call-pointer-messages.ts +53 -0
  107. package/src/calls/call-recovery.ts +3 -8
  108. package/src/calls/call-store.ts +69 -87
  109. package/src/calls/elevenlabs-config.ts +3 -2
  110. package/src/calls/guardian-action-sweep.ts +105 -0
  111. package/src/calls/guardian-dispatch.ts +203 -0
  112. package/src/calls/guardian-question-copy.ts +133 -0
  113. package/src/calls/relay-server.ts +466 -8
  114. package/src/calls/speaker-identification.ts +1 -1
  115. package/src/calls/twilio-config.ts +7 -5
  116. package/src/calls/twilio-provider.ts +6 -4
  117. package/src/calls/twilio-rest.ts +40 -15
  118. package/src/calls/twilio-routes.ts +60 -45
  119. package/src/calls/types.ts +3 -1
  120. package/src/channels/types.ts +25 -0
  121. package/src/cli/amazon.ts +815 -0
  122. package/src/cli/config-commands.ts +2 -2
  123. package/src/cli/core-commands.ts +4 -3
  124. package/src/cli/influencer.ts +244 -0
  125. package/src/cli/map.ts +89 -6
  126. package/src/cli.ts +1 -1
  127. package/src/config/agent-schema.ts +171 -0
  128. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  129. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  130. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  131. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  132. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  133. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  134. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  135. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  136. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  137. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  138. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  139. package/src/config/bundled-skills/media-processing/SKILL.md +72 -95
  140. package/src/config/bundled-skills/media-processing/TOOLS.json +57 -147
  141. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  142. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  143. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  144. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  145. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  146. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  147. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  148. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +7 -9
  149. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  150. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +88 -253
  151. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +22 -153
  152. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
  153. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +28 -51
  154. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +35 -270
  155. package/src/config/bundled-skills/messaging/SKILL.md +12 -2
  156. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  157. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  158. package/src/config/bundled-skills/phone-calls/SKILL.md +86 -21
  159. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  160. package/src/config/bundled-tool-registry.ts +310 -0
  161. package/src/config/calls-schema.ts +181 -0
  162. package/src/config/core-schema.ts +309 -0
  163. package/src/config/defaults.ts +26 -2
  164. package/src/config/env-registry.ts +162 -0
  165. package/src/config/env.ts +175 -0
  166. package/src/config/loader.ts +6 -6
  167. package/src/config/memory-schema.ts +528 -0
  168. package/src/config/sandbox-schema.ts +55 -0
  169. package/src/config/schema.ts +156 -1137
  170. package/src/config/skill-state.ts +1 -1
  171. package/src/config/skills-schema.ts +32 -0
  172. package/src/config/skills.ts +35 -24
  173. package/src/config/system-prompt.ts +107 -56
  174. package/src/config/templates/SOUL.md +1 -1
  175. package/src/config/types.ts +1 -0
  176. package/src/config/user-reference.ts +4 -9
  177. package/src/config/vellum-skills/catalog.json +0 -7
  178. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  179. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +1 -0
  180. package/src/config/vellum-skills/sms-setup/SKILL.md +112 -14
  181. package/src/context/window-manager.ts +27 -7
  182. package/src/daemon/approval-generators.ts +186 -0
  183. package/src/daemon/approved-devices-store.ts +140 -0
  184. package/src/daemon/assistant-attachments.ts +1 -1
  185. package/src/daemon/classifier.ts +35 -32
  186. package/src/daemon/config-watcher.ts +1 -1
  187. package/src/daemon/daemon-control.ts +217 -0
  188. package/src/daemon/handlers/apps.ts +2 -3
  189. package/src/daemon/handlers/config-channels.ts +158 -0
  190. package/src/daemon/handlers/config-inbox.ts +540 -0
  191. package/src/daemon/handlers/config-ingress.ts +231 -0
  192. package/src/daemon/handlers/config-integrations.ts +258 -0
  193. package/src/daemon/handlers/config-model.ts +143 -0
  194. package/src/daemon/handlers/config-parental.ts +163 -0
  195. package/src/daemon/handlers/config-scheduling.ts +172 -0
  196. package/src/daemon/handlers/config-slack.ts +92 -0
  197. package/src/daemon/handlers/config-telegram.ts +301 -0
  198. package/src/daemon/handlers/config-tools.ts +177 -0
  199. package/src/daemon/handlers/config-trust.ts +104 -0
  200. package/src/daemon/handlers/config-twilio.ts +1080 -0
  201. package/src/daemon/handlers/config.ts +53 -2463
  202. package/src/daemon/handlers/diagnostics.ts +1 -1
  203. package/src/daemon/handlers/dictation.ts +4 -6
  204. package/src/daemon/handlers/documents.ts +18 -32
  205. package/src/daemon/handlers/index.ts +9 -0
  206. package/src/daemon/handlers/misc.ts +3 -5
  207. package/src/daemon/handlers/pairing.ts +98 -0
  208. package/src/daemon/handlers/sessions.ts +54 -5
  209. package/src/daemon/handlers/shared.ts +3 -1
  210. package/src/daemon/handlers/skills.ts +1 -1
  211. package/src/daemon/handlers/twitter-auth.ts +2 -0
  212. package/src/daemon/handlers/work-items.ts +2 -2
  213. package/src/daemon/handlers/workspace-files.ts +4 -3
  214. package/src/daemon/install-cli-launchers.ts +113 -0
  215. package/src/daemon/ipc-contract/apps.ts +356 -0
  216. package/src/daemon/ipc-contract/browser.ts +74 -0
  217. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  218. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  219. package/src/daemon/ipc-contract/documents.ts +74 -0
  220. package/src/daemon/ipc-contract/inbox.ts +209 -0
  221. package/src/daemon/ipc-contract/integrations.ts +284 -0
  222. package/src/daemon/ipc-contract/memory.ts +48 -0
  223. package/src/daemon/ipc-contract/messages.ts +211 -0
  224. package/src/daemon/ipc-contract/pairing.ts +45 -0
  225. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  226. package/src/daemon/ipc-contract/schedules.ts +97 -0
  227. package/src/daemon/ipc-contract/sessions.ts +315 -0
  228. package/src/daemon/ipc-contract/shared.ts +42 -0
  229. package/src/daemon/ipc-contract/skills.ts +120 -0
  230. package/src/daemon/ipc-contract/subagents.ts +58 -0
  231. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  232. package/src/daemon/ipc-contract/trust.ts +60 -0
  233. package/src/daemon/ipc-contract/work-items.ts +225 -0
  234. package/src/daemon/ipc-contract/workspace.ts +113 -0
  235. package/src/daemon/ipc-contract-inventory.json +60 -0
  236. package/src/daemon/ipc-contract-inventory.ts +55 -29
  237. package/src/daemon/ipc-contract.ts +226 -2527
  238. package/src/daemon/ipc-protocol.ts +1 -1
  239. package/src/daemon/ipc-validate.ts +7 -0
  240. package/src/daemon/lifecycle.ts +97 -379
  241. package/src/daemon/pairing-store.ts +177 -0
  242. package/src/daemon/providers-setup.ts +43 -0
  243. package/src/daemon/ride-shotgun-handler.ts +67 -2
  244. package/src/daemon/server.ts +60 -44
  245. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  246. package/src/daemon/session-agent-loop.ts +113 -275
  247. package/src/daemon/session-dynamic-profile.ts +1 -1
  248. package/src/daemon/session-history.ts +1 -1
  249. package/src/daemon/session-media-retry.ts +1 -1
  250. package/src/daemon/session-messaging.ts +37 -2
  251. package/src/daemon/session-notifiers.ts +5 -25
  252. package/src/daemon/session-process.ts +99 -59
  253. package/src/daemon/session-queue-manager.ts +96 -4
  254. package/src/daemon/session-runtime-assembly.ts +149 -15
  255. package/src/daemon/session-surfaces.ts +19 -4
  256. package/src/daemon/session-tool-setup.ts +28 -30
  257. package/src/daemon/session-workspace.ts +1 -1
  258. package/src/daemon/session.ts +24 -1
  259. package/src/daemon/shutdown-handlers.ts +122 -0
  260. package/src/daemon/trace-emitter.ts +1 -1
  261. package/src/daemon/watch-handler.ts +36 -33
  262. package/src/doordash/cart-queries.ts +787 -0
  263. package/src/doordash/client.ts +144 -127
  264. package/src/doordash/order-queries.ts +85 -0
  265. package/src/doordash/queries.ts +10 -1308
  266. package/src/doordash/search-queries.ts +203 -0
  267. package/src/doordash/session.ts +3 -2
  268. package/src/doordash/store-queries.ts +246 -0
  269. package/src/doordash/types.ts +367 -0
  270. package/src/email/providers/agentmail.ts +2 -1
  271. package/src/email/providers/index.ts +3 -2
  272. package/src/email/service.ts +3 -2
  273. package/src/errors.ts +43 -0
  274. package/src/home-base/prebuilt/seed.ts +1 -1
  275. package/src/hooks/cli.ts +6 -5
  276. package/src/hooks/config.ts +6 -8
  277. package/src/hooks/discovery.ts +6 -5
  278. package/src/hooks/manager.ts +4 -3
  279. package/src/hooks/runner.ts +2 -2
  280. package/src/hooks/templates.ts +5 -5
  281. package/src/inbound/public-ingress-urls.ts +3 -1
  282. package/src/index.ts +4 -2
  283. package/src/influencer/client.ts +1104 -0
  284. package/src/instrument.ts +4 -3
  285. package/src/logfire.ts +4 -3
  286. package/src/memory/admin.ts +25 -35
  287. package/src/memory/attachments-store.ts +4 -7
  288. package/src/memory/channel-delivery-store.ts +30 -1
  289. package/src/memory/channel-guardian-store.ts +200 -1
  290. package/src/memory/clarification-resolver.ts +37 -33
  291. package/src/memory/conflict-store.ts +67 -61
  292. package/src/memory/contradiction-checker.ts +141 -117
  293. package/src/memory/conversation-store.ts +335 -51
  294. package/src/memory/db-connection.ts +27 -4
  295. package/src/memory/db-init.ts +121 -4
  296. package/src/memory/db.ts +14 -1
  297. package/src/memory/embedding-backend.ts +27 -5
  298. package/src/memory/embedding-ollama.ts +2 -1
  299. package/src/memory/entity-extractor.ts +38 -35
  300. package/src/memory/guardian-action-store.ts +430 -0
  301. package/src/memory/inbox-escalation-projection.ts +59 -0
  302. package/src/memory/inbox-thread-store.ts +218 -0
  303. package/src/memory/ingress-invite-store.ts +338 -0
  304. package/src/memory/ingress-member-store.ts +350 -0
  305. package/src/memory/items-extractor.ts +91 -97
  306. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  307. package/src/memory/job-handlers/media-processing.ts +11 -42
  308. package/src/memory/job-handlers/summarization.ts +32 -26
  309. package/src/memory/job-utils.ts +3 -10
  310. package/src/memory/jobs-store.ts +6 -9
  311. package/src/memory/jobs-worker.ts +51 -36
  312. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  313. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  314. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  315. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  316. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  317. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  318. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  319. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  320. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  321. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  322. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  323. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  324. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  325. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  326. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  327. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  328. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  329. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  330. package/src/memory/migrations/index.ts +24 -0
  331. package/src/memory/migrations/registry.ts +79 -0
  332. package/src/memory/migrations/validate-migration-state.ts +69 -0
  333. package/src/memory/qdrant-manager.ts +49 -8
  334. package/src/memory/query-builder.ts +1 -1
  335. package/src/memory/raw-query.ts +119 -0
  336. package/src/memory/recall-cache.ts +4 -1
  337. package/src/memory/retriever.ts +160 -47
  338. package/src/memory/schema-migration.ts +25 -984
  339. package/src/memory/schema.ts +130 -7
  340. package/src/memory/search/entity.ts +10 -19
  341. package/src/memory/search/lexical.ts +81 -52
  342. package/src/memory/search/ranking.ts +21 -22
  343. package/src/memory/search/semantic.ts +157 -19
  344. package/src/memory/shared-app-links-store.ts +4 -5
  345. package/src/memory/validation.ts +19 -0
  346. package/src/messaging/draft-store.ts +5 -6
  347. package/src/messaging/providers/sms/adapter.ts +3 -6
  348. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  349. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  350. package/src/messaging/providers/whatsapp/client.ts +67 -0
  351. package/src/messaging/style-analyzer.ts +5 -4
  352. package/src/messaging/thread-summarizer.ts +61 -69
  353. package/src/messaging/triage-engine.ts +62 -71
  354. package/src/migrations/config-merge.ts +53 -0
  355. package/src/migrations/data-layout.ts +68 -0
  356. package/src/migrations/data-merge.ts +33 -0
  357. package/src/migrations/hooks-merge.ts +90 -0
  358. package/src/migrations/index.ts +6 -0
  359. package/src/migrations/log.ts +23 -0
  360. package/src/migrations/skills-merge.ts +33 -0
  361. package/src/migrations/workspace-layout.ts +79 -0
  362. package/src/permissions/checker.ts +119 -11
  363. package/src/permissions/prompter.ts +14 -0
  364. package/src/permissions/shell-identity.ts +31 -1
  365. package/src/permissions/trust-store.ts +21 -1
  366. package/src/providers/anthropic/client.ts +4 -4
  367. package/src/providers/failover.ts +2 -2
  368. package/src/providers/model-intents.ts +70 -0
  369. package/src/providers/ollama/client.ts +2 -1
  370. package/src/providers/provider-send-message.ts +176 -0
  371. package/src/providers/registry.ts +71 -30
  372. package/src/providers/retry.ts +35 -1
  373. package/src/providers/types.ts +12 -1
  374. package/src/runtime/approval-conversation-turn.ts +97 -0
  375. package/src/runtime/approval-message-composer.ts +115 -5
  376. package/src/runtime/channel-approval-parser.ts +36 -2
  377. package/src/runtime/channel-approvals.ts +0 -21
  378. package/src/runtime/channel-guardian-service.ts +48 -7
  379. package/src/runtime/channel-readiness-service.ts +160 -34
  380. package/src/runtime/channel-readiness-types.ts +10 -4
  381. package/src/runtime/channel-retry-sweep.ts +184 -0
  382. package/src/runtime/guardian-context-resolver.ts +108 -0
  383. package/src/runtime/http-server.ts +275 -743
  384. package/src/runtime/http-types.ts +56 -3
  385. package/src/runtime/middleware/auth.ts +116 -0
  386. package/src/runtime/middleware/error-handler.ts +33 -0
  387. package/src/runtime/middleware/twilio-validation.ts +127 -0
  388. package/src/runtime/routes/app-routes.ts +1 -1
  389. package/src/runtime/routes/call-routes.ts +49 -6
  390. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  391. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  392. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  393. package/src/runtime/routes/channel-route-shared.ts +144 -0
  394. package/src/runtime/routes/channel-routes.ts +32 -1634
  395. package/src/runtime/routes/conversation-routes.ts +50 -7
  396. package/src/runtime/routes/events-routes.ts +2 -2
  397. package/src/runtime/routes/identity-routes.ts +126 -0
  398. package/src/runtime/routes/pairing-routes.ts +143 -0
  399. package/src/runtime/routes/run-routes.ts +15 -1
  400. package/src/runtime/run-orchestrator.ts +52 -34
  401. package/src/schedule/schedule-store.ts +36 -32
  402. package/src/schedule/scheduler.ts +3 -3
  403. package/src/security/encrypted-store.ts +5 -7
  404. package/src/security/oauth2.ts +45 -15
  405. package/src/security/parental-control-store.ts +183 -0
  406. package/src/security/secret-allowlist.ts +4 -3
  407. package/src/security/secret-scanner.ts +5 -5
  408. package/src/security/secure-keys.ts +1 -1
  409. package/src/security/token-manager.ts +3 -2
  410. package/src/services/vercel-deploy.ts +6 -2
  411. package/src/skills/tool-manifest.ts +3 -3
  412. package/src/skills/vellum-catalog-remote.ts +75 -16
  413. package/src/slack/slack-webhook.ts +2 -1
  414. package/src/swarm/orchestrator.ts +92 -1
  415. package/src/swarm/router-planner.ts +6 -9
  416. package/src/swarm/worker-prompts.ts +9 -12
  417. package/src/tasks/task-compiler.ts +19 -28
  418. package/src/tasks/task-runner.ts +1 -1
  419. package/src/tools/assets/search.ts +15 -14
  420. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  421. package/src/tools/browser/auto-navigate.ts +1 -0
  422. package/src/tools/browser/browser-execution.ts +10 -1
  423. package/src/tools/browser/browser-manager.ts +119 -4
  424. package/src/tools/browser/network-recorder.ts +5 -0
  425. package/src/tools/credentials/broker.ts +11 -2
  426. package/src/tools/credentials/metadata-store.ts +18 -14
  427. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  428. package/src/tools/credentials/vault.ts +49 -23
  429. package/src/tools/executor.ts +68 -9
  430. package/src/tools/host-terminal/cli-discover.ts +1 -1
  431. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  432. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  433. package/src/tools/network/script-proxy/server.ts +1 -1
  434. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  435. package/src/tools/network/web-fetch.ts +18 -2
  436. package/src/tools/network/web-search.ts +7 -3
  437. package/src/tools/reminder/reminder-store.ts +14 -15
  438. package/src/tools/schedule/create.ts +1 -0
  439. package/src/tools/schedule/list.ts +2 -1
  440. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  441. package/src/tools/skills/skill-script-runner.ts +24 -9
  442. package/src/tools/skills/skill-tool-factory.ts +1 -0
  443. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  444. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  445. package/src/tools/terminal/parser.ts +50 -0
  446. package/src/tools/watcher/delete.ts +6 -0
  447. package/src/tools/weather/service.ts +1 -1
  448. package/src/twitter/client.ts +190 -24
  449. package/src/twitter/session.ts +4 -3
  450. package/src/util/clipboard.ts +1 -1
  451. package/src/util/errors.ts +65 -8
  452. package/src/util/fs.ts +40 -0
  453. package/src/util/json.ts +10 -0
  454. package/src/util/log-redact.ts +189 -0
  455. package/src/util/logger.ts +19 -17
  456. package/src/util/object.ts +3 -0
  457. package/src/util/platform.ts +72 -365
  458. package/src/util/pricing.ts +1 -1
  459. package/src/util/promise-guard.ts +1 -1
  460. package/src/util/retry.ts +19 -0
  461. package/src/util/row-mapper.ts +79 -0
  462. package/src/util/silently.ts +21 -0
  463. package/src/watcher/engine.ts +5 -1
  464. package/src/watcher/provider-types.ts +20 -0
  465. package/src/watcher/providers/github.ts +156 -0
  466. package/src/watcher/providers/gmail.ts +1 -0
  467. package/src/watcher/providers/google-calendar.ts +1 -0
  468. package/src/watcher/providers/linear.ts +460 -0
  469. package/src/watcher/providers/slack.ts +1 -0
  470. package/src/work-items/work-item-runner.ts +1 -1
  471. package/src/workspace/git-service.ts +1 -1
  472. package/src/workspace/provider-commit-message-generator.ts +51 -22
  473. package/src/__tests__/call-bridge.test.ts +0 -517
  474. package/src/__tests__/session-process-bridge.test.ts +0 -244
  475. package/src/calls/call-bridge.ts +0 -168
  476. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +0 -137
  477. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +0 -280
  478. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +0 -144
  479. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +0 -136
  480. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +0 -95
  481. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +0 -267
  482. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +0 -110
  483. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +0 -235
  484. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +0 -142
  485. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +0 -150
  486. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -34,15 +34,35 @@ export interface WatcherProvider {
34
34
  /**
35
35
  * Fetch new events since the given watermark.
36
36
  * Returns new items and an updated watermark.
37
+ *
38
+ * `watcherKey` is the unique watcher instance ID (e.g. the DB row UUID).
39
+ * Providers that maintain per-watcher in-process state (like the Linear
40
+ * issue-state cache) must key that state by `watcherKey` — not just
41
+ * `credentialService` — so that multiple watchers sharing the same
42
+ * credential maintain independent baselines.
37
43
  */
38
44
  fetchNew(
39
45
  credentialService: string,
40
46
  watermark: string | null,
41
47
  config: Record<string, unknown>,
48
+ watcherKey: string,
42
49
  ): Promise<FetchResult>;
43
50
 
44
51
  /**
45
52
  * Get the initial watermark (start from "now" so we don't replay history).
46
53
  */
47
54
  getInitialWatermark(credentialService: string): Promise<string>;
55
+
56
+ /**
57
+ * Release any in-process state held for a watcher instance.
58
+ * Called only when a watcher is truly deleted so that providers with
59
+ * per-watcher caches (e.g. the Linear issue-state map) can evict the
60
+ * stale entry and prevent unbounded memory growth.
61
+ *
62
+ * Must NOT be called on circuit-breaker auto-disable — that path is
63
+ * reversible, and clearing state would cause missed events on re-enable.
64
+ *
65
+ * Optional — providers with no per-watcher state need not implement this.
66
+ */
67
+ cleanup?(watcherKey: string): void;
48
68
  }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * GitHub watcher provider — polls for new PRs, issues, and review requests.
3
+ *
4
+ * Uses the GitHub Notifications API (`GET /notifications`) with a timestamp
5
+ * watermark. On first poll, captures the current time as the watermark so we
6
+ * start from "now" and don't replay historical notifications.
7
+ *
8
+ * The credential service expects a GitHub Personal Access Token (or fine-grained
9
+ * token) stored under `integration:github`. The token needs at minimum the
10
+ * `notifications` scope (classic) or Notification read permission (fine-grained).
11
+ */
12
+
13
+ import { withValidToken } from '../../security/token-manager.js';
14
+ import { truncate } from '../../util/truncate.js';
15
+ import type { WatcherProvider, WatcherItem, FetchResult } from '../provider-types.js';
16
+ import { getLogger } from '../../util/logger.js';
17
+
18
+ const log = getLogger('watcher:github');
19
+
20
+ const GITHUB_API_BASE = 'https://api.github.com';
21
+
22
+ // ── API types ──────────────────────────────────────────────────────────────────
23
+
24
+ interface GitHubNotification {
25
+ id: string;
26
+ reason: string; // 'assign', 'author', 'comment', 'mention', 'review_requested', 'subscribed', etc.
27
+ unread: boolean;
28
+ updated_at: string;
29
+ subject: {
30
+ title: string;
31
+ url: string | null;
32
+ latest_comment_url: string | null;
33
+ type: 'Issue' | 'PullRequest' | 'Release' | 'Commit' | string;
34
+ };
35
+ repository: {
36
+ full_name: string;
37
+ html_url: string;
38
+ };
39
+ }
40
+
41
+ // ── Helpers ────────────────────────────────────────────────────────────────────
42
+
43
+ /** Map a GitHub notification reason to a watcher event type. */
44
+ function reasonToEventType(reason: string, subjectType: string): string {
45
+ if (reason === 'review_requested') return 'github_review_requested';
46
+ if (reason === 'assign') return subjectType === 'Issue' ? 'github_issue_assigned' : 'github_pr_assigned';
47
+ if (reason === 'mention') return 'github_mention';
48
+ if (subjectType === 'PullRequest') return 'github_pr_activity';
49
+ return 'github_notification';
50
+ }
51
+
52
+ function notificationToItem(n: GitHubNotification): WatcherItem {
53
+ const eventType = reasonToEventType(n.reason, n.subject.type);
54
+ const repoName = n.repository.full_name;
55
+ const title = n.subject.title;
56
+ const subjectType = n.subject.type;
57
+
58
+ return {
59
+ externalId: n.id,
60
+ eventType,
61
+ summary: `GitHub ${subjectType} in ${repoName}: ${truncate(title, 80)}`,
62
+ payload: {
63
+ id: n.id,
64
+ reason: n.reason,
65
+ subjectType: n.subject.type,
66
+ title,
67
+ subjectUrl: n.subject.url,
68
+ repoFullName: repoName,
69
+ repoHtmlUrl: n.repository.html_url,
70
+ updatedAt: n.updated_at,
71
+ },
72
+ timestamp: new Date(n.updated_at).getTime(),
73
+ };
74
+ }
75
+
76
+ /** Fetch a single page of notifications since a timestamp. */
77
+ async function fetchNotificationsPage(
78
+ token: string,
79
+ since: string,
80
+ page: number,
81
+ ): Promise<{ items: GitHubNotification[]; hasMore: boolean }> {
82
+ const params = new URLSearchParams({
83
+ all: 'false', // only unread
84
+ since,
85
+ per_page: '50',
86
+ page: String(page),
87
+ });
88
+
89
+ const resp = await fetch(`${GITHUB_API_BASE}/notifications?${params}`, {
90
+ headers: {
91
+ Authorization: `Bearer ${token}`,
92
+ Accept: 'application/vnd.github+json',
93
+ 'X-GitHub-Api-Version': '2022-11-28',
94
+ },
95
+ });
96
+
97
+ if (!resp.ok) {
98
+ const body = await resp.text().catch(() => '');
99
+ throw new Error(`GitHub Notifications API ${resp.status}: ${body}`);
100
+ }
101
+
102
+ const items = (await resp.json()) as GitHubNotification[];
103
+ // GitHub returns 50 per page; if we got a full page there may be more
104
+ const hasMore = items.length === 50;
105
+ return { items, hasMore };
106
+ }
107
+
108
+ // ── Provider ───────────────────────────────────────────────────────────────────
109
+
110
+ export const githubProvider: WatcherProvider = {
111
+ id: 'github',
112
+ displayName: 'GitHub',
113
+ requiredCredentialService: 'integration:github',
114
+
115
+ async getInitialWatermark(_credentialService: string): Promise<string> {
116
+ // Start from "now" so we don't replay all existing notifications
117
+ return new Date().toISOString();
118
+ },
119
+
120
+ async fetchNew(
121
+ credentialService: string,
122
+ watermark: string | null,
123
+ _config: Record<string, unknown>,
124
+ _watcherKey: string,
125
+ ): Promise<FetchResult> {
126
+ return withValidToken(credentialService, async (token) => {
127
+ const since = watermark ?? new Date().toISOString();
128
+ const items: WatcherItem[] = [];
129
+ let page = 1;
130
+
131
+ while (true) {
132
+ const { items: pageItems, hasMore } = await fetchNotificationsPage(token, since, page);
133
+
134
+ for (const n of pageItems) {
135
+ // Only surface notifications for reasons that warrant attention
136
+ const relevantReasons = new Set([
137
+ 'assign', 'mention', 'review_requested', 'team_mention',
138
+ ]);
139
+ if (!relevantReasons.has(n.reason)) continue;
140
+
141
+ items.push(notificationToItem(n));
142
+ }
143
+
144
+ if (!hasMore) break;
145
+ page++;
146
+ }
147
+
148
+ // New watermark: the time just before we fetched so we don't miss events
149
+ // that arrive between poll cycles.
150
+ const newWatermark = new Date().toISOString();
151
+
152
+ log.info({ count: items.length, watermark: newWatermark }, 'GitHub: fetched new notifications');
153
+ return { items, watermark: newWatermark };
154
+ });
155
+ },
156
+ };
@@ -113,6 +113,7 @@ export const gmailProvider: WatcherProvider = {
113
113
  credentialService: string,
114
114
  watermark: string | null,
115
115
  _config: Record<string, unknown>,
116
+ _watcherKey: string,
116
117
  ): Promise<FetchResult> {
117
118
  return withValidToken(credentialService, async (token) => {
118
119
  if (!watermark) {
@@ -139,6 +139,7 @@ export const googleCalendarProvider: WatcherProvider = {
139
139
  credentialService: string,
140
140
  watermark: string | null,
141
141
  _config: Record<string, unknown>,
142
+ _watcherKey: string,
142
143
  ): Promise<FetchResult> {
143
144
  return withValidToken(credentialService, async (token) => {
144
145
  if (!watermark) {
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Linear watcher provider — polls for assigned issues, status changes, and @mentions.
3
+ *
4
+ * Uses the Linear GraphQL API with a timestamp watermark. On first poll, captures
5
+ * the current time as the watermark so we start from "now" and don't replay history.
6
+ *
7
+ * The watermark is an ISO 8601 timestamp string used in the `updatedAt_gte` filter.
8
+ * We query notifications (which cover assignments and mentions) and issue status changes
9
+ * for issues assigned to the authenticated user.
10
+ *
11
+ * The credential service expects a Linear API key (personal or OAuth access token)
12
+ * stored under `integration:linear`. The token only needs read access to notifications
13
+ * and issues.
14
+ */
15
+
16
+ import { withValidToken } from '../../security/token-manager.js';
17
+ import { truncate } from '../../util/truncate.js';
18
+ import type { WatcherProvider, WatcherItem, FetchResult } from '../provider-types.js';
19
+ import { getLogger } from '../../util/logger.js';
20
+
21
+ const log = getLogger('watcher:linear');
22
+
23
+ const LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql';
24
+
25
+ // ── GraphQL response types ────────────────────────────────────────────────────
26
+
27
+ interface LinearNotification {
28
+ id: string;
29
+ type: string;
30
+ createdAt: string;
31
+ updatedAt: string;
32
+ issue?: {
33
+ id: string;
34
+ identifier: string;
35
+ title: string;
36
+ url: string;
37
+ state?: {
38
+ id: string;
39
+ name: string;
40
+ type: string;
41
+ };
42
+ assignee?: {
43
+ id: string;
44
+ name: string;
45
+ email: string;
46
+ };
47
+ team?: {
48
+ id: string;
49
+ name: string;
50
+ };
51
+ };
52
+ comment?: {
53
+ id: string;
54
+ body: string;
55
+ };
56
+ }
57
+
58
+ interface LinearIssue {
59
+ id: string;
60
+ identifier: string;
61
+ title: string;
62
+ url: string;
63
+ updatedAt: string;
64
+ state: {
65
+ id: string;
66
+ name: string;
67
+ type: string;
68
+ };
69
+ team: {
70
+ id: string;
71
+ name: string;
72
+ };
73
+ assignee?: {
74
+ id: string;
75
+ name: string;
76
+ };
77
+ }
78
+
79
+ interface LinearViewer {
80
+ id: string;
81
+ name: string;
82
+ email: string;
83
+ }
84
+
85
+ // ── GraphQL helpers ───────────────────────────────────────────────────────────
86
+
87
+ async function graphql<T>(token: string, query: string, variables?: Record<string, unknown>): Promise<T> {
88
+ const resp = await fetch(LINEAR_GRAPHQL_URL, {
89
+ method: 'POST',
90
+ headers: {
91
+ // Linear accepts both personal API keys and OAuth tokens; the Bearer scheme
92
+ // is required for all token types per Linear's API docs.
93
+ 'Authorization': `Bearer ${token}`,
94
+ 'Content-Type': 'application/json',
95
+ },
96
+ body: JSON.stringify({ query, variables }),
97
+ });
98
+
99
+ if (!resp.ok) {
100
+ const body = await resp.text().catch(() => '');
101
+ throw new Error(`Linear API ${resp.status}: ${body}`);
102
+ }
103
+
104
+ const result = await resp.json() as { data?: T; errors?: Array<{ message: string }> };
105
+
106
+ if (result.errors?.length) {
107
+ throw new Error(`Linear GraphQL errors: ${result.errors.map((e) => e.message).join(', ')}`);
108
+ }
109
+
110
+ if (!result.data) {
111
+ throw new Error('Linear API returned no data');
112
+ }
113
+
114
+ return result.data;
115
+ }
116
+
117
+ /** Fetch the authenticated user's ID and name. */
118
+ async function fetchViewer(token: string): Promise<LinearViewer> {
119
+ const data = await graphql<{ viewer: LinearViewer }>(token, `
120
+ query {
121
+ viewer {
122
+ id
123
+ name
124
+ email
125
+ }
126
+ }
127
+ `);
128
+ return data.viewer;
129
+ }
130
+
131
+ /**
132
+ * Fetch all notifications since a given ISO timestamp, paginating until
133
+ * `pageInfo.hasNextPage` is false so we never miss events when 50+ arrive
134
+ * between polls.
135
+ */
136
+ async function fetchNotifications(
137
+ token: string,
138
+ since: string,
139
+ ): Promise<LinearNotification[]> {
140
+ const allNodes: LinearNotification[] = [];
141
+ let cursor: string | null = null;
142
+
143
+ type NotificationsResponse = {
144
+ notifications: { nodes: LinearNotification[]; pageInfo: { hasNextPage: boolean; endCursor: string } };
145
+ };
146
+
147
+ do {
148
+ const data: NotificationsResponse = await graphql<NotificationsResponse>(token, `
149
+ query FetchNotifications($after: DateTime, $cursor: String) {
150
+ notifications(
151
+ filter: { updatedAt: { gte: $after } }
152
+ orderBy: updatedAt
153
+ first: 50
154
+ after: $cursor
155
+ ) {
156
+ nodes {
157
+ id
158
+ type
159
+ createdAt
160
+ updatedAt
161
+ ... on IssueNotification {
162
+ issue {
163
+ id
164
+ identifier
165
+ title
166
+ url
167
+ state {
168
+ id
169
+ name
170
+ type
171
+ }
172
+ assignee {
173
+ id
174
+ name
175
+ email
176
+ }
177
+ team {
178
+ id
179
+ name
180
+ }
181
+ }
182
+ }
183
+ ... on IssueCommentMentionNotification {
184
+ issue {
185
+ id
186
+ identifier
187
+ title
188
+ url
189
+ team {
190
+ id
191
+ name
192
+ }
193
+ }
194
+ comment {
195
+ id
196
+ body
197
+ }
198
+ }
199
+ }
200
+ pageInfo {
201
+ hasNextPage
202
+ endCursor
203
+ }
204
+ }
205
+ }
206
+ `, { after: since, cursor });
207
+
208
+ allNodes.push(...data.notifications.nodes);
209
+ cursor = data.notifications.pageInfo.hasNextPage ? data.notifications.pageInfo.endCursor : null;
210
+ } while (cursor != null);
211
+
212
+ return allNodes;
213
+ }
214
+
215
+ /**
216
+ * Fetch all assigned issues updated since the watermark, paginating until
217
+ * `pageInfo.hasNextPage` is false so updates beyond the first 50 aren't skipped.
218
+ */
219
+ async function fetchAssignedIssueUpdates(
220
+ token: string,
221
+ viewerId: string,
222
+ since: string,
223
+ ): Promise<LinearIssue[]> {
224
+ const allNodes: LinearIssue[] = [];
225
+ let cursor: string | null = null;
226
+
227
+ type IssuesResponse = {
228
+ issues: { nodes: LinearIssue[]; pageInfo: { hasNextPage: boolean; endCursor: string } };
229
+ };
230
+
231
+ do {
232
+ const data: IssuesResponse = await graphql<IssuesResponse>(token, `
233
+ query FetchAssignedIssues($assigneeId: ID, $after: DateTime, $cursor: String) {
234
+ issues(
235
+ filter: {
236
+ assignee: { id: { eq: $assigneeId } }
237
+ updatedAt: { gte: $after }
238
+ }
239
+ orderBy: updatedAt
240
+ first: 50
241
+ after: $cursor
242
+ ) {
243
+ nodes {
244
+ id
245
+ identifier
246
+ title
247
+ url
248
+ updatedAt
249
+ state {
250
+ id
251
+ name
252
+ type
253
+ }
254
+ team {
255
+ id
256
+ name
257
+ }
258
+ assignee {
259
+ id
260
+ name
261
+ }
262
+ }
263
+ pageInfo {
264
+ hasNextPage
265
+ endCursor
266
+ }
267
+ }
268
+ }
269
+ `, { assigneeId: viewerId, after: since, cursor });
270
+
271
+ allNodes.push(...data.issues.nodes);
272
+ cursor = data.issues.pageInfo.hasNextPage ? data.issues.pageInfo.endCursor : null;
273
+ } while (cursor != null);
274
+
275
+ return allNodes;
276
+ }
277
+
278
+ // ── Issue state tracking ──────────────────────────────────────────────────────
279
+
280
+ /**
281
+ * Tracks the last known state ID per issue, scoped by watcher instance key
282
+ * (the watcher's DB UUID) so that multiple Linear watchers in the same process
283
+ * — even when they share the same `credentialService` string — maintain
284
+ * completely independent baselines.
285
+ *
286
+ * Keying by `credentialService` alone is insufficient: `runWatchersOnce` polls
287
+ * watchers sequentially, so watcher-1 would write its post-poll state into the
288
+ * shared map, and watcher-2 would then see already-updated IDs and silently
289
+ * skip emitting valid transitions. Outer key: watcherKey; inner key: issue ID.
290
+ *
291
+ * In-memory only; resets on daemon restart, which is acceptable — the first
292
+ * poll after restart will seed the map without emitting false-positive events.
293
+ */
294
+ const knownIssueStateIdsByWatcher = new Map<string, Map<string, string>>();
295
+
296
+ /** Get (or lazily create) the per-watcher state map. */
297
+ function getStateCache(watcherKey: string): Map<string, string> {
298
+ let cache = knownIssueStateIdsByWatcher.get(watcherKey);
299
+ if (!cache) {
300
+ cache = new Map<string, string>();
301
+ knownIssueStateIdsByWatcher.set(watcherKey, cache);
302
+ }
303
+ return cache;
304
+ }
305
+
306
+ /**
307
+ * Evict the state cache for a watcher that has been deleted or permanently
308
+ * disabled. Prevents unbounded growth of `knownIssueStateIdsByWatcher` in
309
+ * environments that create and delete watchers frequently (watcher churn).
310
+ */
311
+ export function clearLinearStateCache(watcherKey: string): void {
312
+ knownIssueStateIdsByWatcher.delete(watcherKey);
313
+ }
314
+
315
+ // ── Event type mapping ────────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * Map a Linear notification type to a watcher event type.
319
+ * Linear notification types include: issueAssignedToYou, issueMentionedYou,
320
+ * issueCommentMentionedYou, issueStatusChanged, etc.
321
+ */
322
+ function notificationTypeToEventType(type: string): string {
323
+ if (type === 'issueAssignedToYou') return 'linear_issue_assigned';
324
+ if (type === 'issueMentionedYou') return 'linear_mention';
325
+ if (type === 'issueCommentMentionedYou') return 'linear_comment_mention';
326
+ if (type === 'issueStatusChanged') return 'linear_status_changed';
327
+ return 'linear_notification';
328
+ }
329
+
330
+ function notificationToItem(n: LinearNotification): WatcherItem {
331
+ const eventType = notificationTypeToEventType(n.type);
332
+ const issue = n.issue;
333
+ const teamName = issue?.team?.name ?? 'Unknown Team';
334
+ const issueRef = issue ? `${issue.identifier}: ${truncate(issue.title, 60)}` : 'Unknown issue';
335
+
336
+ const summary = eventType === 'linear_comment_mention' && n.comment
337
+ ? `Linear @mention in ${teamName} / ${issueRef}: ${truncate(n.comment.body, 80)}`
338
+ : `Linear ${n.type.replace(/([A-Z])/g, ' $1').trim()} in ${teamName} / ${issueRef}`;
339
+
340
+ return {
341
+ externalId: n.id,
342
+ eventType,
343
+ summary,
344
+ payload: {
345
+ notificationId: n.id,
346
+ type: n.type,
347
+ issueId: issue?.id,
348
+ issueIdentifier: issue?.identifier,
349
+ issueTitle: issue?.title,
350
+ issueUrl: issue?.url,
351
+ issueStateName: issue?.state?.name,
352
+ issueStateType: issue?.state?.type,
353
+ teamName,
354
+ commentBody: n.comment?.body,
355
+ updatedAt: n.updatedAt,
356
+ },
357
+ timestamp: new Date(n.updatedAt).getTime(),
358
+ };
359
+ }
360
+
361
+ function issueToStatusChangeItem(issue: LinearIssue, previousStateId: string): WatcherItem {
362
+ // Composite key encodes both the old and new state so re-polling the same
363
+ // transition doesn't generate a duplicate event via the dedup layer.
364
+ const externalId = `status_change:${issue.id}:${previousStateId}→${issue.state.id}`;
365
+ const teamName = issue.team?.name ?? 'Unknown Team';
366
+
367
+ return {
368
+ externalId,
369
+ eventType: 'linear_status_changed',
370
+ summary: `Linear status → ${issue.state.name} in ${teamName} / ${issue.identifier}: ${truncate(issue.title, 60)}`,
371
+ payload: {
372
+ issueId: issue.id,
373
+ issueIdentifier: issue.identifier,
374
+ issueTitle: issue.title,
375
+ issueUrl: issue.url,
376
+ stateName: issue.state.name,
377
+ stateType: issue.state.type,
378
+ teamName,
379
+ updatedAt: issue.updatedAt,
380
+ },
381
+ timestamp: new Date(issue.updatedAt).getTime(),
382
+ };
383
+ }
384
+
385
+ // ── Provider ──────────────────────────────────────────────────────────────────
386
+
387
+ export const linearProvider: WatcherProvider = {
388
+ id: 'linear',
389
+ displayName: 'Linear',
390
+ requiredCredentialService: 'integration:linear',
391
+
392
+ async getInitialWatermark(_credentialService: string): Promise<string> {
393
+ // Start from "now" so we don't replay all existing notifications
394
+ return new Date().toISOString();
395
+ },
396
+
397
+ cleanup(watcherKey: string): void {
398
+ clearLinearStateCache(watcherKey);
399
+ },
400
+
401
+ async fetchNew(
402
+ credentialService: string,
403
+ watermark: string | null,
404
+ _config: Record<string, unknown>,
405
+ watcherKey: string,
406
+ ): Promise<FetchResult> {
407
+ return withValidToken(credentialService, async (token) => {
408
+ const since = watermark ?? new Date().toISOString();
409
+
410
+ // Resolve the authenticated viewer's ID once per poll for the assigned-issues query
411
+ const viewer = await fetchViewer(token);
412
+
413
+ // Fetch notifications (assignments, mentions, status changes via notification feed)
414
+ const notifications = await fetchNotifications(token, since);
415
+
416
+ // Only surface notification types that warrant attention
417
+ const relevantTypes = new Set([
418
+ 'issueAssignedToYou',
419
+ 'issueMentionedYou',
420
+ 'issueCommentMentionedYou',
421
+ 'issueStatusChanged',
422
+ ]);
423
+
424
+ const items: WatcherItem[] = [];
425
+
426
+ for (const n of notifications) {
427
+ if (!relevantTypes.has(n.type)) continue;
428
+ items.push(notificationToItem(n));
429
+ }
430
+
431
+ // Also poll assigned issues directly for status changes not covered by
432
+ // notifications (e.g., bulk team updates). We only emit an event when the
433
+ // state ID differs from what we recorded on the previous poll — any other
434
+ // field update (title, description, etc.) does not constitute a status change.
435
+ // On first sight of an issue we seed the map without emitting, so we don't
436
+ // fire false-positive events after a daemon restart.
437
+ const assignedIssues = await fetchAssignedIssueUpdates(token, viewer.id, since);
438
+ // Scope the state cache by watcherKey (the watcher's DB UUID) rather than
439
+ // credentialService so that multiple Linear watchers sharing the same credential
440
+ // maintain independent baselines and cannot clobber each other's state.
441
+ const stateCache = getStateCache(watcherKey);
442
+ for (const issue of assignedIssues) {
443
+ const previousStateId = stateCache.get(issue.id);
444
+ if (previousStateId !== undefined && previousStateId !== issue.state.id) {
445
+ items.push(issueToStatusChangeItem(issue, previousStateId));
446
+ }
447
+ // Always update the map so the next poll has an accurate baseline.
448
+ stateCache.set(issue.id, issue.state.id);
449
+ }
450
+
451
+ const newWatermark = new Date().toISOString();
452
+ log.info(
453
+ { count: items.length, viewer: viewer.name, watermark: newWatermark },
454
+ 'Linear: fetched new notifications',
455
+ );
456
+
457
+ return { items, watermark: newWatermark };
458
+ });
459
+ },
460
+ };
@@ -49,6 +49,7 @@ export const slackProvider: WatcherProvider = {
49
49
  credentialService: string,
50
50
  watermark: string | null,
51
51
  _config: Record<string, unknown>,
52
+ _watcherKey: string,
52
53
  ): Promise<FetchResult> {
53
54
  return withValidToken(credentialService, async (token) => {
54
55
  if (!watermark) {
@@ -89,7 +89,7 @@ export function runWorkItemInBackground(workItemId: string): RunWorkItemResult {
89
89
 
90
90
  // Resolve required tools
91
91
  let requiredTools: string[];
92
- if (workItem.requiredTools !== null && workItem.requiredTools !== undefined) {
92
+ if (workItem.requiredTools != null) {
93
93
  requiredTools = sanitizeToolList(JSON.parse(workItem.requiredTools));
94
94
  } else {
95
95
  requiredTools = task.requiredTools
@@ -651,7 +651,7 @@ export class WorkspaceGitService {
651
651
  return;
652
652
  }
653
653
 
654
- const state = currentBranch === null ? 'detached HEAD' : `branch '${currentBranch}'`;
654
+ const state = currentBranch == null ? 'detached HEAD' : `branch '${currentBranch}'`;
655
655
  log.warn(
656
656
  { workspaceDir: this.workspaceDir, currentBranch },
657
657
  `Workspace repo is on ${state}; auto-switching to main`,