@vellumai/assistant 0.4.15 → 0.4.17

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 (73) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/package.json +1 -1
  4. package/src/__tests__/approval-routes-http.test.ts +383 -254
  5. package/src/__tests__/call-controller.test.ts +1074 -751
  6. package/src/__tests__/call-routes-http.test.ts +329 -279
  7. package/src/__tests__/channel-approval-routes.test.ts +2 -13
  8. package/src/__tests__/channel-approvals.test.ts +227 -182
  9. package/src/__tests__/channel-guardian.test.ts +1 -0
  10. package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
  11. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  12. package/src/__tests__/conversation-routes.test.ts +71 -41
  13. package/src/__tests__/daemon-server-session-init.test.ts +258 -191
  14. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
  15. package/src/__tests__/extract-email.test.ts +42 -0
  16. package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
  17. package/src/__tests__/gateway-only-guard.test.ts +54 -55
  18. package/src/__tests__/gmail-integration.test.ts +48 -46
  19. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
  20. package/src/__tests__/guardian-outbound-http.test.ts +334 -208
  21. package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
  22. package/src/__tests__/guardian-routing-state.test.ts +257 -209
  23. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
  24. package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
  25. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
  26. package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
  27. package/src/__tests__/ingress-reconcile.test.ts +184 -142
  28. package/src/__tests__/non-member-access-request.test.ts +291 -247
  29. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  30. package/src/__tests__/pairing-concurrent.test.ts +78 -0
  31. package/src/__tests__/recording-intent-handler.test.ts +422 -291
  32. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
  33. package/src/__tests__/runtime-events-sse.test.ts +67 -50
  34. package/src/__tests__/send-endpoint-busy.test.ts +314 -232
  35. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  36. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  37. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
  38. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
  39. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
  40. package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
  41. package/src/__tests__/twilio-config.test.ts +49 -41
  42. package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
  43. package/src/__tests__/twilio-routes.test.ts +389 -280
  44. package/src/calls/call-controller.ts +1 -1
  45. package/src/calls/guardian-action-sweep.ts +6 -6
  46. package/src/calls/twilio-routes.ts +2 -4
  47. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
  48. package/src/config/bundled-skills/messaging/SKILL.md +5 -4
  49. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +69 -4
  50. package/src/config/env.ts +39 -29
  51. package/src/daemon/handlers/config-inbox.ts +5 -5
  52. package/src/daemon/handlers/skills.ts +18 -10
  53. package/src/daemon/ipc-contract/messages.ts +1 -0
  54. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  55. package/src/daemon/pairing-store.ts +15 -2
  56. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  57. package/src/daemon/session-agent-loop.ts +1 -1
  58. package/src/daemon/session-process.ts +1 -1
  59. package/src/daemon/session-slash.ts +4 -4
  60. package/src/daemon/session-surfaces.ts +42 -2
  61. package/src/runtime/auth/token-service.ts +95 -45
  62. package/src/runtime/channel-retry-sweep.ts +2 -2
  63. package/src/runtime/http-server.ts +8 -7
  64. package/src/runtime/http-types.ts +1 -1
  65. package/src/runtime/routes/conversation-routes.ts +1 -1
  66. package/src/runtime/routes/guardian-bootstrap-routes.ts +3 -2
  67. package/src/runtime/routes/guardian-expiry-sweep.ts +5 -5
  68. package/src/runtime/routes/pairing-routes.ts +4 -1
  69. package/src/sequence/reply-matcher.ts +14 -4
  70. package/src/skills/frontmatter.ts +9 -6
  71. package/src/tools/ui-surface/definitions.ts +3 -1
  72. package/src/util/platform.ts +0 -12
  73. package/docs/architecture/http-token-refresh.md +0 -274
@@ -51,6 +51,7 @@ mock.module("../messaging/providers/sms/client.js", () => ({
51
51
  }));
52
52
 
53
53
  mock.module("../config/env.js", () => ({
54
+ isHttpAuthDisabled: () => true,
54
55
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
55
56
  }));
56
57
 
@@ -1,31 +1,33 @@
1
- import { mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
4
 
5
- import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
5
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
6
+
7
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
6
8
 
7
9
  // ---------------------------------------------------------------------------
8
10
  // Test isolation: in-memory SQLite via temp directory
9
11
  // ---------------------------------------------------------------------------
10
12
 
11
- const testDir = mkdtempSync(join(tmpdir(), 'conv-attn-telegram-test-'));
13
+ const testDir = mkdtempSync(join(tmpdir(), "conv-attn-telegram-test-"));
12
14
 
13
- mock.module('../util/platform.js', () => ({
15
+ mock.module("../util/platform.js", () => ({
14
16
  getRootDir: () => testDir,
15
17
  getDataDir: () => testDir,
16
- isMacOS: () => process.platform === 'darwin',
17
- isLinux: () => process.platform === 'linux',
18
- isWindows: () => process.platform === 'win32',
19
- getSocketPath: () => join(testDir, 'test.sock'),
20
- getPidPath: () => join(testDir, 'test.pid'),
21
- getDbPath: () => join(testDir, 'test.db'),
22
- getLogPath: () => join(testDir, 'test.log'),
18
+ isMacOS: () => process.platform === "darwin",
19
+ isLinux: () => process.platform === "linux",
20
+ isWindows: () => process.platform === "win32",
21
+ getSocketPath: () => join(testDir, "test.sock"),
22
+ getPidPath: () => join(testDir, "test.pid"),
23
+ getDbPath: () => join(testDir, "test.db"),
24
+ getLogPath: () => join(testDir, "test.log"),
23
25
  ensureDataDir: () => {},
24
26
  migrateToDataLayout: () => {},
25
27
  migrateToWorkspaceLayout: () => {},
26
28
  }));
27
29
 
28
- mock.module('../util/logger.js', () => ({
30
+ mock.module("../util/logger.js", () => ({
29
31
  getLogger: () =>
30
32
  new Proxy({} as Record<string, unknown>, {
31
33
  get: () => () => {},
@@ -35,29 +37,29 @@ mock.module('../util/logger.js', () => ({
35
37
  }));
36
38
 
37
39
  // Mock security check to always pass
38
- mock.module('../security/secret-ingress.js', () => ({
40
+ mock.module("../security/secret-ingress.js", () => ({
39
41
  checkIngressForSecrets: () => ({ blocked: false }),
40
42
  }));
41
43
 
42
44
  // Mock render to return the raw content as text
43
- mock.module('../daemon/handlers.js', () => ({
45
+ mock.module("../daemon/handlers.js", () => ({
44
46
  renderHistoryContent: (content: unknown) => ({
45
- text: typeof content === 'string' ? content : JSON.stringify(content),
47
+ text: typeof content === "string" ? content : JSON.stringify(content),
46
48
  }),
47
49
  }));
48
50
 
49
51
  // Mock ingress member store to return an active member for all lookups
50
- mock.module('../memory/ingress-member-store.js', () => ({
52
+ mock.module("../memory/ingress-member-store.js", () => ({
51
53
  findMember: () => ({
52
- id: 'member-test-default',
53
- assistantId: 'self',
54
- sourceChannel: 'telegram',
55
- externalUserId: 'telegram-user-default',
54
+ id: "member-test-default",
55
+ assistantId: "self",
56
+ sourceChannel: "telegram",
57
+ externalUserId: "telegram-user-default",
56
58
  externalChatId: null,
57
59
  displayName: null,
58
60
  username: null,
59
- status: 'active',
60
- policy: 'allow',
61
+ status: "active",
62
+ policy: "allow",
61
63
  inviteId: null,
62
64
  createdBySessionId: null,
63
65
  revokedReason: null,
@@ -70,17 +72,17 @@ mock.module('../memory/ingress-member-store.js', () => ({
70
72
  upsertMember: () => {},
71
73
  }));
72
74
 
73
- import { eq } from 'drizzle-orm';
75
+ import { eq } from "drizzle-orm";
74
76
 
75
- import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
76
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
77
+ import * as channelDeliveryStore from "../memory/channel-delivery-store.js";
78
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
77
79
  import {
78
80
  attachments,
79
81
  conversationAssistantAttentionState,
80
82
  conversationAttentionEvents,
81
- } from '../memory/schema.js';
82
- import * as pendingInteractions from '../runtime/pending-interactions.js';
83
- import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
83
+ } from "../memory/schema.js";
84
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
85
+ import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
84
86
 
85
87
  initializeDb();
86
88
 
@@ -101,42 +103,42 @@ function resetTables(): void {
101
103
  const db = getDb();
102
104
  db.delete(conversationAttentionEvents).run();
103
105
  db.delete(conversationAssistantAttentionState).run();
104
- db.run('DELETE FROM channel_guardian_approval_requests');
105
- db.run('DELETE FROM channel_guardian_verification_challenges');
106
- db.run('DELETE FROM channel_guardian_bindings');
107
- db.run('DELETE FROM conversation_keys');
108
- db.run('DELETE FROM message_runs');
109
- db.run('DELETE FROM channel_inbound_events');
110
- db.run('DELETE FROM messages');
111
- db.run('DELETE FROM conversations');
106
+ db.run("DELETE FROM channel_guardian_approval_requests");
107
+ db.run("DELETE FROM channel_guardian_verification_challenges");
108
+ db.run("DELETE FROM channel_guardian_bindings");
109
+ db.run("DELETE FROM conversation_keys");
110
+ db.run("DELETE FROM message_runs");
111
+ db.run("DELETE FROM channel_inbound_events");
112
+ db.run("DELETE FROM messages");
113
+ db.run("DELETE FROM conversations");
112
114
  channelDeliveryStore.resetAllRunDeliveryClaims();
113
115
  pendingInteractions.clear();
114
116
  }
115
117
 
116
- const TEST_BEARER_TOKEN = 'token';
118
+ const TEST_BEARER_TOKEN = "token";
117
119
 
118
120
  function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
119
121
  const body = {
120
- sourceChannel: 'telegram',
121
- interface: 'telegram',
122
- conversationExternalId: 'chat-123',
123
- actorExternalId: 'telegram-user-default',
122
+ sourceChannel: "telegram",
123
+ interface: "telegram",
124
+ conversationExternalId: "chat-123",
125
+ actorExternalId: "telegram-user-default",
124
126
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
125
- content: 'hello',
126
- replyCallbackUrl: 'https://gateway.test/deliver',
127
+ content: "hello",
128
+ replyCallbackUrl: "https://gateway.test/deliver",
127
129
  ...overrides,
128
130
  };
129
- return new Request('http://localhost/channels/inbound', {
130
- method: 'POST',
131
+ return new Request("http://localhost/channels/inbound", {
132
+ method: "POST",
131
133
  headers: {
132
- 'Content-Type': 'application/json',
133
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
134
+ "Content-Type": "application/json",
135
+ "X-Gateway-Origin": TEST_BEARER_TOKEN,
134
136
  },
135
137
  body: JSON.stringify(body),
136
138
  });
137
139
  }
138
140
 
139
- const noopProcessMessage = mock(async () => ({ messageId: 'msg-1' }));
141
+ const noopProcessMessage = mock(async () => ({ messageId: "msg-1" }));
140
142
 
141
143
  function getAttentionEvents(conversationId: string) {
142
144
  const db = getDb();
@@ -156,11 +158,15 @@ beforeEach(() => {
156
158
  // Telegram inbound messages record inferred seen signals
157
159
  // ═══════════════════════════════════════════════════════════════════════════
158
160
 
159
- describe('Telegram inbound message seen signals', () => {
160
- test('records inferred seen signal for non-duplicate text message', async () => {
161
- const req = makeInboundRequest({ content: 'Hello there!' });
161
+ describe("Telegram inbound message seen signals", () => {
162
+ test("records inferred seen signal for non-duplicate text message", async () => {
163
+ const req = makeInboundRequest({ content: "Hello there!" });
162
164
 
163
- const res = await handleChannelInbound(req, noopProcessMessage, TEST_BEARER_TOKEN);
165
+ const res = await handleChannelInbound(
166
+ req,
167
+ noopProcessMessage,
168
+ TEST_BEARER_TOKEN,
169
+ );
164
170
  const body = (await res.json()) as Record<string, unknown>;
165
171
 
166
172
  expect(body.accepted).toBe(true);
@@ -169,7 +175,7 @@ describe('Telegram inbound message seen signals', () => {
169
175
  // Find the conversation ID from inbound events
170
176
  const db = getDb();
171
177
  const inboundEvents = db.$client
172
- .prepare('SELECT conversation_id FROM channel_inbound_events')
178
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
173
179
  .all() as Array<{ conversation_id: string }>;
174
180
  expect(inboundEvents.length).toBeGreaterThan(0);
175
181
 
@@ -177,56 +183,64 @@ describe('Telegram inbound message seen signals', () => {
177
183
  const events = getAttentionEvents(conversationId);
178
184
 
179
185
  expect(events.length).toBe(1);
180
- expect(events[0].signalType).toBe('telegram_inbound_message');
181
- expect(events[0].confidence).toBe('inferred');
182
- expect(events[0].sourceChannel).toBe('telegram');
183
- expect(events[0].source).toBe('inbound-message-handler');
186
+ expect(events[0].signalType).toBe("telegram_inbound_message");
187
+ expect(events[0].confidence).toBe("inferred");
188
+ expect(events[0].sourceChannel).toBe("telegram");
189
+ expect(events[0].source).toBe("inbound-message-handler");
184
190
  expect(events[0].evidenceText).toBe("User sent message: 'Hello there!'");
185
191
  });
186
192
 
187
- test('records inferred seen signal for media attachment without text', async () => {
193
+ test("records inferred seen signal for media attachment without text", async () => {
188
194
  // Insert a fake attachment directly so the handler's validation passes
189
195
  const db = getDb();
190
196
  const attachmentId = `att-${Date.now()}`;
191
197
  db.insert(attachments)
192
198
  .values({
193
199
  id: attachmentId,
194
- originalFilename: 'photo.jpg',
195
- mimeType: 'image/jpeg',
200
+ originalFilename: "photo.jpg",
201
+ mimeType: "image/jpeg",
196
202
  sizeBytes: 1024,
197
- kind: 'base64',
198
- dataBase64: 'dGVzdA==',
203
+ kind: "base64",
204
+ dataBase64: "dGVzdA==",
199
205
  createdAt: Date.now(),
200
206
  })
201
207
  .run();
202
208
 
203
209
  const req = makeInboundRequest({
204
- content: '',
210
+ content: "",
205
211
  attachmentIds: [attachmentId],
206
212
  });
207
213
 
208
- const res = await handleChannelInbound(req, noopProcessMessage, TEST_BEARER_TOKEN);
214
+ const res = await handleChannelInbound(
215
+ req,
216
+ noopProcessMessage,
217
+ TEST_BEARER_TOKEN,
218
+ );
209
219
  const body = (await res.json()) as Record<string, unknown>;
210
220
 
211
221
  expect(body.accepted).toBe(true);
212
222
  expect(body.duplicate).toBe(false);
213
223
 
214
224
  const inboundEvents2 = db.$client
215
- .prepare('SELECT conversation_id FROM channel_inbound_events')
225
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
216
226
  .all() as Array<{ conversation_id: string }>;
217
227
  const conversationId = inboundEvents2[0].conversation_id;
218
228
  const events = getAttentionEvents(conversationId);
219
229
 
220
230
  expect(events.length).toBe(1);
221
- expect(events[0].signalType).toBe('telegram_inbound_message');
222
- expect(events[0].evidenceText).toBe('User sent media attachment');
231
+ expect(events[0].signalType).toBe("telegram_inbound_message");
232
+ expect(events[0].evidenceText).toBe("User sent media attachment");
223
233
  });
224
234
 
225
- test('evidence text is correctly truncated for long messages', async () => {
226
- const longMessage = 'A'.repeat(120);
235
+ test("evidence text is correctly truncated for long messages", async () => {
236
+ const longMessage = "A".repeat(120);
227
237
  const req = makeInboundRequest({ content: longMessage });
228
238
 
229
- const res = await handleChannelInbound(req, noopProcessMessage, TEST_BEARER_TOKEN);
239
+ const res = await handleChannelInbound(
240
+ req,
241
+ noopProcessMessage,
242
+ TEST_BEARER_TOKEN,
243
+ );
230
244
  const body = (await res.json()) as Record<string, unknown>;
231
245
 
232
246
  expect(body.accepted).toBe(true);
@@ -234,15 +248,17 @@ describe('Telegram inbound message seen signals', () => {
234
248
 
235
249
  const db = getDb();
236
250
  const inboundEvents = db.$client
237
- .prepare('SELECT conversation_id FROM channel_inbound_events')
251
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
238
252
  .all() as Array<{ conversation_id: string }>;
239
253
  const conversationId = inboundEvents[0].conversation_id;
240
254
  const events = getAttentionEvents(conversationId);
241
255
 
242
256
  expect(events.length).toBe(1);
243
257
  // 80 chars of 'A' + '...'
244
- const expectedPreview = 'A'.repeat(80) + '...';
245
- expect(events[0].evidenceText).toBe(`User sent message: '${expectedPreview}'`);
258
+ const expectedPreview = "A".repeat(80) + "...";
259
+ expect(events[0].evidenceText).toBe(
260
+ `User sent message: '${expectedPreview}'`,
261
+ );
246
262
  });
247
263
  });
248
264
 
@@ -250,42 +266,51 @@ describe('Telegram inbound message seen signals', () => {
250
266
  // Telegram callbacks record inferred seen signals
251
267
  // ═══════════════════════════════════════════════════════════════════════════
252
268
 
253
- describe('Telegram callback seen signals', () => {
254
- test('records inferred seen signal for handled callback', async () => {
269
+ describe("Telegram callback seen signals", () => {
270
+ test("records inferred seen signal for handled callback", async () => {
255
271
  // First, send a regular message to establish the conversation
256
- const initReq = makeInboundRequest({ content: 'init' });
272
+ const initReq = makeInboundRequest({ content: "init" });
257
273
  await handleChannelInbound(initReq, noopProcessMessage, TEST_BEARER_TOKEN);
258
274
 
259
275
  const db = getDb();
260
276
  const inboundEvents = db.$client
261
- .prepare('SELECT conversation_id FROM channel_inbound_events')
277
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
262
278
  .all() as Array<{ conversation_id: string }>;
263
279
  const conversationId = inboundEvents[0].conversation_id;
264
280
 
265
281
  // Register a pending interaction so the approval interception handles it
266
282
  const handleConfirmationResponse = mock(() => {});
267
- const mockSession = { handleConfirmationResponse } as unknown as import('../daemon/session.js').Session;
268
- pendingInteractions.register('req-cb-test', {
283
+ const mockSession = {
284
+ handleConfirmationResponse,
285
+ } as unknown as import("../daemon/session.js").Session;
286
+ pendingInteractions.register("req-cb-test", {
269
287
  session: mockSession,
270
288
  conversationId,
271
- kind: 'confirmation',
289
+ kind: "confirmation",
272
290
  confirmationDetails: {
273
- toolName: 'shell',
274
- input: { command: 'echo hello' },
275
- riskLevel: 'high',
276
- allowlistOptions: [{ label: 'echo hello', description: 'echo hello', pattern: 'echo hello' }],
277
- scopeOptions: [{ label: 'everywhere', scope: 'everywhere' }],
291
+ toolName: "shell",
292
+ input: { command: "echo hello" },
293
+ riskLevel: "high",
294
+ allowlistOptions: [
295
+ {
296
+ label: "echo hello",
297
+ description: "echo hello",
298
+ pattern: "echo hello",
299
+ },
300
+ ],
301
+ scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
278
302
  },
279
303
  });
280
304
 
281
305
  // Create a guardian binding so approval can be handled
282
- const { createBinding } = await import('../memory/channel-guardian-store.js');
306
+ const { createBinding } =
307
+ await import("../memory/channel-guardian-store.js");
283
308
  createBinding({
284
- assistantId: 'self',
285
- channel: 'telegram',
286
- guardianExternalUserId: 'telegram-user-default',
287
- guardianDeliveryChatId: 'chat-123',
288
- guardianPrincipalId: 'telegram-user-default',
309
+ assistantId: "self",
310
+ channel: "telegram",
311
+ guardianExternalUserId: "telegram-user-default",
312
+ guardianDeliveryChatId: "chat-123",
313
+ guardianPrincipalId: "telegram-user-default",
289
314
  });
290
315
 
291
316
  // Clear attention events from the init message
@@ -293,11 +318,15 @@ describe('Telegram callback seen signals', () => {
293
318
 
294
319
  // Send callback data that matches the pending approval
295
320
  const cbReq = makeInboundRequest({
296
- content: 'approve',
297
- callbackData: 'apr:req-cb-test:approve_once',
321
+ content: "approve",
322
+ callbackData: "apr:req-cb-test:approve_once",
298
323
  });
299
324
 
300
- const res = await handleChannelInbound(cbReq, noopProcessMessage, TEST_BEARER_TOKEN);
325
+ const res = await handleChannelInbound(
326
+ cbReq,
327
+ noopProcessMessage,
328
+ TEST_BEARER_TOKEN,
329
+ );
301
330
  const body = (await res.json()) as Record<string, unknown>;
302
331
 
303
332
  expect(body.accepted).toBe(true);
@@ -305,11 +334,13 @@ describe('Telegram callback seen signals', () => {
305
334
 
306
335
  const events = getAttentionEvents(conversationId);
307
336
  expect(events.length).toBe(1);
308
- expect(events[0].signalType).toBe('telegram_callback');
309
- expect(events[0].confidence).toBe('inferred');
310
- expect(events[0].sourceChannel).toBe('telegram');
311
- expect(events[0].source).toBe('inbound-message-handler');
312
- expect(events[0].evidenceText).toContain("User tapped callback: 'apr:req-cb-test:approve_once'");
337
+ expect(events[0].signalType).toBe("telegram_callback");
338
+ expect(events[0].confidence).toBe("inferred");
339
+ expect(events[0].sourceChannel).toBe("telegram");
340
+ expect(events[0].source).toBe("inbound-message-handler");
341
+ expect(events[0].evidenceText).toContain(
342
+ "User tapped callback: 'apr:req-cb-test:approve_once'",
343
+ );
313
344
  });
314
345
  });
315
346
 
@@ -317,32 +348,40 @@ describe('Telegram callback seen signals', () => {
317
348
  // Duplicate events do NOT produce duplicate seen signals
318
349
  // ═══════════════════════════════════════════════════════════════════════════
319
350
 
320
- describe('duplicate event deduplication', () => {
321
- test('duplicate Telegram message does not record a second seen signal', async () => {
351
+ describe("duplicate event deduplication", () => {
352
+ test("duplicate Telegram message does not record a second seen signal", async () => {
322
353
  const fixedMessageId = `msg-dedup-${Date.now()}`;
323
354
 
324
355
  // First (non-duplicate) message
325
356
  const req1 = makeInboundRequest({
326
- content: 'first message',
357
+ content: "first message",
327
358
  externalMessageId: fixedMessageId,
328
359
  });
329
- const res1 = await handleChannelInbound(req1, noopProcessMessage, TEST_BEARER_TOKEN);
360
+ const res1 = await handleChannelInbound(
361
+ req1,
362
+ noopProcessMessage,
363
+ TEST_BEARER_TOKEN,
364
+ );
330
365
  const body1 = (await res1.json()) as Record<string, unknown>;
331
366
  expect(body1.duplicate).toBe(false);
332
367
 
333
368
  // Same externalMessageId => duplicate
334
369
  const req2 = makeInboundRequest({
335
- content: 'first message',
370
+ content: "first message",
336
371
  externalMessageId: fixedMessageId,
337
372
  });
338
- const res2 = await handleChannelInbound(req2, noopProcessMessage, TEST_BEARER_TOKEN);
373
+ const res2 = await handleChannelInbound(
374
+ req2,
375
+ noopProcessMessage,
376
+ TEST_BEARER_TOKEN,
377
+ );
339
378
  const body2 = (await res2.json()) as Record<string, unknown>;
340
379
  expect(body2.duplicate).toBe(true);
341
380
 
342
381
  // Only one attention event should exist
343
382
  const db = getDb();
344
383
  const inboundEvents = db.$client
345
- .prepare('SELECT conversation_id FROM channel_inbound_events')
384
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
346
385
  .all() as Array<{ conversation_id: string }>;
347
386
  const conversationId = inboundEvents[0].conversation_id;
348
387
  const events = getAttentionEvents(conversationId);
@@ -355,16 +394,20 @@ describe('duplicate event deduplication', () => {
355
394
  // Non-Telegram channels do NOT record Telegram seen signals
356
395
  // ═══════════════════════════════════════════════════════════════════════════
357
396
 
358
- describe('non-Telegram channel filtering', () => {
359
- test('SMS inbound message does not record a Telegram seen signal', async () => {
397
+ describe("non-Telegram channel filtering", () => {
398
+ test("SMS inbound message does not record a Telegram seen signal", async () => {
360
399
  // Override ingress member store for SMS channel
361
400
  const req = makeInboundRequest({
362
- sourceChannel: 'sms',
363
- interface: 'sms',
364
- content: 'sms message',
401
+ sourceChannel: "sms",
402
+ interface: "sms",
403
+ content: "sms message",
365
404
  });
366
405
 
367
- const res = await handleChannelInbound(req, noopProcessMessage, TEST_BEARER_TOKEN);
406
+ const res = await handleChannelInbound(
407
+ req,
408
+ noopProcessMessage,
409
+ TEST_BEARER_TOKEN,
410
+ );
368
411
  const body = (await res.json()) as Record<string, unknown>;
369
412
 
370
413
  expect(body.accepted).toBe(true);