@vellumai/assistant 0.3.2 → 0.3.4

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 (109) hide show
  1. package/README.md +82 -21
  2. package/package.json +1 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
  4. package/src/__tests__/app-git-history.test.ts +22 -27
  5. package/src/__tests__/app-git-service.test.ts +44 -78
  6. package/src/__tests__/call-orchestrator.test.ts +321 -0
  7. package/src/__tests__/channel-approval-routes.test.ts +1267 -93
  8. package/src/__tests__/channel-approval.test.ts +2 -0
  9. package/src/__tests__/channel-approvals.test.ts +51 -2
  10. package/src/__tests__/channel-delivery-store.test.ts +130 -1
  11. package/src/__tests__/channel-guardian.test.ts +371 -1
  12. package/src/__tests__/config-schema.test.ts +1 -1
  13. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  14. package/src/__tests__/daemon-lifecycle.test.ts +635 -0
  15. package/src/__tests__/daemon-server-session-init.test.ts +5 -0
  16. package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
  17. package/src/__tests__/handlers-telegram-config.test.ts +82 -0
  18. package/src/__tests__/handlers-twilio-config.test.ts +738 -5
  19. package/src/__tests__/ingress-url-consistency.test.ts +64 -0
  20. package/src/__tests__/ipc-snapshot.test.ts +10 -0
  21. package/src/__tests__/run-orchestrator.test.ts +1 -1
  22. package/src/__tests__/secret-scanner.test.ts +223 -0
  23. package/src/__tests__/session-process-bridge.test.ts +2 -0
  24. package/src/__tests__/shell-parser-property.test.ts +357 -2
  25. package/src/__tests__/system-prompt.test.ts +25 -1
  26. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  27. package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
  28. package/src/__tests__/user-reference.test.ts +68 -0
  29. package/src/calls/call-orchestrator.ts +63 -11
  30. package/src/calls/twilio-config.ts +10 -1
  31. package/src/calls/twilio-rest.ts +70 -0
  32. package/src/cli/map.ts +6 -0
  33. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  34. package/src/commands/cc-command-registry.ts +14 -1
  35. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  36. package/src/config/bundled-skills/email-setup/SKILL.md +56 -0
  37. package/src/config/bundled-skills/messaging/SKILL.md +4 -0
  38. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  39. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  40. package/src/config/defaults.ts +1 -1
  41. package/src/config/schema.ts +6 -3
  42. package/src/config/skills.ts +5 -32
  43. package/src/config/system-prompt.ts +16 -0
  44. package/src/config/user-reference.ts +29 -0
  45. package/src/config/vellum-skills/catalog.json +52 -0
  46. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  47. package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
  48. package/src/daemon/auth-manager.ts +103 -0
  49. package/src/daemon/computer-use-session.ts +8 -1
  50. package/src/daemon/config-watcher.ts +253 -0
  51. package/src/daemon/handlers/config.ts +193 -17
  52. package/src/daemon/handlers/sessions.ts +5 -3
  53. package/src/daemon/handlers/skills.ts +60 -17
  54. package/src/daemon/ipc-contract-inventory.json +4 -0
  55. package/src/daemon/ipc-contract.ts +16 -0
  56. package/src/daemon/ipc-handler.ts +87 -0
  57. package/src/daemon/lifecycle.ts +16 -4
  58. package/src/daemon/ride-shotgun-handler.ts +11 -1
  59. package/src/daemon/server.ts +105 -502
  60. package/src/daemon/session-agent-loop.ts +9 -14
  61. package/src/daemon/session-process.ts +20 -3
  62. package/src/daemon/session-runtime-assembly.ts +60 -44
  63. package/src/daemon/session-slash.ts +50 -2
  64. package/src/daemon/session-surfaces.ts +17 -1
  65. package/src/daemon/session.ts +8 -1
  66. package/src/inbound/public-ingress-urls.ts +20 -3
  67. package/src/index.ts +1 -23
  68. package/src/memory/app-git-service.ts +24 -0
  69. package/src/memory/app-store.ts +0 -21
  70. package/src/memory/channel-delivery-store.ts +74 -3
  71. package/src/memory/channel-guardian-store.ts +54 -26
  72. package/src/memory/conversation-key-store.ts +20 -0
  73. package/src/memory/conversation-store.ts +14 -2
  74. package/src/memory/db-connection.ts +28 -0
  75. package/src/memory/db-init.ts +1019 -0
  76. package/src/memory/db.ts +2 -1995
  77. package/src/memory/embedding-backend.ts +79 -11
  78. package/src/memory/indexer.ts +2 -0
  79. package/src/memory/job-utils.ts +64 -4
  80. package/src/memory/jobs-worker.ts +7 -1
  81. package/src/memory/recall-cache.ts +107 -0
  82. package/src/memory/retriever.ts +30 -1
  83. package/src/memory/schema-migration.ts +984 -0
  84. package/src/memory/schema.ts +6 -0
  85. package/src/memory/search/types.ts +2 -0
  86. package/src/permissions/prompter.ts +14 -3
  87. package/src/permissions/trust-store.ts +7 -0
  88. package/src/runtime/channel-approvals.ts +17 -3
  89. package/src/runtime/gateway-client.ts +2 -1
  90. package/src/runtime/http-server.ts +28 -9
  91. package/src/runtime/routes/channel-routes.ts +279 -100
  92. package/src/runtime/routes/run-routes.ts +7 -1
  93. package/src/runtime/run-orchestrator.ts +8 -1
  94. package/src/security/secret-scanner.ts +218 -0
  95. package/src/skills/clawhub.ts +6 -2
  96. package/src/skills/frontmatter.ts +63 -0
  97. package/src/skills/slash-commands.ts +23 -0
  98. package/src/skills/vellum-catalog-remote.ts +107 -0
  99. package/src/subagent/manager.ts +4 -1
  100. package/src/subagent/types.ts +2 -0
  101. package/src/tools/browser/auto-navigate.ts +132 -24
  102. package/src/tools/browser/browser-manager.ts +67 -61
  103. package/src/tools/claude-code/claude-code.ts +55 -3
  104. package/src/tools/executor.ts +10 -2
  105. package/src/tools/skills/vellum-catalog.ts +75 -127
  106. package/src/tools/subagent/spawn.ts +2 -0
  107. package/src/tools/terminal/parser.ts +21 -5
  108. package/src/util/platform.ts +8 -1
  109. package/src/util/retry.ts +4 -4
@@ -211,4 +211,68 @@ describe('Ingress URL consistency between assistant and gateway', () => {
211
211
 
212
212
  expect(gatewayCanonical).toBe(callbackUrl);
213
213
  });
214
+
215
+ // ── SMS-specific URL consistency ──────────────────────────────────
216
+
217
+ test('SMS webhook URL consistency: gateway signature URL matches configured ingress', () => {
218
+ const publicBase = 'https://sms-gateway.example.com';
219
+ const authToken = 'test-sms-auth-token';
220
+
221
+ // The gateway registers /webhooks/twilio/sms with Twilio. Twilio signs
222
+ // inbound SMS requests against the full public URL.
223
+ const smsWebhookUrl = `${publicBase}/webhooks/twilio/sms`;
224
+
225
+ const params = {
226
+ Body: 'hello',
227
+ From: '+15551234567',
228
+ To: '+15559876543',
229
+ MessageSid: 'SM123',
230
+ };
231
+ const twilioSignature = computeTwilioSignature(smsWebhookUrl, params, authToken);
232
+
233
+ // Gateway receives the request on its local address and reconstructs
234
+ // the canonical URL using the configured ingress base.
235
+ const localUrl = 'http://127.0.0.1:7830/webhooks/twilio/sms';
236
+ const canonicalUrl = reconstructGatewayCanonicalUrl(publicBase, localUrl);
237
+
238
+ expect(canonicalUrl).toBe(smsWebhookUrl);
239
+
240
+ const recomputed = computeTwilioSignature(canonicalUrl, params, authToken);
241
+ expect(recomputed).toBe(twilioSignature);
242
+ });
243
+
244
+ test('SMS webhook signature fails when ingress URL is not configured (fail-visible)', () => {
245
+ const publicBase = 'https://sms-gateway.example.com';
246
+ const authToken = 'test-sms-auth-token';
247
+
248
+ const smsWebhookUrl = `${publicBase}/webhooks/twilio/sms`;
249
+ const params = { Body: 'test', From: '+15550001111', MessageSid: 'SM456' };
250
+ const twilioSignature = computeTwilioSignature(smsWebhookUrl, params, authToken);
251
+
252
+ // Without ingress config, the gateway uses the local URL — signature mismatch.
253
+ const localUrl = 'http://127.0.0.1:7830/webhooks/twilio/sms';
254
+ const canonicalWithout = reconstructGatewayCanonicalUrl(undefined, localUrl);
255
+ const recomputedWithout = computeTwilioSignature(canonicalWithout, params, authToken);
256
+ expect(recomputedWithout).not.toBe(twilioSignature);
257
+ });
258
+
259
+ test('all Twilio webhook paths share the /webhooks/twilio/ prefix consistently', () => {
260
+ const config: IngressConfig = {
261
+ ingress: { publicBaseUrl: 'https://consistent.example.com' },
262
+ };
263
+ const base = getPublicBaseUrl(config);
264
+
265
+ // Document the path contract: all Twilio webhooks live under /webhooks/twilio/
266
+ const voiceUrl = getTwilioVoiceWebhookUrl(config, 'sess');
267
+ const statusUrl = getTwilioStatusCallbackUrl(config);
268
+
269
+ // Verify they all share the same base and prefix
270
+ expect(voiceUrl).toStartWith(`${base}/webhooks/twilio/`);
271
+ expect(statusUrl).toStartWith(`${base}/webhooks/twilio/`);
272
+
273
+ // SMS is currently handled at the gateway level (/webhooks/twilio/sms)
274
+ // but the path pattern is the same
275
+ const smsUrl = `${base}/webhooks/twilio/sms`;
276
+ expect(smsUrl).toStartWith(`${base}/webhooks/twilio/`);
277
+ });
214
278
  });
@@ -378,6 +378,10 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
378
378
  type: 'telegram_config',
379
379
  action: 'get',
380
380
  },
381
+ twilio_config: {
382
+ type: 'twilio_config',
383
+ action: 'get',
384
+ },
381
385
  guardian_verification: {
382
386
  type: 'guardian_verification',
383
387
  action: 'create_challenge',
@@ -1218,6 +1222,12 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1218
1222
  connected: true,
1219
1223
  hasWebhookSecret: true,
1220
1224
  },
1225
+ twilio_config_response: {
1226
+ type: 'twilio_config_response',
1227
+ success: true,
1228
+ hasCredentials: true,
1229
+ phoneNumber: '+15551234567',
1230
+ },
1221
1231
  guardian_verification_response: {
1222
1232
  type: 'guardian_verification_response',
1223
1233
  success: true,
@@ -1,4 +1,4 @@
1
- import { describe, test, expect, beforeEach, afterAll, mock, spyOn } from 'bun:test';
1
+ import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test';
2
2
  import { mkdtempSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
@@ -6,6 +6,10 @@ import {
6
6
  _isPlaceholder,
7
7
  _redact,
8
8
  _hasSecretContext,
9
+ _tryDecodeBase64,
10
+ _tryDecodePercentEncoded,
11
+ _tryDecodeHexEscapes,
12
+ _tryDecodeContinuousHex,
9
13
  type SecretMatch,
10
14
  } from '../security/secret-scanner.js';
11
15
 
@@ -898,3 +902,222 @@ describe('word-boundary context keywords', () => {
898
902
  expect(entropy.length).toBeGreaterThan(0);
899
903
  });
900
904
  });
905
+
906
+ // ---------------------------------------------------------------------------
907
+ // Encoded secret detection — decode + re-scan
908
+ // ---------------------------------------------------------------------------
909
+ describe('encoded secret detection', () => {
910
+ // -- Base64-encoded secrets --
911
+ describe('base64-encoded', () => {
912
+ test('detects base64-encoded Stripe key', () => {
913
+ const secret = 'sk_live_abcdefghijklmnopqrstuvwx';
914
+ const encoded = Buffer.from(secret).toString('base64');
915
+ const input = `config: ${encoded}`;
916
+ const matches = scanText(input);
917
+ const found = matches.find((m) => m.type === 'Stripe Secret Key (base64-encoded)');
918
+ expect(found).toBeDefined();
919
+ });
920
+
921
+ test('detects base64-encoded GitHub token', () => {
922
+ const secret = `ghp_${'A'.repeat(36)}`;
923
+ const encoded = Buffer.from(secret).toString('base64');
924
+ const input = `value=${encoded}`;
925
+ const matches = scanText(input);
926
+ const found = matches.find((m) => m.type === 'GitHub Token (base64-encoded)');
927
+ expect(found).toBeDefined();
928
+ });
929
+
930
+ test('detects base64-encoded private key header', () => {
931
+ const secret = '-----BEGIN RSA PRIVATE KEY-----';
932
+ const encoded = Buffer.from(secret).toString('base64');
933
+ const input = `data: ${encoded}`;
934
+ const matches = scanText(input);
935
+ const found = matches.find((m) => m.type === 'Private Key (base64-encoded)');
936
+ expect(found).toBeDefined();
937
+ });
938
+
939
+ test('does not flag base64 that decodes to non-secret text', () => {
940
+ const encoded = Buffer.from('Hello, this is just normal text!').toString('base64');
941
+ const input = `data: ${encoded}`;
942
+ const matches = scanText(input);
943
+ const encoded_matches = matches.filter((m) => m.type.includes('base64-encoded'));
944
+ expect(encoded_matches).toHaveLength(0);
945
+ });
946
+
947
+ test('does not flag base64 that decodes to binary data', () => {
948
+ // Create a base64 string that decodes to non-printable bytes
949
+ const binary = Buffer.from([0x00, 0x01, 0x02, 0x80, 0xff, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15]);
950
+ const encoded = binary.toString('base64');
951
+ const input = `data: ${encoded}`;
952
+ const matches = scanText(input);
953
+ const encoded_matches = matches.filter((m) => m.type.includes('base64-encoded'));
954
+ expect(encoded_matches).toHaveLength(0);
955
+ });
956
+
957
+ test('does not double-count secrets already detected by raw patterns', () => {
958
+ // A JWT is already detected directly — should not be re-detected as base64-encoded
959
+ const header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
960
+ const payload = 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ';
961
+ const signature = 'SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
962
+ const jwt = `${header}.${payload}.${signature}`;
963
+ const input = `token: ${jwt}`;
964
+ const matches = scanText(input);
965
+ const jwtMatches = matches.filter((m) => m.type === 'JSON Web Token');
966
+ const encodedMatches = matches.filter((m) => m.type.includes('base64-encoded'));
967
+ expect(jwtMatches).toHaveLength(1);
968
+ expect(encodedMatches).toHaveLength(0);
969
+ });
970
+ });
971
+
972
+ // -- Percent-encoded secrets --
973
+ describe('percent-encoded', () => {
974
+ test('detects percent-encoded database connection string', () => {
975
+ const secret = 'postgres://user:secret@db.example.com:5432/mydb';
976
+ const encoded = encodeURIComponent(secret);
977
+ const input = `url=${encoded}`;
978
+ const matches = scanText(input);
979
+ const found = matches.find((m) => m.type === 'Database Connection String (percent-encoded)');
980
+ expect(found).toBeDefined();
981
+ });
982
+
983
+ test('detects percent-encoded secret assignment', () => {
984
+ const encoded = 'password%3D%22SuperSecret123%21%22';
985
+ const input = `data=${encoded}`;
986
+ const matches = scanText(input);
987
+ const found = matches.find((m) => m.type.includes('percent-encoded'));
988
+ expect(found).toBeDefined();
989
+ });
990
+
991
+ test('does not flag percent-encoded non-secret text', () => {
992
+ const encoded = 'hello%20world%20this%20is%20normal';
993
+ const input = `text=${encoded}`;
994
+ const matches = scanText(input);
995
+ const encoded_matches = matches.filter((m) => m.type.includes('percent-encoded'));
996
+ expect(encoded_matches).toHaveLength(0);
997
+ });
998
+ });
999
+
1000
+ // -- Hex-escaped secrets --
1001
+ describe('hex-escaped', () => {
1002
+ test('detects hex-escaped Stripe key', () => {
1003
+ const secret = 'sk_live_abcdefghijklmnopqrstuvwx';
1004
+ const escaped = Array.from(secret).map((c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`).join('');
1005
+ const input = `value = "${escaped}"`;
1006
+ const matches = scanText(input);
1007
+ const found = matches.find((m) => m.type === 'Stripe Secret Key (hex-escaped)');
1008
+ expect(found).toBeDefined();
1009
+ });
1010
+
1011
+ test('does not flag hex-escaped non-secret text', () => {
1012
+ const escaped = '\\x48\\x65\\x6c\\x6c\\x6f';
1013
+ const input = `value = "${escaped}"`;
1014
+ const matches = scanText(input);
1015
+ const encoded_matches = matches.filter((m) => m.type.includes('hex-escaped'));
1016
+ expect(encoded_matches).toHaveLength(0);
1017
+ });
1018
+ });
1019
+
1020
+ // -- Continuous hex-encoded secrets --
1021
+ describe('hex-encoded (continuous)', () => {
1022
+ test('detects hex-encoded GitHub token', () => {
1023
+ const secret = `ghp_${'A'.repeat(36)}`;
1024
+ const hexEncoded = Buffer.from(secret).toString('hex');
1025
+ const input = `payload: ${hexEncoded}`;
1026
+ const matches = scanText(input);
1027
+ const found = matches.find((m) => m.type === 'GitHub Token (hex-encoded)');
1028
+ expect(found).toBeDefined();
1029
+ });
1030
+
1031
+ test('detects hex-encoded AWS access key', () => {
1032
+ const secret = 'AKIAIOSFODNN7REALKEY';
1033
+ const hexEncoded = Buffer.from(secret).toString('hex');
1034
+ const input = `data: ${hexEncoded}`;
1035
+ const matches = scanText(input);
1036
+ const found = matches.find((m) => m.type === 'AWS Access Key (hex-encoded)');
1037
+ expect(found).toBeDefined();
1038
+ });
1039
+
1040
+ test('does not flag hex strings that decode to non-secret text', () => {
1041
+ const hexEncoded = Buffer.from('This is just normal harmless text').toString('hex');
1042
+ const input = `data: ${hexEncoded}`;
1043
+ const matches = scanText(input);
1044
+ const encoded_matches = matches.filter((m) => m.type.includes('hex-encoded'));
1045
+ expect(encoded_matches).toHaveLength(0);
1046
+ });
1047
+
1048
+ test('does not flag git SHAs or similar hex hashes', () => {
1049
+ // 40-char hex SHA — too short to decode to a meaningful secret
1050
+ const sha = '4b825dc642cb6eb9a060e54bf899d15f13fe1d7a';
1051
+ const input = `commit: ${sha}`;
1052
+ const matches = scanText(input);
1053
+ const encoded_matches = matches.filter((m) => m.type.includes('hex-encoded'));
1054
+ expect(encoded_matches).toHaveLength(0);
1055
+ });
1056
+ });
1057
+
1058
+ // -- Redaction of encoded secrets --
1059
+ describe('redaction of encoded secrets', () => {
1060
+ test('redactSecrets replaces base64-encoded secrets', () => {
1061
+ const secret = 'sk_live_abcdefghijklmnopqrstuvwx';
1062
+ const encoded = Buffer.from(secret).toString('base64');
1063
+ const input = `config: ${encoded}`;
1064
+ const result = redactSecrets(input);
1065
+ expect(result).toContain('<redacted type="Stripe Secret Key (base64-encoded)" />');
1066
+ expect(result).not.toContain(encoded);
1067
+ });
1068
+
1069
+ test('redactSecrets replaces hex-encoded secrets', () => {
1070
+ const secret = `ghp_${'A'.repeat(36)}`;
1071
+ const hexEncoded = Buffer.from(secret).toString('hex');
1072
+ const input = `data: ${hexEncoded}`;
1073
+ const result = redactSecrets(input);
1074
+ expect(result).toContain('<redacted type="GitHub Token (hex-encoded)" />');
1075
+ expect(result).not.toContain(hexEncoded);
1076
+ });
1077
+ });
1078
+ });
1079
+
1080
+ // ---------------------------------------------------------------------------
1081
+ // Decode helper unit tests
1082
+ // ---------------------------------------------------------------------------
1083
+ describe('decode helpers', () => {
1084
+ test('tryDecodeBase64 returns decoded text for valid base64', () => {
1085
+ const encoded = Buffer.from('sk_live_abcdefghijklmnopqrstuvwx').toString('base64');
1086
+ expect(_tryDecodeBase64(encoded)).toBe('sk_live_abcdefghijklmnopqrstuvwx');
1087
+ });
1088
+
1089
+ test('tryDecodeBase64 returns null for binary content', () => {
1090
+ const binary = Buffer.from([0x00, 0x01, 0x80, 0xff]).toString('base64');
1091
+ expect(_tryDecodeBase64(binary)).toBeNull();
1092
+ });
1093
+
1094
+ test('tryDecodeBase64 returns null for invalid base64', () => {
1095
+ expect(_tryDecodeBase64('not!!valid!!base64!!')).toBeNull();
1096
+ });
1097
+
1098
+ test('tryDecodePercentEncoded returns decoded text', () => {
1099
+ expect(_tryDecodePercentEncoded('hello%20world%21')).toBe('hello world!');
1100
+ });
1101
+
1102
+ test('tryDecodePercentEncoded returns null when nothing to decode', () => {
1103
+ expect(_tryDecodePercentEncoded('no-encoding-here')).toBeNull();
1104
+ });
1105
+
1106
+ test('tryDecodeHexEscapes returns decoded text', () => {
1107
+ expect(_tryDecodeHexEscapes('\\x48\\x65\\x6c\\x6c\\x6f')).toBe('Hello');
1108
+ });
1109
+
1110
+ test('tryDecodeHexEscapes returns null when no escapes', () => {
1111
+ expect(_tryDecodeHexEscapes('plain text')).toBeNull();
1112
+ });
1113
+
1114
+ test('tryDecodeContinuousHex returns decoded text', () => {
1115
+ const hex = Buffer.from('Hello').toString('hex');
1116
+ expect(_tryDecodeContinuousHex(hex)).toBe('Hello');
1117
+ });
1118
+
1119
+ test('tryDecodeContinuousHex returns null for non-printable result', () => {
1120
+ // Hex that decodes to binary
1121
+ expect(_tryDecodeContinuousHex('0001ff80')).toBeNull();
1122
+ });
1123
+ });
@@ -33,6 +33,7 @@ mock.module('../config/loader.js', () => ({
33
33
  provider: 'anthropic',
34
34
  memory: { enabled: false },
35
35
  calls: { enabled: false },
36
+ contextWindow: { maxInputTokens: 200000 },
36
37
  }),
37
38
  }));
38
39
 
@@ -75,6 +76,7 @@ function createMockSession(overrides?: Partial<ProcessSessionContext>): ProcessS
75
76
  traceEmitter: {
76
77
  emit: () => {},
77
78
  } as unknown as ProcessSessionContext['traceEmitter'],
79
+ usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
78
80
  persistUserMessage: mock((_content: string, _attachments: unknown[], _requestId?: string) => 'mock-msg-id'),
79
81
  runAgentLoop: mock(async () => {}),
80
82
  ...overrides,