@vellumai/assistant 0.4.52 → 0.4.53

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 (205) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/keychain-broker.md +6 -20
  3. package/docs/architecture/memory.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/approval-cascade.test.ts +3 -1
  6. package/src/__tests__/approval-routes-http.test.ts +0 -1
  7. package/src/__tests__/asset-materialize-tool.test.ts +0 -1
  8. package/src/__tests__/asset-search-tool.test.ts +0 -1
  9. package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
  10. package/src/__tests__/attachments-store.test.ts +0 -1
  11. package/src/__tests__/avatar-e2e.test.ts +6 -1
  12. package/src/__tests__/browser-fill-credential.test.ts +3 -0
  13. package/src/__tests__/btw-routes.test.ts +39 -0
  14. package/src/__tests__/call-controller.test.ts +0 -1
  15. package/src/__tests__/call-domain.test.ts +1 -0
  16. package/src/__tests__/call-routes-http.test.ts +1 -2
  17. package/src/__tests__/canonical-guardian-store.test.ts +33 -2
  18. package/src/__tests__/channel-readiness-service.test.ts +1 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +6 -2
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +7 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +1 -2
  22. package/src/__tests__/config-schema.test.ts +6 -37
  23. package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
  24. package/src/__tests__/credential-broker-server-use.test.ts +16 -16
  25. package/src/__tests__/credential-security-invariants.test.ts +14 -0
  26. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  27. package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
  28. package/src/__tests__/gateway-only-enforcement.test.ts +0 -2
  29. package/src/__tests__/host-shell-tool.test.ts +0 -1
  30. package/src/__tests__/http-user-message-parity.test.ts +19 -0
  31. package/src/__tests__/list-messages-attachments.test.ts +0 -1
  32. package/src/__tests__/log-export-workspace.test.ts +233 -0
  33. package/src/__tests__/managed-proxy-context.test.ts +1 -1
  34. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  35. package/src/__tests__/media-generate-image.test.ts +7 -2
  36. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -1
  37. package/src/__tests__/memory-regressions.test.ts +0 -1
  38. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  39. package/src/__tests__/migration-export-http.test.ts +0 -1
  40. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  41. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  42. package/src/__tests__/migration-validate-http.test.ts +0 -1
  43. package/src/__tests__/notification-schedule-dedup.test.ts +237 -0
  44. package/src/__tests__/oauth-cli.test.ts +1 -10
  45. package/src/__tests__/oauth-store.test.ts +3 -5
  46. package/src/__tests__/oauth2-gateway-transport.test.ts +5 -4
  47. package/src/__tests__/onboarding-starter-tasks.test.ts +1 -1
  48. package/src/__tests__/onboarding-template-contract.test.ts +1 -2
  49. package/src/__tests__/pricing.test.ts +0 -11
  50. package/src/__tests__/provider-commit-message-generator.test.ts +21 -14
  51. package/src/__tests__/provider-fail-open-selection.test.ts +9 -8
  52. package/src/__tests__/provider-managed-proxy-integration.test.ts +27 -24
  53. package/src/__tests__/provider-registry-ollama.test.ts +8 -2
  54. package/src/__tests__/recording-handler.test.ts +0 -1
  55. package/src/__tests__/relay-server.test.ts +0 -1
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  57. package/src/__tests__/runtime-events-sse-parity.test.ts +0 -1
  58. package/src/__tests__/runtime-events-sse.test.ts +0 -1
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -1
  60. package/src/__tests__/secret-scanner-executor.test.ts +0 -1
  61. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  62. package/src/__tests__/session-abort-tool-results.test.ts +3 -1
  63. package/src/__tests__/session-agent-loop-overflow.test.ts +1012 -838
  64. package/src/__tests__/session-agent-loop.test.ts +2 -2
  65. package/src/__tests__/session-confirmation-signals.test.ts +3 -1
  66. package/src/__tests__/session-error.test.ts +5 -4
  67. package/src/__tests__/session-history-web-search.test.ts +34 -9
  68. package/src/__tests__/session-pre-run-repair.test.ts +3 -1
  69. package/src/__tests__/session-provider-retry-repair.test.ts +31 -26
  70. package/src/__tests__/session-queue.test.ts +3 -1
  71. package/src/__tests__/session-runtime-assembly.test.ts +118 -0
  72. package/src/__tests__/session-slash-known.test.ts +31 -13
  73. package/src/__tests__/session-slash-queue.test.ts +3 -1
  74. package/src/__tests__/session-slash-unknown.test.ts +3 -1
  75. package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
  76. package/src/__tests__/session-workspace-injection.test.ts +3 -1
  77. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -1
  78. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  79. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  80. package/src/__tests__/skillssh-registry.test.ts +21 -0
  81. package/src/__tests__/slack-share-routes.test.ts +1 -1
  82. package/src/__tests__/swarm-recursion.test.ts +5 -1
  83. package/src/__tests__/swarm-session-integration.test.ts +25 -14
  84. package/src/__tests__/swarm-tool.test.ts +5 -2
  85. package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
  86. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1521 -0
  87. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/trust-store.test.ts +5 -1
  92. package/src/__tests__/twilio-routes.test.ts +2 -2
  93. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  94. package/src/__tests__/voice-quality.test.ts +2 -1
  95. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  96. package/src/__tests__/web-search.test.ts +1 -1
  97. package/src/agent/loop.ts +17 -1
  98. package/src/bundler/app-bundler.ts +40 -24
  99. package/src/calls/call-controller.ts +16 -0
  100. package/src/calls/relay-server.ts +29 -13
  101. package/src/calls/voice-control-protocol.ts +1 -0
  102. package/src/calls/voice-quality.ts +1 -1
  103. package/src/calls/voice-session-bridge.ts +9 -3
  104. package/src/channels/types.ts +16 -0
  105. package/src/cli/commands/bash.ts +173 -0
  106. package/src/cli/commands/doctor.ts +5 -23
  107. package/src/cli/commands/oauth/connections.ts +4 -2
  108. package/src/cli/commands/oauth/providers.ts +1 -13
  109. package/src/cli/program.ts +2 -0
  110. package/src/cli/reference.ts +1 -0
  111. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -1
  112. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +3 -5
  113. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -3
  114. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +1 -1
  115. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
  116. package/src/config/feature-flag-registry.json +8 -0
  117. package/src/config/loader.ts +7 -135
  118. package/src/config/schema.ts +0 -6
  119. package/src/config/schemas/channels.ts +1 -0
  120. package/src/config/schemas/elevenlabs.ts +2 -2
  121. package/src/contacts/contact-store.ts +21 -25
  122. package/src/contacts/contacts-write.ts +6 -6
  123. package/src/contacts/types.ts +2 -0
  124. package/src/context/token-estimator.ts +35 -2
  125. package/src/context/window-manager.ts +16 -2
  126. package/src/daemon/config-watcher.ts +24 -6
  127. package/src/daemon/context-overflow-reducer.ts +13 -2
  128. package/src/daemon/handlers/config-ingress.ts +25 -8
  129. package/src/daemon/handlers/config-model.ts +21 -15
  130. package/src/daemon/handlers/config-telegram.ts +18 -6
  131. package/src/daemon/handlers/dictation.ts +0 -429
  132. package/src/daemon/handlers/skills.ts +1 -200
  133. package/src/daemon/lifecycle.ts +8 -5
  134. package/src/daemon/message-types/contacts.ts +2 -0
  135. package/src/daemon/message-types/integrations.ts +1 -0
  136. package/src/daemon/message-types/sessions.ts +2 -0
  137. package/src/daemon/parse-actual-tokens-from-error.test.ts +75 -0
  138. package/src/daemon/server.ts +23 -2
  139. package/src/daemon/session-agent-loop-handlers.ts +1 -1
  140. package/src/daemon/session-agent-loop.ts +27 -79
  141. package/src/daemon/session-error.ts +5 -4
  142. package/src/daemon/session-process.ts +17 -10
  143. package/src/daemon/session-runtime-assembly.ts +50 -0
  144. package/src/daemon/session-slash.ts +32 -20
  145. package/src/daemon/session.ts +1 -0
  146. package/src/events/domain-events.ts +1 -0
  147. package/src/media/app-icon-generator.ts +2 -1
  148. package/src/media/avatar-router.ts +3 -2
  149. package/src/memory/canonical-guardian-store.ts +25 -3
  150. package/src/memory/db-init.ts +12 -0
  151. package/src/memory/embedding-backend.ts +25 -16
  152. package/src/memory/migrations/158-channel-interaction-columns.ts +18 -0
  153. package/src/memory/migrations/159-drop-contact-interaction-columns.ts +16 -0
  154. package/src/memory/migrations/160-drop-loopback-port-column.ts +13 -0
  155. package/src/memory/migrations/index.ts +3 -0
  156. package/src/memory/retriever.test.ts +19 -12
  157. package/src/memory/schema/contacts.ts +2 -2
  158. package/src/memory/schema/oauth.ts +0 -1
  159. package/src/oauth/connect-orchestrator.ts +5 -3
  160. package/src/oauth/connect-types.ts +9 -2
  161. package/src/oauth/manual-token-connection.ts +9 -7
  162. package/src/oauth/oauth-store.ts +2 -8
  163. package/src/oauth/provider-behaviors.ts +10 -0
  164. package/src/oauth/seed-providers.ts +13 -5
  165. package/src/permissions/checker.ts +20 -1
  166. package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
  167. package/src/prompts/system-prompt.ts +2 -11
  168. package/src/prompts/templates/BOOTSTRAP.md +1 -3
  169. package/src/providers/anthropic/client.ts +16 -8
  170. package/src/providers/managed-proxy/constants.ts +1 -1
  171. package/src/providers/registry.ts +21 -15
  172. package/src/providers/types.ts +1 -1
  173. package/src/runtime/auth/route-policy.ts +4 -0
  174. package/src/runtime/channel-invite-transports/telegram.ts +12 -6
  175. package/src/runtime/channel-retry-sweep.ts +6 -0
  176. package/src/runtime/http-types.ts +1 -0
  177. package/src/runtime/middleware/error-handler.ts +1 -2
  178. package/src/runtime/routes/app-management-routes.ts +1 -0
  179. package/src/runtime/routes/btw-routes.ts +20 -1
  180. package/src/runtime/routes/conversation-routes.ts +32 -13
  181. package/src/runtime/routes/inbound-message-handler.ts +10 -2
  182. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  183. package/src/runtime/routes/inbound-stages/edit-intercept.ts +5 -5
  184. package/src/runtime/routes/integrations/slack/share.ts +5 -5
  185. package/src/runtime/routes/log-export-routes.ts +122 -10
  186. package/src/runtime/routes/session-query-routes.ts +3 -3
  187. package/src/runtime/routes/settings-routes.ts +53 -0
  188. package/src/runtime/routes/workspace-routes.ts +3 -0
  189. package/src/runtime/verification-templates.ts +1 -1
  190. package/src/security/oauth2.ts +4 -4
  191. package/src/security/secure-keys.ts +4 -4
  192. package/src/signals/bash.ts +157 -0
  193. package/src/skills/skillssh-registry.ts +6 -1
  194. package/src/swarm/backend-claude-code.ts +6 -6
  195. package/src/swarm/worker-backend.ts +1 -1
  196. package/src/swarm/worker-runner.ts +1 -1
  197. package/src/telegram/bot-username.ts +11 -0
  198. package/src/tools/claude-code/claude-code.ts +4 -4
  199. package/src/tools/credentials/broker.ts +7 -5
  200. package/src/tools/credentials/vault.ts +3 -2
  201. package/src/tools/network/__tests__/web-search.test.ts +18 -86
  202. package/src/tools/network/web-search.ts +9 -15
  203. package/src/util/platform.ts +7 -1
  204. package/src/util/pricing.ts +0 -1
  205. package/src/workspace/provider-commit-message-generator.ts +10 -6
@@ -25,7 +25,6 @@ mock.module("../config/loader.js", () => ({
25
25
 
26
26
  provider: "anthropic",
27
27
  model: "test",
28
- apiKeys: {},
29
28
  maxTokens: 4096,
30
29
  dataDir: "/tmp",
31
30
  timeouts: {
@@ -8,7 +8,6 @@ import type {
8
8
  const mockConfig = {
9
9
  provider: "anthropic",
10
10
  model: "test",
11
- apiKeys: {},
12
11
  maxTokens: 4096,
13
12
  dataDir: "/tmp",
14
13
  timeouts: {
@@ -17,7 +17,6 @@ import type { ToolContext } from "../tools/types.js";
17
17
  const mockConfig = {
18
18
  provider: "anthropic",
19
19
  model: "test",
20
- apiKeys: {},
21
20
  maxTokens: 4096,
22
21
  dataDir: "/tmp",
23
22
  timeouts: {
@@ -26,7 +26,6 @@ import type {
26
26
  const mockConfig = {
27
27
  provider: "anthropic",
28
28
  model: "test",
29
- apiKeys: {},
30
29
  maxTokens: 4096,
31
30
  dataDir: "/tmp",
32
31
  timeouts: {
@@ -1099,7 +1099,7 @@ describe("Trust Store", () => {
1099
1099
 
1100
1100
  // ── default allow: browser tools ────────────────────────────
1101
1101
 
1102
- test("all 10 browser tools have default allow rules", () => {
1102
+ test("all 14 browser tools have default allow rules", () => {
1103
1103
  const templates = getDefaultRuleTemplates();
1104
1104
  const browserTools = [
1105
1105
  "browser_navigate",
@@ -1109,8 +1109,12 @@ describe("Trust Store", () => {
1109
1109
  "browser_click",
1110
1110
  "browser_type",
1111
1111
  "browser_press_key",
1112
+ "browser_scroll",
1113
+ "browser_select_option",
1114
+ "browser_hover",
1112
1115
  "browser_wait_for",
1113
1116
  "browser_extract",
1117
+ "browser_wait_for_download",
1114
1118
  "browser_fill_credential",
1115
1119
  ];
1116
1120
 
@@ -120,11 +120,10 @@ mock.module("../util/logger.js", () => ({
120
120
  const mockConfigObj = {
121
121
  model: "test",
122
122
  provider: "test",
123
- apiKeys: {},
124
123
  memory: { enabled: false },
125
124
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
126
125
  secretDetection: { enabled: false },
127
- elevenlabs: { voiceId: "21m00Tcm4TlvDq8ikWAM" },
126
+ elevenlabs: { voiceId: DEFAULT_ELEVENLABS_VOICE_ID },
128
127
  calls: {
129
128
  voice: {
130
129
  language: "en-US",
@@ -322,6 +321,7 @@ import {
322
321
  handleStatusCallback,
323
322
  handleVoiceWebhook,
324
323
  } from "../calls/twilio-routes.js";
324
+ import { DEFAULT_ELEVENLABS_VOICE_ID } from "../config/schemas/elevenlabs.js";
325
325
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
326
326
  import { conversations } from "../memory/schema.js";
327
327
  import {
@@ -19,7 +19,6 @@ import type {
19
19
  const mockConfig = {
20
20
  provider: "anthropic",
21
21
  model: "test",
22
- apiKeys: {},
23
22
  maxTokens: 4096,
24
23
  dataDir: "/tmp",
25
24
  timeouts: {
@@ -10,6 +10,7 @@ import {
10
10
  buildElevenLabsVoiceSpec,
11
11
  resolveVoiceQualityProfile,
12
12
  } from "../calls/voice-quality.js";
13
+ import { DEFAULT_ELEVENLABS_VOICE_ID } from "../config/schemas/elevenlabs.js";
13
14
 
14
15
  describe("buildElevenLabsVoiceSpec", () => {
15
16
  test("returns bare voiceId when no model is set", () => {
@@ -63,7 +64,7 @@ describe("buildElevenLabsVoiceSpec", () => {
63
64
  describe("resolveVoiceQualityProfile", () => {
64
65
  test("always returns ElevenLabs ttsProvider", () => {
65
66
  mockConfig = {
66
- elevenlabs: { voiceId: "21m00Tcm4TlvDq8ikWAM" },
67
+ elevenlabs: { voiceId: DEFAULT_ELEVENLABS_VOICE_ID },
67
68
  calls: {
68
69
  voice: {
69
70
  language: "en-US",
@@ -55,7 +55,6 @@ mock.module("../config/loader.js", () => ({
55
55
 
56
56
  provider: "anthropic",
57
57
  providerOrder: ["anthropic"],
58
- apiKeys: { anthropic: "test-key" },
59
58
  calls: {
60
59
  enabled: true,
61
60
  provider: "twilio",
@@ -548,7 +548,7 @@ async function executeWebSearch(
548
548
  if (!apiKey) {
549
549
  return {
550
550
  content:
551
- "Error: No web search API key configured. Provide a PERPLEXITY_API_KEY or BRAVE_API_KEY here in the chat using the secure credential prompt, or set it from the Settings page.",
551
+ "Error: No web search API key configured. Set it via `keys set perplexity <key>` or `keys set brave <key>`, or configure it from the Settings page under API Keys.",
552
552
  isError: true,
553
553
  };
554
554
  }
package/src/agent/loop.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as Sentry from "@sentry/node";
2
2
 
3
+ import { estimateToolsTokens } from "../context/token-estimator.js";
3
4
  import { truncateOversizedToolResults } from "../context/tool-result-truncation.js";
4
5
  import { getHookManager } from "../hooks/manager.js";
5
6
  import type {
@@ -78,7 +79,7 @@ export type AgentEvent =
78
79
  toolUseId: string;
79
80
  input: Record<string, unknown>;
80
81
  }
81
- | { type: "server_tool_complete"; toolUseId: string }
82
+ | { type: "server_tool_complete"; toolUseId: string; isError: boolean }
82
83
  | { type: "error"; error: Error }
83
84
  | {
84
85
  type: "usage";
@@ -175,6 +176,20 @@ export class AgentLoop {
175
176
  this.toolExecutor = toolExecutor ?? null;
176
177
  }
177
178
 
179
+ /**
180
+ * Estimate token cost of the tool definitions sent to the provider.
181
+ *
182
+ * When `history` is provided and a dynamic `resolveTools` callback
183
+ * exists, the budget is derived from the resolved tool list for that
184
+ * turn — matching what `run()` actually sends. Without `history` (or
185
+ * without a resolver), falls back to the static `this.tools`.
186
+ */
187
+ getToolTokenBudget(history?: Message[]): number {
188
+ const tools =
189
+ history && this.resolveTools ? this.resolveTools(history) : this.tools;
190
+ return estimateToolsTokens(tools);
191
+ }
192
+
178
193
  async run(
179
194
  messages: Message[],
180
195
  onEvent: (event: AgentEvent) => void | Promise<void>,
@@ -318,6 +333,7 @@ export class AgentLoop {
318
333
  onEvent({
319
334
  type: "server_tool_complete",
320
335
  toolUseId: event.toolUseId,
336
+ isError: event.isError,
321
337
  });
322
338
  }
323
339
  },
@@ -14,7 +14,7 @@ import { join } from "node:path";
14
14
  import archiver from "archiver";
15
15
  import JSZip from "jszip";
16
16
 
17
- import { getApp, getAppsDir } from "../memory/app-store.js";
17
+ import { getApp, getAppsDir, isMultifileApp } from "../memory/app-store.js";
18
18
  import { computeContentId } from "../util/content-id.js";
19
19
  import { getLogger } from "../util/logger.js";
20
20
  import { compileApp } from "./app-compiler.js";
@@ -60,8 +60,10 @@ export async function packageApp(
60
60
  const version = app.version ?? "1.0.0";
61
61
  const contentId = computeContentId(app.name);
62
62
 
63
+ const multifile = isMultifileApp(app);
64
+
63
65
  const manifest: AppManifest = {
64
- format_version: 2,
66
+ format_version: multifile ? 2 : 1,
65
67
  name: app.name,
66
68
  ...(app.description ? { description: app.description } : {}),
67
69
  ...(app.icon ? { icon: app.icon } : {}),
@@ -74,34 +76,48 @@ export async function packageApp(
74
76
  content_id: contentId,
75
77
  };
76
78
 
77
- // Compile the app and bundle the dist/ output.
79
+ // Compile the app and bundle the output.
78
80
  const compiledFiles: { name: string; data: Buffer }[] = [];
79
81
 
80
82
  const appDir = join(getAppsDir(), appId);
81
- const compileResult = await compileApp(appDir);
82
- if (!compileResult.ok) {
83
- const messages = compileResult.errors
84
- .map((e) => {
85
- const loc = e.location
86
- ? ` (${e.location.file}:${e.location.line}:${e.location.column})`
87
- : "";
88
- return `${e.text}${loc}`;
89
- })
90
- .join("\n");
91
- throw new Error(`Compilation failed for app "${app.name}":\n${messages}`);
92
- }
93
83
 
94
- const distDir = join(appDir, "dist");
95
- const indexHtml = await readFile(join(distDir, "index.html"), "utf-8");
96
- const mainJs = await readFile(join(distDir, "main.js"));
84
+ if (multifile) {
85
+ // Multi-file TSX app: compile src/ -> dist/
86
+ const compileResult = await compileApp(appDir);
87
+ if (!compileResult.ok) {
88
+ const messages = compileResult.errors
89
+ .map((e) => {
90
+ const loc = e.location
91
+ ? ` (${e.location.file}:${e.location.line}:${e.location.column})`
92
+ : "";
93
+ return `${e.text}${loc}`;
94
+ })
95
+ .join("\n");
96
+ throw new Error(`Compilation failed for app "${app.name}":\n${messages}`);
97
+ }
98
+
99
+ const distDir = join(appDir, "dist");
100
+ const indexHtml = await readFile(join(distDir, "index.html"), "utf-8");
101
+ const mainJs = await readFile(join(distDir, "main.js"));
97
102
 
98
- compiledFiles.push({ name: "index.html", data: Buffer.from(indexHtml) });
99
- compiledFiles.push({ name: "main.js", data: mainJs });
103
+ compiledFiles.push({ name: "index.html", data: Buffer.from(indexHtml) });
104
+ compiledFiles.push({ name: "main.js", data: mainJs });
100
105
 
101
- // main.css is optional — only produced when the app imports CSS
102
- const cssPath = join(distDir, "main.css");
103
- if (existsSync(cssPath)) {
104
- compiledFiles.push({ name: "main.css", data: await readFile(cssPath) });
106
+ // main.css is optional — only produced when the app imports CSS
107
+ const cssPath = join(distDir, "main.css");
108
+ if (existsSync(cssPath)) {
109
+ compiledFiles.push({ name: "main.css", data: await readFile(cssPath) });
110
+ }
111
+ } else {
112
+ // Single-file HTML app: bundle index.html directly
113
+ const indexHtmlPath = join(appDir, "index.html");
114
+ if (!existsSync(indexHtmlPath)) {
115
+ throw new Error(
116
+ `App "${app.name}" has no src/ directory and no index.html`,
117
+ );
118
+ }
119
+ const indexHtml = await readFile(indexHtmlPath, "utf-8");
120
+ compiledFiles.push({ name: "index.html", data: Buffer.from(indexHtml) });
105
121
  }
106
122
 
107
123
  // Create the zip archive
@@ -50,6 +50,7 @@ import {
50
50
  ASK_GUARDIAN_CAPTURE_REGEX,
51
51
  CALL_OPENING_ACK_MARKER,
52
52
  CALL_OPENING_MARKER,
53
+ CALL_VERIFICATION_COMPLETE_MARKER,
53
54
  couldBeControlMarker,
54
55
  END_CALL_MARKER,
55
56
  extractBalancedJson,
@@ -209,6 +210,21 @@ export class CallController {
209
210
  await this.runTurn(CALL_OPENING_MARKER);
210
211
  }
211
212
 
213
+ /**
214
+ * Kick off the first utterance after the caller has completed outbound
215
+ * phone verification. Sends a verification-aware marker so the LLM can
216
+ * greet naturally with context that verification just happened.
217
+ */
218
+ async startPostVerificationGreeting(): Promise<void> {
219
+ if (this.initialGreetingStarted) return;
220
+ if (this.state !== "idle") return;
221
+
222
+ this.initialGreetingStarted = true;
223
+ this.resetSilenceTimer();
224
+ this.lastSentWasOpener = true;
225
+ await this.runTurn(CALL_VERIFICATION_COMPLETE_MARKER);
226
+ }
227
+
212
228
  /**
213
229
  * Handle a final caller utterance from the ConversationRelay.
214
230
  * Caller utterances always trigger normal turns, even when a guardian
@@ -595,7 +595,7 @@ export class RelayConnection {
595
595
  (resolved.actorTrust.trustClass === "guardian" ||
596
596
  resolved.actorTrust.trustClass === "trusted_contact")
597
597
  ) {
598
- touchContactInteraction(resolved.actorTrust.memberRecord.contact.id);
598
+ touchContactInteraction(resolved.actorTrust.memberRecord.channel.id);
599
599
  }
600
600
  if (this.controller && resolved.actorTrust.trustClass !== "unknown") {
601
601
  this.controller.setTrustContext(
@@ -612,7 +612,7 @@ export class RelayConnection {
612
612
  resolved.actorTrust.trustClass === "trusted_contact")
613
613
  ) {
614
614
  touchContactInteraction(
615
- resolved.actorTrust.memberRecord.contact.id,
615
+ resolved.actorTrust.memberRecord.channel.id,
616
616
  );
617
617
  }
618
618
  if (this.controller && resolved.actorTrust.trustClass !== "unknown") {
@@ -992,14 +992,7 @@ export class RelayConnection {
992
992
  }
993
993
 
994
994
  if (isOutbound) {
995
- this.connectionState = "disconnecting";
996
- this.sendTextToken(result.ttsMessage!, true);
997
-
998
- updateCallSession(this.callSessionId, {
999
- status: "completed",
1000
- endedAt: Date.now(),
1001
- });
1002
-
995
+ // Keep the pointer message back to the initiating conversation
1003
996
  const successSession = getCallSession(this.callSessionId);
1004
997
  if (successSession?.initiatedFromConversationId) {
1005
998
  addPointerMessage(
@@ -1018,9 +1011,32 @@ export class RelayConnection {
1018
1011
  });
1019
1012
  }
1020
1013
 
1021
- setTimeout(() => {
1022
- this.endSession("Verified — guardian challenge passed");
1023
- }, getTtsPlaybackDelayMs());
1014
+ // Update trust context on the controller so the LLM knows this is the guardian
1015
+ if (this.controller) {
1016
+ const verifiedActorTrust = resolveActorTrust({
1017
+ assistantId,
1018
+ sourceChannel: "phone",
1019
+ conversationExternalId: fromNumber,
1020
+ actorExternalId: fromNumber,
1021
+ });
1022
+ this.controller.setTrustContext(
1023
+ toTrustContext(verifiedActorTrust, fromNumber),
1024
+ );
1025
+ }
1026
+
1027
+ // Mark session as in-progress and transition to guardian conversation
1028
+ // with verification context so the LLM greets naturally.
1029
+ updateCallSession(this.callSessionId, { status: "in_progress" });
1030
+ if (this.controller) {
1031
+ this.controller
1032
+ .startPostVerificationGreeting()
1033
+ .catch((err) =>
1034
+ log.error(
1035
+ { err, callSessionId: this.callSessionId },
1036
+ "Failed to start post-verification greeting",
1037
+ ),
1038
+ );
1039
+ }
1024
1040
  } else if (result.verificationType === "trusted_contact") {
1025
1041
  this.continueCallAfterTrustedContactActivation({
1026
1042
  assistantId,
@@ -11,6 +11,7 @@
11
11
 
12
12
  export const CALL_OPENING_MARKER = "[CALL_OPENING]";
13
13
  export const CALL_OPENING_ACK_MARKER = "[CALL_OPENING_ACK]";
14
+ export const CALL_VERIFICATION_COMPLETE_MARKER = "[CALL_VERIFICATION_COMPLETE]";
14
15
  export const END_CALL_MARKER = "[END_CALL]";
15
16
 
16
17
  // ---------------------------------------------------------------------------
@@ -44,7 +44,7 @@ export function buildElevenLabsVoiceSpec(config: {
44
44
  *
45
45
  * Always uses ElevenLabs TTS via Twilio ConversationRelay.
46
46
  * The voice ID comes from the shared `elevenlabs.voiceId` config
47
- * (defaults to Rachel21m00Tcm4TlvDq8ikWAM).
47
+ * (defaults to AmeliaZF6FPAbjXT4488VcRRnw).
48
48
  */
49
49
  export function resolveVoiceQualityProfile(
50
50
  config?: ReturnType<typeof loadConfig>,
@@ -24,7 +24,10 @@ import { checkIngressForSecrets } from "../security/secret-ingress.js";
24
24
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
25
25
  import { IngressBlockedError } from "../util/errors.js";
26
26
  import { getLogger } from "../util/logger.js";
27
- import { CALL_OPENING_MARKER } from "./voice-control-protocol.js";
27
+ import {
28
+ CALL_OPENING_MARKER,
29
+ CALL_VERIFICATION_COMPLETE_MARKER,
30
+ } from "./voice-control-protocol.js";
28
31
 
29
32
  const log = getLogger("voice-session-bridge");
30
33
 
@@ -197,7 +200,8 @@ function buildVoiceCallControlPrompt(opts: {
197
200
  ? " However, the disclosure text from rule 0 is separate from self-introduction and must always be included in your opening greeting, even if the Task does not mention introducing yourself."
198
201
  : "";
199
202
  lines.push(
200
- `7. If the latest user turn is "(call connecteddeliver opening greeting)", deliver your opening greeting based solely on the Task context above. The Task already describes how to open the call follow it directly without adding any extra introduction on top. If the Task says to introduce yourself, do so once. If the Task does not mention introducing yourself, skip the introduction.${disclosureReminder} Vary the wording naturally; do not use a fixed template.`,
203
+ '7. If the latest user turn is "(verification completedtransitioning into conversation)", the caller just completed a phone verification code challenge on this call. Greet them naturally and ask if there is anything you can help with. Keep it casual and brief.',
204
+ `If the latest user turn is "(call connected — deliver opening greeting)", deliver your opening greeting based solely on the Task context above. The Task already describes how to open the call — follow it directly without adding any extra introduction on top. If the Task says to introduce yourself, do so once. If the Task does not mention introducing yourself, skip the introduction.${disclosureReminder} Vary the wording naturally; do not use a fixed template.`,
201
205
  "8. If the latest user turn includes [CALL_OPENING_ACK], treat it as the callee acknowledging your opener and continue the conversation naturally without re-introducing yourself or repeating the initial check-in question.",
202
206
  );
203
207
  }
@@ -269,7 +273,9 @@ export async function startVoiceTurn(
269
273
  const persistedContent =
270
274
  opts.content === CALL_OPENING_MARKER
271
275
  ? "(call connected — deliver opening greeting)"
272
- : opts.content;
276
+ : opts.content === CALL_VERIFICATION_COMPLETE_MARKER
277
+ ? "(verification completed — transitioning into conversation)"
278
+ : opts.content;
273
279
 
274
280
  // Build the call-control protocol prompt so the model knows how to emit
275
281
  // control markers (ASK_GUARDIAN, END_CALL, etc.) and recognize opener turns.
@@ -74,6 +74,22 @@ export function assertInterfaceId(value: unknown, field: string): InterfaceId {
74
74
  return value;
75
75
  }
76
76
 
77
+ /**
78
+ * Interfaces that have an SSE client capable of displaying interactive
79
+ * permission prompts. Channel interfaces (telegram, slack, etc.) route
80
+ * approvals through the guardian system and have no interactive prompter UI.
81
+ */
82
+ export const INTERACTIVE_INTERFACES: ReadonlySet<InterfaceId> = new Set([
83
+ "macos",
84
+ "ios",
85
+ "cli",
86
+ "vellum",
87
+ ]);
88
+
89
+ export function isInteractiveInterface(id: InterfaceId): boolean {
90
+ return INTERACTIVE_INTERFACES.has(id);
91
+ }
92
+
77
93
  export interface TurnInterfaceContext {
78
94
  userMessageInterface: InterfaceId;
79
95
  assistantMessageInterface: InterfaceId;
@@ -0,0 +1,173 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ import type { Command } from "commander";
12
+
13
+ import { getWorkspaceDir } from "../../util/platform.js";
14
+ import { log } from "../logger.js";
15
+
16
+ const DEFAULT_TIMEOUT_MS = 30_000;
17
+ const POLL_INTERVAL_MS = 100;
18
+
19
+ interface BashSignalResult {
20
+ requestId: string;
21
+ stdout: string;
22
+ stderr: string;
23
+ exitCode: number | null;
24
+ timedOut: boolean;
25
+ error?: string;
26
+ }
27
+
28
+ export function registerBashCommand(program: Command): void {
29
+ program
30
+ .command("bash <command>")
31
+ .description(
32
+ "Execute a shell command through the assistant process for debugging",
33
+ )
34
+ .option(
35
+ "-t, --timeout <ms>",
36
+ "Timeout in milliseconds for command execution",
37
+ String(DEFAULT_TIMEOUT_MS),
38
+ )
39
+ .addHelpText(
40
+ "after",
41
+ `
42
+ Sends a shell command to the running assistant for execution via the
43
+ signals directory. The assistant spawns the command in its own process environment
44
+ and returns stdout, stderr, and the exit code.
45
+
46
+ This is a developer debugging tool for inspecting how the assistant invokes and
47
+ observes shell commands. The command runs with the assistant's environment, working
48
+ directory, and process context — not the caller's shell.
49
+
50
+ The CLI writes the command to signals/bash.<requestId> and polls
51
+ signals/bash.<requestId>.result for the output. The assistant must be running
52
+ for this to work.
53
+
54
+ Arguments:
55
+ command The shell command string to execute (e.g. "echo hello", "ls -la").
56
+ Runs in bash via \`bash -c\` in the assistant's process environment.
57
+
58
+ Examples:
59
+ $ assistant bash "echo hello"
60
+ $ assistant bash "which node"
61
+ $ assistant bash "env | grep PATH" --timeout 10000
62
+ $ assistant bash "ls -la"`,
63
+ )
64
+ .action((command: string, opts: { timeout: string }) => {
65
+ const timeoutMs = parseInt(opts.timeout, 10);
66
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
67
+ log.error("Invalid timeout value. Must be a positive integer.");
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+
72
+ const requestId = randomUUID();
73
+ const signalsDir = join(getWorkspaceDir(), "signals");
74
+
75
+ try {
76
+ mkdirSync(signalsDir, { recursive: true });
77
+ } catch {
78
+ log.error("Failed to create signals directory.");
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+
83
+ // Write the command signal for the assistant to pick up.
84
+ const signalPath = join(signalsDir, `bash.${requestId}`);
85
+ const resultPath = join(signalsDir, `bash.${requestId}.result`);
86
+
87
+ try {
88
+ writeFileSync(
89
+ signalPath,
90
+ JSON.stringify({ requestId, command, timeoutMs }),
91
+ );
92
+ } catch {
93
+ log.error("Failed to write bash signal file.");
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+
98
+ log.info(`Sent command to assistant (requestId: ${requestId})`);
99
+ log.info("Waiting for result...");
100
+
101
+ // Poll for the result file until timeout.
102
+ const deadline = Date.now() + timeoutMs + 5_000; // extra buffer for assistant overhead
103
+
104
+ const poll = setInterval(() => {
105
+ if (Date.now() > deadline) {
106
+ clearInterval(poll);
107
+ cleanupSignalFiles();
108
+ log.error(
109
+ "Timed out waiting for response. Is the assistant running?",
110
+ );
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+
115
+ if (!existsSync(resultPath)) return;
116
+
117
+ let result: BashSignalResult;
118
+ try {
119
+ const content = readFileSync(resultPath, "utf-8");
120
+ result = JSON.parse(content) as BashSignalResult;
121
+ } catch {
122
+ // File may be partially written; retry on next poll.
123
+ return;
124
+ }
125
+
126
+ // Ignore stale results from a previous invocation.
127
+ if (result.requestId !== requestId) return;
128
+
129
+ clearInterval(poll);
130
+ cleanupSignalFiles();
131
+
132
+ if (result.error) {
133
+ log.error(`Spawn error: ${result.error}`);
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+
138
+ if (result.stdout) {
139
+ process.stdout.write(result.stdout);
140
+ if (!result.stdout.endsWith("\n")) {
141
+ process.stdout.write("\n");
142
+ }
143
+ }
144
+
145
+ if (result.stderr) {
146
+ process.stderr.write(result.stderr);
147
+ if (!result.stderr.endsWith("\n")) {
148
+ process.stderr.write("\n");
149
+ }
150
+ }
151
+
152
+ if (result.timedOut) {
153
+ log.info(`Command timed out in assistant.`);
154
+ }
155
+
156
+ if (result.exitCode != null && result.exitCode !== 0) {
157
+ log.info(`Exit code: ${result.exitCode}`);
158
+ }
159
+
160
+ process.exitCode = result.exitCode ?? 1;
161
+ }, POLL_INTERVAL_MS);
162
+
163
+ function cleanupSignalFiles(): void {
164
+ for (const p of [signalPath, resultPath]) {
165
+ try {
166
+ unlinkSync(p);
167
+ } catch {
168
+ // Best-effort cleanup; the file may already be gone.
169
+ }
170
+ }
171
+ }
172
+ });
173
+ }
@@ -7,7 +7,7 @@ import { getRuntimeHttpPort } from "../../config/env.js";
7
7
  import { loadRawConfig } from "../../config/loader.js";
8
8
  import { shouldAutoStartDaemon } from "../../daemon/connection-policy.js";
9
9
  import { isHttpHealthy } from "../../daemon/daemon-control.js";
10
- import { getSecureKey } from "../../security/secure-keys.js";
10
+ import { getSecureKeyAsync } from "../../security/secure-keys.js";
11
11
  import {
12
12
  getDbPath,
13
13
  getLogPath,
@@ -35,7 +35,7 @@ Output symbols:
35
35
 
36
36
  Diagnostic checks performed:
37
37
  1. Bun is installed Verifies bun is available in PATH
38
- 2. API key configured Checks for a valid provider API key in config or env
38
+ 2. API key configured Checks for a valid provider API key in secure storage
39
39
  3. Assistant reachable HTTP health check against the assistant server
40
40
  4. Database exists/readable Opens the SQLite database and runs a test query
41
41
  5. Directory structure Verifies required ~/.vellum/ directories exist
@@ -76,32 +76,14 @@ Examples:
76
76
  const raw = loadRawConfig();
77
77
  const provider =
78
78
  typeof raw.provider === "string" ? raw.provider : "anthropic";
79
- const providerEnvVar: Record<string, string> = {
80
- anthropic: "ANTHROPIC_API_KEY",
81
- openai: "OPENAI_API_KEY",
82
- gemini: "GEMINI_API_KEY",
83
- ollama: "OLLAMA_API_KEY",
84
- fireworks: "FIREWORKS_API_KEY",
85
- openrouter: "OPENROUTER_API_KEY",
86
- };
87
- const configKey = getSecureKey(provider);
88
- const envVar = providerEnvVar[provider];
89
- const envKey = envVar ? process.env[envVar] : undefined;
90
- const plaintextKey = (
91
- raw.apiKeys as Record<string, string> | undefined
92
- )?.[provider];
79
+ const configKey = await getSecureKeyAsync(provider);
93
80
 
94
81
  if (provider === "ollama") {
95
82
  pass("Provider configured (Ollama; API key optional)");
96
- } else if (envKey || configKey || plaintextKey) {
83
+ } else if (configKey) {
97
84
  pass("API key configured");
98
85
  } else {
99
- fail(
100
- "API key configured",
101
- envVar
102
- ? `set ${envVar} or run: assistant keys set ${provider} <key>`
103
- : `set API key for provider "${provider}"`,
104
- );
86
+ fail("API key configured", `run: assistant keys set ${provider} <key>`);
105
87
  }
106
88
 
107
89
  // 3. Daemon reachable (HTTP health check)