@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
@@ -37,14 +37,20 @@ mock.module("../daemon/identity-helpers.js", () => ({
37
37
  getAssistantName: () => mockAssistantName,
38
38
  }));
39
39
 
40
- import { createConversation } from "../memory/conversation-crud.js";
40
+ import {
41
+ createConversation,
42
+ recordConversationPersistedSeq,
43
+ setConversationProcessingStartedAt,
44
+ } from "../memory/conversation-crud.js";
41
45
  import { getDb } from "../memory/db-connection.js";
42
46
  import { initializeDb } from "../memory/db-init.js";
43
47
  import { messages } from "../memory/schema.js";
44
48
  import { writeSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
49
+ import type { AssistantEvent } from "../runtime/assistant-event.js";
45
50
  import {
46
51
  _resetStreamStateForTesting,
47
- recordPersistedSeq,
52
+ getCurrentSeq,
53
+ stampAndBuffer,
48
54
  } from "../runtime/assistant-stream-state.js";
49
55
  import { handleListMessages } from "../runtime/routes/conversation-routes.js";
50
56
  import { BadRequestError } from "../runtime/routes/errors.js";
@@ -103,6 +109,13 @@ interface MessagePayload {
103
109
  sender?: { displayName?: string; externalUserId?: string };
104
110
  messageLink?: { appUrl?: string; webUrl?: string };
105
111
  threadLink?: { appUrl?: string; webUrl?: string };
112
+ eventKind?: "message" | "reaction";
113
+ reaction?: {
114
+ emoji: string;
115
+ op: "added" | "removed";
116
+ actorDisplayName?: string;
117
+ targetChannelTs: string;
118
+ };
106
119
  };
107
120
  }
108
121
 
@@ -112,6 +125,28 @@ interface ListResponse {
112
125
  oldestTimestamp?: number | null;
113
126
  oldestMessageId?: string | null;
114
127
  seq?: number | null;
128
+ processing?: boolean;
129
+ }
130
+
131
+ /**
132
+ * Stamp `count` conversation-scoped events to advance the global `seq`
133
+ * counter, mirroring `mkEvent` in assistant-stream-state.test.ts. The
134
+ * conversationId is irrelevant — `getCurrentSeq()` is process-global — so
135
+ * any value works for nudging the high-water mark before a creation.
136
+ */
137
+ function advanceGlobalSeq(count: number): void {
138
+ for (let i = 0; i < count; i++) {
139
+ stampAndBuffer({
140
+ id: `seq-bump-${i}`,
141
+ conversationId: "seq-bump-conv",
142
+ emittedAt: new Date().toISOString(),
143
+ message: {
144
+ type: "assistant_text_delta",
145
+ conversationId: "seq-bump-conv",
146
+ text: "x",
147
+ },
148
+ } as AssistantEvent);
149
+ }
115
150
  }
116
151
 
117
152
  function callList(query: Record<string, string>): ListResponse {
@@ -138,7 +173,7 @@ describe("handleListMessages page=latest", () => {
138
173
  const conv = createConversation();
139
174
  seedMessages(conv.id, 3);
140
175
  // AND the daemon has recorded a persisted seq for it
141
- recordPersistedSeq(conv.id, 42);
176
+ recordConversationPersistedSeq(conv.id, 42);
142
177
 
143
178
  // WHEN the snapshot is fetched
144
179
  const body = callList({ conversationId: conv.id, page: "latest" });
@@ -171,7 +206,7 @@ describe("handleListMessages page=latest", () => {
171
206
  // GIVEN a conversation with a recorded persisted seq
172
207
  const conv = createConversation();
173
208
  seedMessages(conv.id, 2);
174
- recordPersistedSeq(conv.id, 7);
209
+ recordConversationPersistedSeq(conv.id, 7);
175
210
 
176
211
  // WHEN fetched with no pagination params
177
212
  const body = callList({ conversationId: conv.id });
@@ -179,6 +214,138 @@ describe("handleListMessages page=latest", () => {
179
214
  // THEN the seq still rides along
180
215
  expect(body.seq).toBe(7);
181
216
  });
217
+
218
+ test("recording advances the seq monotonically and never regresses", () => {
219
+ /**
220
+ * Out-of-order async commits must not lower the high-water mark — the
221
+ * `WHERE seq < ?` guard on the persist UPDATE keeps it monotonic.
222
+ */
223
+
224
+ // GIVEN a conversation whose persisted seq has been recorded
225
+ const conv = createConversation();
226
+ recordConversationPersistedSeq(conv.id, 12);
227
+
228
+ // WHEN a lower seq is recorded (a late, out-of-order commit)
229
+ recordConversationPersistedSeq(conv.id, 8);
230
+
231
+ // THEN the high-water seq is unchanged
232
+ expect(callList({ conversationId: conv.id }).seq).toBe(12);
233
+
234
+ // AND a higher seq still advances it
235
+ recordConversationPersistedSeq(conv.id, 20);
236
+ expect(callList({ conversationId: conv.id }).seq).toBe(20);
237
+ });
238
+
239
+ test("recording ignores non-positive and non-finite seq values", () => {
240
+ // GIVEN a freshly created conversation (no stream activity → null seed)
241
+ const conv = createConversation();
242
+
243
+ // WHEN non-positive / non-finite seqs are recorded
244
+ recordConversationPersistedSeq(conv.id, 0);
245
+ recordConversationPersistedSeq(conv.id, -3);
246
+ recordConversationPersistedSeq(conv.id, Number.NaN);
247
+ recordConversationPersistedSeq(conv.id, Number.POSITIVE_INFINITY);
248
+
249
+ // THEN none take effect and seq stays null
250
+ expect(callList({ conversationId: conv.id }).seq).toBeNull();
251
+ });
252
+
253
+ test("a freshly created conversation is anchored to the current global seq", () => {
254
+ /**
255
+ * Conversations created after some stream activity must not report a
256
+ * null `seq` — that would force the client to cold-start with no
257
+ * alignment baseline. They inherit the current high-water global seq
258
+ * at creation time so the first snapshot can align with the stream.
259
+ */
260
+
261
+ // GIVEN the global stream has already advanced past seq 0
262
+ advanceGlobalSeq(5);
263
+ expect(getCurrentSeq()).toBe(5);
264
+
265
+ // WHEN a brand-new conversation is created and its snapshot fetched
266
+ const conv = createConversation();
267
+ const body = callList({ conversationId: conv.id, page: "latest" });
268
+
269
+ // THEN the snapshot is anchored to the global seq at creation time
270
+ expect(body.seq).toBe(5);
271
+ });
272
+
273
+ test("a conversation created before any stream activity stays null", () => {
274
+ /**
275
+ * With nothing stamped this process, `getCurrentSeq()` is 0 and the
276
+ * creation seed stores NULL (non-positive seqs aren't recorded), so the
277
+ * snapshot legitimately reports null and the client cold-starts.
278
+ */
279
+
280
+ // GIVEN no events have been stamped (stream reset in beforeEach)
281
+ expect(getCurrentSeq()).toBe(0);
282
+
283
+ // WHEN a conversation is created and its snapshot fetched
284
+ const conv = createConversation();
285
+ const body = callList({ conversationId: conv.id, page: "latest" });
286
+
287
+ // THEN seq remains null
288
+ expect(body.seq).toBeNull();
289
+ });
290
+ });
291
+
292
+ describe("processing state", () => {
293
+ test("reports processing=true while the conversation is mid-turn", () => {
294
+ /**
295
+ * The authoritative processing flag lets a client recover from a
296
+ * dropped stream: it can ask the server whether a turn is still
297
+ * running rather than spinning forever.
298
+ */
299
+
300
+ // GIVEN a conversation marked as processing
301
+ const conv = createConversation();
302
+ seedMessages(conv.id, 1);
303
+ setConversationProcessingStartedAt(conv.id, Date.now());
304
+
305
+ // WHEN the snapshot is fetched
306
+ const body = callList({ conversationId: conv.id, page: "latest" });
307
+
308
+ // THEN it reports the conversation is processing
309
+ expect(body.processing).toBe(true);
310
+ });
311
+
312
+ test("reports processing=false when the conversation is idle", () => {
313
+ // GIVEN an idle conversation (processing_started_at is null)
314
+ const conv = createConversation();
315
+ seedMessages(conv.id, 2);
316
+
317
+ // WHEN the snapshot is fetched
318
+ const body = callList({ conversationId: conv.id, page: "latest" });
319
+
320
+ // THEN it reports the conversation is not processing
321
+ expect(body.processing).toBe(false);
322
+ });
323
+
324
+ test("the no-pagination path also reports processing state", () => {
325
+ /** `processing` is present on every resolved-conversation shape. */
326
+
327
+ // GIVEN a processing conversation
328
+ const conv = createConversation();
329
+ seedMessages(conv.id, 1);
330
+ setConversationProcessingStartedAt(conv.id, Date.now());
331
+
332
+ // WHEN fetched with no pagination params
333
+ const body = callList({ conversationId: conv.id });
334
+
335
+ // THEN processing rides along
336
+ expect(body.processing).toBe(true);
337
+ });
338
+
339
+ test("an unresolved conversationKey reports processing=false (page=latest)", () => {
340
+ // WHEN a never-created key is fetched with the stable contract
341
+ const body = callList({
342
+ conversationKey: "no-such-key",
343
+ page: "latest",
344
+ });
345
+
346
+ // THEN it cannot be processing
347
+ expect(body.processing).toBe(false);
348
+ });
182
349
  });
183
350
 
184
351
  test("page=latest with no limit returns all messages chronologically", () => {
@@ -322,6 +489,7 @@ describe("handleListMessages page=latest", () => {
322
489
  webUrl:
323
490
  "https://example.slack.com/archives/C123ABCDEF/p1710000000000100",
324
491
  },
492
+ eventKind: "message",
325
493
  });
326
494
  });
327
495
 
@@ -367,6 +535,7 @@ describe("handleListMessages page=latest", () => {
367
535
  webUrl:
368
536
  "https://example.slack.com/archives/C123ABCDEF/p1710000000000200",
369
537
  },
538
+ eventKind: "message",
370
539
  });
371
540
  });
372
541
 
@@ -8,6 +8,7 @@ mock.module("../util/logger.js", () => ({
8
8
  getLogger: () => makeMockLogger(),
9
9
  initLogger: () => {},
10
10
  pruneOldLogFiles: () => 0,
11
+ getCurrentLogFilePath: () => "/tmp/test-assistant.log",
11
12
  truncateForLog: (value: string, maxLen = 500) => value.slice(0, maxLen),
12
13
  }));
13
14
 
@@ -110,6 +111,7 @@ mock.module("../providers/registry.js", () => ({
110
111
  isNativeWebSearchCapableProvider: () => false,
111
112
  listProviders: () => [],
112
113
  resolveProviderFromConnection: async () => null,
114
+ shouldUseNativeWebSearch: () => false,
113
115
  }));
114
116
 
115
117
  mock.module("../memory/embedding-backend.js", () => ({
@@ -121,6 +123,7 @@ mock.module("../memory/embedding-backend.js", () => ({
121
123
  model: "test",
122
124
  vectors: [],
123
125
  }),
126
+ geminiCacheExtras: () => [],
124
127
  generateSparseEmbedding: () => ({ indices: [], values: [] }),
125
128
  getMemoryBackendStatus: async () => ({
126
129
  enabled: false,
@@ -551,10 +551,14 @@ describe("access-request-helper unit tests", () => {
551
551
  expect(payload.guardianBindingChannel).toBe("telegram");
552
552
  });
553
553
 
554
- test("notifyGuardianOfAccessRequest falls back to local source-channel binding when gateway delivery is empty", async () => {
555
- // Gateway delivery read yields nothing (restart/timeout/malformed IPC).
556
- gatewayGuardians = [];
557
- // Local dual-written mirror still has the vellum anchor + telegram binding.
554
+ test("notifyGuardianOfAccessRequest resolves the source-channel guardian from the gateway delivery", async () => {
555
+ // Gateway delivery serves a telegram guardian matching the vellum anchor.
556
+ seedGatewayGuardian({
557
+ channelType: "telegram",
558
+ address: "guardian-tg",
559
+ externalChatId: "tg-chat",
560
+ principalId: anchorPrincipalId,
561
+ });
558
562
  createGuardianBinding({
559
563
  channel: "telegram",
560
564
  guardianExternalUserId: "guardian-tg",
@@ -578,7 +582,7 @@ describe("access-request-helper unit tests", () => {
578
582
  kind: "access_request",
579
583
  });
580
584
  expect(pending.length).toBe(1);
581
- // Request is decidable: local read supplied the principal + source binding.
585
+ // Request is decidable: gateway delivery supplied the principal + source binding.
582
586
  expect(pending[0].guardianPrincipalId).toBe(anchorPrincipalId);
583
587
  expect(pending[0].guardianExternalUserId).toBe("guardian-tg");
584
588
 
@@ -589,10 +593,8 @@ describe("access-request-helper unit tests", () => {
589
593
  expect(payload.guardianBindingChannel).toBe("telegram");
590
594
  });
591
595
 
592
- test("notifyGuardianOfAccessRequest falls back to local vellum anchor when gateway delivery is empty", async () => {
593
- // Gateway empty; only the local vellum anchor from resetState remains.
594
- gatewayGuardians = [];
595
-
596
+ test("notifyGuardianOfAccessRequest resolves the vellum anchor from the gateway delivery", async () => {
597
+ // Only the vellum anchor (seeded in resetState) is served by the gateway.
596
598
  const result = await notifyGuardianOfAccessRequest({
597
599
  canonicalAssistantId: "self",
598
600
  sourceChannel: "telegram",
@@ -608,7 +610,7 @@ describe("access-request-helper unit tests", () => {
608
610
  kind: "access_request",
609
611
  });
610
612
  expect(pending.length).toBe(1);
611
- // Decidable via the local vellum anchor principal.
613
+ // Decidable via the gateway-served vellum anchor principal.
612
614
  expect(pending[0].guardianPrincipalId).toBe(anchorPrincipalId);
613
615
 
614
616
  const payload = emitSignalCalls[0].contextPayload as Record<
@@ -618,9 +620,9 @@ describe("access-request-helper unit tests", () => {
618
620
  expect(payload.guardianBindingChannel).toBe("vellum");
619
621
  });
620
622
 
621
- test("notifyGuardianOfAccessRequest does not create a decisionable request when both gateway and local reads are empty", async () => {
622
- // Genuinely unbound assistant: gateway empty AND no local binding. Prior
623
- // behavior rejects creation of a decisionable request without a principal.
623
+ test("notifyGuardianOfAccessRequest does not create a decisionable request when the gateway delivery is empty", async () => {
624
+ // Genuinely unbound assistant: gateway serves no guardian. The guard
625
+ // rejects creation of a decisionable request without a principal.
624
626
  gatewayGuardians = [];
625
627
  const db = getDb();
626
628
  db.run("DELETE FROM contact_channels");
@@ -30,11 +30,24 @@ import {
30
30
 
31
31
  // ── Mock state ────────────────────────────────────────────────────
32
32
 
33
+ interface GuardianDeliveryStub {
34
+ channelType: string;
35
+ address: string;
36
+ status: string;
37
+ }
38
+
33
39
  let mockWorkspaceDir: string = "";
34
- let mockVellumGuardian: {
35
- contact: { userFile: string | null };
36
- channel: Record<string, unknown>;
37
- } | null = null;
40
+ // Gateway guardian delivery cache; the guardian's userFile (local INFO) is
41
+ // joined via findContactByAddress on the delivery's address.
42
+ let mockGuardianDeliveries: GuardianDeliveryStub[] = [];
43
+ let mockContactsByAddress: Record<string, { userFile: string | null }> = {};
44
+
45
+ function seedVellumGuardian(userFile: string | null): void {
46
+ mockGuardianDeliveries = [
47
+ { channelType: "vellum", address: "vellum:self", status: "active" },
48
+ ];
49
+ mockContactsByAddress["vellum:vellum:self"] = { userFile };
50
+ }
38
51
 
39
52
  // ── Mock modules (must precede imports from the module under test) ──
40
53
 
@@ -59,10 +72,20 @@ mock.module("../util/platform.js", () => ({
59
72
  }));
60
73
 
61
74
  mock.module("../contacts/contact-store.js", () => ({
62
- findContactByAddress: () => null,
63
- findGuardianForChannel: (channelType: string) =>
64
- channelType === "vellum" ? mockVellumGuardian : null,
65
- listGuardianChannels: () => null,
75
+ findContactByAddress: (channelType: string, address: string) =>
76
+ mockContactsByAddress[`${channelType}:${address}`] ?? null,
77
+ }));
78
+
79
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
80
+ peekCachedGuardianDelivery: (input?: { channelTypes?: string[] }) => {
81
+ if (!input?.channelTypes) return mockGuardianDeliveries;
82
+ return mockGuardianDeliveries.filter((g) =>
83
+ input.channelTypes!.includes(g.channelType),
84
+ );
85
+ },
86
+ guardianForChannel: (list: GuardianDeliveryStub[], channelType: string) =>
87
+ list.find((g) => g.channelType === channelType && g.status === "active"),
88
+ anyGuardian: (list: GuardianDeliveryStub[]) => list[0],
66
89
  }));
67
90
 
68
91
  // Import AFTER mocks so the module under test binds to the stubbed
@@ -87,7 +110,8 @@ afterAll(() => {
87
110
 
88
111
  beforeEach(() => {
89
112
  mockWorkspaceDir = mkdtempSync(join(testRoot, "ws-"));
90
- mockVellumGuardian = null;
113
+ mockGuardianDeliveries = [];
114
+ mockContactsByAddress = {};
91
115
  });
92
116
 
93
117
  afterEach(() => {
@@ -98,10 +122,7 @@ afterEach(() => {
98
122
 
99
123
  describe("writeOnboardingSection", () => {
100
124
  test("writes section to guardian persona file when it exists", () => {
101
- mockVellumGuardian = {
102
- contact: { userFile: "alice.md" },
103
- channel: {},
104
- };
125
+ seedVellumGuardian("alice.md");
105
126
  const guardianPath = workspacePath("users/alice.md");
106
127
  mkdirSync(workspacePath("users"), { recursive: true });
107
128
  writeFileSync(guardianPath, "# User Profile\n\n- **Name:** Alice\n");
@@ -123,7 +144,8 @@ describe("writeOnboardingSection", () => {
123
144
  });
124
145
 
125
146
  test("falls back to users/default.md when guardian path is null", () => {
126
- mockVellumGuardian = null;
147
+ mockGuardianDeliveries = [];
148
+ mockContactsByAddress = {};
127
149
  mkdirSync(workspacePath("users"), { recursive: true });
128
150
  writeFileSync(
129
151
  workspacePath("users/default.md"),
@@ -147,7 +169,8 @@ describe("writeOnboardingSection", () => {
147
169
  });
148
170
 
149
171
  test("falls back to USER.md when no users/ files exist", () => {
150
- mockVellumGuardian = null;
172
+ mockGuardianDeliveries = [];
173
+ mockContactsByAddress = {};
151
174
 
152
175
  writeOnboardingSection({
153
176
  preferredName: "Alice",
@@ -162,7 +185,8 @@ describe("writeOnboardingSection", () => {
162
185
  });
163
186
 
164
187
  test("creates file with header + section when target doesn't exist", () => {
165
- mockVellumGuardian = null;
188
+ mockGuardianDeliveries = [];
189
+ mockContactsByAddress = {};
166
190
 
167
191
  writeOnboardingSection({
168
192
  preferredName: "Alice",
@@ -179,7 +203,8 @@ describe("writeOnboardingSection", () => {
179
203
  });
180
204
 
181
205
  test("idempotent: calling twice produces the same file content", () => {
182
- mockVellumGuardian = null;
206
+ mockGuardianDeliveries = [];
207
+ mockContactsByAddress = {};
183
208
  const normalized = {
184
209
  preferredName: "Alice",
185
210
  commonWork: ["builds code, apps, or tools"],
@@ -196,7 +221,8 @@ describe("writeOnboardingSection", () => {
196
221
  });
197
222
 
198
223
  test("replaces existing onboarding section with updated data", () => {
199
- mockVellumGuardian = null;
224
+ mockGuardianDeliveries = [];
225
+ mockContactsByAddress = {};
200
226
 
201
227
  writeOnboardingSection({
202
228
  preferredName: "Alice",
@@ -222,7 +248,8 @@ describe("writeOnboardingSection", () => {
222
248
  });
223
249
 
224
250
  test("preserves content outside the managed section", () => {
225
- mockVellumGuardian = null;
251
+ mockGuardianDeliveries = [];
252
+ mockContactsByAddress = {};
226
253
  writeFileSync(
227
254
  workspacePath("USER.md"),
228
255
  "# User Profile\n\n- **Name:** Alice\n- **Role:** Engineer\n",
@@ -242,7 +269,8 @@ describe("writeOnboardingSection", () => {
242
269
  });
243
270
 
244
271
  test("omits empty fields", () => {
245
- mockVellumGuardian = null;
272
+ mockGuardianDeliveries = [];
273
+ mockContactsByAddress = {};
246
274
 
247
275
  writeOnboardingSection({
248
276
  commonWork: [],
@@ -257,7 +285,8 @@ describe("writeOnboardingSection", () => {
257
285
  });
258
286
 
259
287
  test("omits preferredName when undefined", () => {
260
- mockVellumGuardian = null;
288
+ mockGuardianDeliveries = [];
289
+ mockContactsByAddress = {};
261
290
 
262
291
  writeOnboardingSection({
263
292
  preferredName: undefined,
@@ -272,7 +301,8 @@ describe("writeOnboardingSection", () => {
272
301
  });
273
302
 
274
303
  test("preserves content after onboarding section when followed by another heading", () => {
275
- mockVellumGuardian = null;
304
+ mockGuardianDeliveries = [];
305
+ mockContactsByAddress = {};
276
306
  writeFileSync(
277
307
  workspacePath("USER.md"),
278
308
  [
@@ -23,6 +23,7 @@ mock.module("../util/logger.js", () => ({
23
23
  truncateForLog: (v: string) => v,
24
24
  initLogger: () => {},
25
25
  pruneOldLogFiles: () => 0,
26
+ getCurrentLogFilePath: () => "/tmp/test-assistant.log",
26
27
  }));
27
28
 
28
29
  let writeRelationshipStateCalled = false;