@vellumai/assistant 0.9.0-dev.202606182147.d5eebd3 → 0.9.0-dev.202606182338.1cc9a2c

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 (45) hide show
  1. package/openapi.yaml +46 -0
  2. package/package.json +1 -1
  3. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -13
  4. package/src/__tests__/config-schema.test.ts +34 -0
  5. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  6. package/src/__tests__/guardian-card-withdrawal.test.ts +150 -25
  7. package/src/__tests__/reaction-persistence.test.ts +110 -1
  8. package/src/api/events/tool-result.ts +6 -0
  9. package/src/calls/guardian-dispatch.ts +17 -41
  10. package/src/config/call-site-defaults.ts +3 -0
  11. package/src/config/llm-resolver.ts +3 -0
  12. package/src/config/schemas/call-site-catalog.ts +7 -0
  13. package/src/config/schemas/llm.ts +23 -0
  14. package/src/config/schemas/services.ts +18 -0
  15. package/src/config/seed-inference-profiles.ts +20 -0
  16. package/src/daemon/message-types/web-activity.ts +7 -1
  17. package/src/messaging/providers/slack/send.test.ts +114 -2
  18. package/src/messaging/providers/slack/send.ts +30 -7
  19. package/src/notifications/AGENTS.md +2 -0
  20. package/src/notifications/canonical-delivery-recorder.ts +124 -24
  21. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  22. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +5 -3
  23. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +6 -3
  24. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  25. package/src/plugins/defaults/advisor/config.ts +10 -11
  26. package/src/plugins/defaults/advisor/consult.ts +25 -6
  27. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +7 -1
  28. package/src/plugins/defaults/advisor/tools/advisor.ts +11 -0
  29. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  30. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  31. package/src/providers/fetch-provider-catalog.ts +85 -0
  32. package/src/providers/search-provider-catalog.ts +10 -0
  33. package/src/runtime/access-request-helper.ts +17 -44
  34. package/src/runtime/confirmation-request-guardian-bridge.ts +18 -13
  35. package/src/runtime/routes/conversation-query-routes.ts +18 -0
  36. package/src/runtime/routes/guardian-approval-prompt.ts +19 -47
  37. package/src/runtime/routes/inbound-message-handler.ts +33 -234
  38. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  39. package/src/runtime/tool-grant-request-helper.ts +17 -10
  40. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  41. package/src/security/secret-patterns.ts +3 -0
  42. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  43. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  44. package/src/tools/network/web-fetch.ts +372 -1
  45. package/src/tools/network/web-search.ts +213 -10
package/openapi.yaml CHANGED
@@ -16239,6 +16239,7 @@ paths:
16239
16239
  - brave
16240
16240
  - perplexity
16241
16241
  - tavily
16242
+ - firecrawl
16242
16243
  resultCount:
16243
16244
  type: number
16244
16245
  durationMs:
@@ -16286,6 +16287,11 @@ paths:
16286
16287
  type: string
16287
16288
  finalUrl:
16288
16289
  type: string
16290
+ provider:
16291
+ type: string
16292
+ enum:
16293
+ - default
16294
+ - firecrawl
16289
16295
  status:
16290
16296
  type: number
16291
16297
  contentType:
@@ -16621,6 +16627,7 @@ paths:
16621
16627
  - brave
16622
16628
  - perplexity
16623
16629
  - tavily
16630
+ - firecrawl
16624
16631
  resultCount:
16625
16632
  type: number
16626
16633
  durationMs:
@@ -16668,6 +16675,11 @@ paths:
16668
16675
  type: string
16669
16676
  finalUrl:
16670
16677
  type: string
16678
+ provider:
16679
+ type: string
16680
+ enum:
16681
+ - default
16682
+ - firecrawl
16671
16683
  status:
16672
16684
  type: number
16673
16685
  contentType:
@@ -28733,6 +28745,10 @@ components:
28733
28745
  anyOf:
28734
28746
  - type: string
28735
28747
  - type: "null"
28748
+ advisorProfile:
28749
+ anyOf:
28750
+ - type: string
28751
+ - type: "null"
28736
28752
  callSites:
28737
28753
  type: object
28738
28754
  propertyNames:
@@ -28768,6 +28784,16 @@ components:
28768
28784
  type: string
28769
28785
  additionalProperties: {}
28770
28786
  - type: "null"
28787
+ web-fetch:
28788
+ anyOf:
28789
+ - type: object
28790
+ properties:
28791
+ mode:
28792
+ $ref: "#/components/schemas/ServiceMode"
28793
+ provider:
28794
+ type: string
28795
+ additionalProperties: {}
28796
+ - type: "null"
28771
28797
  image-generation:
28772
28798
  anyOf:
28773
28799
  - type: object
@@ -28963,6 +28989,12 @@ components:
28963
28989
  - $ref: "#/components/schemas/ProfileStatus"
28964
28990
  - type: "null"
28965
28991
  - type: "null"
28992
+ advisorEnabled:
28993
+ anyOf:
28994
+ - anyOf:
28995
+ - type: boolean
28996
+ - type: "null"
28997
+ - type: "null"
28966
28998
  mix:
28967
28999
  anyOf:
28968
29000
  - minItems: 2
@@ -29354,6 +29386,8 @@ components:
29354
29386
  type: string
29355
29387
  activeProfile:
29356
29388
  type: string
29389
+ advisorProfile:
29390
+ type: string
29357
29391
  callSites:
29358
29392
  type: object
29359
29393
  propertyNames:
@@ -29504,6 +29538,14 @@ components:
29504
29538
  provider:
29505
29539
  type: string
29506
29540
  additionalProperties: {}
29541
+ web-fetch:
29542
+ type: object
29543
+ properties:
29544
+ mode:
29545
+ $ref: "#/components/schemas/ServiceMode"
29546
+ provider:
29547
+ type: string
29548
+ additionalProperties: {}
29507
29549
  image-generation:
29508
29550
  type: object
29509
29551
  properties:
@@ -29653,6 +29695,10 @@ components:
29653
29695
  anyOf:
29654
29696
  - $ref: "#/components/schemas/ProfileStatus"
29655
29697
  - type: "null"
29698
+ advisorEnabled:
29699
+ anyOf:
29700
+ - type: boolean
29701
+ - type: "null"
29656
29702
  mix:
29657
29703
  minItems: 2
29658
29704
  type: array
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.9.0-dev.202606182147.d5eebd3",
3
+ "version": "0.9.0-dev.202606182338.1cc9a2c",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -236,7 +236,7 @@ describe("channel inbound disk pressure gate", () => {
236
236
  expect(db.select().from(messages).all()).toHaveLength(0);
237
237
  });
238
238
 
239
- test("blocks non-guardian Slack reactions before persistence while locked", async () => {
239
+ test("blocks non-guardian Slack reactions silently (no reply) before persistence while locked", async () => {
240
240
  upsertContact({
241
241
  displayName: "Example Slack User",
242
242
  channels: [
@@ -274,18 +274,10 @@ describe("channel inbound disk pressure gate", () => {
274
274
  reason: "trusted-contact",
275
275
  });
276
276
  expect(processMessage).not.toHaveBeenCalled();
277
- expect(deliverChannelReplyMock.mock.calls).toEqual([
278
- [
279
- "https://gateway.test/deliver/slack",
280
- {
281
- chatId: "slack-channel-1",
282
- text: expectedRemoteBlockReply,
283
- assistantId: "self",
284
- ephemeral: true,
285
- user: "slack-user-1",
286
- },
287
- ],
288
- ]);
277
+ // Reactions are blocked silently during disk pressure: a passive signal
278
+ // has nothing to "try again", so no block reply is delivered (unlike the
279
+ // message path, which does reply).
280
+ expect(deliverChannelReplyMock).not.toHaveBeenCalled();
289
281
 
290
282
  const db = getDb();
291
283
  const event = db
@@ -145,6 +145,40 @@ describe("AssistantConfigSchema", () => {
145
145
  expect(result.services["web-search"].mode).toBe("your-own");
146
146
  });
147
147
 
148
+ test("accepts Firecrawl as a web search provider", () => {
149
+ const result = AssistantConfigSchema.parse({
150
+ services: {
151
+ "web-search": { mode: "your-own", provider: "firecrawl" },
152
+ },
153
+ });
154
+
155
+ expect(result.services["web-search"].provider).toBe("firecrawl");
156
+ expect(result.services["web-search"].mode).toBe("your-own");
157
+ });
158
+
159
+ test("defaults the web-fetch provider to the built-in fetcher", () => {
160
+ const result = AssistantConfigSchema.parse({});
161
+ expect(result.services["web-fetch"].provider).toBe("default");
162
+ });
163
+
164
+ test("accepts Firecrawl as a web fetch provider", () => {
165
+ const result = AssistantConfigSchema.parse({
166
+ services: {
167
+ "web-fetch": { mode: "your-own", provider: "firecrawl" },
168
+ },
169
+ });
170
+
171
+ expect(result.services["web-fetch"].provider).toBe("firecrawl");
172
+ });
173
+
174
+ test("rejects an unknown web-fetch provider", () => {
175
+ expect(() =>
176
+ AssistantConfigSchema.parse({
177
+ services: { "web-fetch": { provider: "nope" } },
178
+ }),
179
+ ).toThrow();
180
+ });
181
+
148
182
  test("accepts valid complete config", () => {
149
183
  const input = {
150
184
  llm: {
@@ -158,6 +158,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
158
158
  "credential-execution/prompted-credential.ts", // shared prompt-action persistence (stores secret via setSecureKeyAsync)
159
159
  "tools/credentials/broker.ts", // brokered credential access
160
160
  "tools/network/web-search.ts", // web search API key lookup
161
+ "tools/network/web-fetch.ts", // web fetch provider (Firecrawl) API key lookup
161
162
  "daemon/handlers/config-telegram.ts", // Telegram bot token management
162
163
  "daemon/handlers/config-vercel.ts", // Vercel API token management
163
164
  "runtime/routes/integrations/twilio.ts", // Twilio credential management (HTTP control-plane)
@@ -25,12 +25,15 @@ import {
25
25
  type CanonicalGuardianRequest,
26
26
  createCanonicalGuardianDelivery,
27
27
  createCanonicalGuardianRequest,
28
+ getPendingCanonicalRequestByDestinationMessage,
28
29
  listCanonicalGuardianDeliveries,
29
30
  } from "../memory/canonical-guardian-store.js";
30
31
  import { getDb } from "../memory/db-connection.js";
31
32
  import { initializeDb } from "../memory/db-init.js";
32
- import { recordCanonicalChannelDelivery } from "../notifications/canonical-delivery-recorder.js";
33
- import type { NotificationDeliveryResult } from "../notifications/types.js";
33
+ import {
34
+ recordApprovalCardDelivery,
35
+ recordGuardianRequestDeliveries,
36
+ } from "../notifications/canonical-delivery-recorder.js";
34
37
 
35
38
  initializeDb();
36
39
 
@@ -232,28 +235,161 @@ describe("withdrawGuardianRequestCards", () => {
232
235
  });
233
236
  });
234
237
 
235
- describe("recordCanonicalChannelDelivery", () => {
238
+ describe("recordApprovalCardDelivery", () => {
236
239
  beforeEach(() => {
237
240
  resetTables();
238
- withdrawSlackApprovalCard.mockClear();
239
241
  });
240
242
 
241
- test("captures the channel-native message id so the card can be withdrawn", async () => {
243
+ test("records a channel card with its addressing and status", () => {
242
244
  const req = makeRequest();
243
- const result: NotificationDeliveryResult = {
245
+ const delivery = recordApprovalCardDelivery({
246
+ requestId: req.id,
247
+ channel: "slack",
248
+ chatId: "C1",
249
+ messageId: "1700000000.0001",
250
+ status: "sent",
251
+ });
252
+ expect(delivery?.destinationChannel).toBe("slack");
253
+ expect(delivery?.destinationChatId).toBe("C1");
254
+ expect(delivery?.destinationMessageId).toBe("1700000000.0001");
255
+ expect(delivery?.status).toBe("sent");
256
+ });
257
+
258
+ test("records a vellum card addressed by conversation id, defaulting to pending", () => {
259
+ const req = makeRequest();
260
+ const delivery = recordApprovalCardDelivery({
261
+ requestId: req.id,
262
+ channel: "vellum",
263
+ conversationId: "conv-x",
264
+ });
265
+ expect(delivery?.destinationConversationId).toBe("conv-x");
266
+ expect(delivery?.destinationChatId).toBeNull();
267
+ expect(delivery?.status).toBe("pending");
268
+ });
269
+
270
+ test("lets a Slack reaction resolve back to its request (LUM-2502)", () => {
271
+ // A delivered Slack approval card must be addressable by (channel, chat, ts)
272
+ // so an emoji reaction on it resolves to the right request rather than
273
+ // silently falling through to transcript persistence.
274
+ const req = makeRequest();
275
+ recordApprovalCardDelivery({
276
+ requestId: req.id,
244
277
  channel: "slack",
245
- destination: "C999",
278
+ chatId: "C-guardian",
279
+ messageId: "1700000000.5678",
246
280
  status: "sent",
247
- messageId: "1700000000.1234",
248
- };
281
+ });
282
+
283
+ const resolved = getPendingCanonicalRequestByDestinationMessage(
284
+ "slack",
285
+ "C-guardian",
286
+ "1700000000.5678",
287
+ );
288
+ expect(resolved?.id).toBe(req.id);
289
+ });
290
+ });
291
+
292
+ describe("recordGuardianRequestDeliveries", () => {
293
+ beforeEach(() => {
294
+ resetTables();
295
+ withdrawSlackApprovalCard.mockClear();
296
+ });
297
+
298
+ test("records each delivery with addressing + status and returns the vellum id", () => {
299
+ const req = makeRequest();
300
+ const vellumId = recordGuardianRequestDeliveries({
301
+ requestId: req.id,
302
+ deliveryResults: [
303
+ {
304
+ channel: "vellum",
305
+ destination: "",
306
+ status: "sent",
307
+ conversationId: "conv-1",
308
+ },
309
+ {
310
+ channel: "slack",
311
+ destination: "C999",
312
+ status: "sent",
313
+ messageId: "1700000000.1234",
314
+ },
315
+ ],
316
+ });
317
+
318
+ const deliveries = listCanonicalGuardianDeliveries(req.id);
319
+ expect(deliveries).toHaveLength(2);
320
+ const vellum = deliveries.find((d) => d.destinationChannel === "vellum");
321
+ const slack = deliveries.find((d) => d.destinationChannel === "slack");
322
+ expect(vellumId).toBe(vellum?.id);
323
+ expect(vellum?.destinationConversationId).toBe("conv-1");
324
+ expect(vellum?.status).toBe("sent");
325
+ expect(slack?.destinationChatId).toBe("C999");
326
+ expect(slack?.destinationMessageId).toBe("1700000000.1234");
327
+ expect(slack?.status).toBe("sent");
328
+ });
329
+
330
+ test("reuses a pre-created vellum row instead of creating a second", () => {
331
+ const req = makeRequest();
332
+ const pre = recordApprovalCardDelivery({
333
+ requestId: req.id,
334
+ channel: "vellum",
335
+ conversationId: "conv-1",
336
+ });
337
+
338
+ const vellumId = recordGuardianRequestDeliveries({
339
+ requestId: req.id,
340
+ deliveryResults: [
341
+ {
342
+ channel: "vellum",
343
+ destination: "",
344
+ status: "sent",
345
+ conversationId: "conv-1",
346
+ },
347
+ ],
348
+ vellumDeliveryId: pre?.id,
349
+ });
350
+
351
+ expect(vellumId).toBe(pre?.id);
352
+ const deliveries = listCanonicalGuardianDeliveries(req.id);
353
+ expect(deliveries).toHaveLength(1);
354
+ expect(deliveries[0].status).toBe("sent");
355
+ });
356
+
357
+ test("marks a non-sent delivery result as failed (status now tracked for all producers)", () => {
358
+ const req = makeRequest();
359
+ recordGuardianRequestDeliveries({
360
+ requestId: req.id,
361
+ deliveryResults: [
362
+ { channel: "slack", destination: "C1", status: "failed" },
363
+ ],
364
+ });
365
+ const [delivery] = listCanonicalGuardianDeliveries(req.id);
366
+ expect(delivery.status).toBe("failed");
367
+ });
249
368
 
250
- const delivery = recordCanonicalChannelDelivery(req.id, result);
369
+ test("omits chat id when the channel destination is empty", () => {
370
+ const req = makeRequest();
371
+ recordGuardianRequestDeliveries({
372
+ requestId: req.id,
373
+ deliveryResults: [{ channel: "slack", destination: "", status: "sent" }],
374
+ });
375
+ const [delivery] = listCanonicalGuardianDeliveries(req.id);
376
+ expect(delivery.destinationChatId).toBeNull();
377
+ });
251
378
 
252
- expect(delivery.destinationChannel).toBe("slack");
253
- expect(delivery.destinationChatId).toBe("C999");
254
- expect(delivery.destinationMessageId).toBe("1700000000.1234");
379
+ test("records a Slack delivery the withdrawal path can then edit in place", async () => {
380
+ const req = makeRequest();
381
+ recordGuardianRequestDeliveries({
382
+ requestId: req.id,
383
+ deliveryResults: [
384
+ {
385
+ channel: "slack",
386
+ destination: "C999",
387
+ status: "sent",
388
+ messageId: "1700000000.1234",
389
+ },
390
+ ],
391
+ });
255
392
 
256
- // End-to-end: the recorded delivery is now editable by the withdrawal path.
257
393
  await withdrawGuardianRequestCards({
258
394
  request: req,
259
395
  status: "approved",
@@ -264,15 +400,4 @@ describe("recordCanonicalChannelDelivery", () => {
264
400
  "1700000000.1234",
265
401
  );
266
402
  });
267
-
268
- test("omits chat id when the destination is empty", () => {
269
- const req = makeRequest();
270
- const delivery = recordCanonicalChannelDelivery(req.id, {
271
- channel: "slack",
272
- destination: "",
273
- status: "sent",
274
- });
275
- expect(delivery.destinationChatId).toBeNull();
276
- expect(listCanonicalGuardianDeliveries(req.id)).toHaveLength(1);
277
- });
278
403
  });
@@ -59,7 +59,7 @@ import * as pendingInteractions from "../runtime/pending-interactions.js";
59
59
  import {
60
60
  isSlackReactionEvent,
61
61
  parseSlackReactionCallbackData,
62
- } from "../runtime/routes/inbound-message-handler.js";
62
+ } from "../runtime/routes/inbound-stages/reaction-intercept.js";
63
63
  import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
64
64
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
65
65
 
@@ -575,3 +575,112 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
575
575
  expect(reactionRows.length).toBe(0);
576
576
  });
577
577
  });
578
+
579
+ // ---------------------------------------------------------------------------
580
+ // Reaction access-control regression (LUM-2489)
581
+ // ---------------------------------------------------------------------------
582
+ //
583
+ // A reaction is a passive signal, not an access attempt. Because reactions are
584
+ // dispatched before the ingress pipeline, an unknown user's 👍 must never run
585
+ // ACL (no trusted-contact verification handshake), create a conversation, or
586
+ // write a binding — it is dropped as channel noise. A known contact's reaction
587
+ // is recorded; neither triggers a verification challenge.
588
+
589
+ describe("reaction access control (no verification handshake)", () => {
590
+ const STRANGER_USER_ID = "U_REACTION_STRANGER";
591
+ const CONTACT_USER_ID = "U_REACTION_CONTACT";
592
+ // Guardian's approval channel is a DM, distinct from the public channel the
593
+ // reaction lands in (mirroring production). Reusing the public channel id
594
+ // would let a reactor match the guardian's channel via findContactChannel's
595
+ // externalChatId fallback and mask the bug.
596
+ const GUARDIAN_DM_CHAT = "D_GUARDIAN_DM";
597
+
598
+ function tableCount(table: string): number {
599
+ return (
600
+ getDb().$client.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as {
601
+ n: number;
602
+ }
603
+ ).n;
604
+ }
605
+
606
+ beforeEach(() => {
607
+ resetState();
608
+ getDb().run("DELETE FROM channel_verification_sessions");
609
+ // The assistant has a guardian (as in production); the reactors below are
610
+ // different users.
611
+ createGuardianBinding({
612
+ channel: "slack",
613
+ guardianExternalUserId: GUARDIAN_USER_ID,
614
+ guardianDeliveryChatId: GUARDIAN_DM_CHAT,
615
+ guardianPrincipalId: GUARDIAN_USER_ID,
616
+ });
617
+ msgCounter = 0;
618
+ });
619
+
620
+ test("stranger's reaction is dropped — no challenge, session, conversation, or row", async () => {
621
+ let agentDispatched = false;
622
+ const processMessage = async (): Promise<{ messageId: string }> => {
623
+ agentDispatched = true;
624
+ return { messageId: "should-not-be-called" };
625
+ };
626
+
627
+ const req = buildReactionRequest("reaction:thumbsup", {
628
+ actorExternalId: STRANGER_USER_ID,
629
+ actorDisplayName: "Outside Reactor",
630
+ actorUsername: "outsider",
631
+ });
632
+ const resp = await handleChannelInbound(
633
+ req,
634
+ processMessage,
635
+ TEST_BEARER_TOKEN,
636
+ );
637
+ const json = (await resp.json()) as Record<string, unknown>;
638
+
639
+ // Accepted as a passive signal — never denied or turned into a challenge.
640
+ expect(json.accepted).toBe(true);
641
+ expect(json.denied).not.toBe(true);
642
+ expect(json.reason).not.toBe("verification_challenge_sent");
643
+ expect(json.verificationSessionId).toBeUndefined();
644
+ expect(agentDispatched).toBe(false);
645
+
646
+ // No verification handshake, and no side effects: a dropped reaction leaves
647
+ // no transcript row, no conversation, and no binding.
648
+ expect(tableCount("channel_verification_sessions")).toBe(0);
649
+ expect(readPersistedMessages().length).toBe(0);
650
+ expect(tableCount("conversations")).toBe(0);
651
+ expect(tableCount("external_conversation_bindings")).toBe(0);
652
+ });
653
+
654
+ test("known contact's reaction is recorded — no challenge", async () => {
655
+ // A pending contact classifies as `unverified_contact` — a known tier, so
656
+ // its reactions are recorded. On a real message it would be re-challenged,
657
+ // but a reaction must not trigger that.
658
+ upsertContactChannel({
659
+ sourceChannel: "slack",
660
+ externalUserId: CONTACT_USER_ID,
661
+ externalChatId: SLACK_CHANNEL_ID,
662
+ status: "pending",
663
+ policy: "allow",
664
+ displayName: "Pending Contact",
665
+ });
666
+
667
+ const req = buildReactionRequest("reaction:tada", {
668
+ actorExternalId: CONTACT_USER_ID,
669
+ actorDisplayName: "Pending Contact",
670
+ actorUsername: "pending_contact",
671
+ });
672
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
673
+ const json = (await resp.json()) as Record<string, unknown>;
674
+
675
+ expect(json.denied).not.toBe(true);
676
+ expect(json.reason).not.toBe("verification_challenge_sent");
677
+ expect(tableCount("channel_verification_sessions")).toBe(0);
678
+
679
+ const rows = readPersistedMessages();
680
+ expect(rows.length).toBe(1);
681
+ const envelope = JSON.parse(rows[0].metadata!) as Record<string, unknown>;
682
+ const slackMeta = readSlackMetadata(envelope.slackMeta as string);
683
+ expect(slackMeta?.eventKind).toBe("reaction");
684
+ expect(slackMeta?.reaction?.emoji).toBe("tada");
685
+ });
686
+ });
@@ -47,10 +47,15 @@ export const WebSearchProviderIdSchema = z.enum([
47
47
  "brave",
48
48
  "perplexity",
49
49
  "tavily",
50
+ "firecrawl",
50
51
  ]);
51
52
 
52
53
  export type WebSearchProviderId = z.infer<typeof WebSearchProviderIdSchema>;
53
54
 
55
+ export const WebFetchProviderIdSchema = z.enum(["default", "firecrawl"]);
56
+
57
+ export type WebFetchProviderId = z.infer<typeof WebFetchProviderIdSchema>;
58
+
54
59
  export const WebSearchResultItemSchema = z.object({
55
60
  rank: z.number(),
56
61
  title: z.string(),
@@ -78,6 +83,7 @@ export type WebSearchMetadata = z.infer<typeof WebSearchMetadataSchema>;
78
83
  export const WebFetchMetadataSchema = z.object({
79
84
  url: z.string(),
80
85
  finalUrl: z.string(),
86
+ provider: WebFetchProviderIdSchema.optional(),
81
87
  status: z.number(),
82
88
  contentType: z.string().optional(),
83
89
  byteCount: z.number(),
@@ -9,15 +9,15 @@
9
9
 
10
10
  import { findGuardianForChannel } from "../contacts/contact-store.js";
11
11
  import {
12
- createCanonicalGuardianDelivery,
13
12
  createCanonicalGuardianRequest,
14
13
  listCanonicalGuardianDeliveries,
15
14
  listCanonicalGuardianRequests,
16
- updateCanonicalGuardianDelivery,
17
15
  } from "../memory/canonical-guardian-store.js";
18
- import { recordCanonicalChannelDelivery } from "../notifications/canonical-delivery-recorder.js";
16
+ import {
17
+ recordApprovalCardDelivery,
18
+ recordGuardianRequestDeliveries,
19
+ } from "../notifications/canonical-delivery-recorder.js";
19
20
  import { emitNotificationSignal } from "../notifications/emit-signal.js";
20
- import type { NotificationDeliveryResult } from "../notifications/types.js";
21
21
  import { getLogger } from "../util/logger.js";
22
22
  import { getUserConsultationTimeoutMs } from "./call-constants.js";
23
23
  import type { CallPendingQuestion } from "./types.js";
@@ -40,17 +40,6 @@ export interface GuardianDispatchParams {
40
40
  inputDigest?: string;
41
41
  }
42
42
 
43
- function applyDeliveryStatus(
44
- deliveryId: string,
45
- result: NotificationDeliveryResult,
46
- ): void {
47
- if (result.status === "sent") {
48
- updateCanonicalGuardianDelivery(deliveryId, { status: "sent" });
49
- return;
50
- }
51
- updateCanonicalGuardianDelivery(deliveryId, { status: "failed" });
52
- }
53
-
54
43
  /**
55
44
  * Dispatch a guardian action request to all configured channels.
56
45
  * Fire-and-forget: errors are logged but do not propagate.
@@ -180,7 +169,7 @@ async function dispatchGuardianQuestionInner(
180
169
 
181
170
  // Route through the canonical notification pipeline. The paired vellum
182
171
  // conversation from this pipeline is the canonical guardian conversation.
183
- let vellumDeliveryId: string | null = null;
172
+ let vellumDeliveryId: string | undefined;
184
173
  const requestCode =
185
174
  request.requestCode ?? request.id.slice(0, 6).toUpperCase();
186
175
  const signalResult = await emitNotificationSignal({
@@ -209,39 +198,26 @@ async function dispatchGuardianQuestionInner(
209
198
  onConversationCreated: (info) => {
210
199
  if (info.sourceEventName !== "guardian.question" || vellumDeliveryId)
211
200
  return;
212
- const delivery = createCanonicalGuardianDelivery({
201
+ vellumDeliveryId = recordApprovalCardDelivery({
213
202
  requestId: request.id,
214
- destinationChannel: "vellum",
215
- destinationConversationId: info.conversationId,
216
- });
217
- vellumDeliveryId = delivery.id;
203
+ channel: "vellum",
204
+ conversationId: info.conversationId,
205
+ })?.id;
218
206
  },
219
207
  });
220
208
 
221
- for (const result of signalResult.deliveryResults) {
222
- if (result.channel === "vellum") {
223
- if (!vellumDeliveryId) {
224
- const delivery = createCanonicalGuardianDelivery({
225
- requestId: request.id,
226
- destinationChannel: "vellum",
227
- destinationConversationId: result.conversationId,
228
- });
229
- vellumDeliveryId = delivery.id;
230
- }
231
- applyDeliveryStatus(vellumDeliveryId, result);
232
- continue;
233
- }
234
-
235
- const delivery = recordCanonicalChannelDelivery(request.id, result);
236
- applyDeliveryStatus(delivery.id, result);
237
- }
209
+ vellumDeliveryId = recordGuardianRequestDeliveries({
210
+ requestId: request.id,
211
+ deliveryResults: signalResult.deliveryResults,
212
+ vellumDeliveryId,
213
+ });
238
214
 
239
215
  if (!vellumDeliveryId) {
240
- const fallback = createCanonicalGuardianDelivery({
216
+ recordApprovalCardDelivery({
241
217
  requestId: request.id,
242
- destinationChannel: "vellum",
218
+ channel: "vellum",
219
+ status: "failed",
243
220
  });
244
- updateCanonicalGuardianDelivery(fallback.id, { status: "failed" });
245
221
  log.warn(
246
222
  { requestId: request.id, reason: signalResult.reason },
247
223
  "Notification pipeline did not produce a vellum delivery result",
@@ -69,6 +69,9 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
69
69
  meetConsentMonitor: { profile: "cost-optimized" },
70
70
  meetChatOpportunity: { profile: "cost-optimized" },
71
71
  inference: { profile: "cost-optimized" },
72
+ // The advisor consults the strongest managed profile by default; a workspace
73
+ // overrides this via `llm.advisorProfile` (which floats above this).
74
+ advisor: { profile: "quality-optimized" },
72
75
 
73
76
  heartbeatAgent: {
74
77
  profile: "cost-optimized",
@@ -366,6 +366,9 @@ function profileConfigFragment(profile: ProfileEntry): Mergeable {
366
366
  // lower-precedence (e.g. active) profile into one that merely inherited it.
367
367
  // `RetryProvider` resolves it from the applied profile, not the merge.
368
368
  logitBias: _logitBias,
369
+ // Per-profile advisor toggle is profile identity, not inheritable model
370
+ // config — strip it so it can't leak into the merged `LLMConfigBase`.
371
+ advisorEnabled: _advisorEnabled,
369
372
  ...config
370
373
  } = profile;
371
374
  return config as Mergeable;