@vellumai/assistant 0.5.7 → 0.5.8

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 (197) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  14. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  15. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  16. package/src/__tests__/config-schema.test.ts +2 -0
  17. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  21. package/src/__tests__/conversation-error.test.ts +15 -1
  22. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  23. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-queue.test.ts +0 -1
  26. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  28. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  29. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  30. package/src/__tests__/credential-execution-client.test.ts +5 -2
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  33. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  35. package/src/__tests__/credentials-cli.test.ts +4 -3
  36. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  37. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  38. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  39. package/src/__tests__/journal-context.test.ts +335 -0
  40. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  42. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  43. package/src/__tests__/memory-regressions.test.ts +408 -363
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  45. package/src/__tests__/non-member-access-request.test.ts +2 -2
  46. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  47. package/src/__tests__/oauth-cli.test.ts +5 -1
  48. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  49. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  50. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  51. package/src/__tests__/relay-server.test.ts +1 -2
  52. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  53. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  54. package/src/__tests__/secure-keys.test.ts +18 -15
  55. package/src/__tests__/skill-memory.test.ts +17 -3
  56. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  57. package/src/__tests__/stt-hints.test.ts +437 -0
  58. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  59. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  60. package/src/__tests__/voice-quality.test.ts +58 -0
  61. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  62. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  63. package/src/acp/agent-process.ts +9 -1
  64. package/src/agent/loop.ts +1 -1
  65. package/src/approvals/guardian-request-resolvers.ts +164 -38
  66. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  67. package/src/calls/call-controller.ts +9 -5
  68. package/src/calls/fish-audio-client.ts +26 -14
  69. package/src/calls/stt-hints.ts +189 -0
  70. package/src/calls/tts-text-sanitizer.ts +61 -0
  71. package/src/calls/twilio-routes.ts +32 -4
  72. package/src/calls/voice-quality.ts +15 -3
  73. package/src/calls/voice-session-bridge.ts +1 -0
  74. package/src/cli/commands/avatar.ts +2 -2
  75. package/src/cli/commands/credentials.ts +110 -94
  76. package/src/cli/commands/doctor.ts +2 -2
  77. package/src/cli/commands/keys.ts +7 -7
  78. package/src/cli/commands/memory.ts +1 -1
  79. package/src/cli/commands/oauth/connections.ts +11 -29
  80. package/src/cli/commands/oauth/platform.ts +389 -43
  81. package/src/cli/lib/daemon-credential-client.ts +284 -0
  82. package/src/cli.ts +1 -1
  83. package/src/config/bundled-skills/AGENTS.md +34 -0
  84. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  85. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  86. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  87. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  88. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  89. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  90. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  91. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  92. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  93. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  94. package/src/config/bundled-tool-registry.ts +4 -0
  95. package/src/config/defaults.ts +0 -2
  96. package/src/config/env-registry.ts +4 -4
  97. package/src/config/env.ts +14 -1
  98. package/src/config/feature-flag-registry.json +1 -1
  99. package/src/config/loader.ts +8 -11
  100. package/src/config/schema.ts +5 -16
  101. package/src/config/schemas/calls.ts +17 -0
  102. package/src/config/schemas/inference.ts +2 -2
  103. package/src/config/schemas/journal.ts +16 -0
  104. package/src/config/schemas/memory-processing.ts +2 -2
  105. package/src/config/types.ts +1 -0
  106. package/src/contacts/contact-store.ts +2 -2
  107. package/src/credential-execution/executable-discovery.ts +1 -1
  108. package/src/credential-execution/startup-timeout.ts +36 -0
  109. package/src/daemon/approval-generators.ts +3 -9
  110. package/src/daemon/conversation-error.ts +13 -1
  111. package/src/daemon/conversation-memory.ts +1 -2
  112. package/src/daemon/conversation-process.ts +18 -1
  113. package/src/daemon/conversation-surfaces.ts +30 -1
  114. package/src/daemon/conversation.ts +20 -9
  115. package/src/daemon/guardian-action-generators.ts +3 -9
  116. package/src/daemon/lifecycle.ts +18 -11
  117. package/src/daemon/message-types/conversations.ts +1 -0
  118. package/src/daemon/server.ts +2 -3
  119. package/src/memory/app-store.ts +31 -0
  120. package/src/memory/db-init.ts +4 -0
  121. package/src/memory/indexer.ts +19 -10
  122. package/src/memory/items-extractor.ts +315 -322
  123. package/src/memory/job-handlers/summarization.ts +26 -16
  124. package/src/memory/jobs-store.ts +33 -1
  125. package/src/memory/journal-memory.ts +214 -0
  126. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  127. package/src/memory/migrations/index.ts +1 -0
  128. package/src/memory/migrations/registry.ts +8 -0
  129. package/src/memory/retriever.test.ts +37 -25
  130. package/src/memory/retriever.ts +24 -49
  131. package/src/memory/schema/memory-core.ts +2 -0
  132. package/src/memory/search/formatting.ts +7 -44
  133. package/src/memory/search/staleness.ts +4 -0
  134. package/src/memory/search/tier-classifier.ts +10 -2
  135. package/src/memory/search/types.ts +2 -5
  136. package/src/memory/task-memory-cleanup.ts +4 -3
  137. package/src/notifications/adapters/slack.ts +168 -6
  138. package/src/notifications/broadcaster.ts +1 -0
  139. package/src/notifications/copy-composer.ts +59 -2
  140. package/src/notifications/signal.ts +2 -0
  141. package/src/notifications/types.ts +2 -0
  142. package/src/prompts/journal-context.ts +133 -0
  143. package/src/prompts/persona-resolver.ts +80 -24
  144. package/src/prompts/system-prompt.ts +8 -0
  145. package/src/prompts/templates/SOUL.md +10 -0
  146. package/src/providers/provider-send-message.ts +3 -32
  147. package/src/providers/registry.ts +2 -139
  148. package/src/providers/types.ts +1 -1
  149. package/src/runtime/access-request-helper.ts +4 -0
  150. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  151. package/src/runtime/auth/route-policy.ts +2 -0
  152. package/src/runtime/gateway-client.ts +47 -4
  153. package/src/runtime/guardian-decision-types.ts +45 -4
  154. package/src/runtime/http-server.ts +5 -2
  155. package/src/runtime/routes/access-request-decision.ts +2 -2
  156. package/src/runtime/routes/app-management-routes.ts +2 -1
  157. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  158. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  159. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  160. package/src/runtime/routes/debug-routes.ts +12 -9
  161. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  162. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  163. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  164. package/src/runtime/routes/identity-routes.ts +1 -1
  165. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  168. package/src/runtime/routes/integrations/twilio.ts +52 -10
  169. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  170. package/src/runtime/routes/memory-item-routes.ts +25 -11
  171. package/src/runtime/routes/secret-routes.ts +141 -10
  172. package/src/runtime/routes/tts-routes.ts +11 -1
  173. package/src/security/ces-credential-client.ts +18 -9
  174. package/src/security/ces-rpc-credential-backend.ts +4 -3
  175. package/src/security/credential-backend.ts +10 -4
  176. package/src/security/secure-keys.ts +21 -4
  177. package/src/skills/catalog-install.ts +4 -36
  178. package/src/skills/skill-memory.ts +1 -0
  179. package/src/subagent/manager.ts +2 -5
  180. package/src/tools/acp/spawn.ts +78 -1
  181. package/src/tools/credentials/vault.ts +5 -3
  182. package/src/tools/memory/definitions.ts +3 -2
  183. package/src/tools/memory/handlers.ts +10 -7
  184. package/src/tools/terminal/safe-env.ts +1 -0
  185. package/src/util/browser.ts +15 -0
  186. package/src/util/platform.ts +1 -1
  187. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  188. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  189. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  190. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  191. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  192. package/src/workspace/migrations/registry.ts +4 -0
  193. package/src/workspace/provider-commit-message-generator.ts +12 -21
  194. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  195. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  196. package/src/memory/search/lexical.ts +0 -48
  197. package/src/providers/failover.ts +0 -186
@@ -0,0 +1,437 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { ContactWithChannels } from "../contacts/types.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mock state for resolveCallHints tests
7
+ // ---------------------------------------------------------------------------
8
+
9
+ let mockAssistantName: string | null = "Velissa";
10
+ let mockGuardianName: string = "Sidd";
11
+ let mockTargetContact: ContactWithChannels | null = null;
12
+ let mockRecentContacts: ContactWithChannels[] = [];
13
+ let mockFindContactByAddressThrows = false;
14
+ let mockListContactsThrows = false;
15
+
16
+ const logWarnFn = mock(() => {});
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Mock modules — must be before importing module under test
20
+ // ---------------------------------------------------------------------------
21
+
22
+ mock.module("../daemon/identity-helpers.js", () => ({
23
+ getAssistantName: () => mockAssistantName,
24
+ }));
25
+
26
+ mock.module("../prompts/user-reference.js", () => ({
27
+ DEFAULT_USER_REFERENCE: "my human",
28
+ resolveGuardianName: () => mockGuardianName,
29
+ }));
30
+
31
+ mock.module("../contacts/contact-store.js", () => ({
32
+ findContactByAddress: (_type: string, _address: string) => {
33
+ if (mockFindContactByAddressThrows) {
34
+ throw new Error("DB error: findContactByAddress");
35
+ }
36
+ return mockTargetContact;
37
+ },
38
+ findGuardianForChannel: () => null,
39
+ listGuardianChannels: () => null,
40
+ listContacts: (_limit?: number) => {
41
+ if (mockListContactsThrows) {
42
+ throw new Error("DB error: listContacts");
43
+ }
44
+ return mockRecentContacts;
45
+ },
46
+ }));
47
+
48
+ // Bun's mock.module for "../util/logger.js" doesn't intercept the transitive
49
+ // import in stt-hints.ts due to a Bun limitation. Mocking pino at the package
50
+ // level works because getLogger uses a Proxy that lazily creates a pino child
51
+ // logger — intercepting pino itself captures all log calls.
52
+ const mockChildLogger = {
53
+ debug: () => {},
54
+ info: () => {},
55
+ warn: logWarnFn,
56
+ error: () => {},
57
+ child: () => mockChildLogger,
58
+ };
59
+ const mockPinoLogger = Object.assign(() => mockChildLogger, {
60
+ destination: () => ({}),
61
+ multistream: () => ({}),
62
+ });
63
+ mock.module("pino", () => ({ default: mockPinoLogger }));
64
+ mock.module("pino-pretty", () => ({ default: () => ({}) }));
65
+
66
+ // Import after mocking
67
+ import { buildSttHints, resolveCallHints, type SttHintsInput } from "../calls/stt-hints.js";
68
+
69
+ function emptyInput(): SttHintsInput {
70
+ return {
71
+ staticHints: [],
72
+ assistantName: null,
73
+ guardianName: null,
74
+ taskDescription: null,
75
+ targetContactName: null,
76
+ callerContactName: null,
77
+ inviteFriendName: null,
78
+ inviteGuardianName: null,
79
+ recentContactNames: [],
80
+ };
81
+ }
82
+
83
+ describe("buildSttHints", () => {
84
+ test("empty inputs produce empty string", () => {
85
+ expect(buildSttHints(emptyInput())).toBe("");
86
+ });
87
+
88
+ test("static hints included verbatim", () => {
89
+ const input = emptyInput();
90
+ input.staticHints = ["Vellum", "Acme"];
91
+ expect(buildSttHints(input)).toBe("Vellum,Acme");
92
+ });
93
+
94
+ test("assistant name included", () => {
95
+ const input = emptyInput();
96
+ input.assistantName = "Velissa";
97
+ expect(buildSttHints(input)).toBe("Velissa");
98
+ });
99
+
100
+ test("guardian name included", () => {
101
+ const input = emptyInput();
102
+ input.guardianName = "Sidd";
103
+ expect(buildSttHints(input)).toBe("Sidd");
104
+ });
105
+
106
+ test('default guardian name "my human" excluded', () => {
107
+ const input = emptyInput();
108
+ input.guardianName = "my human";
109
+ expect(buildSttHints(input)).toBe("");
110
+ });
111
+
112
+ test("guardian name with whitespace around default sentinel excluded", () => {
113
+ const input = emptyInput();
114
+ input.guardianName = " my human ";
115
+ expect(buildSttHints(input)).toBe("");
116
+ });
117
+
118
+ test("invite friend name included", () => {
119
+ const input = emptyInput();
120
+ input.inviteFriendName = "Alice";
121
+ expect(buildSttHints(input)).toBe("Alice");
122
+ });
123
+
124
+ test("invite guardian name included", () => {
125
+ const input = emptyInput();
126
+ input.inviteGuardianName = "Bob";
127
+ expect(buildSttHints(input)).toBe("Bob");
128
+ });
129
+
130
+ test("target contact name included", () => {
131
+ const input = emptyInput();
132
+ input.targetContactName = "Charlie";
133
+ expect(buildSttHints(input)).toBe("Charlie");
134
+ });
135
+
136
+ test("caller contact name included", () => {
137
+ const input = emptyInput();
138
+ input.callerContactName = "Diana";
139
+ expect(buildSttHints(input)).toBe("Diana");
140
+ });
141
+
142
+ test("recent contact names included", () => {
143
+ const input = emptyInput();
144
+ input.recentContactNames = ["Dave", "Eve"];
145
+ expect(buildSttHints(input)).toBe("Dave,Eve");
146
+ });
147
+
148
+ test("proper nouns extracted from task description", () => {
149
+ const input = emptyInput();
150
+ input.taskDescription = "Call John Smith at Acme Corp";
151
+ const result = buildSttHints(input);
152
+ expect(result).toContain("John");
153
+ expect(result).toContain("Smith");
154
+ expect(result).toContain("Acme");
155
+ expect(result).toContain("Corp");
156
+ // "Call" is the first word of the sentence — should not be extracted
157
+ expect(result).not.toContain("Call");
158
+ });
159
+
160
+ test("proper nouns extracted across sentence boundaries", () => {
161
+ const input = emptyInput();
162
+ input.taskDescription = "Meet with Alice. Then call Bob! Ask Charlie? Done.";
163
+ const result = buildSttHints(input);
164
+ expect(result).toContain("Alice");
165
+ expect(result).toContain("Bob");
166
+ expect(result).toContain("Charlie");
167
+ // First words of sentences should be excluded
168
+ expect(result).not.toContain("Meet");
169
+ expect(result).not.toContain("Then");
170
+ expect(result).not.toContain("Ask");
171
+ expect(result).not.toContain("Done");
172
+ });
173
+
174
+ test("duplicates removed (case-insensitive)", () => {
175
+ const input = emptyInput();
176
+ input.staticHints = ["Vellum", "vellum", "VELLUM"];
177
+ input.recentContactNames = ["Vellum"];
178
+ const result = buildSttHints(input);
179
+ // Should appear only once — the first occurrence is kept
180
+ expect(result).toBe("Vellum");
181
+ });
182
+
183
+ test("empty and whitespace-only entries filtered", () => {
184
+ const input = emptyInput();
185
+ input.staticHints = ["", " ", "Valid", " ", "Also Valid"];
186
+ expect(buildSttHints(input)).toBe("Valid,Also Valid");
187
+ });
188
+
189
+ test("entries are trimmed", () => {
190
+ const input = emptyInput();
191
+ input.staticHints = [" Padded ", " Spaces "];
192
+ expect(buildSttHints(input)).toBe("Padded,Spaces");
193
+ });
194
+
195
+ test("output truncated at MAX_HINTS_LENGTH without partial words", () => {
196
+ const input = emptyInput();
197
+ // Create hints that will exceed 500 chars when joined
198
+ const longHints: string[] = [];
199
+ for (let i = 0; i < 100; i++) {
200
+ longHints.push(`Hint${i}LongWord`);
201
+ }
202
+ input.staticHints = longHints;
203
+ const result = buildSttHints(input);
204
+ expect(result.length).toBeLessThanOrEqual(500);
205
+ // Should not end with a comma (that would indicate a truncation right after a separator)
206
+ expect(result).not.toMatch(/,$/);
207
+ // Every comma-separated part should be a complete hint from our input
208
+ const parts = result.split(",");
209
+ for (const part of parts) {
210
+ expect(input.staticHints).toContain(part);
211
+ }
212
+ });
213
+
214
+ test("all sources combined in correct order", () => {
215
+ const input: SttHintsInput = {
216
+ staticHints: ["StaticOne"],
217
+ assistantName: "Velissa",
218
+ guardianName: "Sidd",
219
+ taskDescription: "Call John at Acme",
220
+ targetContactName: "Target",
221
+ callerContactName: "Caller",
222
+ inviteFriendName: "Friend",
223
+ inviteGuardianName: "Guardian",
224
+ recentContactNames: ["Recent"],
225
+ };
226
+ const result = buildSttHints(input);
227
+ const parts = result.split(",");
228
+ // Verify all expected hints are present
229
+ expect(parts).toContain("StaticOne");
230
+ expect(parts).toContain("Velissa");
231
+ expect(parts).toContain("Sidd");
232
+ expect(parts).toContain("John");
233
+ expect(parts).toContain("Acme");
234
+ expect(parts).toContain("Target");
235
+ expect(parts).toContain("Caller");
236
+ expect(parts).toContain("Friend");
237
+ expect(parts).toContain("Guardian");
238
+ expect(parts).toContain("Recent");
239
+ });
240
+
241
+ test("surnames after abbreviation periods are preserved", () => {
242
+ const input = emptyInput();
243
+ input.taskDescription = "Call Dr. Smith at Acme";
244
+ const result = buildSttHints(input);
245
+ expect(result).toContain("Smith");
246
+ expect(result).toContain("Acme");
247
+ // "Dr" should also appear as a capitalized word
248
+ expect(result).toContain("Dr");
249
+ });
250
+
251
+ test("multiple abbreviation titles preserve following names", () => {
252
+ const input = emptyInput();
253
+ input.taskDescription = "Meet Mr. Johnson and Mrs. Williams at the office";
254
+ const result = buildSttHints(input);
255
+ expect(result).toContain("Johnson");
256
+ expect(result).toContain("Williams");
257
+ });
258
+
259
+ test("non-ASCII letters preserved in names", () => {
260
+ const input = emptyInput();
261
+ input.taskDescription = "Call José García and Łukasz Nowak";
262
+ const result = buildSttHints(input);
263
+ expect(result).toContain("José");
264
+ expect(result).toContain("García");
265
+ expect(result).toContain("Łukasz");
266
+ expect(result).toContain("Nowak");
267
+ });
268
+
269
+ test("accented uppercase letters detected as proper nouns", () => {
270
+ const input = emptyInput();
271
+ input.taskDescription = "Talk to Zoë about the project";
272
+ const result = buildSttHints(input);
273
+ expect(result).toContain("Zoë");
274
+ });
275
+
276
+ test("null and empty string names are excluded", () => {
277
+ const input = emptyInput();
278
+ input.assistantName = "";
279
+ input.guardianName = "";
280
+ input.targetContactName = null;
281
+ input.inviteFriendName = null;
282
+ input.inviteGuardianName = null;
283
+ expect(buildSttHints(input)).toBe("");
284
+ });
285
+ });
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // resolveCallHints — wiring and error resilience
289
+ // ---------------------------------------------------------------------------
290
+
291
+ function makeContact(displayName: string): ContactWithChannels {
292
+ const now = Date.now();
293
+ return {
294
+ id: `contact-${displayName.toLowerCase()}`,
295
+ displayName,
296
+ notes: null,
297
+ lastInteraction: null,
298
+ interactionCount: 0,
299
+ createdAt: now,
300
+ updatedAt: now,
301
+ role: "contact",
302
+ contactType: "human",
303
+ principalId: null,
304
+ userFile: null,
305
+ channels: [],
306
+ };
307
+ }
308
+
309
+ describe("resolveCallHints", () => {
310
+ beforeEach(() => {
311
+ mockAssistantName = "Velissa";
312
+ mockGuardianName = "Sidd";
313
+ mockTargetContact = null;
314
+ mockRecentContacts = [];
315
+ mockFindContactByAddressThrows = false;
316
+ mockListContactsThrows = false;
317
+ logWarnFn.mockClear();
318
+ });
319
+
320
+ test("happy path wires all sources correctly", () => {
321
+ mockTargetContact = makeContact("Alice");
322
+ mockRecentContacts = [makeContact("Bob"), makeContact("Charlie")];
323
+
324
+ const session = {
325
+ task: "Call Alice at Acme",
326
+ toNumber: "+15551234567",
327
+ fromNumber: "+15559876543",
328
+ direction: "outbound" as const,
329
+ inviteFriendName: "Dave",
330
+ inviteGuardianName: "Eve",
331
+ };
332
+
333
+ const result = resolveCallHints(session, ["StaticHint"]);
334
+ const parts = result.split(",");
335
+
336
+ expect(parts).toContain("StaticHint");
337
+ expect(parts).toContain("Velissa");
338
+ expect(parts).toContain("Sidd");
339
+ expect(parts).toContain("Alice");
340
+ expect(parts).toContain("Dave");
341
+ expect(parts).toContain("Eve");
342
+ expect(parts).toContain("Bob");
343
+ expect(parts).toContain("Charlie");
344
+ // Proper nouns from task description
345
+ expect(parts).toContain("Acme");
346
+ expect(logWarnFn).not.toHaveBeenCalled();
347
+ });
348
+
349
+ test("findContactByAddress failure is caught and logged without throwing", () => {
350
+ mockFindContactByAddressThrows = true;
351
+ mockRecentContacts = [makeContact("Bob")];
352
+
353
+ const session = {
354
+ task: null,
355
+ toNumber: "+15551234567",
356
+ fromNumber: "+15559876543",
357
+ direction: "outbound" as const,
358
+ inviteFriendName: null,
359
+ inviteGuardianName: null,
360
+ };
361
+
362
+ // Should not throw
363
+ const result = resolveCallHints(session, []);
364
+ const parts = result.split(",");
365
+
366
+ // Target contact should be absent (lookup failed)
367
+ // But other sources should still work
368
+ expect(parts).toContain("Velissa");
369
+ expect(parts).toContain("Sidd");
370
+ expect(parts).toContain("Bob");
371
+ expect(logWarnFn).toHaveBeenCalled();
372
+ });
373
+
374
+ test("listContacts failure is caught and logged without throwing", () => {
375
+ mockListContactsThrows = true;
376
+ mockTargetContact = makeContact("Alice");
377
+
378
+ const session = {
379
+ task: null,
380
+ toNumber: "+15551234567",
381
+ fromNumber: "+15559876543",
382
+ direction: "outbound" as const,
383
+ inviteFriendName: null,
384
+ inviteGuardianName: null,
385
+ };
386
+
387
+ // Should not throw
388
+ const result = resolveCallHints(session, []);
389
+ const parts = result.split(",");
390
+
391
+ // Recent contacts should be absent (listing failed)
392
+ // But other sources should still work
393
+ expect(parts).toContain("Velissa");
394
+ expect(parts).toContain("Sidd");
395
+ expect(parts).toContain("Alice");
396
+ expect(logWarnFn).toHaveBeenCalled();
397
+ });
398
+
399
+ test("inbound call resolves caller contact from fromNumber", () => {
400
+ mockTargetContact = makeContact("Alice");
401
+ mockRecentContacts = [makeContact("Bob")];
402
+
403
+ const session = {
404
+ task: null,
405
+ toNumber: "+15559876543",
406
+ fromNumber: "+15551234567",
407
+ direction: "inbound" as const,
408
+ inviteFriendName: null,
409
+ inviteGuardianName: null,
410
+ };
411
+
412
+ const result = resolveCallHints(session, []);
413
+ const parts = result.split(",");
414
+
415
+ // For inbound, the contact found via fromNumber should appear as caller, not target
416
+ expect(parts).toContain("Alice");
417
+ expect(parts).toContain("Velissa");
418
+ expect(parts).toContain("Sidd");
419
+ expect(parts).toContain("Bob");
420
+ expect(logWarnFn).not.toHaveBeenCalled();
421
+ });
422
+
423
+ test("null session produces hints from assistant name, guardian name, and recent contacts", () => {
424
+ mockRecentContacts = [makeContact("RecentOne"), makeContact("RecentTwo")];
425
+
426
+ const result = resolveCallHints(null, ["Static"]);
427
+ const parts = result.split(",");
428
+
429
+ expect(parts).toContain("Static");
430
+ expect(parts).toContain("Velissa");
431
+ expect(parts).toContain("Sidd");
432
+ expect(parts).toContain("RecentOne");
433
+ expect(parts).toContain("RecentTwo");
434
+ // No target contact lookup should have been attempted (no session)
435
+ expect(logWarnFn).not.toHaveBeenCalled();
436
+ });
437
+ });
@@ -175,6 +175,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
175
175
  importance: 0.7,
176
176
  fingerprint: "fp-assistant-inferred",
177
177
  verificationState: "assistant_inferred",
178
+ sourceType: "extraction",
179
+ sourceMessageRole: "assistant",
178
180
  scopeId: "default",
179
181
  firstSeenAt: now + 10,
180
182
  lastSeenAt: now + 10,
@@ -189,6 +191,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
189
191
  importance: 0.8,
190
192
  fingerprint: "fp-user-reported",
191
193
  verificationState: "user_reported",
194
+ sourceType: "extraction",
195
+ sourceMessageRole: "user",
192
196
  scopeId: "default",
193
197
  firstSeenAt: now + 20,
194
198
  lastSeenAt: now + 20,
@@ -203,6 +207,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
203
207
  importance: 0.5,
204
208
  fingerprint: "fp-other-conv",
205
209
  verificationState: "assistant_inferred",
210
+ sourceType: "extraction",
211
+ sourceMessageRole: "assistant",
206
212
  scopeId: "default",
207
213
  firstSeenAt: now + 30,
208
214
  lastSeenAt: now + 30,
@@ -217,6 +223,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
217
223
  importance: 0.4,
218
224
  fingerprint: "fp-already-superseded",
219
225
  verificationState: "assistant_inferred",
226
+ sourceType: "extraction",
227
+ sourceMessageRole: "assistant",
220
228
  scopeId: "default",
221
229
  firstSeenAt: now + 5,
222
230
  lastSeenAt: now + 5,
@@ -449,6 +457,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
449
457
  importance: 0.7,
450
458
  fingerprint: "fp-cross-sourced",
451
459
  verificationState: "assistant_inferred",
460
+ sourceType: "extraction",
461
+ sourceMessageRole: "assistant",
452
462
  scopeId: "default",
453
463
  firstSeenAt: now + 10,
454
464
  lastSeenAt: now + 20,
@@ -572,6 +582,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
572
582
  importance: 0.7,
573
583
  fingerprint: "fp-cross-sched",
574
584
  verificationState: "assistant_inferred",
585
+ sourceType: "extraction",
586
+ sourceMessageRole: "assistant",
575
587
  scopeId: "default",
576
588
  firstSeenAt: now + 10,
577
589
  lastSeenAt: now + 20,
@@ -688,6 +700,8 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
688
700
  importance: 0.7,
689
701
  fingerprint: "fp-good-corroboration",
690
702
  verificationState: "assistant_inferred",
703
+ sourceType: "extraction",
704
+ sourceMessageRole: "assistant",
691
705
  scopeId: "default",
692
706
  firstSeenAt: now + 10,
693
707
  lastSeenAt: now + 20,