@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
@@ -69,7 +69,6 @@ mock.module("../daemon/approval-generators.js", () => ({
69
69
  createApprovalConversationGenerator: () => _testApprovalConversationGenerator,
70
70
  }));
71
71
 
72
- import { upsertContact } from "../contacts/contact-store.js";
73
72
  import type { Conversation } from "../daemon/conversation.js";
74
73
  import {
75
74
  createCanonicalGuardianDelivery,
@@ -86,7 +85,10 @@ import * as gatewayClient from "../runtime/gateway-client.js";
86
85
  import * as pendingInteractions from "../runtime/pending-interactions.js";
87
86
  import { _setTestPollMaxWait } from "../runtime/routes/channel-route-shared.js";
88
87
  import { resetDbForTesting } from "./db-test-helpers.js";
89
- import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
88
+ import {
89
+ handleChannelInbound,
90
+ seedContactChannel,
91
+ } from "./helpers/channel-test-adapter.js";
90
92
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
91
93
 
92
94
  await initializeDb();
@@ -212,22 +214,19 @@ function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
212
214
  const noopProcessMessage = mock(async () => ({ messageId: "msg-1" }));
213
215
 
214
216
  function ensureTestContact(): void {
215
- upsertContact({
217
+ seedContactChannel({
218
+ sourceChannel: "telegram",
219
+ externalUserId: "telegram-user-default",
216
220
  displayName: "Test User",
217
- channels: [
218
- {
219
- type: "telegram",
220
- address: "telegram-user-default",
221
- status: "active",
222
- policy: "allow",
223
- },
224
- {
225
- type: "slack",
226
- address: "slack-user-default",
227
- status: "active",
228
- policy: "allow",
229
- },
230
- ],
221
+ status: "active",
222
+ policy: "allow",
223
+ });
224
+ seedContactChannel({
225
+ sourceChannel: "slack",
226
+ externalUserId: "slack-user-default",
227
+ displayName: "Test User",
228
+ status: "active",
229
+ policy: "allow",
231
230
  });
232
231
  }
233
232
 
@@ -1926,16 +1925,12 @@ describe("trusted-contact self-approval blocked before guardian approval row exi
1926
1925
  guardianDeliveryChatId: "guardian-tc-selfapproval-chat",
1927
1926
  guardianPrincipalId: "guardian-tc-selfapproval",
1928
1927
  });
1929
- upsertContact({
1928
+ seedContactChannel({
1929
+ sourceChannel: "telegram",
1930
+ externalUserId: "tc-selfapproval-user",
1930
1931
  displayName: "TC Self-Approval User",
1931
- channels: [
1932
- {
1933
- type: "telegram",
1934
- address: "tc-selfapproval-user",
1935
- status: "active",
1936
- policy: "allow",
1937
- },
1938
- ],
1932
+ status: "active",
1933
+ policy: "allow",
1939
1934
  });
1940
1935
  });
1941
1936
 
@@ -493,6 +493,34 @@ describe("channel-delivery-store", () => {
493
493
  });
494
494
  });
495
495
 
496
+ test("conversation detail omits Slack metadata for non-Slack channels", () => {
497
+ const result = recordInbound("telegram", "tg-chat-1", "msg-1", {
498
+ sourceThreadId: "9001",
499
+ });
500
+
501
+ upsertBinding({
502
+ conversationId: result.conversationId,
503
+ sourceChannel: "telegram",
504
+ externalChatId: "tg-chat-1",
505
+ externalChatName: "Family",
506
+ externalThreadId: "9001",
507
+ });
508
+
509
+ const detail = buildConversationDetailResponse(result.conversationId);
510
+ const binding = detail?.conversation.channelBinding;
511
+
512
+ // The channel-neutral fields pass through for any source channel...
513
+ expect(binding).toMatchObject({
514
+ sourceChannel: "telegram",
515
+ externalChatId: "tg-chat-1",
516
+ externalChatName: "Family",
517
+ externalThreadId: "9001",
518
+ });
519
+ // ...but Slack-only deep-link metadata is not synthesized.
520
+ expect(binding).not.toHaveProperty("slackThread");
521
+ expect(binding).not.toHaveProperty("slackChannel");
522
+ });
523
+
496
524
  test("binding upsert preserves existing chat name when incoming name is missing", () => {
497
525
  const result = recordInbound("slack", "C0123ABCDEF", "msg-1", {
498
526
  sourceThreadId: "1710000000.000100",
@@ -65,45 +65,77 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
65
65
 
66
66
  // Gateway relay mock — the revoke path relays the ACL downgrade over IPC and
67
67
  // validates the response; return a well-formed mark_channel_revoked result.
68
+ // The gateway owns the revoke and dual-writes the local assistant row to
69
+ // "revoked"; mirror that dual-write here so guardian-resolution reads under
70
+ // test observe the downgrade (the assistant-side teardown is now a no-op shim).
68
71
  mock.module("../ipc/gateway-client.js", () => ({
69
72
  ipcCallPersistent: async (
70
- _method: string,
73
+ method: string,
71
74
  params?: Record<string, unknown>,
72
- ) => ({
73
- ok: true,
74
- didWrite: true,
75
- channel: {
76
- id: (params?.contactChannelId as string) ?? "ch1",
77
- contactId: "c1",
78
- type: "phone",
79
- address: "addr",
80
- status: "revoked",
81
- revokedReason: (params?.reason as string) ?? null,
82
- },
83
- }),
75
+ ) => {
76
+ if (method === "mark_channel_revoked") {
77
+ const { getDb } = await import("../memory/db-connection.js");
78
+ const { contactChannels } = await import("../memory/schema.js");
79
+ const { eq } = await import("drizzle-orm");
80
+ const channelId = params?.contactChannelId as string | undefined;
81
+ if (channelId) {
82
+ getDb()
83
+ .update(contactChannels)
84
+ .set({ status: "revoked" })
85
+ .where(eq(contactChannels.id, channelId))
86
+ .run();
87
+ }
88
+ }
89
+ return {
90
+ ok: true,
91
+ didWrite: true,
92
+ channel: {
93
+ id: (params?.contactChannelId as string) ?? "ch1",
94
+ contactId: "c1",
95
+ type: "phone",
96
+ address: "addr",
97
+ status: "revoked",
98
+ revokedReason: (params?.reason as string) ?? null,
99
+ },
100
+ };
101
+ },
84
102
  }));
85
103
 
86
104
  // Guardian-delivery reader mock — the inbound challenge guard reads guardian
87
105
  // existence from the gateway. Derive the list from the local binding state so
88
106
  // the gateway-backed presence guard mirrors the DB the rest of the test sets up.
89
107
  const resolveGuardianList = async (input?: { channelTypes?: string[] }) => {
90
- const { findGuardianForChannel } = await import(
91
- "../contacts/contact-store.js"
92
- );
108
+ const { getDb } = await import("../memory/db-connection.js");
109
+ const { contacts, contactChannels } = await import("../memory/schema.js");
110
+ const { and, eq } = await import("drizzle-orm");
93
111
  const channels = input?.channelTypes ?? [];
94
112
  return channels
95
113
  .map((channelType) => {
96
- const found = findGuardianForChannel(channelType);
97
- if (!found) return null;
114
+ const row = getDb()
115
+ .select({ contact: contacts, channel: contactChannels })
116
+ .from(contacts)
117
+ .innerJoin(
118
+ contactChannels,
119
+ eq(contacts.id, contactChannels.contactId),
120
+ )
121
+ .where(
122
+ and(
123
+ eq(contacts.role, "guardian"),
124
+ eq(contactChannels.type, channelType),
125
+ eq(contactChannels.status, "active"),
126
+ ),
127
+ )
128
+ .get();
129
+ if (!row) return null;
98
130
  return {
99
131
  channelType,
100
- contactId: found.contact.id,
101
- principalId: found.contact.principalId ?? null,
102
- displayName: found.contact.displayName ?? null,
103
- address: found.channel.address,
104
- externalChatId: found.channel.externalChatId ?? null,
132
+ contactId: row.contact.id,
133
+ principalId: row.contact.principalId ?? null,
134
+ displayName: row.contact.displayName ?? null,
135
+ address: row.channel.address,
136
+ externalChatId: row.channel.externalChatId ?? null,
105
137
  status: "active",
106
- verifiedAt: found.channel.verifiedAt ?? null,
138
+ verifiedAt: row.channel.verifiedAt ?? null,
107
139
  };
108
140
  })
109
141
  .filter((g) => g !== null);
@@ -147,6 +179,7 @@ import {
147
179
  } from "../memory/guardian-rate-limits.js";
148
180
  import {
149
181
  channelVerificationSessions,
182
+ contactChannels,
150
183
  conversations,
151
184
  } from "../memory/schema.js";
152
185
  import {
@@ -192,6 +225,19 @@ function resetTables(): void {
192
225
  mockBotUsername = "test_bot";
193
226
  }
194
227
 
228
+ /**
229
+ * Revoke a guardian channel's local ACL state directly. The production revoke
230
+ * is gateway-owned (relayed via mark_channel_revoked); this stamps the local
231
+ * mirror so the guardian-resolution reads still under test see the downgrade.
232
+ */
233
+ function revokeGuardianChannelLocally(channelType: string): void {
234
+ getDb()
235
+ .update(contactChannels)
236
+ .set({ status: "revoked" })
237
+ .where(eq(contactChannels.type, channelType))
238
+ .run();
239
+ }
240
+
195
241
  // ═══════════════════════════════════════════════════════════════════════════
196
242
  // 2. Verification Challenge Lifecycle (Store)
197
243
  // ═══════════════════════════════════════════════════════════════════════════
@@ -558,7 +604,7 @@ describe("guardian identity check", () => {
558
604
  guardianDeliveryChatId: "chat-42",
559
605
  });
560
606
 
561
- serviceRevokeBinding("asst-1", "telegram");
607
+ revokeGuardianChannelLocally("telegram");
562
608
 
563
609
  expect(await isGuardian("asst-1", "telegram", "user-42")).toBe(false);
564
610
  });
@@ -595,7 +641,7 @@ describe("guardian identity check", () => {
595
641
  expect(await isGuardian("asst-1", "telegram", "phone-user-1")).toBe(false);
596
642
  });
597
643
 
598
- test("serviceRevokeBinding revokes the active binding", async () => {
644
+ test("guardian binding read reflects a gateway-owned revoke", async () => {
599
645
  createGuardianBinding({
600
646
  channel: "telegram",
601
647
  guardianExternalUserId: "user-42",
@@ -603,8 +649,10 @@ describe("guardian identity check", () => {
603
649
  guardianDeliveryChatId: "chat-42",
604
650
  });
605
651
 
606
- const result = serviceRevokeBinding("asst-1", "telegram");
607
- expect(result).toBe(true);
652
+ // The revoke is gateway-owned; serviceRevokeBinding's local teardown is a
653
+ // no-op shim. Stamp the local downgrade and assert the read reflects it.
654
+ serviceRevokeBinding("asst-1", "telegram");
655
+ revokeGuardianChannelLocally("telegram");
608
656
  expect(await getGuardianBinding("asst-1", "telegram")).toBeNull();
609
657
  });
610
658
  });
@@ -958,7 +1006,7 @@ describe("channel-scoped guardian resolution", () => {
958
1006
  guardianDeliveryChatId: "chat-beta",
959
1007
  });
960
1008
 
961
- serviceRevokeBinding("self", "telegram");
1009
+ revokeGuardianChannelLocally("telegram");
962
1010
 
963
1011
  expect(await getGuardianBinding("self", "telegram")).toBeNull();
964
1012
  expect(await getGuardianBinding("self", "phone")).not.toBeNull();
@@ -1472,8 +1520,10 @@ describe("voice guardian identity and revocation", () => {
1472
1520
  guardianDeliveryChatId: "voice-chat-1",
1473
1521
  });
1474
1522
 
1475
- const result = serviceRevokeBinding("asst-1", "phone");
1476
- expect(result).toBe(true);
1523
+ // The revoke is gateway-owned; serviceRevokeBinding's local teardown is a
1524
+ // no-op shim. Stamp the local downgrade and assert the read reflects it.
1525
+ serviceRevokeBinding("asst-1", "phone");
1526
+ revokeGuardianChannelLocally("phone");
1477
1527
  expect(await getGuardianBinding("asst-1", "phone")).toBeNull();
1478
1528
  });
1479
1529
 
@@ -1491,7 +1541,7 @@ describe("voice guardian identity and revocation", () => {
1491
1541
  guardianDeliveryChatId: "tg-chat-1",
1492
1542
  });
1493
1543
 
1494
- serviceRevokeBinding("asst-1", "phone");
1544
+ revokeGuardianChannelLocally("phone");
1495
1545
 
1496
1546
  expect(await getGuardianBinding("asst-1", "phone")).toBeNull();
1497
1547
  expect(await getGuardianBinding("asst-1", "telegram")).not.toBeNull();
@@ -59,7 +59,6 @@ mock.module("../daemon/disk-pressure-guard.js", () => ({
59
59
  diskPressureStatusSequence?.shift() ?? diskPressureStatus,
60
60
  }));
61
61
 
62
- import { upsertContact } from "../contacts/contact-store.js";
63
62
  import { getDb } from "../memory/db-connection.js";
64
63
  import { initializeDb } from "../memory/db-init.js";
65
64
  import * as deliveryCrud from "../memory/delivery-crud.js";
@@ -71,6 +70,7 @@ import {
71
70
  import { sweepFailedEvents } from "../runtime/channel-retry-sweep.js";
72
71
  import {
73
72
  handleChannelInbound,
73
+ seedContactChannel,
74
74
  setAdapterProcessMessage,
75
75
  } from "./helpers/channel-test-adapter.js";
76
76
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
@@ -90,16 +90,12 @@ function resetTables(): void {
90
90
  }
91
91
 
92
92
  function seedTrustedContact(policy: "allow" | "escalate" = "allow"): void {
93
- upsertContact({
93
+ seedContactChannel({
94
+ sourceChannel: "telegram",
95
+ externalUserId: "telegram-user-1",
94
96
  displayName: "Example User",
95
- channels: [
96
- {
97
- type: "telegram",
98
- address: "telegram-user-1",
99
- status: "active",
100
- policy,
101
- },
102
- ],
97
+ status: "active",
98
+ policy,
103
99
  });
104
100
  }
105
101
 
@@ -236,16 +232,12 @@ describe("channel inbound disk pressure gate", () => {
236
232
  });
237
233
 
238
234
  test("blocks non-guardian Slack reactions silently (no reply) before persistence while locked", async () => {
239
- upsertContact({
235
+ seedContactChannel({
236
+ sourceChannel: "slack",
237
+ externalUserId: "slack-user-1",
240
238
  displayName: "Example Slack User",
241
- channels: [
242
- {
243
- type: "slack",
244
- address: "slack-user-1",
245
- status: "active",
246
- policy: "allow",
247
- },
248
- ],
239
+ status: "active",
240
+ policy: "allow",
249
241
  });
250
242
  const processMessage = mock(async () => {
251
243
  throw new Error("processMessage should not run");
@@ -81,8 +81,8 @@ mock.module("../runtime/gateway-client.js", () => ({
81
81
  }));
82
82
 
83
83
  mock.module("../memory/conversation-crud.js", () => ({
84
- setConversationProcessingStartedAt: () => {},
85
- isConversationProcessing: () => false,
84
+ setConversationProcessingStartedAt: () => {},
85
+ isConversationProcessing: () => false,
86
86
  setConversationOriginChannelIfUnset: () => {},
87
87
  updateConversationContextWindow: () => {},
88
88
  deleteMessageById: () => {},
@@ -240,6 +240,7 @@ describe("channel-reply-delivery", () => {
240
240
  payload: {
241
241
  chatId: "chat-1",
242
242
  text: "Before tool.",
243
+ useBlocks: true,
243
244
  attachments: undefined,
244
245
  assistantId: "assistant-1",
245
246
  },
@@ -249,6 +250,7 @@ describe("channel-reply-delivery", () => {
249
250
  payload: {
250
251
  chatId: "chat-1",
251
252
  text: "After tool.",
253
+ useBlocks: true,
252
254
  attachments,
253
255
  assistantId: "assistant-1",
254
256
  },
@@ -307,12 +309,14 @@ describe("channel-reply-delivery", () => {
307
309
  expect(deliveryCalls[0].payload).toEqual({
308
310
  chatId: "chat-3",
309
311
  text: "Before tool.",
312
+ useBlocks: true,
310
313
  attachments: undefined,
311
314
  assistantId: "assistant-2",
312
315
  });
313
316
  expect(deliveryCalls[1].payload).toEqual({
314
317
  chatId: "chat-3",
315
318
  text: "After tool.",
319
+ useBlocks: true,
316
320
  attachments: [
317
321
  {
318
322
  id: "att-2",
@@ -0,0 +1,128 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { eq } from "drizzle-orm";
4
+
5
+ import { makeMockLogger } from "./helpers/mock-logger.js";
6
+
7
+ mock.module("../util/logger.js", () => ({
8
+ getLogger: () => makeMockLogger(),
9
+ }));
10
+
11
+ import {
12
+ appendCompactionEvent,
13
+ forkCompactionLedger,
14
+ getLatestCompactionEventAtOrBefore,
15
+ } from "../memory/compaction-ledger-store.js";
16
+ import { getDb } from "../memory/db-connection.js";
17
+ import { initializeDb } from "../memory/db-init.js";
18
+ import {
19
+ conversationCompactionEvents,
20
+ conversations,
21
+ } from "../memory/schema.js";
22
+
23
+ await initializeDb();
24
+
25
+ function reset(): void {
26
+ const db = getDb();
27
+ db.delete(conversationCompactionEvents).run();
28
+ db.delete(conversations).run();
29
+ }
30
+
31
+ function makeConversation(id: string): void {
32
+ const now = Date.now();
33
+ getDb()
34
+ .insert(conversations)
35
+ .values({ id, title: id, createdAt: now, updatedAt: now })
36
+ .run();
37
+ }
38
+
39
+ function forkEventsFor(conversationId: string) {
40
+ return getDb()
41
+ .select()
42
+ .from(conversationCompactionEvents)
43
+ .where(eq(conversationCompactionEvents.conversationId, conversationId))
44
+ .all();
45
+ }
46
+
47
+ describe("compaction-ledger-store", () => {
48
+ beforeEach(reset);
49
+
50
+ test("getLatestCompactionEventAtOrBefore returns the newest event at-or-before the cutoff", () => {
51
+ makeConversation("conv");
52
+ appendCompactionEvent("conv", {
53
+ compactedAt: 100,
54
+ summary: "s100",
55
+ compactedMessageCount: 1,
56
+ });
57
+ appendCompactionEvent("conv", {
58
+ compactedAt: 200,
59
+ summary: "s200",
60
+ compactedMessageCount: 3,
61
+ });
62
+ appendCompactionEvent("conv", {
63
+ compactedAt: 300,
64
+ summary: "s300",
65
+ compactedMessageCount: 5,
66
+ });
67
+
68
+ expect(getLatestCompactionEventAtOrBefore("conv", 50)).toBeNull();
69
+ expect(
70
+ getLatestCompactionEventAtOrBefore("conv", 100)?.compactedMessageCount,
71
+ ).toBe(1);
72
+ expect(
73
+ getLatestCompactionEventAtOrBefore("conv", 250)?.compactedMessageCount,
74
+ ).toBe(3);
75
+ expect(
76
+ getLatestCompactionEventAtOrBefore("conv", 999)?.compactedMessageCount,
77
+ ).toBe(5);
78
+ expect(getLatestCompactionEventAtOrBefore("conv", null)).toBeNull();
79
+ });
80
+
81
+ test("getLatestCompactionEventAtOrBefore is scoped per conversation", () => {
82
+ makeConversation("a");
83
+ makeConversation("b");
84
+ appendCompactionEvent("a", {
85
+ compactedAt: 100,
86
+ summary: "a",
87
+ compactedMessageCount: 1,
88
+ });
89
+
90
+ expect(getLatestCompactionEventAtOrBefore("b", 999)).toBeNull();
91
+ });
92
+
93
+ test("forkCompactionLedger copies only events at-or-before the boundary", () => {
94
+ makeConversation("src");
95
+ makeConversation("fork");
96
+ appendCompactionEvent("src", {
97
+ compactedAt: 100,
98
+ summary: "s100",
99
+ compactedMessageCount: 1,
100
+ });
101
+ appendCompactionEvent("src", {
102
+ compactedAt: 300,
103
+ summary: "s300",
104
+ compactedMessageCount: 5,
105
+ });
106
+
107
+ forkCompactionLedger(getDb(), "src", "fork", 200);
108
+
109
+ const copied = forkEventsFor("fork");
110
+ expect(copied).toHaveLength(1);
111
+ expect(copied[0]?.compactedAt).toBe(100);
112
+ expect(copied[0]?.summary).toBe("s100");
113
+ });
114
+
115
+ test("forkCompactionLedger with a null boundary copies nothing", () => {
116
+ makeConversation("src");
117
+ makeConversation("fork");
118
+ appendCompactionEvent("src", {
119
+ compactedAt: 100,
120
+ summary: "s100",
121
+ compactedMessageCount: 1,
122
+ });
123
+
124
+ forkCompactionLedger(getDb(), "src", "fork", null);
125
+
126
+ expect(forkEventsFor("fork")).toHaveLength(0);
127
+ });
128
+ });