@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
@@ -1,73 +1,84 @@
1
- import * as net from 'node:net';
1
+ import * as net from "node:net";
2
2
 
3
- import { beforeEach, describe, expect, mock, test } from 'bun:test';
3
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
5
- import type { HandlerContext } from '../daemon/handlers.js';
6
- import type { ConfirmationResponse, UserMessage } from '../daemon/ipc-contract.js';
7
- import type { ServerMessage } from '../daemon/ipc-protocol.js';
8
- import { DebouncerMap } from '../util/debounce.js';
5
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
6
+
7
+ import type { HandlerContext } from "../daemon/handlers.js";
8
+ import type {
9
+ ConfirmationResponse,
10
+ UserMessage,
11
+ } from "../daemon/ipc-contract.js";
12
+ import type { ServerMessage } from "../daemon/ipc-protocol.js";
13
+ import { DebouncerMap } from "../util/debounce.js";
9
14
 
10
15
  const routeGuardianReplyMock = mock(async () => ({
11
16
  consumed: false,
12
17
  decisionApplied: false,
13
- type: 'not_consumed' as const,
18
+ type: "not_consumed" as const,
14
19
  })) as any;
15
20
  const createCanonicalGuardianRequestMock = mock(() => ({
16
- id: 'canonical-id',
21
+ id: "canonical-id",
17
22
  }));
18
- const generateCanonicalRequestCodeMock = mock(() => 'ABC123');
19
- const listPendingByDestinationMock = mock(() => [] as Array<{ id: string; kind?: string }>);
23
+ const generateCanonicalRequestCodeMock = mock(() => "ABC123");
24
+ const listPendingByDestinationMock = mock(
25
+ () => [] as Array<{ id: string; kind?: string }>,
26
+ );
20
27
  const listCanonicalMock = mock(() => [] as Array<{ id: string }>);
21
- const resolveCanonicalGuardianRequestMock = mock(() => null as { id: string } | null);
28
+ const resolveCanonicalGuardianRequestMock = mock(
29
+ () => null as { id: string } | null,
30
+ );
22
31
  const getByConversationMock = mock(
23
- () => [] as Array<{
24
- requestId: string;
25
- kind: 'confirmation' | 'secret';
26
- session?: unknown;
27
- }>,
32
+ () =>
33
+ [] as Array<{
34
+ requestId: string;
35
+ kind: "confirmation" | "secret";
36
+ session?: unknown;
37
+ }>,
28
38
  );
29
39
  const registerMock = mock(() => {});
30
40
  const resolveMock = mock(() => undefined as unknown);
31
- const addMessageMock = mock(async () => ({ id: 'persisted-message-id' }));
41
+ const addMessageMock = mock(async () => ({ id: "persisted-message-id" }));
32
42
  const getConfigMock = mock(() => ({
33
43
  daemon: { standaloneRecording: false },
34
44
  secretDetection: { customPatterns: [], entropyThreshold: 3.5 },
35
45
  }));
36
46
 
37
- mock.module('../runtime/guardian-reply-router.js', () => ({
47
+ mock.module("../runtime/guardian-reply-router.js", () => ({
38
48
  routeGuardianReply: routeGuardianReplyMock,
39
49
  }));
40
50
 
41
- mock.module('../memory/canonical-guardian-store.js', () => ({
51
+ mock.module("../memory/canonical-guardian-store.js", () => ({
42
52
  createCanonicalGuardianRequest: createCanonicalGuardianRequestMock,
43
53
  generateCanonicalRequestCode: generateCanonicalRequestCodeMock,
44
- listPendingCanonicalGuardianRequestsByDestinationConversation: listPendingByDestinationMock,
54
+ listPendingCanonicalGuardianRequestsByDestinationConversation:
55
+ listPendingByDestinationMock,
45
56
  listCanonicalGuardianRequests: listCanonicalMock,
46
57
  resolveCanonicalGuardianRequest: resolveCanonicalGuardianRequestMock,
47
58
  }));
48
59
 
49
- mock.module('../runtime/pending-interactions.js', () => ({
60
+ mock.module("../runtime/pending-interactions.js", () => ({
50
61
  register: registerMock,
51
62
  getByConversation: getByConversationMock,
52
63
  resolve: resolveMock,
53
64
  }));
54
65
 
55
- mock.module('../memory/conversation-store.js', () => ({
66
+ mock.module("../memory/conversation-store.js", () => ({
56
67
  addMessage: addMessageMock,
57
68
  }));
58
69
 
59
- mock.module('../config/loader.js', () => ({
70
+ mock.module("../config/loader.js", () => ({
60
71
  getConfig: getConfigMock,
61
72
  }));
62
73
 
63
- mock.module('../daemon/approval-generators.js', () => ({
74
+ mock.module("../daemon/approval-generators.js", () => ({
64
75
  createApprovalConversationGenerator: () => async () => ({
65
- disposition: 'keep_pending',
66
- replyText: 'pending',
76
+ disposition: "keep_pending",
77
+ replyText: "pending",
67
78
  }),
68
79
  }));
69
80
 
70
- mock.module('../util/logger.js', () => ({
81
+ mock.module("../util/logger.js", () => ({
71
82
  getLogger: () => ({
72
83
  info: () => {},
73
84
  warn: () => {},
@@ -82,7 +93,10 @@ mock.module('../util/logger.js', () => ({
82
93
  }),
83
94
  }));
84
95
 
85
- import { handleConfirmationResponse, handleUserMessage } from '../daemon/handlers/sessions.js';
96
+ import {
97
+ handleConfirmationResponse,
98
+ handleUserMessage,
99
+ } from "../daemon/handlers/sessions.js";
86
100
 
87
101
  interface TestSession {
88
102
  messages: Array<{ role: string; content: unknown[] }>;
@@ -93,20 +107,30 @@ interface TestSession {
93
107
  hasAnyPendingConfirmation: () => boolean;
94
108
  getQueueDepth: () => number;
95
109
  denyAllPendingConfirmations: () => void;
96
- enqueueMessage: (...args: unknown[]) => { queued: boolean; rejected?: boolean; requestId: string };
110
+ enqueueMessage: (...args: unknown[]) => {
111
+ queued: boolean;
112
+ rejected?: boolean;
113
+ requestId: string;
114
+ };
97
115
  traceEmitter: { emit: (...args: unknown[]) => void };
98
116
  setTurnChannelContext: (ctx: unknown) => void;
99
117
  setTurnInterfaceContext: (ctx: unknown) => void;
100
118
  setAssistantId: (assistantId: string) => void;
101
119
  setGuardianContext: (ctx: unknown) => void;
102
120
  setCommandIntent: (intent: unknown) => void;
103
- updateClient: (sendToClient: (msg: ServerMessage) => void, hasNoClient?: boolean) => void;
121
+ updateClient: (
122
+ sendToClient: (msg: ServerMessage) => void,
123
+ hasNoClient?: boolean,
124
+ ) => void;
104
125
  emitActivityState: (...args: unknown[]) => void;
105
126
  emitConfirmationStateChanged: (...args: unknown[]) => void;
106
127
  processMessage: (...args: unknown[]) => Promise<string>;
107
128
  }
108
129
 
109
- function createContext(session: TestSession): { ctx: HandlerContext; sent: ServerMessage[] } {
130
+ function createContext(session: TestSession): {
131
+ ctx: HandlerContext;
132
+ sent: ServerMessage[];
133
+ } {
110
134
  const sent: ServerMessage[] = [];
111
135
  const ctx: HandlerContext = {
112
136
  sessions: new Map(),
@@ -120,7 +144,9 @@ function createContext(session: TestSession): { ctx: HandlerContext; sent: Serve
120
144
  suppressConfigReload: false,
121
145
  setSuppressConfigReload: () => {},
122
146
  updateConfigFingerprint: () => {},
123
- send: (_socket, msg) => { sent.push(msg); },
147
+ send: (_socket, msg) => {
148
+ sent.push(msg);
149
+ },
124
150
  broadcast: () => {},
125
151
  clearAllSessions: () => 0,
126
152
  getOrCreateSession: async () => session as any,
@@ -131,11 +157,11 @@ function createContext(session: TestSession): { ctx: HandlerContext; sent: Serve
131
157
 
132
158
  function makeMessage(content: string): UserMessage {
133
159
  return {
134
- type: 'user_message',
135
- sessionId: 'conv-1',
160
+ type: "user_message",
161
+ sessionId: "conv-1",
136
162
  content,
137
- channel: 'vellum',
138
- interface: 'macos',
163
+ channel: "vellum",
164
+ interface: "macos",
139
165
  };
140
166
  }
141
167
 
@@ -149,7 +175,7 @@ function makeSession(overrides: Partial<TestSession> = {}): TestSession {
149
175
  hasAnyPendingConfirmation: () => true,
150
176
  getQueueDepth: () => 0,
151
177
  denyAllPendingConfirmations: mock(() => {}),
152
- enqueueMessage: mock(() => ({ queued: true, requestId: 'queued-id' })),
178
+ enqueueMessage: mock(() => ({ queued: true, requestId: "queued-id" })),
153
179
  traceEmitter: { emit: () => {} },
154
180
  setTurnChannelContext: () => {},
155
181
  setTurnInterfaceContext: () => {},
@@ -159,12 +185,12 @@ function makeSession(overrides: Partial<TestSession> = {}): TestSession {
159
185
  updateClient: () => {},
160
186
  emitActivityState: () => {},
161
187
  emitConfirmationStateChanged: () => {},
162
- processMessage: async () => 'msg-id',
188
+ processMessage: async () => "msg-id",
163
189
  ...overrides,
164
190
  };
165
191
  }
166
192
 
167
- describe('handleUserMessage pending-confirmation reply interception', () => {
193
+ describe("handleUserMessage pending-confirmation reply interception", () => {
168
194
  beforeEach(() => {
169
195
  routeGuardianReplyMock.mockClear();
170
196
  createCanonicalGuardianRequestMock.mockClear();
@@ -179,79 +205,90 @@ describe('handleUserMessage pending-confirmation reply interception', () => {
179
205
  getConfigMock.mockClear();
180
206
  });
181
207
 
182
- test('consumes decision replies before auto-deny', async () => {
183
- listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
184
- listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
208
+ test("consumes decision replies before auto-deny", async () => {
209
+ listPendingByDestinationMock.mockReturnValue([
210
+ { id: "req-1", kind: "tool_approval" },
211
+ ]);
212
+ listCanonicalMock.mockReturnValue([{ id: "req-1" }]);
185
213
  routeGuardianReplyMock.mockResolvedValue({
186
214
  consumed: true,
187
215
  decisionApplied: true,
188
- type: 'canonical_decision_applied',
189
- requestId: 'req-1',
216
+ type: "canonical_decision_applied",
217
+ requestId: "req-1",
190
218
  });
191
219
 
192
220
  const session = makeSession();
193
221
  const { ctx, sent } = createContext(session);
194
222
 
195
- await handleUserMessage(makeMessage('go for it'), {} as net.Socket, ctx);
223
+ await handleUserMessage(makeMessage("go for it"), {} as net.Socket, ctx);
196
224
 
197
225
  expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
198
- const routeCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
199
- expect(routeCall.messageText).toBe('go for it');
200
- expect(typeof routeCall.approvalConversationGenerator).toBe('function');
201
- expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(0);
226
+ const routeCall = (routeGuardianReplyMock as any).mock
227
+ .calls[0][0] as Record<string, unknown>;
228
+ expect(routeCall.messageText).toBe("go for it");
229
+ expect(typeof routeCall.approvalConversationGenerator).toBe("function");
230
+ expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(
231
+ 0,
232
+ );
202
233
  expect((session.enqueueMessage as any).mock.calls.length).toBe(0);
203
234
  expect(session.messages).toHaveLength(2);
204
- expect(session.messages[0]?.role).toBe('user');
205
- expect(session.messages[1]?.role).toBe('assistant');
235
+ expect(session.messages[0]?.role).toBe("user");
236
+ expect(session.messages[1]?.role).toBe("assistant");
206
237
  expect(addMessageMock).toHaveBeenCalledTimes(2);
207
238
  expect(addMessageMock).toHaveBeenCalledWith(
208
- 'conv-1',
209
- 'user',
239
+ "conv-1",
240
+ "user",
210
241
  expect.any(String),
211
242
  expect.objectContaining({
212
- userMessageChannel: 'vellum',
213
- assistantMessageChannel: 'vellum',
214
- userMessageInterface: 'macos',
215
- assistantMessageInterface: 'macos',
216
- provenanceTrustClass: 'guardian',
243
+ userMessageChannel: "vellum",
244
+ assistantMessageChannel: "vellum",
245
+ userMessageInterface: "macos",
246
+ assistantMessageInterface: "macos",
247
+ provenanceTrustClass: "guardian",
217
248
  }),
218
249
  );
219
250
  expect(addMessageMock).toHaveBeenCalledWith(
220
- 'conv-1',
221
- 'assistant',
222
- expect.stringContaining('Decision applied.'),
251
+ "conv-1",
252
+ "assistant",
253
+ expect.stringContaining("Decision applied."),
223
254
  expect.objectContaining({
224
- userMessageChannel: 'vellum',
225
- assistantMessageChannel: 'vellum',
226
- userMessageInterface: 'macos',
227
- assistantMessageInterface: 'macos',
228
- provenanceTrustClass: 'guardian',
255
+ userMessageChannel: "vellum",
256
+ assistantMessageChannel: "vellum",
257
+ userMessageInterface: "macos",
258
+ assistantMessageInterface: "macos",
259
+ provenanceTrustClass: "guardian",
229
260
  }),
230
261
  );
231
262
  expect(sent.map((msg) => msg.type)).toEqual([
232
- 'message_queued',
233
- 'message_dequeued',
234
- 'assistant_text_delta',
235
- 'message_request_complete',
263
+ "message_queued",
264
+ "message_dequeued",
265
+ "assistant_text_delta",
266
+ "message_request_complete",
236
267
  ]);
237
268
  const assistantDelta = sent.find(
238
- (msg): msg is Extract<ServerMessage, { type: 'assistant_text_delta' }> => msg.type === 'assistant_text_delta',
269
+ (msg): msg is Extract<ServerMessage, { type: "assistant_text_delta" }> =>
270
+ msg.type === "assistant_text_delta",
239
271
  );
240
- expect(assistantDelta?.text).toBe('Decision applied.');
272
+ expect(assistantDelta?.text).toBe("Decision applied.");
241
273
  const requestComplete = sent.find(
242
- (msg): msg is Extract<ServerMessage, { type: 'message_request_complete' }> => msg.type === 'message_request_complete',
274
+ (
275
+ msg,
276
+ ): msg is Extract<ServerMessage, { type: "message_request_complete" }> =>
277
+ msg.type === "message_request_complete",
243
278
  );
244
279
  expect(requestComplete?.runStillActive).toBe(false);
245
280
  });
246
281
 
247
- test('consumes decision replies even when queue depth is non-zero', async () => {
248
- listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
249
- listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
282
+ test("consumes decision replies even when queue depth is non-zero", async () => {
283
+ listPendingByDestinationMock.mockReturnValue([
284
+ { id: "req-1", kind: "tool_approval" },
285
+ ]);
286
+ listCanonicalMock.mockReturnValue([{ id: "req-1" }]);
250
287
  routeGuardianReplyMock.mockResolvedValue({
251
288
  consumed: true,
252
289
  decisionApplied: true,
253
- type: 'canonical_decision_applied',
254
- requestId: 'req-1',
290
+ type: "canonical_decision_applied",
291
+ requestId: "req-1",
255
292
  });
256
293
 
257
294
  const session = makeSession({
@@ -259,274 +296,311 @@ describe('handleUserMessage pending-confirmation reply interception', () => {
259
296
  });
260
297
  const { ctx } = createContext(session);
261
298
 
262
- await handleUserMessage(makeMessage('approve'), {} as net.Socket, ctx);
299
+ await handleUserMessage(makeMessage("approve"), {} as net.Socket, ctx);
263
300
 
264
301
  expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
265
- expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(0);
302
+ expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(
303
+ 0,
304
+ );
266
305
  expect((session.enqueueMessage as any).mock.calls.length).toBe(0);
267
306
  });
268
307
 
269
- test('does not mutate in-memory history while processing', async () => {
270
- listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
271
- listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
308
+ test("does not mutate in-memory history while processing", async () => {
309
+ listPendingByDestinationMock.mockReturnValue([
310
+ { id: "req-1", kind: "tool_approval" },
311
+ ]);
312
+ listCanonicalMock.mockReturnValue([{ id: "req-1" }]);
272
313
  routeGuardianReplyMock.mockResolvedValue({
273
314
  consumed: true,
274
315
  decisionApplied: true,
275
- type: 'canonical_decision_applied',
276
- requestId: 'req-1',
316
+ type: "canonical_decision_applied",
317
+ requestId: "req-1",
277
318
  });
278
319
 
279
320
  const session = makeSession({ isProcessing: () => true });
280
321
  const { ctx, sent } = createContext(session);
281
322
 
282
- await handleUserMessage(makeMessage('approve'), {} as net.Socket, ctx);
323
+ await handleUserMessage(makeMessage("approve"), {} as net.Socket, ctx);
283
324
 
284
325
  expect(addMessageMock).toHaveBeenCalledTimes(2);
285
326
  expect(session.messages).toHaveLength(0);
286
327
  // assistant_text_delta must NOT be sent when the session is processing —
287
328
  // it would contaminate the agent's in-flight streaming message on the client.
288
- expect(sent.some((msg) => msg.type === 'assistant_text_delta')).toBe(false);
329
+ expect(sent.some((msg) => msg.type === "assistant_text_delta")).toBe(false);
289
330
  expect(sent.map((msg) => msg.type)).toEqual([
290
- 'message_queued',
291
- 'message_dequeued',
292
- 'message_request_complete',
331
+ "message_queued",
332
+ "message_dequeued",
333
+ "message_request_complete",
293
334
  ]);
294
335
  const requestComplete = sent.find(
295
- (msg): msg is Extract<ServerMessage, { type: 'message_request_complete' }> => msg.type === 'message_request_complete',
336
+ (
337
+ msg,
338
+ ): msg is Extract<ServerMessage, { type: "message_request_complete" }> =>
339
+ msg.type === "message_request_complete",
296
340
  );
297
341
  expect(requestComplete?.runStillActive).toBe(true);
298
342
  });
299
343
 
300
- test('nl keep_pending falls back to existing auto-deny + queue behavior', async () => {
301
- listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
302
- listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
344
+ test("nl keep_pending falls back to existing auto-deny + queue behavior", async () => {
345
+ listPendingByDestinationMock.mockReturnValue([
346
+ { id: "req-1", kind: "tool_approval" },
347
+ ]);
348
+ listCanonicalMock.mockReturnValue([{ id: "req-1" }]);
303
349
  routeGuardianReplyMock.mockResolvedValue({
304
350
  consumed: true,
305
351
  decisionApplied: false,
306
- type: 'nl_keep_pending',
307
- requestId: 'req-1',
308
- replyText: 'Need clarification',
352
+ type: "nl_keep_pending",
353
+ requestId: "req-1",
354
+ replyText: "Need clarification",
309
355
  });
310
356
 
311
357
  const session = makeSession();
312
358
  const { ctx, sent } = createContext(session);
313
359
 
314
- await handleUserMessage(makeMessage('what does that do?'), {} as net.Socket, ctx);
360
+ await handleUserMessage(
361
+ makeMessage("what does that do?"),
362
+ {} as net.Socket,
363
+ ctx,
364
+ );
315
365
 
316
366
  expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
317
- expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(1);
367
+ expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(
368
+ 1,
369
+ );
318
370
  expect((session.enqueueMessage as any).mock.calls.length).toBe(1);
319
371
  expect(session.messages).toHaveLength(0);
320
372
  expect(addMessageMock).toHaveBeenCalledTimes(0);
321
- expect(sent.some((msg) => msg.type === 'message_queued')).toBe(true);
322
- expect(sent.some((msg) => msg.type === 'message_dequeued')).toBe(false);
373
+ expect(sent.some((msg) => msg.type === "message_queued")).toBe(true);
374
+ expect(sent.some((msg) => msg.type === "message_dequeued")).toBe(false);
323
375
  });
324
376
 
325
- test('routes only live pending confirmation request ids', async () => {
377
+ test("routes only live pending confirmation request ids", async () => {
326
378
  const session = makeSession({
327
- hasPendingConfirmation: (requestId: string) => requestId === 'req-live',
379
+ hasPendingConfirmation: (requestId: string) => requestId === "req-live",
328
380
  });
329
381
 
330
382
  getByConversationMock.mockReturnValue([
331
- { requestId: 'req-stale', kind: 'confirmation', session: {} },
332
- { requestId: 'req-live', kind: 'confirmation', session: session as unknown },
383
+ { requestId: "req-stale", kind: "confirmation", session: {} },
384
+ {
385
+ requestId: "req-live",
386
+ kind: "confirmation",
387
+ session: session as unknown,
388
+ },
333
389
  ]);
334
390
  listPendingByDestinationMock.mockReturnValue([
335
- { id: 'req-stale', kind: 'tool_approval' },
336
- { id: 'req-live', kind: 'tool_approval' },
391
+ { id: "req-stale", kind: "tool_approval" },
392
+ { id: "req-live", kind: "tool_approval" },
337
393
  ]);
338
394
  listCanonicalMock.mockReturnValue([
339
- { id: 'req-stale' },
340
- { id: 'req-live' },
395
+ { id: "req-stale" },
396
+ { id: "req-live" },
341
397
  ]);
342
398
  routeGuardianReplyMock.mockResolvedValue({
343
399
  consumed: false,
344
400
  decisionApplied: false,
345
- type: 'not_consumed',
401
+ type: "not_consumed",
346
402
  });
347
403
 
348
404
  const { ctx } = createContext(session);
349
- await handleUserMessage(makeMessage('allow'), {} as net.Socket, ctx);
405
+ await handleUserMessage(makeMessage("allow"), {} as net.Socket, ctx);
350
406
 
351
407
  expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
352
- const routeCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
353
- expect(routeCall.pendingRequestIds).toEqual(['req-live']);
408
+ const routeCall = (routeGuardianReplyMock as any).mock
409
+ .calls[0][0] as Record<string, unknown>;
410
+ expect(routeCall.pendingRequestIds).toEqual(["req-live"]);
354
411
  // Auto-deny clears matching confirmation entries from pending-interactions
355
412
  // so stale IDs are not reused as routing candidates. Only the live
356
413
  // session-scoped interaction should be resolved.
357
414
  expect(resolveMock).toHaveBeenCalledTimes(1);
358
- expect(resolveMock).toHaveBeenCalledWith('req-live');
415
+ expect(resolveMock).toHaveBeenCalledWith("req-live");
359
416
  expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledTimes(1);
360
417
  expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledWith(
361
- 'req-live',
362
- 'pending',
363
- { status: 'denied' },
418
+ "req-live",
419
+ "pending",
420
+ { status: "denied" },
364
421
  );
365
422
  });
366
423
 
367
- test('registers IPC confirmation events for NL approval routing', async () => {
424
+ test("registers IPC confirmation events for NL approval routing", async () => {
368
425
  const session = makeSession({
369
426
  hasAnyPendingConfirmation: () => false,
370
- enqueueMessage: mock(() => ({ queued: false, requestId: 'direct-id' })),
427
+ enqueueMessage: mock(() => ({ queued: false, requestId: "direct-id" })),
371
428
  processMessage: async (_content, _attachments, onEvent) => {
372
429
  (onEvent as (msg: ServerMessage) => void)({
373
- type: 'confirmation_request',
374
- requestId: 'req-confirm-1',
375
- toolName: 'call_start',
376
- input: { phone_number: '+18084436762' },
377
- riskLevel: 'high',
378
- executionTarget: 'host',
430
+ type: "confirmation_request",
431
+ requestId: "req-confirm-1",
432
+ toolName: "call_start",
433
+ input: { phone_number: "+18084436762" },
434
+ riskLevel: "high",
435
+ executionTarget: "host",
379
436
  allowlistOptions: [],
380
437
  scopeOptions: [],
381
438
  persistentDecisionsAllowed: false,
382
439
  } as ServerMessage);
383
- return 'msg-id';
440
+ return "msg-id";
384
441
  },
385
442
  });
386
443
  const { ctx, sent } = createContext(session);
387
444
 
388
- await handleUserMessage(makeMessage('please call now'), {} as net.Socket, ctx);
445
+ await handleUserMessage(
446
+ makeMessage("please call now"),
447
+ {} as net.Socket,
448
+ ctx,
449
+ );
389
450
 
390
451
  expect(registerMock).toHaveBeenCalledTimes(1);
391
452
  expect(registerMock).toHaveBeenCalledWith(
392
- 'req-confirm-1',
453
+ "req-confirm-1",
393
454
  expect.objectContaining({
394
- conversationId: 'conv-1',
395
- kind: 'confirmation',
455
+ conversationId: "conv-1",
456
+ kind: "confirmation",
396
457
  session,
397
458
  confirmationDetails: expect.objectContaining({
398
- toolName: 'call_start',
399
- riskLevel: 'high',
400
- executionTarget: 'host',
459
+ toolName: "call_start",
460
+ riskLevel: "high",
461
+ executionTarget: "host",
401
462
  }),
402
463
  }),
403
464
  );
404
465
  expect(createCanonicalGuardianRequestMock).toHaveBeenCalledTimes(1);
405
466
  expect(createCanonicalGuardianRequestMock).toHaveBeenCalledWith(
406
467
  expect.objectContaining({
407
- id: 'req-confirm-1',
408
- kind: 'tool_approval',
409
- sourceType: 'desktop',
410
- sourceChannel: 'vellum',
411
- conversationId: 'conv-1',
412
- toolName: 'call_start',
413
- status: 'pending',
414
- requestCode: 'ABC123',
468
+ id: "req-confirm-1",
469
+ kind: "tool_approval",
470
+ sourceType: "desktop",
471
+ sourceChannel: "vellum",
472
+ conversationId: "conv-1",
473
+ toolName: "call_start",
474
+ status: "pending",
475
+ requestCode: "ABC123",
415
476
  }),
416
477
  );
417
- expect(sent.some((event) => event.type === 'confirmation_request')).toBe(true);
478
+ expect(sent.some((event) => event.type === "confirmation_request")).toBe(
479
+ true,
480
+ );
418
481
  });
419
482
 
420
- test('registers IPC confirmation events emitted via session sender (prompter path)', async () => {
483
+ test("registers IPC confirmation events emitted via session sender (prompter path)", async () => {
421
484
  let currentSender: (msg: ServerMessage) => void = () => {};
422
485
  const session = makeSession({
423
486
  hasAnyPendingConfirmation: () => false,
424
- enqueueMessage: mock(() => ({ queued: false, requestId: 'direct-id' })),
487
+ enqueueMessage: mock(() => ({ queued: false, requestId: "direct-id" })),
425
488
  updateClient: (sendToClient: (msg: ServerMessage) => void) => {
426
489
  currentSender = sendToClient;
427
490
  },
428
491
  processMessage: async () => {
429
492
  currentSender({
430
- type: 'confirmation_request',
431
- requestId: 'req-prompter-1',
432
- toolName: 'call_start',
433
- input: { phone_number: '+18084436762' },
434
- riskLevel: 'high',
435
- executionTarget: 'host',
493
+ type: "confirmation_request",
494
+ requestId: "req-prompter-1",
495
+ toolName: "call_start",
496
+ input: { phone_number: "+18084436762" },
497
+ riskLevel: "high",
498
+ executionTarget: "host",
436
499
  allowlistOptions: [],
437
500
  scopeOptions: [],
438
501
  persistentDecisionsAllowed: false,
439
502
  } as ServerMessage);
440
- return 'msg-id';
503
+ return "msg-id";
441
504
  },
442
505
  });
443
506
  const { ctx, sent } = createContext(session);
444
507
 
445
- await handleUserMessage(makeMessage('please call now'), {} as net.Socket, ctx);
508
+ await handleUserMessage(
509
+ makeMessage("please call now"),
510
+ {} as net.Socket,
511
+ ctx,
512
+ );
446
513
 
447
514
  expect(registerMock).toHaveBeenCalledWith(
448
- 'req-prompter-1',
515
+ "req-prompter-1",
449
516
  expect.objectContaining({
450
- conversationId: 'conv-1',
451
- kind: 'confirmation',
517
+ conversationId: "conv-1",
518
+ kind: "confirmation",
452
519
  session,
453
520
  }),
454
521
  );
455
522
  expect(createCanonicalGuardianRequestMock).toHaveBeenCalledWith(
456
523
  expect.objectContaining({
457
- id: 'req-prompter-1',
458
- kind: 'tool_approval',
459
- sourceType: 'desktop',
460
- sourceChannel: 'vellum',
461
- conversationId: 'conv-1',
524
+ id: "req-prompter-1",
525
+ kind: "tool_approval",
526
+ sourceType: "desktop",
527
+ sourceChannel: "vellum",
528
+ conversationId: "conv-1",
462
529
  }),
463
530
  );
464
- expect(sent.some((event) => event.type === 'confirmation_request')).toBe(true);
531
+ expect(sent.some((event) => event.type === "confirmation_request")).toBe(
532
+ true,
533
+ );
465
534
  });
466
535
 
467
- test('syncs canonical status to approved for IPC allow decisions', () => {
536
+ test("syncs canonical status to approved for IPC allow decisions", () => {
468
537
  const session = {
469
- hasPendingConfirmation: (requestId: string) => requestId === 'req-confirm-allow',
538
+ hasPendingConfirmation: (requestId: string) =>
539
+ requestId === "req-confirm-allow",
470
540
  handleConfirmationResponse: mock(() => {}),
471
541
  };
472
542
  const { ctx } = createContext(makeSession());
473
- ctx.sessions.set('conv-1', session as any);
543
+ ctx.sessions.set("conv-1", session as any);
474
544
 
475
545
  const msg: ConfirmationResponse = {
476
- type: 'confirmation_response',
477
- requestId: 'req-confirm-allow',
478
- decision: 'always_allow',
546
+ type: "confirmation_response",
547
+ requestId: "req-confirm-allow",
548
+ decision: "always_allow",
479
549
  };
480
550
 
481
551
  handleConfirmationResponse(msg, {} as net.Socket, ctx);
482
552
 
483
- expect((session.handleConfirmationResponse as any).mock.calls.length).toBe(1);
553
+ expect((session.handleConfirmationResponse as any).mock.calls.length).toBe(
554
+ 1,
555
+ );
484
556
  expect((session.handleConfirmationResponse as any).mock.calls[0]).toEqual([
485
- 'req-confirm-allow',
486
- 'always_allow',
557
+ "req-confirm-allow",
558
+ "always_allow",
487
559
  undefined,
488
560
  undefined,
489
561
  undefined,
490
- { source: 'button' },
562
+ { source: "button" },
491
563
  ]);
492
564
  expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledWith(
493
- 'req-confirm-allow',
494
- 'pending',
495
- { status: 'approved' },
565
+ "req-confirm-allow",
566
+ "pending",
567
+ { status: "approved" },
496
568
  );
497
- expect(resolveMock).toHaveBeenCalledWith('req-confirm-allow');
569
+ expect(resolveMock).toHaveBeenCalledWith("req-confirm-allow");
498
570
  });
499
571
 
500
- test('syncs canonical status to denied for IPC deny decisions in CU sessions', () => {
572
+ test("syncs canonical status to denied for IPC deny decisions in CU sessions", () => {
501
573
  const cuSession = {
502
- hasPendingConfirmation: (requestId: string) => requestId === 'req-confirm-deny',
574
+ hasPendingConfirmation: (requestId: string) =>
575
+ requestId === "req-confirm-deny",
503
576
  handleConfirmationResponse: mock(() => {}),
504
577
  };
505
- const { ctx } = createContext(makeSession({
506
- hasPendingConfirmation: () => false,
507
- }));
508
- ctx.cuSessions.set('cu-1', cuSession as any);
578
+ const { ctx } = createContext(
579
+ makeSession({
580
+ hasPendingConfirmation: () => false,
581
+ }),
582
+ );
583
+ ctx.cuSessions.set("cu-1", cuSession as any);
509
584
 
510
585
  const msg: ConfirmationResponse = {
511
- type: 'confirmation_response',
512
- requestId: 'req-confirm-deny',
513
- decision: 'always_deny',
586
+ type: "confirmation_response",
587
+ requestId: "req-confirm-deny",
588
+ decision: "always_deny",
514
589
  };
515
590
 
516
591
  handleConfirmationResponse(msg, {} as net.Socket, ctx);
517
592
 
518
- expect((cuSession.handleConfirmationResponse as any).mock.calls.length).toBe(1);
519
- expect((cuSession.handleConfirmationResponse as any).mock.calls[0]).toEqual([
520
- 'req-confirm-deny',
521
- 'always_deny',
522
- undefined,
523
- undefined,
524
- ]);
593
+ expect(
594
+ (cuSession.handleConfirmationResponse as any).mock.calls.length,
595
+ ).toBe(1);
596
+ expect((cuSession.handleConfirmationResponse as any).mock.calls[0]).toEqual(
597
+ ["req-confirm-deny", "always_deny", undefined, undefined],
598
+ );
525
599
  expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledWith(
526
- 'req-confirm-deny',
527
- 'pending',
528
- { status: 'denied' },
600
+ "req-confirm-deny",
601
+ "pending",
602
+ { status: "denied" },
529
603
  );
530
- expect(resolveMock).toHaveBeenCalledWith('req-confirm-deny');
604
+ expect(resolveMock).toHaveBeenCalledWith("req-confirm-deny");
531
605
  });
532
606
  });