@vellumai/assistant 0.5.7 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  14. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  15. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  16. package/src/__tests__/config-schema.test.ts +2 -0
  17. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  21. package/src/__tests__/conversation-error.test.ts +15 -1
  22. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  23. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-queue.test.ts +0 -1
  26. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  28. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  29. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  30. package/src/__tests__/credential-execution-client.test.ts +5 -2
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  33. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  35. package/src/__tests__/credentials-cli.test.ts +4 -3
  36. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  37. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  38. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  39. package/src/__tests__/journal-context.test.ts +335 -0
  40. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  42. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  43. package/src/__tests__/memory-regressions.test.ts +408 -363
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  45. package/src/__tests__/non-member-access-request.test.ts +2 -2
  46. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  47. package/src/__tests__/oauth-cli.test.ts +5 -1
  48. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  49. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  50. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  51. package/src/__tests__/relay-server.test.ts +1 -2
  52. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  53. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  54. package/src/__tests__/secure-keys.test.ts +18 -15
  55. package/src/__tests__/skill-memory.test.ts +17 -3
  56. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  57. package/src/__tests__/stt-hints.test.ts +437 -0
  58. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  59. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  60. package/src/__tests__/voice-quality.test.ts +58 -0
  61. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  62. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  63. package/src/acp/agent-process.ts +9 -1
  64. package/src/agent/loop.ts +1 -1
  65. package/src/approvals/guardian-request-resolvers.ts +164 -38
  66. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  67. package/src/calls/call-controller.ts +9 -5
  68. package/src/calls/fish-audio-client.ts +26 -14
  69. package/src/calls/stt-hints.ts +189 -0
  70. package/src/calls/tts-text-sanitizer.ts +61 -0
  71. package/src/calls/twilio-routes.ts +32 -4
  72. package/src/calls/voice-quality.ts +15 -3
  73. package/src/calls/voice-session-bridge.ts +1 -0
  74. package/src/cli/commands/avatar.ts +2 -2
  75. package/src/cli/commands/credentials.ts +110 -94
  76. package/src/cli/commands/doctor.ts +2 -2
  77. package/src/cli/commands/keys.ts +7 -7
  78. package/src/cli/commands/memory.ts +1 -1
  79. package/src/cli/commands/oauth/connections.ts +11 -29
  80. package/src/cli/commands/oauth/platform.ts +389 -43
  81. package/src/cli/lib/daemon-credential-client.ts +284 -0
  82. package/src/cli.ts +1 -1
  83. package/src/config/bundled-skills/AGENTS.md +34 -0
  84. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  85. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  86. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  87. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  88. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  89. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  90. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  91. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  92. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  93. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  94. package/src/config/bundled-tool-registry.ts +4 -0
  95. package/src/config/defaults.ts +0 -2
  96. package/src/config/env-registry.ts +4 -4
  97. package/src/config/env.ts +14 -1
  98. package/src/config/feature-flag-registry.json +1 -1
  99. package/src/config/loader.ts +8 -11
  100. package/src/config/schema.ts +5 -16
  101. package/src/config/schemas/calls.ts +17 -0
  102. package/src/config/schemas/inference.ts +2 -2
  103. package/src/config/schemas/journal.ts +16 -0
  104. package/src/config/schemas/memory-processing.ts +2 -2
  105. package/src/config/types.ts +1 -0
  106. package/src/contacts/contact-store.ts +2 -2
  107. package/src/credential-execution/executable-discovery.ts +1 -1
  108. package/src/credential-execution/startup-timeout.ts +36 -0
  109. package/src/daemon/approval-generators.ts +3 -9
  110. package/src/daemon/conversation-error.ts +13 -1
  111. package/src/daemon/conversation-memory.ts +1 -2
  112. package/src/daemon/conversation-process.ts +18 -1
  113. package/src/daemon/conversation-surfaces.ts +30 -1
  114. package/src/daemon/conversation.ts +20 -9
  115. package/src/daemon/guardian-action-generators.ts +3 -9
  116. package/src/daemon/lifecycle.ts +18 -11
  117. package/src/daemon/message-types/conversations.ts +1 -0
  118. package/src/daemon/server.ts +2 -3
  119. package/src/memory/app-store.ts +31 -0
  120. package/src/memory/db-init.ts +4 -0
  121. package/src/memory/indexer.ts +19 -10
  122. package/src/memory/items-extractor.ts +315 -322
  123. package/src/memory/job-handlers/summarization.ts +26 -16
  124. package/src/memory/jobs-store.ts +33 -1
  125. package/src/memory/journal-memory.ts +214 -0
  126. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  127. package/src/memory/migrations/index.ts +1 -0
  128. package/src/memory/migrations/registry.ts +8 -0
  129. package/src/memory/retriever.test.ts +37 -25
  130. package/src/memory/retriever.ts +24 -49
  131. package/src/memory/schema/memory-core.ts +2 -0
  132. package/src/memory/search/formatting.ts +7 -44
  133. package/src/memory/search/staleness.ts +4 -0
  134. package/src/memory/search/tier-classifier.ts +10 -2
  135. package/src/memory/search/types.ts +2 -5
  136. package/src/memory/task-memory-cleanup.ts +4 -3
  137. package/src/notifications/adapters/slack.ts +168 -6
  138. package/src/notifications/broadcaster.ts +1 -0
  139. package/src/notifications/copy-composer.ts +59 -2
  140. package/src/notifications/signal.ts +2 -0
  141. package/src/notifications/types.ts +2 -0
  142. package/src/prompts/journal-context.ts +133 -0
  143. package/src/prompts/persona-resolver.ts +80 -24
  144. package/src/prompts/system-prompt.ts +8 -0
  145. package/src/prompts/templates/SOUL.md +10 -0
  146. package/src/providers/provider-send-message.ts +3 -32
  147. package/src/providers/registry.ts +2 -139
  148. package/src/providers/types.ts +1 -1
  149. package/src/runtime/access-request-helper.ts +4 -0
  150. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  151. package/src/runtime/auth/route-policy.ts +2 -0
  152. package/src/runtime/gateway-client.ts +47 -4
  153. package/src/runtime/guardian-decision-types.ts +45 -4
  154. package/src/runtime/http-server.ts +5 -2
  155. package/src/runtime/routes/access-request-decision.ts +2 -2
  156. package/src/runtime/routes/app-management-routes.ts +2 -1
  157. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  158. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  159. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  160. package/src/runtime/routes/debug-routes.ts +12 -9
  161. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  162. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  163. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  164. package/src/runtime/routes/identity-routes.ts +1 -1
  165. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  168. package/src/runtime/routes/integrations/twilio.ts +52 -10
  169. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  170. package/src/runtime/routes/memory-item-routes.ts +25 -11
  171. package/src/runtime/routes/secret-routes.ts +141 -10
  172. package/src/runtime/routes/tts-routes.ts +11 -1
  173. package/src/security/ces-credential-client.ts +18 -9
  174. package/src/security/ces-rpc-credential-backend.ts +4 -3
  175. package/src/security/credential-backend.ts +10 -4
  176. package/src/security/secure-keys.ts +21 -4
  177. package/src/skills/catalog-install.ts +4 -36
  178. package/src/skills/skill-memory.ts +1 -0
  179. package/src/subagent/manager.ts +2 -5
  180. package/src/tools/acp/spawn.ts +78 -1
  181. package/src/tools/credentials/vault.ts +5 -3
  182. package/src/tools/memory/definitions.ts +3 -2
  183. package/src/tools/memory/handlers.ts +10 -7
  184. package/src/tools/terminal/safe-env.ts +1 -0
  185. package/src/util/browser.ts +15 -0
  186. package/src/util/platform.ts +1 -1
  187. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  188. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  189. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  190. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  191. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  192. package/src/workspace/migrations/registry.ts +4 -0
  193. package/src/workspace/provider-commit-message-generator.ts +12 -21
  194. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  195. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  196. package/src/memory/search/lexical.ts +0 -48
  197. package/src/providers/failover.ts +0 -186
package/src/config/env.ts CHANGED
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { getLogger } from "../util/logger.js";
17
17
  import { checkUnrecognizedEnvVars } from "./env-registry.js";
18
+ import { getConfig } from "./loader.js";
18
19
 
19
20
  const log = getLogger("env");
20
21
 
@@ -144,7 +145,19 @@ export function setPlatformBaseUrl(value: string | undefined): void {
144
145
  }
145
146
 
146
147
  export function getPlatformBaseUrl(): string {
147
- return str("VELLUM_PLATFORM_URL") ?? _platformBaseUrlOverride ?? "";
148
+ let configUrl: string | undefined;
149
+ try {
150
+ const val = getConfig().platform.baseUrl;
151
+ if (val) configUrl = val;
152
+ } catch {
153
+ // Config not yet available (early bootstrap) — fall through
154
+ }
155
+ return (
156
+ configUrl ||
157
+ str("VELLUM_PLATFORM_URL") ||
158
+ _platformBaseUrlOverride ||
159
+ "https://platform.vellum.ai"
160
+ );
148
161
  }
149
162
 
150
163
  let _platformAssistantIdOverride: string | undefined;
@@ -111,7 +111,7 @@
111
111
  "key": "feature_flags.ces-managed-sidecar.enabled",
112
112
  "label": "CES Managed Sidecar Transport",
113
113
  "description": "Use managed sidecar transport for CES communication when running in a containerized environment",
114
- "defaultEnabled": false
114
+ "defaultEnabled": true
115
115
  },
116
116
  {
117
117
  "id": "ces-credential-backend",
@@ -40,19 +40,16 @@ function ensureMigratedDataDir(): void {
40
40
  }
41
41
 
42
42
  /**
43
- * Zod 4's .default({}) returns {} as output without running inner-schema
44
- * parsing, so nested object defaults are never applied. Re-parse the config
45
- * to cascade defaults through each nesting level.
46
- * Max chain of .default({}) on object schemas is 4
47
- * (e.g. memory retrieval freshness maxAgeDays),
48
- * so 5 parses are needed (N+1) to fully cascade.
43
+ * Parse a raw config through the Zod schema, applying all nested defaults.
44
+ *
45
+ * All nested object schemas use `.default(SubSchema.parse({}))` which
46
+ * pre-computes fully-resolved defaults at schema construction time, so a
47
+ * single parse is sufficient to cascade defaults through every nesting level.
49
48
  */
50
49
  export function applyNestedDefaults(config: unknown): AssistantConfig {
51
- let current: unknown = config;
52
- for (let i = 0; i < 5; i++) {
53
- current = AssistantConfigSchema.parse(current);
54
- }
55
- return current as AssistantConfig;
50
+ return structuredClone(
51
+ AssistantConfigSchema.parse(config),
52
+ ) as AssistantConfig;
56
53
  }
57
54
 
58
55
  function cloneDefaultConfig(): AssistantConfig {
@@ -68,6 +68,8 @@ export {
68
68
  IngressRateLimitConfigSchema,
69
69
  IngressWebhookConfigSchema,
70
70
  } from "./schemas/ingress.js";
71
+ export type { JournalConfig } from "./schemas/journal.js";
72
+ export { JournalConfigSchema } from "./schemas/journal.js";
71
73
  export type { AuditLogConfig, LogFileConfig } from "./schemas/logging.js";
72
74
  export {
73
75
  AuditLogConfigSchema,
@@ -202,6 +204,7 @@ import {
202
204
  ThinkingConfigSchema,
203
205
  } from "./schemas/inference.js";
204
206
  import { IngressConfigSchema } from "./schemas/ingress.js";
207
+ import { JournalConfigSchema } from "./schemas/journal.js";
205
208
  import {
206
209
  AuditLogConfigSchema,
207
210
  LogFileConfigSchema,
@@ -219,10 +222,7 @@ import {
219
222
  PermissionsConfigSchema,
220
223
  SecretDetectionConfigSchema,
221
224
  } from "./schemas/security.js";
222
- import {
223
- ServicesSchema,
224
- VALID_INFERENCE_PROVIDERS,
225
- } from "./schemas/services.js";
225
+ import { ServicesSchema } from "./schemas/services.js";
226
226
  import { SkillsConfigSchema } from "./schemas/skills.js";
227
227
  import {
228
228
  RateLimitConfigSchema,
@@ -233,18 +233,6 @@ import { WorkspaceGitConfigSchema } from "./schemas/workspace-git.js";
233
233
  export const AssistantConfigSchema = z
234
234
  .object({
235
235
  services: ServicesSchema.default(ServicesSchema.parse({})),
236
- providerOrder: z
237
- .array(
238
- z.enum(VALID_INFERENCE_PROVIDERS, {
239
- error: `Each providerOrder entry must be one of: ${VALID_INFERENCE_PROVIDERS.join(
240
- ", ",
241
- )}`,
242
- }),
243
- )
244
- .default([])
245
- .describe(
246
- "Fallback order of LLM providers — the assistant tries each in sequence if the previous one fails",
247
- ),
248
236
  maxTokens: z
249
237
  .number({ error: "maxTokens must be a number" })
250
238
  .int("maxTokens must be an integer")
@@ -281,6 +269,7 @@ export const AssistantConfigSchema = z
281
269
  "Custom pricing overrides for specific provider/model combinations",
282
270
  ),
283
271
  heartbeat: HeartbeatConfigSchema.default(HeartbeatConfigSchema.parse({})),
272
+ journal: JournalConfigSchema.default(JournalConfigSchema.parse({})),
284
273
  mcp: McpConfigSchema.default(McpConfigSchema.parse({})),
285
274
  acp: AcpConfigSchema.default(AcpConfigSchema.parse({})),
286
275
  skills: SkillsConfigSchema.default(SkillsConfigSchema.parse({})),
@@ -70,6 +70,23 @@ export const CallsVoiceConfigSchema = z
70
70
  })
71
71
  .default("elevenlabs")
72
72
  .describe("Text-to-speech provider for phone calls"),
73
+ hints: z
74
+ .array(
75
+ z.string({ error: "calls.voice.hints values must be strings" }),
76
+ )
77
+ .default([])
78
+ .describe(
79
+ "Static vocabulary hints for speech recognition — proper nouns, domain terms, and other words the STT provider should prioritize",
80
+ ),
81
+ interruptSensitivity: z
82
+ .enum(["low", "medium", "high"], {
83
+ error:
84
+ "calls.voice.interruptSensitivity must be one of: low, medium, high",
85
+ })
86
+ .default("low")
87
+ .describe(
88
+ "How aggressively the STT provider detects the start of caller speech — low reduces false interrupts from background noise",
89
+ ),
73
90
  })
74
91
  .describe("Voice and speech settings for phone calls");
75
92
 
@@ -26,8 +26,8 @@ export const ThinkingConfigSchema = z
26
26
  .describe("Extended thinking (chain-of-thought) configuration");
27
27
 
28
28
  export const EffortSchema = z
29
- .enum(["low", "medium", "high"], {
30
- error: 'effort must be "low", "medium", or "high"',
29
+ .enum(["low", "medium", "high", "max"], {
30
+ error: 'effort must be "low", "medium", "high", or "max"',
31
31
  })
32
32
  .default("high")
33
33
  .describe(
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+
3
+ export const JournalConfigSchema = z
4
+ .object({
5
+ contextWindowSize: z
6
+ .number({ error: "journal.contextWindowSize must be a number" })
7
+ .int("journal.contextWindowSize must be an integer")
8
+ .min(0, "journal.contextWindowSize must be >= 0")
9
+ .default(10)
10
+ .describe(
11
+ "Number of recent journal entries to include in context (0 to disable)",
12
+ ),
13
+ })
14
+ .describe("Journal context window configuration");
15
+
16
+ export type JournalConfig = z.infer<typeof JournalConfigSchema>;
@@ -12,7 +12,7 @@ export const MemoryExtractionConfigSchema = z
12
12
  .enum(["latency-optimized", "quality-optimized", "vision-optimized"], {
13
13
  error: "memory.extraction.modelIntent must be a valid model intent",
14
14
  })
15
- .default("latency-optimized")
15
+ .default("quality-optimized")
16
16
  .describe(
17
17
  "Model selection strategy for extraction — trade off speed vs quality",
18
18
  ),
@@ -39,7 +39,7 @@ export const MemorySummarizationConfigSchema = z
39
39
  .enum(["latency-optimized", "quality-optimized", "vision-optimized"], {
40
40
  error: "memory.summarization.modelIntent must be a valid model intent",
41
41
  })
42
- .default("latency-optimized")
42
+ .default("quality-optimized")
43
43
  .describe(
44
44
  "Model selection strategy for summarization — trade off speed vs quality",
45
45
  ),
@@ -12,6 +12,7 @@ export type {
12
12
  ImageGenerationService,
13
13
  InferenceService,
14
14
  IngressConfig,
15
+ JournalConfig,
15
16
  LogFileConfig,
16
17
  MemoryConfig,
17
18
  MemoryEmbeddingsConfig,
@@ -38,7 +38,7 @@ export function generateUserFileSlug(displayName: string): string {
38
38
  .toLowerCase()
39
39
  .replace(/[^a-z0-9]+/g, "-")
40
40
  .replace(/^-+|-+$/g, "")
41
- .slice(0, 50) || "user";
41
+ .slice(0, 100) || "user";
42
42
 
43
43
  const db = getDb();
44
44
  const rows = db
@@ -47,7 +47,7 @@ export function generateUserFileSlug(displayName: string): string {
47
47
  .where(like(contacts.userFile, `${escapeLike(slug)}%`))
48
48
  .all();
49
49
 
50
- const taken = new Set(rows.map((r) => r.userFile));
50
+ const taken = new Set(rows.map((r) => r.userFile?.toLowerCase()));
51
51
 
52
52
  const base = `${slug}.md`;
53
53
  if (!taken.has(base)) return base;
@@ -127,7 +127,7 @@ export function discoverLocalCes():
127
127
  }
128
128
 
129
129
  // Fallback: check for source entry point in the monorepo
130
- const monorepoRoot = join(import.meta.dir, "..", "..", "..", "..");
130
+ const monorepoRoot = join(import.meta.dir, "..", "..", "..");
131
131
  const sourceEntry = join(
132
132
  monorepoRoot,
133
133
  "credential-executor",
@@ -0,0 +1,36 @@
1
+ import type { CesClient } from "./client.js";
2
+
3
+ export interface AwaitCesClientWithTimeoutOptions {
4
+ timeoutMs?: number;
5
+ onTimeout?: () => void;
6
+ }
7
+
8
+ export const DEFAULT_CES_STARTUP_TIMEOUT_MS = 20_000;
9
+
10
+ export async function awaitCesClientWithTimeout(
11
+ clientPromise: Promise<CesClient | undefined>,
12
+ options: AwaitCesClientWithTimeoutOptions = {},
13
+ ): Promise<CesClient | undefined> {
14
+ const {
15
+ timeoutMs = DEFAULT_CES_STARTUP_TIMEOUT_MS,
16
+ onTimeout = () => {},
17
+ } = options;
18
+
19
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
20
+
21
+ try {
22
+ return await Promise.race([
23
+ clientPromise,
24
+ new Promise<undefined>((resolve) => {
25
+ timeoutId = setTimeout(() => {
26
+ onTimeout();
27
+ resolve(undefined);
28
+ }, timeoutMs);
29
+ }),
30
+ ]);
31
+ } finally {
32
+ if (timeoutId !== undefined) {
33
+ clearTimeout(timeoutId);
34
+ }
35
+ }
36
+ }
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from "../config/loader.js";
2
- import { getFailoverProvider, listProviders } from "../providers/registry.js";
2
+ import { getProvider, listProviders } from "../providers/registry.js";
3
3
  import {
4
4
  APPROVAL_COPY_MAX_TOKENS,
5
5
  APPROVAL_COPY_SYSTEM_PROMPT,
@@ -91,10 +91,7 @@ export function createApprovalCopyGenerator(): ApprovalCopyGenerator {
91
91
  const config = loadConfig();
92
92
  let provider;
93
93
  try {
94
- provider = getFailoverProvider(
95
- config.services.inference.provider,
96
- config.providerOrder,
97
- );
94
+ provider = getProvider(config.services.inference.provider);
98
95
  } catch {
99
96
  return null;
100
97
  }
@@ -148,10 +145,7 @@ export function createApprovalConversationGenerator(): ApprovalConversationGener
148
145
  if (!listProviders().includes(config.services.inference.provider)) {
149
146
  throw new Error("No provider available for approval conversation");
150
147
  }
151
- const provider = getFailoverProvider(
152
- config.services.inference.provider,
153
- config.providerOrder,
154
- );
148
+ const provider = getProvider(config.services.inference.provider);
155
149
 
156
150
  const pendingDescription = context.pendingApprovals
157
151
  .map((p) => `- Request ${p.requestId}: tool "${p.toolName}"`)
@@ -1,4 +1,4 @@
1
- import { ProviderError } from "../util/errors.js";
1
+ import { ProviderError, ProviderNotConfiguredError } from "../util/errors.js";
2
2
  import type {
3
3
  ConversationErrorCode,
4
4
  ConversationErrorMessage,
@@ -149,6 +149,18 @@ export function classifyConversationError(
149
149
  (error instanceof Error ? error.stack : undefined) ?? message;
150
150
  const debugDetails = truncateDebugDetails(rawDetails);
151
151
 
152
+ // Dedicated classification for missing provider API key
153
+ if (error instanceof ProviderNotConfiguredError) {
154
+ return {
155
+ code: "PROVIDER_NOT_CONFIGURED",
156
+ userMessage:
157
+ "No API key configured for inference. Add one in Settings to start chatting.",
158
+ retryable: true,
159
+ errorCategory: "provider_not_configured",
160
+ debugDetails,
161
+ };
162
+ }
163
+
152
164
  // Phase-specific overrides
153
165
  if (ctx.phase === "regenerate") {
154
166
  const base = classifyCore(error, message);
@@ -92,7 +92,6 @@ export async function prepareMemoryContext(
92
92
  degraded: false,
93
93
  injectedText: "",
94
94
  semanticHits: 0,
95
- recencyHits: 0,
96
95
  mergedCount: 0,
97
96
  selectedCount: 0,
98
97
  injectedTokens: 0,
@@ -188,7 +187,7 @@ export async function prepareMemoryContext(
188
187
  }
189
188
  : undefined,
190
189
  semanticHits: recall.semanticHits,
191
- recencyHits: recall.recencyHits,
190
+ recencyHits: 0,
192
191
  tier1Count: recall.tier1Count ?? 0,
193
192
  tier2Count: recall.tier2Count ?? 0,
194
193
  hybridSearchLatencyMs: recall.hybridSearchMs ?? 0,
@@ -31,7 +31,10 @@ import { routeGuardianReply } from "../runtime/guardian-reply-router.js";
31
31
  import { getLogger } from "../util/logger.js";
32
32
  import type { MessageQueue } from "./conversation-queue-manager.js";
33
33
  import type { QueueDrainReason } from "./conversation-queue-manager.js";
34
- import type { TrustContext } from "./conversation-runtime-assembly.js";
34
+ import type {
35
+ ChannelCapabilities,
36
+ TrustContext,
37
+ } from "./conversation-runtime-assembly.js";
35
38
  import { resolveSlash, type SlashContext } from "./conversation-slash.js";
36
39
  import { getModelInfo } from "./handlers/config-model.js";
37
40
  import type {
@@ -80,6 +83,11 @@ export interface ProcessConversationContext {
80
83
  /** Assistant identity — used for scoping notification preferences. */
81
84
  readonly assistantId?: string;
82
85
  trustContext?: TrustContext;
86
+ channelCapabilities?: ChannelCapabilities;
87
+ /** Per-turn snapshot of trustContext, frozen at message-processing start. */
88
+ currentTurnTrustContext?: TrustContext;
89
+ /** Per-turn snapshot of channelCapabilities, frozen at message-processing start. */
90
+ currentTurnChannelCapabilities?: ChannelCapabilities;
83
91
  ensureActorScopedHistory(): Promise<void>;
84
92
  persistUserMessage(
85
93
  content: string,
@@ -282,6 +290,11 @@ export async function drainQueue(
282
290
  }
283
291
  }
284
292
 
293
+ // Snapshot persona context at turn start so later tool turns can't pick up
294
+ // a different actor's context if a concurrent request mutates the live fields.
295
+ conversation.currentTurnTrustContext = conversation.trustContext;
296
+ conversation.currentTurnChannelCapabilities = conversation.channelCapabilities;
297
+
285
298
  // Resolve slash commands for queued messages
286
299
  const slashResult = await resolveSlash(
287
300
  next.content,
@@ -561,6 +574,10 @@ export async function processMessage(
561
574
  displayContent?: string,
562
575
  ): Promise<string> {
563
576
  await conversation.ensureActorScopedHistory();
577
+ // Snapshot persona context at turn start so later tool turns can't pick up
578
+ // a different actor's context if a concurrent request mutates the live fields.
579
+ conversation.currentTurnTrustContext = conversation.trustContext;
580
+ conversation.currentTurnChannelCapabilities = conversation.channelCapabilities;
564
581
  conversation.currentActiveSurfaceId = activeSurfaceId;
565
582
  conversation.currentPage = currentPage;
566
583
  const trimmedContent = content.trim();
@@ -2,7 +2,10 @@ import { v4 as uuid } from "uuid";
2
2
 
3
3
  import {
4
4
  getApp,
5
+ getAppDirPath,
5
6
  getAppPreview,
7
+ inlineDistAssets,
8
+ isMultifileApp,
6
9
  resolveAppDir,
7
10
  updateApp,
8
11
  } from "../memory/app-store.js";
@@ -1364,8 +1367,34 @@ export async function surfaceProxyResolver(
1364
1367
 
1365
1368
  const storedPreview = getAppPreview(app.id);
1366
1369
  const { dirName } = resolveAppDir(app.id);
1370
+
1371
+ // For multifile TSX apps, resolve HTML from compiled dist/index.html
1372
+ // rather than the root index.html (which is empty for formatVersion 2).
1373
+ let html = app.htmlDefinition;
1374
+ if (isMultifileApp(app)) {
1375
+ const { existsSync, readFileSync } = await import("node:fs");
1376
+ const { join } = await import("node:path");
1377
+ const appDir = getAppDirPath(app.id);
1378
+ const distIndex = join(appDir, "dist", "index.html");
1379
+ if (!existsSync(distIndex)) {
1380
+ const { compileApp } = await import("../bundler/app-compiler.js");
1381
+ const result = await compileApp(appDir);
1382
+ if (!result.ok) {
1383
+ log.warn(
1384
+ { appId, errors: result.errors },
1385
+ "Auto-compile failed on app_open",
1386
+ );
1387
+ }
1388
+ }
1389
+ if (existsSync(distIndex)) {
1390
+ html = inlineDistAssets(appDir, readFileSync(distIndex, "utf-8"));
1391
+ } else {
1392
+ html = `<p>App compilation failed. Edit a source file to trigger a rebuild.</p>`;
1393
+ }
1394
+ }
1395
+
1367
1396
  const surfaceData: DynamicPageSurfaceData = {
1368
- html: app.htmlDefinition,
1397
+ html,
1369
1398
  appId: app.id,
1370
1399
  dirName,
1371
1400
  preview: {
@@ -176,6 +176,14 @@ export class Conversation {
176
176
  /** @internal */ currentPage?: string;
177
177
  /** @internal */ channelCapabilities?: ChannelCapabilities;
178
178
  /** @internal */ trustContext?: TrustContext;
179
+ /**
180
+ * Per-turn snapshots of persona-relevant context, captured at the start of
181
+ * each message processing turn. The system prompt callback reads these
182
+ * instead of the live fields so that a concurrent request cannot swap
183
+ * another actor's persona mid-turn.
184
+ */
185
+ /** @internal */ currentTurnTrustContext?: TrustContext;
186
+ /** @internal */ currentTurnChannelCapabilities?: ChannelCapabilities;
179
187
  /** @internal */ authContext?: AuthContext;
180
188
  /** @internal */ loadedHistoryTrustClass?: TrustClass;
181
189
  /** @internal */ voiceCallControlPrompt?: string;
@@ -354,18 +362,21 @@ export class Conversation {
354
362
  const resolveSystemPromptCallback = (
355
363
  _history: import("../providers/types.js").Message[],
356
364
  ): ResolvedSystemPrompt => {
357
- const persona = resolvePersonaContext(
358
- this.trustContext,
359
- this.channelCapabilities,
360
- );
361
365
  const resolved = {
362
366
  systemPrompt: hasSystemPromptOverride
363
367
  ? systemPrompt
364
- : buildSystemPrompt({
365
- hasNoClient: this.hasNoClient,
366
- userPersona: persona.userPersona,
367
- channelPersona: persona.channelPersona,
368
- }),
368
+ : (() => {
369
+ const persona = resolvePersonaContext(
370
+ this.currentTurnTrustContext,
371
+ this.currentTurnChannelCapabilities,
372
+ );
373
+ return buildSystemPrompt({
374
+ hasNoClient: this.hasNoClient,
375
+ userPersona: persona.userPersona,
376
+ channelPersona: persona.channelPersona,
377
+ userSlug: persona.userSlug,
378
+ });
379
+ })(),
369
380
  maxTokens: configuredMaxTokens,
370
381
  };
371
382
  return resolved;
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from "../config/loader.js";
2
- import { getFailoverProvider } from "../providers/registry.js";
2
+ import { getProvider } from "../providers/registry.js";
3
3
  import {
4
4
  buildGuardianActionGenerationPrompt,
5
5
  getGuardianActionFallbackMessage,
@@ -29,10 +29,7 @@ export function createGuardianActionCopyGenerator(): GuardianActionCopyGenerator
29
29
  const config = loadConfig();
30
30
  let provider;
31
31
  try {
32
- provider = getFailoverProvider(
33
- config.services.inference.provider,
34
- config.providerOrder,
35
- );
32
+ provider = getProvider(config.services.inference.provider);
36
33
  } catch {
37
34
  return null;
38
35
  }
@@ -134,10 +131,7 @@ const VALID_FOLLOWUP_DISPOSITIONS: ReadonlySet<string> = new Set([
134
131
  export function createGuardianFollowUpConversationGenerator(): GuardianFollowUpConversationGenerator {
135
132
  return async (context) => {
136
133
  const config = loadConfig();
137
- const provider = getFailoverProvider(
138
- config.services.inference.provider,
139
- config.providerOrder,
140
- );
134
+ const provider = getProvider(config.services.inference.provider);
141
135
 
142
136
  const userPrompt = [
143
137
  `Original question from the voice call: "${context.questionText}"`,
@@ -28,6 +28,10 @@ import {
28
28
  CesUnavailableError,
29
29
  createCesProcessManager,
30
30
  } from "../credential-execution/process-manager.js";
31
+ import {
32
+ awaitCesClientWithTimeout,
33
+ DEFAULT_CES_STARTUP_TIMEOUT_MS,
34
+ } from "../credential-execution/startup-timeout.js";
31
35
  import { HeartbeatService } from "../heartbeat/heartbeat-service.js";
32
36
  import { getHookManager } from "../hooks/manager.js";
33
37
  import { installTemplates } from "../hooks/templates.js";
@@ -161,7 +165,8 @@ export interface CesStartupResult {
161
165
  export async function startCesProcess(
162
166
  config: AssistantConfig,
163
167
  ): Promise<CesStartupResult> {
164
- const shouldStartCes = isCesToolsEnabled(config) || isCesCredentialBackendEnabled(config);
168
+ const shouldStartCes =
169
+ isCesToolsEnabled(config) || isCesCredentialBackendEnabled(config);
165
170
  if (!shouldStartCes) {
166
171
  return {
167
172
  client: undefined,
@@ -456,23 +461,25 @@ export async function runDaemon(): Promise<void> {
456
461
 
457
462
  // When the credential backend flag is enabled, CES startup must complete
458
463
  // BEFORE provider initialization so credential reads can go through CES.
459
- // Block with a 3-second timeout — fall back to direct credential store
464
+ // Block with a 20-second timeout — fall back to direct credential store
460
465
  // on timeout.
461
466
  if (isCesCredentialBackendEnabled(config)) {
462
467
  const cesResult = await cesStartupPromise;
463
468
  // startCesProcess() returns immediately — the actual handshake runs
464
- // inside clientPromise. Await it (with a 3s timeout) so the CES client
469
+ // inside clientPromise. Await it (with a 20s timeout) so the CES client
465
470
  // is available before provider initialization.
466
471
  if (cesResult.clientPromise) {
467
- const client = await Promise.race([
472
+ const client = await awaitCesClientWithTimeout(
468
473
  cesResult.clientPromise,
469
- new Promise<undefined>((resolve) =>
470
- setTimeout(() => {
471
- log.warn("CES handshake timed out after 3s — falling back to direct credential store");
472
- resolve(undefined);
473
- }, 3000),
474
- ),
475
- ]);
474
+ {
475
+ timeoutMs: DEFAULT_CES_STARTUP_TIMEOUT_MS,
476
+ onTimeout: () => {
477
+ log.warn(
478
+ "CES handshake timed out after 20s — falling back to direct credential store",
479
+ );
480
+ },
481
+ },
482
+ );
476
483
  if (client) {
477
484
  setCesClient(client);
478
485
  }
@@ -371,6 +371,7 @@ export type ConversationErrorCode =
371
371
  | "PROVIDER_BILLING"
372
372
  | "PROVIDER_ORDERING"
373
373
  | "PROVIDER_WEB_SEARCH"
374
+ | "PROVIDER_NOT_CONFIGURED"
374
375
  | "CONTEXT_TOO_LARGE"
375
376
  | "CONVERSATION_ABORTED"
376
377
  | "CONVERSATION_PROCESSING_FAILED"
@@ -41,7 +41,7 @@ import { getOrCreateConversation } from "../memory/conversation-key-store.js";
41
41
  import { buildSystemPrompt } from "../prompts/system-prompt.js";
42
42
  import { RateLimitProvider } from "../providers/ratelimit.js";
43
43
  import {
44
- getFailoverProvider,
44
+ getProvider,
45
45
  initializeProviders,
46
46
  } from "../providers/registry.js";
47
47
  import { buildAssistantEvent } from "../runtime/assistant-event.js";
@@ -702,9 +702,8 @@ export class DaemonServer {
702
702
 
703
703
  const createPromise = (async () => {
704
704
  const config = getConfig();
705
- let provider = getFailoverProvider(
705
+ let provider = getProvider(
706
706
  config.services.inference.provider,
707
- config.providerOrder,
708
707
  );
709
708
  const { rateLimit } = config;
710
709
  if (rateLimit.maxRequestsPerMinute > 0) {
@@ -64,6 +64,37 @@ export function isMultifileApp(app: AppDefinition): boolean {
64
64
  return app.formatVersion === 2;
65
65
  }
66
66
 
67
+ /**
68
+ * Inline dist assets (main.js, main.css) into the compiled HTML so it can be
69
+ * delivered as a self-contained string via loadHTMLString/SSE without needing
70
+ * the client to resolve external script/stylesheet URLs.
71
+ */
72
+ export function inlineDistAssets(appDir: string, html: string): string {
73
+ const distDir = join(appDir, "dist");
74
+
75
+ // Inline main.js
76
+ const jsPath = join(distDir, "main.js");
77
+ if (existsSync(jsPath)) {
78
+ const js = readFileSync(jsPath, "utf-8");
79
+ html = html.replace(
80
+ /<script\s+type="module"\s+src="main\.js"\s*><\/script>/,
81
+ `<script type="module">${js}</script>`,
82
+ );
83
+ }
84
+
85
+ // Inline main.css
86
+ const cssPath = join(distDir, "main.css");
87
+ if (existsSync(cssPath)) {
88
+ const css = readFileSync(cssPath, "utf-8");
89
+ html = html.replace(
90
+ /<link\s+rel="stylesheet"\s+href="main\.css"\s*>/,
91
+ `<style>${css}</style>`,
92
+ );
93
+ }
94
+
95
+ return html;
96
+ }
97
+
67
98
  export interface AppRecord {
68
99
  id: string;
69
100
  appId: string;
@@ -34,6 +34,7 @@ import {
34
34
  createSequenceTables,
35
35
  createTasksAndWorkItemsTables,
36
36
  createWatchersAndLogsTables,
37
+ migrateAddSourceTypeColumns,
37
38
  migrateAssistantContactMetadata,
38
39
  migrateBackfillAudioAttachmentMimeTypes,
39
40
  migrateBackfillContactInteractionStats,
@@ -504,6 +505,9 @@ export function initializeDb(): void {
504
505
  // 89. Add user_file column to contacts for per-user persona file mapping
505
506
  migrateContactsUserFileColumn(database);
506
507
 
508
+ // 90. Add source_type and source_message_role columns to memory_items
509
+ migrateAddSourceTypeColumns(database);
510
+
507
511
  validateMigrationState(database);
508
512
 
509
513
  if (process.env.BUN_TEST === "1") {