@vellumai/assistant 0.4.16 → 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 (58) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/package.json +1 -1
  4. package/src/__tests__/call-controller.test.ts +1074 -751
  5. package/src/__tests__/call-routes-http.test.ts +329 -279
  6. package/src/__tests__/channel-approval-routes.test.ts +0 -11
  7. package/src/__tests__/channel-approvals.test.ts +227 -182
  8. package/src/__tests__/channel-guardian.test.ts +1 -0
  9. package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
  10. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  11. package/src/__tests__/conversation-routes.test.ts +71 -41
  12. package/src/__tests__/daemon-server-session-init.test.ts +258 -191
  13. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
  14. package/src/__tests__/extract-email.test.ts +42 -0
  15. package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
  16. package/src/__tests__/gateway-only-guard.test.ts +54 -55
  17. package/src/__tests__/gmail-integration.test.ts +48 -46
  18. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
  19. package/src/__tests__/guardian-outbound-http.test.ts +334 -208
  20. package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
  21. package/src/__tests__/guardian-routing-state.test.ts +257 -209
  22. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
  23. package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
  24. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
  25. package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
  26. package/src/__tests__/ingress-reconcile.test.ts +184 -142
  27. package/src/__tests__/non-member-access-request.test.ts +291 -247
  28. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  29. package/src/__tests__/recording-intent-handler.test.ts +422 -291
  30. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
  31. package/src/__tests__/runtime-events-sse.test.ts +67 -50
  32. package/src/__tests__/send-endpoint-busy.test.ts +314 -232
  33. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  34. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  35. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
  36. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
  37. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
  38. package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
  39. package/src/__tests__/twilio-config.test.ts +49 -41
  40. package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
  41. package/src/__tests__/twilio-routes.test.ts +389 -280
  42. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
  43. package/src/config/bundled-skills/messaging/SKILL.md +5 -4
  44. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +11 -7
  45. package/src/config/env.ts +39 -29
  46. package/src/daemon/handlers/skills.ts +18 -10
  47. package/src/daemon/ipc-contract/messages.ts +1 -0
  48. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  49. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  50. package/src/daemon/session-agent-loop.ts +1 -1
  51. package/src/daemon/session-process.ts +1 -1
  52. package/src/daemon/session-surfaces.ts +42 -2
  53. package/src/runtime/auth/token-service.ts +74 -47
  54. package/src/sequence/reply-matcher.ts +10 -6
  55. package/src/skills/frontmatter.ts +9 -6
  56. package/src/tools/ui-surface/definitions.ts +2 -1
  57. package/src/util/platform.ts +0 -12
  58. package/docs/architecture/http-token-refresh.md +0 -274
@@ -1,8 +1,10 @@
1
- import type * as net from 'node:net';
1
+ import type * 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 * as pendingInteractions from '../runtime/pending-interactions.js';
5
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
6
+
7
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
6
8
 
7
9
  interface MockMemoryPolicy {
8
10
  scopeId: string;
@@ -11,20 +13,20 @@ interface MockMemoryPolicy {
11
13
  }
12
14
 
13
15
  const MOCK_DEFAULT_MEMORY_POLICY: MockMemoryPolicy = {
14
- scopeId: 'default',
16
+ scopeId: "default",
15
17
  includeDefaultFallback: false,
16
18
  strictSideEffects: false,
17
19
  };
18
20
 
19
21
  const conversation = {
20
- id: 'conv-1',
21
- title: 'Test Conversation',
22
+ id: "conv-1",
23
+ title: "Test Conversation",
22
24
  updatedAt: Date.now(),
23
25
  totalInputTokens: 0,
24
26
  totalOutputTokens: 0,
25
27
  totalEstimatedCost: 0,
26
- threadType: 'standard' as string,
27
- memoryScopeId: 'default' as string,
28
+ threadType: "standard" as string,
29
+ memoryScopeId: "default" as string,
28
30
  };
29
31
 
30
32
  let lastCreatedWorkingDir: string | undefined;
@@ -44,8 +46,12 @@ class MockSession {
44
46
  public updateClientCalls = 0;
45
47
  public ensureActorScopedHistoryCalls = 0;
46
48
  public lastUpdateClientHasNoClient: boolean | undefined;
47
- public lastUpdateClientSender: ((msg: Record<string, unknown>) => void) | undefined;
48
- public lastRunAgentLoopOptions: { skipPreMessageRollback?: boolean; isInteractive?: boolean } | undefined;
49
+ public lastUpdateClientSender:
50
+ | ((msg: Record<string, unknown>) => void)
51
+ | undefined;
52
+ public lastRunAgentLoopOptions:
53
+ | { skipPreMessageRollback?: boolean; isInteractive?: boolean }
54
+ | undefined;
49
55
  public updateClientHistory: Array<{ hasNoClient: boolean }> = [];
50
56
  public setSandboxOverrideCalls = 0;
51
57
  private stale = false;
@@ -75,7 +81,10 @@ class MockSession {
75
81
  this.ensureActorScopedHistoryCalls += 1;
76
82
  }
77
83
 
78
- updateClient(sender?: (msg: Record<string, unknown>) => void, hasNoClient = false): void {
84
+ updateClient(
85
+ sender?: (msg: Record<string, unknown>) => void,
86
+ hasNoClient = false,
87
+ ): void {
79
88
  this.updateClientCalls += 1;
80
89
  this.lastUpdateClientSender = sender;
81
90
  this.lastUpdateClientHasNoClient = hasNoClient;
@@ -141,7 +150,7 @@ class MockSession {
141
150
 
142
151
  persistUserMessage(): string {
143
152
  this.processing = true;
144
- return 'msg-1';
153
+ return "msg-1";
145
154
  }
146
155
 
147
156
  async runAgentLoop(
@@ -173,48 +182,49 @@ class MockSession {
173
182
 
174
183
  // Mock child_process to prevent getScreenDimensions() from running osascript on Linux CI
175
184
  // where AppKit/NSScreen is not available and the execSync call would fail.
176
- mock.module('node:child_process', () => ({
177
- execSync: () => '1920x1080',
178
- execFileSync: () => '',
185
+ mock.module("node:child_process", () => ({
186
+ execSync: () => "1920x1080",
187
+ execFileSync: () => "",
179
188
  }));
180
189
 
181
- mock.module('../util/logger.js', () => ({
182
- getLogger: () => new Proxy({} as Record<string, unknown>, {
183
- get: () => () => {},
184
- }),
190
+ mock.module("../util/logger.js", () => ({
191
+ getLogger: () =>
192
+ new Proxy({} as Record<string, unknown>, {
193
+ get: () => () => {},
194
+ }),
185
195
  }));
186
196
 
187
- mock.module('../util/platform.js', () => ({
188
- getSocketPath: () => '/tmp/test.sock',
189
- getDataDir: () => '/tmp',
190
- getSandboxWorkingDir: () => '/tmp/workspace',
197
+ mock.module("../util/platform.js", () => ({
198
+ getSocketPath: () => "/tmp/test.sock",
199
+ getDataDir: () => "/tmp",
200
+ getSandboxWorkingDir: () => "/tmp/workspace",
191
201
  }));
192
202
 
193
- mock.module('../providers/registry.js', () => ({
194
- getProvider: () => ({ name: 'mock-provider' }),
195
- getFailoverProvider: () => ({ name: 'mock-provider' }),
203
+ mock.module("../providers/registry.js", () => ({
204
+ getProvider: () => ({ name: "mock-provider" }),
205
+ getFailoverProvider: () => ({ name: "mock-provider" }),
196
206
  initializeProviders: () => {},
197
207
  }));
198
208
 
199
- mock.module('../providers/ratelimit.js', () => ({
209
+ mock.module("../providers/ratelimit.js", () => ({
200
210
  RateLimitProvider: class {
201
211
  constructor(..._args: unknown[]) {}
202
212
  },
203
213
  }));
204
214
 
205
- mock.module('../config/loader.js', () => ({
215
+ mock.module("../config/loader.js", () => ({
206
216
  getConfig: () => ({
207
217
  ui: {},
208
-
209
- provider: 'mock-provider',
210
- providerOrder: ['mock-provider'],
218
+
219
+ provider: "mock-provider",
220
+ providerOrder: ["mock-provider"],
211
221
  maxTokens: 4096,
212
222
  thinking: false,
213
223
  contextWindow: {
214
224
  maxInputTokens: 100000,
215
225
  thresholdTokens: 80000,
216
226
  preserveRecentMessages: 6,
217
- summaryModel: 'mock-model',
227
+ summaryModel: "mock-model",
218
228
  maxSummaryTokens: 512,
219
229
  },
220
230
  rateLimit: {
@@ -230,68 +240,82 @@ mock.module('../config/loader.js', () => ({
230
240
  invalidateConfigCache: () => {},
231
241
  }));
232
242
 
233
- mock.module('../config/system-prompt.js', () => ({
234
- buildSystemPrompt: () => 'system prompt',
243
+ mock.module("../config/system-prompt.js", () => ({
244
+ buildSystemPrompt: () => "system prompt",
235
245
  }));
236
246
 
237
- mock.module('../permissions/trust-store.js', () => ({
247
+ mock.module("../permissions/trust-store.js", () => ({
238
248
  clearCache: () => {},
239
249
  }));
240
250
 
241
- mock.module('../security/secret-allowlist.js', () => ({
251
+ mock.module("../security/secret-allowlist.js", () => ({
242
252
  resetAllowlist: () => {},
243
253
  }));
244
254
 
245
- mock.module('../memory/external-conversation-store.js', () => ({
255
+ mock.module("../memory/external-conversation-store.js", () => ({
246
256
  getBindingsForConversations: () => new Map(),
247
257
  }));
248
258
 
249
- mock.module('../memory/conversation-attention-store.js', () => ({
259
+ mock.module("../memory/conversation-attention-store.js", () => ({
250
260
  getAttentionStateByConversationIds: () => new Map(),
251
261
  recordAttentionSignal: () => {},
252
262
  recordConversationSeenSignal: () => {},
253
263
  }));
254
264
 
255
- mock.module('../memory/canonical-guardian-store.js', () => ({
256
- generateCanonicalRequestCode: () => 'mock-code-0000',
265
+ mock.module("../memory/canonical-guardian-store.js", () => ({
266
+ generateCanonicalRequestCode: () => "mock-code-0000",
257
267
  createCanonicalGuardianRequest: (params: Record<string, unknown>) => {
258
268
  lastCanonicalGuardianCreateParams = params;
259
- return { requestCode: 'mock-code-0000', status: 'pending' };
269
+ return { requestCode: "mock-code-0000", status: "pending" };
260
270
  },
261
- submitCanonicalRequest: () => ({ requestCode: 'mock-code-0000', status: 'pending' }),
271
+ submitCanonicalRequest: () => ({
272
+ requestCode: "mock-code-0000",
273
+ status: "pending",
274
+ }),
262
275
  getCanonicalRequest: () => null,
263
276
  resolveCanonicalRequest: () => false,
264
277
  listPendingCanonicalRequests: () => [],
265
278
  }));
266
279
 
267
- mock.module('../memory/conversation-store.js', () => ({
280
+ mock.module("../memory/conversation-store.js", () => ({
268
281
  setConversationOriginChannelIfUnset: () => {},
269
282
  updateConversationContextWindow: () => {},
270
283
  deleteMessageById: () => {},
271
284
  updateConversationTitle: () => {},
272
285
  updateConversationUsage: () => {},
273
- addMessage: () => ({ id: 'mock-msg-id' }),
274
- provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
286
+ addMessage: () => ({ id: "mock-msg-id" }),
287
+ provenanceFromGuardianContext: () => ({
288
+ source: "user",
289
+ guardianContext: undefined,
290
+ }),
275
291
  getConversationOriginInterface: () => null,
276
292
  getConversationOriginChannel: () => null,
277
293
  getLatestConversation: () => conversation,
278
- createConversation: (titleOrOpts?: string | { title?: string; threadType?: string }) => {
294
+ createConversation: (
295
+ titleOrOpts?: string | { title?: string; threadType?: string },
296
+ ) => {
279
297
  lastCreateConversationArgs = titleOrOpts;
280
298
  // Derive threadType and memoryScopeId from input, mirroring real implementation
281
- const opts = typeof titleOrOpts === 'string' ? { title: titleOrOpts } : (titleOrOpts ?? {});
282
- const threadType = opts.threadType ?? 'standard';
299
+ const opts =
300
+ typeof titleOrOpts === "string"
301
+ ? { title: titleOrOpts }
302
+ : (titleOrOpts ?? {});
303
+ const threadType = opts.threadType ?? "standard";
283
304
  conversation.threadType = threadType;
284
- conversation.memoryScopeId = threadType === 'private' ? `private:${conversation.id}` : 'default';
305
+ conversation.memoryScopeId =
306
+ threadType === "private" ? `private:${conversation.id}` : "default";
285
307
  return conversation;
286
308
  },
287
- getConversation: (id: string) => (id === conversation.id ? conversation : null),
309
+ getConversation: (id: string) =>
310
+ id === conversation.id ? conversation : null,
288
311
  getConversationThreadType: (id: string) => {
289
- if (id === conversation.id) return conversation.threadType === 'private' ? 'private' : 'standard';
290
- return 'standard';
312
+ if (id === conversation.id)
313
+ return conversation.threadType === "private" ? "private" : "standard";
314
+ return "standard";
291
315
  },
292
316
  getConversationMemoryScopeId: (id: string) => {
293
317
  if (id === conversation.id) return conversation.memoryScopeId;
294
- return 'default';
318
+ return "default";
295
319
  },
296
320
  getMessages: () => [],
297
321
  listConversations: () => [conversation],
@@ -299,25 +323,33 @@ mock.module('../memory/conversation-store.js', () => ({
299
323
  getDisplayMetaForConversations: () => new Map(),
300
324
  }));
301
325
 
302
- mock.module('../runtime/confirmation-request-guardian-bridge.js', () => ({
303
- bridgeConfirmationRequestToGuardian: () => ({ skipped: true, reason: 'not_trusted_contact' }),
326
+ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
327
+ bridgeConfirmationRequestToGuardian: () => ({
328
+ skipped: true,
329
+ reason: "not_trusted_contact",
330
+ }),
304
331
  }));
305
332
 
306
- mock.module('../daemon/session.js', () => ({
333
+ mock.module("../daemon/session.js", () => ({
307
334
  Session: MockSession,
308
335
  DEFAULT_MEMORY_POLICY: MOCK_DEFAULT_MEMORY_POLICY,
309
336
  }));
310
337
 
311
- import { DaemonServer } from '../daemon/server.js';
338
+ import { DaemonServer } from "../daemon/server.js";
312
339
 
313
340
  type DaemonServerTestAccess = {
314
341
  sendInitialSession: (socket: net.Socket) => Promise<void>;
315
- dispatchMessage: (msg: { type: string; [key: string]: unknown }, socket: net.Socket) => void;
342
+ dispatchMessage: (
343
+ msg: { type: string; [key: string]: unknown },
344
+ socket: net.Socket,
345
+ ) => void;
316
346
  sessions: Map<string, MockSession>;
317
347
  socketToSession: Map<net.Socket, string>;
318
348
  };
319
349
 
320
- function asDaemonServerTestAccess(server: DaemonServer): DaemonServerTestAccess {
350
+ function asDaemonServerTestAccess(
351
+ server: DaemonServer,
352
+ ): DaemonServerTestAccess {
321
353
  return server as unknown as DaemonServerTestAccess;
322
354
  }
323
355
 
@@ -337,16 +369,16 @@ function createFakeSocket() {
337
369
 
338
370
  function decodeMessages(writes: string[]): Array<Record<string, unknown>> {
339
371
  return writes
340
- .flatMap((chunk) => chunk.split('\n'))
372
+ .flatMap((chunk) => chunk.split("\n"))
341
373
  .filter((line) => line.length > 0)
342
374
  .map((line) => JSON.parse(line) as Record<string, unknown>);
343
375
  }
344
376
 
345
- describe('DaemonServer initial session hydration', () => {
377
+ describe("DaemonServer initial session hydration", () => {
346
378
  beforeEach(() => {
347
379
  conversation.updatedAt = Date.now();
348
- conversation.threadType = 'standard';
349
- conversation.memoryScopeId = 'default';
380
+ conversation.threadType = "standard";
381
+ conversation.memoryScopeId = "default";
350
382
  lastCreatedWorkingDir = undefined;
351
383
  lastCreatedMemoryPolicy = undefined;
352
384
  lastCreateConversationArgs = undefined;
@@ -356,25 +388,28 @@ describe('DaemonServer initial session hydration', () => {
356
388
  pendingInteractions.clear();
357
389
  });
358
390
 
359
- test('hydrates latest session before session_info so undo works after reconnect', async () => {
391
+ test("hydrates latest session before session_info so undo works after reconnect", async () => {
360
392
  const server = new DaemonServer();
361
393
  const internal = asDaemonServerTestAccess(server);
362
394
  const { socket, writes } = createFakeSocket();
363
395
 
364
396
  await internal.sendInitialSession(socket);
365
- internal.dispatchMessage({ type: 'undo', sessionId: conversation.id }, socket);
397
+ internal.dispatchMessage(
398
+ { type: "undo", sessionId: conversation.id },
399
+ socket,
400
+ );
366
401
 
367
402
  const messages = decodeMessages(writes);
368
- const sessionInfo = messages.find((msg) => msg.type === 'session_info');
369
- const undoComplete = messages.find((msg) => msg.type === 'undo_complete');
370
- const error = messages.find((msg) => msg.type === 'error');
403
+ const sessionInfo = messages.find((msg) => msg.type === "session_info");
404
+ const undoComplete = messages.find((msg) => msg.type === "undo_complete");
405
+ const error = messages.find((msg) => msg.type === "error");
371
406
 
372
407
  expect(sessionInfo).toBeDefined();
373
408
  expect(undoComplete).toBeDefined();
374
409
  expect(error).toBeUndefined();
375
410
  });
376
411
 
377
- test('does not rebind existing session client during initial handshake', async () => {
412
+ test("does not rebind existing session client during initial handshake", async () => {
378
413
  const server = new DaemonServer();
379
414
  const internal = asDaemonServerTestAccess(server);
380
415
  const existingSession = new MockSession(conversation.id);
@@ -387,17 +422,17 @@ describe('DaemonServer initial session hydration', () => {
387
422
  expect(internal.socketToSession.size).toBe(0);
388
423
  });
389
424
 
390
- test('creates sessions with sandbox working dir by default', async () => {
425
+ test("creates sessions with sandbox working dir by default", async () => {
391
426
  const server = new DaemonServer();
392
427
  const internal = asDaemonServerTestAccess(server);
393
428
  const { socket } = createFakeSocket();
394
429
 
395
430
  await internal.sendInitialSession(socket);
396
431
 
397
- expect(lastCreatedWorkingDir).toBe('/tmp/workspace');
432
+ expect(lastCreatedWorkingDir).toBe("/tmp/workspace");
398
433
  });
399
434
 
400
- test('ignores deprecated sandbox_set runtime override messages', async () => {
435
+ test("ignores deprecated sandbox_set runtime override messages", async () => {
401
436
  const server = new DaemonServer();
402
437
  const internal = asDaemonServerTestAccess(server);
403
438
  const { socket } = createFakeSocket();
@@ -407,13 +442,13 @@ describe('DaemonServer initial session hydration', () => {
407
442
  expect(session).toBeDefined();
408
443
  expect(session!.setSandboxOverrideCalls).toBe(0);
409
444
 
410
- internal.dispatchMessage({ type: 'sandbox_set', enabled: false }, socket);
445
+ internal.dispatchMessage({ type: "sandbox_set", enabled: false }, socket);
411
446
 
412
447
  expect(session!.setSandboxOverrideCalls).toBe(0);
413
448
  });
414
449
 
415
- test('sendInitialSession includes threadType in session_info', async () => {
416
- conversation.threadType = 'private';
450
+ test("sendInitialSession includes threadType in session_info", async () => {
451
+ conversation.threadType = "private";
417
452
  const server = new DaemonServer();
418
453
  const internal = asDaemonServerTestAccess(server);
419
454
  const { socket, writes } = createFakeSocket();
@@ -421,13 +456,13 @@ describe('DaemonServer initial session hydration', () => {
421
456
  await internal.sendInitialSession(socket);
422
457
 
423
458
  const messages = decodeMessages(writes);
424
- const sessionInfo = messages.find((msg) => msg.type === 'session_info');
459
+ const sessionInfo = messages.find((msg) => msg.type === "session_info");
425
460
  expect(sessionInfo).toBeDefined();
426
- expect(sessionInfo!.threadType).toBe('private');
461
+ expect(sessionInfo!.threadType).toBe("private");
427
462
  });
428
463
 
429
- test('sendInitialSession includes standard threadType by default', async () => {
430
- conversation.threadType = 'standard';
464
+ test("sendInitialSession includes standard threadType by default", async () => {
465
+ conversation.threadType = "standard";
431
466
  const server = new DaemonServer();
432
467
  const internal = asDaemonServerTestAccess(server);
433
468
  const { socket, writes } = createFakeSocket();
@@ -435,13 +470,13 @@ describe('DaemonServer initial session hydration', () => {
435
470
  await internal.sendInitialSession(socket);
436
471
 
437
472
  const messages = decodeMessages(writes);
438
- const sessionInfo = messages.find((msg) => msg.type === 'session_info');
473
+ const sessionInfo = messages.find((msg) => msg.type === "session_info");
439
474
  expect(sessionInfo).toBeDefined();
440
- expect(sessionInfo!.threadType).toBe('standard');
475
+ expect(sessionInfo!.threadType).toBe("standard");
441
476
  });
442
477
 
443
- test('session_switch includes threadType in session_info', async () => {
444
- conversation.threadType = 'private';
478
+ test("session_switch includes threadType in session_info", async () => {
479
+ conversation.threadType = "private";
445
480
  const server = new DaemonServer();
446
481
  const internal = asDaemonServerTestAccess(server);
447
482
  const { socket, writes } = createFakeSocket();
@@ -450,65 +485,73 @@ describe('DaemonServer initial session hydration', () => {
450
485
  await internal.sendInitialSession(socket);
451
486
  writes.length = 0;
452
487
 
453
- internal.dispatchMessage({ type: 'session_switch', sessionId: conversation.id }, socket);
488
+ internal.dispatchMessage(
489
+ { type: "session_switch", sessionId: conversation.id },
490
+ socket,
491
+ );
454
492
  // Allow async handler to complete
455
493
  await new Promise((r) => setTimeout(r, 50));
456
494
 
457
495
  const messages = decodeMessages(writes);
458
- const sessionInfo = messages.find((msg) => msg.type === 'session_info');
496
+ const sessionInfo = messages.find((msg) => msg.type === "session_info");
459
497
  expect(sessionInfo).toBeDefined();
460
- expect(sessionInfo!.threadType).toBe('private');
498
+ expect(sessionInfo!.threadType).toBe("private");
461
499
  });
462
500
 
463
- test('session_create includes threadType in session_info response', async () => {
501
+ test("session_create includes threadType in session_info response", async () => {
464
502
  // conversation.threadType starts as 'standard' from beforeEach — the mock
465
503
  // createConversation must derive 'private' from the IPC request input.
466
504
  const server = new DaemonServer();
467
505
  const internal = asDaemonServerTestAccess(server);
468
506
  const { socket, writes } = createFakeSocket();
469
507
 
470
- internal.dispatchMessage({
471
- type: 'session_create',
472
- title: 'Thread-type test',
473
- threadType: 'private',
474
- }, socket);
508
+ internal.dispatchMessage(
509
+ {
510
+ type: "session_create",
511
+ title: "Thread-type test",
512
+ threadType: "private",
513
+ },
514
+ socket,
515
+ );
475
516
  // Allow async handler to complete
476
517
  await new Promise((r) => setTimeout(r, 50));
477
518
 
478
519
  // Verify createConversation was called with the threadType from the request
479
520
  expect(lastCreateConversationArgs).toEqual({
480
- title: 'Thread-type test',
481
- threadType: 'private',
521
+ title: "Thread-type test",
522
+ threadType: "private",
482
523
  });
483
524
 
484
525
  const messages = decodeMessages(writes);
485
- const sessionInfo = messages.find((msg) => msg.type === 'session_info');
526
+ const sessionInfo = messages.find((msg) => msg.type === "session_info");
486
527
  expect(sessionInfo).toBeDefined();
487
- expect(sessionInfo!.threadType).toBe('private');
528
+ expect(sessionInfo!.threadType).toBe("private");
488
529
  });
489
530
 
490
- test('session_list includes threadType on each session row', async () => {
491
- conversation.threadType = 'private';
531
+ test("session_list includes threadType on each session row", async () => {
532
+ conversation.threadType = "private";
492
533
  const server = new DaemonServer();
493
534
  const internal = asDaemonServerTestAccess(server);
494
535
  const { socket, writes } = createFakeSocket();
495
536
 
496
- internal.dispatchMessage({ type: 'session_list' }, socket);
537
+ internal.dispatchMessage({ type: "session_list" }, socket);
497
538
 
498
539
  const messages = decodeMessages(writes);
499
- const listResponse = messages.find((msg) => msg.type === 'session_list_response');
540
+ const listResponse = messages.find(
541
+ (msg) => msg.type === "session_list_response",
542
+ );
500
543
  expect(listResponse).toBeDefined();
501
544
 
502
545
  const sessions = listResponse!.sessions as Array<Record<string, unknown>>;
503
546
  expect(sessions.length).toBeGreaterThan(0);
504
547
  for (const session of sessions) {
505
- expect(session.threadType).toBe('private');
548
+ expect(session.threadType).toBe("private");
506
549
  }
507
550
  });
508
551
 
509
- test('session for private conversation derives strict memory policy', async () => {
510
- conversation.threadType = 'private';
511
- conversation.memoryScopeId = 'private:conv-1';
552
+ test("session for private conversation derives strict memory policy", async () => {
553
+ conversation.threadType = "private";
554
+ conversation.memoryScopeId = "private:conv-1";
512
555
  const server = new DaemonServer();
513
556
  const internal = asDaemonServerTestAccess(server);
514
557
  const { socket } = createFakeSocket();
@@ -518,15 +561,15 @@ describe('DaemonServer initial session hydration', () => {
518
561
  const session = internal.sessions.get(conversation.id);
519
562
  expect(session).toBeDefined();
520
563
  expect(session!.memoryPolicy).toEqual({
521
- scopeId: 'private:conv-1',
564
+ scopeId: "private:conv-1",
522
565
  includeDefaultFallback: true,
523
566
  strictSideEffects: true,
524
567
  });
525
568
  });
526
569
 
527
- test('session for standard conversation uses default memory policy', async () => {
528
- conversation.threadType = 'standard';
529
- conversation.memoryScopeId = 'default';
570
+ test("session for standard conversation uses default memory policy", async () => {
571
+ conversation.threadType = "standard";
572
+ conversation.memoryScopeId = "default";
530
573
  const server = new DaemonServer();
531
574
  const internal = asDaemonServerTestAccess(server);
532
575
  const { socket } = createFakeSocket();
@@ -538,10 +581,10 @@ describe('DaemonServer initial session hydration', () => {
538
581
  expect(session!.memoryPolicy).toEqual(MOCK_DEFAULT_MEMORY_POLICY);
539
582
  });
540
583
 
541
- test('session_switch to private conversation derives correct policy on fresh session', async () => {
584
+ test("session_switch to private conversation derives correct policy on fresh session", async () => {
542
585
  // Start with standard conversation
543
- conversation.threadType = 'standard';
544
- conversation.memoryScopeId = 'default';
586
+ conversation.threadType = "standard";
587
+ conversation.memoryScopeId = "default";
545
588
  const server = new DaemonServer();
546
589
  const internal = asDaemonServerTestAccess(server);
547
590
  const { socket } = createFakeSocket();
@@ -549,8 +592,8 @@ describe('DaemonServer initial session hydration', () => {
549
592
  await internal.sendInitialSession(socket);
550
593
 
551
594
  // Now switch the conversation metadata to private before the switch
552
- conversation.threadType = 'private';
553
- conversation.memoryScopeId = 'private:conv-1';
595
+ conversation.threadType = "private";
596
+ conversation.memoryScopeId = "private:conv-1";
554
597
 
555
598
  // Evict the existing session so the switch recreates it
556
599
  const existingSession = internal.sessions.get(conversation.id);
@@ -558,116 +601,134 @@ describe('DaemonServer initial session hydration', () => {
558
601
  existingSession.markStale();
559
602
  }
560
603
 
561
- internal.dispatchMessage({ type: 'session_switch', sessionId: conversation.id }, socket);
604
+ internal.dispatchMessage(
605
+ { type: "session_switch", sessionId: conversation.id },
606
+ socket,
607
+ );
562
608
  await new Promise((r) => setTimeout(r, 50));
563
609
 
564
610
  // The recreated session should have the private policy
565
611
  expect(lastCreatedMemoryPolicy).toEqual({
566
- scopeId: 'private:conv-1',
612
+ scopeId: "private:conv-1",
567
613
  includeDefaultFallback: true,
568
614
  strictSideEffects: true,
569
615
  });
570
616
  });
571
617
 
572
- test('session_create normalizes unrecognized threadType to standard', async () => {
618
+ test("session_create normalizes unrecognized threadType to standard", async () => {
573
619
  const server = new DaemonServer();
574
620
  const internal = asDaemonServerTestAccess(server);
575
621
  const { socket, writes } = createFakeSocket();
576
622
 
577
- internal.dispatchMessage({
578
- type: 'session_create',
579
- title: 'Bad threadType',
580
- threadType: 'bogus' as unknown,
581
- }, socket);
623
+ internal.dispatchMessage(
624
+ {
625
+ type: "session_create",
626
+ title: "Bad threadType",
627
+ threadType: "bogus" as unknown,
628
+ },
629
+ socket,
630
+ );
582
631
  await new Promise((r) => setTimeout(r, 50));
583
632
 
584
633
  // Should normalize to 'standard'
585
634
  expect(lastCreateConversationArgs).toEqual({
586
- title: 'Bad threadType',
587
- threadType: 'standard',
635
+ title: "Bad threadType",
636
+ threadType: "standard",
588
637
  });
589
638
 
590
639
  const messages = decodeMessages(writes);
591
- const sessionInfo = messages.find((msg) => msg.type === 'session_info');
640
+ const sessionInfo = messages.find((msg) => msg.type === "session_info");
592
641
  expect(sessionInfo).toBeDefined();
593
- expect(sessionInfo!.threadType).toBe('standard');
642
+ expect(sessionInfo!.threadType).toBe("standard");
594
643
  });
595
644
 
596
- test('session_create defaults missing threadType to standard', async () => {
645
+ test("session_create defaults missing threadType to standard", async () => {
597
646
  const server = new DaemonServer();
598
647
  const internal = asDaemonServerTestAccess(server);
599
648
  const { socket, writes } = createFakeSocket();
600
649
 
601
- internal.dispatchMessage({
602
- type: 'session_create',
603
- title: 'No threadType',
604
- }, socket);
650
+ internal.dispatchMessage(
651
+ {
652
+ type: "session_create",
653
+ title: "No threadType",
654
+ },
655
+ socket,
656
+ );
605
657
  await new Promise((r) => setTimeout(r, 50));
606
658
 
607
659
  expect(lastCreateConversationArgs).toEqual({
608
- title: 'No threadType',
609
- threadType: 'standard',
660
+ title: "No threadType",
661
+ threadType: "standard",
610
662
  });
611
663
 
612
664
  const messages = decodeMessages(writes);
613
- const sessionInfo = messages.find((msg) => msg.type === 'session_info');
665
+ const sessionInfo = messages.find((msg) => msg.type === "session_info");
614
666
  expect(sessionInfo).toBeDefined();
615
- expect(sessionInfo!.threadType).toBe('standard');
667
+ expect(sessionInfo!.threadType).toBe("standard");
616
668
  });
617
669
 
618
- test('session_create with private threadType derives correct policy', async () => {
670
+ test("session_create with private threadType derives correct policy", async () => {
619
671
  // conversation starts as 'standard' from beforeEach — the mock
620
672
  // createConversation must derive private state from the IPC request.
621
673
  const server = new DaemonServer();
622
674
  const internal = asDaemonServerTestAccess(server);
623
675
  const { socket } = createFakeSocket();
624
676
 
625
- internal.dispatchMessage({
626
- type: 'session_create',
627
- title: 'Private Thread',
628
- threadType: 'private',
629
- }, socket);
677
+ internal.dispatchMessage(
678
+ {
679
+ type: "session_create",
680
+ title: "Private Thread",
681
+ threadType: "private",
682
+ },
683
+ socket,
684
+ );
630
685
  await new Promise((r) => setTimeout(r, 50));
631
686
 
632
687
  // Verify createConversation received the threadType
633
688
  expect(lastCreateConversationArgs).toEqual({
634
- title: 'Private Thread',
635
- threadType: 'private',
689
+ title: "Private Thread",
690
+ threadType: "private",
636
691
  });
637
692
 
638
693
  const session = internal.sessions.get(conversation.id);
639
694
  expect(session).toBeDefined();
640
695
  expect(session!.memoryPolicy).toEqual({
641
- scopeId: 'private:conv-1',
696
+ scopeId: "private:conv-1",
642
697
  includeDefaultFallback: true,
643
698
  strictSideEffects: true,
644
699
  });
645
700
  });
646
701
 
647
- test('interactive HTTP processing marks no-socket sessions interactive and registers confirmation prompts', async () => {
702
+ test("interactive HTTP processing marks no-socket sessions interactive and registers confirmation prompts", async () => {
648
703
  const server = new DaemonServer();
649
704
  const internal = asDaemonServerTestAccess(server);
650
705
 
651
706
  // Pre-configure the mock to emit a confirmation_request during runAgentLoop,
652
707
  // simulating a tool requesting approval while the session is interactive.
653
708
  mockConfirmationToEmitDuringLoop = {
654
- type: 'confirmation_request',
655
- requestId: 'req-interactive-1',
656
- toolName: 'notify_desktop',
657
- input: { title: 'Weather' },
658
- riskLevel: 'high',
659
- allowlistOptions: [{ label: 'notify_desktop:*', description: 'notify_desktop:*', pattern: 'notify_desktop:*' }],
660
- scopeOptions: [{ label: 'everywhere', scope: 'everywhere' }],
709
+ type: "confirmation_request",
710
+ requestId: "req-interactive-1",
711
+ toolName: "notify_desktop",
712
+ input: { title: "Weather" },
713
+ riskLevel: "high",
714
+ allowlistOptions: [
715
+ {
716
+ label: "notify_desktop:*",
717
+ description: "notify_desktop:*",
718
+ pattern: "notify_desktop:*",
719
+ },
720
+ ],
721
+ scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
661
722
  persistentDecisionsAllowed: true,
662
723
  };
663
724
 
664
725
  await server.processMessage(
665
726
  conversation.id,
666
- 'send me a notification',
727
+ "send me a notification",
667
728
  undefined,
668
729
  { isInteractive: true },
669
- 'telegram',
670
- 'telegram',
730
+ "telegram",
731
+ "telegram",
671
732
  );
672
733
 
673
734
  mockConfirmationToEmitDuringLoop = undefined;
@@ -689,61 +750,67 @@ describe('DaemonServer initial session hydration', () => {
689
750
  expect(session!.lastUpdateClientHasNoClient).toBe(true);
690
751
 
691
752
  // The pending interaction was registered during the loop.
692
- const interaction = pendingInteractions.get('req-interactive-1');
753
+ const interaction = pendingInteractions.get("req-interactive-1");
693
754
  expect(interaction).toBeDefined();
694
- expect(interaction?.kind).toBe('confirmation');
755
+ expect(interaction?.kind).toBe("confirmation");
695
756
  expect(interaction?.conversationId).toBe(conversation.id);
696
757
  });
697
758
 
698
- test('confirmation_request canonical records include bound guardian identity context', async () => {
759
+ test("confirmation_request canonical records include bound guardian identity context", async () => {
699
760
  const server = new DaemonServer();
700
761
 
701
762
  mockConfirmationToEmitDuringLoop = {
702
- type: 'confirmation_request',
703
- requestId: 'req-bound-1',
704
- toolName: 'host_bash',
705
- input: { command: 'ls' },
706
- riskLevel: 'high',
707
- allowlistOptions: [{ label: 'host_bash:*', description: 'host_bash:*', pattern: 'host_bash:*' }],
708
- scopeOptions: [{ label: 'everywhere', scope: 'everywhere' }],
763
+ type: "confirmation_request",
764
+ requestId: "req-bound-1",
765
+ toolName: "host_bash",
766
+ input: { command: "ls" },
767
+ riskLevel: "high",
768
+ allowlistOptions: [
769
+ {
770
+ label: "host_bash:*",
771
+ description: "host_bash:*",
772
+ pattern: "host_bash:*",
773
+ },
774
+ ],
775
+ scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
709
776
  persistentDecisionsAllowed: true,
710
777
  };
711
778
 
712
779
  await server.processMessage(
713
780
  conversation.id,
714
- 'run ls',
781
+ "run ls",
715
782
  undefined,
716
783
  {
717
784
  isInteractive: false,
718
785
  guardianContext: {
719
- sourceChannel: 'telegram',
720
- trustClass: 'trusted_contact',
721
- guardianExternalUserId: 'guardian-123',
722
- requesterExternalUserId: 'trusted-456',
723
- requesterChatId: 'chat-789',
786
+ sourceChannel: "telegram",
787
+ trustClass: "trusted_contact",
788
+ guardianExternalUserId: "guardian-123",
789
+ requesterExternalUserId: "trusted-456",
790
+ requesterChatId: "chat-789",
724
791
  },
725
792
  },
726
- 'telegram',
727
- 'telegram',
793
+ "telegram",
794
+ "telegram",
728
795
  );
729
796
 
730
797
  expect(lastCanonicalGuardianCreateParams).toBeDefined();
731
798
  expect(lastCanonicalGuardianCreateParams).toMatchObject({
732
- id: 'req-bound-1',
733
- kind: 'tool_approval',
734
- sourceType: 'channel',
735
- sourceChannel: 'telegram',
799
+ id: "req-bound-1",
800
+ kind: "tool_approval",
801
+ sourceType: "channel",
802
+ sourceChannel: "telegram",
736
803
  conversationId: conversation.id,
737
- guardianExternalUserId: 'guardian-123',
738
- requesterExternalUserId: 'trusted-456',
739
- requesterChatId: 'chat-789',
740
- toolName: 'host_bash',
741
- status: 'pending',
742
- requestCode: 'mock-code-0000',
804
+ guardianExternalUserId: "guardian-123",
805
+ requesterExternalUserId: "trusted-456",
806
+ requesterChatId: "chat-789",
807
+ toolName: "host_bash",
808
+ status: "pending",
809
+ requestCode: "mock-code-0000",
743
810
  });
744
811
  });
745
812
 
746
- test('finally block does not overwrite IPC client that connected during interactive agent loop (processMessage)', async () => {
813
+ test("finally block does not overwrite IPC client that connected during interactive agent loop (processMessage)", async () => {
747
814
  const server = new DaemonServer();
748
815
  const internal = asDaemonServerTestAccess(server);
749
816
 
@@ -757,11 +824,11 @@ describe('DaemonServer initial session hydration', () => {
757
824
 
758
825
  await server.processMessage(
759
826
  conversation.id,
760
- 'hello',
827
+ "hello",
761
828
  undefined,
762
829
  { isInteractive: true },
763
- 'telegram',
764
- 'telegram',
830
+ "telegram",
831
+ "telegram",
765
832
  );
766
833
 
767
834
  mockMidLoopCallback = undefined;
@@ -776,7 +843,7 @@ describe('DaemonServer initial session hydration', () => {
776
843
  expect(session!.lastUpdateClientHasNoClient).toBe(false);
777
844
  });
778
845
 
779
- test('finally block does not overwrite IPC client that connected during interactive agent loop (persistAndProcessMessage)', async () => {
846
+ test("finally block does not overwrite IPC client that connected during interactive agent loop (persistAndProcessMessage)", async () => {
780
847
  const server = new DaemonServer();
781
848
  const internal = asDaemonServerTestAccess(server);
782
849
 
@@ -790,13 +857,13 @@ describe('DaemonServer initial session hydration', () => {
790
857
 
791
858
  const { messageId } = await server.persistAndProcessMessage(
792
859
  conversation.id,
793
- 'hello',
860
+ "hello",
794
861
  undefined,
795
862
  { isInteractive: true },
796
- 'telegram',
797
- 'telegram',
863
+ "telegram",
864
+ "telegram",
798
865
  );
799
- expect(messageId).toBe('msg-1');
866
+ expect(messageId).toBe("msg-1");
800
867
 
801
868
  // persistAndProcessMessage fires the loop in the background; wait for it.
802
869
  await new Promise((r) => setTimeout(r, 50));