@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
package/src/agent/loop.ts CHANGED
@@ -35,6 +35,7 @@ export interface CheckpointInfo {
35
35
  turnIndex: number;
36
36
  toolCount: number;
37
37
  hasToolUse: boolean;
38
+ history: Message[]; // current history snapshot for token estimation
38
39
  }
39
40
 
40
41
  export type CheckpointDecision = "continue" | "yield";
@@ -71,7 +72,13 @@ export type AgentEvent =
71
72
  toolUseId: string;
72
73
  accumulatedJson: string;
73
74
  }
74
- | { type: "server_tool_start"; name: string; toolUseId: string }
75
+ | {
76
+ type: "server_tool_start";
77
+ name: string;
78
+ toolUseId: string;
79
+ input: Record<string, unknown>;
80
+ }
81
+ | { type: "server_tool_complete"; toolUseId: string }
75
82
  | { type: "error"; error: Error }
76
83
  | {
77
84
  type: "usage";
@@ -305,6 +312,12 @@ export class AgentLoop {
305
312
  type: "server_tool_start",
306
313
  name: event.name,
307
314
  toolUseId: event.toolUseId,
315
+ input: event.input,
316
+ });
317
+ } else if (event.type === "server_tool_complete") {
318
+ onEvent({
319
+ type: "server_tool_complete",
320
+ toolUseId: event.toolUseId,
308
321
  });
309
322
  }
310
323
  },
@@ -561,6 +574,7 @@ export class AgentLoop {
561
574
  turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
562
575
  toolCount: toolUseBlocks.length,
563
576
  hasToolUse: true,
577
+ history,
564
578
  });
565
579
  if (decision === "yield") {
566
580
  break;
@@ -622,40 +636,53 @@ export function escapeAxTreeContent(content: string): string {
622
636
  * `MAX_AX_TREES_IN_HISTORY` `<ax-tree>` blocks have been replaced with a
623
637
  * short placeholder. This keeps the conversation context small so that
624
638
  * TTFT does not grow linearly with step count in computer-use sessions.
639
+ *
640
+ * Counting is per-block, not per-message — a single user message can
641
+ * contain multiple tool_result blocks each with their own AX tree snapshot.
625
642
  */
626
643
  export function compactAxTreeHistory(messages: Message[]): Message[] {
627
- // Collect indices of user messages that contain an <ax-tree> block
628
- const indicesWithAxTree: number[] = [];
644
+ // Collect (messageIndex, blockIndex) for every tool_result block with <ax-tree>
645
+ const axBlocks: Array<{ msgIdx: number; blockIdx: number }> = [];
629
646
  for (let i = 0; i < messages.length; i++) {
630
647
  const msg = messages[i];
631
648
  if (msg.role !== "user") continue;
632
- for (const block of msg.content) {
649
+ for (let j = 0; j < msg.content.length; j++) {
650
+ const block = msg.content[j];
633
651
  if (
634
652
  block.type === "tool_result" &&
635
653
  typeof block.content === "string" &&
636
654
  block.content.includes("<ax-tree>")
637
655
  ) {
638
- indicesWithAxTree.push(i);
639
- break;
656
+ axBlocks.push({ msgIdx: i, blockIdx: j });
640
657
  }
641
658
  }
642
659
  }
643
660
 
644
- if (indicesWithAxTree.length <= MAX_AX_TREES_IN_HISTORY) {
661
+ if (axBlocks.length <= MAX_AX_TREES_IN_HISTORY) {
645
662
  return messages;
646
663
  }
647
664
 
648
- const toStrip = new Set(indicesWithAxTree.slice(0, -MAX_AX_TREES_IN_HISTORY));
665
+ // Build a set of "msgIdx:blockIdx" keys for blocks that should be stripped
666
+ const toStrip = new Set(
667
+ axBlocks
668
+ .slice(0, -MAX_AX_TREES_IN_HISTORY)
669
+ .map((b) => `${b.msgIdx}:${b.blockIdx}`),
670
+ );
649
671
 
650
672
  return messages.map((msg, idx) => {
651
- if (!toStrip.has(idx)) return msg;
673
+ // Quick check: does this message have any blocks to strip?
674
+ const hasStripTarget = msg.content.some((_, j) =>
675
+ toStrip.has(`${idx}:${j}`),
676
+ );
677
+ if (!hasStripTarget) return msg;
678
+
652
679
  return {
653
680
  ...msg,
654
- content: msg.content.map((block) => {
681
+ content: msg.content.map((block, j) => {
655
682
  if (
683
+ toStrip.has(`${idx}:${j}`) &&
656
684
  block.type === "tool_result" &&
657
- typeof block.content === "string" &&
658
- block.content.includes("<ax-tree>")
685
+ typeof block.content === "string"
659
686
  ) {
660
687
  return {
661
688
  ...block,
@@ -16,7 +16,7 @@ Conversational guardian verification control-plane invocation is guardian-only.
16
16
 
17
17
  ## Memory Provenance Invariant
18
18
 
19
- All memory extraction and retrieval decisions must consider actor-role provenance. Untrusted actors (non-guardian, unverified_channel) must not trigger profile extraction or receive memory recall/conflict disclosures. This invariant is enforced in `indexer.ts` (write gate) and `session-memory.ts` (read gate).
19
+ All memory retrieval decisions must consider actor-role provenance. Untrusted actors (non-guardian, unverified_channel) must not receive memory recall results. This invariant is enforced in `indexer.ts` (write gate) and `session-memory.ts` (read gate).
20
20
 
21
21
  ## Guardian Privilege Isolation Invariant
22
22
 
@@ -424,11 +424,17 @@ const accessRequestResolver: GuardianRequestResolver = {
424
424
  dedupeKey: `trusted-contact:denied:${request.id}`,
425
425
  });
426
426
  } else if (desktopDeliverUrl && requesterChatId) {
427
+ // For Slack, route to DM via requesterExternalUserId (user ID) instead
428
+ // of requesterChatId (channel ID) to avoid posting in public channels.
429
+ const targetChatId =
430
+ channel === "slack" && requesterExternalUserId
431
+ ? requesterExternalUserId
432
+ : requesterChatId;
427
433
  try {
428
434
  await deliverChannelReply(
429
435
  desktopDeliverUrl,
430
436
  {
431
- chatId: requesterChatId,
437
+ chatId: targetChatId,
432
438
  text: "Your access request has been denied by the guardian.",
433
439
  assistantId,
434
440
  },
@@ -601,11 +607,17 @@ const accessRequestResolver: GuardianRequestResolver = {
601
607
  });
602
608
  }
603
609
  } else if (desktopDeliverUrl && requesterChatId) {
610
+ // For Slack, route to DM via requesterExternalUserId (user ID) instead
611
+ // of requesterChatId (channel ID) to avoid posting in public channels.
612
+ const targetChatId =
613
+ channel === "slack" && requesterExternalUserId
614
+ ? requesterExternalUserId
615
+ : requesterChatId;
604
616
  try {
605
617
  await deliverChannelReply(
606
618
  desktopDeliverUrl,
607
619
  {
608
- chatId: requesterChatId,
620
+ chatId: targetChatId,
609
621
  text:
610
622
  "Your access request has been approved! " +
611
623
  "Please enter the 6-digit verification code you receive from the guardian.",
@@ -6,6 +6,7 @@
6
6
  * same pattern as EmbeddingRuntimeManager.
7
7
  */
8
8
 
9
+ import { createHash } from "node:crypto";
9
10
  import {
10
11
  chmodSync,
11
12
  existsSync,
@@ -54,11 +55,69 @@ function npmTarballUrl(pkg: string, version: string): string {
54
55
  return `https://registry.npmjs.org/${encoded}/-/${basename}-${version}.tgz`;
55
56
  }
56
57
 
58
+ async function fetchNpmIntegrity(
59
+ pkg: string,
60
+ version: string,
61
+ ): Promise<string> {
62
+ const encoded = pkg.replace("/", "%2f");
63
+ const metadataUrl = `https://registry.npmjs.org/${encoded}/${version}`;
64
+ const response = await fetch(metadataUrl);
65
+ if (!response.ok) {
66
+ throw new Error(
67
+ `Failed to fetch npm metadata for ${pkg}@${version}: ${response.status} ${response.statusText}`,
68
+ );
69
+ }
70
+
71
+ const data = (await response.json()) as {
72
+ dist?: { integrity?: string; shasum?: string };
73
+ };
74
+
75
+ if (
76
+ typeof data.dist?.integrity === "string" &&
77
+ data.dist.integrity.length > 0
78
+ ) {
79
+ return data.dist.integrity;
80
+ }
81
+
82
+ if (typeof data.dist?.shasum === "string" && data.dist.shasum.length > 0) {
83
+ return `sha1-${Buffer.from(data.dist.shasum, "hex").toString("base64")}`;
84
+ }
85
+
86
+ throw new Error(`Missing npm integrity metadata for ${pkg}@${version}`);
87
+ }
88
+
89
+ function verifyIntegrity(
90
+ tarball: Uint8Array,
91
+ integrity: string,
92
+ pkg: string,
93
+ version: string,
94
+ ): void {
95
+ const [algorithm, expectedDigest] = integrity.split("-", 2);
96
+ if (!algorithm || !expectedDigest) {
97
+ throw new Error(`Invalid integrity metadata for ${pkg}@${version}`);
98
+ }
99
+
100
+ if (algorithm !== "sha512" && algorithm !== "sha1") {
101
+ throw new Error(
102
+ `Unsupported integrity algorithm ${algorithm} for ${pkg}@${version}`,
103
+ );
104
+ }
105
+
106
+ const actualDigest = createHash(algorithm).update(tarball).digest("base64");
107
+ if (actualDigest !== expectedDigest) {
108
+ throw new Error(`Integrity verification failed for ${pkg}@${version}`);
109
+ }
110
+ }
111
+
57
112
  async function downloadAndExtract(
113
+ pkg: string,
114
+ version: string,
58
115
  url: string,
59
116
  targetDir: string,
60
117
  ): Promise<void> {
61
- log.info({ url, targetDir }, "Downloading npm package");
118
+ log.info({ pkg, version, url, targetDir }, "Downloading npm package");
119
+
120
+ const integrity = await fetchNpmIntegrity(pkg, version);
62
121
 
63
122
  const response = await fetch(url);
64
123
  if (!response.ok) {
@@ -67,7 +126,8 @@ async function downloadAndExtract(
67
126
  );
68
127
  }
69
128
 
70
- const tarball = await response.arrayBuffer();
129
+ const tarball = new Uint8Array(await response.arrayBuffer());
130
+ verifyIntegrity(tarball, integrity, pkg, version);
71
131
  mkdirSync(targetDir, { recursive: true });
72
132
 
73
133
  const tmpTar = join(targetDir, `download-${Date.now()}.tgz`);
@@ -191,10 +251,14 @@ async function install(baseDir: string): Promise<void> {
191
251
  // Download esbuild binary + preact in parallel
192
252
  await Promise.all([
193
253
  downloadAndExtract(
254
+ `@esbuild/${esbuildPlatform}`,
255
+ ESBUILD_VERSION,
194
256
  npmTarballUrl(`@esbuild/${esbuildPlatform}`, ESBUILD_VERSION),
195
257
  join(tmpDir, "esbuild-pkg"),
196
258
  ),
197
259
  downloadAndExtract(
260
+ "preact",
261
+ PREACT_VERSION,
198
262
  npmTarballUrl("preact", PREACT_VERSION),
199
263
  join(tmpDir, "node_modules", "preact"),
200
264
  ),
@@ -1017,3 +1017,135 @@ export async function startVerificationCall(
1017
1017
  };
1018
1018
  }
1019
1019
  }
1020
+
1021
+ // ── Invite call ───────────────────────────────────────────────────────
1022
+
1023
+ export type StartInviteCallInput = {
1024
+ phoneNumber: string;
1025
+ friendName: string;
1026
+ guardianName: string;
1027
+ assistantId?: string;
1028
+ };
1029
+
1030
+ export type StartInviteCallResult = { ok: true; callSid: string } | CallError;
1031
+
1032
+ /**
1033
+ * Initiate an outbound call to deliver a voice invite to a contact.
1034
+ *
1035
+ * Creates a minimal call session with a voice channel binding and
1036
+ * passes invite-specific custom parameters so the relay server can
1037
+ * detect this is an invite redemption call.
1038
+ */
1039
+ export async function startInviteCall(
1040
+ input: StartInviteCallInput,
1041
+ ): Promise<StartInviteCallResult> {
1042
+ const { phoneNumber, friendName, guardianName } = input;
1043
+
1044
+ if (!phoneNumber || !E164_REGEX.test(phoneNumber)) {
1045
+ return {
1046
+ ok: false,
1047
+ error: "phone_number must be in E.164 format",
1048
+ status: 400,
1049
+ };
1050
+ }
1051
+
1052
+ let sessionId: string | null = null;
1053
+
1054
+ try {
1055
+ const config = loadConfig();
1056
+ const provider = new TwilioConversationRelayProvider();
1057
+
1058
+ // Resolve the assistant's Twilio number as the caller ID
1059
+ const identityResult = await resolveCallerIdentity(config);
1060
+ if (!identityResult.ok) {
1061
+ return { ok: false, error: identityResult.error, status: 400 };
1062
+ }
1063
+
1064
+ const preflightResult = await preflightVoiceIngress();
1065
+ if (!preflightResult.ok) {
1066
+ return preflightResult;
1067
+ }
1068
+ const ingressConfig = preflightResult.ingressConfig;
1069
+
1070
+ // Create a minimal conversation so the call session has a valid FK,
1071
+ // and bind it to the voice channel so it never appears as an unbound
1072
+ // desktop thread.
1073
+ const timestamp = Date.now();
1074
+ const convKey = `invite-call:${phoneNumber}:${timestamp}`;
1075
+ const { conversationId } = getOrCreateConversation(convKey);
1076
+
1077
+ upsertBinding({
1078
+ conversationId,
1079
+ sourceChannel: "phone",
1080
+ externalChatId: `invite-call:${phoneNumber}:${timestamp}`,
1081
+ });
1082
+
1083
+ const session = createCallSession({
1084
+ conversationId,
1085
+ provider: "twilio",
1086
+ fromNumber: identityResult.fromNumber,
1087
+ toNumber: phoneNumber,
1088
+ callMode: "invite",
1089
+ inviteFriendName: friendName,
1090
+ inviteGuardianName: guardianName,
1091
+ initiatedFromConversationId: conversationId,
1092
+ });
1093
+ sessionId = session.id;
1094
+
1095
+ const webhookUrl = await resolveCallbackUrl(
1096
+ () => getTwilioVoiceWebhookUrl(ingressConfig, session.id),
1097
+ "webhooks/twilio/voice",
1098
+ "twilio_voice",
1099
+ { callSessionId: session.id },
1100
+ );
1101
+ const statusCallbackUrl = await resolveCallbackUrl(
1102
+ () => getTwilioStatusCallbackUrl(ingressConfig),
1103
+ "webhooks/twilio/status",
1104
+ "twilio_status",
1105
+ );
1106
+
1107
+ upsertActiveCallLease({ callSessionId: session.id });
1108
+
1109
+ const { callSid } = await provider.initiateCall({
1110
+ from: identityResult.fromNumber,
1111
+ to: phoneNumber,
1112
+ webhookUrl,
1113
+ statusCallbackUrl,
1114
+ });
1115
+
1116
+ updateCallSession(session.id, { providerCallSid: callSid });
1117
+
1118
+ log.info(
1119
+ {
1120
+ callSessionId: session.id,
1121
+ callSid,
1122
+ to: phoneNumber,
1123
+ friendName,
1124
+ guardianName,
1125
+ },
1126
+ "Invite call initiated",
1127
+ );
1128
+
1129
+ return { ok: true, callSid };
1130
+ } catch (err) {
1131
+ const msg = err instanceof Error ? err.message : String(err);
1132
+ log.error(
1133
+ { err, phoneNumber, friendName, guardianName },
1134
+ "Failed to initiate invite call",
1135
+ );
1136
+
1137
+ if (sessionId) {
1138
+ updateCallSession(sessionId, {
1139
+ status: "failed",
1140
+ endedAt: Date.now(),
1141
+ lastError: msg,
1142
+ });
1143
+ }
1144
+
1145
+ return {
1146
+ ok: false,
1147
+ error: `Error initiating invite call: ${msg}`,
1148
+ status: 500,
1149
+ };
1150
+ }
1151
+ }
@@ -37,6 +37,8 @@ const parseCallSession = createRowMapper<
37
37
  status: { from: "status", transform: cast<CallSession["status"]>() },
38
38
  callMode: { from: "callMode", transform: cast<CallSession["callMode"]>() },
39
39
  verificationSessionId: "verificationSessionId",
40
+ inviteFriendName: "inviteFriendName",
41
+ inviteGuardianName: "inviteGuardianName",
40
42
  callerIdentityMode: "callerIdentityMode",
41
43
  callerIdentitySource: "callerIdentitySource",
42
44
  initiatedFromConversationId: "initiatedFromConversationId",
@@ -81,6 +83,8 @@ export function createCallSession(opts: {
81
83
  task?: string;
82
84
  callMode?: string;
83
85
  verificationSessionId?: string;
86
+ inviteFriendName?: string;
87
+ inviteGuardianName?: string;
84
88
  callerIdentityMode?: string;
85
89
  callerIdentitySource?: string;
86
90
  initiatedFromConversationId?: string;
@@ -98,6 +102,8 @@ export function createCallSession(opts: {
98
102
  status: "initiated" as const,
99
103
  callMode: (opts.callMode ?? null) as CallSession["callMode"],
100
104
  verificationSessionId: opts.verificationSessionId ?? null,
105
+ inviteFriendName: opts.inviteFriendName ?? null,
106
+ inviteGuardianName: opts.inviteGuardianName ?? null,
101
107
  callerIdentityMode: opts.callerIdentityMode ?? null,
102
108
  callerIdentitySource: opts.callerIdentitySource ?? null,
103
109
  initiatedFromConversationId: opts.initiatedFromConversationId ?? null,
@@ -575,6 +575,7 @@ export class RelayConnection {
575
575
  outcome.fromNumber,
576
576
  outcome.friendName,
577
577
  outcome.guardianName,
578
+ !resolved.isInbound,
578
579
  );
579
580
  return;
580
581
  case "name_capture":
@@ -772,6 +773,12 @@ export class RelayConnection {
772
773
  fromNumber: string;
773
774
  callerName?: string;
774
775
  skipMemberActivation?: boolean;
776
+ activationReason?:
777
+ | "invite_redeemed"
778
+ | "access_approved"
779
+ | "trusted_contact_verified";
780
+ friendName?: string;
781
+ guardianName?: string;
775
782
  }): void {
776
783
  const { assistantId, fromNumber, callerName } = params;
777
784
 
@@ -808,7 +815,25 @@ export class RelayConnection {
808
815
  updateCallSession(this.callSessionId, { status: "in_progress" });
809
816
 
810
817
  const guardianLabel = this.resolveGuardianLabel();
811
- const handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
818
+ let handoffText: string;
819
+
820
+ if (params.activationReason === "invite_redeemed") {
821
+ const name = params.friendName;
822
+ const assistantName = this.resolveAssistantLabel();
823
+ const gLabel = params.guardianName || guardianLabel;
824
+ if (name) {
825
+ handoffText = assistantName
826
+ ? `Great, I've verified that you are ${name}. It's nice to meet you! I'm ${assistantName}, ${gLabel}'s assistant. How can I help?`
827
+ : `Great, I've verified that you are ${name}. It's nice to meet you! How can I help?`;
828
+ } else {
829
+ handoffText = assistantName
830
+ ? `Great, I've verified your identity. It's nice to meet you! I'm ${assistantName}, ${gLabel}'s assistant. How can I help?`
831
+ : `Great, I've verified your identity. It's nice to meet you! How can I help?`;
832
+ }
833
+ } else {
834
+ handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
835
+ }
836
+
812
837
  this.sendTextToken(handoffText, true);
813
838
 
814
839
  recordCallEvent(this.callSessionId, "assistant_spoke", {
@@ -1000,6 +1025,7 @@ export class RelayConnection {
1000
1025
  this.continueCallAfterTrustedContactActivation({
1001
1026
  assistantId,
1002
1027
  fromNumber,
1028
+ activationReason: "trusted_contact_verified",
1003
1029
  });
1004
1030
  } else {
1005
1031
  // Inbound guardian verification: binding already handled above,
@@ -1096,6 +1122,7 @@ export class RelayConnection {
1096
1122
  fromNumber: string,
1097
1123
  friendName: string | null,
1098
1124
  guardianName: string | null,
1125
+ isOutbound: boolean,
1099
1126
  ): void {
1100
1127
  this.inviteRedemptionActive = true;
1101
1128
  this.inviteRedemptionAssistantId = assistantId;
@@ -1116,10 +1143,17 @@ export class RelayConnection {
1116
1143
 
1117
1144
  const displayFriend = friendName ?? "there";
1118
1145
  const displayGuardian = guardianName ?? "your contact";
1119
- this.sendTextToken(
1120
- `Welcome ${displayFriend}. Please enter the 6-digit code that ${displayGuardian} provided you to verify your identity.`,
1121
- true,
1122
- );
1146
+
1147
+ let promptText: string;
1148
+ if (isOutbound) {
1149
+ const assistantName = this.resolveAssistantLabel();
1150
+ promptText = assistantName
1151
+ ? `Hi ${displayFriend}, this is ${assistantName}, ${displayGuardian}'s assistant. To get started, please enter the 6-digit code that ${displayGuardian} shared with you.`
1152
+ : `Hi ${displayFriend}, this is ${displayGuardian}'s assistant. To get started, please enter the 6-digit code that ${displayGuardian} shared with you.`;
1153
+ } else {
1154
+ promptText = `Welcome ${displayFriend}. Please enter the 6-digit code that ${displayGuardian} provided you to verify your identity.`;
1155
+ }
1156
+ this.sendTextToken(promptText, true);
1123
1157
 
1124
1158
  log.info(
1125
1159
  { callSessionId: this.callSessionId, assistantId },
@@ -1358,6 +1392,7 @@ export class RelayConnection {
1358
1392
  assistantId,
1359
1393
  fromNumber,
1360
1394
  callerName: callerName ?? undefined,
1395
+ activationReason: "access_approved",
1361
1396
  });
1362
1397
 
1363
1398
  recordCallEvent(
@@ -1541,6 +1576,9 @@ export class RelayConnection {
1541
1576
  fromNumber: this.inviteRedemptionFromNumber,
1542
1577
  callerName: this.inviteRedemptionFriendName ?? undefined,
1543
1578
  skipMemberActivation: true,
1579
+ activationReason: "invite_redeemed",
1580
+ friendName: this.inviteRedemptionFriendName ?? undefined,
1581
+ guardianName: this.inviteRedemptionGuardianName ?? undefined,
1544
1582
  });
1545
1583
  } else {
1546
1584
  this.inviteRedemptionActive = false;
@@ -99,8 +99,24 @@ export function routeSetup(ctx: SetupContext): {
99
99
  actorTrust,
100
100
  };
101
101
 
102
- // ── Outbound guardian verification (persisted mode) ──────────────
102
+ // ── Outbound flow selection based on persisted call mode ──────────
103
103
  const persistedMode = ctx.session?.callMode;
104
+
105
+ // ── Outbound invite redemption (persisted mode) ─────────────────
106
+ if (persistedMode === "invite") {
107
+ return {
108
+ outcome: {
109
+ action: "invite_redemption" as const,
110
+ assistantId,
111
+ fromNumber: ctx.to,
112
+ friendName: ctx.session?.inviteFriendName ?? null,
113
+ guardianName: ctx.session?.inviteGuardianName ?? null,
114
+ },
115
+ resolved,
116
+ };
117
+ }
118
+
119
+ // ── Outbound guardian verification (persisted mode) ──────────────
104
120
  const persistedVsId = ctx.session?.verificationSessionId;
105
121
  const customParamVsId = ctx.customParameters?.verificationSessionId;
106
122
  const verificationSessionId = persistedVsId ?? customParamVsId;
@@ -20,7 +20,7 @@ export interface TwilioConfig {
20
20
 
21
21
  /**
22
22
  * Resolve the Twilio phone number using a unified fallback chain so that
23
- * all callers (calls, SMS adapter, readiness checks, invite transports)
23
+ * all callers (calls, readiness checks, invite transports)
24
24
  * agree on the same number.
25
25
  *
26
26
  * Resolution order:
@@ -58,7 +58,7 @@ export type PendingQuestionStatus =
58
58
  * uses this as the primary signal for deterministic flow selection,
59
59
  * with Twilio setup custom parameters as a secondary/observability signal.
60
60
  */
61
- export type CallMode = "normal" | "verification";
61
+ export type CallMode = "normal" | "verification" | "invite";
62
62
 
63
63
  export interface CallSession {
64
64
  id: string;
@@ -71,6 +71,8 @@ export interface CallSession {
71
71
  status: CallStatus;
72
72
  callMode: CallMode | null;
73
73
  verificationSessionId: string | null;
74
+ inviteFriendName: string | null;
75
+ inviteGuardianName: string | null;
74
76
  callerIdentityMode: string | null;
75
77
  callerIdentitySource: string | null;
76
78
  initiatedFromConversationId?: string | null;
@@ -3,8 +3,10 @@ import { existsSync, readFileSync, statSync } from "node:fs";
3
3
 
4
4
  import type { Command } from "commander";
5
5
 
6
+ import { getRuntimeHttpPort } from "../../config/env.js";
6
7
  import { loadRawConfig } from "../../config/loader.js";
7
8
  import { shouldAutoStartDaemon } from "../../daemon/connection-policy.js";
9
+ import { isHttpHealthy } from "../../daemon/daemon-control.js";
8
10
  import {
9
11
  getDbPath,
10
12
  getLogPath,
@@ -13,7 +15,6 @@ import {
13
15
  getWorkspaceHooksDir,
14
16
  getWorkspaceSkillsDir,
15
17
  } from "../../util/platform.js";
16
- import { getHttpBaseUrl, httpHealthCheck } from "../http-client.js";
17
18
  import { log } from "../logger.js";
18
19
 
19
20
  export function registerDoctorCommand(program: Command): void {
@@ -57,7 +58,7 @@ Examples:
57
58
  log.info("Vellum Doctor\n");
58
59
 
59
60
  // 0. Connection policy info
60
- const httpUrl = getHttpBaseUrl();
61
+ const httpUrl = `http://127.0.0.1:${getRuntimeHttpPort()}`;
61
62
  const autostart = shouldAutoStartDaemon();
62
63
  log.info(` HTTP: ${httpUrl}`);
63
64
  log.info(` Autostart: ${autostart ? "enabled" : "disabled"}\n`);
@@ -103,7 +104,7 @@ Examples:
103
104
 
104
105
  // 3. Daemon reachable (HTTP health check)
105
106
  try {
106
- const healthy = await httpHealthCheck(2000);
107
+ const healthy = await isHttpHealthy();
107
108
  if (healthy) {
108
109
  pass("Assistant reachable");
109
110
  } else {