@vellumai/assistant 0.3.27 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/ARCHITECTURE.md +81 -4
  2. package/Dockerfile +2 -2
  3. package/bun.lock +4 -1
  4. package/docs/trusted-contact-access.md +9 -2
  5. package/package.json +6 -3
  6. package/scripts/ipc/generate-swift.ts +9 -5
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  8. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  9. package/src/__tests__/agent-loop.test.ts +119 -0
  10. package/src/__tests__/approval-routes-http.test.ts +13 -5
  11. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  12. package/src/__tests__/asset-search-tool.test.ts +2 -0
  13. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  14. package/src/__tests__/attachments-store.test.ts +2 -0
  15. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  16. package/src/__tests__/bundled-asset.test.ts +107 -0
  17. package/src/__tests__/call-controller.test.ts +30 -29
  18. package/src/__tests__/call-routes-http.test.ts +34 -32
  19. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  20. package/src/__tests__/canonical-guardian-store.test.ts +636 -0
  21. package/src/__tests__/channel-approval-routes.test.ts +174 -1
  22. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  23. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  24. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  25. package/src/__tests__/clarification-resolver.test.ts +2 -0
  26. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  27. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  28. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  29. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  30. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  31. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  32. package/src/__tests__/config-schema.test.ts +5 -5
  33. package/src/__tests__/config-watcher.test.ts +3 -1
  34. package/src/__tests__/connection-policy.test.ts +14 -5
  35. package/src/__tests__/contacts-tools.test.ts +3 -1
  36. package/src/__tests__/contradiction-checker.test.ts +2 -0
  37. package/src/__tests__/conversation-pairing.test.ts +10 -0
  38. package/src/__tests__/conversation-routes.test.ts +1 -1
  39. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  40. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  41. package/src/__tests__/credential-vault.test.ts +5 -4
  42. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  43. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  44. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  45. package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
  46. package/src/__tests__/encrypted-store.test.ts +10 -5
  47. package/src/__tests__/followup-tools.test.ts +3 -1
  48. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  49. package/src/__tests__/gmail-integration.test.ts +0 -1
  50. package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
  51. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  52. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
  53. package/src/__tests__/guardian-dispatch.test.ts +21 -19
  54. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  55. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  56. package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
  57. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  58. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  59. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  60. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  61. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  63. package/src/__tests__/heartbeat-service.test.ts +20 -0
  64. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  65. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  66. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  67. package/src/__tests__/intent-routing.test.ts +2 -0
  68. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  69. package/src/__tests__/mcp-cli.test.ts +77 -0
  70. package/src/__tests__/media-generate-image.test.ts +21 -0
  71. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  72. package/src/__tests__/memory-regressions.test.ts +20 -20
  73. package/src/__tests__/non-member-access-request.test.ts +212 -36
  74. package/src/__tests__/notification-decision-fallback.test.ts +63 -3
  75. package/src/__tests__/notification-decision-strategy.test.ts +78 -0
  76. package/src/__tests__/notification-guardian-path.test.ts +15 -15
  77. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  78. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  79. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  80. package/src/__tests__/onboarding-template-contract.test.ts +116 -21
  81. package/src/__tests__/pairing-routes.test.ts +171 -0
  82. package/src/__tests__/playbook-execution.test.ts +3 -1
  83. package/src/__tests__/playbook-tools.test.ts +3 -1
  84. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  85. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  86. package/src/__tests__/recording-handler.test.ts +11 -0
  87. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  88. package/src/__tests__/recording-state-machine.test.ts +13 -2
  89. package/src/__tests__/registry.test.ts +7 -3
  90. package/src/__tests__/relay-server.test.ts +148 -28
  91. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  92. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  93. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  94. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  95. package/src/__tests__/schedule-tools.test.ts +3 -1
  96. package/src/__tests__/secret-scanner-executor.test.ts +59 -0
  97. package/src/__tests__/secret-scanner.test.ts +8 -0
  98. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  99. package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
  100. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  101. package/src/__tests__/session-agent-loop.test.ts +16 -0
  102. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  103. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  104. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  105. package/src/__tests__/session-profile-injection.test.ts +21 -0
  106. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  107. package/src/__tests__/session-queue.test.ts +23 -0
  108. package/src/__tests__/session-runtime-assembly.test.ts +126 -59
  109. package/src/__tests__/session-skill-tools.test.ts +27 -5
  110. package/src/__tests__/session-slash-known.test.ts +23 -0
  111. package/src/__tests__/session-slash-queue.test.ts +23 -0
  112. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  113. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  114. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  115. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  116. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  117. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  118. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  119. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  120. package/src/__tests__/skills.test.ts +8 -4
  121. package/src/__tests__/slack-channel-config.test.ts +3 -1
  122. package/src/__tests__/subagent-tools.test.ts +19 -0
  123. package/src/__tests__/swarm-recursion.test.ts +2 -0
  124. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  125. package/src/__tests__/swarm-tool.test.ts +2 -0
  126. package/src/__tests__/system-prompt.test.ts +3 -1
  127. package/src/__tests__/task-compiler.test.ts +3 -1
  128. package/src/__tests__/task-management-tools.test.ts +3 -1
  129. package/src/__tests__/task-tools.test.ts +3 -1
  130. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  131. package/src/__tests__/terminal-tools.test.ts +2 -0
  132. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  133. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  134. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  135. package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  138. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  139. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  140. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  141. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  142. package/src/__tests__/view-image-tool.test.ts +3 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  145. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  146. package/src/__tests__/work-item-output.test.ts +3 -1
  147. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  148. package/src/agent/loop.ts +46 -3
  149. package/src/approvals/guardian-decision-primitive.ts +285 -0
  150. package/src/approvals/guardian-request-resolvers.ts +539 -0
  151. package/src/calls/call-controller.ts +26 -23
  152. package/src/calls/guardian-action-sweep.ts +10 -2
  153. package/src/calls/guardian-dispatch.ts +46 -40
  154. package/src/calls/relay-server.ts +358 -24
  155. package/src/calls/types.ts +1 -1
  156. package/src/calls/voice-session-bridge.ts +3 -3
  157. package/src/cli.ts +12 -0
  158. package/src/config/agent-schema.ts +14 -3
  159. package/src/config/calls-schema.ts +6 -6
  160. package/src/config/core-schema.ts +3 -3
  161. package/src/config/feature-flag-registry.json +8 -0
  162. package/src/config/mcp-schema.ts +1 -1
  163. package/src/config/memory-schema.ts +27 -19
  164. package/src/config/schema.ts +21 -21
  165. package/src/config/skills-schema.ts +7 -7
  166. package/src/config/system-prompt.ts +2 -1
  167. package/src/config/templates/BOOTSTRAP.md +47 -31
  168. package/src/config/templates/USER.md +5 -0
  169. package/src/config/update-bulletin-template-path.ts +4 -1
  170. package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
  171. package/src/daemon/handlers/config-inbox.ts +4 -4
  172. package/src/daemon/handlers/guardian-actions.ts +45 -66
  173. package/src/daemon/handlers/sessions.ts +148 -4
  174. package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
  175. package/src/daemon/ipc-contract/messages.ts +16 -0
  176. package/src/daemon/ipc-contract-inventory.json +1 -0
  177. package/src/daemon/lifecycle.ts +22 -16
  178. package/src/daemon/pairing-store.ts +86 -3
  179. package/src/daemon/server.ts +18 -0
  180. package/src/daemon/session-agent-loop-handlers.ts +5 -4
  181. package/src/daemon/session-agent-loop.ts +33 -6
  182. package/src/daemon/session-lifecycle.ts +25 -17
  183. package/src/daemon/session-memory.ts +2 -2
  184. package/src/daemon/session-process.ts +68 -326
  185. package/src/daemon/session-runtime-assembly.ts +119 -25
  186. package/src/daemon/session-tool-setup.ts +3 -2
  187. package/src/daemon/session.ts +4 -3
  188. package/src/home-base/prebuilt/seed.ts +2 -1
  189. package/src/hooks/templates.ts +2 -1
  190. package/src/memory/canonical-guardian-store.ts +586 -0
  191. package/src/memory/channel-guardian-store.ts +2 -0
  192. package/src/memory/conversation-crud.ts +7 -7
  193. package/src/memory/db-init.ts +20 -0
  194. package/src/memory/embedding-local.ts +257 -39
  195. package/src/memory/embedding-runtime-manager.ts +471 -0
  196. package/src/memory/guardian-action-store.ts +7 -60
  197. package/src/memory/guardian-approvals.ts +9 -4
  198. package/src/memory/guardian-bindings.ts +25 -1
  199. package/src/memory/indexer.ts +3 -3
  200. package/src/memory/ingress-invite-store.ts +45 -0
  201. package/src/memory/job-handlers/backfill.ts +16 -9
  202. package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
  203. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  204. package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
  205. package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
  206. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
  207. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
  208. package/src/memory/migrations/index.ts +5 -0
  209. package/src/memory/migrations/registry.ts +5 -0
  210. package/src/memory/qdrant-client.ts +31 -22
  211. package/src/memory/schema-migration.ts +1 -0
  212. package/src/memory/schema.ts +56 -0
  213. package/src/notifications/copy-composer.ts +31 -4
  214. package/src/notifications/decision-engine.ts +57 -0
  215. package/src/permissions/defaults.ts +2 -0
  216. package/src/runtime/access-request-helper.ts +173 -0
  217. package/src/runtime/actor-trust-resolver.ts +221 -0
  218. package/src/runtime/channel-guardian-service.ts +12 -4
  219. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  220. package/src/runtime/channel-retry-sweep.ts +18 -6
  221. package/src/runtime/guardian-context-resolver.ts +38 -71
  222. package/src/runtime/guardian-decision-types.ts +6 -0
  223. package/src/runtime/guardian-reply-router.ts +717 -0
  224. package/src/runtime/http-server.ts +8 -0
  225. package/src/runtime/ingress-service.ts +80 -3
  226. package/src/runtime/invite-redemption-service.ts +141 -2
  227. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
  228. package/src/runtime/routes/channel-route-shared.ts +1 -1
  229. package/src/runtime/routes/channel-routes.ts +1 -1
  230. package/src/runtime/routes/conversation-routes.ts +20 -2
  231. package/src/runtime/routes/guardian-action-routes.ts +100 -109
  232. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  233. package/src/runtime/routes/inbound-message-handler.ts +205 -529
  234. package/src/runtime/routes/ingress-routes.ts +52 -4
  235. package/src/runtime/routes/pairing-routes.ts +3 -0
  236. package/src/runtime/tool-grant-request-helper.ts +195 -0
  237. package/src/tools/executor.ts +13 -1
  238. package/src/tools/guardian-control-plane-policy.ts +2 -2
  239. package/src/tools/sensitive-output-placeholders.ts +203 -0
  240. package/src/tools/tool-approval-handler.ts +53 -10
  241. package/src/tools/types.ts +13 -2
  242. package/src/util/bundled-asset.ts +31 -0
  243. package/src/util/canonicalize-identity.ts +52 -0
  244. package/src/util/logger.ts +20 -8
  245. package/src/util/platform.ts +10 -0
  246. package/src/util/voice-code.ts +29 -0
  247. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Downloads and manages the local embedding runtime post-hatch.
3
+ *
4
+ * Instead of shipping heavy native + JS dependencies inside the .app bundle,
5
+ * we download them from npm and run embeddings in a **separate bun process**.
6
+ * The compiled daemon binary cannot resolve bare specifiers in dynamically
7
+ * imported files, so we spawn a standalone bun process that runs an embed
8
+ * worker script communicating via JSON-lines over stdin/stdout.
9
+ *
10
+ * The runtime is stored in ~/.vellum/workspace/embedding-models/ and used
11
+ * by embedding-local.ts on demand.
12
+ *
13
+ * Follows the same download/install pattern as qdrant-manager.ts.
14
+ */
15
+
16
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
17
+ import { arch, platform } from 'node:os';
18
+ import { join } from 'node:path';
19
+
20
+ import { getLogger } from '../util/logger.js';
21
+ import { getEmbeddingModelsDir } from '../util/platform.js';
22
+ import { PromiseGuard } from '../util/promise-guard.js';
23
+
24
+ const log = getLogger('embedding-runtime-manager');
25
+
26
+ // Pinned versions matching assistant/bun.lock
27
+ const ONNXRUNTIME_NODE_VERSION = '1.21.0';
28
+ const ONNXRUNTIME_COMMON_VERSION = '1.21.0';
29
+ const TRANSFORMERS_VERSION = '3.8.1';
30
+
31
+ /** Bun version to download when system bun is not available. */
32
+ const BUN_VERSION = '1.2.0';
33
+
34
+ /** Composite version string for cache invalidation. */
35
+ const RUNTIME_VERSION = `ort-${ONNXRUNTIME_NODE_VERSION}_hf-${TRANSFORMERS_VERSION}`;
36
+
37
+ const WORKER_FILENAME = 'embed-worker.mjs';
38
+
39
+ /** Module-level guard so concurrent in-process calls share one download. */
40
+ const installGuard = new PromiseGuard<void>();
41
+
42
+ interface VersionManifest {
43
+ runtimeVersion: string;
44
+ onnxruntimeNodeVersion: string;
45
+ onnxruntimeCommonVersion: string;
46
+ transformersVersion: string;
47
+ platform: string;
48
+ arch: string;
49
+ installedAt: string;
50
+ }
51
+
52
+ // ── npm tarball helpers ─────────────────────────────────────────────
53
+
54
+ function npmTarballUrl(pkg: string, version: string): string {
55
+ // Scoped packages encode the scope in the URL
56
+ const encoded = pkg.replace('/', '%2f');
57
+ const basename = pkg.startsWith('@') ? pkg.split('/')[1] : pkg;
58
+ return `https://registry.npmjs.org/${encoded}/-/${basename}-${version}.tgz`;
59
+ }
60
+
61
+ async function downloadAndExtract(
62
+ url: string,
63
+ targetDir: string,
64
+ signal?: AbortSignal,
65
+ ): Promise<void> {
66
+ log.info({ url, targetDir }, 'Downloading npm package');
67
+
68
+ const response = await fetch(url, { signal });
69
+ if (!response.ok) {
70
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
71
+ }
72
+
73
+ const tarball = await response.arrayBuffer();
74
+
75
+ // npm tarballs extract to package/, we need to redirect to targetDir
76
+ mkdirSync(targetDir, { recursive: true });
77
+
78
+ const tmpTar = join(targetDir, `download-${Date.now()}.tgz`);
79
+ writeFileSync(tmpTar, Buffer.from(tarball));
80
+
81
+ try {
82
+ // Extract tarball, stripping the leading "package/" directory
83
+ const proc = Bun.spawn({
84
+ cmd: ['tar', 'xzf', tmpTar, '-C', targetDir, '--strip-components=1'],
85
+ stdout: 'ignore',
86
+ stderr: 'pipe',
87
+ });
88
+ await proc.exited;
89
+ if (proc.exitCode !== 0) {
90
+ const stderr = await new Response(proc.stderr).text();
91
+ throw new Error(`Failed to extract ${url}: ${stderr}`);
92
+ }
93
+ } finally {
94
+ try { rmSync(tmpTar); } catch { /* ignore */ }
95
+ }
96
+ }
97
+
98
+ // ── Worker script content ───────────────────────────────────────────
99
+
100
+ function generateWorkerScript(): string {
101
+ // This script is run by a standalone bun process (not the compiled daemon).
102
+ // Because it runs in a real bun runtime, bare specifier resolution works
103
+ // normally — node_modules/ in the same directory is found automatically.
104
+ return `\
105
+ // embed-worker.mjs — Auto-generated by EmbeddingRuntimeManager
106
+ // Runs in a separate bun process, communicates via JSON-lines over stdin/stdout.
107
+ import { pipeline, env } from '@huggingface/transformers';
108
+
109
+ const model = process.argv[2];
110
+ const cacheDir = process.argv[3];
111
+ if (cacheDir && env) env.cacheDir = cacheDir;
112
+
113
+ let extractor;
114
+ try {
115
+ extractor = await pipeline('feature-extraction', model, { dtype: 'fp32' });
116
+ process.stdout.write(JSON.stringify({ type: 'ready' }) + '\\n');
117
+ } catch (err) {
118
+ process.stdout.write(JSON.stringify({ type: 'error', error: err.message || String(err) }) + '\\n');
119
+ process.exit(1);
120
+ }
121
+
122
+ // Sequential request queue to avoid concurrent ONNX inference
123
+ const decoder = new TextDecoder();
124
+ let buffer = '';
125
+ let processing = false;
126
+ const queue = [];
127
+
128
+ process.stdin.on('data', (chunk) => {
129
+ buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
130
+ let idx;
131
+ while ((idx = buffer.indexOf('\\n')) !== -1) {
132
+ const line = buffer.slice(0, idx);
133
+ buffer = buffer.slice(idx + 1);
134
+ if (line.trim()) queue.push(line);
135
+ }
136
+ if (!processing) processQueue();
137
+ });
138
+
139
+ async function processQueue() {
140
+ processing = true;
141
+ while (queue.length > 0) {
142
+ const line = queue.shift();
143
+ let req;
144
+ try { req = JSON.parse(line); } catch { continue; }
145
+ try {
146
+ const output = await extractor(req.texts, { pooling: 'cls', normalize: true });
147
+ const vectors = output.tolist();
148
+ process.stdout.write(JSON.stringify({ id: req.id, vectors }) + '\\n');
149
+ } catch (err) {
150
+ process.stdout.write(JSON.stringify({ id: req.id, error: err.message || String(err) }) + '\\n');
151
+ }
152
+ }
153
+ processing = false;
154
+ }
155
+
156
+ process.stdin.on('end', () => process.exit(0));
157
+ `;
158
+ }
159
+
160
+ // ── Main manager ────────────────────────────────────────────────────
161
+
162
+ export class EmbeddingRuntimeManager {
163
+ private readonly baseDir: string;
164
+
165
+ constructor(baseDir?: string) {
166
+ this.baseDir = baseDir ?? getEmbeddingModelsDir();
167
+ }
168
+
169
+ /** Check if the embedding runtime is installed and up-to-date. */
170
+ isReady(): boolean {
171
+ const manifest = this.readManifest();
172
+ if (!manifest) return false;
173
+ if (manifest.runtimeVersion !== RUNTIME_VERSION) return false;
174
+
175
+ // Verify the worker script exists and a bun binary is available
176
+ return existsSync(this.getWorkerPath()) && this.getBunPath() !== undefined;
177
+ }
178
+
179
+ /** Path to the embed worker script. */
180
+ getWorkerPath(): string {
181
+ return join(this.baseDir, WORKER_FILENAME);
182
+ }
183
+
184
+ /**
185
+ * Find a usable bun binary.
186
+ * Checks: downloaded copy → common install locations.
187
+ */
188
+ getBunPath(): string | undefined {
189
+ // 1. Downloaded bun
190
+ const downloadedBun = join(this.baseDir, 'bin', 'bun');
191
+ if (existsSync(downloadedBun)) return downloadedBun;
192
+
193
+ // 2. Common installation paths — the compiled daemon inherits a
194
+ // restricted PATH, so we check well-known prefixes directly.
195
+ const home = process.env.HOME ?? '';
196
+ for (const p of [
197
+ join(home, '.bun', 'bin', 'bun'),
198
+ '/opt/homebrew/bin/bun',
199
+ '/usr/local/bin/bun',
200
+ ]) {
201
+ if (existsSync(p)) return p;
202
+ }
203
+
204
+ return undefined;
205
+ }
206
+
207
+ /**
208
+ * Download and install the embedding runtime if not already present.
209
+ * Safe to call concurrently — in-process calls share one promise via
210
+ * PromiseGuard, and cross-process calls are serialized via a lock file.
211
+ */
212
+ async ensureInstalled(signal?: AbortSignal): Promise<void> {
213
+ if (this.isReady()) return;
214
+
215
+ // Deduplicate concurrent in-process calls
216
+ await installGuard.run(() => this.acquireLockAndInstall(signal));
217
+
218
+ // If another process was downloading and we skipped, or if the download
219
+ // somehow failed silently, reset the guard so we can retry next time.
220
+ if (!this.isReady()) {
221
+ installGuard.reset();
222
+ }
223
+ }
224
+
225
+ private async acquireLockAndInstall(signal?: AbortSignal): Promise<void> {
226
+ // Re-check after acquiring the in-process guard
227
+ if (this.isReady()) return;
228
+
229
+ // Cross-process lock to prevent duplicate downloads
230
+ const lockPath = join(this.baseDir, '.downloading');
231
+ if (existsSync(lockPath)) {
232
+ try {
233
+ const lockContent = readFileSync(lockPath, 'utf-8').trim();
234
+ const lockPid = parseInt(lockContent, 10);
235
+ if (!isNaN(lockPid) && lockPid !== process.pid) {
236
+ try {
237
+ process.kill(lockPid, 0);
238
+ log.info({ lockPid }, 'Another process is downloading the embedding runtime, skipping');
239
+ return;
240
+ } catch {
241
+ log.info({ lockPid }, 'Cleaning up stale download lock');
242
+ }
243
+ }
244
+ } catch {
245
+ // Can't read lock file, proceed
246
+ }
247
+ }
248
+
249
+ mkdirSync(this.baseDir, { recursive: true });
250
+
251
+ // Write a .gitignore so the workspace git repo ignores this directory
252
+ const gitignorePath = join(this.baseDir, '.gitignore');
253
+ if (!existsSync(gitignorePath)) {
254
+ writeFileSync(gitignorePath, '*\n!.gitignore\n');
255
+ }
256
+
257
+ writeFileSync(lockPath, String(process.pid));
258
+
259
+ try {
260
+ await this.install(signal);
261
+ } finally {
262
+ try { rmSync(lockPath); } catch { /* ignore */ }
263
+ }
264
+ }
265
+
266
+ private async install(signal?: AbortSignal): Promise<void> {
267
+ const os = platform();
268
+ const cpu = arch();
269
+ log.info({ os, cpu, runtimeVersion: RUNTIME_VERSION }, 'Installing embedding runtime');
270
+
271
+ // Work in a temp directory for atomic install
272
+ const tmpDir = join(this.baseDir, `.installing-${Date.now()}`);
273
+ mkdirSync(tmpDir, { recursive: true });
274
+
275
+ try {
276
+ // Step 1: Download npm packages (and bun if needed) in parallel
277
+ const nodeModules = join(tmpDir, 'node_modules');
278
+ const downloads: Promise<void>[] = [
279
+ downloadAndExtract(
280
+ npmTarballUrl('onnxruntime-node', ONNXRUNTIME_NODE_VERSION),
281
+ join(nodeModules, 'onnxruntime-node'),
282
+ signal,
283
+ ),
284
+ downloadAndExtract(
285
+ npmTarballUrl('onnxruntime-common', ONNXRUNTIME_COMMON_VERSION),
286
+ join(nodeModules, 'onnxruntime-common'),
287
+ signal,
288
+ ),
289
+ downloadAndExtract(
290
+ npmTarballUrl('@huggingface/transformers', TRANSFORMERS_VERSION),
291
+ join(nodeModules, '@huggingface', 'transformers'),
292
+ signal,
293
+ ),
294
+ ];
295
+
296
+ // Download bun binary if not already available on the system
297
+ if (!this.getBunPath()) {
298
+ downloads.push(this.downloadBunBinary(tmpDir, signal));
299
+ }
300
+
301
+ await Promise.all(downloads);
302
+
303
+ if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
304
+
305
+ log.info('npm packages downloaded, stripping non-platform binaries');
306
+
307
+ // Step 2: Strip non-platform native binaries
308
+ const onnxBinDir = join(nodeModules, 'onnxruntime-node', 'bin', 'napi-v3');
309
+ if (existsSync(onnxBinDir)) {
310
+ const entries = readdirSync(onnxBinDir);
311
+ for (const entry of entries) {
312
+ // Keep all darwin architectures (arm64 and x86_64) since uname -m
313
+ // is unreliable under Rosetta (returns x86_64 on Apple Silicon)
314
+ if (entry !== os) {
315
+ rmSync(join(onnxBinDir, entry), { recursive: true, force: true });
316
+ }
317
+ }
318
+ }
319
+
320
+ // Strip non-runtime files to reduce disk usage.
321
+ // Keep lib/ directories — they contain JS entry points needed for bare
322
+ // specifier imports in the worker subprocess.
323
+ const onnxNodeDir = join(nodeModules, 'onnxruntime-node');
324
+ rmSync(join(onnxNodeDir, 'script'), { recursive: true, force: true });
325
+ rmSync(join(onnxNodeDir, 'README.md'), { force: true });
326
+ rmSync(join(nodeModules, 'onnxruntime-common', 'README.md'), { force: true });
327
+
328
+ // Step 3: Create a stub "sharp" package so that the pre-built
329
+ // transformers.node.mjs can import it without error. The bundle checks
330
+ // `if (sharp)` at module initialization time — the stub must be truthy.
331
+ // We only use text embeddings, never image processing.
332
+ const sharpDir = join(nodeModules, 'sharp');
333
+ mkdirSync(sharpDir, { recursive: true });
334
+ writeFileSync(join(sharpDir, 'package.json'), '{"name":"sharp","version":"0.0.0","main":"index.js"}\n');
335
+ writeFileSync(join(sharpDir, 'index.js'), [
336
+ '// Stub: only text embeddings are used, no image processing.',
337
+ '// Must be a truthy function so transformers.node.mjs initialization passes.',
338
+ 'function sharp() { throw new Error("sharp stub: image processing not available"); }',
339
+ 'sharp.format = {};',
340
+ 'module.exports = sharp;',
341
+ '',
342
+ ].join('\n'));
343
+
344
+ // Step 4: Write embed worker script
345
+ writeFileSync(join(tmpDir, WORKER_FILENAME), generateWorkerScript());
346
+
347
+ // Step 5: Write version manifest
348
+ const manifest: VersionManifest = {
349
+ runtimeVersion: RUNTIME_VERSION,
350
+ onnxruntimeNodeVersion: ONNXRUNTIME_NODE_VERSION,
351
+ onnxruntimeCommonVersion: ONNXRUNTIME_COMMON_VERSION,
352
+ transformersVersion: TRANSFORMERS_VERSION,
353
+ platform: os,
354
+ arch: cpu,
355
+ installedAt: new Date().toISOString(),
356
+ };
357
+ writeFileSync(join(tmpDir, 'version.json'), JSON.stringify(manifest, null, 2) + '\n');
358
+
359
+ // Step 6: Atomic swap — remove old install and rename temp to final
360
+ // Preserve model-cache/, bin/ (downloaded bun), and .gitignore
361
+ const modelCacheDir = join(this.baseDir, 'model-cache');
362
+ const hadModelCache = existsSync(modelCacheDir);
363
+ let tmpModelCache: string | null = null;
364
+ if (hadModelCache) {
365
+ tmpModelCache = join(this.baseDir, `.model-cache-preserve-${Date.now()}`);
366
+ renameSync(modelCacheDir, tmpModelCache);
367
+ }
368
+
369
+ // Preserve downloaded bun binary if it exists and we didn't just download a new one
370
+ const existingBinDir = join(this.baseDir, 'bin');
371
+ const newBinDir = join(tmpDir, 'bin');
372
+ const hadBinDir = existsSync(existingBinDir) && !existsSync(newBinDir);
373
+ let tmpBinDir: string | null = null;
374
+ if (hadBinDir) {
375
+ tmpBinDir = join(this.baseDir, `.bin-preserve-${Date.now()}`);
376
+ renameSync(existingBinDir, tmpBinDir);
377
+ }
378
+
379
+ // Remove old install (preserving dotfiles like .gitignore, .downloading, temp dirs)
380
+ for (const entry of readdirSync(this.baseDir)) {
381
+ if (entry.startsWith('.') || entry === tmpDir.split('/').pop()) continue;
382
+ rmSync(join(this.baseDir, entry), { recursive: true, force: true });
383
+ }
384
+
385
+ // Move new files into place
386
+ for (const entry of readdirSync(tmpDir)) {
387
+ renameSync(join(tmpDir, entry), join(this.baseDir, entry));
388
+ }
389
+
390
+ // Restore model cache
391
+ if (tmpModelCache && existsSync(tmpModelCache)) {
392
+ renameSync(tmpModelCache, modelCacheDir);
393
+ }
394
+
395
+ // Restore bun binary
396
+ if (tmpBinDir && existsSync(tmpBinDir)) {
397
+ renameSync(tmpBinDir, existingBinDir);
398
+ }
399
+
400
+ log.info({ runtimeVersion: RUNTIME_VERSION }, 'Embedding runtime installed successfully');
401
+ } catch (err) {
402
+ log.error({ err }, 'Failed to install embedding runtime');
403
+ throw err;
404
+ } finally {
405
+ // Clean up temp directory
406
+ rmSync(tmpDir, { recursive: true, force: true });
407
+ }
408
+ }
409
+
410
+ private async downloadBunBinary(installDir: string, signal?: AbortSignal): Promise<void> {
411
+ const os = platform();
412
+ const cpu = arch() === 'arm64' ? 'aarch64' : arch();
413
+ const target = `${os}-${cpu}`;
414
+ const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-${target}.zip`;
415
+
416
+ log.info({ url, target, bunVersion: BUN_VERSION }, 'Downloading bun binary');
417
+
418
+ const response = await fetch(url, {
419
+ signal,
420
+ redirect: 'follow',
421
+ });
422
+ if (!response.ok) {
423
+ throw new Error(`Failed to download bun: ${response.status} ${response.statusText}`);
424
+ }
425
+
426
+ const zipData = await response.arrayBuffer();
427
+ const binDir = join(installDir, 'bin');
428
+ mkdirSync(binDir, { recursive: true });
429
+
430
+ const tmpZip = join(binDir, `bun-download-${Date.now()}.zip`);
431
+ writeFileSync(tmpZip, Buffer.from(zipData));
432
+
433
+ try {
434
+ // Extract zip
435
+ const proc = Bun.spawn({
436
+ cmd: ['unzip', '-o', tmpZip, '-d', binDir],
437
+ stdout: 'ignore',
438
+ stderr: 'pipe',
439
+ });
440
+ await proc.exited;
441
+ if (proc.exitCode !== 0) {
442
+ const stderr = await new Response(proc.stderr).text();
443
+ throw new Error(`Failed to extract bun zip: ${stderr}`);
444
+ }
445
+
446
+ // Move binary from bun-{target}/bun to bin/bun
447
+ const extractedBun = join(binDir, `bun-${target}`, 'bun');
448
+ if (existsSync(extractedBun)) {
449
+ renameSync(extractedBun, join(binDir, 'bun'));
450
+ rmSync(join(binDir, `bun-${target}`), { recursive: true, force: true });
451
+ }
452
+
453
+ // Make executable
454
+ chmodSync(join(binDir, 'bun'), 0o755);
455
+ } finally {
456
+ try { rmSync(tmpZip); } catch { /* ignore */ }
457
+ }
458
+
459
+ log.info('Bun binary downloaded successfully');
460
+ }
461
+
462
+ private readManifest(): VersionManifest | null {
463
+ const manifestPath = join(this.baseDir, 'version.json');
464
+ if (!existsSync(manifestPath)) return null;
465
+ try {
466
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
467
+ } catch {
468
+ return null;
469
+ }
470
+ }
471
+ }
@@ -7,7 +7,7 @@
7
7
  * answer resolves the request and all other deliveries are marked answered.
8
8
  */
9
9
 
10
- import { and, count, desc, eq, inArray, isNotNull, lt } from 'drizzle-orm';
10
+ import { and, desc, eq, inArray, lt } from 'drizzle-orm';
11
11
  import { v4 as uuid } from 'uuid';
12
12
 
13
13
  import { getLogger } from '../util/logger.js';
@@ -136,6 +136,12 @@ function generateRequestCode(): string {
136
136
  // Guardian Action Requests
137
137
  // ---------------------------------------------------------------------------
138
138
 
139
+ /**
140
+ * @internal Test-only helper. Production code should create guardian requests
141
+ * via `createCanonicalGuardianRequest` in canonical-guardian-store.ts.
142
+ * This function is retained solely so that existing test fixtures that seed
143
+ * legacy guardian action rows continue to compile.
144
+ */
139
145
  export function createGuardianActionRequest(params: {
140
146
  assistantId?: string;
141
147
  kind: string;
@@ -226,65 +232,6 @@ export function getPendingRequestByCallSessionId(callSessionId: string): Guardia
226
232
  return row ? rowToRequest(row) : null;
227
233
  }
228
234
 
229
- /**
230
- * Count pending guardian action requests for a given call session.
231
- * Used as a candidate-affinity hint so the decision engine knows how many
232
- * active guardian requests already exist for the current call.
233
- */
234
- export function countPendingRequestsByCallSessionId(callSessionId: string): number {
235
- const db = getDb();
236
- const row = db
237
- .select({ count: count() })
238
- .from(guardianActionRequests)
239
- .where(
240
- and(
241
- eq(guardianActionRequests.callSessionId, callSessionId),
242
- eq(guardianActionRequests.status, 'pending'),
243
- ),
244
- )
245
- .get();
246
- return row?.count ?? 0;
247
- }
248
-
249
- /**
250
- * Look up the vellum conversation ID used for the first guardian question
251
- * delivery in a given call session. Returns the conversation ID when one
252
- * exists, or null if no vellum delivery has been recorded yet.
253
- *
254
- * Used by guardian-dispatch to enforce deterministic thread affinity:
255
- * all guardian questions within the same call session should route to
256
- * the same vellum conversation.
257
- */
258
- export function getGuardianConversationIdForCallSession(callSessionId: string): string | null {
259
- try {
260
- const db = getDb();
261
- const row = db
262
- .select({ conversationId: guardianActionDeliveries.destinationConversationId })
263
- .from(guardianActionDeliveries)
264
- .innerJoin(
265
- guardianActionRequests,
266
- eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
267
- )
268
- .where(
269
- and(
270
- eq(guardianActionRequests.callSessionId, callSessionId),
271
- eq(guardianActionDeliveries.destinationChannel, 'vellum'),
272
- isNotNull(guardianActionDeliveries.destinationConversationId),
273
- ),
274
- )
275
- .orderBy(guardianActionDeliveries.createdAt)
276
- .limit(1)
277
- .get();
278
- return row?.conversationId ?? null;
279
- } catch (err) {
280
- if (err instanceof Error && err.message.includes('no such table')) {
281
- log.warn({ err }, 'guardian tables not yet created');
282
- return null;
283
- }
284
- throw err;
285
- }
286
- }
287
-
288
235
  /**
289
236
  * First-response-wins resolution. Checks that the request is still
290
237
  * 'pending' before updating; returns the updated request on success
@@ -70,6 +70,12 @@ function rowToApprovalRequest(row: typeof channelGuardianApprovalRequests.$infer
70
70
  // Operations
71
71
  // ---------------------------------------------------------------------------
72
72
 
73
+ /**
74
+ * @internal Test-only helper. Production code should create guardian requests
75
+ * via `createCanonicalGuardianRequest` in canonical-guardian-store.ts.
76
+ * This function is retained solely so that existing test fixtures that seed
77
+ * legacy approval rows continue to compile.
78
+ */
73
79
  export function createApprovalRequest(params: {
74
80
  runId: string;
75
81
  requestId?: string;
@@ -535,10 +541,9 @@ export function countPendingByConversation(
535
541
  }
536
542
 
537
543
  /**
538
- * Check for an existing pending (non-expired) approval request for a specific
539
- * requester on a channel. Used to deduplicate access requests — repeated
540
- * messages from the same non-member should not create duplicate approval
541
- * requests while one is already pending.
544
+ * @internal Test-only helper. Production code should query canonical guardian
545
+ * requests via `listCanonicalGuardianRequests` in canonical-guardian-store.ts.
546
+ * Retained for existing test fixtures that check legacy approval dedup.
542
547
  */
543
548
  export function findPendingAccessRequestForRequester(
544
549
  assistantId: string,
@@ -5,7 +5,7 @@
5
5
  * for a given (assistantId, channel) pair.
6
6
  */
7
7
 
8
- import { and, eq } from 'drizzle-orm';
8
+ import { and, asc, desc, eq } from 'drizzle-orm';
9
9
  import { v4 as uuid } from 'uuid';
10
10
 
11
11
  import { getDb } from './db.js';
@@ -103,6 +103,30 @@ export function getActiveBinding(assistantId: string, channel: string): Guardian
103
103
  return row ? rowToBinding(row) : null;
104
104
  }
105
105
 
106
+ /**
107
+ * List all active guardian bindings for an assistant across all channels.
108
+ * Deterministic ordering: verifiedAt DESC (most recently verified first),
109
+ * then channel ASC (alphabetical tiebreaker).
110
+ */
111
+ export function listActiveBindingsByAssistant(assistantId: string): GuardianBinding[] {
112
+ const db = getDb();
113
+ return db
114
+ .select()
115
+ .from(channelGuardianBindings)
116
+ .where(
117
+ and(
118
+ eq(channelGuardianBindings.assistantId, assistantId),
119
+ eq(channelGuardianBindings.status, 'active'),
120
+ ),
121
+ )
122
+ .orderBy(
123
+ desc(channelGuardianBindings.verifiedAt),
124
+ asc(channelGuardianBindings.channel),
125
+ )
126
+ .all()
127
+ .map(rowToBinding);
128
+ }
129
+
106
130
  export function revokeBinding(assistantId: string, channel: string): boolean {
107
131
  const db = getDb();
108
132
  const now = Date.now();
@@ -23,7 +23,7 @@ export interface IndexMessageInput {
23
23
  createdAt: number;
24
24
  scopeId?: string;
25
25
  // Provenance for trust-aware extraction gating (M3)
26
- provenanceActorRole?: 'guardian' | 'non-guardian' | 'unverified_channel';
26
+ provenanceTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
27
27
  }
28
28
 
29
29
  export interface IndexMessageResult {
@@ -39,7 +39,7 @@ export function indexMessageNow(
39
39
 
40
40
  // Provenance-based trust gating: only guardian and legacy (undefined) actors
41
41
  // are trusted for extraction and conflict resolution.
42
- const isTrustedActor = input.provenanceActorRole === 'guardian' || input.provenanceActorRole === undefined;
42
+ const isTrustedActor = input.provenanceTrustClass === 'guardian' || input.provenanceTrustClass === undefined;
43
43
 
44
44
  const text = extractTextFromStoredMessageContent(input.content);
45
45
  if (text.length === 0) {
@@ -123,7 +123,7 @@ export function indexMessageNow(
123
123
  }
124
124
 
125
125
  if (!isTrustedActor && (shouldExtract || shouldResolveConflicts)) {
126
- log.info(`Skipping extraction/conflict jobs for untrusted actor (role=${input.provenanceActorRole})`);
126
+ log.info(`Skipping extraction/conflict jobs for untrusted actor (trustClass=${input.provenanceTrustClass})`);
127
127
  }
128
128
 
129
129
  enqueueSummaryRollupJobsIfDue();