@vellumai/assistant 0.4.31 → 0.4.32

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 (121) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  4. package/src/__tests__/anthropic-provider.test.ts +86 -1
  5. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  6. package/src/__tests__/checker.test.ts +37 -98
  7. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -0
  8. package/src/__tests__/config-schema.test.ts +6 -5
  9. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  10. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  11. package/src/__tests__/followup-tools.test.ts +0 -30
  12. package/src/__tests__/gemini-provider.test.ts +79 -1
  13. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  14. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  15. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  16. package/src/__tests__/memory-regressions.test.ts +6 -6
  17. package/src/__tests__/openai-provider.test.ts +82 -0
  18. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  19. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  20. package/src/__tests__/recurrence-types.test.ts +0 -15
  21. package/src/__tests__/schedule-tools.test.ts +28 -44
  22. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  23. package/src/__tests__/task-management-tools.test.ts +111 -0
  24. package/src/__tests__/twilio-config.test.ts +0 -3
  25. package/src/amazon/session.ts +30 -91
  26. package/src/calls/call-controller.ts +423 -571
  27. package/src/calls/finalize-call.ts +20 -0
  28. package/src/calls/relay-access-wait.ts +340 -0
  29. package/src/calls/relay-server.ts +267 -902
  30. package/src/calls/relay-setup-router.ts +307 -0
  31. package/src/calls/relay-verification.ts +280 -0
  32. package/src/calls/twilio-config.ts +1 -8
  33. package/src/calls/voice-control-protocol.ts +184 -0
  34. package/src/calls/voice-session-bridge.ts +1 -8
  35. package/src/config/agent-schema.ts +1 -1
  36. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  37. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  38. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  39. package/src/config/core-schema.ts +1 -1
  40. package/src/config/env.ts +0 -10
  41. package/src/config/feature-flag-registry.json +1 -1
  42. package/src/config/loader.ts +19 -0
  43. package/src/config/schema.ts +2 -2
  44. package/src/daemon/handlers/session-history.ts +398 -0
  45. package/src/daemon/handlers/session-user-message.ts +982 -0
  46. package/src/daemon/handlers/sessions.ts +9 -1338
  47. package/src/daemon/ipc-contract/sessions.ts +0 -6
  48. package/src/daemon/ipc-contract-inventory.json +0 -1
  49. package/src/daemon/lifecycle.ts +0 -29
  50. package/src/home-base/app-link-store.ts +0 -7
  51. package/src/memory/conversation-attention-store.ts +1 -1
  52. package/src/memory/conversation-store.ts +0 -51
  53. package/src/memory/db-init.ts +5 -1
  54. package/src/memory/job-handlers/conflict.ts +24 -0
  55. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  56. package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
  57. package/src/memory/migrations/registry.ts +6 -0
  58. package/src/memory/recall-cache.ts +0 -5
  59. package/src/memory/schema/calls.ts +274 -0
  60. package/src/memory/schema/contacts.ts +125 -0
  61. package/src/memory/schema/conversations.ts +129 -0
  62. package/src/memory/schema/guardian.ts +172 -0
  63. package/src/memory/schema/index.ts +8 -0
  64. package/src/memory/schema/infrastructure.ts +205 -0
  65. package/src/memory/schema/memory-core.ts +196 -0
  66. package/src/memory/schema/notifications.ts +191 -0
  67. package/src/memory/schema/tasks.ts +78 -0
  68. package/src/memory/schema.ts +1 -1385
  69. package/src/memory/slack-thread-store.ts +0 -69
  70. package/src/notifications/decisions-store.ts +2 -105
  71. package/src/notifications/deliveries-store.ts +0 -11
  72. package/src/notifications/preferences-store.ts +1 -58
  73. package/src/permissions/checker.ts +6 -17
  74. package/src/providers/anthropic/client.ts +6 -2
  75. package/src/providers/gemini/client.ts +13 -2
  76. package/src/providers/managed-proxy/constants.ts +55 -0
  77. package/src/providers/managed-proxy/context.ts +77 -0
  78. package/src/providers/registry.ts +112 -0
  79. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  80. package/src/runtime/http-server.ts +83 -722
  81. package/src/runtime/http-types.ts +0 -16
  82. package/src/runtime/middleware/auth.ts +0 -12
  83. package/src/runtime/routes/app-routes.ts +33 -0
  84. package/src/runtime/routes/approval-routes.ts +32 -0
  85. package/src/runtime/routes/attachment-routes.ts +32 -0
  86. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  87. package/src/runtime/routes/call-routes.ts +41 -0
  88. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  89. package/src/runtime/routes/channel-routes.ts +70 -0
  90. package/src/runtime/routes/contact-routes.ts +63 -0
  91. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  92. package/src/runtime/routes/conversation-routes.ts +190 -193
  93. package/src/runtime/routes/debug-routes.ts +15 -0
  94. package/src/runtime/routes/events-routes.ts +16 -0
  95. package/src/runtime/routes/global-search-routes.ts +15 -0
  96. package/src/runtime/routes/guardian-action-routes.ts +22 -0
  97. package/src/runtime/routes/guardian-bootstrap-routes.ts +20 -0
  98. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  99. package/src/runtime/routes/identity-routes.ts +20 -0
  100. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  101. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +6 -6
  102. package/src/runtime/routes/integration-routes.ts +83 -0
  103. package/src/runtime/routes/invite-routes.ts +31 -0
  104. package/src/runtime/routes/migration-routes.ts +30 -0
  105. package/src/runtime/routes/pairing-routes.ts +18 -0
  106. package/src/runtime/routes/secret-routes.ts +20 -0
  107. package/src/runtime/routes/surface-action-routes.ts +26 -0
  108. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  109. package/src/runtime/routes/twilio-routes.ts +79 -0
  110. package/src/schedule/recurrence-types.ts +1 -11
  111. package/src/tools/followups/followup_create.ts +9 -3
  112. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  113. package/src/tools/memory/definitions.ts +0 -6
  114. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  115. package/src/tools/schedule/create.ts +1 -3
  116. package/src/tools/schedule/update.ts +9 -6
  117. package/src/twitter/session.ts +29 -77
  118. package/src/util/cookie-session.ts +114 -0
  119. package/src/__tests__/conversation-routes.test.ts +0 -99
  120. package/src/__tests__/task-tools.test.ts +0 -685
  121. package/src/contacts/startup-migration.ts +0 -21
@@ -0,0 +1,398 @@
1
+ import * as net from "node:net";
2
+
3
+ import {
4
+ getAttachmentsForMessage,
5
+ getFilePathForAttachment,
6
+ setAttachmentThumbnail,
7
+ } from "../../memory/attachments-store.js";
8
+ import * as conversationStore from "../../memory/conversation-store.js";
9
+ import { silentlyWithLog } from "../../util/silently.js";
10
+ import { truncate } from "../../util/truncate.js";
11
+ import type { UserMessageAttachment } from "../ipc-contract.js";
12
+ import type {
13
+ ConversationSearchRequest,
14
+ HistoryRequest,
15
+ MessageContentRequest,
16
+ } from "../ipc-protocol.js";
17
+ import { generateVideoThumbnail } from "../video-thumbnail.js";
18
+ import {
19
+ type HandlerContext,
20
+ type HistorySurface,
21
+ type HistoryToolCall,
22
+ log,
23
+ mergeToolResults,
24
+ type ParsedHistoryMessage,
25
+ renderHistoryContent,
26
+ } from "./shared.js";
27
+
28
+ export function handleHistoryRequest(
29
+ msg: HistoryRequest,
30
+ socket: net.Socket,
31
+ ctx: HandlerContext,
32
+ ): void {
33
+ // Default to unlimited when callers don't specify a limit, preserving
34
+ // backward-compatible behavior of returning full conversation history.
35
+ const limit = msg.limit;
36
+
37
+ // Resolve include flags: explicit flags override mode, mode provides defaults.
38
+ // Default mode is 'light' when no mode and no include flags are specified.
39
+ const isFullMode = msg.mode === "full";
40
+ const includeAttachments = msg.includeAttachments ?? isFullMode;
41
+ const includeToolImages = msg.includeToolImages ?? isFullMode;
42
+ const includeSurfaceData = msg.includeSurfaceData ?? isFullMode;
43
+
44
+ const { messages: dbMessages, hasMore } =
45
+ conversationStore.getMessagesPaginated(
46
+ msg.sessionId,
47
+ limit,
48
+ msg.beforeTimestamp,
49
+ msg.beforeMessageId,
50
+ );
51
+
52
+ const parsed: ParsedHistoryMessage[] = dbMessages.map((m) => {
53
+ let text = "";
54
+ let toolCalls: HistoryToolCall[] = [];
55
+ let toolCallsBeforeText = false;
56
+ let textSegments: string[] = [];
57
+ let contentOrder: string[] = [];
58
+ let surfaces: HistorySurface[] = [];
59
+ try {
60
+ const content = JSON.parse(m.content);
61
+ const rendered = renderHistoryContent(content);
62
+ text = rendered.text;
63
+ toolCalls = rendered.toolCalls;
64
+ toolCallsBeforeText = rendered.toolCallsBeforeText;
65
+ textSegments = rendered.textSegments;
66
+ contentOrder = rendered.contentOrder;
67
+ surfaces = rendered.surfaces;
68
+ if (m.role === "assistant" && toolCalls.length > 0) {
69
+ log.info(
70
+ {
71
+ messageId: m.id,
72
+ toolCallCount: toolCalls.length,
73
+ text: truncate(text, 100, ""),
74
+ },
75
+ "History message with tool calls",
76
+ );
77
+ }
78
+ } catch (err) {
79
+ log.debug(
80
+ { err, messageId: m.id },
81
+ "Failed to parse message content as JSON, using raw text",
82
+ );
83
+ text = m.content;
84
+ textSegments = text ? [text] : [];
85
+ contentOrder = text ? ["text:0"] : [];
86
+ surfaces = [];
87
+ }
88
+ let subagentNotification: ParsedHistoryMessage["subagentNotification"];
89
+ if (m.metadata) {
90
+ try {
91
+ subagentNotification = (
92
+ JSON.parse(m.metadata) as {
93
+ subagentNotification?: ParsedHistoryMessage["subagentNotification"];
94
+ }
95
+ ).subagentNotification;
96
+ } catch (err) {
97
+ log.debug(
98
+ { err, messageId: m.id },
99
+ "Failed to parse message metadata as JSON, ignoring",
100
+ );
101
+ }
102
+ }
103
+ return {
104
+ id: m.id,
105
+ role: m.role,
106
+ text,
107
+ timestamp: m.createdAt,
108
+ toolCalls,
109
+ toolCallsBeforeText,
110
+ textSegments,
111
+ contentOrder,
112
+ surfaces,
113
+ ...(subagentNotification ? { subagentNotification } : {}),
114
+ };
115
+ });
116
+
117
+ // Merge tool_result data from user messages into the preceding assistant
118
+ // message's toolCalls, and suppress user messages that only contain
119
+ // tool_result blocks (internal agent-loop turns).
120
+ const merged = mergeToolResults(parsed);
121
+
122
+ const historyMessages = merged.map((m) => {
123
+ let attachments: UserMessageAttachment[] | undefined;
124
+ if (m.role === "assistant" && m.id) {
125
+ const linked = getAttachmentsForMessage(m.id);
126
+ if (linked.length > 0) {
127
+ if (includeAttachments) {
128
+ // Full attachment data: same behavior as before
129
+ const MAX_INLINE_B64_SIZE = 512 * 1024;
130
+ attachments = linked.map((a) => {
131
+ const isFileBacked = !a.dataBase64;
132
+ const omit =
133
+ isFileBacked ||
134
+ (a.mimeType.startsWith("video/") &&
135
+ a.dataBase64.length > MAX_INLINE_B64_SIZE);
136
+
137
+ if (
138
+ a.mimeType.startsWith("video/") &&
139
+ !a.thumbnailBase64 &&
140
+ a.dataBase64
141
+ ) {
142
+ const attachmentId = a.id;
143
+ const base64 = a.dataBase64;
144
+ silentlyWithLog(
145
+ generateVideoThumbnail(base64).then((thumb) => {
146
+ if (thumb) setAttachmentThumbnail(attachmentId, thumb);
147
+ }),
148
+ "video thumbnail generation",
149
+ );
150
+ }
151
+
152
+ const fp = getFilePathForAttachment(a.id);
153
+ return {
154
+ id: a.id,
155
+ filename: a.originalFilename,
156
+ mimeType: a.mimeType,
157
+ data: omit ? "" : a.dataBase64,
158
+ ...(omit ? { sizeBytes: a.sizeBytes } : {}),
159
+ ...(a.thumbnailBase64
160
+ ? { thumbnailData: a.thumbnailBase64 }
161
+ : {}),
162
+ ...(fp ? { filePath: fp } : {}),
163
+ };
164
+ });
165
+ } else {
166
+ // Light mode: metadata only, strip base64 data
167
+ attachments = linked.map((a) => {
168
+ const fp = getFilePathForAttachment(a.id);
169
+ return {
170
+ id: a.id,
171
+ filename: a.originalFilename,
172
+ mimeType: a.mimeType,
173
+ data: "",
174
+ sizeBytes: a.sizeBytes,
175
+ ...(a.thumbnailBase64
176
+ ? { thumbnailData: a.thumbnailBase64 }
177
+ : {}),
178
+ ...(fp ? { filePath: fp } : {}),
179
+ };
180
+ });
181
+ }
182
+ }
183
+ }
184
+
185
+ // In light mode, strip imageData from tool calls
186
+ const filteredToolCalls =
187
+ m.toolCalls.length > 0
188
+ ? includeToolImages
189
+ ? m.toolCalls
190
+ : m.toolCalls.map((tc) => {
191
+ if (tc.imageData) {
192
+ const { imageData: _, ...rest } = tc;
193
+ return rest;
194
+ }
195
+ return tc;
196
+ })
197
+ : m.toolCalls;
198
+
199
+ // In light mode, strip full data from surfaces (keep metadata)
200
+ const filteredSurfaces =
201
+ m.surfaces.length > 0
202
+ ? includeSurfaceData
203
+ ? m.surfaces
204
+ : m.surfaces.map((s) => ({
205
+ surfaceId: s.surfaceId,
206
+ surfaceType: s.surfaceType,
207
+ title: s.title,
208
+ data: {
209
+ ...(s.surfaceType === "dynamic_page"
210
+ ? {
211
+ ...(s.data.preview ? { preview: s.data.preview } : {}),
212
+ ...(s.data.appId ? { appId: s.data.appId } : {}),
213
+ }
214
+ : {}),
215
+ } as Record<string, unknown>,
216
+ ...(s.actions ? { actions: s.actions } : {}),
217
+ ...(s.display ? { display: s.display } : {}),
218
+ }))
219
+ : m.surfaces;
220
+
221
+ // Apply text truncation when maxTextChars is set
222
+ let wasTruncated = false;
223
+ let textWasTruncated = false;
224
+ let text = m.text;
225
+ if (msg.maxTextChars !== undefined && text.length > msg.maxTextChars) {
226
+ text = text.slice(0, msg.maxTextChars) + " \u2026 [truncated]";
227
+ wasTruncated = true;
228
+ textWasTruncated = true;
229
+ }
230
+
231
+ // Apply tool result truncation when maxToolResultChars is set
232
+ const truncatedToolCalls =
233
+ msg.maxToolResultChars !== undefined && filteredToolCalls.length > 0
234
+ ? filteredToolCalls.map((tc) => {
235
+ if (
236
+ tc.result !== undefined &&
237
+ tc.result.length > msg.maxToolResultChars!
238
+ ) {
239
+ wasTruncated = true;
240
+ return {
241
+ ...tc,
242
+ result:
243
+ tc.result.slice(0, msg.maxToolResultChars!) +
244
+ " \u2026 [truncated]",
245
+ };
246
+ }
247
+ return tc;
248
+ })
249
+ : filteredToolCalls;
250
+
251
+ return {
252
+ ...(m.id ? { id: m.id } : {}),
253
+ role: m.role,
254
+ text,
255
+ timestamp: m.timestamp,
256
+ ...(truncatedToolCalls.length > 0
257
+ ? {
258
+ toolCalls: truncatedToolCalls,
259
+ toolCallsBeforeText: m.toolCallsBeforeText,
260
+ }
261
+ : {}),
262
+ ...(attachments ? { attachments } : {}),
263
+ ...(!textWasTruncated && m.textSegments.length > 0
264
+ ? { textSegments: m.textSegments }
265
+ : {}),
266
+ ...(!textWasTruncated && m.contentOrder.length > 0
267
+ ? { contentOrder: m.contentOrder }
268
+ : {}),
269
+ ...(filteredSurfaces.length > 0 ? { surfaces: filteredSurfaces } : {}),
270
+ ...(m.subagentNotification
271
+ ? { subagentNotification: m.subagentNotification }
272
+ : {}),
273
+ ...(wasTruncated ? { wasTruncated: true } : {}),
274
+ };
275
+ });
276
+
277
+ const oldestTimestamp =
278
+ historyMessages.length > 0 ? historyMessages[0].timestamp : undefined;
279
+ // Provide the oldest message ID as a tie-breaker cursor so clients can
280
+ // paginate without skipping same-millisecond messages at page boundaries.
281
+ const oldestMessageId =
282
+ historyMessages.length > 0 ? historyMessages[0].id : undefined;
283
+
284
+ ctx.send(socket, {
285
+ type: "history_response",
286
+ sessionId: msg.sessionId,
287
+ messages: historyMessages,
288
+ hasMore,
289
+ ...(oldestTimestamp !== undefined ? { oldestTimestamp } : {}),
290
+ ...(oldestMessageId ? { oldestMessageId } : {}),
291
+ });
292
+
293
+ // Surfaces are now included directly in the history_response message (in the surfaces array),
294
+ // so we no longer emit separate ui_surface_show messages during history loading.
295
+ }
296
+
297
+ export function handleConversationSearch(
298
+ msg: ConversationSearchRequest,
299
+ socket: net.Socket,
300
+ ctx: HandlerContext,
301
+ ): void {
302
+ const results = conversationStore.searchConversations(msg.query, {
303
+ limit: msg.limit,
304
+ maxMessagesPerConversation: msg.maxMessagesPerConversation,
305
+ });
306
+ ctx.send(socket, {
307
+ type: "conversation_search_response",
308
+ query: msg.query,
309
+ results,
310
+ });
311
+ }
312
+
313
+ export function handleMessageContentRequest(
314
+ msg: MessageContentRequest,
315
+ socket: net.Socket,
316
+ ctx: HandlerContext,
317
+ ): void {
318
+ const dbMessage = conversationStore.getMessageById(
319
+ msg.messageId,
320
+ msg.sessionId,
321
+ );
322
+ if (!dbMessage) {
323
+ ctx.send(socket, {
324
+ type: "error",
325
+ message: `Message ${msg.messageId} not found in session ${msg.sessionId}`,
326
+ });
327
+ return;
328
+ }
329
+
330
+ let text: string | undefined;
331
+ let toolCalls:
332
+ | Array<{ name: string; result?: string; input?: Record<string, unknown> }>
333
+ | undefined;
334
+
335
+ try {
336
+ const content = JSON.parse(dbMessage.content);
337
+ const rendered = renderHistoryContent(content);
338
+ text = rendered.text || undefined;
339
+ const mergedToolCalls = rendered.toolCalls;
340
+
341
+ // Handle legacy conversations where tool_result blocks are stored in the
342
+ // following user message rather than inline with the assistant message.
343
+ // This mirrors the mergeToolResults logic used by handleHistoryRequest.
344
+ if (
345
+ dbMessage.role === "assistant" &&
346
+ mergedToolCalls.some((tc) => tc.result === undefined)
347
+ ) {
348
+ const nextMsg = conversationStore.getNextMessage(
349
+ msg.sessionId,
350
+ dbMessage.createdAt,
351
+ dbMessage.id,
352
+ );
353
+ if (nextMsg && nextMsg.role === "user") {
354
+ try {
355
+ const nextContent = JSON.parse(nextMsg.content);
356
+ const nextRendered = renderHistoryContent(nextContent);
357
+ if (
358
+ nextRendered.text.trim() === "" &&
359
+ nextRendered.toolCalls.length > 0
360
+ ) {
361
+ for (const resultEntry of nextRendered.toolCalls) {
362
+ const unresolved = mergedToolCalls.find(
363
+ (tc) => tc.result === undefined,
364
+ );
365
+ if (unresolved) {
366
+ unresolved.result = resultEntry.result;
367
+ unresolved.isError = resultEntry.isError;
368
+ if (resultEntry.imageData)
369
+ unresolved.imageData = resultEntry.imageData;
370
+ }
371
+ }
372
+ }
373
+ } catch {
374
+ // Next message isn't valid JSON — skip merging
375
+ }
376
+ }
377
+ }
378
+
379
+ if (mergedToolCalls.length > 0) {
380
+ toolCalls = mergedToolCalls.map((tc) => ({
381
+ name: tc.name,
382
+ input: tc.input,
383
+ ...(tc.result !== undefined ? { result: tc.result } : {}),
384
+ }));
385
+ }
386
+ } catch {
387
+ // Raw text content (not JSON)
388
+ text = dbMessage.content || undefined;
389
+ }
390
+
391
+ ctx.send(socket, {
392
+ type: "message_content_response",
393
+ sessionId: msg.sessionId,
394
+ messageId: msg.messageId,
395
+ ...(text !== undefined ? { text } : {}),
396
+ ...(toolCalls ? { toolCalls } : {}),
397
+ });
398
+ }