@vellumai/assistant 0.5.11 → 0.5.13

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 (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -1,8 +1,15 @@
1
- import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import {
2
+ copyFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ unlinkSync,
8
+ } from "node:fs";
2
9
  import { join } from "node:path";
3
10
 
4
11
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
5
- import { getBaseDataDir, getIsContainerized } from "../config/env-registry.js";
12
+ import { getIsContainerized } from "../config/env-registry.js";
6
13
  import { getConfig } from "../config/loader.js";
7
14
  import { skillFlagKey } from "../config/skill-state.js";
8
15
  import { loadSkillCatalog, type SkillSummary } from "../config/skills.js";
@@ -10,6 +17,7 @@ import { listConnections } from "../oauth/oauth-store.js";
10
17
  import { resolveBundledDir } from "../util/bundled-asset.js";
11
18
  import { getLogger } from "../util/logger.js";
12
19
  import {
20
+ getConversationsDir,
13
21
  getWorkspaceDir,
14
22
  getWorkspacePromptPath,
15
23
  isMacOS,
@@ -84,6 +92,24 @@ export function ensurePromptFiles(): void {
84
92
  }
85
93
  }
86
94
 
95
+ // Auto-delete stale BOOTSTRAP.md at startup. The model is instructed to
96
+ // delete it at the end of the first conversation, but if the user closes
97
+ // the app or starts a new thread before the model gets another turn, it
98
+ // never gets the chance. If BOOTSTRAP.md still exists but prior
99
+ // conversations are present, the onboarding window has passed — clean up.
100
+ const bootstrapCleanup = getWorkspacePromptPath("BOOTSTRAP.md");
101
+ if (!isFirstRun && existsSync(bootstrapCleanup)) {
102
+ const convDir = getConversationsDir();
103
+ try {
104
+ if (existsSync(convDir) && readdirSync(convDir).length > 0) {
105
+ unlinkSync(bootstrapCleanup);
106
+ log.info("Auto-deleted stale BOOTSTRAP.md — prior conversations exist");
107
+ }
108
+ } catch (err) {
109
+ log.warn({ err }, "Failed to auto-delete stale BOOTSTRAP.md");
110
+ }
111
+ }
112
+
87
113
  // Seed NOW.md scratchpad — always created if missing, regardless of whether
88
114
  // this is a fresh install or not. Kept out of PROMPT_FILES because NOW.md is
89
115
  // ephemeral state, not identity context.
@@ -171,6 +197,7 @@ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
171
197
  // System Permissions section removed — guidance lives in request_system_permission tool description.
172
198
  // Parallel Task Orchestration section removed — orchestration skill description + hints cover this.
173
199
  staticParts.push(buildAccessPreferenceSection(hasNoClient));
200
+ staticParts.push(buildCredentialSecuritySection());
174
201
  // Memory Persistence, Memory Recall, Workspace Reflection, Learning from Mistakes
175
202
  // sections removed — guidance lives in memory_manage/memory_recall tool descriptions
176
203
  // and the Proactive Workspace Editing subsection in Configuration.
@@ -206,7 +233,15 @@ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
206
233
  const userIsTemplate = isTemplateContent(user, "USER.md");
207
234
 
208
235
  if (identity && !identityIsTemplate) {
209
- dynamicParts.push(identity);
236
+ // Strip placeholder lines (e.g. "- **Name:** _(not yet chosen)_") so
237
+ // the model doesn't treat unresolved fields as prompts to ask the user.
238
+ const cleanedIdentity = identity
239
+ .split("\n")
240
+ .filter((line) => !/_\(not yet (?:chosen|established)\)_/.test(line))
241
+ .join("\n");
242
+ if (cleanedIdentity.trim()) {
243
+ dynamicParts.push(cleanedIdentity);
244
+ }
210
245
  }
211
246
  if (soul) dynamicParts.push(soul);
212
247
  if (options?.userPersona) dynamicParts.push(options.userPersona);
@@ -275,6 +310,8 @@ function buildInChatConfigurationSection(): string {
275
310
  "## In-Chat Configuration",
276
311
  "",
277
312
  "When the user needs to configure a value, collect it conversationally in the chat. Never direct the user to the Settings page for initial setup - Settings is for reviewing and updating existing configuration.",
313
+ "",
314
+ 'The Settings tabs are: General, Models & Services, Voice, Sounds, Permissions & Privacy, Billing, Archived Conversations, Schedules, Developer. There is NO "Integrations" tab — never refer to "Settings > Integrations". For API keys and provider configuration, the correct tab is "Models & Services".',
278
315
  ].join("\n");
279
316
  }
280
317
 
@@ -300,6 +337,14 @@ function buildAccessPreferenceSection(hasNoClient: boolean): string {
300
337
  ].join("\n");
301
338
  }
302
339
 
340
+ function buildCredentialSecuritySection(): string {
341
+ return [
342
+ "## Credential Security",
343
+ "",
344
+ 'Never ask users to share secrets (API keys, tokens, passwords, webhook secrets) in chat — secret messages may be blocked at ingress. Use the `credential_store` tool with `action: "prompt"` instead; it collects secrets through a secure UI that never exposes the value in the conversation. Non-secret values (Client IDs, Account SIDs, usernames) may be collected conversationally.',
345
+ ].join("\n");
346
+ }
347
+
303
348
  function buildIntegrationSection(): string {
304
349
  let connections: { providerKey: string; accountInfo?: string | null }[];
305
350
  try {
@@ -323,16 +368,16 @@ function buildIntegrationSection(): string {
323
368
  }
324
369
 
325
370
  function buildContainerizedSection(): string {
326
- const baseDataDir = getBaseDataDir() ?? "$BASE_DATA_DIR";
371
+ const workspaceDir = getWorkspaceDir();
327
372
  return [
328
373
  "## Running in a Container - Data Persistence",
329
374
  "",
330
- `You are running inside a container. Only the directory \`${baseDataDir}\` is mounted to a persistent volume.`,
375
+ `You are running inside a container. Only the directory \`${workspaceDir}\` is mounted to a persistent volume.`,
331
376
  "",
332
377
  "**Any new files or data you create MUST be written inside that directory, or they will be lost when the container restarts.**",
333
378
  "",
334
379
  "Rules:",
335
- `- Always store new data, notes, memories, configs, and downloads under \`${baseDataDir}\``,
380
+ `- Always store new data, notes, memories, configs, and downloads under \`${workspaceDir}\``,
336
381
  "- Never write persistent data to system directories, `/tmp`, or paths outside the mounted volume",
337
382
  "- When in doubt, prefer paths nested under the data directory",
338
383
  "- If you create a file that is only needed temporarily (scratch files, intermediate outputs, download staging), delete it when you are done - disk space on the persistent volume is finite and will grow unboundedly if temp files are not cleaned up",
@@ -435,9 +480,9 @@ function readPromptFile(path: string): string | null {
435
480
  * This is useful for injecting identity context into subsystems (e.g. memory
436
481
  * extraction) that run outside the main system prompt pipeline.
437
482
  */
438
- export function buildCoreIdentityContext(
439
- opts?: { userPersona?: string | null },
440
- ): string | null {
483
+ export function buildCoreIdentityContext(opts?: {
484
+ userPersona?: string | null;
485
+ }): string | null {
441
486
  const parts: string[] = [];
442
487
  for (const file of PROMPT_FILES) {
443
488
  const content = readPromptFile(getWorkspacePromptPath(file));
@@ -194,9 +194,13 @@ When saving to `USER.md`, mark declined fields so you don't re-ask later (e.g.,
194
194
 
195
195
  ## Saving What You Learn
196
196
 
197
- Save what you learn as you go. Update `IDENTITY.md` (name, nature, personality, style tendency) and `USER.md` (their name, how to address them, goals, locale, work role, hobbies, daily tools) using `file_edit`. If the conversation reveals how the user wants you to behave (e.g., "be direct," "don't be too chatty"), save those behavioral guidelines to `SOUL.md`.
197
+ **Call `file_edit` immediately whenever you learn something, in the same turn.** Don't batch saves for later. Don't wait until onboarding is "done." The moment the user gives you a name, call `file_edit` on IDENTITY.md in that same response. The moment you infer their communication style, call `file_edit` on SOUL.md. Every piece of information gets saved the turn you learn it.
198
198
 
199
- Do it quietly. Don't tell the user which files you're editing or mention tool names.
199
+ **The contents of IDENTITY.md, SOUL.md, and USER.md are already in your system prompt.** Use the exact text you see there for the `old_string` in `file_edit`. Do not guess or invent content that isn't in your context.
200
+
201
+ Update `IDENTITY.md` (name, nature, personality, style tendency) and `USER.md` (their name, how to address them, goals, locale, work role, hobbies, daily tools). If the conversation reveals how the user wants you to behave (e.g., "be direct," "don't be too chatty"), save those behavioral guidelines to `SOUL.md`.
202
+
203
+ Do it silently. Never tell the user you're saving, never mention file names or tool names. Just call `file_edit` alongside your normal response.
200
204
 
201
205
  When saving to `IDENTITY.md`, be specific about the tone, energy, and conversational style you discovered during onboarding. This file persists after onboarding, so everything about how you should come across needs to be captured there. Not just your name, but the full vibe: how you talk, how much energy you bring, whether you're blunt or gentle, funny or serious.
202
206
 
@@ -204,11 +208,18 @@ When saving to `SOUL.md`, also add an `## Identity Intro` section with a very sh
204
208
 
205
209
  ## Wrapping Up
206
210
 
207
- Once you've completed Phase 1 and made reasonable progress through Phase 2, you're done with onboarding. Use your best judgment on when the conversation has naturally moved past the bootstrap stage. There's no hard checklist. The goal is that the user feels set up and ready to work, not that every box is ticked.
211
+ **Always delete `BOOTSTRAP.md` at the end of this conversation, regardless of how far you got through the phases.** Onboarding is a one-shot opportunity. If the user skipped steps, partially completed them, or blazed through everything, it doesn't matter. Delete it. Never let a second conversation start with the first-run script.
212
+
213
+ Deletion triggers:
214
+ - The conversation is ending (user stops responding, says bye, moves on)
215
+ - The user completed Phase 2 (ideal case)
216
+ - The user named you but skipped other steps
217
+ - The user explicitly skipped onboarding ("skip", "not now", "later")
218
+ - The user ignored the onboarding pitch and just did tasks
208
219
 
209
- If you still haven't shown the two suggestions (Phase 2 step 4), do that before wrapping.
220
+ If onboarding was partial, that's fine. IDENTITY.md, SOUL.md, and USER.md persist. You can organically pick up incomplete personalization in future conversations by checking those files, without replaying the bootstrap script.
210
221
 
211
- When you're confident onboarding is complete, delete `BOOTSTRAP.md` so it doesn't re-trigger on the next conversation.
222
+ If you still haven't shown the two suggestions (Phase 2 step 4), try to fit them in before wrapping, but do NOT let that block deletion of BOOTSTRAP.md.
212
223
 
213
224
  ---
214
225
 
@@ -253,8 +253,7 @@ function expandCollapsedAssistantTurns(
253
253
 
254
254
  for (const block of content) {
255
255
  const type = (block as { type: string }).type;
256
- const isThinking =
257
- type === "thinking" || type === "redacted_thinking";
256
+ const isThinking = type === "thinking" || type === "redacted_thinking";
258
257
 
259
258
  if (isThinking && segmentHasToolUse) {
260
259
  segments.push(current);
@@ -310,10 +309,7 @@ function expandCollapsedAssistantTurns(
310
309
  // tool_results that were already distributed to intermediate segments.
311
310
  if (nextIsUser) {
312
311
  const remainingResults = Array.from(toolResultMap.values());
313
- const rebuiltUserContent = [
314
- ...remainingResults,
315
- ...nonToolResultContent,
316
- ];
312
+ const rebuiltUserContent = [...remainingResults, ...nonToolResultContent];
317
313
  // Replace the original user message with the rebuilt one
318
314
  result.push({
319
315
  role: "user" as const,
@@ -949,10 +945,6 @@ export class AnthropicProvider implements Provider {
949
945
  signal: timeoutSignal,
950
946
  }) as unknown as UnifiedStream;
951
947
 
952
- // Track whether we've seen a text content block so we can insert a
953
- // separator between consecutive text blocks in the same response.
954
- let hasSeenTextBlock = false;
955
-
956
948
  stream.on("text", (text) => {
957
949
  onEvent?.({ type: "text_delta", text });
958
950
  });
@@ -969,29 +961,6 @@ export class AnthropicProvider implements Provider {
969
961
  let pendingInputJsonFlush: ReturnType<typeof setTimeout> | undefined;
970
962
 
971
963
  stream.on("streamEvent", (event) => {
972
- // Insert a space separator when a new text content block starts
973
- // after a previous one, so consecutive text blocks don't get
974
- // concatenated without whitespace (e.g. "sentence.NextSentence").
975
- // Uses a space instead of \n because the client's MarkdownRenderer
976
- // can collapse soft line breaks (\n) within a paragraph.
977
- if (
978
- event.type === "content_block_start" &&
979
- event.content_block.type === "text"
980
- ) {
981
- if (hasSeenTextBlock) {
982
- onEvent?.({ type: "text_delta", text: " " });
983
- }
984
- hasSeenTextBlock = true;
985
- } else if (
986
- event.type === "content_block_start" &&
987
- event.content_block.type === "tool_use"
988
- ) {
989
- // Reset only for client-side tool_use blocks, which create visual
990
- // separators in the UI. Server-side tool blocks (server_tool_use,
991
- // web_search_tool_result) are transparent in the text stream and
992
- // need the space preserved between surrounding text blocks.
993
- hasSeenTextBlock = false;
994
- }
995
964
  if (
996
965
  event.type === "content_block_start" &&
997
966
  event.content_block.type === "tool_use"
@@ -42,7 +42,7 @@ export interface ProcessGuardianDecisionParams {
42
42
  }
43
43
 
44
44
  export type ProcessGuardianDecisionResult =
45
- | { ok: true; applied: true; requestId: string }
45
+ | { ok: true; applied: true; requestId: string; replyText?: string }
46
46
  | {
47
47
  ok: true;
48
48
  applied: false;
@@ -116,7 +116,12 @@ export async function processGuardianDecision(
116
116
  };
117
117
  }
118
118
 
119
- return { ok: true, applied: true, requestId: canonicalResult.requestId };
119
+ return {
120
+ ok: true,
121
+ applied: true,
122
+ requestId: canonicalResult.requestId,
123
+ replyText: canonicalResult.resolverReplyText,
124
+ };
120
125
  }
121
126
 
122
127
  return {
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  import { existsSync, readFileSync } from "node:fs";
9
- import { homedir } from "node:os";
10
9
  import { join, resolve } from "node:path";
11
10
 
12
11
  import type { ServerWebSocket } from "bun";
@@ -28,13 +27,11 @@ import {
28
27
  handleVoiceWebhook,
29
28
  } from "../calls/twilio-routes.js";
30
29
  import { parseChannelId } from "../channels/types.js";
31
- import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
32
30
  import {
33
31
  getGatewayInternalBaseUrl,
34
32
  hasUngatedHttpAuthDisabled,
35
33
  isHttpAuthDisabled,
36
34
  } from "../config/env.js";
37
- import { getConfig } from "../config/loader.js";
38
35
  import type { ServerMessage } from "../daemon/message-protocol.js";
39
36
  import { PairingStore } from "../daemon/pairing-store.js";
40
37
  import {
@@ -64,6 +61,7 @@ import {
64
61
  } from "../security/oauth-callback-registry.js";
65
62
  import { UserError } from "../util/errors.js";
66
63
  import { getLogger } from "../util/logger.js";
64
+ import { getRootDir } from "../util/platform.js";
67
65
  import { buildAssistantEvent } from "./assistant-event.js";
68
66
  import { assistantEventHub } from "./assistant-event-hub.js";
69
67
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
@@ -251,6 +249,7 @@ export class RuntimeHttpServer {
251
249
  private getWatchDeps?: RuntimeHttpServerOptions["getWatchDeps"];
252
250
  private getRecordingDeps?: RuntimeHttpServerOptions["getRecordingDeps"];
253
251
  private getCesClient?: RuntimeHttpServerOptions["getCesClient"];
252
+ private onProviderCredentialsChanged?: RuntimeHttpServerOptions["onProviderCredentialsChanged"];
254
253
  private getHeartbeatService?: RuntimeHttpServerOptions["getHeartbeatService"];
255
254
  private router: HttpRouter;
256
255
 
@@ -274,6 +273,7 @@ export class RuntimeHttpServer {
274
273
  this.getWatchDeps = options.getWatchDeps;
275
274
  this.getRecordingDeps = options.getRecordingDeps;
276
275
  this.getCesClient = options.getCesClient;
276
+ this.onProviderCredentialsChanged = options.onProviderCredentialsChanged;
277
277
  this.getHeartbeatService = options.getHeartbeatService;
278
278
  this.router = new HttpRouter(this.buildRouteTable());
279
279
  }
@@ -296,8 +296,7 @@ export class RuntimeHttpServer {
296
296
  /** Read the feature-flag client token from disk so it can be included in pairing approval responses. */
297
297
  private readFeatureFlagToken(): string | undefined {
298
298
  try {
299
- const baseDir = process.env.BASE_DATA_DIR?.trim() || homedir();
300
- const tokenPath = join(baseDir, ".vellum", "feature-flag-token");
299
+ const tokenPath = join(getRootDir(), "feature-flag-token");
301
300
  const token = readFileSync(tokenPath, "utf-8").trim();
302
301
  return token || undefined;
303
302
  } catch {
@@ -944,6 +943,7 @@ export class RuntimeHttpServer {
944
943
  ...appManagementRouteDefinitions(),
945
944
  ...secretRouteDefinitions({
946
945
  getCesClient: this.getCesClient,
946
+ onProviderCredentialsChanged: this.onProviderCredentialsChanged,
947
947
  }),
948
948
  ...identityRouteDefinitions(),
949
949
  ...upgradeBroadcastRouteDefinitions(),
@@ -1028,17 +1028,11 @@ export class RuntimeHttpServer {
1028
1028
  handler: ({ url }) => {
1029
1029
  const limit = Number(url.searchParams.get("limit") ?? 50);
1030
1030
  const offset = Number(url.searchParams.get("offset") ?? 0);
1031
- const includeBackground = isAssistantFeatureFlagEnabled(
1032
- "show-background-conversations",
1033
- getConfig(),
1034
- );
1035
- const conversations = listConversations(
1036
- limit,
1037
- includeBackground,
1038
- offset,
1039
- );
1040
- const totalCount = countConversations(includeBackground);
1041
- const conversationIds = conversations.map((c) => c.id);
1031
+ const backgroundOnly =
1032
+ url.searchParams.get("conversationType") === "background";
1033
+ const rows = listConversations(limit, backgroundOnly, offset);
1034
+ const totalCount = countConversations(backgroundOnly);
1035
+ const conversationIds = rows.map((c) => c.id);
1042
1036
  const displayMeta = getDisplayMetaForConversations(conversationIds);
1043
1037
  const bindings =
1044
1038
  externalConversationStore.getBindingsForConversations(
@@ -1048,7 +1042,7 @@ export class RuntimeHttpServer {
1048
1042
  getAttentionStateByConversationIds(conversationIds);
1049
1043
  const parentCache = new Map<string, ConversationRow | null>();
1050
1044
  return Response.json({
1051
- conversations: conversations.map((conversation) =>
1045
+ conversations: rows.map((conversation) =>
1052
1046
  this.serializeConversationSummary({
1053
1047
  conversation,
1054
1048
  binding: bindings.get(conversation.id),
@@ -1057,7 +1051,7 @@ export class RuntimeHttpServer {
1057
1051
  parentCache,
1058
1052
  }),
1059
1053
  ),
1060
- hasMore: offset + conversations.length < totalCount,
1054
+ hasMore: offset + rows.length < totalCount,
1061
1055
  });
1062
1056
  },
1063
1057
  },
@@ -228,8 +228,15 @@ export interface RuntimeHttpServerOptions {
228
228
  getRecordingDeps?: () => import("./routes/recording-routes.js").RecordingDeps;
229
229
  /** Accessor for the CES client, used to push API key updates to CES after hatch. */
230
230
  getCesClient?: () => CesClient | undefined;
231
+ /**
232
+ * Called after provider-affecting credentials reload so live conversations
233
+ * can be recreated with fresh provider instances.
234
+ */
235
+ onProviderCredentialsChanged?: () => void | Promise<void>;
231
236
  /** Accessor for the heartbeat service (for run-now and config routes). */
232
- getHeartbeatService?: () => import("../heartbeat/heartbeat-service.js").HeartbeatService | undefined;
237
+ getHeartbeatService?: () =>
238
+ | import("../heartbeat/heartbeat-service.js").HeartbeatService
239
+ | undefined;
233
240
  }
234
241
 
235
242
  export interface RuntimeAttachmentMetadata {
@@ -106,7 +106,7 @@ const TASK_DEFINITIONS: readonly TaskDefinition[] = [
106
106
  "Secrets are redacted in export bundles for security. Re-enter all API keys, tokens, and credentials in the destination instance.",
107
107
  required: true,
108
108
  helpText:
109
- "Navigate to Settings > Integrations to re-enter provider API keys (e.g., Anthropic, OpenAI). Check Settings > Secrets for any custom secrets used by skills.",
109
+ "Navigate to Settings > Models & Services to re-enter provider API keys (e.g., Anthropic, OpenAI). Check Settings > Models & Services for any custom secrets used by skills.",
110
110
  },
111
111
  {
112
112
  id: "rebind-channels",
@@ -133,7 +133,7 @@ const TASK_DEFINITIONS: readonly TaskDefinition[] = [
133
133
  "Ensure all webhook URLs registered with external services point to the new instance's public ingress URL.",
134
134
  required: false,
135
135
  helpText:
136
- "Review the public ingress URL in Settings > Gateway. Update any external services (GitHub, calendar providers, etc.) that send webhooks to this assistant.",
136
+ "Review the public ingress URL in Settings > Developer. Update any external services (GitHub, calendar providers, etc.) that send webhooks to this assistant.",
137
137
  },
138
138
  ] as const;
139
139
 
@@ -18,7 +18,9 @@ import { z } from "zod";
18
18
 
19
19
  import {
20
20
  batchSetDisplayOrders,
21
+ countConversationsByScheduleJobId,
21
22
  deleteConversation,
23
+ getConversation,
22
24
  PRIVATE_CONVERSATION_FORK_ERROR,
23
25
  wipeConversation,
24
26
  } from "../../memory/conversation-crud.js";
@@ -29,6 +31,7 @@ import {
29
31
  setConversationKeyIfAbsent,
30
32
  } from "../../memory/conversation-key-store.js";
31
33
  import { enqueueMemoryJob } from "../../memory/jobs-store.js";
34
+ import { deleteSchedule } from "../../schedule/schedule-store.js";
32
35
  import { UserError } from "../../util/errors.js";
33
36
  import { getLogger } from "../../util/logger.js";
34
37
  import { httpError } from "../http-errors.js";
@@ -318,6 +321,20 @@ export function conversationManagementRouteDefinitions(
318
321
  404,
319
322
  );
320
323
  }
324
+
325
+ // Cancel the associated schedule job (if any) before wiping the
326
+ // conversation — but only when this is the last conversation that
327
+ // references the schedule. Recurring schedules create a new
328
+ // conversation per run, so we must not cancel the schedule when
329
+ // earlier run conversations are cleaned up.
330
+ const conv = getConversation(resolvedId);
331
+ if (
332
+ conv?.scheduleJobId &&
333
+ countConversationsByScheduleJobId(conv.scheduleJobId) <= 1
334
+ ) {
335
+ deleteSchedule(conv.scheduleJobId);
336
+ }
337
+
321
338
  deps.destroyConversation(resolvedId);
322
339
  const result = wipeConversation(resolvedId);
323
340
  // Enqueue Qdrant vector cleanup jobs
@@ -372,6 +389,20 @@ export function conversationManagementRouteDefinitions(
372
389
  404,
373
390
  );
374
391
  }
392
+
393
+ // Cancel the associated schedule job (if any) before deleting the
394
+ // conversation — but only when this is the last conversation that
395
+ // references the schedule. Recurring schedules create a new
396
+ // conversation per run, so we must not cancel the schedule when
397
+ // earlier run conversations are cleaned up.
398
+ const conv = getConversation(resolvedId);
399
+ if (
400
+ conv?.scheduleJobId &&
401
+ countConversationsByScheduleJobId(conv.scheduleJobId) <= 1
402
+ ) {
403
+ deleteSchedule(conv.scheduleJobId);
404
+ }
405
+
375
406
  // Tear down the in-memory conversation (abort + dispose) before removing
376
407
  // persistence so that a running agent loop doesn't write to a deleted
377
408
  // conversation row, tripping FK constraints.
@@ -48,7 +48,10 @@ import {
48
48
  } from "../../memory/canonical-guardian-store.js";
49
49
  import {
50
50
  addMessage,
51
+ getLastAssistantTimestampBefore,
51
52
  getMessages,
53
+ getMessagesPaginated,
54
+ type MessageRow,
52
55
  provenanceFromTrustContext,
53
56
  setConversationOriginChannelIfUnset,
54
57
  setConversationOriginInterfaceIfUnset,
@@ -360,7 +363,49 @@ export function handleListMessages(
360
363
  if (!resolvedConversationId) {
361
364
  return Response.json({ messages: [] });
362
365
  }
363
- const rawMessages = getMessages(resolvedConversationId);
366
+
367
+ const beforeTimestampRaw = url.searchParams.get("beforeTimestamp");
368
+ const limitRaw = url.searchParams.get("limit");
369
+
370
+ // Validate: reject NaN values with 400
371
+ if (beforeTimestampRaw !== null && isNaN(Number(beforeTimestampRaw))) {
372
+ return httpError(
373
+ "BAD_REQUEST",
374
+ "beforeTimestamp must be a valid number",
375
+ 400,
376
+ );
377
+ }
378
+ if (limitRaw !== null && isNaN(Number(limitRaw))) {
379
+ return httpError("BAD_REQUEST", "limit must be a valid number", 400);
380
+ }
381
+
382
+ const beforeTimestamp = beforeTimestampRaw
383
+ ? Number(beforeTimestampRaw)
384
+ : undefined;
385
+ // Clamp limit to 1-500 range
386
+ const limit = limitRaw
387
+ ? Math.min(Math.max(Math.floor(Number(limitRaw)), 1), 500)
388
+ : undefined;
389
+
390
+ // Option A: only paginate when beforeTimestamp is present.
391
+ // Initial load and reconnect send limit but no beforeTimestamp — those must continue
392
+ // returning all messages for zero regression risk.
393
+ const isPaginated = beforeTimestamp != null;
394
+
395
+ let rawMessages: MessageRow[];
396
+ let hasMore = false;
397
+
398
+ if (isPaginated) {
399
+ const result = getMessagesPaginated(
400
+ resolvedConversationId,
401
+ limit,
402
+ beforeTimestamp,
403
+ );
404
+ rawMessages = result.messages;
405
+ hasMore = result.hasMore;
406
+ } else {
407
+ rawMessages = getMessages(resolvedConversationId);
408
+ }
364
409
 
365
410
  // Parse content blocks and extract text + tool calls
366
411
  const parsed = rawMessages.map((msg) => {
@@ -429,6 +474,12 @@ export function handleListMessages(
429
474
  const interfaceFiles = getInterfaceFilesWithMtimes(interfacesDir);
430
475
 
431
476
  let prevAssistantTimestamp = 0;
477
+ if (isPaginated && rawMessages.length > 0) {
478
+ prevAssistantTimestamp = getLastAssistantTimestampBefore(
479
+ resolvedConversationId!,
480
+ rawMessages[0].createdAt,
481
+ );
482
+ }
432
483
  const messages: RuntimeMessagePayload[] = parsed.map((m) => {
433
484
  let msgAttachments: RuntimeAttachmentMetadata[] = [];
434
485
  if (m.id) {
@@ -498,6 +549,19 @@ export function handleListMessages(
498
549
  };
499
550
  });
500
551
 
552
+ if (isPaginated) {
553
+ const oldestTimestamp =
554
+ rawMessages.length > 0 ? rawMessages[0].createdAt : undefined;
555
+ const oldestMessageId =
556
+ rawMessages.length > 0 ? rawMessages[0].id : undefined;
557
+ return Response.json({
558
+ messages,
559
+ hasMore,
560
+ ...(oldestTimestamp != null ? { oldestTimestamp } : {}),
561
+ ...(oldestMessageId != null ? { oldestMessageId } : {}),
562
+ });
563
+ }
564
+
501
565
  return Response.json({ messages });
502
566
  }
503
567
 
@@ -1502,9 +1566,20 @@ export function conversationRouteDefinitions(deps: {
1502
1566
  tags: ["messages"],
1503
1567
  responseBody: z.object({
1504
1568
  messages: z.array(z.unknown()).describe("Array of message objects"),
1505
- interfaceFiles: z
1506
- .array(z.unknown())
1507
- .describe("Interface file paths with modification timestamps"),
1569
+ hasMore: z
1570
+ .boolean()
1571
+ .optional()
1572
+ .describe("Whether older messages exist beyond this page"),
1573
+ oldestTimestamp: z
1574
+ .number()
1575
+ .optional()
1576
+ .describe(
1577
+ "Timestamp of the oldest message in this page (ms since epoch)",
1578
+ ),
1579
+ oldestMessageId: z
1580
+ .string()
1581
+ .optional()
1582
+ .describe("ID of the oldest message in this page"),
1508
1583
  }),
1509
1584
  handler: ({ url }) => handleListMessages(url, deps.interfacesDir),
1510
1585
  },
@@ -121,7 +121,11 @@ export async function handleGuardianActionDecision(
121
121
  return httpError("BAD_REQUEST", result.message, 400);
122
122
  }
123
123
  if (result.applied) {
124
- return Response.json({ applied: true, requestId: result.requestId });
124
+ return Response.json({
125
+ applied: true,
126
+ requestId: result.requestId,
127
+ ...(result.replyText ? { replyText: result.replyText } : {}),
128
+ });
125
129
  }
126
130
  return result.reason === "not_found"
127
131
  ? httpError(
@@ -297,7 +301,16 @@ export function guardianActionRouteDefinitions(): RouteDefinition[] {
297
301
  responseBody: z.object({
298
302
  applied: z.boolean(),
299
303
  requestId: z.string(),
300
- reason: z.string(),
304
+ reason: z
305
+ .string()
306
+ .optional()
307
+ .describe("Decline reason (present only when applied is false)"),
308
+ replyText: z
309
+ .string()
310
+ .optional()
311
+ .describe(
312
+ "Resolver reply text for the guardian (e.g. verification code)",
313
+ ),
301
314
  }),
302
315
  handler: async ({ req, authContext }) =>
303
316
  handleGuardianActionDecision(req, authContext),