@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
@@ -0,0 +1,129 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import { getSqliteFrom } from "../memory/db-connection.js";
7
+ import { migrateCreateCompactionEvents } from "../memory/migrations/302-create-compaction-events.js";
8
+ import * as schema from "../memory/schema.js";
9
+
10
+ interface EventRow {
11
+ conversation_id: string;
12
+ compacted_at: number;
13
+ summary: string;
14
+ compacted_message_count: number;
15
+ }
16
+
17
+ function createTestDb() {
18
+ const sqlite = new Database(":memory:");
19
+ sqlite.exec("PRAGMA journal_mode=WAL");
20
+ sqlite.exec("PRAGMA foreign_keys = ON");
21
+ return drizzle(sqlite, { schema });
22
+ }
23
+
24
+ function bootstrapCheckpointsTable(raw: Database): void {
25
+ raw.exec(/*sql*/ `
26
+ CREATE TABLE IF NOT EXISTS memory_checkpoints (
27
+ key TEXT PRIMARY KEY,
28
+ value TEXT NOT NULL,
29
+ updated_at INTEGER NOT NULL
30
+ )
31
+ `);
32
+ }
33
+
34
+ function bootstrapConversations(raw: Database): void {
35
+ raw.exec(/*sql*/ `
36
+ CREATE TABLE conversations (
37
+ id TEXT PRIMARY KEY,
38
+ title TEXT,
39
+ created_at INTEGER NOT NULL,
40
+ updated_at INTEGER NOT NULL,
41
+ context_summary TEXT,
42
+ context_compacted_message_count INTEGER NOT NULL DEFAULT 0,
43
+ context_compacted_at INTEGER
44
+ )
45
+ `);
46
+ }
47
+
48
+ function insertConversation(
49
+ raw: Database,
50
+ id: string,
51
+ summary: string | null,
52
+ count: number,
53
+ compactedAt: number | null,
54
+ ): void {
55
+ raw
56
+ .query(
57
+ /*sql*/ `
58
+ INSERT INTO conversations (
59
+ id, title, created_at, updated_at,
60
+ context_summary, context_compacted_message_count, context_compacted_at
61
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
62
+ `,
63
+ )
64
+ .run(id, id, 1000, 2000, summary, count, compactedAt);
65
+ }
66
+
67
+ function getEvents(raw: Database): EventRow[] {
68
+ return raw
69
+ .query(
70
+ /*sql*/ `
71
+ SELECT conversation_id, compacted_at, summary, compacted_message_count
72
+ FROM conversation_compaction_events
73
+ ORDER BY conversation_id
74
+ `,
75
+ )
76
+ .all() as EventRow[];
77
+ }
78
+
79
+ describe("migrateCreateCompactionEvents", () => {
80
+ test("creates the ledger and backfills one event per compacted conversation", () => {
81
+ const db = createTestDb();
82
+ const raw = getSqliteFrom(db);
83
+ bootstrapCheckpointsTable(raw);
84
+ bootstrapConversations(raw);
85
+
86
+ insertConversation(raw, "compacted", "Summary text", 5, 3000);
87
+ insertConversation(raw, "uncompacted", null, 0, null);
88
+ // count>0 but never actually compacted (no timestamp) — must be skipped.
89
+ insertConversation(raw, "count-without-at", "Orphan summary", 4, null);
90
+
91
+ migrateCreateCompactionEvents(db);
92
+
93
+ expect(getEvents(raw)).toEqual([
94
+ {
95
+ conversation_id: "compacted",
96
+ compacted_at: 3000,
97
+ summary: "Summary text",
98
+ compacted_message_count: 5,
99
+ },
100
+ ]);
101
+ });
102
+
103
+ test("is idempotent — re-running does not duplicate backfilled events", () => {
104
+ const db = createTestDb();
105
+ const raw = getSqliteFrom(db);
106
+ bootstrapCheckpointsTable(raw);
107
+ bootstrapConversations(raw);
108
+ insertConversation(raw, "compacted", "Summary text", 5, 3000);
109
+
110
+ migrateCreateCompactionEvents(db);
111
+ migrateCreateCompactionEvents(db);
112
+
113
+ expect(getEvents(raw)).toHaveLength(1);
114
+ });
115
+
116
+ test("the NOT EXISTS guard prevents duplicates even if the checkpoint is lost", () => {
117
+ const db = createTestDb();
118
+ const raw = getSqliteFrom(db);
119
+ bootstrapCheckpointsTable(raw);
120
+ bootstrapConversations(raw);
121
+ insertConversation(raw, "compacted", "Summary text", 5, 3000);
122
+
123
+ migrateCreateCompactionEvents(db);
124
+ raw.exec(`DELETE FROM memory_checkpoints`);
125
+ migrateCreateCompactionEvents(db);
126
+
127
+ expect(getEvents(raw)).toHaveLength(1);
128
+ });
129
+ });
@@ -23,7 +23,6 @@ mock.module("../config/env.js", () => ({
23
23
 
24
24
  import { eq } from "drizzle-orm";
25
25
 
26
- import { upsertContactChannel } from "../contacts/contacts-write.js";
27
26
  import { getDb } from "../memory/db-connection.js";
28
27
  import { initializeDb } from "../memory/db-init.js";
29
28
  import { linkMessage, recordInbound } from "../memory/delivery-crud.js";
@@ -33,7 +32,10 @@ import {
33
32
  writeSlackMetadata,
34
33
  } from "../messaging/providers/slack/message-metadata.js";
35
34
  import { _setDeleteLookupConfigForTests } from "../runtime/routes/inbound-message-handler.js";
36
- import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
35
+ import {
36
+ handleChannelInbound,
37
+ seedContactChannel,
38
+ } from "./helpers/channel-test-adapter.js";
37
39
 
38
40
  await initializeDb();
39
41
 
@@ -56,7 +58,7 @@ function resetState(): void {
56
58
  }
57
59
 
58
60
  function seedActiveDeleteActor(externalChatId: string): void {
59
- upsertContactChannel({
61
+ seedContactChannel({
60
62
  sourceChannel: "slack",
61
63
  externalUserId: SLACK_DELETE_ACTOR_ID,
62
64
  externalChatId,
@@ -78,7 +78,6 @@ import {
78
78
  saveRawConfig,
79
79
  setNestedValue,
80
80
  } from "../config/loader.js";
81
- import { upsertContactChannel } from "../contacts/contacts-write.js";
82
81
  import { getDb } from "../memory/db-connection.js";
83
82
  import { initializeDb } from "../memory/db-init.js";
84
83
  import { messages } from "../memory/schema/conversations.js";
@@ -86,7 +85,10 @@ import {
86
85
  readSlackMetadata,
87
86
  type SlackMessageMetadata,
88
87
  } from "../messaging/providers/slack/message-metadata.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
 
91
93
  await initializeDb();
92
94
 
@@ -117,7 +119,7 @@ function setConfiguredSlackBotUserId(botUserId: string): void {
117
119
  }
118
120
 
119
121
  function seedActiveMember(): void {
120
- upsertContactChannel({
122
+ seedContactChannel({
121
123
  sourceChannel: "slack",
122
124
  externalUserId: SLACK_DM_USER_ID,
123
125
  externalChatId: SLACK_DM_CHANNEL_ID,
@@ -128,7 +130,7 @@ function seedActiveMember(): void {
128
130
  }
129
131
 
130
132
  function seedSlackGuardian(): void {
131
- upsertContactChannel({
133
+ seedContactChannel({
132
134
  sourceChannel: "slack",
133
135
  externalUserId: SLACK_DM_USER_ID,
134
136
  externalChatId: SLACK_DM_CHANNEL_ID,
@@ -21,17 +21,13 @@ mock.module("../channels/config.js", () => ({
21
21
 
22
22
  // Guardian connectivity is resolved from the gateway pull. No active guardian
23
23
  // binding ⇒ binding-based channels (telegram) are not reported connected.
24
+ // Guardian connectivity resolves solely from the gateway delivery; an empty
25
+ // list ⇒ telegram stays disconnected.
24
26
  mock.module("../contacts/guardian-delivery-reader.js", () => ({
25
27
  getGuardianDelivery: async () => [],
26
28
  guardianForChannel: () => undefined,
27
29
  }));
28
30
 
29
- // connectivity falls back to the local contacts read on a per-channel gateway
30
- // no-match; no local binding ⇒ telegram stays disconnected.
31
- mock.module("../contacts/contact-store.js", () => ({
32
- findGuardianForChannel: () => null,
33
- }));
34
-
35
31
  mock.module("../notifications/adapters/macos.js", () => ({
36
32
  VellumAdapter: class {
37
33
  constructor(_broadcastFn: unknown) {}
@@ -17,7 +17,7 @@ mock.module("../contacts/guardian-delivery-reader.js", () => ({
17
17
  list.find((g) => g.channelType === channelType && g.status === "active"),
18
18
  }));
19
19
 
20
- import { findGuardianForChannel } from "../contacts/contact-store.js";
20
+ import { findContactByAddress } from "../contacts/contact-store.js";
21
21
  import { getDb } from "../memory/db-connection.js";
22
22
  import { initializeDb } from "../memory/db-init.js";
23
23
  import { healGuardianBindingDrift } from "../runtime/guardian-vellum-migration.js";
@@ -31,17 +31,31 @@ function resetTables(): void {
31
31
  db.run("DELETE FROM contacts");
32
32
  }
33
33
 
34
- /** Gateway delivery mirroring the local guardian binding's principal. */
35
- function gatewayGuardian(principalId: string): GuardianDelivery {
34
+ /**
35
+ * Gateway delivery for the vellum guardian. `principalId` is the gateway-owned
36
+ * binding principal; `address` is the local channel address the heal resolves
37
+ * its write target by (defaults to the principal for same-principal bindings).
38
+ */
39
+ function gatewayGuardian(
40
+ principalId: string,
41
+ address: string = principalId,
42
+ ): GuardianDelivery {
36
43
  return {
37
44
  channelType: "vellum",
38
45
  contactId: "guardian-contact",
39
46
  principalId,
40
- address: principalId,
47
+ address,
41
48
  status: "active",
42
49
  };
43
50
  }
44
51
 
52
+ /** Read the local vellum guardian channel/contact by its channel address. */
53
+ function localVellumGuardian(address: string) {
54
+ const contact = findContactByAddress("vellum", address);
55
+ const channel = contact?.channels.find((c) => c.type === "vellum");
56
+ return contact && channel ? { contact, channel } : null;
57
+ }
58
+
45
59
  describe("healGuardianBindingDrift", () => {
46
60
  beforeEach(() => {
47
61
  resetTables();
@@ -63,18 +77,17 @@ describe("healGuardianBindingDrift", () => {
63
77
  const healed = await healGuardianBindingDrift("vellum-principal-old-uuid");
64
78
  expect(healed).toBe(true);
65
79
 
66
- // Guardian binding now matches the old JWT
67
- const guardian = findGuardianForChannel("vellum");
80
+ // The heal repairs the channel identity address to match the JWT. The
81
+ // principalId column is gateway-owned and no longer written locally.
82
+ const guardian = localVellumGuardian("vellum-principal-old-uuid");
68
83
  expect(guardian).not.toBeNull();
69
- expect(guardian!.contact.principalId).toBe("vellum-principal-old-uuid");
70
84
  expect(guardian!.channel.address).toBe("vellum-principal-old-uuid");
71
85
  });
72
86
 
73
- test("repairs the stale local mirror even when the gateway already matches the JWT", async () => {
74
- // Gateway binding already matches the incoming JWT principal, but the
75
- // local mirror is stale the gateway-source-of-truth drift mode. The
76
- // /v1/messages trust path still reads the local mirror in this plan, so
77
- // a stale row must be repaired or the actor stays classified `unknown`.
87
+ test("repairs the local mirror toward the JWT when the gateway diverges", async () => {
88
+ // Gateway principal diverges from the incoming JWT the drift signal. The
89
+ // /v1/messages trust path still reads the local mirror in this plan, so the
90
+ // mirror is repaired toward the JWT or the actor stays `unknown`.
78
91
  createGuardianBinding({
79
92
  channel: "vellum",
80
93
  guardianExternalUserId: "vellum-principal-stale-local",
@@ -82,15 +95,19 @@ describe("healGuardianBindingDrift", () => {
82
95
  guardianPrincipalId: "vellum-principal-stale-local",
83
96
  verifiedVia: "startup-migration",
84
97
  });
85
- mockGuardians = [gatewayGuardian("vellum-principal-jwt")];
98
+ // Gateway address matches the local channel address so the heal can resolve
99
+ // its local write target; only the gateway principal has drifted.
100
+ mockGuardians = [
101
+ gatewayGuardian("vellum-principal-gateway", "vellum-principal-stale-local"),
102
+ ];
86
103
 
87
104
  const healed = await healGuardianBindingDrift("vellum-principal-jwt");
88
105
  expect(healed).toBe(true);
89
106
 
90
- // Local mirror now matches the JWT, so a subsequent local trust
91
- // resolution classifies the actor as guardian rather than unknown.
92
- const guardian = findGuardianForChannel("vellum");
93
- expect(guardian!.contact.principalId).toBe("vellum-principal-jwt");
107
+ // The local mirror's identity address now matches the JWT, so a subsequent
108
+ // local trust resolution classifies the actor as guardian rather than
109
+ // unknown. The principalId column is gateway-owned and not written locally.
110
+ const guardian = localVellumGuardian("vellum-principal-jwt");
94
111
  expect(guardian!.channel.address).toBe("vellum-principal-jwt");
95
112
  });
96
113
 
@@ -122,12 +139,13 @@ describe("healGuardianBindingDrift", () => {
122
139
  const healed = await healGuardianBindingDrift("platform-user-12345");
123
140
  expect(healed).toBe(false);
124
141
 
125
- // Guardian unchanged
126
- const guardian = findGuardianForChannel("vellum");
127
- expect(guardian!.contact.principalId).toBe("vellum-principal-aaa");
142
+ // Guardian unchanged: the local channel identity address still resolves to
143
+ // the original binding (principalId is gateway-owned, no longer local).
144
+ const guardian = localVellumGuardian("vellum-principal-aaa");
145
+ expect(guardian!.channel.address).toBe("vellum-principal-aaa");
128
146
  });
129
147
 
130
- test("refuses to heal when stored principal lacks vellum-principal- prefix", async () => {
148
+ test("refuses to heal when the gateway principal lacks vellum-principal- prefix", async () => {
131
149
  createGuardianBinding({
132
150
  channel: "vellum",
133
151
  guardianExternalUserId: "verified-phone-guardian",
@@ -141,8 +159,10 @@ describe("healGuardianBindingDrift", () => {
141
159
  const healed = await healGuardianBindingDrift("vellum-principal-attacker");
142
160
  expect(healed).toBe(false);
143
161
 
144
- const guardian = findGuardianForChannel("vellum");
145
- expect(guardian!.contact.principalId).toBe("verified-phone-guardian");
162
+ // Guardian unchanged: the local channel identity address still resolves to
163
+ // the original binding (principalId is gateway-owned, no longer local).
164
+ const guardian = localVellumGuardian("verified-phone-guardian");
165
+ expect(guardian!.channel.address).toBe("verified-phone-guardian");
146
166
  });
147
167
 
148
168
  test("returns false when gateway reports no guardian binding", async () => {
@@ -24,11 +24,56 @@ mock.module("../config/loader.js", () => ({
24
24
  }),
25
25
  }));
26
26
 
27
- // The pending_question request principal is resolved via the SAME local source
28
- // the Vellum actor uses — findGuardianForChannel("vellum")?.contact.principalId
29
- // — so the stamped principal always equals the submitting actor principal. The
30
- // real contacts DB is seeded in resetTables(); tests model drift / missing
31
- // guardian by reseeding or clearing that local binding directly.
27
+ // The pending_question request principal is resolved via the gateway guardian
28
+ // delivery for the vellum channel — the SAME source the Vellum actor uses — so
29
+ // the stamped principal always equals the submitting actor principal. The real
30
+ // contacts DB is seeded in resetTables(); the reader mock below derives the
31
+ // gateway delivery from that DB binding so tests model drift / missing guardian
32
+ // by reseeding or clearing the local binding directly.
33
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
34
+ getGuardianDelivery: async (input?: { channelTypes?: string[] }) => {
35
+ const { getDb } = await import("../memory/db-connection.js");
36
+ const { contacts, contactChannels } = await import("../memory/schema.js");
37
+ const { and, eq } = await import("drizzle-orm");
38
+ const channels = input?.channelTypes ?? [];
39
+ return channels
40
+ .map((channelType) => {
41
+ const row = getDb()
42
+ .select({ contact: contacts, channel: contactChannels })
43
+ .from(contacts)
44
+ .innerJoin(
45
+ contactChannels,
46
+ eq(contacts.id, contactChannels.contactId),
47
+ )
48
+ .where(
49
+ and(
50
+ eq(contacts.role, "guardian"),
51
+ eq(contactChannels.type, channelType),
52
+ eq(contactChannels.status, "active"),
53
+ ),
54
+ )
55
+ .get();
56
+ if (!row) return null;
57
+ return {
58
+ channelType,
59
+ contactId: row.contact.id,
60
+ principalId: row.contact.principalId ?? null,
61
+ displayName: row.contact.displayName ?? null,
62
+ address: row.channel.address,
63
+ externalChatId: row.channel.externalChatId ?? null,
64
+ status: "active",
65
+ verifiedAt: row.channel.verifiedAt ?? null,
66
+ };
67
+ })
68
+ .filter((g) => g !== null);
69
+ },
70
+ guardianForChannel: (
71
+ list: Array<{ channelType: string; status: string }>,
72
+ channelType: string,
73
+ ) =>
74
+ list.find((g) => g.channelType === channelType && g.status === "active"),
75
+ }));
76
+
32
77
  const emitCalls: unknown[] = [];
33
78
  let conversationCreatedFromMock: ConversationCreatedInfo | null = null;
34
79
  let mockEmitResult: {
@@ -14,7 +14,6 @@ mock.module("../util/logger.js", () => ({
14
14
  }),
15
15
  }));
16
16
 
17
- import { upsertContact } from "../contacts/contact-store.js";
18
17
  import type { TrustContext } from "../daemon/trust-context.js";
19
18
  import { getDb } from "../memory/db-connection.js";
20
19
  import { initializeDb } from "../memory/db-init.js";
@@ -27,6 +26,7 @@ import {
27
26
  } from "../runtime/trust-context-resolver.js";
28
27
  import {
29
28
  handleChannelInbound,
29
+ seedContactChannel,
30
30
  setAdapterProcessMessage,
31
31
  } from "./helpers/channel-test-adapter.js";
32
32
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
@@ -155,16 +155,12 @@ describe("inbound-message-handler trusted-contact interactivity", () => {
155
155
  resetTables();
156
156
  setAdapterProcessMessage(undefined);
157
157
  // Insert a test contact so the contacts-based ACL lookup passes
158
- upsertContact({
158
+ seedContactChannel({
159
+ sourceChannel: "telegram",
160
+ externalUserId: "telegram-user-default",
159
161
  displayName: "Test User",
160
- channels: [
161
- {
162
- type: "telegram",
163
- address: "telegram-user-default",
164
- status: "active",
165
- policy: "allow",
166
- },
167
- ],
162
+ status: "active",
163
+ policy: "allow",
168
164
  });
169
165
  });
170
166
 
@@ -62,11 +62,11 @@ mock.module("../../daemon/approval-generators.js", () => ({
62
62
  }));
63
63
 
64
64
  import type { TrustClass, TrustVerdict } from "@vellumai/gateway-client";
65
+ import { and, desc, eq } from "drizzle-orm";
65
66
 
66
- import {
67
- findContactChannel,
68
- findGuardianForChannel,
69
- } from "../../contacts/contact-store.js";
67
+ import { findContactChannel } from "../../contacts/contact-store.js";
68
+ import { getDb } from "../../memory/db-connection.js";
69
+ import { contactChannels, contacts } from "../../memory/schema.js";
70
70
  import type {
71
71
  ApprovalConversationGenerator,
72
72
  ApprovalCopyGenerator,
@@ -147,6 +147,27 @@ function stampTrustVerdict(body: Record<string, unknown>): void {
147
147
  body.sourceMetadata = { ...(meta ?? {}), trustVerdict: verdict };
148
148
  }
149
149
 
150
+ /**
151
+ * Local mirror of the gateway's active-guardian-channel resolution, querying
152
+ * the contact store directly (the production query now lives in the gateway).
153
+ */
154
+ function localGuardianForChannel(channelType: string) {
155
+ const row = getDb()
156
+ .select({ contact: contacts, channel: contactChannels })
157
+ .from(contacts)
158
+ .innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
159
+ .where(
160
+ and(
161
+ eq(contacts.role, "guardian"),
162
+ eq(contactChannels.type, channelType),
163
+ eq(contactChannels.status, "active"),
164
+ ),
165
+ )
166
+ .orderBy(desc(contactChannels.verifiedAt))
167
+ .get();
168
+ return row ? { contact: row.contact, channel: row.channel } : null;
169
+ }
170
+
150
171
  /** Local mirror of the gateway resolver, reading the daemon contact store. */
151
172
  export function resolveLocalTrustVerdict(input: {
152
173
  channelType: string;
@@ -161,18 +182,28 @@ export function resolveLocalTrustVerdict(input: {
161
182
  address: input.actorExternalId,
162
183
  })
163
184
  : null;
164
- const guardian = findGuardianForChannel(input.channelType);
185
+ const guardian = localGuardianForChannel(input.channelType);
165
186
 
166
187
  const isGuardian =
167
188
  !!guardian &&
168
189
  !!canonicalSenderId &&
169
190
  guardian.channel.address.toLowerCase() === canonicalSenderId.toLowerCase();
170
191
 
192
+ // ACL columns are no longer on the slimmed ContactChannel type; read the raw
193
+ // row (columns still exist) so this local mirror sees status/policy/verified.
194
+ const memberRow = member
195
+ ? (getDb()
196
+ .select()
197
+ .from(contactChannels)
198
+ .where(eq(contactChannels.id, member.channel.id))
199
+ .get() ?? null)
200
+ : null;
201
+
171
202
  let trustClass: TrustClass;
172
203
  if (isGuardian) {
173
204
  trustClass = "guardian";
174
- } else if (member) {
175
- const status = member.channel.status;
205
+ } else if (memberRow) {
206
+ const status = memberRow.status;
176
207
  if (status === "active") trustClass = "trusted_contact";
177
208
  else if (status === "unverified" || status === "pending")
178
209
  trustClass = "unverified_contact";
@@ -191,22 +222,24 @@ export function resolveLocalTrustVerdict(input: {
191
222
  verdict.guardianDisplayName = guardian.contact.displayName;
192
223
  }
193
224
 
194
- if (member) {
225
+ if (member && memberRow) {
195
226
  verdict.contactId = member.channel.contactId;
196
227
  verdict.channelId = member.channel.id;
197
228
  verdict.type = member.channel.type;
198
229
  verdict.address = member.channel.address;
199
230
  verdict.externalChatId = member.channel.externalChatId;
200
- verdict.status = member.channel.status;
201
- verdict.policy = member.channel.policy;
202
- verdict.verifiedAt = member.channel.verifiedAt;
203
- verdict.verifiedVia = member.channel.verifiedVia;
231
+ verdict.status = memberRow.status;
232
+ verdict.policy = memberRow.policy;
233
+ verdict.verifiedAt = memberRow.verifiedAt;
234
+ verdict.verifiedVia = memberRow.verifiedVia;
204
235
  verdict.memberDisplayName = member.contact.displayName;
205
236
  }
206
237
 
207
238
  return verdict;
208
239
  }
209
240
 
241
+ export { seedContactChannel } from "./seed-contact-channel.js";
242
+
210
243
  // ---------------------------------------------------------------------------
211
244
  // handleDeleteConversation adapter
212
245
  // ---------------------------------------------------------------------------
@@ -4,11 +4,10 @@
4
4
  * path was moved to the gateway.
5
5
  */
6
6
 
7
- import type { ChannelId } from "../../channels/types.js";
8
- import { upsertContact } from "../../contacts/contact-store.js";
7
+ import { getContact } from "../../contacts/contact-store.js";
9
8
  import type { GuardianBinding } from "../../memory/channel-verification-sessions.js";
10
9
  import { ensureGuardianPersonaFile } from "../../prompts/persona-resolver.js";
11
- import { canonicalizeInboundIdentity } from "../../util/canonicalize-identity.js";
10
+ import { seedContactChannel } from "./seed-contact-channel.js";
12
11
 
13
12
  function parseDisplayNameFromMetadata(
14
13
  metadataJson: string | null | undefined,
@@ -36,37 +35,30 @@ export function createGuardianBinding(params: {
36
35
  verifiedVia?: string;
37
36
  metadataJson?: string | null;
38
37
  }): GuardianBinding {
39
- const canonicalId =
40
- canonicalizeInboundIdentity(
41
- params.channel as ChannelId,
42
- params.guardianExternalUserId,
43
- ) ?? params.guardianExternalUserId;
44
-
45
38
  const displayName =
46
39
  parseDisplayNameFromMetadata(params.metadataJson) ??
47
40
  params.guardianExternalUserId;
48
41
 
49
- const contact = upsertContact({
42
+ // The production identity upsert no longer writes ACL columns (gateway-owned);
43
+ // seedContactChannel stamps the guardian ACL state directly so the local
44
+ // guardian-resolution reads still under test resolve this binding.
45
+ const { contactId } = seedContactChannel({
46
+ sourceChannel: params.channel,
47
+ externalUserId: params.guardianExternalUserId,
48
+ externalChatId: params.guardianDeliveryChatId,
50
49
  displayName,
51
50
  role: "guardian",
52
- notes: "guardian",
53
51
  principalId: params.guardianPrincipalId,
54
- channels: [
55
- {
56
- type: params.channel,
57
- address: canonicalId,
58
- externalChatId: params.guardianDeliveryChatId,
59
- status: "active",
60
- verifiedAt: Date.now(),
61
- verifiedVia: params.verifiedVia ?? "challenge",
62
- },
63
- ],
52
+ status: "active",
53
+ verifiedAt: Date.now(),
54
+ verifiedVia: params.verifiedVia ?? "challenge",
64
55
  });
65
56
 
66
57
  // Seed persona file (mirrors gateway's production behavior)
67
- if (contact.userFile) {
58
+ const userFile = getContact(contactId)?.userFile;
59
+ if (userFile) {
68
60
  try {
69
- ensureGuardianPersonaFile(contact.userFile);
61
+ ensureGuardianPersonaFile(userFile);
70
62
  } catch {
71
63
  // Tolerate filesystem failures in tests
72
64
  }
@@ -56,6 +56,7 @@ export function createMockLoggerModule(
56
56
  initLogger: () => {},
57
57
  truncateForLog: (value: string) => value,
58
58
  pruneOldLogFiles: () => 0,
59
+ getCurrentLogFilePath: () => "/tmp/assistant-test-mock.log",
59
60
  LOG_FILE_PATTERN: /^assistant-(\d{4}-\d{2}-\d{2})\.log$/,
60
61
  ...overrides,
61
62
  };