@vellumai/assistant 0.10.3 → 0.10.4-staging.1

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/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -4,6 +4,8 @@ import type { TrustVerdict } from "@vellumai/gateway-client";
4
4
 
5
5
  import { channelStatusToMemberStatus } from "../../contacts/member-status.js";
6
6
  import type {
7
+ ChannelPolicy,
8
+ ChannelStatus,
7
9
  ContactChannel,
8
10
  ContactWithChannels,
9
11
  } from "../../contacts/types.js";
@@ -11,7 +13,6 @@ import type { ActorTrustContext } from "../actor-trust-resolver.js";
11
13
  import { toTrustContext } from "../actor-trust-resolver.js";
12
14
  import {
13
15
  actorTrustContextFromVerdict,
14
- resolvedMemberFromVerdict,
15
16
  trustContextFromVerdict,
16
17
  verdictHasMemberIdentity,
17
18
  verdictMemberFromVerdict,
@@ -288,8 +289,77 @@ describe("actorTrustContextFromVerdict", () => {
288
289
  expect(ctx.memberRecord).not.toBeNull();
289
290
  expect(ctx.memberRecord!.contact.id).toBe("contact-1");
290
291
  expect(ctx.memberRecord!.channel.id).toBe("channel-1");
291
- expect(ctx.memberRecord!.channel.status).toBe("blocked");
292
- expect(ctx.memberRecord!.channel.policy).toBe("deny");
292
+ expect(ctx.memberRecord!.status).toBe("blocked");
293
+ expect(ctx.memberRecord!.policy).toBe("deny");
294
+ });
295
+
296
+ test("memberRecord surfaces channel ACL/identity, info fields null", () => {
297
+ const verdict = {
298
+ trustClass: "trusted_contact",
299
+ canonicalSenderId: "u-1",
300
+ contactId: "contact-1",
301
+ channelId: "channel-1",
302
+ type: "slack",
303
+ address: "u-1",
304
+ status: "active",
305
+ policy: "allow",
306
+ externalChatId: "chat-1",
307
+ verifiedAt: 1700000000,
308
+ verifiedVia: "code",
309
+ memberDisplayName: "Dora",
310
+ } satisfies TrustVerdict;
311
+
312
+ const { memberRecord } = actorTrustContextFromVerdict(verdict, {
313
+ sourceChannel: "slack",
314
+ conversationExternalId: CONV,
315
+ });
316
+ expect(memberRecord!.status).toBe("active");
317
+ expect(memberRecord!.policy).toBe("allow");
318
+ expect(memberRecord!.channel.externalChatId).toBe("chat-1");
319
+
320
+ expect(memberRecord!.contact.displayName).toBe("Dora");
321
+ expect(memberRecord!.role).toBe("contact");
322
+ // INFO fields must be null/default placeholders.
323
+ expect(memberRecord!.contact.notes).toBeNull();
324
+ expect(memberRecord!.contact.userFile).toBeNull();
325
+ expect(memberRecord!.contact.interactionCount).toBe(0);
326
+ expect(memberRecord!.contact.lastInteraction).toBeNull();
327
+ });
328
+
329
+ test("guardian member verdict maps role guardian + principalId", () => {
330
+ const verdict = {
331
+ trustClass: "guardian",
332
+ canonicalSenderId: "u-g",
333
+ contactId: "contact-g",
334
+ channelId: "channel-g",
335
+ guardianPrincipalId: "vellum-principal-g",
336
+ status: "active",
337
+ policy: "allow",
338
+ } satisfies TrustVerdict;
339
+
340
+ const ctx = actorTrustContextFromVerdict(verdict, {
341
+ sourceChannel: "slack",
342
+ conversationExternalId: CONV,
343
+ });
344
+ expect(ctx.memberRecord!.role).toBe("guardian");
345
+ expect(ctx.guardianPrincipalId).toBe("vellum-principal-g");
346
+ });
347
+
348
+ test("memberRecord null when status missing (fail-closed)", () => {
349
+ const verdict = {
350
+ trustClass: "trusted_contact",
351
+ canonicalSenderId: "u-4",
352
+ contactId: "contact-4",
353
+ channelId: "channel-4",
354
+ policy: "allow",
355
+ } satisfies TrustVerdict;
356
+
357
+ expect(
358
+ actorTrustContextFromVerdict(verdict, {
359
+ sourceChannel: "slack",
360
+ conversationExternalId: CONV,
361
+ }).memberRecord,
362
+ ).toBeNull();
293
363
  });
294
364
 
295
365
  test("stranger verdict (no contactId/channelId) leaves memberRecord null", () => {
@@ -347,20 +417,18 @@ describe("actorTrustContextFromVerdict", () => {
347
417
  actorTrustContextFromVerdict(verdict, input),
348
418
  input.conversationExternalId,
349
419
  );
350
- const member = resolvedMemberFromVerdict(verdict);
420
+ const member = verdictMemberFromVerdict(verdict);
351
421
  expect(member).not.toBeNull();
352
- expected.requesterContactId = member!.contact.id;
353
- expected.memberStatus = channelStatusToMemberStatus(member!.channel.status);
354
- expected.memberPolicy = member!.channel.policy;
422
+ expected.requesterContactId = member!.contactId;
423
+ expected.memberStatus = channelStatusToMemberStatus(member!.status);
424
+ expected.memberPolicy = member!.policy;
355
425
 
356
426
  expect(trustContextFromVerdict(verdict, input)).toEqual(expected);
357
427
  });
358
428
  });
359
429
 
360
430
  describe("toTrustContext member grounding", () => {
361
- function memberChannel(
362
- overrides: Partial<ContactChannel> = {},
363
- ): ContactChannel {
431
+ function memberChannel(): ContactChannel {
364
432
  return {
365
433
  id: "channel-1",
366
434
  contactId: "contact-1",
@@ -368,19 +436,12 @@ describe("toTrustContext member grounding", () => {
368
436
  address: "+15550100",
369
437
  isPrimary: true,
370
438
  externalChatId: null,
371
- status: "unverified",
372
- policy: "escalate",
373
- verifiedAt: null,
374
- verifiedVia: null,
375
439
  inviteId: null,
376
- revokedReason: null,
377
- blockedReason: null,
378
440
  lastSeenAt: null,
379
441
  interactionCount: 0,
380
442
  lastInteraction: null,
381
443
  updatedAt: null,
382
444
  createdAt: 0,
383
- ...overrides,
384
445
  };
385
446
  }
386
447
 
@@ -389,24 +450,34 @@ describe("toTrustContext member grounding", () => {
389
450
  id: "contact-1",
390
451
  displayName: "Frank",
391
452
  notes: null,
453
+ role: "contact",
392
454
  lastInteraction: null,
393
455
  interactionCount: 0,
394
456
  createdAt: 0,
395
457
  updatedAt: 0,
396
- role: "contact",
397
458
  contactType: "human",
398
- principalId: null,
399
459
  userFile: null,
400
460
  channels: [memberChannel()],
401
461
  };
402
462
  }
403
463
 
404
- function ctxWithMember(channel: ContactChannel): ActorTrustContext {
464
+ function ctxWithMember(
465
+ acl: { status: ChannelStatus; policy: ChannelPolicy } = {
466
+ status: "unverified",
467
+ policy: "escalate",
468
+ },
469
+ ): ActorTrustContext {
405
470
  return {
406
471
  canonicalSenderId: "+15550100",
407
472
  guardianBindingMatch: null,
408
473
  guardianPrincipalId: undefined,
409
- memberRecord: { contact: memberContact(), channel },
474
+ memberRecord: {
475
+ contact: memberContact(),
476
+ channel: memberChannel(),
477
+ status: acl.status,
478
+ policy: acl.policy,
479
+ role: "contact",
480
+ },
410
481
  trustClass: "trusted_contact",
411
482
  actorMetadata: {
412
483
  identifier: "+15550100",
@@ -421,7 +492,7 @@ describe("toTrustContext member grounding", () => {
421
492
  }
422
493
 
423
494
  test("populates member fields from memberRecord (voice path)", () => {
424
- const context = toTrustContext(ctxWithMember(memberChannel()), CONV);
495
+ const context = toTrustContext(ctxWithMember(), CONV);
425
496
  expect(context.requesterContactId).toBe("contact-1");
426
497
  // "unverified" maps to the API-facing "pending" member status.
427
498
  expect(context.memberStatus).toBe("pending");
@@ -430,7 +501,7 @@ describe("toTrustContext member grounding", () => {
430
501
 
431
502
  test("passes through active status + allow policy", () => {
432
503
  const context = toTrustContext(
433
- ctxWithMember(memberChannel({ status: "active", policy: "allow" })),
504
+ ctxWithMember({ status: "active", policy: "allow" }),
434
505
  CONV,
435
506
  );
436
507
  expect(context.memberStatus).toBe("active");
@@ -463,151 +534,6 @@ describe("toTrustContext member grounding", () => {
463
534
  });
464
535
  });
465
536
 
466
- describe("resolvedMemberFromVerdict", () => {
467
- test("member verdict surfaces channel ACL/identity, info fields null", () => {
468
- const verdict = {
469
- trustClass: "trusted_contact",
470
- canonicalSenderId: "u-1",
471
- contactId: "contact-1",
472
- channelId: "channel-1",
473
- type: "slack",
474
- address: "u-1",
475
- status: "active",
476
- policy: "allow",
477
- externalChatId: "chat-1",
478
- verifiedAt: 1700000000,
479
- verifiedVia: "code",
480
- memberDisplayName: "Dora",
481
- } satisfies TrustVerdict;
482
-
483
- const member = resolvedMemberFromVerdict(verdict);
484
- expect(member).not.toBeNull();
485
- expect(member!.channel.id).toBe("channel-1");
486
- expect(member!.channel.status).toBe("active");
487
- expect(member!.channel.policy).toBe("allow");
488
- expect(member!.channel.verifiedAt).toBe(1700000000);
489
- expect(member!.channel.verifiedVia).toBe("code");
490
- expect(member!.channel.externalChatId).toBe("chat-1");
491
-
492
- expect(member!.contact.id).toBe("contact-1");
493
- expect(member!.contact.displayName).toBe("Dora");
494
- expect(member!.contact.role).toBe("contact");
495
- // INFO fields must be null/default placeholders.
496
- expect(member!.contact.notes).toBeNull();
497
- expect(member!.contact.userFile).toBeNull();
498
- expect(member!.contact.interactionCount).toBe(0);
499
- expect(member!.contact.lastInteraction).toBeNull();
500
- });
501
-
502
- test("guardian member verdict maps role guardian + principalId", () => {
503
- const verdict = {
504
- trustClass: "guardian",
505
- canonicalSenderId: "u-g",
506
- contactId: "contact-g",
507
- channelId: "channel-g",
508
- guardianPrincipalId: "vellum-principal-g",
509
- status: "active",
510
- policy: "allow",
511
- } satisfies TrustVerdict;
512
-
513
- const member = resolvedMemberFromVerdict(verdict);
514
- expect(member!.contact.role).toBe("guardian");
515
- expect(member!.contact.principalId).toBe("vellum-principal-g");
516
- });
517
-
518
- test("memberless verdict (no contactId) returns null", () => {
519
- const verdict = {
520
- trustClass: "unknown",
521
- canonicalSenderId: "u-2",
522
- } satisfies TrustVerdict;
523
-
524
- expect(resolvedMemberFromVerdict(verdict)).toBeNull();
525
- });
526
-
527
- test("member verdict missing status returns null (fail-closed)", () => {
528
- const verdict = {
529
- trustClass: "trusted_contact",
530
- canonicalSenderId: "u-4",
531
- contactId: "contact-4",
532
- channelId: "channel-4",
533
- policy: "allow",
534
- } satisfies TrustVerdict;
535
-
536
- expect(resolvedMemberFromVerdict(verdict)).toBeNull();
537
- });
538
-
539
- test("member verdict missing policy returns null (fail-closed)", () => {
540
- const verdict = {
541
- trustClass: "trusted_contact",
542
- canonicalSenderId: "u-5",
543
- contactId: "contact-5",
544
- channelId: "channel-5",
545
- status: "active",
546
- } satisfies TrustVerdict;
547
-
548
- expect(resolvedMemberFromVerdict(verdict)).toBeNull();
549
- });
550
-
551
- test("member verdict with unknown policy returns null (fail-closed)", () => {
552
- const verdict = {
553
- trustClass: "trusted_contact",
554
- canonicalSenderId: "u-6",
555
- contactId: "contact-6",
556
- channelId: "channel-6",
557
- status: "active",
558
- policy: "bogus",
559
- } satisfies TrustVerdict;
560
-
561
- expect(resolvedMemberFromVerdict(verdict)).toBeNull();
562
- });
563
-
564
- test("member verdict with unknown status returns null (fail-closed)", () => {
565
- const verdict = {
566
- trustClass: "trusted_contact",
567
- canonicalSenderId: "u-7",
568
- contactId: "contact-7",
569
- channelId: "channel-7",
570
- status: "quarantined",
571
- policy: "allow",
572
- } satisfies TrustVerdict;
573
-
574
- expect(resolvedMemberFromVerdict(verdict)).toBeNull();
575
- });
576
-
577
- test("member verdict with valid known status+policy returns a member", () => {
578
- const verdict = {
579
- trustClass: "trusted_contact",
580
- canonicalSenderId: "u-8",
581
- contactId: "contact-8",
582
- channelId: "channel-8",
583
- status: "active",
584
- policy: "allow",
585
- } satisfies TrustVerdict;
586
-
587
- const member = resolvedMemberFromVerdict(verdict);
588
- expect(member).not.toBeNull();
589
- expect(member!.channel.status).toBe("active");
590
- expect(member!.channel.policy).toBe("allow");
591
- });
592
-
593
- test("blocked/revoked verdict surfaces channel.status verbatim", () => {
594
- for (const status of ["blocked", "revoked"] as const) {
595
- const verdict = {
596
- trustClass: "unknown",
597
- canonicalSenderId: "u-3",
598
- contactId: "contact-3",
599
- channelId: "channel-3",
600
- status,
601
- policy: "deny",
602
- } satisfies TrustVerdict;
603
-
604
- const member = resolvedMemberFromVerdict(verdict);
605
- expect(member!.channel.status).toBe(status);
606
- expect(member!.channel.policy).toBe("deny");
607
- }
608
- });
609
- });
610
-
611
537
  describe("verdictMemberFromVerdict", () => {
612
538
  test("active member verdict yields the narrow ACL view", () => {
613
539
  const verdict = {
@@ -684,6 +610,27 @@ describe("verdictMemberFromVerdict", () => {
684
610
  } satisfies TrustVerdict),
685
611
  ).toBeNull();
686
612
  });
613
+
614
+ test("missing status or policy returns null (fail-closed)", () => {
615
+ expect(
616
+ verdictMemberFromVerdict({
617
+ trustClass: "trusted_contact",
618
+ canonicalSenderId: "u-4",
619
+ contactId: "contact-4",
620
+ channelId: "channel-4",
621
+ policy: "allow",
622
+ } satisfies TrustVerdict),
623
+ ).toBeNull();
624
+ expect(
625
+ verdictMemberFromVerdict({
626
+ trustClass: "trusted_contact",
627
+ canonicalSenderId: "u-5",
628
+ contactId: "contact-5",
629
+ channelId: "channel-5",
630
+ status: "active",
631
+ } satisfies TrustVerdict),
632
+ ).toBeNull();
633
+ });
687
634
  });
688
635
 
689
636
  describe("verdict predicates", () => {
@@ -97,11 +97,10 @@ export async function notifyGuardianOfAccessRequest(
97
97
 
98
98
  // Resolve guardian identity with the assistant-anchored strategy (gateway
99
99
  // source-channel match validated against the vellum anchor, else the vellum
100
- // anchor), with a LOCAL-store fallback when the gateway read is empty.
100
+ // anchor).
101
101
  const anchored = resolveAnchoredGuardian({
102
102
  guardians: await getGuardianDelivery(),
103
103
  sourceChannel,
104
- useLocalFallback: true,
105
104
  });
106
105
  const guardianExternalUserId = anchored?.address ?? null;
107
106
  const guardianPrincipalId = anchored?.principalId ?? null;
@@ -17,15 +17,23 @@
17
17
  */
18
18
 
19
19
  import type { ChannelId } from "../channels/types.js";
20
+ import { findContactByAddress } from "../contacts/contact-store.js";
20
21
  import {
21
- findContactByAddress,
22
- findGuardianForChannel,
23
- } from "../contacts/contact-store.js";
22
+ guardianForChannel,
23
+ peekCachedGuardianDelivery,
24
+ } from "../contacts/guardian-delivery-reader.js";
24
25
  import { channelStatusToMemberStatus } from "../contacts/member-status.js";
25
- import type { ContactChannel, ContactWithChannels } from "../contacts/types.js";
26
+ import type {
27
+ ChannelPolicy,
28
+ ChannelStatus,
29
+ ContactChannel,
30
+ ContactRole,
31
+ ContactWithChannels,
32
+ } from "../contacts/types.js";
26
33
  import type { TrustContext } from "../daemon/trust-context.js";
27
34
  import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
28
35
  import { getLogger } from "../util/logger.js";
36
+ import { getCachedMemberAcl } from "./member-verdict-cache.js";
29
37
 
30
38
  const log = getLogger("actor-trust-resolver");
31
39
 
@@ -78,10 +86,18 @@ export interface ActorTrustContext {
78
86
  } | null;
79
87
  /** Canonical principal ID from the guardian binding. */
80
88
  guardianPrincipalId?: string;
81
- /** Resolved contact + channel for this sender, if any. */
89
+ /**
90
+ * Resolved contact + channel for this sender, if any. The ACL view
91
+ * (status/policy/role) is carried here rather than on the contact/channel
92
+ * objects, sourced from the gateway verdict — the verdict path reads it
93
+ * inline, the sync fallback from the in-memory member-verdict cache.
94
+ */
82
95
  memberRecord: {
83
96
  contact: ContactWithChannels;
84
97
  channel: ContactChannel;
98
+ status: ChannelStatus;
99
+ policy: ChannelPolicy;
100
+ role: ContactRole;
85
101
  } | null;
86
102
  /** Trust classification. */
87
103
  trustClass: TrustClass;
@@ -175,21 +191,27 @@ export function resolveActorTrust(
175
191
  }
176
192
 
177
193
  // --- Guardian lookup ---
178
- const guardianResult = findGuardianForChannel(input.sourceChannel);
194
+ // Sync read of the gateway guardian delivery from the IO-free cache snapshot
195
+ // (kept warm by the async hot paths + daemon-startup warm). A cold cache
196
+ // yields no guardian match, the same outcome as no binding.
197
+ const cachedGuardians = peekCachedGuardianDelivery({
198
+ channelTypes: [input.sourceChannel],
199
+ });
200
+ const guardianDelivery = cachedGuardians
201
+ ? guardianForChannel(cachedGuardians, input.sourceChannel)
202
+ : undefined;
179
203
  let guardianBindingMatch: ActorTrustContext["guardianBindingMatch"] = null;
180
204
  let guardianPrincipalId: string | undefined;
181
205
  let isGuardian = false;
182
206
 
183
- if (guardianResult) {
184
- const { contact: guardianContact, channel: guardianChannel } =
185
- guardianResult;
207
+ if (guardianDelivery) {
186
208
  guardianBindingMatch = {
187
- guardianExternalUserId: guardianChannel.address,
188
- guardianDeliveryChatId: guardianChannel.externalChatId,
209
+ guardianExternalUserId: guardianDelivery.address,
210
+ guardianDeliveryChatId: guardianDelivery.externalChatId ?? null,
189
211
  };
190
- guardianPrincipalId = guardianContact.principalId ?? undefined;
212
+ guardianPrincipalId = guardianDelivery.principalId ?? undefined;
191
213
  isGuardian =
192
- guardianChannel.address.toLowerCase() === canonicalSenderId.toLowerCase();
214
+ guardianDelivery.address.toLowerCase() === canonicalSenderId.toLowerCase();
193
215
  }
194
216
 
195
217
  log.debug(
@@ -213,7 +235,12 @@ export function resolveActorTrust(
213
235
  ch.address.toLowerCase() === canonicalSenderId.toLowerCase(),
214
236
  );
215
237
  if (byAddress && byAddressChannel) {
216
- memberRecord = { contact: byAddress, channel: byAddressChannel };
238
+ const acl = getCachedMemberAcl(input.sourceChannel, canonicalSenderId);
239
+ if (acl) {
240
+ memberRecord = { contact: byAddress, channel: byAddressChannel, ...acl };
241
+ }
242
+ // Fail-closed: already in the sync fallback (no live verdict) and no cached
243
+ // verdict → leave memberRecord null so trustClass resolves to unknown.
217
244
  }
218
245
 
219
246
  log.debug(
@@ -252,7 +279,7 @@ export function resolveActorTrust(
252
279
  if (isGuardian) {
253
280
  trustClass = "guardian";
254
281
  } else if (memberMatchesSender && memberRecord) {
255
- const status = memberRecord.channel.status;
282
+ const status = memberRecord.status;
256
283
  if (status === "active") {
257
284
  trustClass = "trusted_contact";
258
285
  } else if (status === "unverified" || status === "pending") {
@@ -327,8 +354,8 @@ export function toTrustContext(
327
354
  // both populate it).
328
355
  requesterContactId: ctx.memberRecord?.contact.id,
329
356
  memberStatus: ctx.memberRecord
330
- ? channelStatusToMemberStatus(ctx.memberRecord.channel.status)
357
+ ? channelStatusToMemberStatus(ctx.memberRecord.status)
331
358
  : undefined,
332
- memberPolicy: ctx.memberRecord?.channel.policy,
359
+ memberPolicy: ctx.memberRecord?.policy,
333
360
  };
334
361
  }
@@ -2,24 +2,14 @@
2
2
  * Unit tests for `resolveAnchoredGuardian`.
3
3
  *
4
4
  * Covers the gateway arms (source-channel match validated against the vellum
5
- * anchor, vellum-anchor fallback), the LOCAL-store fallback when the gateway
6
- * list is empty, and the cosmetic `requireAnchorPrincipal` guard.
5
+ * anchor, vellum-anchor fallback) and the cosmetic `requireAnchorPrincipal`
6
+ * guard.
7
7
  */
8
- import { afterEach, describe, expect, mock, test } from "bun:test";
8
+ import { describe, expect, test } from "bun:test";
9
9
 
10
10
  import type { GuardianDelivery } from "@vellumai/gateway-client";
11
11
 
12
- // Local store fallback is mocked so we can drive both arms deterministically.
13
- let localGuardians: Record<
14
- string,
15
- { contact: { principalId: string | null; displayName: string }; channel: { address: string; type: string } } | null
16
- > = {};
17
- mock.module("../contacts/contact-store.js", () => ({
18
- findGuardianForChannel: (channelType: string) =>
19
- localGuardians[channelType] ?? null,
20
- }));
21
-
22
- const { resolveAnchoredGuardian } = await import("./anchored-guardian.js");
12
+ import { resolveAnchoredGuardian } from "./anchored-guardian.js";
23
13
 
24
14
  function gw(g: Partial<GuardianDelivery> & { channelType: string; address: string }): GuardianDelivery {
25
15
  return {
@@ -29,10 +19,6 @@ function gw(g: Partial<GuardianDelivery> & { channelType: string; address: strin
29
19
  };
30
20
  }
31
21
 
32
- afterEach(() => {
33
- localGuardians = {};
34
- });
35
-
36
22
  describe("resolveAnchoredGuardian — gateway arm", () => {
37
23
  test("source-channel guardian matching the anchor wins", () => {
38
24
  const result = resolveAnchoredGuardian({
@@ -80,40 +66,8 @@ describe("resolveAnchoredGuardian — gateway arm", () => {
80
66
  });
81
67
  });
82
68
 
83
- describe("resolveAnchoredGuardian — local fallback", () => {
84
- test("gateway empty + local source-channel match returns the local record", () => {
85
- localGuardians = {
86
- vellum: { contact: { principalId: "p-local", displayName: "LocalVellum" }, channel: { address: "lv-addr", type: "vellum" } },
87
- telegram: { contact: { principalId: "p-local", displayName: "LocalAlice" }, channel: { address: "ltg-addr", type: "telegram" } },
88
- };
89
- const result = resolveAnchoredGuardian({
90
- guardians: null,
91
- sourceChannel: "telegram",
92
- useLocalFallback: true,
93
- });
94
- expect(result).toEqual({
95
- principalId: "p-local",
96
- address: "ltg-addr",
97
- displayName: "LocalAlice",
98
- channelType: "telegram",
99
- source: "source-channel-contact",
100
- });
101
- });
102
-
103
- test("gateway empty + only local vellum returns the local vellum-anchor", () => {
104
- localGuardians = {
105
- vellum: { contact: { principalId: "p-local", displayName: "LocalVellum" }, channel: { address: "lv-addr", type: "vellum" } },
106
- };
107
- const result = resolveAnchoredGuardian({
108
- guardians: null,
109
- sourceChannel: "telegram",
110
- useLocalFallback: true,
111
- });
112
- expect(result?.source).toBe("vellum-anchor");
113
- expect(result?.address).toBe("lv-addr");
114
- });
115
-
116
- test("gateway empty + no local + fallback disabled returns null", () => {
69
+ describe("resolveAnchoredGuardian — gateway empty", () => {
70
+ test("null gateway list returns null", () => {
117
71
  const result = resolveAnchoredGuardian({
118
72
  guardians: null,
119
73
  sourceChannel: "telegram",
@@ -121,11 +75,10 @@ describe("resolveAnchoredGuardian — local fallback", () => {
121
75
  expect(result).toBeNull();
122
76
  });
123
77
 
124
- test("nothing anywhere returns null", () => {
78
+ test("empty gateway list returns null", () => {
125
79
  const result = resolveAnchoredGuardian({
126
80
  guardians: [],
127
81
  sourceChannel: "telegram",
128
- useLocalFallback: true,
129
82
  });
130
83
  expect(result).toBeNull();
131
84
  });
@@ -7,16 +7,13 @@
7
7
  * anchor identity is used. This blocks stale or cross-assistant contacts from
8
8
  * being bound to a request.
9
9
  *
10
- * Gateway-first: resolves from the gateway delivery list, then falls back to
11
- * the LOCAL dual-written binding when the gateway read is empty/unavailable
12
- * (restart, timeout, malformed IPC). The local fallback is transitional and is
13
- * removed in a later step.
10
+ * Resolves from the gateway delivery list, the source of truth for guardian
11
+ * principals.
14
12
  */
15
13
 
16
14
  import type { GuardianDelivery } from "@vellumai/gateway-client";
17
15
 
18
16
  import type { ChannelId } from "../channels/types.js";
19
- import { findGuardianForChannel } from "../contacts/contact-store.js";
20
17
  import { guardianForChannel } from "../contacts/guardian-delivery-reader.js";
21
18
  import type { GuardianResolutionSource } from "../notifications/signal.js";
22
19
 
@@ -33,13 +30,6 @@ export interface ResolveAnchoredGuardianInput {
33
30
  /** Gateway delivery list; `null` when the gateway read failed/was empty. */
34
31
  guardians: GuardianDelivery[] | null;
35
32
  sourceChannel: ChannelId;
36
- /**
37
- * Fall back to the LOCAL dual-written binding when the gateway arm resolves
38
- * nothing. Access-request enables this to avoid an undecidable request; the
39
- * cosmetic guardian-label path leaves it off so a missing gateway read
40
- * degrades gracefully to the default reference.
41
- */
42
- useLocalFallback?: boolean;
43
33
  /**
44
34
  * Require a non-null anchor principal for the vellum-anchor arm. When the
45
35
  * vellum guardian has no principal, return `null` instead of a vellum-anchor
@@ -52,12 +42,12 @@ export interface ResolveAnchoredGuardianInput {
52
42
  /**
53
43
  * Resolve the anchored guardian for `sourceChannel`, or `null` when none can be
54
44
  * resolved. Gateway source-channel match → that record; gateway anchor-only →
55
- * vellum-anchor; gateway empty + local has it → local fallback record.
45
+ * vellum-anchor.
56
46
  */
57
47
  export function resolveAnchoredGuardian(
58
48
  input: ResolveAnchoredGuardianInput,
59
49
  ): AnchoredGuardian | null {
60
- const { sourceChannel, useLocalFallback, requireAnchorPrincipal } = input;
50
+ const { sourceChannel, requireAnchorPrincipal } = input;
61
51
  const guardians = input.guardians ?? [];
62
52
 
63
53
  const vellumGuardian = guardianForChannel(guardians, "vellum");
@@ -92,44 +82,5 @@ export function resolveAnchoredGuardian(
92
82
  };
93
83
  }
94
84
 
95
- // Gateway resolved a principal — done.
96
- if (resolved?.principalId) {
97
- return resolved;
98
- }
99
-
100
- if (!useLocalFallback) {
101
- return resolved;
102
- }
103
-
104
- // Fallback: gateway read was empty/unavailable (or carried no principal), so
105
- // resolve from the LOCAL dual-written binding to avoid an undecidable
106
- // request. A local match overwrites the principal-less gateway record; with
107
- // no local match the gateway record (if any) is retained.
108
- const localVellum = findGuardianForChannel("vellum");
109
- const localAnchorPrincipalId = localVellum?.contact.principalId;
110
- const localSource = findGuardianForChannel(sourceChannel);
111
- if (
112
- localAnchorPrincipalId &&
113
- localSource &&
114
- localSource.contact.principalId === localAnchorPrincipalId
115
- ) {
116
- return {
117
- principalId: localSource.contact.principalId,
118
- address: localSource.channel.address,
119
- displayName: localSource.contact.displayName,
120
- channelType: localSource.channel.type,
121
- source: "source-channel-contact",
122
- };
123
- }
124
- if (localVellum) {
125
- return {
126
- principalId: localAnchorPrincipalId ?? null,
127
- address: localVellum.channel.address,
128
- displayName: localVellum.contact.displayName,
129
- channelType: "vellum",
130
- source: "vellum-anchor",
131
- };
132
- }
133
-
134
85
  return resolved;
135
86
  }