@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
@@ -57,12 +57,9 @@ mock.module("../runtime/gateway-client.js", () => ({
57
57
  }));
58
58
 
59
59
  // ── Guardian binding mock ──
60
- // mockGuardianContact controls what findGuardianForChannel returns.
61
- // When non-null, it should look like { contact: { displayName: "..." }, channel: { ... } }.
62
- let mockGuardianContact: {
63
- contact: { displayName: string };
64
- channel: Record<string, unknown>;
65
- } | null = null;
60
+ // mockGuardianDelivery controls what the gateway guardian-delivery reader
61
+ // returns for the source channel; non-null carries at least a displayName.
62
+ let mockGuardianDelivery: { displayName: string } | null = null;
66
63
 
67
64
  mock.module("../runtime/channel-verification-service.js", () => ({
68
65
  getGuardianBinding: () => null,
@@ -82,9 +79,17 @@ mock.module("../runtime/channel-verification-service.js", () => ({
82
79
  }),
83
80
  }));
84
81
 
85
- // ── Contact store mock ──
86
- mock.module("../contacts/contact-store.js", () => ({
87
- findGuardianForChannel: () => mockGuardianContact,
82
+ // ── Guardian delivery reader mock ──
83
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
84
+ getGuardianDelivery: async (input?: { channelTypes?: string[] }) => {
85
+ if (!mockGuardianDelivery) return [];
86
+ const channelType = input?.channelTypes?.[0] ?? "telegram";
87
+ return [{ channelType, status: "active", ...mockGuardianDelivery }];
88
+ },
89
+ guardianForChannel: (
90
+ list: Array<{ channelType: string; status: string }>,
91
+ channelType: string,
92
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
88
93
  }));
89
94
 
90
95
  // ── Pending interactions mock ──
@@ -126,7 +131,10 @@ mock.module("../prompts/user-reference.js", () => ({
126
131
 
127
132
  // Import module under test AFTER mocks are set up
128
133
  import type { ChannelId } from "../channels/types.js";
129
- import { findGuardianForChannel } from "../contacts/contact-store.js";
134
+ import {
135
+ getGuardianDelivery,
136
+ guardianForChannel,
137
+ } from "../contacts/guardian-delivery-reader.js";
130
138
  import type { TrustContext } from "../daemon/trust-context.js";
131
139
  import { resolveGuardianName } from "../prompts/user-reference.js";
132
140
 
@@ -197,9 +205,14 @@ async function simulateNotifierPoll(params: {
197
205
 
198
206
  notifiedRequestIds.set(info.requestId, conversationId);
199
207
 
200
- // Resolve guardian name via the contacts-based approach
201
- const guardian = findGuardianForChannel(params.sourceChannel);
202
- const guardianName = resolveGuardianName(guardian?.contact.displayName);
208
+ // Resolve guardian name via the gateway guardian-delivery reader
209
+ const guardians = await getGuardianDelivery({
210
+ channelTypes: [params.sourceChannel],
211
+ });
212
+ const guardian = guardians
213
+ ? guardianForChannel(guardians, params.sourceChannel)
214
+ : undefined;
215
+ const guardianName = resolveGuardianName(guardian?.displayName ?? undefined);
203
216
 
204
217
  const waitingText = `Waiting for ${guardianName}'s approval...`;
205
218
 
@@ -225,7 +238,7 @@ describe("trusted-contact pending-approval notifier", () => {
225
238
  deliveredReplies.length = 0;
226
239
  deliverShouldFail = false;
227
240
  mockPendingApprovals = [];
228
- mockGuardianContact = null;
241
+ mockGuardianDelivery = null;
229
242
  });
230
243
 
231
244
  test("sends waiting message to trusted contact when pending approval exists", async () => {
@@ -238,10 +251,7 @@ describe("trusted-contact pending-approval notifier", () => {
238
251
  },
239
252
  ];
240
253
 
241
- mockGuardianContact = {
242
- contact: { displayName: "Mom" },
243
- channel: {},
244
- };
254
+ mockGuardianDelivery = { displayName: "Mom" };
245
255
 
246
256
  const notified = new Map<string, string>();
247
257
  const sent = await simulateNotifierPoll({
@@ -273,10 +283,7 @@ describe("trusted-contact pending-approval notifier", () => {
273
283
  },
274
284
  ];
275
285
 
276
- mockGuardianContact = {
277
- contact: { displayName: "Guardian User" },
278
- channel: {},
279
- };
286
+ mockGuardianDelivery = { displayName: "Guardian User" };
280
287
 
281
288
  const notified = new Map<string, string>();
282
289
  await simulateNotifierPoll({
@@ -306,10 +313,7 @@ describe("trusted-contact pending-approval notifier", () => {
306
313
  ];
307
314
 
308
315
  // Guardian contact exists but has an empty displayName
309
- mockGuardianContact = {
310
- contact: { displayName: "" },
311
- channel: {},
312
- };
316
+ mockGuardianDelivery = { displayName: "" };
313
317
 
314
318
  const notified = new Map<string, string>();
315
319
  await simulateNotifierPoll({
@@ -338,7 +342,7 @@ describe("trusted-contact pending-approval notifier", () => {
338
342
  },
339
343
  ];
340
344
 
341
- mockGuardianContact = null;
345
+ mockGuardianDelivery = null;
342
346
 
343
347
  const notified = new Map<string, string>();
344
348
  await simulateNotifierPoll({
@@ -367,10 +371,7 @@ describe("trusted-contact pending-approval notifier", () => {
367
371
  },
368
372
  ];
369
373
 
370
- mockGuardianContact = {
371
- contact: { displayName: "Guardian" },
372
- channel: {},
373
- };
374
+ mockGuardianDelivery = { displayName: "Guardian" };
374
375
 
375
376
  const notified = new Map<string, string>();
376
377
  const baseParams = {
@@ -395,10 +396,7 @@ describe("trusted-contact pending-approval notifier", () => {
395
396
  });
396
397
 
397
398
  test("sends separate messages for different requestIds", async () => {
398
- mockGuardianContact = {
399
- contact: { displayName: "Guardian" },
400
- channel: {},
401
- };
399
+ mockGuardianDelivery = { displayName: "Guardian" };
402
400
 
403
401
  const notified = new Map<string, string>();
404
402
  const baseParams = {
@@ -437,10 +435,7 @@ describe("trusted-contact pending-approval notifier", () => {
437
435
  });
438
436
 
439
437
  test("concurrent pollers for different conversations do not evict each other", async () => {
440
- mockGuardianContact = {
441
- contact: { displayName: "Guardian" },
442
- channel: {},
443
- };
438
+ mockGuardianDelivery = { displayName: "Guardian" };
444
439
 
445
440
  // Shared dedupe map simulating the module-level global
446
441
  const notified = new Map<string, string>();
@@ -589,10 +584,7 @@ describe("trusted-contact pending-approval notifier", () => {
589
584
  },
590
585
  ];
591
586
 
592
- mockGuardianContact = {
593
- contact: { displayName: "Guardian" },
594
- channel: {},
595
- };
587
+ mockGuardianDelivery = { displayName: "Guardian" };
596
588
 
597
589
  const notified = new Map<string, string>();
598
590
  const baseParams = {
@@ -647,10 +639,7 @@ describe("trusted-contact pending-approval notifier", () => {
647
639
  },
648
640
  ];
649
641
 
650
- mockGuardianContact = {
651
- contact: { displayName: "Sarah" },
652
- channel: {},
653
- };
642
+ mockGuardianDelivery = { displayName: "Sarah" };
654
643
 
655
644
  const notified = new Map<string, string>();
656
645
  await simulateNotifierPoll({
@@ -679,10 +668,7 @@ describe("trusted-contact pending-approval notifier", () => {
679
668
  },
680
669
  ];
681
670
 
682
- mockGuardianContact = {
683
- contact: { displayName: " " },
684
- channel: {},
685
- };
671
+ mockGuardianDelivery = { displayName: " " };
686
672
 
687
673
  const notified = new Map<string, string>();
688
674
  await simulateNotifierPoll({
@@ -157,7 +157,6 @@ mock.module("../config/env.js", () => ({
157
157
  import { applyCanonicalGuardianDecision } from "../approvals/guardian-decision-primitive.js";
158
158
  import type { ActorContext } from "../approvals/guardian-request-resolvers.js";
159
159
  import { getResolver } from "../approvals/guardian-request-resolvers.js";
160
- import { upsertContactChannel } from "../contacts/contacts-write.js";
161
160
  import type { TrustContext } from "../daemon/trust-context.js";
162
161
  import {
163
162
  createCanonicalGuardianRequest,
@@ -176,6 +175,7 @@ import {
176
175
  waitForInlineGrant,
177
176
  } from "../tools/tool-approval-handler.js";
178
177
  import type { ToolContext, ToolLifecycleEvent } from "../tools/types.js";
178
+ import { seedContactChannel } from "./helpers/seed-contact-channel.js";
179
179
 
180
180
  await initializeDb();
181
181
 
@@ -1352,7 +1352,7 @@ describe("(g) access_request resolver: requester code delivery", () => {
1352
1352
 
1353
1353
  test("guardian-facing reply uses the requester's display name, not the raw ID", async () => {
1354
1354
  // Seed a contact so the resolver can resolve a display name.
1355
- upsertContactChannel({
1355
+ seedContactChannel({
1356
1356
  sourceChannel: "slack",
1357
1357
  externalUserId: REQUESTER_UID,
1358
1358
  displayName: "Alice",
@@ -65,11 +65,13 @@ mock.module("../runtime/approval-message-composer.js", () => ({
65
65
  }));
66
66
 
67
67
  import { getResolver } from "../approvals/guardian-request-resolvers.js";
68
- import { upsertContactChannel } from "../contacts/contacts-write.js";
69
68
  import { createCanonicalGuardianRequest } from "../memory/canonical-guardian-store.js";
70
69
  import { getDb } from "../memory/db-connection.js";
71
70
  import { initializeDb } from "../memory/db-init.js";
72
- import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
71
+ import {
72
+ handleChannelInbound,
73
+ seedContactChannel,
74
+ } from "./helpers/channel-test-adapter.js";
73
75
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
74
76
 
75
77
  await initializeDb();
@@ -140,7 +142,7 @@ describe("trusted contact lifecycle notification signals", () => {
140
142
  guardianPrincipalId: "guardian-user-789",
141
143
  verifiedVia: "test",
142
144
  });
143
- upsertContactChannel({
145
+ seedContactChannel({
144
146
  sourceChannel: "telegram",
145
147
  externalUserId: "guardian-user-789",
146
148
  externalChatId: "guardian-chat-789",
@@ -150,7 +152,7 @@ describe("trusted contact lifecycle notification signals", () => {
150
152
  });
151
153
 
152
154
  // Set up requester contact with a display name so payloads are enriched
153
- upsertContactChannel({
155
+ seedContactChannel({
154
156
  sourceChannel: "telegram",
155
157
  externalUserId: "requester-user-456",
156
158
  externalChatId: "requester-chat-456",
@@ -234,7 +236,7 @@ describe("trusted contact lifecycle notification signals", () => {
234
236
  guardianPrincipalId: "guardian-user-789",
235
237
  verifiedVia: "test",
236
238
  });
237
- upsertContactChannel({
239
+ seedContactChannel({
238
240
  sourceChannel: "telegram",
239
241
  externalUserId: "guardian-user-789",
240
242
  externalChatId: "guardian-chat-789",
@@ -244,7 +246,7 @@ describe("trusted contact lifecycle notification signals", () => {
244
246
  });
245
247
 
246
248
  // Set up requester contact with a display name
247
- upsertContactChannel({
249
+ seedContactChannel({
248
250
  sourceChannel: "telegram",
249
251
  externalUserId: "requester-user-456",
250
252
  externalChatId: "requester-chat-456",
@@ -322,7 +324,7 @@ describe("trusted contact lifecycle notification signals", () => {
322
324
  guardianPrincipalId: "guardian-user-789",
323
325
  verifiedVia: "test",
324
326
  });
325
- upsertContactChannel({
327
+ seedContactChannel({
326
328
  sourceChannel: "telegram",
327
329
  externalUserId: "guardian-user-789",
328
330
  externalChatId: "guardian-chat-789",
@@ -73,14 +73,16 @@ mock.module("../runtime/approval-message-composer.js", () => ({
73
73
  }));
74
74
 
75
75
  import { findContactChannel } from "../contacts/contact-store.js";
76
- import { upsertContactChannel } from "../contacts/contacts-write.js";
77
- import { getDb } from "../memory/db-connection.js";
76
+ import { getDb, getSqlite } from "../memory/db-connection.js";
78
77
  import { initializeDb } from "../memory/db-init.js";
79
78
  import {
80
79
  createOutboundSession,
81
80
  validateAndConsumeVerification,
82
81
  } from "../runtime/channel-verification-service.js";
83
- import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
82
+ import {
83
+ handleChannelInbound,
84
+ seedContactChannel,
85
+ } from "./helpers/channel-test-adapter.js";
84
86
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
85
87
 
86
88
  await initializeDb();
@@ -253,7 +255,7 @@ for (const config of CHANNEL_CONFIGS) {
253
255
  expect(challengeResult.verificationType).toBe("trusted_contact");
254
256
  }
255
257
 
256
- upsertContactChannel({
258
+ seedContactChannel({
257
259
  sourceChannel: config.channel,
258
260
  externalUserId: config.senderExternalUserId,
259
261
  externalChatId: config.externalChatId,
@@ -269,14 +271,21 @@ for (const config of CHANNEL_CONFIGS) {
269
271
  });
270
272
 
271
273
  expect(contactResult).not.toBeNull();
272
- expect(contactResult!.channel.status).toBe("active");
273
- expect(contactResult!.channel.policy).toBe("allow");
274
+ // Assert the gateway dual-write landed in the local ACL columns.
275
+ const acl = getSqlite()
276
+ .query("SELECT status, policy FROM contact_channels WHERE id = ?")
277
+ .get(contactResult!.channel.id) as {
278
+ status: string;
279
+ policy: string;
280
+ } | null;
281
+ expect(acl!.status).toBe("active");
282
+ expect(acl!.policy).toBe("allow");
274
283
  expect(contactResult!.channel.type).toBe(config.channel);
275
284
  });
276
285
 
277
286
  test("no cross-channel leakage between member records", () => {
278
287
  // Create a member for this channel
279
- upsertContactChannel({
288
+ seedContactChannel({
280
289
  sourceChannel: config.channel,
281
290
  externalUserId: config.senderExternalUserId,
282
291
  externalChatId: config.externalChatId,
@@ -22,21 +22,27 @@ mock.module("../util/logger.js", () => ({
22
22
  }),
23
23
  }));
24
24
 
25
- import {
26
- findContactChannel,
27
- findGuardianForChannel,
28
- } from "../contacts/contact-store.js";
25
+ import { and, desc, eq } from "drizzle-orm";
26
+
27
+ import type { ChannelId } from "../channels/types.js";
28
+ import { findContactChannel } from "../contacts/contact-store.js";
29
29
  import {
30
30
  revokeMember,
31
31
  upsertContactChannel,
32
32
  } from "../contacts/contacts-write.js";
33
+ import type { ChannelStatus } from "../contacts/types.js";
33
34
  import { getDb } from "../memory/db-connection.js";
34
35
  import { initializeDb } from "../memory/db-init.js";
36
+ import { contactChannels, contacts } from "../memory/schema.js";
35
37
  import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
36
38
  import {
37
39
  createOutboundSession,
38
40
  validateAndConsumeVerification,
39
41
  } from "../runtime/channel-verification-service.js";
42
+ import {
43
+ __resetMemberVerdictCacheForTest,
44
+ setMemberVerdict,
45
+ } from "../runtime/member-verdict-cache.js";
40
46
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
41
47
 
42
48
  await initializeDb();
@@ -53,6 +59,48 @@ function resetTables(): void {
53
59
  db.run("DELETE FROM contacts");
54
60
  }
55
61
 
62
+ // Mirror a warmed gateway verdict so the sync resolveActorTrust fallback
63
+ // resolves the member with the given status; the local ACL columns are no
64
+ // longer read.
65
+ function warmMemberVerdict(
66
+ channelType: ChannelId,
67
+ address: string,
68
+ status: ChannelStatus = "unverified",
69
+ ): void {
70
+ const found = findContactChannel({ channelType, address });
71
+ if (!found) return;
72
+ setMemberVerdict(channelType, address, {
73
+ trustClass: "unknown",
74
+ canonicalSenderId: address,
75
+ contactId: found.contact.id,
76
+ channelId: found.channel.id,
77
+ status,
78
+ policy: "allow",
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Read the local active guardian channel for a channel type, mirroring the
84
+ * gateway's role/status resolution. Used by assertions that confirm the local
85
+ * guardian binding state after verification flows.
86
+ */
87
+ function localGuardianForChannel(channelType: string) {
88
+ const row = getDb()
89
+ .select({ contact: contacts, channel: contactChannels })
90
+ .from(contacts)
91
+ .innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
92
+ .where(
93
+ and(
94
+ eq(contacts.role, "guardian"),
95
+ eq(contactChannels.type, channelType),
96
+ eq(contactChannels.status, "active"),
97
+ ),
98
+ )
99
+ .orderBy(desc(contactChannels.verifiedAt))
100
+ .get();
101
+ return row ? { contact: row.contact, channel: row.channel } : null;
102
+ }
103
+
56
104
  // ---------------------------------------------------------------------------
57
105
  // Tests
58
106
  // ---------------------------------------------------------------------------
@@ -60,6 +108,7 @@ function resetTables(): void {
60
108
  describe("trusted contact verification → member activation", () => {
61
109
  beforeEach(() => {
62
110
  resetTables();
111
+ __resetMemberVerdictCacheForTest();
63
112
  });
64
113
 
65
114
  test("successful verification creates active member with allow policy", () => {
@@ -88,26 +137,24 @@ describe("trusted contact verification → member activation", () => {
88
137
  expect(result.verificationType).toBe("trusted_contact");
89
138
  }
90
139
 
91
- // Simulate the member upsert that inbound-message-handler performs on success
140
+ // Simulate the member upsert that inbound-message-handler performs on
141
+ // success. The local mirror persists identity only; the gateway owns the
142
+ // ACL verdict, so the channel lands at the schema-default status.
92
143
  upsertContactChannel({
93
144
  sourceChannel: "telegram",
94
145
  externalUserId: "requester-user-123",
95
146
  externalChatId: "requester-chat-123",
96
- status: "active",
97
- policy: "allow",
98
147
  displayName: "Requester Name",
99
148
  username: "requester_username",
100
149
  });
101
150
 
102
- // Verify: active member record exists
151
+ // Verify: member identity record exists
103
152
  const contactResult = findContactChannel({
104
153
  channelType: "telegram",
105
154
  address: "requester-user-123",
106
155
  });
107
156
 
108
157
  expect(contactResult).not.toBeNull();
109
- expect(contactResult!.channel.status).toBe("active");
110
- expect(contactResult!.channel.policy).toBe("allow");
111
158
  expect(contactResult!.channel.address).toBe("requester-user-123");
112
159
  expect(contactResult!.channel.externalChatId).toBe("requester-chat-123");
113
160
  expect(contactResult!.contact.displayName).toBe("Requester Name");
@@ -119,11 +166,10 @@ describe("trusted contact verification → member activation", () => {
119
166
  sourceChannel: "telegram",
120
167
  externalUserId: "requester-user-jeff",
121
168
  externalChatId: "requester-chat-jeff",
122
- status: "active",
123
- policy: "allow",
124
169
  displayName: "Jeff",
125
170
  username: "jeff_handle",
126
171
  });
172
+ warmMemberVerdict("telegram", "requester-user-jeff");
127
173
 
128
174
  const trust = resolveActorTrust({
129
175
  assistantId: "self",
@@ -132,7 +178,9 @@ describe("trusted contact verification → member activation", () => {
132
178
  actorExternalId: "requester-user-jeff",
133
179
  });
134
180
 
135
- expect(trust.trustClass).toBe("trusted_contact");
181
+ // The local mirror persists identity only; the schema-default status places
182
+ // the contact in the unverified_contact tier (gateway owns elevation).
183
+ expect(trust.trustClass).toBe("unverified_contact");
136
184
  expect(trust.actorMetadata.displayName).toBe("Jeff");
137
185
  expect(trust.actorMetadata.senderDisplayName).toBeUndefined();
138
186
  expect(trust.actorMetadata.memberDisplayName).toBe("Jeff");
@@ -147,11 +195,10 @@ describe("trusted contact verification → member activation", () => {
147
195
  sourceChannel: "telegram",
148
196
  externalUserId: "requester-user-jeff-priority",
149
197
  externalChatId: "requester-chat-jeff-priority",
150
- status: "active",
151
- policy: "allow",
152
198
  displayName: "Jeff",
153
199
  username: "jeff_handle",
154
200
  });
201
+ warmMemberVerdict("telegram", "requester-user-jeff-priority");
155
202
 
156
203
  const trust = resolveActorTrust({
157
204
  assistantId: "self",
@@ -162,7 +209,7 @@ describe("trusted contact verification → member activation", () => {
162
209
  actorDisplayName: "Jeffrey",
163
210
  });
164
211
 
165
- expect(trust.trustClass).toBe("trusted_contact");
212
+ expect(trust.trustClass).toBe("unverified_contact");
166
213
  expect(trust.actorMetadata.displayName).toBe("Jeff");
167
214
  expect(trust.actorMetadata.senderDisplayName).toBe("Jeffrey");
168
215
  expect(trust.actorMetadata.memberDisplayName).toBe("Jeff");
@@ -179,8 +226,6 @@ describe("trusted contact verification → member activation", () => {
179
226
  sourceChannel: "telegram",
180
227
  externalUserId: "other-user-in-group",
181
228
  externalChatId: "shared-group-chat",
182
- status: "active",
183
- policy: "allow",
184
229
  displayName: "Other User",
185
230
  username: "other_handle",
186
231
  });
@@ -229,20 +274,17 @@ describe("trusted contact verification → member activation", () => {
229
274
  sourceChannel: "telegram",
230
275
  externalUserId: "requester-user-456",
231
276
  externalChatId: "requester-chat-456",
232
- status: "active",
233
- policy: "allow",
234
277
  });
235
278
 
236
- // Simulate the ACL check that inbound-message-handler performs
279
+ // The local mirror persists the member identity; the gateway owns the ACL
280
+ // verdict the inbound handler enforces.
237
281
  const contactResult = findContactChannel({
238
282
  channelType: "telegram",
239
283
  address: "requester-user-456",
240
284
  });
241
285
 
242
286
  expect(contactResult).not.toBeNull();
243
- expect(contactResult!.channel.status).toBe("active");
244
- expect(contactResult!.channel.policy).toBe("allow");
245
- // ACL check passes: member exists, is active, and has allow policy
287
+ expect(contactResult!.channel.address).toBe("requester-user-456");
246
288
  });
247
289
 
248
290
  test("member lookup is scoped by channel type", () => {
@@ -267,8 +309,6 @@ describe("trusted contact verification → member activation", () => {
267
309
  sourceChannel: "telegram",
268
310
  externalUserId: "user-cross-test",
269
311
  externalChatId: "chat-cross-test",
270
- status: "active",
271
- policy: "allow",
272
312
  });
273
313
 
274
314
  // Member should be found via contacts
@@ -277,7 +317,7 @@ describe("trusted contact verification → member activation", () => {
277
317
  address: "user-cross-test",
278
318
  });
279
319
  expect(contactResult).not.toBeNull();
280
- expect(contactResult!.channel.status).toBe("active");
320
+ expect(contactResult!.channel.address).toBe("user-cross-test");
281
321
 
282
322
  // Member should NOT be found for a different channel type
283
323
  const otherChannel = findContactChannel({
@@ -287,31 +327,22 @@ describe("trusted contact verification → member activation", () => {
287
327
  expect(otherChannel).toBeNull();
288
328
  });
289
329
 
290
- test("re-verification of previously revoked member reactivates them", () => {
291
- // Create and activate a member
330
+ test("revokeMember resolves the member identity without mutating local ACL", () => {
331
+ // Create a member identity row.
292
332
  const member = upsertContactChannel({
293
333
  sourceChannel: "telegram",
294
334
  externalUserId: "user-revoked",
295
335
  externalChatId: "chat-revoked",
296
- status: "active",
297
- policy: "allow",
298
336
  displayName: "Revoked User",
299
337
  });
300
338
 
301
- // Revoke the member
302
- const revoked = revokeMember(member!.channel.id, "testing revocation");
339
+ // The local revoke is a pure resolver now — the gateway owns the ACL
340
+ // downgrade; revokeMember returns the resolved native contact/channel.
341
+ const revoked = revokeMember(member!.channel.id);
303
342
  expect(revoked).not.toBeNull();
304
- expect(revoked!.channel.status).toBe("revoked");
305
-
306
- // Verify the member is indeed revoked (ACL would reject)
307
- const revokedResult = findContactChannel({
308
- channelType: "telegram",
309
- address: "user-revoked",
310
- });
311
- expect(revokedResult).not.toBeNull();
312
- expect(revokedResult!.channel.status).toBe("revoked");
343
+ expect(revoked!.channel.id).toBe(member!.channel.id);
313
344
 
314
- // Guardian re-approves, new outbound session created
345
+ // Re-upsert on the same identity is idempotent on the identity row.
315
346
  const session = createOutboundSession({
316
347
  channel: "telegram",
317
348
  expectedExternalUserId: "user-revoked",
@@ -321,7 +352,6 @@ describe("trusted contact verification → member activation", () => {
321
352
  verificationPurpose: "trusted_contact",
322
353
  });
323
354
 
324
- // Requester enters the new code
325
355
  const result = validateAndConsumeVerification(
326
356
  "telegram",
327
357
  session.secret,
@@ -333,23 +363,18 @@ describe("trusted contact verification → member activation", () => {
333
363
  expect(result.verificationType).toBe("trusted_contact");
334
364
  }
335
365
 
336
- // upsertContactChannel reactivates the existing record
337
366
  upsertContactChannel({
338
367
  sourceChannel: "telegram",
339
368
  externalUserId: "user-revoked",
340
369
  externalChatId: "chat-revoked",
341
- status: "active",
342
- policy: "allow",
343
370
  });
344
371
 
345
- // Verify: member is now active again
346
- const reactivatedResult = findContactChannel({
372
+ const reResolved = findContactChannel({
347
373
  channelType: "telegram",
348
374
  address: "user-revoked",
349
375
  });
350
- expect(reactivatedResult).not.toBeNull();
351
- expect(reactivatedResult!.channel.status).toBe("active");
352
- expect(reactivatedResult!.channel.policy).toBe("allow");
376
+ expect(reResolved).not.toBeNull();
377
+ expect(reResolved!.channel.id).toBe(member!.channel.id);
353
378
  });
354
379
 
355
380
  test("trusted contact verification does NOT create a guardian binding", () => {
@@ -388,7 +413,7 @@ describe("trusted contact verification → member activation", () => {
388
413
  }
389
414
 
390
415
  // The original guardian binding should remain intact
391
- const guardianResult = findGuardianForChannel("telegram");
416
+ const guardianResult = localGuardianForChannel("telegram");
392
417
  expect(guardianResult).not.toBeNull();
393
418
  expect(guardianResult!.channel.address).toBe("guardian-user-original");
394
419
  });
@@ -412,7 +437,7 @@ describe("trusted contact verification → member activation", () => {
412
437
  expect(result.verificationType).toBe("guardian");
413
438
  }
414
439
 
415
- const guardianResult = findGuardianForChannel("telegram");
440
+ const guardianResult = localGuardianForChannel("telegram");
416
441
  expect(guardianResult).toBeNull();
417
442
  });
418
443
  });