@vellumai/assistant 0.4.50 → 0.4.51

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 (153) hide show
  1. package/docs/architecture/integrations.md +2 -2
  2. package/docs/architecture/keychain-broker.md +6 -6
  3. package/knip.json +32 -0
  4. package/package.json +3 -2
  5. package/src/__tests__/btw-routes.test.ts +61 -5
  6. package/src/__tests__/config-watcher.test.ts +8 -0
  7. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  8. package/src/__tests__/credential-vault-unit.test.ts +19 -18
  9. package/src/__tests__/credential-vault.test.ts +17 -17
  10. package/src/__tests__/credentials-cli.test.ts +257 -82
  11. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  12. package/src/__tests__/integration-status.test.ts +31 -30
  13. package/src/__tests__/invite-redemption-service.test.ts +121 -32
  14. package/src/__tests__/invite-routes-http.test.ts +166 -5
  15. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  16. package/src/__tests__/oauth-cli.test.ts +286 -60
  17. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  18. package/src/__tests__/oauth-store.test.ts +243 -11
  19. package/src/__tests__/relay-server.test.ts +9 -0
  20. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  21. package/src/__tests__/secure-keys.test.ts +71 -16
  22. package/src/__tests__/server-history-render.test.ts +2 -2
  23. package/src/__tests__/skills.test.ts +2 -2
  24. package/src/__tests__/slack-channel-config.test.ts +10 -8
  25. package/src/__tests__/twilio-config.test.ts +11 -10
  26. package/src/__tests__/twilio-provider.test.ts +9 -4
  27. package/src/__tests__/voice-invite-redemption.test.ts +58 -9
  28. package/src/calls/call-domain.ts +3 -4
  29. package/src/calls/relay-server.ts +1 -1
  30. package/src/calls/twilio-config.ts +4 -3
  31. package/src/calls/twilio-provider.ts +14 -9
  32. package/src/calls/twilio-rest.ts +10 -7
  33. package/src/cli/commands/config.ts +14 -9
  34. package/src/cli/commands/contacts.ts +3 -0
  35. package/src/cli/commands/credentials.ts +170 -174
  36. package/src/cli/commands/doctor.ts +7 -5
  37. package/src/cli/commands/keys.ts +9 -9
  38. package/src/cli/commands/oauth/apps.ts +40 -11
  39. package/src/cli/commands/oauth/connections.ts +66 -30
  40. package/src/cli/commands/oauth/index.ts +3 -3
  41. package/src/cli/commands/oauth/providers.ts +3 -3
  42. package/src/cli.ts +16 -12
  43. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  44. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  45. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  46. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  47. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  48. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  49. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  50. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  51. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  52. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  53. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  54. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  55. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  56. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  57. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  58. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  59. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  60. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  61. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  62. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  63. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  64. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  65. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  66. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  67. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  68. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  69. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  70. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  71. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  72. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  73. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  74. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  75. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  76. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  77. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  78. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  79. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  80. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  81. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  82. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  83. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  84. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  85. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  86. package/src/config/loader.ts +6 -42
  87. package/src/contacts/contact-store.ts +39 -2
  88. package/src/contacts/contacts-write.ts +9 -0
  89. package/src/daemon/config-watcher.ts +8 -13
  90. package/src/daemon/handlers/config-ingress.ts +2 -2
  91. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  92. package/src/daemon/handlers/config-telegram.ts +23 -14
  93. package/src/daemon/handlers/session-history.ts +1 -358
  94. package/src/daemon/handlers/shared.ts +3 -17
  95. package/src/daemon/lifecycle.ts +8 -1
  96. package/src/daemon/message-types/sessions.ts +0 -42
  97. package/src/daemon/server.ts +0 -6
  98. package/src/daemon/session-slash.ts +3 -5
  99. package/src/email/providers/index.ts +2 -2
  100. package/src/media/avatar-router.ts +1 -1
  101. package/src/memory/conversation-queries.ts +3 -80
  102. package/src/memory/db-init.ts +4 -0
  103. package/src/memory/invite-store.ts +19 -0
  104. package/src/memory/migrations/149-oauth-tables.ts +1 -1
  105. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +1 -1
  106. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  107. package/src/memory/migrations/index.ts +1 -0
  108. package/src/memory/migrations/registry.ts +6 -0
  109. package/src/memory/schema/contacts.ts +1 -0
  110. package/src/messaging/provider.ts +1 -1
  111. package/src/messaging/providers/gmail/adapter.ts +1 -1
  112. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  113. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  114. package/src/messaging/registry.ts +9 -5
  115. package/src/oauth/byo-connection.test.ts +32 -24
  116. package/src/oauth/connect-orchestrator.ts +4 -10
  117. package/src/oauth/connection-resolver.ts +20 -6
  118. package/src/oauth/manual-token-connection.ts +5 -5
  119. package/src/oauth/oauth-store.ts +83 -17
  120. package/src/oauth/platform-connection.test.ts +1 -1
  121. package/src/oauth/provider-behaviors.ts +503 -4
  122. package/src/oauth/seed-providers.ts +208 -8
  123. package/src/oauth/token-persistence.ts +20 -13
  124. package/src/runtime/channel-readiness-service.ts +48 -40
  125. package/src/runtime/http-types.ts +2 -0
  126. package/src/runtime/invite-redemption-service.ts +71 -29
  127. package/src/runtime/invite-service.ts +40 -22
  128. package/src/runtime/middleware/twilio-validation.ts +1 -1
  129. package/src/runtime/routes/btw-routes.ts +10 -5
  130. package/src/runtime/routes/conversation-routes.ts +47 -10
  131. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  132. package/src/runtime/routes/integrations/telegram.ts +2 -2
  133. package/src/runtime/routes/integrations/twilio.ts +17 -17
  134. package/src/runtime/routes/invite-routes.ts +29 -4
  135. package/src/runtime/routes/secret-routes.ts +17 -0
  136. package/src/runtime/routes/settings-routes.ts +3 -3
  137. package/src/runtime/routes/workspace-routes.ts +7 -3
  138. package/src/runtime/routes/workspace-utils.ts +8 -2
  139. package/src/schedule/integration-status.ts +26 -19
  140. package/src/security/oauth2.ts +6 -7
  141. package/src/security/secure-keys.ts +19 -16
  142. package/src/security/token-manager.ts +13 -6
  143. package/src/services/vercel-deploy.ts +0 -24
  144. package/src/signals/confirm.ts +78 -0
  145. package/src/signals/mcp-reload.ts +18 -0
  146. package/src/tools/credentials/vault.ts +22 -5
  147. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  148. package/src/tools/schedule/create.ts +2 -2
  149. package/src/watcher/provider-types.ts +1 -1
  150. package/src/watcher/providers/github.ts +1 -1
  151. package/src/watcher/providers/gmail.ts +3 -3
  152. package/src/watcher/providers/google-calendar.ts +3 -3
  153. package/src/watcher/providers/linear.ts +1 -1
@@ -16,7 +16,7 @@ import { getConnectionByProvider } from "../../oauth/oauth-store.js";
16
16
  import { credentialKey } from "../../security/credential-key.js";
17
17
  import {
18
18
  deleteSecureKeyAsync,
19
- getSecureKey,
19
+ getSecureKeyAsync,
20
20
  setSecureKeyAsync,
21
21
  } from "../../security/secure-keys.js";
22
22
  import { getTelegramBotUsername } from "../../telegram/bot-username.js";
@@ -65,11 +65,13 @@ export type TelegramConfigResult = Omit<TelegramConfigResponse, "type">;
65
65
 
66
66
  // -- Extracted business logic functions --
67
67
 
68
- export function getTelegramConfig(): TelegramConfigResult {
69
- const hasBotToken = !!getSecureKey(credentialKey("telegram", "bot_token"));
70
- const hasWebhookSecret = !!getSecureKey(
68
+ export async function getTelegramConfig(): Promise<TelegramConfigResult> {
69
+ const hasBotToken = !!(await getSecureKeyAsync(
70
+ credentialKey("telegram", "bot_token"),
71
+ ));
72
+ const hasWebhookSecret = !!(await getSecureKeyAsync(
71
73
  credentialKey("telegram", "webhook_secret"),
72
- );
74
+ ));
73
75
  const conn = getConnectionByProvider("telegram");
74
76
  const connected = !!(conn && conn.status === "active");
75
77
  const botUsername = getTelegramBotUsername();
@@ -89,7 +91,8 @@ export async function setTelegramConfig(
89
91
  // Track provenance so we only rollback tokens that were freshly provided.
90
92
  const isNewToken = !!botToken;
91
93
  const resolvedToken =
92
- botToken || getSecureKey(credentialKey("telegram", "bot_token"));
94
+ botToken ||
95
+ (await getSecureKeyAsync(credentialKey("telegram", "bot_token")));
93
96
  if (!resolvedToken) {
94
97
  return {
95
98
  success: false,
@@ -166,9 +169,9 @@ export async function setTelegramConfig(
166
169
  invalidateConfigCache();
167
170
 
168
171
  // Ensure webhook secret exists (generate if missing)
169
- let hasWebhookSecret = !!getSecureKey(
172
+ let hasWebhookSecret = !!(await getSecureKeyAsync(
170
173
  credentialKey("telegram", "webhook_secret"),
171
- );
174
+ ));
172
175
  if (!hasWebhookSecret) {
173
176
  const { randomUUID } = await import("node:crypto");
174
177
  const webhookSecret = randomUUID();
@@ -241,7 +244,9 @@ export async function clearTelegramConfig(): Promise<TelegramConfigResult> {
241
244
  // The gateway reconcile short-circuits when credentials are absent,
242
245
  // so we must call the Telegram API directly while the token is still
243
246
  // available.
244
- const botToken = getSecureKey(credentialKey("telegram", "bot_token"));
247
+ const botToken = await getSecureKeyAsync(
248
+ credentialKey("telegram", "bot_token"),
249
+ );
245
250
  if (botToken) {
246
251
  try {
247
252
  await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`);
@@ -260,10 +265,12 @@ export async function clearTelegramConfig(): Promise<TelegramConfigResult> {
260
265
 
261
266
  if (r1 === "error" || r2 === "error") {
262
267
  // Check each key individually so partial deletions report accurate status.
263
- const hasBotToken = !!getSecureKey(credentialKey("telegram", "bot_token"));
264
- const hasWebhookSecret = !!getSecureKey(
268
+ const hasBotToken = !!(await getSecureKeyAsync(
269
+ credentialKey("telegram", "bot_token"),
270
+ ));
271
+ const hasWebhookSecret = !!(await getSecureKeyAsync(
265
272
  credentialKey("telegram", "webhook_secret"),
266
- );
273
+ ));
267
274
  return {
268
275
  success: false,
269
276
  hasBotToken,
@@ -297,7 +304,9 @@ export async function clearTelegramConfig(): Promise<TelegramConfigResult> {
297
304
  export async function setTelegramCommands(
298
305
  commands?: Array<{ command: string; description: string }>,
299
306
  ): Promise<TelegramConfigResult> {
300
- const storedToken = getSecureKey(credentialKey("telegram", "bot_token"));
307
+ const storedToken = await getSecureKeyAsync(
308
+ credentialKey("telegram", "bot_token"),
309
+ );
301
310
  if (!storedToken) {
302
311
  return {
303
312
  success: false,
@@ -398,7 +407,7 @@ export async function handleTelegramConfig(
398
407
  let result: TelegramConfigResult;
399
408
 
400
409
  if (msg.action === "get") {
401
- result = getTelegramConfig();
410
+ result = await getTelegramConfig();
402
411
  } else if (msg.action === "set") {
403
412
  result = await setTelegramConfig(msg.botToken);
404
413
  } else if (msg.action === "clear") {
@@ -1,324 +1,9 @@
1
- import {
2
- getAttachmentsForMessage,
3
- getFilePathForAttachment,
4
- setAttachmentThumbnail,
5
- } from "../../memory/attachments-store.js";
6
1
  import { getMessageById } from "../../memory/conversation-crud.js";
7
2
  import {
8
- getMessagesPaginated,
9
3
  listConversations,
10
4
  searchConversations,
11
5
  } from "../../memory/conversation-queries.js";
12
- import { silentlyWithLog } from "../../util/silently.js";
13
- import { truncate } from "../../util/truncate.js";
14
- import type {
15
- ConversationSearchRequest,
16
- HistoryRequest,
17
- MessageContentRequest,
18
- UserMessageAttachment,
19
- } from "../message-protocol.js";
20
- import { generateVideoThumbnail } from "../video-thumbnail.js";
21
- import {
22
- type HandlerContext,
23
- type HistorySurface,
24
- type HistoryToolCall,
25
- log,
26
- type ParsedHistoryMessage,
27
- renderHistoryContent,
28
- } from "./shared.js";
29
-
30
- /**
31
- * In light mode, strip heavy payloads (e.g. full HTML) from surface data
32
- * but preserve the fields the client needs to parse and render the surface.
33
- */
34
- function lightModeSurfaceData(s: HistorySurface): Record<string, unknown> {
35
- switch (s.surfaceType) {
36
- case "dynamic_page":
37
- return {
38
- ...(s.data.preview ? { preview: s.data.preview } : {}),
39
- ...(s.data.appId ? { appId: s.data.appId } : {}),
40
- };
41
- case "card":
42
- return {
43
- ...(typeof s.data.title === "string" ? { title: s.data.title } : {}),
44
- ...(typeof s.data.body === "string" ? { body: s.data.body } : {}),
45
- ...(typeof s.data.template === "string"
46
- ? { template: s.data.template }
47
- : {}),
48
- ...(s.data.templateData ? { templateData: s.data.templateData } : {}),
49
- };
50
- case "document_preview":
51
- return {
52
- ...(typeof s.data.surfaceId === "string"
53
- ? { surfaceId: s.data.surfaceId }
54
- : {}),
55
- ...(typeof s.data.title === "string" ? { title: s.data.title } : {}),
56
- ...(typeof s.data.content === "string"
57
- ? { content: s.data.content }
58
- : {}),
59
- };
60
- default:
61
- // For other types (list, table, form, confirmation, etc.),
62
- // preserve the full data — these are generally small.
63
- return s.data;
64
- }
65
- }
66
-
67
- export function handleHistoryRequest(
68
- msg: HistoryRequest,
69
- ctx: HandlerContext,
70
- ): void {
71
- // No limit means return all messages.
72
- const limit = msg.limit;
73
-
74
- // Resolve include flags: explicit flags override mode, mode provides defaults.
75
- // Default mode is 'light' when no mode and no include flags are specified.
76
- const isFullMode = msg.mode === "full";
77
- const includeAttachments = msg.includeAttachments ?? isFullMode;
78
- const includeToolImages = msg.includeToolImages ?? isFullMode;
79
- const includeSurfaceData = msg.includeSurfaceData ?? isFullMode;
80
-
81
- const { messages: dbMessages, hasMore } = getMessagesPaginated(
82
- msg.sessionId,
83
- limit,
84
- msg.beforeTimestamp,
85
- msg.beforeMessageId,
86
- );
87
-
88
- const parsed: ParsedHistoryMessage[] = dbMessages.map((m) => {
89
- let text = "";
90
- let toolCalls: HistoryToolCall[] = [];
91
- let toolCallsBeforeText = false;
92
- let textSegments: string[] = [];
93
- let contentOrder: string[] = [];
94
- let surfaces: HistorySurface[] = [];
95
- try {
96
- const content = JSON.parse(m.content);
97
- const rendered = renderHistoryContent(content);
98
- text = rendered.text;
99
- toolCalls = rendered.toolCalls;
100
- toolCallsBeforeText = rendered.toolCallsBeforeText;
101
- textSegments = rendered.textSegments;
102
- contentOrder = rendered.contentOrder;
103
- surfaces = rendered.surfaces;
104
- if (m.role === "assistant" && toolCalls.length > 0) {
105
- log.info(
106
- {
107
- messageId: m.id,
108
- toolCallCount: toolCalls.length,
109
- text: truncate(text, 100, ""),
110
- },
111
- "History message with tool calls",
112
- );
113
- }
114
- } catch (err) {
115
- log.debug(
116
- { err, messageId: m.id },
117
- "Failed to parse message content as JSON, using raw text",
118
- );
119
- text = m.content;
120
- textSegments = text ? [text] : [];
121
- contentOrder = text ? ["text:0"] : [];
122
- surfaces = [];
123
- }
124
- let subagentNotification: ParsedHistoryMessage["subagentNotification"];
125
- if (m.metadata) {
126
- try {
127
- subagentNotification = (
128
- JSON.parse(m.metadata) as {
129
- subagentNotification?: ParsedHistoryMessage["subagentNotification"];
130
- }
131
- ).subagentNotification;
132
- } catch (err) {
133
- log.debug(
134
- { err, messageId: m.id },
135
- "Failed to parse message metadata as JSON, ignoring",
136
- );
137
- }
138
- }
139
- return {
140
- id: m.id,
141
- role: m.role,
142
- text,
143
- timestamp: m.createdAt,
144
- toolCalls,
145
- toolCallsBeforeText,
146
- textSegments,
147
- contentOrder,
148
- surfaces,
149
- ...(subagentNotification ? { subagentNotification } : {}),
150
- };
151
- });
152
-
153
- const historyMessages = parsed.map((m) => {
154
- let attachments: UserMessageAttachment[] | undefined;
155
- if (m.role === "assistant" && m.id) {
156
- const linked = getAttachmentsForMessage(m.id);
157
- if (linked.length > 0) {
158
- if (includeAttachments) {
159
- // Full attachment data: same behavior as before
160
- const MAX_INLINE_B64_SIZE = 512 * 1024;
161
- attachments = linked.map((a) => {
162
- const isFileBacked = !a.dataBase64;
163
- const omit =
164
- isFileBacked ||
165
- (a.mimeType.startsWith("video/") &&
166
- a.dataBase64.length > MAX_INLINE_B64_SIZE);
167
-
168
- if (
169
- a.mimeType.startsWith("video/") &&
170
- !a.thumbnailBase64 &&
171
- a.dataBase64
172
- ) {
173
- const attachmentId = a.id;
174
- const base64 = a.dataBase64;
175
- silentlyWithLog(
176
- generateVideoThumbnail(base64).then((thumb) => {
177
- if (thumb) setAttachmentThumbnail(attachmentId, thumb);
178
- }),
179
- "video thumbnail generation",
180
- );
181
- }
182
-
183
- const fp = getFilePathForAttachment(a.id);
184
- return {
185
- id: a.id,
186
- filename: a.originalFilename,
187
- mimeType: a.mimeType,
188
- data: omit ? "" : a.dataBase64,
189
- ...(omit ? { sizeBytes: a.sizeBytes } : {}),
190
- ...(a.thumbnailBase64
191
- ? { thumbnailData: a.thumbnailBase64 }
192
- : {}),
193
- ...(fp ? { filePath: fp } : {}),
194
- };
195
- });
196
- } else {
197
- // Light mode: metadata only, strip base64 data
198
- attachments = linked.map((a) => {
199
- const fp = getFilePathForAttachment(a.id);
200
- return {
201
- id: a.id,
202
- filename: a.originalFilename,
203
- mimeType: a.mimeType,
204
- data: "",
205
- sizeBytes: a.sizeBytes,
206
- ...(a.thumbnailBase64
207
- ? { thumbnailData: a.thumbnailBase64 }
208
- : {}),
209
- ...(fp ? { filePath: fp } : {}),
210
- };
211
- });
212
- }
213
- }
214
- }
215
-
216
- // In light mode, strip imageData from tool calls
217
- const filteredToolCalls =
218
- m.toolCalls.length > 0
219
- ? includeToolImages
220
- ? m.toolCalls
221
- : m.toolCalls.map((tc) => {
222
- if (tc.imageData) {
223
- const { imageData: _, ...rest } = tc;
224
- return rest;
225
- }
226
- return tc;
227
- })
228
- : m.toolCalls;
229
-
230
- // In light mode, strip heavy payloads but keep essential fields so
231
- // the client can still parse and render surfaces (e.g. card title/body,
232
- // dynamic_page preview, document_preview metadata).
233
- const filteredSurfaces =
234
- m.surfaces.length > 0
235
- ? includeSurfaceData
236
- ? m.surfaces
237
- : m.surfaces.map((s) => ({
238
- surfaceId: s.surfaceId,
239
- surfaceType: s.surfaceType,
240
- title: s.title,
241
- data: lightModeSurfaceData(s),
242
- ...(s.actions ? { actions: s.actions } : {}),
243
- ...(s.display ? { display: s.display } : {}),
244
- }))
245
- : m.surfaces;
246
-
247
- // Apply text truncation when maxTextChars is set
248
- let wasTruncated = false;
249
- let textWasTruncated = false;
250
- let text = m.text;
251
- if (msg.maxTextChars !== undefined && text.length > msg.maxTextChars) {
252
- text = text.slice(0, msg.maxTextChars) + " \u2026 [truncated]";
253
- wasTruncated = true;
254
- textWasTruncated = true;
255
- }
256
-
257
- // Apply tool result truncation when maxToolResultChars is set
258
- const truncatedToolCalls =
259
- msg.maxToolResultChars !== undefined && filteredToolCalls.length > 0
260
- ? filteredToolCalls.map((tc) => {
261
- if (
262
- tc.result !== undefined &&
263
- tc.result.length > msg.maxToolResultChars!
264
- ) {
265
- wasTruncated = true;
266
- return {
267
- ...tc,
268
- result:
269
- tc.result.slice(0, msg.maxToolResultChars!) +
270
- " \u2026 [truncated]",
271
- };
272
- }
273
- return tc;
274
- })
275
- : filteredToolCalls;
276
-
277
- return {
278
- ...(m.id ? { id: m.id } : {}),
279
- role: m.role,
280
- text,
281
- timestamp: m.timestamp,
282
- ...(truncatedToolCalls.length > 0
283
- ? {
284
- toolCalls: truncatedToolCalls,
285
- toolCallsBeforeText: m.toolCallsBeforeText,
286
- }
287
- : {}),
288
- ...(attachments ? { attachments } : {}),
289
- ...(!textWasTruncated && m.textSegments.length > 0
290
- ? { textSegments: m.textSegments }
291
- : {}),
292
- ...(!textWasTruncated && m.contentOrder.length > 0
293
- ? { contentOrder: m.contentOrder }
294
- : {}),
295
- ...(filteredSurfaces.length > 0 ? { surfaces: filteredSurfaces } : {}),
296
- ...(m.subagentNotification
297
- ? { subagentNotification: m.subagentNotification }
298
- : {}),
299
- ...(wasTruncated ? { wasTruncated: true } : {}),
300
- };
301
- });
302
-
303
- const oldestTimestamp =
304
- historyMessages.length > 0 ? historyMessages[0].timestamp : undefined;
305
- // Provide the oldest message ID as a tie-breaker cursor so clients can
306
- // paginate without skipping same-millisecond messages at page boundaries.
307
- const oldestMessageId =
308
- historyMessages.length > 0 ? historyMessages[0].id : undefined;
309
-
310
- ctx.send({
311
- type: "history_response",
312
- sessionId: msg.sessionId,
313
- messages: historyMessages,
314
- hasMore,
315
- ...(oldestTimestamp !== undefined ? { oldestTimestamp } : {}),
316
- ...(oldestMessageId ? { oldestMessageId } : {}),
317
- });
318
-
319
- // Surfaces are now included directly in the history_response message (in the surfaces array),
320
- // so we no longer emit separate ui_surface_show messages during history loading.
321
- }
6
+ import { renderHistoryContent } from "./shared.js";
322
7
 
323
8
  // ---------------------------------------------------------------------------
324
9
  // Shared business logic (transport-agnostic)
@@ -400,45 +85,3 @@ export function getMessageContent(
400
85
  ...(toolCalls ? { toolCalls } : {}),
401
86
  };
402
87
  }
403
-
404
- // ---------------------------------------------------------------------------
405
- // HTTP handlers (delegate to shared logic)
406
- // ---------------------------------------------------------------------------
407
-
408
- export function handleConversationSearch(
409
- msg: ConversationSearchRequest,
410
- ctx: HandlerContext,
411
- ): void {
412
- const results = performConversationSearch({
413
- query: msg.query,
414
- limit: msg.limit,
415
- maxMessagesPerConversation: msg.maxMessagesPerConversation,
416
- });
417
- ctx.send({
418
- type: "conversation_search_response",
419
- query: msg.query,
420
- results,
421
- });
422
- }
423
-
424
- export function handleMessageContentRequest(
425
- msg: MessageContentRequest,
426
- ctx: HandlerContext,
427
- ): void {
428
- const result = getMessageContent(msg.messageId, msg.sessionId);
429
- if (!result) {
430
- ctx.send({
431
- type: "error",
432
- message: `Message ${msg.messageId} not found in session ${msg.sessionId}`,
433
- });
434
- return;
435
- }
436
-
437
- ctx.send({
438
- type: "message_content_response",
439
- sessionId: msg.sessionId,
440
- messageId: msg.messageId,
441
- ...(result.text !== undefined ? { text: result.text } : {}),
442
- ...(result.toolCalls ? { toolCalls: result.toolCalls } : {}),
443
- });
444
- }
@@ -176,22 +176,6 @@ function clampAttachmentText(text: string): string {
176
176
  return `${text.slice(0, HISTORY_ATTACHMENT_TEXT_LIMIT)}<truncated />`;
177
177
  }
178
178
 
179
- function renderImageBlockForHistory(block: Record<string, unknown>): string {
180
- const source = isRecord(block.source) ? block.source : null;
181
- const mediaType =
182
- source && typeof source.media_type === "string"
183
- ? source.media_type
184
- : "image/*";
185
- const sizeBytes =
186
- source && typeof source.data === "string"
187
- ? estimateBase64Bytes(source.data)
188
- : 0;
189
- if (sizeBytes <= 0) {
190
- return `[Image attachment] ${mediaType}`;
191
- }
192
- return `[Image attachment] ${mediaType}, ${formatBytes(sizeBytes)}`;
193
- }
194
-
195
179
  function renderFileBlockForHistory(block: Record<string, unknown>): string {
196
180
  const source = isRecord(block.source) ? block.source : null;
197
181
  const mediaType =
@@ -328,7 +312,9 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
328
312
  continue;
329
313
  }
330
314
  if (block.type === "image") {
331
- attachmentParts.push(renderImageBlockForHistory(block));
315
+ // Image data is sent as a separate attachment — skip the placeholder
316
+ // text so the client doesn't render both "[Image attachment]" and the
317
+ // actual image thumbnail.
332
318
  continue;
333
319
  }
334
320
  if (block.type === "tool_use") {
@@ -167,7 +167,14 @@ export async function runDaemon(): Promise<void> {
167
167
  // Backfill oauth_connection rows for manual-token providers (Telegram,
168
168
  // Slack channel) that already have keychain credentials from before the
169
169
  // oauth_connection migration. Safe to call on every startup.
170
- await backfillManualTokenConnections();
170
+ try {
171
+ await backfillManualTokenConnections();
172
+ } catch (err) {
173
+ log.warn(
174
+ { err },
175
+ "Manual-token connection backfill failed — continuing startup",
176
+ );
177
+ }
171
178
  log.info("Daemon startup: DB initialized");
172
179
 
173
180
  // Expire any pending canonical guardian requests left over from before
@@ -85,35 +85,6 @@ export interface ImageGenModelSetRequest {
85
85
  model: string;
86
86
  }
87
87
 
88
- export interface HistoryRequest {
89
- type: "history_request";
90
- sessionId: string;
91
- /** Max messages to return. When omitted, all messages are returned (unlimited). */
92
- limit?: number;
93
- /** Pagination cursor: return messages with timestamp before this value. */
94
- beforeTimestamp?: number;
95
- /** Pagination cursor tie-breaker: exclude this message ID when beforeTimestamp matches. */
96
- beforeMessageId?: string;
97
- /** Include attachment base64 data. Defaults to false in light mode. */
98
- includeAttachments?: boolean;
99
- /** Include tool screenshot base64 data. Defaults to false in light mode. */
100
- includeToolImages?: boolean;
101
- /** Include surface HTML payloads. Defaults to false in light mode. */
102
- includeSurfaceData?: boolean;
103
- /** Shorthand: 'light' = all include flags false (default), 'full' = all include flags true. */
104
- mode?: "light" | "full";
105
- /** Truncate message text fields beyond this character limit. When omitted, full text is returned. */
106
- maxTextChars?: number;
107
- /** Truncate tool result strings beyond this character limit. When omitted, full results are returned. */
108
- maxToolResultChars?: number;
109
- }
110
-
111
- export interface MessageContentRequest {
112
- type: "message_content_request";
113
- sessionId: string;
114
- messageId: string;
115
- }
116
-
117
88
  export interface MessageContentResponse {
118
89
  type: "message_content_response";
119
90
  sessionId: string;
@@ -145,16 +116,6 @@ export interface SessionsClearRequest {
145
116
  type: "sessions_clear";
146
117
  }
147
118
 
148
- export interface ConversationSearchRequest {
149
- type: "conversation_search";
150
- /** The search query string. */
151
- query: string;
152
- /** Maximum number of conversations to return. Defaults to 20. */
153
- limit?: number;
154
- /** Maximum number of matching messages to return per conversation. Defaults to 3. */
155
- maxMessagesPerConversation?: number;
156
- }
157
-
158
119
  export interface ReorderThreadsRequest {
159
120
  type: "reorder_threads";
160
121
  updates: Array<{
@@ -431,7 +392,6 @@ export type _SessionsClientMessages =
431
392
  | ModelGetRequest
432
393
  | ModelSetRequest
433
394
  | ImageGenModelSetRequest
434
- | HistoryRequest
435
395
  | UndoRequest
436
396
  | RegenerateRequest
437
397
  | UsageRequest
@@ -440,8 +400,6 @@ export type _SessionsClientMessages =
440
400
  | SessionSwitchRequest
441
401
  | SessionRenameRequest
442
402
  | SessionsClearRequest
443
- | ConversationSearchRequest
444
- | MessageContentRequest
445
403
  | ReorderThreadsRequest;
446
404
 
447
405
  export type _SessionsServerMessages =
@@ -58,7 +58,6 @@ import type { SkillOperationContext } from "./handlers/skills.js";
58
58
  import { HostBashProxy } from "./host-bash-proxy.js";
59
59
  import { HostCuProxy } from "./host-cu-proxy.js";
60
60
  import { HostFileProxy } from "./host-file-proxy.js";
61
- import { reloadMcpServers } from "./mcp-reload-service.js";
62
61
  import type { ServerMessage } from "./message-protocol.js";
63
62
  import {
64
63
  DEFAULT_MEMORY_POLICY,
@@ -393,11 +392,6 @@ export class DaemonServer {
393
392
  this.configWatcher.start(
394
393
  () => this.evictSessionsForReload(),
395
394
  () => this.broadcastIdentityChanged(),
396
- () => {
397
- reloadMcpServers().catch((err: unknown) => {
398
- log.error({ err }, "MCP reload triggered by config change failed");
399
- });
400
- },
401
395
  );
402
396
 
403
397
  // Broadcast contacts_changed to all clients when any contact mutation occurs.
@@ -166,7 +166,7 @@ function resolveProviderModelCommand(content: string): SlashResolution | null {
166
166
  if (provider !== "ollama" && !config.apiKeys[provider]) {
167
167
  return {
168
168
  kind: "unknown",
169
- message: `Cannot switch to ${displayName}. No API key configured for ${provider}.\n\nSet it with: \`config set apiKeys.${provider} <your-key>\``,
169
+ message: `Cannot switch to ${displayName}. No API key configured for ${provider}.\n\nSet it with: \`keys set ${provider} <your-key>\``,
170
170
  };
171
171
  }
172
172
 
@@ -216,9 +216,7 @@ function resolveModelList(): SlashResolution {
216
216
  }
217
217
 
218
218
  lines.push("\n✓ = API key configured, ✗ = not configured");
219
- lines.push(
220
- "\nTip: Configure a provider with `config set apiKeys.<provider> <key>`",
221
- );
219
+ lines.push("\nTip: Configure a provider with `keys set <provider> <key>`");
222
220
 
223
221
  return {
224
222
  kind: "unknown",
@@ -286,7 +284,7 @@ function resolveModelCommand(content: string): SlashResolution | null {
286
284
  const displayName = MODEL_DISPLAY_NAMES[matched] ?? matched;
287
285
  return {
288
286
  kind: "unknown",
289
- message: `Cannot switch to ${displayName}. No API key configured for Anthropic.\n\nSet it with: \`config set apiKeys.anthropic <your-key>\``,
287
+ message: `Cannot switch to ${displayName}. No API key configured for Anthropic.\n\nSet it with: \`keys set anthropic <your-key>\``,
290
288
  };
291
289
  }
292
290
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { getNestedValue, loadRawConfig } from "../../config/loader.js";
8
8
  import { credentialKey } from "../../security/credential-key.js";
9
- import { getSecureKey } from "../../security/secure-keys.js";
9
+ import { getSecureKeyAsync } from "../../security/secure-keys.js";
10
10
  import { ConfigError } from "../../util/errors.js";
11
11
  import type { EmailProvider } from "../provider.js";
12
12
 
@@ -47,7 +47,7 @@ export async function createProvider(
47
47
  const candidates = PROVIDER_KEY_MAP.agentmail;
48
48
  let apiKey: string | undefined;
49
49
  for (const account of candidates) {
50
- apiKey = getSecureKey(account);
50
+ apiKey = await getSecureKeyAsync(account);
51
51
  if (apiKey) break;
52
52
  }
53
53
  if (!apiKey) {
@@ -32,7 +32,7 @@ export async function generateAvatar(
32
32
 
33
33
  if (!credentials) {
34
34
  throw new ConfigError(
35
- "Gemini API key is not configured. Set it via `config set apiKeys.gemini <key>` or the GEMINI_API_KEY environment variable.",
35
+ "Gemini API key is not configured. Set it via `keys set gemini <key>` or the GEMINI_API_KEY environment variable.",
36
36
  );
37
37
  }
38
38