@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f

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 (866) hide show
  1. package/ARCHITECTURE.md +94 -91
  2. package/Dockerfile +5 -16
  3. package/README.md +7 -10
  4. package/bun.lock +0 -4
  5. package/docs/architecture/security.md +20 -20
  6. package/docs/stt-provider-onboarding.md +4 -5
  7. package/docs/trusted-contact-access.md +7 -7
  8. package/eslint-rules/cli-no-daemon-internals.js +4 -0
  9. package/eslint.config.mjs +7 -1
  10. package/knip.json +0 -1
  11. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
  13. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
  14. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
  15. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
  17. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
  19. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
  21. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
  22. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
  23. package/openapi.yaml +1073 -225
  24. package/package.json +1 -4
  25. package/scripts/test.sh +35 -13
  26. package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
  27. package/src/__tests__/acp-session.test.ts +4 -1
  28. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
  29. package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
  30. package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
  31. package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
  32. package/src/__tests__/app-source-watcher.test.ts +11 -0
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
  34. package/src/__tests__/assistant-attachments.test.ts +62 -4
  35. package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
  36. package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
  37. package/src/__tests__/attachments-store.test.ts +3 -0
  38. package/src/__tests__/background-shell-bash.test.ts +34 -0
  39. package/src/__tests__/background-shell-host-bash.test.ts +12 -0
  40. package/src/__tests__/background-tool-registry.test.ts +97 -0
  41. package/src/__tests__/background-tool-routes.test.ts +72 -1
  42. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  43. package/src/__tests__/btw-routes.test.ts +57 -3
  44. package/src/__tests__/call-controller.test.ts +474 -84
  45. package/src/__tests__/call-domain.test.ts +48 -1
  46. package/src/__tests__/call-pointer-messages.test.ts +57 -19
  47. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
  48. package/src/__tests__/call-routes-http.test.ts +15 -14
  49. package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
  50. package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
  51. package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
  52. package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
  53. package/src/__tests__/call-setup-flow.test.ts +311 -0
  54. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
  55. package/src/__tests__/channel-guardian.test.ts +4 -7
  56. package/src/__tests__/channel-readiness-service.test.ts +0 -6
  57. package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
  58. package/src/__tests__/channel-retry-sweep.test.ts +76 -64
  59. package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
  60. package/src/__tests__/clear-all-lexical.test.ts +90 -0
  61. package/src/__tests__/compactor-media-strip.test.ts +177 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +5 -5
  63. package/src/__tests__/config-schema.test.ts +17 -0
  64. package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
  65. package/src/__tests__/contacts-relay-reads.test.ts +188 -23
  66. package/src/__tests__/contacts-tools.test.ts +42 -1
  67. package/src/__tests__/contacts-write.test.ts +181 -0
  68. package/src/__tests__/context-search-conversations-source.test.ts +220 -81
  69. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  71. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  72. package/src/__tests__/conversation-agent-loop.test.ts +39 -0
  73. package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
  74. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
  75. package/src/__tests__/conversation-clear-safety.test.ts +0 -1
  76. package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
  77. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
  78. package/src/__tests__/conversation-disk-view.test.ts +1 -0
  79. package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
  80. package/src/__tests__/conversation-error.test.ts +0 -18
  81. package/src/__tests__/conversation-fork-crud.test.ts +67 -0
  82. package/src/__tests__/conversation-history-web-search.test.ts +7 -137
  83. package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
  84. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
  85. package/src/__tests__/conversation-process-callsite.test.ts +37 -0
  86. package/src/__tests__/conversation-queue.test.ts +149 -0
  87. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
  88. package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
  89. package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
  90. package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
  91. package/src/__tests__/conversation-skill-tools.test.ts +73 -0
  92. package/src/__tests__/conversation-wipe.test.ts +20 -5
  93. package/src/__tests__/credential-prompt-route.test.ts +69 -0
  94. package/src/__tests__/credential-security-invariants.test.ts +1 -3
  95. package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
  96. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
  97. package/src/__tests__/edit-propagation.test.ts +56 -2
  98. package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
  99. package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
  100. package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
  101. package/src/__tests__/guardian-question-mode.test.ts +57 -0
  102. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
  103. package/src/__tests__/guardian-wait-controller.test.ts +619 -0
  104. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
  105. package/src/__tests__/heic-fixture.ts +55 -0
  106. package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
  107. package/src/__tests__/http-conversation-lineage.test.ts +6 -1
  108. package/src/__tests__/image-conversion.test.ts +170 -0
  109. package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
  110. package/src/__tests__/introduction-card-resolver.test.ts +354 -0
  111. package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
  112. package/src/__tests__/invite-routes-http.test.ts +246 -420
  113. package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
  114. package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
  115. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
  116. package/src/__tests__/list-messages-attachments.test.ts +159 -0
  117. package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
  118. package/src/__tests__/list-messages-queued.test.ts +25 -0
  119. package/src/__tests__/managed-store.test.ts +129 -1
  120. package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
  121. package/src/__tests__/media-stream-output.test.ts +350 -19
  122. package/src/__tests__/media-stream-server-integration.test.ts +943 -169
  123. package/src/__tests__/media-stream-stt-session.test.ts +498 -1
  124. package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
  125. package/src/__tests__/model-intents.test.ts +1 -1
  126. package/src/__tests__/mtime-cache.test.ts +19 -0
  127. package/src/__tests__/non-member-access-request.test.ts +349 -3
  128. package/src/__tests__/notification-decision-fallback.test.ts +104 -8
  129. package/src/__tests__/notification-decision-strategy.test.ts +57 -8
  130. package/src/__tests__/onboarding-template-contract.test.ts +27 -43
  131. package/src/__tests__/openai-provider.test.ts +33 -0
  132. package/src/__tests__/openai-responses-provider.test.ts +40 -0
  133. package/src/__tests__/persistence-layering-guard.test.ts +29 -20
  134. package/src/__tests__/persona-resolver.test.ts +56 -2
  135. package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
  136. package/src/__tests__/phone.test.ts +1 -15
  137. package/src/__tests__/plugin-bootstrap.test.ts +25 -196
  138. package/src/__tests__/plugin-disabled-state.test.ts +33 -0
  139. package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
  140. package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
  141. package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
  142. package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
  143. package/src/__tests__/plugin-pipeline.test.ts +63 -1
  144. package/src/__tests__/plugin-route-contribution.test.ts +62 -115
  145. package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
  146. package/src/__tests__/plugin-types.test.ts +0 -1
  147. package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
  148. package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
  149. package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
  150. package/src/__tests__/public-ingress-urls.test.ts +0 -48
  151. package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
  152. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
  153. package/src/__tests__/resolve-trust-class.test.ts +28 -10
  154. package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
  155. package/src/__tests__/schedule-retry.test.ts +38 -14
  156. package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
  157. package/src/__tests__/schedule-routes.test.ts +14 -6
  158. package/src/__tests__/schedule-store.test.ts +3 -1
  159. package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
  160. package/src/__tests__/scheduler-recurrence.test.ts +57 -107
  161. package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
  162. package/src/__tests__/scheduler-wake.test.ts +13 -23
  163. package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
  164. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  165. package/src/__tests__/shell-observability.test.ts +1 -0
  166. package/src/__tests__/skill-boundary-guard.test.ts +3 -4
  167. package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
  168. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
  169. package/src/__tests__/skill-script-runner-host.test.ts +53 -0
  170. package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
  171. package/src/__tests__/skill-tool-factory.test.ts +73 -1
  172. package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
  173. package/src/__tests__/slack-inbound-verification.test.ts +36 -0
  174. package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
  175. package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
  176. package/src/__tests__/stt-hints.test.ts +2 -256
  177. package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
  178. package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
  179. package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
  180. package/src/__tests__/subagent-persistence.test.ts +122 -0
  181. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  182. package/src/__tests__/subagent-terminal-message.test.ts +99 -0
  183. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
  184. package/src/__tests__/system-prompt.test.ts +17 -118
  185. package/src/__tests__/task-scheduler.test.ts +68 -89
  186. package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
  187. package/src/__tests__/telephony-tts-guard.test.ts +538 -0
  188. package/src/__tests__/title-generate-hook.test.ts +16 -0
  189. package/src/__tests__/tool-approval-handler.test.ts +87 -1
  190. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
  191. package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
  192. package/src/__tests__/trust-context-guards.test.ts +21 -10
  193. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
  194. package/src/__tests__/twilio-provider.test.ts +17 -17
  195. package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
  196. package/src/__tests__/twilio-routes.test.ts +257 -387
  197. package/src/__tests__/twilio-validation.test.ts +1 -1
  198. package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
  199. package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
  200. package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
  201. package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
  202. package/src/__tests__/voice-quality.test.ts +2 -2
  203. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
  204. package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
  205. package/src/__tests__/voice-session-bridge.test.ts +1 -10
  206. package/src/__tests__/workspace-git-service.test.ts +1 -1
  207. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
  208. package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
  209. package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
  210. package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
  211. package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
  212. package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
  213. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
  214. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
  215. package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
  216. package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
  217. package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
  218. package/src/acp/__tests__/session-manager.test.ts +6 -0
  219. package/src/acp/agent-process.test.ts +116 -0
  220. package/src/acp/agent-process.ts +71 -0
  221. package/src/acp/auto-install.ts +12 -8
  222. package/src/acp/failure-error.test.ts +109 -0
  223. package/src/acp/failure-error.ts +111 -0
  224. package/src/acp/prepare-agent-env.ts +0 -12
  225. package/src/acp/resolve-agent.test.ts +6 -45
  226. package/src/acp/resolve-agent.ts +2 -4
  227. package/src/acp/session-manager.test.ts +312 -0
  228. package/src/acp/session-manager.ts +102 -46
  229. package/src/agent/ax-tree-compaction.test.ts +4 -1
  230. package/src/agent/image-optimize.ts +17 -127
  231. package/src/agent/loop.ts +45 -192
  232. package/src/api/events/background-tool-completed.test.ts +51 -0
  233. package/src/api/events/background-tool-completed.ts +29 -0
  234. package/src/api/events/background-tool-started.test.ts +36 -0
  235. package/src/api/events/background-tool-started.ts +28 -0
  236. package/src/api/events/conversation-error.ts +0 -1
  237. package/src/api/events/open-panel.ts +33 -0
  238. package/src/api/events/subagent-status-changed.ts +4 -1
  239. package/src/api/index.ts +19 -0
  240. package/src/api/responses/conversation-message.ts +34 -0
  241. package/src/approvals/guardian-decision-primitive.ts +74 -24
  242. package/src/approvals/guardian-request-resolvers.ts +450 -122
  243. package/src/apps/app-store.ts +1 -0
  244. package/src/apps/shared-app-links-store.ts +1 -0
  245. package/src/avatar/identity-avatar.ts +3 -7
  246. package/src/background-wake/wake-intent-hooks.test.ts +1 -1
  247. package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
  248. package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
  249. package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
  250. package/src/calls/call-controller.ts +134 -48
  251. package/src/calls/call-domain.ts +18 -42
  252. package/src/calls/call-pointer-messages.ts +51 -67
  253. package/src/calls/call-setup-flow-types.ts +76 -0
  254. package/src/calls/call-setup-flow.ts +1743 -0
  255. package/src/calls/call-speech-output.ts +87 -23
  256. package/src/calls/call-store.ts +4 -0
  257. package/src/calls/call-transport.ts +22 -36
  258. package/src/calls/fish-audio-client.ts +3 -0
  259. package/src/calls/gateway-invite-reader.ts +98 -0
  260. package/src/calls/guardian-wait-controller.ts +451 -0
  261. package/src/calls/media-stream-audio-transcode.ts +82 -2
  262. package/src/calls/media-stream-output.ts +142 -36
  263. package/src/calls/media-stream-server.ts +297 -178
  264. package/src/calls/media-stream-stt-session.ts +393 -50
  265. package/src/calls/media-turn-detector.ts +4 -6
  266. package/src/calls/relay-access-wait.ts +3 -4
  267. package/src/calls/relay-setup-router.ts +22 -43
  268. package/src/calls/relay-verification.ts +17 -24
  269. package/src/calls/resolve-call-tts-provider.ts +96 -29
  270. package/src/calls/stt-hints.ts +24 -87
  271. package/src/calls/telephony-credential-preflight.ts +159 -0
  272. package/src/calls/telephony-tts-capability.ts +183 -0
  273. package/src/calls/tts-call-strategy.ts +18 -20
  274. package/src/calls/twilio-provider.ts +3 -2
  275. package/src/calls/twilio-routes.ts +47 -351
  276. package/src/calls/types.ts +5 -3
  277. package/src/calls/voice-ingress-preflight.ts +9 -0
  278. package/src/calls/voice-quality.ts +13 -18
  279. package/src/calls/voice-session-bridge.ts +25 -35
  280. package/src/channels/__tests__/types.test.ts +28 -0
  281. package/src/channels/channel-verification-sessions.ts +7 -2
  282. package/src/channels/config.ts +0 -42
  283. package/src/channels/slack-thread-store.ts +12 -0
  284. package/src/channels/types.ts +23 -0
  285. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
  286. package/src/cli/commands/__tests__/schedules.test.ts +83 -34
  287. package/src/cli/commands/__tests__/status.test.ts +42 -2
  288. package/src/cli/commands/contacts.ts +2 -2
  289. package/src/cli/commands/credentials.ts +17 -0
  290. package/src/cli/commands/monitoring.ts +191 -0
  291. package/src/cli/commands/schedules-worker.ts +175 -0
  292. package/src/cli/commands/schedules.ts +169 -43
  293. package/src/cli/commands/status.ts +12 -1
  294. package/src/cli/commands/tools.ts +5 -1
  295. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
  296. package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
  297. package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
  298. package/src/cli/lib/search-plugins.ts +6 -0
  299. package/src/cli/lib/toggle-plugin.ts +21 -2
  300. package/src/cli/program.ts +3 -1
  301. package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
  302. package/src/config/acp-defaults.test.ts +0 -10
  303. package/src/config/acp-defaults.ts +0 -6
  304. package/src/config/bundled-skills/acp/SKILL.md +13 -45
  305. package/src/config/bundled-skills/acp/TOOLS.json +3 -3
  306. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  307. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
  308. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
  309. package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
  310. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  311. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
  312. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
  313. package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
  314. package/src/config/call-site-defaults.ts +0 -2
  315. package/src/config/feature-flag-registry.json +16 -8
  316. package/src/config/loader.ts +11 -0
  317. package/src/config/schema.ts +9 -1
  318. package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
  319. package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
  320. package/src/config/schemas/call-site-catalog.ts +0 -12
  321. package/src/config/schemas/calls.ts +8 -0
  322. package/src/config/schemas/elevenlabs.ts +1 -1
  323. package/src/config/schemas/llm.ts +0 -2
  324. package/src/config/schemas/memory-v2.ts +1 -1
  325. package/src/config/schemas/memory-v3.ts +82 -0
  326. package/src/config/schemas/monitoring.ts +61 -0
  327. package/src/config/schemas/schedules.ts +35 -0
  328. package/src/config/seed-inference-profiles.ts +2 -2
  329. package/src/config/skills-plugin-filter.test.ts +96 -0
  330. package/src/config/skills.ts +26 -0
  331. package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
  332. package/src/contacts/canonical-guardian-store.ts +5 -0
  333. package/src/contacts/contact-store.ts +81 -35
  334. package/src/contacts/contacts-write.ts +50 -25
  335. package/src/contacts/member-write-relay.ts +115 -31
  336. package/src/contacts/types.ts +0 -8
  337. package/src/context/compactor.ts +29 -15
  338. package/src/context/outbound-sanitize.ts +202 -0
  339. package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
  340. package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
  341. package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
  342. package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
  343. package/src/daemon/abort-watchdog.ts +7 -0
  344. package/src/daemon/app-source-watcher.ts +9 -0
  345. package/src/daemon/assistant-attachments.ts +42 -0
  346. package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
  347. package/src/daemon/conversation-agent-loop.ts +61 -68
  348. package/src/daemon/conversation-error.ts +1 -15
  349. package/src/daemon/conversation-history.ts +32 -236
  350. package/src/daemon/conversation-initial-prompt.ts +77 -0
  351. package/src/daemon/conversation-lifecycle.ts +25 -1
  352. package/src/daemon/conversation-messaging.ts +2 -2
  353. package/src/daemon/conversation-plugin-scope.ts +38 -0
  354. package/src/daemon/conversation-process.ts +50 -14
  355. package/src/daemon/conversation-runtime-assembly.ts +36 -6
  356. package/src/daemon/conversation-skill-tools.ts +19 -3
  357. package/src/daemon/conversation-store.ts +3 -3
  358. package/src/daemon/conversation-surfaces.ts +171 -0
  359. package/src/daemon/conversation-tool-setup.test.ts +95 -0
  360. package/src/daemon/conversation-tool-setup.ts +116 -1
  361. package/src/daemon/conversation-turn-finalize.ts +211 -0
  362. package/src/daemon/conversation.ts +92 -1
  363. package/src/daemon/disk-pressure-policy.ts +2 -1
  364. package/src/daemon/event-loop-watchdog.ts +22 -6
  365. package/src/daemon/external-plugins-bootstrap.ts +47 -134
  366. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
  367. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
  368. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
  369. package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
  370. package/src/daemon/handlers/config-a2a.ts +7 -14
  371. package/src/daemon/handlers/config-channels.ts +2 -10
  372. package/src/daemon/handlers/conversation-history.ts +3 -1
  373. package/src/daemon/handlers/conversations.ts +0 -52
  374. package/src/daemon/handlers/shared.ts +4 -24
  375. package/src/daemon/handlers/skills.ts +4 -1
  376. package/src/daemon/host-cu-proxy.ts +1 -1
  377. package/src/daemon/lifecycle.ts +59 -472
  378. package/src/daemon/message-protocol.ts +3 -3
  379. package/src/daemon/message-provenance.ts +12 -14
  380. package/src/daemon/message-types/background-tools.ts +28 -0
  381. package/src/daemon/message-types/contacts.ts +2 -10
  382. package/src/daemon/message-types/conversations.ts +10 -0
  383. package/src/daemon/message-types/integrations.ts +2 -0
  384. package/src/daemon/message-types/surfaces.ts +1 -0
  385. package/src/daemon/message-types/sync.ts +1 -0
  386. package/src/daemon/pointer-conversation-trust.ts +23 -9
  387. package/src/daemon/pointer-turn-runner.ts +174 -0
  388. package/src/daemon/providers-setup.ts +16 -0
  389. package/src/daemon/shutdown-handlers.ts +139 -47
  390. package/src/daemon/skill-memory-refresh.ts +2 -2
  391. package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
  392. package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
  393. package/src/daemon/tool-setup-types.ts +8 -0
  394. package/src/daemon/trust-context.ts +21 -6
  395. package/src/daemon/turn-latency-tracker.test.ts +93 -0
  396. package/src/daemon/turn-latency-tracker.ts +38 -0
  397. package/src/daemon/wake-conversation-ops.ts +10 -8
  398. package/src/documents/document-comments-store.test.ts +7 -1
  399. package/src/documents/document-comments-store.ts +8 -0
  400. package/src/documents/document-store.ts +15 -0
  401. package/src/export/__tests__/transcript-formatter.test.ts +11 -0
  402. package/src/export/transcript-formatter.ts +49 -67
  403. package/src/home/__tests__/feed-writer.test.ts +247 -1
  404. package/src/home/conversation-starter-checkpoints.ts +1 -0
  405. package/src/home/feed-writer.ts +61 -0
  406. package/src/home/job-handlers/conversation-starters.ts +1 -0
  407. package/src/hooks/hook-loader.ts +12 -0
  408. package/src/hooks/registry.ts +32 -4
  409. package/src/inbound/public-ingress-urls.ts +0 -17
  410. package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
  411. package/src/ipc/assistant-server.ts +28 -7
  412. package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
  413. package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
  414. package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
  415. package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
  416. package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
  417. package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
  418. package/src/ipc/routes/db-proxy.ts +5 -2
  419. package/src/ipc/routes/invite-ipc-routes.ts +53 -39
  420. package/src/jobs/worker.ts +9 -2
  421. package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
  422. package/src/live-voice/live-voice-archive.ts +5 -0
  423. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
  424. package/src/messaging/providers/channel-transport.ts +6 -0
  425. package/src/messaging/providers/index.ts +3 -0
  426. package/src/messaging/providers/slack/api.test.ts +49 -1
  427. package/src/messaging/providers/slack/api.ts +110 -0
  428. package/src/messaging/providers/slack/send.test.ts +85 -0
  429. package/src/messaging/providers/slack/send.ts +100 -28
  430. package/src/messaging/providers/slack/transport.ts +7 -13
  431. package/src/messaging/providers/whatsapp/send.ts +29 -9
  432. package/src/monitoring/__tests__/control.test.ts +235 -0
  433. package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
  434. package/src/monitoring/control.ts +105 -0
  435. package/src/monitoring/resource-sampler.ts +258 -0
  436. package/src/monitoring/sample-ring-buffer.ts +108 -0
  437. package/src/monitoring/worker.ts +94 -0
  438. package/src/notifications/README.md +1 -1
  439. package/src/notifications/access-request-copy.ts +94 -23
  440. package/src/notifications/adapters/slack.ts +89 -21
  441. package/src/notifications/approval-card-builder.ts +33 -18
  442. package/src/notifications/approval-card-data.ts +48 -15
  443. package/src/notifications/broadcaster.ts +25 -16
  444. package/src/notifications/decision-engine.ts +131 -32
  445. package/src/notifications/guardian-question-mode.ts +58 -10
  446. package/src/permissions/approval-policy.ts +8 -1
  447. package/src/permissions/checker.ts +2 -0
  448. package/src/permissions/gateway-threshold-reader.ts +7 -6
  449. package/src/permissions/ipc-risk-types.ts +1 -0
  450. package/src/permissions/secret-prompter.ts +11 -3
  451. package/src/permissions/types.ts +2 -2
  452. package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
  453. package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
  454. package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
  455. package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
  456. package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
  457. package/src/persistence/a2a-invite-store.ts +205 -0
  458. package/src/persistence/attachments-store.ts +94 -10
  459. package/src/persistence/checkpoints.ts +45 -0
  460. package/src/persistence/conversation-crud.ts +453 -260
  461. package/src/persistence/conversation-display-order-migration.ts +8 -2
  462. package/src/persistence/conversation-group-migration.test.ts +8 -0
  463. package/src/persistence/conversation-group-migration.ts +9 -0
  464. package/src/persistence/conversation-queries.ts +244 -125
  465. package/src/persistence/conversation-row-batch-delete.ts +9 -5
  466. package/src/persistence/conversation-search-lexical.ts +49 -0
  467. package/src/persistence/db-async-query.ts +42 -0
  468. package/src/persistence/db-connection.ts +13 -0
  469. package/src/persistence/delivery-channels.ts +0 -82
  470. package/src/persistence/delivery-crud.ts +27 -6
  471. package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
  472. package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
  473. package/src/persistence/embeddings/embedding-identity.ts +17 -5
  474. package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
  475. package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
  476. package/src/persistence/embeddings/qdrant-client.ts +81 -5
  477. package/src/persistence/group-crud.ts +13 -1
  478. package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
  479. package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
  480. package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
  481. package/src/persistence/job-handlers/cleanup.ts +57 -9
  482. package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
  483. package/src/persistence/job-handlers/message-lexical.ts +251 -0
  484. package/src/persistence/jobs-store.ts +102 -8
  485. package/src/persistence/jobs-worker.ts +68 -28
  486. package/src/persistence/llm-request-log-store.ts +35 -19
  487. package/src/persistence/llm-usage-store.ts +11 -0
  488. package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
  489. package/src/persistence/memory-lifecycle-hooks.ts +55 -1
  490. package/src/persistence/migrations/028-call-session-mode.ts +1 -1
  491. package/src/persistence/migrations/116-messages-fts.ts +8 -1
  492. package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
  493. package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
  494. package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
  495. package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
  496. package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
  497. package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
  498. package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
  499. package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
  500. package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
  501. package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
  502. package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
  503. package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
  504. package/src/persistence/raw-query.ts +50 -11
  505. package/src/persistence/schema/a2a.ts +14 -0
  506. package/src/persistence/schema/contacts.ts +0 -62
  507. package/src/persistence/schema/conversation-groups.ts +12 -0
  508. package/src/persistence/schema/conversations.ts +2 -0
  509. package/src/persistence/schema/documents.ts +65 -0
  510. package/src/persistence/schema/guardian.ts +4 -0
  511. package/src/persistence/schema/index.ts +4 -0
  512. package/src/persistence/schema/memory-injection.ts +62 -0
  513. package/src/persistence/schema/workflows.ts +55 -0
  514. package/src/persistence/slow-query-log.ts +437 -0
  515. package/src/persistence/slow-sync-log.ts +204 -0
  516. package/src/persistence/steps.ts +16 -0
  517. package/src/persistence/subagent-store.ts +125 -0
  518. package/src/persistence/worker-control.ts +45 -238
  519. package/src/plugin-api/event-hub-facade.ts +172 -0
  520. package/src/plugin-api/index.ts +9 -1
  521. package/src/plugin-api/types.ts +40 -9
  522. package/src/plugins/defaults/default-plugin-names.ts +60 -0
  523. package/src/plugins/defaults/index.ts +19 -29
  524. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
  525. package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
  526. package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
  527. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
  528. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
  529. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
  530. package/src/plugins/defaults/memory/config.ts +11 -0
  531. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
  532. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
  533. package/src/plugins/defaults/memory/embeddings.ts +77 -0
  534. package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
  535. package/src/plugins/defaults/memory/frontmatter.ts +50 -0
  536. package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
  537. package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
  538. package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
  539. package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
  540. package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
  541. package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
  542. package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
  543. package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
  544. package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
  545. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  546. package/src/plugins/defaults/memory/hooks/init.ts +31 -5
  547. package/src/plugins/defaults/memory/indexer.ts +2 -3
  548. package/src/plugins/defaults/memory/injectors.ts +2 -2
  549. package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
  550. package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
  551. package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
  552. package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
  553. package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
  554. package/src/plugins/defaults/memory/job-handlers.ts +13 -12
  555. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
  556. package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
  557. package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
  558. package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
  559. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
  560. package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
  561. package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
  562. package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
  563. package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
  564. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
  565. package/src/plugins/defaults/memory/search/semantic.ts +2 -2
  566. package/src/plugins/defaults/memory/startup.ts +244 -0
  567. package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
  568. package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
  569. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
  570. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
  571. package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
  572. package/src/plugins/defaults/memory/v2/activation.ts +2 -4
  573. package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
  574. package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
  575. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
  576. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
  577. package/src/plugins/defaults/memory/v2/migration.ts +2 -5
  578. package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
  579. package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
  580. package/src/plugins/defaults/memory/v2/router.ts +5 -5
  581. package/src/plugins/defaults/memory/v2/sim.ts +2 -4
  582. package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
  583. package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
  584. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
  585. package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
  586. package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
  587. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
  588. package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
  589. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
  590. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
  591. package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
  592. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
  593. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
  594. package/src/plugins/defaults/memory/v3/card.ts +1 -4
  595. package/src/plugins/defaults/memory/v3/dense.ts +60 -25
  596. package/src/plugins/defaults/memory/v3/edge.ts +1 -1
  597. package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
  598. package/src/plugins/defaults/memory/v3/gate.ts +158 -0
  599. package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
  600. package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
  601. package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
  602. package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
  603. package/src/plugins/defaults/memory/v3/prune.ts +2 -2
  604. package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
  605. package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
  606. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
  607. package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
  608. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
  609. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  610. package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
  611. package/src/plugins/injector-registry.ts +13 -2
  612. package/src/plugins/mtime-cache.ts +36 -47
  613. package/src/plugins/pipeline.ts +15 -1
  614. package/src/plugins/types.ts +14 -23
  615. package/src/prompts/__tests__/system-prompt.test.ts +170 -32
  616. package/src/prompts/persona-resolver.ts +13 -9
  617. package/src/prompts/system-prompt.ts +30 -9
  618. package/src/prompts/templates/BOOTSTRAP.md +16 -119
  619. package/src/prompts/templates/SOUL.md +43 -13
  620. package/src/prompts/templates/system-sections.ts +32 -27
  621. package/src/prompts/templates/users/default.md +19 -0
  622. package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
  623. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
  624. package/src/providers/model-intents.ts +2 -2
  625. package/src/providers/openai/chat-completions-provider.ts +14 -1
  626. package/src/providers/openai/responses-provider.ts +18 -0
  627. package/src/providers/placeholder-sentinels.ts +18 -8
  628. package/src/providers/retry.ts +1 -0
  629. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
  630. package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
  631. package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
  632. package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
  633. package/src/providers/speech-to-text/provider-catalog.ts +0 -89
  634. package/src/providers/speech-to-text/resolve.ts +55 -0
  635. package/src/providers/tool-progress-events.test.ts +50 -0
  636. package/src/providers/tool-progress-events.ts +99 -0
  637. package/src/providers/types.ts +1 -1
  638. package/src/providers/vellum-model-routing.test.ts +51 -0
  639. package/src/providers/vellum-model-routing.ts +84 -0
  640. package/src/runtime/AGENTS.md +5 -4
  641. package/src/runtime/__tests__/agent-wake.test.ts +158 -1
  642. package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
  643. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
  644. package/src/runtime/access-request-helper.ts +154 -3
  645. package/src/runtime/actor-trust-resolver.ts +7 -21
  646. package/src/runtime/agent-wake.ts +41 -1
  647. package/src/runtime/assistant-event.ts +86 -12
  648. package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
  649. package/src/runtime/channel-approval-types.ts +52 -2
  650. package/src/runtime/channel-approvals.ts +23 -8
  651. package/src/runtime/channel-invite-transport.ts +2 -2
  652. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  653. package/src/runtime/channel-readiness-service.ts +3 -5
  654. package/src/runtime/channel-retry-sweep.ts +18 -24
  655. package/src/runtime/channel-verification-service.ts +9 -11
  656. package/src/runtime/finalize-event-delivery.ts +30 -16
  657. package/src/runtime/guardian-action-service.ts +10 -11
  658. package/src/runtime/guardian-reply-router.ts +122 -21
  659. package/src/runtime/http-server.ts +44 -101
  660. package/src/runtime/introduction-policy.test.ts +214 -0
  661. package/src/runtime/introduction-policy.ts +284 -0
  662. package/src/runtime/invite-service.ts +104 -361
  663. package/src/runtime/local-actor-identity.test.ts +38 -0
  664. package/src/runtime/local-actor-identity.ts +14 -0
  665. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
  666. package/src/runtime/middleware/rate-limiter.ts +30 -0
  667. package/src/runtime/middleware/twilio-validation.ts +5 -7
  668. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
  669. package/src/runtime/migrations/origin-mode.ts +6 -6
  670. package/src/runtime/pre-first-message-gate.ts +1 -0
  671. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
  672. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
  673. package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
  674. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
  675. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
  676. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
  677. package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
  678. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
  679. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
  680. package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
  681. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
  682. package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
  683. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
  684. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
  685. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
  686. package/src/runtime/routes/acp-routes.ts +15 -3
  687. package/src/runtime/routes/app-management-routes.ts +4 -7
  688. package/src/runtime/routes/approval-routes.ts +4 -0
  689. package/src/runtime/routes/attachment-routes.ts +54 -3
  690. package/src/runtime/routes/avatar-routes.ts +0 -4
  691. package/src/runtime/routes/background-tool-routes.ts +34 -5
  692. package/src/runtime/routes/btw-routes.ts +32 -4
  693. package/src/runtime/routes/channel-route-shared.ts +18 -14
  694. package/src/runtime/routes/channel-verification-routes.ts +0 -3
  695. package/src/runtime/routes/contact-routes.ts +298 -117
  696. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  697. package/src/runtime/routes/conversation-management-routes.ts +74 -37
  698. package/src/runtime/routes/conversation-query-routes.ts +5 -2
  699. package/src/runtime/routes/conversation-routes.ts +222 -82
  700. package/src/runtime/routes/conversations-import-routes.ts +8 -1
  701. package/src/runtime/routes/credential-prompt-routes.ts +20 -5
  702. package/src/runtime/routes/debug-routes.ts +1 -0
  703. package/src/runtime/routes/documents-routes.ts +5 -2
  704. package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
  705. package/src/runtime/routes/global-search-routes.ts +2 -1
  706. package/src/runtime/routes/guardian-approval-interception.ts +51 -0
  707. package/src/runtime/routes/home-feed-routes.ts +60 -1
  708. package/src/runtime/routes/identity-routes.ts +4 -87
  709. package/src/runtime/routes/inbound-message-handler.ts +82 -41
  710. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
  711. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
  712. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
  713. package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
  714. package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
  715. package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
  716. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
  717. package/src/runtime/routes/index.ts +6 -0
  718. package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
  719. package/src/runtime/routes/internal-twilio-routes.ts +0 -15
  720. package/src/runtime/routes/messages-lexical-routes.ts +75 -0
  721. package/src/runtime/routes/migration-routes.ts +69 -6
  722. package/src/runtime/routes/monitoring-routes.ts +237 -0
  723. package/src/runtime/routes/plugins-routes.ts +329 -8
  724. package/src/runtime/routes/schedule-routes.ts +49 -8
  725. package/src/runtime/routes/schedule-worker-routes.ts +210 -0
  726. package/src/runtime/routes/secret-routes.ts +1 -1
  727. package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
  728. package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
  729. package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
  730. package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
  731. package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
  732. package/src/runtime/services/analyze-conversation.ts +14 -4
  733. package/src/runtime/services/conversation-serializer.ts +5 -0
  734. package/src/runtime/slack-no-response.ts +47 -0
  735. package/src/runtime/slack-reply-session.test.ts +633 -0
  736. package/src/runtime/slack-reply-session.ts +382 -0
  737. package/src/runtime/slack-task-progress.ts +95 -0
  738. package/src/runtime/sync/resource-sync-events.ts +25 -0
  739. package/src/runtime/tool-grant-request-helper.ts +17 -1
  740. package/src/runtime/trust-class.test.ts +141 -0
  741. package/src/runtime/trust-class.ts +121 -0
  742. package/src/runtime/trust-verdict-consumer.ts +7 -6
  743. package/src/schedule/__tests__/run-script.test.ts +21 -0
  744. package/src/schedule/__tests__/worker-control.test.ts +241 -0
  745. package/src/schedule/run-script.ts +13 -6
  746. package/src/schedule/schedule-usage-store.ts +1 -0
  747. package/src/schedule/scheduler.ts +178 -127
  748. package/src/schedule/worker-control.ts +118 -0
  749. package/src/schedule/worker.ts +131 -0
  750. package/src/skills/__tests__/categories-cache.test.ts +38 -0
  751. package/src/skills/categories-cache.ts +40 -5
  752. package/src/skills/managed-store.ts +41 -6
  753. package/src/skills/validate-input.ts +47 -0
  754. package/src/stt/types.ts +5 -9
  755. package/src/subagent/manager.ts +267 -39
  756. package/src/subagent/types.ts +6 -2
  757. package/src/subagent/validate-allowlists.ts +65 -0
  758. package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
  759. package/src/telemetry/watchdog-direct-emit.ts +89 -0
  760. package/src/tools/acp/list-agents.test.ts +0 -6
  761. package/src/tools/background-tool-registry.ts +61 -0
  762. package/src/tools/document/document-comment-tool.test.ts +4 -0
  763. package/src/tools/host-terminal/host-shell.test.ts +397 -0
  764. package/src/tools/host-terminal/host-shell.ts +203 -14
  765. package/src/tools/skills/find-similar-skills.test.ts +152 -1
  766. package/src/tools/skills/find-similar-skills.ts +30 -4
  767. package/src/tools/skills/load.ts +36 -0
  768. package/src/tools/skills/scaffold-managed.ts +66 -34
  769. package/src/tools/skills/skill-script-runner.ts +17 -0
  770. package/src/tools/skills/skill-tool-factory.ts +5 -1
  771. package/src/tools/terminal/safe-env.ts +3 -0
  772. package/src/tools/terminal/shell.test.ts +177 -0
  773. package/src/tools/terminal/shell.ts +98 -4
  774. package/src/tools/tool-approval-handler.ts +106 -29
  775. package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
  776. package/src/tools/types.ts +29 -17
  777. package/src/tools/ui-surface/definitions.ts +3 -1
  778. package/src/tts/__tests__/provider-adapters.test.ts +37 -0
  779. package/src/tts/__tests__/provider-catalog.test.ts +21 -0
  780. package/src/tts/provider-catalog.ts +40 -9
  781. package/src/tts/providers/fish-audio-provider.ts +19 -10
  782. package/src/tts/types.ts +3 -4
  783. package/src/util/__tests__/process-tree.test.ts +10 -2
  784. package/src/util/__tests__/text-spacing.test.ts +53 -0
  785. package/src/util/cgroup-memory.ts +142 -0
  786. package/src/util/image-conversion.ts +281 -0
  787. package/src/util/phone.ts +1 -8
  788. package/src/util/platform.ts +33 -0
  789. package/src/util/process-tree.ts +10 -2
  790. package/src/util/text-spacing.ts +53 -0
  791. package/src/util/worker-process.ts +271 -0
  792. package/src/workflows/journal-store.ts +16 -1
  793. package/src/workflows/library.test.ts +35 -20
  794. package/src/workflows/library.ts +105 -66
  795. package/src/workspace/git-service.ts +1 -0
  796. package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
  797. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
  798. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
  799. package/src/workspace/migrations/118-seed-now-md.ts +64 -0
  800. package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
  801. package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
  802. package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
  803. package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
  804. package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
  805. package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
  806. package/src/workspace/migrations/registry.ts +12 -0
  807. package/drizzle/0000_dizzy_maggott.sql +0 -301
  808. package/drizzle/meta/0000_snapshot.json +0 -1933
  809. package/drizzle/meta/_journal.json +0 -13
  810. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
  811. package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
  812. package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
  813. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
  814. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
  815. package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
  816. package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
  817. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
  818. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
  819. package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
  820. package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
  821. package/src/__tests__/invite-redemption-service.test.ts +0 -1180
  822. package/src/__tests__/invite-service-ipc.test.ts +0 -350
  823. package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
  824. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
  825. package/src/__tests__/relay-server.test.ts +0 -6153
  826. package/src/__tests__/telephony-stt-routing.test.ts +0 -329
  827. package/src/__tests__/voice-invite-redemption.test.ts +0 -702
  828. package/src/calls/relay-server.ts +0 -2463
  829. package/src/calls/telephony-stt-routing.ts +0 -145
  830. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
  831. package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
  832. package/src/daemon/daemon-skill-host.ts +0 -262
  833. package/src/daemon/message-types/meet.ts +0 -143
  834. package/src/daemon/shutdown-registry.ts +0 -40
  835. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
  836. package/src/ipc/__tests__/skill-server.test.ts +0 -182
  837. package/src/ipc/skill-ipc-types.ts +0 -54
  838. package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
  839. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
  840. package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
  841. package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
  842. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
  843. package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
  844. package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
  845. package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
  846. package/src/ipc/skill-routes/config.ts +0 -47
  847. package/src/ipc/skill-routes/events.ts +0 -147
  848. package/src/ipc/skill-routes/identity.ts +0 -21
  849. package/src/ipc/skill-routes/index.ts +0 -37
  850. package/src/ipc/skill-routes/log.ts +0 -40
  851. package/src/ipc/skill-routes/memory.ts +0 -92
  852. package/src/ipc/skill-routes/platform.ts +0 -39
  853. package/src/ipc/skill-routes/providers.ts +0 -162
  854. package/src/ipc/skill-routes/registries.ts +0 -338
  855. package/src/ipc/skill-server.ts +0 -831
  856. package/src/ipc/skill-socket-path.ts +0 -20
  857. package/src/jobs/register-job-handlers.ts +0 -74
  858. package/src/persistence/invite-store.ts +0 -481
  859. package/src/plugins/job-handler-registry.ts +0 -94
  860. package/src/providers/cache-control.ts +0 -26
  861. package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
  862. package/src/runtime/invite-redemption-service.ts +0 -828
  863. package/src/runtime/invite-redemption-templates.ts +0 -46
  864. package/src/runtime/runtime-mode.ts +0 -40
  865. package/src/runtime/slack-dm-text-delivery.ts +0 -177
  866. package/src/util/voice-code.ts +0 -31
@@ -1,1349 +0,0 @@
1
- /**
2
- * `SkillHostClient` — IPC-backed concretion of the neutral `SkillHost`
3
- * interface. Lets an out-of-process first-party skill consume the daemon's
4
- * host surface over the Unix domain socket exposed by `SkillIpcServer`.
5
- *
6
- * Wire protocol (mirrors `assistant/src/ipc/skill-server.ts`):
7
- *
8
- * skill-initiated RPC (one-shot)
9
- * → { id: "s:<n>", method, params? }
10
- * ← { id: "s:<n>", result } | { id: "s:<n>", error }
11
- *
12
- * daemon-initiated RPC (one-shot, requires registered handler)
13
- * ← { id: "d:<n>", method, params? }
14
- * → { id: "d:<n>", result } | { id: "d:<n>", error }
15
- *
16
- * streaming RPC (e.g. `host.events.subscribe`, skill-initiated only)
17
- * → { id: "s:<n>", method, params? }
18
- * ← { id: "s:<n>", result: { subscribed: true } } (open ack)
19
- * ← { id: "s:<n>", event: "delivery", payload: <data> } (0..N)
20
- * ← { id: "s:<n>", error } (terminal)
21
- * → { id: "s:<n>", method: "host.events.subscribe.close",
22
- * params: { subscribeId: <original-id> } }
23
- * ← { id: "s:<n>", result: { closed: true } }
24
- *
25
- * The `s:` / `d:` id prefixes namespace the two directions so each side can
26
- * route inbound responses to its own pending-request map without collision.
27
- *
28
- * ### Daemon-initiated dispatch handlers
29
- *
30
- * After the skill registers tools / routes / shutdown hooks via
31
- * `registries.registerTools`, `registries.registerSkillRoute`, and
32
- * `registries.registerShutdownHook`, the client also installs local
33
- * handlers for the matching `skill.dispatch_*` methods so the daemon can
34
- * invoke skill-side closures over the bidirectional RPC. The wire shapes
35
- * are:
36
- *
37
- * skill.dispatch_tool
38
- * daemon → skill: { name: string, input: Record<string, unknown>,
39
- * context?: unknown }
40
- * skill → daemon: { result: unknown }
41
- * errors: throws "unknown tool: <name>" when the tool name is not in
42
- * the most recently registered provider's output.
43
- *
44
- * skill.dispatch_route
45
- * daemon → skill: { patternSource: string,
46
- * request: { method: string, url: string,
47
- * headers?: Record<string, string>,
48
- * body?: string } }
49
- * skill → daemon: { status: number,
50
- * headers: Record<string, string>,
51
- * body: string }
52
- * errors: throws "unknown route: <patternSource>" when no registered
53
- * route matches the patternSource, or "url did not match
54
- * pattern: <patternSource>" when the regex fails to match.
55
- *
56
- * skill.shutdown
57
- * daemon → skill: { name?: string, reason?: string }
58
- * skill → daemon: { ok: true }
59
- * semantics: when `name` is set, runs only that hook; otherwise runs
60
- * all registered hooks in reverse-registration order. Per-
61
- * hook errors are swallowed (logged via the host logger if
62
- * `connect()` has populated one).
63
- *
64
- * ### Sync-method bootstrap
65
- *
66
- * The `SkillHost` contract exposes a number of synchronous accessors
67
- * (`identity.getAssistantName()`, `platform.workspaceDir()`,
68
- * `platform.runtimeMode()`, etc.) that naturally cannot round-trip an async
69
- * IPC call on every invocation. `connect()` prefetches the stable subset of
70
- * these values once, caches them locally, and every subsequent sync accessor
71
- * reads from the cache. Skill code MUST await `connect()` before any
72
- * synchronous host accessor fires; calling a sync accessor before connect
73
- * throws a clear "not connected" error.
74
- *
75
- * ### Opaque handle methods
76
- *
77
- * Several provider accessors on `SkillHost` (`providers.llm.getConfigured`,
78
- * `providers.llm.userMessage`, `providers.llm.extractToolUse`,
79
- * `providers.stt.resolveStreamingTranscriber`, `providers.tts.get`,
80
- * `speakers.createTracker`) return opaque handles whose concrete types live
81
- * inside `assistant/`. Across IPC they cannot carry the handle's method
82
- * closures — the skill treats the return value as a black-box token and
83
- * threads it into `host.providers.llm.complete` / future dispatch routes.
84
- * The client implements each as a passthrough that returns a tagged
85
- * descriptor object; the daemon-side handler that ultimately consumes the
86
- * token narrows it back to the concrete type at its boundary.
87
- *
88
- * ### Reconnect
89
- *
90
- * When `autoReconnect` is enabled, a lost socket connection is retried with
91
- * exponential backoff (capped at `reconnectMaxDelayMs`). In-flight requests
92
- * are rejected with a clear error because no response correlation survives
93
- * a socket reset; callers are responsible for retrying at a higher level.
94
- * Long-lived subscriptions are re-opened on reconnect with the same filter
95
- * so skill-side callbacks keep firing once the socket is back.
96
- */
97
-
98
- import { randomUUID } from "node:crypto";
99
- import { connect, type Socket } from "node:net";
100
-
101
- import type { AssistantEvent } from "./assistant-event.js";
102
- import type { DaemonRuntimeMode } from "./runtime-mode.js";
103
- import type { ServerMessage } from "./server-message.js";
104
- import type {
105
- AssistantEventCallback,
106
- ConfigFacet,
107
- EventsFacet,
108
- Filter,
109
- IdentityFacet,
110
- InsertMessageFn,
111
- LlmProvidersFacet,
112
- Logger,
113
- LoggerFacet,
114
- MemoryFacet,
115
- PlatformFacet,
116
- Provider,
117
- ProvidersFacet,
118
- RegistriesFacet,
119
- SecureKeysFacet,
120
- SkillHost,
121
- SkillRoute,
122
- SkillRouteHandle,
123
- SpeakersFacet,
124
- SttProvidersFacet,
125
- Subscription,
126
- ToolUse,
127
- TtsConfig,
128
- TtsProvider,
129
- TtsProvidersFacet,
130
- UserMessage,
131
- } from "./skill-host.js";
132
- import type { Tool, ToolContext } from "./tool-types.js";
133
-
134
- // ---------------------------------------------------------------------------
135
- // Constants
136
- // ---------------------------------------------------------------------------
137
-
138
- const SUBSCRIBE_CLOSE_METHOD = "host.events.subscribe.close" as const;
139
- const DEFAULT_CALL_TIMEOUT_MS = 30_000;
140
- const DEFAULT_CONNECT_TIMEOUT_MS = 3_000;
141
- const DEFAULT_RECONNECT_BASE_DELAY_MS = 200;
142
- const DEFAULT_RECONNECT_MAX_DELAY_MS = 10_000;
143
-
144
- /** Prefix for ids minted by the daemon (server) side. */
145
- const DAEMON_ID_PREFIX = "d:" as const;
146
- /** Prefix for ids minted by the skill (client) side. */
147
- const SKILL_ID_PREFIX = "s:" as const;
148
-
149
- // ---------------------------------------------------------------------------
150
- // Wire-format types
151
- // ---------------------------------------------------------------------------
152
-
153
- type IpcRequest = {
154
- id: string;
155
- method: string;
156
- params?: Record<string, unknown>;
157
- };
158
-
159
- type IpcResponseFrame = {
160
- id: string;
161
- method?: string;
162
- params?: unknown;
163
- result?: unknown;
164
- error?: string;
165
- event?: "delivery";
166
- payload?: unknown;
167
- };
168
-
169
- /**
170
- * Handler for a daemon-initiated request. Returning a value (or a Promise)
171
- * resolves the daemon's `sendRequest` call; throwing rejects it with the
172
- * thrown error's message. Synchronous returns are wrapped automatically.
173
- */
174
- export type SkillHostRequestHandler = (
175
- params: unknown,
176
- ) => unknown | Promise<unknown>;
177
-
178
- // ---------------------------------------------------------------------------
179
- // Public options
180
- // ---------------------------------------------------------------------------
181
-
182
- export interface SkillHostClientOptions {
183
- /** Absolute path to the `assistant-skill.sock` Unix domain socket. */
184
- socketPath: string;
185
- /**
186
- * Identifier for the owning skill. Sent as the default logger-scope name
187
- * when `logger.get(name)` is not explicitly scoped, and reserved for
188
- * future per-skill routing at the daemon boundary.
189
- */
190
- skillId: string;
191
- /**
192
- * Automatically reconnect the underlying socket when it drops. Existing
193
- * subscriptions are reopened with the same filter; in-flight one-shot
194
- * requests are rejected with a "connection lost" error.
195
- *
196
- * @default false
197
- */
198
- autoReconnect?: boolean;
199
- /** Initial retry delay (ms). Exponentially backs off to the max. */
200
- reconnectBaseDelayMs?: number;
201
- /** Maximum retry delay (ms). */
202
- reconnectMaxDelayMs?: number;
203
- /** Per-call timeout for one-shot RPCs. */
204
- callTimeoutMs?: number;
205
- /** Socket `connect()` timeout. */
206
- connectTimeoutMs?: number;
207
- }
208
-
209
- // ---------------------------------------------------------------------------
210
- // Internal state for pending calls and subscriptions
211
- // ---------------------------------------------------------------------------
212
-
213
- interface PendingCall {
214
- resolve: (value: unknown) => void;
215
- reject: (err: Error) => void;
216
- timer: ReturnType<typeof setTimeout>;
217
- }
218
-
219
- interface ActiveSubscription {
220
- id: string;
221
- filter: Filter;
222
- callback: AssistantEventCallback;
223
- disposed: boolean;
224
- }
225
-
226
- // ---------------------------------------------------------------------------
227
- // Helpers
228
- // ---------------------------------------------------------------------------
229
-
230
- function notConnected(): Error {
231
- return new Error(
232
- "SkillHostClient: not connected. Call `await client.connect()` before using synchronous host accessors.",
233
- );
234
- }
235
-
236
- /**
237
- * Wraps a sync logger call so a host.log RPC failure never throws at the
238
- * call site — skills treat logging as side-effectful and don't want a
239
- * transient socket issue to abort whatever they were doing.
240
- */
241
- function swallow(err: unknown): void {
242
- // Intentional no-op; logging here would recurse into the same broken
243
- // logger. The stderr path is a deliberate last-resort sink.
244
- if (err && process.env.SKILL_HOST_CLIENT_DEBUG) {
245
- // eslint-disable-next-line no-console
246
- console.error("[SkillHostClient] log RPC failed:", err);
247
- }
248
- }
249
-
250
- /**
251
- * Stringify an unknown error value for the wire — `Error.message` when
252
- * available, otherwise `String(err)` so non-Error throws still surface
253
- * something readable on the daemon side.
254
- */
255
- function errorMessage(err: unknown): string {
256
- if (err instanceof Error) return err.message;
257
- return String(err);
258
- }
259
-
260
- // ---------------------------------------------------------------------------
261
- // Client implementation
262
- // ---------------------------------------------------------------------------
263
-
264
- export class SkillHostClient implements SkillHost {
265
- // Facets are populated in the constructor so every `SkillHost` method
266
- // has a concrete target even before `connect()` resolves. Sync methods
267
- // that depend on prefetched state throw `notConnected()` until then.
268
- readonly logger: LoggerFacet;
269
- readonly config: ConfigFacet;
270
- readonly identity: IdentityFacet;
271
- readonly platform: PlatformFacet;
272
- readonly providers: ProvidersFacet;
273
- readonly memory: MemoryFacet;
274
- readonly events: EventsFacet;
275
- readonly registries: RegistriesFacet;
276
- readonly speakers: SpeakersFacet;
277
-
278
- private readonly options: Required<
279
- Pick<
280
- SkillHostClientOptions,
281
- | "socketPath"
282
- | "skillId"
283
- | "callTimeoutMs"
284
- | "connectTimeoutMs"
285
- | "reconnectBaseDelayMs"
286
- | "reconnectMaxDelayMs"
287
- >
288
- > & { autoReconnect: boolean };
289
-
290
- private socket: Socket | null = null;
291
- private buffer = "";
292
- private readonly pending = new Map<string, PendingCall>();
293
- private readonly subscriptions = new Map<string, ActiveSubscription>();
294
- private connectingPromise: Promise<void> | null = null;
295
- private closed = false;
296
- private reconnectAttempt = 0;
297
- /**
298
- * Monotonic counter for skill-initiated request ids, formatted as
299
- * `s:<n>`. The daemon mints `d:<n>` ids independently, so the two
300
- * sequences never collide on a shared socket.
301
- */
302
- private nextSkillRequestSeq = 1;
303
- /**
304
- * Handlers for daemon-initiated requests, keyed by method name. Populated
305
- * via `registerHandler(method, handler)`. Daemon→skill request frames
306
- * (id starts with `d:`) are dispatched through this table.
307
- */
308
- private readonly daemonRequestHandlers = new Map<
309
- string,
310
- SkillHostRequestHandler
311
- >();
312
-
313
- // Prefetched sync state — populated by `connect()`.
314
- private cachedAssistantName: string | undefined = undefined;
315
- private cachedPrefetchDone = false;
316
- private cachedWorkspaceDir: string | null = null;
317
- private cachedVellumRoot: string | null = null;
318
- private cachedRuntimeMode: DaemonRuntimeMode | null = null;
319
-
320
- // ── Local dispatch state ────────────────────────────────────────────────
321
- // Caches populated by `registries.register*` so the daemon can dispatch
322
- // skill-owned closures back over the bidirectional RPC. Mirrors what the
323
- // out-of-process skill installed in-process before the IPC split.
324
-
325
- /** Most recently registered tool provider (last writer wins, matching the in-process semantics where a single skill module owns one provider). */
326
- private cachedToolsProvider: (() => Tool[]) | null = null;
327
- /**
328
- * Routes keyed by `pattern.source`. Last writer wins on collision so
329
- * re-registering the same regex source replaces the prior handler — mirrors
330
- * how an in-process skill would re-`registerSkillRoute` on hot reload.
331
- */
332
- private readonly cachedRoutes = new Map<string, SkillRoute>();
333
- /**
334
- * Shutdown hooks ordered by registration time. We use an array (not a Map
335
- * keyed by name) because the spec requires running hooks in reverse-
336
- * registration order when no name is provided. Re-registering an existing
337
- * name replaces the prior entry in place to keep the cache idempotent.
338
- */
339
- private readonly cachedShutdownHooks: Array<{
340
- name: string;
341
- hook: (reason: string) => Promise<void>;
342
- }> = [];
343
-
344
- constructor(options: SkillHostClientOptions) {
345
- this.options = {
346
- socketPath: options.socketPath,
347
- skillId: options.skillId,
348
- autoReconnect: options.autoReconnect ?? false,
349
- callTimeoutMs: options.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS,
350
- connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
351
- reconnectBaseDelayMs:
352
- options.reconnectBaseDelayMs ?? DEFAULT_RECONNECT_BASE_DELAY_MS,
353
- reconnectMaxDelayMs:
354
- options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS,
355
- };
356
-
357
- this.logger = this.buildLoggerFacet();
358
- this.config = this.buildConfigFacet();
359
- this.identity = this.buildIdentityFacet();
360
- this.platform = this.buildPlatformFacet();
361
- this.providers = this.buildProvidersFacet();
362
- this.memory = this.buildMemoryFacet();
363
- this.events = this.buildEventsFacet();
364
- this.registries = this.buildRegistriesFacet();
365
- this.speakers = this.buildSpeakersFacet();
366
- }
367
-
368
- // ── Public lifecycle ────────────────────────────────────────────────────
369
-
370
- /**
371
- * Connect to the skill IPC socket and prefetch sync-cacheable state
372
- * (assistant id, workspace dir, vellum root, runtime mode, assistant
373
- * name). Safe to call multiple times — the first call initiates the
374
- * connection, concurrent calls await the same promise.
375
- */
376
- async connect(): Promise<void> {
377
- if (this.closed) {
378
- throw new Error("SkillHostClient: cannot connect after close()");
379
- }
380
- if (this.connectingPromise) return this.connectingPromise;
381
- // Fully connected: live socket *and* prefetch already populated.
382
- // If the socket survived but a prior `prefetchSyncState()` rejected,
383
- // the caches are still null and sync accessors would throw — fall
384
- // through and re-run prefetch over the existing socket.
385
- const socketAlive = !!this.socket && !this.socket.destroyed;
386
- const prefetchDone = this.cachedPrefetchDone;
387
- if (socketAlive && prefetchDone) return;
388
-
389
- const ensureSocket = socketAlive ? Promise.resolve() : this.doConnect();
390
- this.connectingPromise = ensureSocket
391
- .then(async () => {
392
- await this.prefetchSyncState();
393
- })
394
- .finally(() => {
395
- this.connectingPromise = null;
396
- });
397
- return this.connectingPromise;
398
- }
399
-
400
- /**
401
- * Close the socket, reject outstanding calls, and dispose all active
402
- * subscriptions. Safe to call multiple times.
403
- */
404
- close(): void {
405
- if (this.closed) return;
406
- this.closed = true;
407
- // Mark every subscription disposed so stray deliveries during teardown
408
- // don't fire user callbacks.
409
- for (const sub of this.subscriptions.values()) {
410
- sub.disposed = true;
411
- }
412
- this.subscriptions.clear();
413
- // Reject any in-flight calls.
414
- const closeErr = new Error(
415
- "SkillHostClient: client closed while request was in flight",
416
- );
417
- for (const { reject, timer } of this.pending.values()) {
418
- clearTimeout(timer);
419
- reject(closeErr);
420
- }
421
- this.pending.clear();
422
- if (this.socket && !this.socket.destroyed) {
423
- this.socket.destroy();
424
- }
425
- this.socket = null;
426
- }
427
-
428
- /**
429
- * Install a handler for daemon-initiated requests of the given method.
430
- * The daemon's `SkillIpcServer.sendRequest(connection, method, ...)`
431
- * resolves with whatever the handler returns (or rejects with the
432
- * handler's thrown error). Re-registering a method replaces the prior
433
- * handler — last writer wins.
434
- */
435
- registerHandler(method: string, handler: SkillHostRequestHandler): void {
436
- this.daemonRequestHandlers.set(method, handler);
437
- }
438
-
439
- // ── Internal: socket lifecycle ──────────────────────────────────────────
440
-
441
- private async doConnect(): Promise<void> {
442
- const { socketPath, connectTimeoutMs } = this.options;
443
- return new Promise<void>((resolve, reject) => {
444
- const socket = connect(socketPath);
445
- let settled = false;
446
-
447
- const timer = setTimeout(() => {
448
- if (settled) return;
449
- settled = true;
450
- socket.destroy();
451
- reject(
452
- new Error(
453
- `SkillHostClient: connect timed out after ${connectTimeoutMs}ms (${socketPath})`,
454
- ),
455
- );
456
- }, connectTimeoutMs);
457
-
458
- socket.once("connect", () => {
459
- if (settled) return;
460
- settled = true;
461
- clearTimeout(timer);
462
- // The client may have been `close()`d while `connect()` was
463
- // still pending. If we attach now we'd reintroduce a live
464
- // socket on a closed client and leak the server-side connection
465
- // until process teardown.
466
- if (this.closed) {
467
- socket.destroy();
468
- reject(new Error("SkillHostClient: closed during connect"));
469
- return;
470
- }
471
- this.attachSocket(socket);
472
- resolve();
473
- });
474
-
475
- socket.once("error", (err) => {
476
- if (settled) return;
477
- settled = true;
478
- clearTimeout(timer);
479
- reject(
480
- new Error(
481
- `SkillHostClient: socket error during connect: ${err.message}`,
482
- ),
483
- );
484
- });
485
- });
486
- }
487
-
488
- private attachSocket(socket: Socket): void {
489
- this.socket = socket;
490
- this.buffer = "";
491
- this.reconnectAttempt = 0;
492
-
493
- socket.on("data", (chunk) => {
494
- this.buffer += chunk.toString();
495
- let newlineIdx: number;
496
- while ((newlineIdx = this.buffer.indexOf("\n")) !== -1) {
497
- const line = this.buffer.slice(0, newlineIdx).trim();
498
- this.buffer = this.buffer.slice(newlineIdx + 1);
499
- if (line) this.handleFrame(line);
500
- }
501
- });
502
-
503
- socket.on("close", () => {
504
- this.socket = null;
505
- const err = new Error("SkillHostClient: socket closed before response");
506
- for (const { reject, timer } of this.pending.values()) {
507
- clearTimeout(timer);
508
- reject(err);
509
- }
510
- this.pending.clear();
511
- if (!this.closed && this.options.autoReconnect) {
512
- void this.scheduleReconnect();
513
- }
514
- });
515
-
516
- socket.on("error", (err) => {
517
- swallow(err);
518
- });
519
- }
520
-
521
- private async scheduleReconnect(): Promise<void> {
522
- if (this.closed) return;
523
- this.reconnectAttempt += 1;
524
- const delay = Math.min(
525
- this.options.reconnectBaseDelayMs *
526
- Math.pow(2, this.reconnectAttempt - 1),
527
- this.options.reconnectMaxDelayMs,
528
- );
529
- await new Promise((resolve) => setTimeout(resolve, delay));
530
- if (this.closed) return;
531
- try {
532
- await this.doConnect();
533
- // Re-open every live subscription with a fresh request so the
534
- // server-side hub installs a new callback.
535
- const live = [...this.subscriptions.values()].filter((s) => !s.disposed);
536
- this.subscriptions.clear();
537
- for (const sub of live) {
538
- this.reopenSubscription(sub);
539
- }
540
- } catch (err) {
541
- swallow(err);
542
- if (!this.closed) {
543
- void this.scheduleReconnect();
544
- }
545
- }
546
- }
547
-
548
- private reopenSubscription(prev: ActiveSubscription): void {
549
- // Same id so the application-visible Subscription handle still works.
550
- const fresh: ActiveSubscription = {
551
- id: prev.id,
552
- filter: prev.filter,
553
- callback: prev.callback,
554
- disposed: false,
555
- };
556
- this.subscriptions.set(fresh.id, fresh);
557
- // Mirror `openSubscription`: pre-register a pending entry so the
558
- // server's `{ id, result: { subscribed: true } }` ack frame is
559
- // matched (otherwise `handleFrame` silently drops it) and so we
560
- // tear the subscription down on ack timeout instead of leaking it.
561
- this.registerSubscribeAck(fresh);
562
- try {
563
- this.writeFrame({
564
- id: fresh.id,
565
- method: "host.events.subscribe",
566
- params: { filter: fresh.filter },
567
- });
568
- } catch (err) {
569
- // Drop only the pending ack and leave `fresh` registered. If the
570
- // socket dropped between `doConnect()` and this write, the close
571
- // handler will trigger another reconnect — and that cycle iterates
572
- // `subscriptions`, so we must NOT remove or dispose `fresh` here
573
- // or the stream is silently lost (autoReconnect contract).
574
- const entry = this.pending.get(fresh.id);
575
- if (entry) {
576
- clearTimeout(entry.timer);
577
- this.pending.delete(fresh.id);
578
- }
579
- swallow(err);
580
- }
581
- }
582
-
583
- private registerSubscribeAck(active: ActiveSubscription): void {
584
- const ackTimer = setTimeout(() => {
585
- if (this.pending.delete(active.id)) {
586
- active.disposed = true;
587
- this.subscriptions.delete(active.id);
588
- }
589
- }, this.options.callTimeoutMs);
590
- this.pending.set(active.id, {
591
- resolve: () => {
592
- clearTimeout(ackTimer);
593
- },
594
- reject: (err) => {
595
- clearTimeout(ackTimer);
596
- active.disposed = true;
597
- this.subscriptions.delete(active.id);
598
- swallow(err);
599
- },
600
- timer: ackTimer,
601
- });
602
- }
603
-
604
- private cancelSubscribeAck(active: ActiveSubscription): void {
605
- const entry = this.pending.get(active.id);
606
- if (entry) {
607
- clearTimeout(entry.timer);
608
- this.pending.delete(active.id);
609
- }
610
- active.disposed = true;
611
- this.subscriptions.delete(active.id);
612
- }
613
-
614
- // ── Internal: frame I/O ─────────────────────────────────────────────────
615
-
616
- private handleFrame(line: string): void {
617
- let frame: IpcResponseFrame;
618
- try {
619
- frame = JSON.parse(line) as IpcResponseFrame;
620
- } catch (err) {
621
- swallow(err);
622
- return;
623
- }
624
-
625
- if (
626
- !frame ||
627
- typeof frame !== "object" ||
628
- Array.isArray(frame) ||
629
- typeof frame.id !== "string"
630
- ) {
631
- return;
632
- }
633
-
634
- // Delivery frames route into the subscription callback.
635
- if (frame.event === "delivery") {
636
- const sub = this.subscriptions.get(frame.id);
637
- if (sub && !sub.disposed) {
638
- try {
639
- const r = sub.callback(frame.payload as AssistantEvent);
640
- if (r instanceof Promise) r.catch(swallow);
641
- } catch (err) {
642
- swallow(err);
643
- }
644
- }
645
- return;
646
- }
647
-
648
- // Daemon-initiated request frame — dispatch to a registered handler
649
- // and write back the response. Identified by the `d:` id prefix and
650
- // the presence of a `method` field.
651
- if (
652
- typeof frame.method === "string" &&
653
- frame.id.startsWith(DAEMON_ID_PREFIX)
654
- ) {
655
- this.dispatchDaemonRequest(frame.id, frame.method, frame.params);
656
- return;
657
- }
658
-
659
- // Response frame — resolve or reject the pending call.
660
- const pending = this.pending.get(frame.id);
661
- if (pending) {
662
- this.pending.delete(frame.id);
663
- clearTimeout(pending.timer);
664
- if (frame.error !== undefined) {
665
- pending.reject(
666
- new Error(`SkillHostClient: remote error: ${frame.error}`),
667
- );
668
- } else {
669
- pending.resolve(frame.result);
670
- }
671
- return;
672
- }
673
-
674
- // No pending entry — could be a terminal error on a subscription.
675
- if (frame.error !== undefined) {
676
- const sub = this.subscriptions.get(frame.id);
677
- if (sub) {
678
- sub.disposed = true;
679
- this.subscriptions.delete(frame.id);
680
- }
681
- }
682
- }
683
-
684
- private dispatchDaemonRequest(
685
- id: string,
686
- method: string,
687
- params: unknown,
688
- ): void {
689
- const handler = this.daemonRequestHandlers.get(method);
690
- if (!handler) {
691
- this.writeResponseFrame({
692
- id,
693
- error: `method not found: ${method}`,
694
- });
695
- return;
696
- }
697
- let result: unknown;
698
- try {
699
- result = handler(params);
700
- } catch (err) {
701
- this.writeResponseFrame({ id, error: errorMessage(err) });
702
- return;
703
- }
704
- if (result instanceof Promise) {
705
- result.then(
706
- (value) => {
707
- this.writeResponseFrame({ id, result: value });
708
- },
709
- (err) => {
710
- this.writeResponseFrame({ id, error: errorMessage(err) });
711
- },
712
- );
713
- } else {
714
- this.writeResponseFrame({ id, result });
715
- }
716
- }
717
-
718
- private writeResponseFrame(response: {
719
- id: string;
720
- result?: unknown;
721
- error?: string;
722
- }): void {
723
- if (!this.socket || this.socket.destroyed) {
724
- // The peer is gone; silently drop. The daemon-side pending entry
725
- // will already have been rejected by the connection-close path.
726
- return;
727
- }
728
- this.socket.write(JSON.stringify(response) + "\n");
729
- }
730
-
731
- private writeFrame(req: IpcRequest): void {
732
- if (!this.socket || this.socket.destroyed) {
733
- throw new Error("SkillHostClient: not connected");
734
- }
735
- this.socket.write(JSON.stringify(req) + "\n");
736
- }
737
-
738
- /** Allocate the next `s:<n>` id for a skill-initiated request. */
739
- private nextSkillRequestId(): string {
740
- return `${SKILL_ID_PREFIX}${this.nextSkillRequestSeq++}`;
741
- }
742
-
743
- private async call<T>(
744
- method: string,
745
- params?: Record<string, unknown>,
746
- ): Promise<T> {
747
- if (this.closed) {
748
- throw new Error("SkillHostClient: client is closed");
749
- }
750
- if (!this.socket || this.socket.destroyed) {
751
- throw new Error(
752
- "SkillHostClient: not connected. Call `await client.connect()` first.",
753
- );
754
- }
755
- const id = this.nextSkillRequestId();
756
- return new Promise<T>((resolve, reject) => {
757
- const timer = setTimeout(() => {
758
- if (this.pending.delete(id)) {
759
- reject(
760
- new Error(
761
- `SkillHostClient: call '${method}' timed out after ${this.options.callTimeoutMs}ms`,
762
- ),
763
- );
764
- }
765
- }, this.options.callTimeoutMs);
766
- this.pending.set(id, {
767
- resolve: (v) => resolve(v as T),
768
- reject,
769
- timer,
770
- });
771
- try {
772
- this.writeFrame({ id, method, params });
773
- } catch (err) {
774
- this.pending.delete(id);
775
- clearTimeout(timer);
776
- reject(err as Error);
777
- }
778
- });
779
- }
780
-
781
- // ── Internal: bootstrap cache ───────────────────────────────────────────
782
-
783
- private async prefetchSyncState(): Promise<void> {
784
- const [workspaceDir, vellumRootValue, runtimeMode, name] =
785
- await Promise.all([
786
- this.call<string>("host.platform.workspaceDir"),
787
- this.call<string>("host.platform.vellumRoot"),
788
- this.call<DaemonRuntimeMode>("host.platform.runtimeMode"),
789
- this.call<string | null>("host.identity.getAssistantName"),
790
- ]);
791
- this.cachedPrefetchDone = true;
792
- this.cachedWorkspaceDir = workspaceDir;
793
- this.cachedVellumRoot = vellumRootValue;
794
- this.cachedRuntimeMode = runtimeMode;
795
- this.cachedAssistantName = name ?? undefined;
796
- }
797
-
798
- // ── Facet builders ──────────────────────────────────────────────────────
799
-
800
- private buildLogger(name: string): Logger {
801
- const scope = name || this.options.skillId;
802
- const write = (
803
- level: "debug" | "info" | "warn" | "error",
804
- msg: string,
805
- meta?: unknown,
806
- ) => {
807
- // Fire-and-forget: skills expect logging to be non-blocking and
808
- // infallible. If the socket is down we just drop the line.
809
- this.call("host.log", { level, msg, name: scope, meta }).catch(swallow);
810
- };
811
- return {
812
- debug: (msg, meta) => write("debug", msg, meta),
813
- info: (msg, meta) => write("info", msg, meta),
814
- warn: (msg, meta) => write("warn", msg, meta),
815
- error: (msg, meta) => write("error", msg, meta),
816
- };
817
- }
818
-
819
- private buildLoggerFacet(): LoggerFacet {
820
- return {
821
- get: (name) => this.buildLogger(name),
822
- };
823
- }
824
-
825
- private buildConfigFacet(): ConfigFacet {
826
- return {
827
- // `isFeatureFlagEnabled` and `getSection` are typed as sync on the
828
- // contract but require a round-trip to resolve. We cannot block on
829
- // async I/O inside a sync accessor, so the client surfaces the
830
- // async semantics by returning a stale-safe value if one has been
831
- // cached via `prefetchFlag` / `prefetchSection` helpers (future
832
- // work) — for now, these throw a clear error so skill code that
833
- // ever reaches them on the client path is audible instead of
834
- // silently returning a wrong value. Async callers should use the
835
- // underlying IPC method names directly via `rawCall`.
836
- isFeatureFlagEnabled: (_key: string): boolean => {
837
- throw new Error(
838
- "SkillHostClient.config.isFeatureFlagEnabled: synchronous feature-flag reads are not supported over IPC. Use `client.rawCall('host.config.isFeatureFlagEnabled', { key })` and await the result.",
839
- );
840
- },
841
- getSection: <T>(_path: string): T | undefined => {
842
- throw new Error(
843
- "SkillHostClient.config.getSection: synchronous config reads are not supported over IPC. Use `client.rawCall('host.config.getSection', { path })` and await the result.",
844
- );
845
- },
846
- };
847
- }
848
-
849
- private buildIdentityFacet(): IdentityFacet {
850
- const self = this;
851
- return {
852
- getAssistantName: () => {
853
- if (!self.cachedPrefetchDone) throw notConnected();
854
- return self.cachedAssistantName;
855
- },
856
- };
857
- }
858
-
859
- private buildPlatformFacet(): PlatformFacet {
860
- return {
861
- workspaceDir: () => {
862
- if (this.cachedWorkspaceDir === null) throw notConnected();
863
- return this.cachedWorkspaceDir;
864
- },
865
- vellumRoot: () => {
866
- if (this.cachedVellumRoot === null) throw notConnected();
867
- return this.cachedVellumRoot;
868
- },
869
- runtimeMode: () => {
870
- if (this.cachedRuntimeMode === null) throw notConnected();
871
- return this.cachedRuntimeMode;
872
- },
873
- };
874
- }
875
-
876
- private buildLlmProvidersFacet(): LlmProvidersFacet {
877
- // The provider, user-message, and tool-use values are opaque tokens on
878
- // the contract; the client synthesizes structurally inert descriptors
879
- // that round-trip through future dispatch routes.
880
- return {
881
- getConfigured: async (callSite: string): Promise<Provider | null> =>
882
- ({
883
- __vellumSkillHostClientHandle: "llm-provider",
884
- callSite,
885
- }) as unknown as Provider,
886
- userMessage: (text: string): UserMessage =>
887
- ({
888
- __vellumSkillHostClientHandle: "user-message",
889
- text,
890
- }) as unknown as UserMessage,
891
- extractToolUse: (_response: unknown): ToolUse | null => {
892
- // The client cannot inspect daemon-shaped completion responses
893
- // without pulling in the Anthropic SDK types; skills that need
894
- // typed tool-use extraction should do it via the completion's
895
- // `content` array directly. Return null as the conservative
896
- // "no tool_use" answer.
897
- return null;
898
- },
899
- createTimeout: (ms: number) => {
900
- const controller = new AbortController();
901
- const timer = setTimeout(() => controller.abort(), ms);
902
- return {
903
- signal: controller.signal,
904
- cleanup: () => clearTimeout(timer),
905
- };
906
- },
907
- };
908
- }
909
-
910
- private buildSttProvidersFacet(): SttProvidersFacet {
911
- // stt sub-facet exposes two pure-data queries and one opaque-handle
912
- // builder. The data queries would require async fetches; we expose
913
- // them synchronously via the same "call rawCall" pattern the config
914
- // facet uses.
915
- return {
916
- listProviderIds: (): string[] => {
917
- throw new Error(
918
- "SkillHostClient.providers.stt.listProviderIds: use `client.rawCall('host.providers.stt.listProviderIds')` and await the result.",
919
- );
920
- },
921
- supportsBoundary: (_id: string): boolean => {
922
- throw new Error(
923
- "SkillHostClient.providers.stt.supportsBoundary: use `client.rawCall('host.providers.stt.supportsBoundary', { id, boundary: 'daemon-streaming' })` and await the result.",
924
- );
925
- },
926
- resolveStreamingTranscriber: async (spec: unknown) =>
927
- ({
928
- __vellumSkillHostClientHandle: "streaming-transcriber",
929
- spec,
930
- }) as unknown,
931
- };
932
- }
933
-
934
- private buildTtsProvidersFacet(): TtsProvidersFacet {
935
- return {
936
- get: (id: string): TtsProvider =>
937
- ({
938
- __vellumSkillHostClientHandle: "tts-provider",
939
- id,
940
- }) as unknown as TtsProvider,
941
- resolveConfig: (): TtsConfig => {
942
- throw new Error(
943
- "SkillHostClient.providers.tts.resolveConfig: use `client.rawCall('host.providers.tts.resolveConfig')` and await the result.",
944
- );
945
- },
946
- };
947
- }
948
-
949
- private buildSecureKeysFacet(): SecureKeysFacet {
950
- return {
951
- getProviderKey: async (id: string): Promise<string | null> =>
952
- this.call<string | null>("host.providers.secureKeys.getProviderKey", {
953
- id,
954
- }),
955
- };
956
- }
957
-
958
- private buildProvidersFacet(): ProvidersFacet {
959
- return {
960
- llm: this.buildLlmProvidersFacet(),
961
- stt: this.buildSttProvidersFacet(),
962
- tts: this.buildTtsProvidersFacet(),
963
- secureKeys: this.buildSecureKeysFacet(),
964
- };
965
- }
966
-
967
- private buildMemoryFacet(): MemoryFacet {
968
- const addMessage: InsertMessageFn = async (
969
- conversationId,
970
- role,
971
- content,
972
- options,
973
- ) =>
974
- this.call("host.memory.addMessage", {
975
- conversationId,
976
- role,
977
- content,
978
- metadata: options?.metadata,
979
- skipIndexing: options?.skipIndexing,
980
- });
981
-
982
- return {
983
- addMessage,
984
- wakeAgentForOpportunity: async (req) => {
985
- // The contract types `req` as opaque; the daemon route validates
986
- // the concrete `{ conversationId, hint, source }` shape.
987
- await this.call("host.memory.wakeAgentForOpportunity", {
988
- ...(req as Record<string, unknown>),
989
- });
990
- },
991
- };
992
- }
993
-
994
- private buildEventsFacet(): EventsFacet {
995
- return {
996
- publish: async (event) => {
997
- await this.call("host.events.publish", { event });
998
- },
999
- subscribe: (filter, cb) => this.openSubscription(filter, cb),
1000
- buildEvent: (message: ServerMessage, conversationId?: string) => {
1001
- // `buildEvent` is typed as sync on the contract (the daemon
1002
- // allocates a uuid + timestamp and returns the envelope). A sync
1003
- // round-trip isn't possible, so the client produces an envelope
1004
- // locally using the standard uuid / timestamp sources. This matches
1005
- // the observable shape of the daemon's `buildAssistantEvent` without
1006
- // the round-trip.
1007
- if (!this.cachedPrefetchDone) throw notConnected();
1008
- return {
1009
- id: randomUUID(),
1010
- conversationId,
1011
- emittedAt: new Date().toISOString(),
1012
- message,
1013
- };
1014
- },
1015
- };
1016
- }
1017
-
1018
- private openSubscription(
1019
- filter: Filter,
1020
- callback: AssistantEventCallback,
1021
- ): Subscription {
1022
- const id = this.nextSkillRequestId();
1023
- const active: ActiveSubscription = {
1024
- id,
1025
- filter,
1026
- callback,
1027
- disposed: false,
1028
- };
1029
- this.subscriptions.set(id, active);
1030
- // Pre-register a pending call for the open ack. The server writes a
1031
- // `{ id, result: { subscribed: true } }` frame back; subsequent
1032
- // `delivery` frames share the same id.
1033
- this.registerSubscribeAck(active);
1034
- try {
1035
- this.writeFrame({
1036
- id,
1037
- method: "host.events.subscribe",
1038
- params: { filter },
1039
- });
1040
- } catch (err) {
1041
- this.cancelSubscribeAck(active);
1042
- throw err;
1043
- }
1044
-
1045
- const self = this;
1046
- return {
1047
- get active() {
1048
- return !active.disposed;
1049
- },
1050
- dispose: () => {
1051
- if (active.disposed) return;
1052
- active.disposed = true;
1053
- self.subscriptions.delete(id);
1054
- // Fire-and-forget close RPC — we don't await the ack because the
1055
- // server also tears down on socket close, which is the fallback.
1056
- if (self.socket && !self.socket.destroyed) {
1057
- self.call(SUBSCRIBE_CLOSE_METHOD, { subscribeId: id }).catch(swallow);
1058
- }
1059
- },
1060
- };
1061
- }
1062
-
1063
- private buildRegistriesFacet(): RegistriesFacet {
1064
- return {
1065
- registerTools: (provider) => {
1066
- // Invoke the provider synchronously so a failure blows up at the
1067
- // registration call site (matching the in-process semantics)
1068
- // rather than silently dropping the tools into the RPC.
1069
- const tools: Tool[] = provider();
1070
- const manifests = tools.map((t) => {
1071
- const def = t.getDefinition();
1072
- return {
1073
- name: t.name,
1074
- description: t.description,
1075
- input_schema: def.input_schema,
1076
- defaultRiskLevel: t.defaultRiskLevel,
1077
- category: t.category,
1078
- executionTarget: t.executionTarget,
1079
- };
1080
- });
1081
- // Cache the provider so `skill.dispatch_tool` can resolve a tool
1082
- // name back to its `execute` closure. Last writer wins.
1083
- this.cachedToolsProvider = provider;
1084
- this.ensureDaemonHandler(
1085
- "skill.dispatch_tool",
1086
- this.dispatchTool.bind(this),
1087
- );
1088
- // Skill ownership travels at the params level (not per-tool) — the
1089
- // daemon-side registry stamps the owner from `skillId` into its
1090
- // own `ownersByName` map keyed by tool name. The `Tool` objects on
1091
- // the wire stay free of owner metadata so callers cannot spoof
1092
- // ownership by forging a field on the manifest.
1093
- //
1094
- // Fire-and-forget; registration failures surface in the daemon log.
1095
- this.call("host.registries.register_tools", {
1096
- skillId: this.options.skillId,
1097
- tools: manifests,
1098
- }).catch(swallow);
1099
- },
1100
- registerSkillRoute: (route: SkillRoute): SkillRouteHandle => {
1101
- // Cache the route by its regex source — `skill.dispatch_route` uses
1102
- // the same key to find the handler closure. Re-registering the same
1103
- // source replaces the prior route, matching in-process hot-reload.
1104
- this.cachedRoutes.set(route.pattern.source, route);
1105
- this.ensureDaemonHandler(
1106
- "skill.dispatch_route",
1107
- this.dispatchRoute.bind(this),
1108
- );
1109
- // The `handler` closure cannot cross IPC; the daemon side installs
1110
- // a proxy that dispatches back over `skill.dispatch_route` (PR D).
1111
- // `patternFlags` ships separately so `i/m/g/s/u/y` survive the
1112
- // RegExp → string round-trip — `new RegExp(source)` alone drops them.
1113
- this.call("host.registries.register_skill_route", {
1114
- patternSource: route.pattern.source,
1115
- patternFlags: route.pattern.flags,
1116
- methods: route.methods,
1117
- }).catch(swallow);
1118
- // The contract models the handle as a branded opaque object — we
1119
- // return a structurally inert placeholder.
1120
- return {} as SkillRouteHandle;
1121
- },
1122
- registerShutdownHook: (name: string, hook) => {
1123
- // Cache the hook so `skill.shutdown` can invoke it. If the same
1124
- // name is re-registered, replace in place to keep the array tidy
1125
- // without disturbing relative order of unrelated entries.
1126
- const existingIdx = this.cachedShutdownHooks.findIndex(
1127
- (h) => h.name === name,
1128
- );
1129
- const entry = { name, hook };
1130
- if (existingIdx >= 0) {
1131
- this.cachedShutdownHooks[existingIdx] = entry;
1132
- } else {
1133
- this.cachedShutdownHooks.push(entry);
1134
- }
1135
- this.ensureDaemonHandler(
1136
- "skill.shutdown",
1137
- this.dispatchShutdown.bind(this),
1138
- );
1139
- // Fire-and-forget; the daemon registers a proxy that fires the
1140
- // skill.shutdown dispatch at teardown (PR D).
1141
- this.call("host.registries.register_shutdown_hook", { name }).catch(
1142
- swallow,
1143
- );
1144
- },
1145
- };
1146
- }
1147
-
1148
- // ── Local dispatch helpers ──────────────────────────────────────────────
1149
-
1150
- /**
1151
- * Install a daemon-initiated request handler exactly once per method.
1152
- * Idempotent so repeated `register*` calls don't churn the handler table
1153
- * — every dispatch is routed through the same bound method anyway.
1154
- */
1155
- private ensureDaemonHandler(
1156
- method: string,
1157
- handler: SkillHostRequestHandler,
1158
- ): void {
1159
- if (!this.daemonRequestHandlers.has(method)) {
1160
- this.daemonRequestHandlers.set(method, handler);
1161
- }
1162
- }
1163
-
1164
- /**
1165
- * `skill.dispatch_tool` handler — resolves the tool by name from the
1166
- * cached provider and invokes its `execute(input, context)`. Returns
1167
- * `{ result }` so the daemon can distinguish the wrapper from the
1168
- * tool's own (potentially undefined) return value.
1169
- */
1170
- private async dispatchTool(params: unknown): Promise<{ result: unknown }> {
1171
- const { name, input, context } = (params ?? {}) as {
1172
- name?: unknown;
1173
- input?: unknown;
1174
- context?: unknown;
1175
- };
1176
- if (typeof name !== "string" || !name) {
1177
- throw new Error(
1178
- "skill.dispatch_tool: missing or invalid 'name' parameter",
1179
- );
1180
- }
1181
- const provider = this.cachedToolsProvider;
1182
- if (!provider) {
1183
- throw new Error(`unknown tool: ${name}`);
1184
- }
1185
- // Re-invoke the provider on each dispatch so feature-flag-gated tool
1186
- // lists stay live — matches the daemon's lazy-manifest semantics in
1187
- // `assistant/src/tools/registry.ts`.
1188
- const tools = provider();
1189
- const tool = tools.find((t) => t.name === name);
1190
- if (!tool) {
1191
- throw new Error(`unknown tool: ${name}`);
1192
- }
1193
- // The daemon-side `ToolContext` is opaque on the wire; the skill's
1194
- // `Tool.execute` runtime-validates any field it actually reads, so a
1195
- // structural cast is sufficient here. Missing required-on-paper fields
1196
- // are tolerated in practice — meet-host's tools only consult a small
1197
- // subset that the daemon serializes through.
1198
- const ctx = (context ?? {}) as ToolContext;
1199
- const result = await tool.execute(
1200
- (input ?? {}) as Record<string, unknown>,
1201
- ctx,
1202
- );
1203
- return { result };
1204
- }
1205
-
1206
- /**
1207
- * `skill.dispatch_route` handler — looks up the route by patternSource,
1208
- * re-runs the regex against the inbound URL to recover match groups,
1209
- * invokes the handler, and serializes the `Response` to a
1210
- * JSON-friendly `{ status, headers, body }`.
1211
- */
1212
- private async dispatchRoute(params: unknown): Promise<{
1213
- status: number;
1214
- headers: Record<string, string>;
1215
- body: string;
1216
- }> {
1217
- const { patternSource, request } = (params ?? {}) as {
1218
- patternSource?: unknown;
1219
- request?: unknown;
1220
- };
1221
- if (typeof patternSource !== "string" || !patternSource) {
1222
- throw new Error(
1223
- "skill.dispatch_route: missing or invalid 'patternSource' parameter",
1224
- );
1225
- }
1226
- const route = this.cachedRoutes.get(patternSource);
1227
- if (!route) {
1228
- throw new Error(`unknown route: ${patternSource}`);
1229
- }
1230
- const req = (request ?? {}) as {
1231
- method?: string;
1232
- url?: string;
1233
- headers?: Record<string, string>;
1234
- body?: string;
1235
- };
1236
- if (typeof req.url !== "string" || !req.url) {
1237
- throw new Error(
1238
- "skill.dispatch_route: missing or invalid 'request.url' parameter",
1239
- );
1240
- }
1241
- // Resolve against a synthetic base so `new Request` accepts the URL
1242
- // (it rejects bare paths, but the daemon forwards `pathname + search`)
1243
- // and so the regex can run against `pathname` alone — keeps query
1244
- // strings out of anchored patterns like `^/v1/...$`.
1245
- const parsedUrl = new URL(req.url, "http://skill.local");
1246
- // Reset lastIndex so a global/sticky regex doesn't carry state across
1247
- // dispatches — `exec()` mutates lastIndex on g/y flags and the route's
1248
- // RegExp may be reused across requests.
1249
- if (route.pattern.global || route.pattern.sticky) {
1250
- route.pattern.lastIndex = 0;
1251
- }
1252
- const match = route.pattern.exec(parsedUrl.pathname);
1253
- if (!match) {
1254
- throw new Error(`url did not match pattern: ${patternSource}`);
1255
- }
1256
- const init: RequestInit = {
1257
- method: req.method ?? "GET",
1258
- headers: req.headers ?? {},
1259
- };
1260
- // GET/HEAD requests cannot carry a body in the standard fetch `Request`
1261
- // constructor; only attach when the verb permits.
1262
- if (
1263
- req.body !== undefined &&
1264
- init.method !== "GET" &&
1265
- init.method !== "HEAD"
1266
- ) {
1267
- init.body = req.body;
1268
- }
1269
- const response = await route.handler(
1270
- new Request(parsedUrl.toString(), init),
1271
- match,
1272
- );
1273
- const headers: Record<string, string> = {};
1274
- response.headers.forEach((value, key) => {
1275
- headers[key] = value;
1276
- });
1277
- const body = await response.text();
1278
- return { status: response.status, headers, body };
1279
- }
1280
-
1281
- /**
1282
- * `skill.shutdown` handler — runs cached shutdown hooks. With `name`
1283
- * set, runs only that hook; otherwise runs all hooks in reverse-
1284
- * registration order. Per-hook errors are logged via the host logger
1285
- * and otherwise swallowed so one misbehaving hook can't block the
1286
- * daemon's overall teardown.
1287
- */
1288
- private async dispatchShutdown(params: unknown): Promise<{ ok: true }> {
1289
- const { name, reason } = (params ?? {}) as {
1290
- name?: unknown;
1291
- reason?: unknown;
1292
- };
1293
- const reasonStr = typeof reason === "string" ? reason : "shutdown";
1294
- const log = this.buildLogger(this.options.skillId);
1295
- const runOne = async (entry: {
1296
- name: string;
1297
- hook: (reason: string) => Promise<void>;
1298
- }): Promise<void> => {
1299
- try {
1300
- await entry.hook(reasonStr);
1301
- } catch (err) {
1302
- log.warn(`shutdown hook '${entry.name}' threw`, {
1303
- error: errorMessage(err),
1304
- });
1305
- }
1306
- };
1307
- if (typeof name === "string" && name) {
1308
- const entry = this.cachedShutdownHooks.find((h) => h.name === name);
1309
- if (entry) await runOne(entry);
1310
- // Silently no-op for unknown names — the daemon may call shutdown
1311
- // for a hook that was never registered (e.g. a stale registration
1312
- // leftover from a previous skill load), which shouldn't fail the
1313
- // overall teardown.
1314
- return { ok: true };
1315
- }
1316
- // Reverse-registration order so later-registered hooks (which often
1317
- // depend on earlier ones) tear down first.
1318
- for (let i = this.cachedShutdownHooks.length - 1; i >= 0; i--) {
1319
- const entry = this.cachedShutdownHooks[i];
1320
- if (entry) await runOne(entry);
1321
- }
1322
- return { ok: true };
1323
- }
1324
-
1325
- private buildSpeakersFacet(): SpeakersFacet {
1326
- return {
1327
- createTracker: () =>
1328
- ({
1329
- __vellumSkillHostClientHandle: "speaker-tracker",
1330
- }) as unknown,
1331
- };
1332
- }
1333
-
1334
- // ── Public escape hatch ─────────────────────────────────────────────────
1335
-
1336
- /**
1337
- * Escape hatch for invoking any `host.*` IPC method directly. Callers
1338
- * that need to bypass the sync-method ergonomic gap (e.g. async reads
1339
- * of `host.config.*` or `host.providers.stt.listProviderIds`) use this
1340
- * to await a single RPC round-trip. The return type is unknown because
1341
- * the method surface is open.
1342
- */
1343
- async rawCall<T = unknown>(
1344
- method: string,
1345
- params?: Record<string, unknown>,
1346
- ): Promise<T> {
1347
- return this.call<T>(method, params);
1348
- }
1349
- }