@vellumai/assistant 0.4.53 → 0.4.54

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 (247) hide show
  1. package/bun.lock +62 -349
  2. package/docs/architecture/integrations.md +1 -1
  3. package/docs/architecture/keychain-broker.md +94 -29
  4. package/docs/architecture/security.md +2 -2
  5. package/knip.json +7 -29
  6. package/package.json +2 -9
  7. package/src/__tests__/agent-loop.test.ts +1 -1
  8. package/src/__tests__/app-git-history.test.ts +0 -2
  9. package/src/__tests__/app-git-service.test.ts +1 -6
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/avatar-e2e.test.ts +0 -1
  12. package/src/__tests__/browser-fill-credential.test.ts +1 -6
  13. package/src/__tests__/call-domain.test.ts +0 -1
  14. package/src/__tests__/call-routes-http.test.ts +0 -1
  15. package/src/__tests__/channel-guardian.test.ts +4 -4
  16. package/src/__tests__/channel-readiness-routes.test.ts +0 -1
  17. package/src/__tests__/channel-readiness-service.test.ts +0 -1
  18. package/src/__tests__/checker.test.ts +13 -11
  19. package/src/__tests__/claude-code-skill-regression.test.ts +0 -1
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +0 -3
  22. package/src/__tests__/config-schema.test.ts +3 -9
  23. package/src/__tests__/config-watcher.test.ts +11 -3
  24. package/src/__tests__/credential-broker-browser-fill.test.ts +27 -24
  25. package/src/__tests__/credential-broker-server-use.test.ts +60 -24
  26. package/src/__tests__/credential-security-e2e.test.ts +1 -6
  27. package/src/__tests__/credential-security-invariants.test.ts +13 -8
  28. package/src/__tests__/credential-vault-unit.test.ts +28 -12
  29. package/src/__tests__/credential-vault.test.ts +40 -28
  30. package/src/__tests__/credentials-cli.test.ts +1 -21
  31. package/src/__tests__/email-invite-adapter.test.ts +0 -1
  32. package/src/__tests__/fixtures/credential-security-fixtures.ts +3 -3
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -79
  34. package/src/__tests__/gateway-only-enforcement.test.ts +1 -21
  35. package/src/__tests__/guardian-action-conversation-turn.test.ts +8 -8
  36. package/src/__tests__/guardian-action-late-reply.test.ts +13 -14
  37. package/src/__tests__/guardian-action-store.test.ts +0 -57
  38. package/src/__tests__/guardian-outbound-http.test.ts +1 -1
  39. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -3
  40. package/src/__tests__/hooks-blocking.test.ts +1 -1
  41. package/src/__tests__/hooks-config.test.ts +5 -29
  42. package/src/__tests__/hooks-discovery.test.ts +1 -1
  43. package/src/__tests__/hooks-integration.test.ts +1 -1
  44. package/src/__tests__/hooks-manager.test.ts +1 -1
  45. package/src/__tests__/hooks-runner.test.ts +1 -23
  46. package/src/__tests__/hooks-settings.test.ts +1 -1
  47. package/src/__tests__/hooks-templates.test.ts +1 -1
  48. package/src/__tests__/integration-status.test.ts +0 -1
  49. package/src/__tests__/invite-routes-http.test.ts +0 -3
  50. package/src/__tests__/llm-usage-store.test.ts +50 -0
  51. package/src/__tests__/managed-proxy-context.test.ts +41 -41
  52. package/src/__tests__/media-generate-image.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -6
  54. package/src/__tests__/memory-regressions.experimental.test.ts +4 -4
  55. package/src/__tests__/memory-regressions.test.ts +27 -27
  56. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  57. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -4
  58. package/src/__tests__/notification-decision-fallback.test.ts +1 -1
  59. package/src/__tests__/oauth-cli.test.ts +1 -4
  60. package/src/__tests__/oauth-store.test.ts +1 -3
  61. package/src/__tests__/openai-provider.test.ts +7 -7
  62. package/src/__tests__/platform.test.ts +14 -4
  63. package/src/__tests__/pricing.test.ts +0 -223
  64. package/src/__tests__/provider-commit-message-generator.test.ts +1 -4
  65. package/src/__tests__/provider-fail-open-selection.test.ts +58 -54
  66. package/src/__tests__/provider-managed-proxy-integration.test.ts +63 -63
  67. package/src/__tests__/provider-registry-ollama.test.ts +3 -3
  68. package/src/__tests__/public-ingress-urls.test.ts +1 -1
  69. package/src/__tests__/registry.test.ts +3 -103
  70. package/src/__tests__/script-proxy-injection-runtime.test.ts +2 -7
  71. package/src/__tests__/secret-onetime-send.test.ts +1 -6
  72. package/src/__tests__/secret-routes-managed-proxy.test.ts +6 -13
  73. package/src/__tests__/secure-keys.test.ts +241 -229
  74. package/src/__tests__/session-abort-tool-results.test.ts +0 -1
  75. package/src/__tests__/session-confirmation-signals.test.ts +0 -1
  76. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -7
  77. package/src/__tests__/session-pre-run-repair.test.ts +0 -1
  78. package/src/__tests__/session-provider-retry-repair.test.ts +0 -1
  79. package/src/__tests__/session-queue.test.ts +2 -4
  80. package/src/__tests__/session-slash-known.test.ts +0 -1
  81. package/src/__tests__/session-slash-queue.test.ts +0 -1
  82. package/src/__tests__/session-slash-unknown.test.ts +0 -1
  83. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  84. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  85. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  86. package/src/__tests__/slack-channel-config.test.ts +1 -7
  87. package/src/__tests__/swarm-recursion.test.ts +0 -1
  88. package/src/__tests__/swarm-session-integration.test.ts +0 -1
  89. package/src/__tests__/swarm-tool.test.ts +0 -1
  90. package/src/__tests__/task-compiler.test.ts +1 -1
  91. package/src/__tests__/test-support/browser-skill-harness.ts +0 -18
  92. package/src/__tests__/test-support/computer-use-skill-harness.ts +0 -23
  93. package/src/__tests__/tool-executor.test.ts +1 -1
  94. package/src/__tests__/trust-store.test.ts +3 -82
  95. package/src/__tests__/twilio-config.test.ts +0 -1
  96. package/src/__tests__/twilio-provider.test.ts +0 -5
  97. package/src/__tests__/twilio-routes.test.ts +0 -1
  98. package/src/__tests__/usage-cache-backfill-migration.test.ts +10 -10
  99. package/src/calls/guardian-question-copy.ts +1 -1
  100. package/src/cli/commands/doctor.ts +10 -34
  101. package/src/cli/commands/memory.ts +3 -5
  102. package/src/cli/commands/sessions.ts +1 -1
  103. package/src/cli/commands/usage.ts +359 -0
  104. package/src/cli/http-client.ts +22 -12
  105. package/src/cli/program.ts +2 -0
  106. package/src/cli/reference.ts +1 -0
  107. package/src/cli.ts +251 -181
  108. package/src/config/assistant-feature-flags.ts +0 -7
  109. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  110. package/src/config/bundled-skills/claude-code/SKILL.md +1 -1
  111. package/src/config/bundled-skills/claude-code/TOOLS.json +1 -1
  112. package/src/config/bundled-skills/gmail/SKILL.md +0 -1
  113. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  114. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  115. package/src/config/bundled-skills/messaging/SKILL.md +0 -1
  116. package/src/config/bundled-skills/sequences/SKILL.md +0 -1
  117. package/src/config/env.ts +13 -0
  118. package/src/config/feature-flag-registry.json +9 -41
  119. package/src/config/schemas/security.ts +1 -2
  120. package/src/config/skills.ts +1 -1
  121. package/src/contacts/contact-store.ts +0 -50
  122. package/src/daemon/approved-devices-store.ts +0 -44
  123. package/src/daemon/classifier.ts +1 -1
  124. package/src/daemon/config-watcher.ts +12 -6
  125. package/src/daemon/handlers/config-model.ts +1 -1
  126. package/src/daemon/handlers/sessions.ts +4 -116
  127. package/src/daemon/handlers/skills.ts +1 -1
  128. package/src/daemon/lifecycle.ts +13 -15
  129. package/src/daemon/providers-setup.ts +1 -1
  130. package/src/daemon/server.ts +19 -3
  131. package/src/daemon/session-slash.ts +2 -2
  132. package/src/daemon/shutdown-handlers.ts +15 -0
  133. package/src/daemon/watch-handler.ts +2 -2
  134. package/src/email/guardrails.ts +1 -1
  135. package/src/email/service.ts +0 -5
  136. package/src/hooks/templates.ts +1 -1
  137. package/src/media/app-icon-generator.ts +2 -2
  138. package/src/media/avatar-router.ts +2 -2
  139. package/src/media/gemini-image-service.ts +5 -5
  140. package/src/memory/admin.ts +2 -2
  141. package/src/memory/app-git-service.ts +0 -7
  142. package/src/memory/conversation-crud.ts +1 -1
  143. package/src/memory/conversation-title-service.ts +2 -2
  144. package/src/memory/embedding-backend.ts +30 -26
  145. package/src/memory/external-conversation-store.ts +0 -30
  146. package/src/memory/guardian-action-store.ts +0 -31
  147. package/src/memory/guardian-approvals.ts +1 -56
  148. package/src/memory/indexer.ts +4 -3
  149. package/src/memory/items-extractor.ts +1 -1
  150. package/src/memory/job-handlers/backfill.ts +5 -2
  151. package/src/memory/job-handlers/index-maintenance.ts +2 -2
  152. package/src/memory/job-handlers/media-processing.ts +2 -2
  153. package/src/memory/job-handlers/summarization.ts +1 -1
  154. package/src/memory/job-utils.ts +1 -2
  155. package/src/memory/jobs-worker.ts +2 -2
  156. package/src/memory/llm-usage-store.ts +57 -11
  157. package/src/memory/media-store.ts +4 -535
  158. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +2 -2
  159. package/src/memory/migrations/110-channel-guardian.ts +0 -1
  160. package/src/memory/published-pages-store.ts +0 -83
  161. package/src/memory/qdrant-circuit-breaker.ts +0 -8
  162. package/src/memory/retriever.ts +1 -1
  163. package/src/memory/search/semantic.ts +1 -8
  164. package/src/memory/shared-app-links-store.ts +0 -15
  165. package/src/messaging/registry.ts +0 -5
  166. package/src/messaging/style-analyzer.ts +1 -1
  167. package/src/notifications/copy-composer.ts +5 -13
  168. package/src/notifications/decision-engine.ts +2 -2
  169. package/src/notifications/deliveries-store.ts +0 -39
  170. package/src/notifications/guardian-question-mode.ts +6 -10
  171. package/src/notifications/preference-extractor.ts +1 -1
  172. package/src/oauth/byo-connection.test.ts +29 -20
  173. package/src/oauth/provider-behaviors.ts +1 -1
  174. package/src/permissions/checker.ts +1 -1
  175. package/src/permissions/shell-identity.ts +0 -5
  176. package/src/permissions/trust-store.ts +0 -37
  177. package/src/prompts/system-prompt.ts +3 -3
  178. package/src/providers/managed-proxy/constants.ts +8 -10
  179. package/src/providers/managed-proxy/context.ts +14 -9
  180. package/src/providers/provider-send-message.ts +4 -52
  181. package/src/providers/registry.ts +16 -50
  182. package/src/runtime/actor-token-store.ts +0 -23
  183. package/src/runtime/http-router.ts +5 -1
  184. package/src/runtime/http-server.ts +101 -4
  185. package/src/runtime/invite-instruction-generator.ts +25 -51
  186. package/src/runtime/invite-service.ts +0 -20
  187. package/src/runtime/routes/attachment-routes.ts +1 -1
  188. package/src/runtime/routes/brain-graph-routes.ts +1 -1
  189. package/src/runtime/routes/call-routes.ts +1 -1
  190. package/src/runtime/routes/conversation-routes.ts +32 -11
  191. package/src/runtime/routes/debug-routes.ts +1 -1
  192. package/src/runtime/routes/diagnostics-routes.ts +2 -2
  193. package/src/runtime/routes/documents-routes.ts +3 -3
  194. package/src/runtime/routes/global-search-routes.ts +1 -1
  195. package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -20
  196. package/src/runtime/routes/guardian-refresh-routes.ts +0 -20
  197. package/src/runtime/routes/secret-routes.ts +4 -4
  198. package/src/runtime/routes/trust-rules-routes.ts +1 -1
  199. package/src/security/credential-backend.ts +148 -0
  200. package/src/security/oauth2.ts +1 -1
  201. package/src/security/secret-allowlist.ts +1 -1
  202. package/src/security/secure-keys.ts +98 -160
  203. package/src/security/token-manager.ts +0 -7
  204. package/src/sequence/guardrails.ts +0 -4
  205. package/src/sequence/store.ts +1 -20
  206. package/src/sequence/types.ts +1 -36
  207. package/src/signals/cancel.ts +69 -0
  208. package/src/signals/conversation-undo.ts +127 -0
  209. package/src/signals/trust-rule.ts +174 -0
  210. package/src/skills/clawhub.ts +5 -5
  211. package/src/skills/managed-store.ts +4 -4
  212. package/src/telemetry/usage-telemetry-reporter.test.ts +366 -0
  213. package/src/telemetry/usage-telemetry-reporter.ts +181 -0
  214. package/src/tools/claude-code/claude-code.ts +2 -2
  215. package/src/tools/credentials/vault.ts +8 -4
  216. package/src/tools/memory/handlers.test.ts +24 -26
  217. package/src/tools/memory/handlers.ts +1 -13
  218. package/src/tools/registry.ts +5 -100
  219. package/src/tools/terminal/parser.ts +34 -4
  220. package/src/tools/tool-manifest.ts +0 -10
  221. package/src/usage/actors.ts +0 -12
  222. package/src/util/canonicalize-identity.ts +0 -9
  223. package/src/util/errors.ts +0 -3
  224. package/src/util/platform.ts +24 -7
  225. package/src/util/pricing.ts +0 -38
  226. package/src/watcher/constants.ts +0 -7
  227. package/src/watcher/providers/linear.ts +1 -1
  228. package/src/work-items/work-item-store.ts +4 -4
  229. package/src/workspace/commit-message-provider.ts +1 -1
  230. package/src/workspace/git-service.ts +44 -1
  231. package/src/workspace/provider-commit-message-generator.ts +1 -1
  232. package/src/__tests__/fixtures/proxy-fixtures.ts +0 -147
  233. package/src/browser-extension-relay/client.ts +0 -155
  234. package/src/contacts/index.ts +0 -18
  235. package/src/daemon/tls-certs.ts +0 -270
  236. package/src/errors.ts +0 -41
  237. package/src/events/index.ts +0 -18
  238. package/src/followups/index.ts +0 -10
  239. package/src/playbooks/index.ts +0 -10
  240. package/src/runtime/auth/index.ts +0 -44
  241. package/src/tasks/candidate-store.ts +0 -95
  242. package/src/tools/browser/api-map.ts +0 -313
  243. package/src/tools/browser/auto-navigate.ts +0 -469
  244. package/src/tools/browser/headless-browser.ts +0 -590
  245. package/src/tools/browser/recording-store.ts +0 -75
  246. package/src/tools/computer-use/registry.ts +0 -21
  247. package/src/tools/tasks/index.ts +0 -27
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { getPlatformBaseUrl } from "../../config/env.js";
13
13
  import { credentialKey } from "../../security/credential-key.js";
14
- import { getSecureKey } from "../../security/secure-keys.js";
14
+ import { getSecureKeyAsync } from "../../security/secure-keys.js";
15
15
  import { MANAGED_PROVIDER_META } from "./constants.js";
16
16
 
17
17
  /** Storage key for the assistant API key credential. */
@@ -35,9 +35,10 @@ export interface ManagedProxyContext {
35
35
  * Returns an enabled context only when both the platform base URL and
36
36
  * the assistant API key are present. Otherwise returns a disabled context.
37
37
  */
38
- export function resolveManagedProxyContext(): ManagedProxyContext {
38
+ export async function resolveManagedProxyContext(): Promise<ManagedProxyContext> {
39
39
  const platformBaseUrl = getPlatformBaseUrl().replace(/\/+$/, "");
40
- const assistantApiKey = getSecureKey(ASSISTANT_API_KEY_STORAGE_KEY) ?? "";
40
+ const assistantApiKey =
41
+ (await getSecureKeyAsync(ASSISTANT_API_KEY_STORAGE_KEY)) ?? "";
41
42
 
42
43
  const enabled = !!platformBaseUrl && !!assistantApiKey;
43
44
 
@@ -48,8 +49,8 @@ export function resolveManagedProxyContext(): ManagedProxyContext {
48
49
  * Check whether managed proxy prerequisites are available.
49
50
  * Shorthand for checking that both platform URL and assistant API key exist.
50
51
  */
51
- export function hasManagedProxyPrereqs(): boolean {
52
- return resolveManagedProxyContext().enabled;
52
+ export async function hasManagedProxyPrereqs(): Promise<boolean> {
53
+ return (await resolveManagedProxyContext()).enabled;
53
54
  }
54
55
 
55
56
  /**
@@ -58,11 +59,13 @@ export function hasManagedProxyPrereqs(): boolean {
58
59
  * Combines the platform base URL with the provider's deterministic proxy path.
59
60
  * Returns undefined if the provider is not managed or prerequisites are missing.
60
61
  */
61
- export function buildManagedBaseUrl(provider: string): string | undefined {
62
+ export async function buildManagedBaseUrl(
63
+ provider: string,
64
+ ): Promise<string | undefined> {
62
65
  const meta = MANAGED_PROVIDER_META[provider];
63
66
  if (!meta?.managed || !meta.proxyPath) return undefined;
64
67
 
65
- const ctx = resolveManagedProxyContext();
68
+ const ctx = await resolveManagedProxyContext();
66
69
  if (!ctx.enabled) return undefined;
67
70
 
68
71
  return `${ctx.platformBaseUrl}${meta.proxyPath}`;
@@ -74,8 +77,10 @@ export function buildManagedBaseUrl(provider: string): string | undefined {
74
77
  * Returns true when the provider supports managed proxy routing and
75
78
  * all prerequisites (platform URL + assistant API key) are satisfied.
76
79
  */
77
- export function managedFallbackEnabledFor(provider: string): boolean {
80
+ export async function managedFallbackEnabledFor(
81
+ provider: string,
82
+ ): Promise<boolean> {
78
83
  const meta = MANAGED_PROVIDER_META[provider];
79
84
  if (!meta?.managed) return false;
80
- return hasManagedProxyPrereqs();
85
+ return await hasManagedProxyPrereqs();
81
86
  }
@@ -40,12 +40,12 @@ let fallbackWarningLogged = false;
40
40
  *
41
41
  * Returns `null` when no providers are available at all.
42
42
  */
43
- export function resolveConfiguredProvider(): ConfiguredProviderResult | null {
43
+ export async function resolveConfiguredProvider(): Promise<ConfiguredProviderResult | null> {
44
44
  const config = getConfig();
45
45
 
46
46
  if (listProviders().length === 0) {
47
47
  try {
48
- initializeProviders(config);
48
+ await initializeProviders(config);
49
49
  } catch {
50
50
  return null;
51
51
  }
@@ -89,8 +89,8 @@ export function resolveConfiguredProvider(): ConfiguredProviderResult | null {
89
89
  *
90
90
  * Returns `null` when no providers are available.
91
91
  */
92
- export function getConfiguredProvider(): Provider | null {
93
- const result = resolveConfiguredProvider();
92
+ export async function getConfiguredProvider(): Promise<Provider | null> {
93
+ const result = await resolveConfiguredProvider();
94
94
  return result?.provider ?? null;
95
95
  }
96
96
 
@@ -170,51 +170,3 @@ export function extractToolUse(
170
170
  export function userMessage(text: string): Message {
171
171
  return { role: "user", content: [{ type: "text", text }] };
172
172
  }
173
-
174
- /**
175
- * Build a single user message with image + text content.
176
- */
177
- export function userMessageWithImage(
178
- imageBase64: string,
179
- mediaType: string,
180
- text: string,
181
- ): Message {
182
- return {
183
- role: "user",
184
- content: [
185
- {
186
- type: "image",
187
- source: {
188
- type: "base64",
189
- media_type: mediaType,
190
- data: imageBase64,
191
- },
192
- },
193
- { type: "text", text },
194
- ],
195
- };
196
- }
197
-
198
- /**
199
- * Build a single user message with multiple images followed by a text block.
200
- * Each image becomes its own content block; the text block comes last.
201
- */
202
- export function userMessageWithImages(
203
- images: Array<{ base64: string; mediaType: string }>,
204
- text: string,
205
- ): Message {
206
- return {
207
- role: "user",
208
- content: [
209
- ...images.map((img) => ({
210
- type: "image" as const,
211
- source: {
212
- type: "base64" as const,
213
- media_type: img.mediaType,
214
- data: img.base64,
215
- },
216
- })),
217
- { type: "text" as const, text },
218
- ],
219
- };
220
- }
@@ -1,5 +1,5 @@
1
1
  import { wrapWithLogfire } from "../logfire.js";
2
- import { getSecureKey } from "../security/secure-keys.js";
2
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
3
3
  import { ConfigError, ProviderNotConfiguredError } from "../util/errors.js";
4
4
  import { AnthropicProvider } from "./anthropic/client.js";
5
5
  import { FailoverProvider, type ProviderHealthStatus } from "./failover.js";
@@ -202,7 +202,9 @@ export function getProviderDebugStatus(
202
202
  };
203
203
  }
204
204
 
205
- export function initializeProviders(config: ProvidersConfig): void {
205
+ export async function initializeProviders(
206
+ config: ProvidersConfig,
207
+ ): Promise<void> {
206
208
  providers.clear();
207
209
  routingSources.clear();
208
210
  cachedFailoverProvider = null;
@@ -211,7 +213,7 @@ export function initializeProviders(config: ProvidersConfig): void {
211
213
  const streamTimeoutMs =
212
214
  (config.timeouts?.providerStreamTimeoutSec ?? 300) * 1000;
213
215
 
214
- const anthropicKey = getSecureKey("anthropic");
216
+ const anthropicKey = await getSecureKeyAsync("anthropic");
215
217
  if (anthropicKey) {
216
218
  const model = resolveModel(config, "anthropic");
217
219
  registerProvider(
@@ -228,9 +230,9 @@ export function initializeProviders(config: ProvidersConfig): void {
228
230
  routingSources.set("anthropic", "user-key");
229
231
  } else {
230
232
  // No user Anthropic key — route through managed proxy
231
- const managedBaseUrl = buildManagedBaseUrl("anthropic");
233
+ const managedBaseUrl = await buildManagedBaseUrl("anthropic");
232
234
  if (managedBaseUrl) {
233
- const ctx = resolveManagedProxyContext();
235
+ const ctx = await resolveManagedProxyContext();
234
236
  const model = resolveModel(config, "anthropic");
235
237
  registerProvider(
236
238
  "anthropic",
@@ -248,7 +250,7 @@ export function initializeProviders(config: ProvidersConfig): void {
248
250
  routingSources.set("anthropic", "managed-proxy");
249
251
  }
250
252
  }
251
- const openaiKey = getSecureKey("openai");
253
+ const openaiKey = await getSecureKeyAsync("openai");
252
254
  if (openaiKey) {
253
255
  const model = resolveModel(config, "openai");
254
256
  registerProvider(
@@ -261,9 +263,9 @@ export function initializeProviders(config: ProvidersConfig): void {
261
263
  );
262
264
  routingSources.set("openai", "user-key");
263
265
  } else {
264
- const managedBaseUrl = buildManagedBaseUrl("openai");
266
+ const managedBaseUrl = await buildManagedBaseUrl("openai");
265
267
  if (managedBaseUrl) {
266
- const ctx = resolveManagedProxyContext();
268
+ const ctx = await resolveManagedProxyContext();
267
269
  const model = resolveModel(config, "openai");
268
270
  registerProvider(
269
271
  "openai",
@@ -279,7 +281,7 @@ export function initializeProviders(config: ProvidersConfig): void {
279
281
  routingSources.set("openai", "managed-proxy");
280
282
  }
281
283
  }
282
- const geminiKey = getSecureKey("gemini");
284
+ const geminiKey = await getSecureKeyAsync("gemini");
283
285
  if (geminiKey) {
284
286
  const model = resolveModel(config, "gemini");
285
287
  registerProvider(
@@ -293,9 +295,9 @@ export function initializeProviders(config: ProvidersConfig): void {
293
295
  routingSources.set("gemini", "user-key");
294
296
  } else {
295
297
  // No user Gemini key — route through Vertex managed proxy
296
- const managedBaseUrl = buildManagedBaseUrl("vertex");
298
+ const managedBaseUrl = await buildManagedBaseUrl("vertex");
297
299
  if (managedBaseUrl) {
298
- const ctx = resolveManagedProxyContext();
300
+ const ctx = await resolveManagedProxyContext();
299
301
  const model = resolveModel(config, "gemini");
300
302
  registerProvider(
301
303
  "gemini",
@@ -311,7 +313,7 @@ export function initializeProviders(config: ProvidersConfig): void {
311
313
  routingSources.set("gemini", "managed-proxy");
312
314
  }
313
315
  }
314
- const ollamaKey = getSecureKey("ollama");
316
+ const ollamaKey = await getSecureKeyAsync("ollama");
315
317
  if (config.provider === "ollama" || ollamaKey) {
316
318
  const model = resolveModel(config, "ollama");
317
319
  registerProvider(
@@ -327,7 +329,7 @@ export function initializeProviders(config: ProvidersConfig): void {
327
329
  );
328
330
  routingSources.set("ollama", "user-key");
329
331
  }
330
- const fireworksKey = getSecureKey("fireworks");
332
+ const fireworksKey = await getSecureKeyAsync("fireworks");
331
333
  if (fireworksKey) {
332
334
  const model = resolveModel(config, "fireworks");
333
335
  registerProvider(
@@ -341,26 +343,8 @@ export function initializeProviders(config: ProvidersConfig): void {
341
343
  ),
342
344
  );
343
345
  routingSources.set("fireworks", "user-key");
344
- } else {
345
- const managedBaseUrl = buildManagedBaseUrl("fireworks");
346
- if (managedBaseUrl) {
347
- const ctx = resolveManagedProxyContext();
348
- const model = resolveModel(config, "fireworks");
349
- registerProvider(
350
- "fireworks",
351
- new RetryProvider(
352
- wrapWithLogfire(
353
- new FireworksProvider(ctx.assistantApiKey, model, {
354
- baseURL: managedBaseUrl,
355
- streamTimeoutMs,
356
- }),
357
- ),
358
- ),
359
- );
360
- routingSources.set("fireworks", "managed-proxy");
361
- }
362
346
  }
363
- const openrouterKey = getSecureKey("openrouter");
347
+ const openrouterKey = await getSecureKeyAsync("openrouter");
364
348
  if (openrouterKey) {
365
349
  const model = resolveModel(config, "openrouter");
366
350
  registerProvider(
@@ -374,23 +358,5 @@ export function initializeProviders(config: ProvidersConfig): void {
374
358
  ),
375
359
  );
376
360
  routingSources.set("openrouter", "user-key");
377
- } else {
378
- const managedBaseUrl = buildManagedBaseUrl("openrouter");
379
- if (managedBaseUrl) {
380
- const ctx = resolveManagedProxyContext();
381
- const model = resolveModel(config, "openrouter");
382
- registerProvider(
383
- "openrouter",
384
- new RetryProvider(
385
- wrapWithLogfire(
386
- new OpenRouterProvider(ctx.assistantApiKey, model, {
387
- baseURL: managedBaseUrl,
388
- streamTimeoutMs,
389
- }),
390
- ),
391
- ),
392
- );
393
- routingSources.set("openrouter", "managed-proxy");
394
- }
395
361
  }
396
362
  }
@@ -155,29 +155,6 @@ export function revokeByDeviceBinding(
155
155
  return matching.length;
156
156
  }
157
157
 
158
- /**
159
- * Find all active actor token records for a given guardianPrincipalId.
160
- * Used for multi-device guardian fanout — returns all bound devices (macOS, iOS, etc.)
161
- * so notification targeting can reach every device for the same guardian identity.
162
- */
163
- export function findActiveByGuardianPrincipalId(
164
- guardianPrincipalId: string,
165
- ): ActorTokenRecord[] {
166
- const db = getDb();
167
- const rows = db
168
- .select()
169
- .from(actorTokenRecords)
170
- .where(
171
- and(
172
- eq(actorTokenRecords.guardianPrincipalId, guardianPrincipalId),
173
- eq(actorTokenRecords.status, "active"),
174
- ),
175
- )
176
- .all();
177
-
178
- return rows.map(rowToRecord);
179
- }
180
-
181
158
  /**
182
159
  * Revoke a single token by its hash.
183
160
  */
@@ -86,10 +86,14 @@ export class HttpRouter {
86
86
  server: ReturnType<typeof Bun.serve>,
87
87
  authContext: AuthContext,
88
88
  ): Promise<Response | null> {
89
+ // Normalize trailing slashes so "/integrations/twilio/config/" matches
90
+ // a route defined as "integrations/twilio/config".
91
+ const normalized = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
92
+
89
93
  for (const compiled of this.compiledRoutes) {
90
94
  if (compiled.def.method !== req.method) continue;
91
95
 
92
- const match = endpoint.match(compiled.regex);
96
+ const match = normalized.match(compiled.regex);
93
97
  if (!match) continue;
94
98
 
95
99
  // Extract named params
@@ -42,6 +42,10 @@ import {
42
42
  recordConversationSeenSignal,
43
43
  type SignalType,
44
44
  } from "../memory/conversation-attention-store.js";
45
+ import {
46
+ getConversation,
47
+ getDisplayMetaForConversations,
48
+ } from "../memory/conversation-crud.js";
45
49
  import {
46
50
  countConversations,
47
51
  listConversations,
@@ -271,16 +275,16 @@ export class RuntimeHttpServer {
271
275
  }
272
276
 
273
277
  private get pairingContext(): PairingHandlerContext {
274
- const ipcBroadcast = this.pairingBroadcast;
278
+ const broadcast = this.pairingBroadcast;
275
279
  return {
276
280
  pairingStore: this.pairingStore,
277
281
  bearerToken: this.bearerToken,
278
282
  featureFlagToken: this.readFeatureFlagToken(),
279
- pairingBroadcast: ipcBroadcast
283
+ pairingBroadcast: broadcast
280
284
  ? (msg) => {
281
285
  // Broadcast to all clients via the event hub so HTTP/SSE clients
282
286
  // (e.g. macOS app) receive pairing approval requests.
283
- ipcBroadcast(msg);
287
+ broadcast(msg);
284
288
  void assistantEventHub.publish(
285
289
  buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg),
286
290
  );
@@ -795,6 +799,7 @@ export class RuntimeHttpServer {
795
799
  const conversations = listConversations(limit, false, offset);
796
800
  const totalCount = countConversations();
797
801
  const conversationIds = conversations.map((c) => c.id);
802
+ const displayMeta = getDisplayMetaForConversations(conversationIds);
798
803
  const bindings =
799
804
  externalConversationStore.getBindingsForConversations(
800
805
  conversationIds,
@@ -856,13 +861,22 @@ export class RuntimeHttpServer {
856
861
  ? { conversationOriginChannel: originChannel }
857
862
  : {}),
858
863
  ...(assistantAttention ? { assistantAttention } : {}),
864
+ ...(displayMeta.get(c.id)?.isPinned
865
+ ? {
866
+ isPinned: true,
867
+ displayOrder: displayMeta.get(c.id)!.displayOrder,
868
+ }
869
+ : displayMeta.get(c.id)?.displayOrder != null
870
+ ? {
871
+ displayOrder: displayMeta.get(c.id)!.displayOrder,
872
+ }
873
+ : {}),
859
874
  };
860
875
  }),
861
876
  hasMore: offset + conversations.length < totalCount,
862
877
  });
863
878
  },
864
879
  },
865
-
866
880
  ...conversationAttentionRouteDefinitions(),
867
881
 
868
882
  ...(this.sessionManagementDeps
@@ -933,6 +947,89 @@ export class RuntimeHttpServer {
933
947
  },
934
948
  },
935
949
 
950
+ // conversations/:id must be registered AFTER all literal conversations/<word>
951
+ // routes above (attention, seen, unread) so the parameterized :id does not
952
+ // shadow them.
953
+ {
954
+ endpoint: "conversations/:id",
955
+ method: "GET",
956
+ handler: ({ params }) => {
957
+ const conversation = getConversation(params.id);
958
+ if (!conversation) {
959
+ return httpError(
960
+ "NOT_FOUND",
961
+ `Conversation ${params.id} not found`,
962
+ 404,
963
+ );
964
+ }
965
+ const bindings =
966
+ externalConversationStore.getBindingsForConversations([
967
+ conversation.id,
968
+ ]);
969
+ const attentionStates = getAttentionStateByConversationIds([
970
+ conversation.id,
971
+ ]);
972
+ const binding = bindings.get(conversation.id);
973
+ const originChannel = parseChannelId(conversation.originChannel);
974
+ const attn = attentionStates.get(conversation.id);
975
+ const assistantAttention = attn
976
+ ? {
977
+ hasUnseenLatestAssistantMessage:
978
+ attn.latestAssistantMessageAt != null &&
979
+ (attn.lastSeenAssistantMessageAt == null ||
980
+ attn.lastSeenAssistantMessageAt <
981
+ attn.latestAssistantMessageAt),
982
+ ...(attn.latestAssistantMessageAt != null
983
+ ? {
984
+ latestAssistantMessageAt: attn.latestAssistantMessageAt,
985
+ }
986
+ : {}),
987
+ ...(attn.lastSeenAssistantMessageAt != null
988
+ ? {
989
+ lastSeenAssistantMessageAt:
990
+ attn.lastSeenAssistantMessageAt,
991
+ }
992
+ : {}),
993
+ ...(attn.lastSeenConfidence != null
994
+ ? { lastSeenConfidence: attn.lastSeenConfidence }
995
+ : {}),
996
+ ...(attn.lastSeenSignalType != null
997
+ ? { lastSeenSignalType: attn.lastSeenSignalType }
998
+ : {}),
999
+ }
1000
+ : undefined;
1001
+ return Response.json({
1002
+ session: {
1003
+ id: conversation.id,
1004
+ title: conversation.title ?? "Untitled",
1005
+ createdAt: conversation.createdAt,
1006
+ updatedAt: conversation.updatedAt,
1007
+ threadType:
1008
+ conversation.threadType === "private" ? "private" : "standard",
1009
+ source: conversation.source ?? "user",
1010
+ ...(conversation.scheduleJobId
1011
+ ? { scheduleJobId: conversation.scheduleJobId }
1012
+ : {}),
1013
+ ...(binding
1014
+ ? {
1015
+ channelBinding: {
1016
+ sourceChannel: binding.sourceChannel,
1017
+ externalChatId: binding.externalChatId,
1018
+ externalUserId: binding.externalUserId,
1019
+ displayName: binding.displayName,
1020
+ username: binding.username,
1021
+ },
1022
+ }
1023
+ : {}),
1024
+ ...(originChannel
1025
+ ? { conversationOriginChannel: originChannel }
1026
+ : {}),
1027
+ ...(assistantAttention ? { assistantAttention } : {}),
1028
+ },
1029
+ });
1030
+ },
1031
+ },
1032
+
936
1033
  ...btwRouteDefinitions({
937
1034
  sendMessageDeps: this.sendMessageDeps,
938
1035
  }),
@@ -23,49 +23,6 @@ const GENERATION_TIMEOUT_MS = 5_000;
23
23
  /** Maximum allowed length for a generated instruction. */
24
24
  const MAX_INSTRUCTION_LENGTH = 500;
25
25
 
26
- // ---------------------------------------------------------------------------
27
- // Channel display label
28
- // ---------------------------------------------------------------------------
29
-
30
- /** Human-readable label for a channel type. */
31
- export function channelDisplayLabel(type: string): string {
32
- switch (type) {
33
- case "telegram":
34
- return "Telegram";
35
- case "email":
36
- return "Email";
37
- case "slack":
38
- return "Slack";
39
- case "phone":
40
- return "Voice";
41
- default:
42
- return type.charAt(0).toUpperCase() + type.slice(1);
43
- }
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Deterministic fallback
48
- // ---------------------------------------------------------------------------
49
-
50
- /**
51
- * Build a deterministic fallback instruction when the LLM is unavailable.
52
- */
53
- export function buildFallbackInstruction(params: {
54
- contactName?: string;
55
- channelLabel: string;
56
- channelHandle?: string;
57
- shareUrl?: string;
58
- }): string {
59
- const contact = params.contactName || "the contact";
60
- const handle = params.channelHandle
61
- ? ` at ${params.channelHandle}`
62
- : ` on ${params.channelLabel}`;
63
- if (params.shareUrl) {
64
- return `Send ${contact} this link: ${params.shareUrl} — or tell them to message me${handle} with the code below.`;
65
- }
66
- return `Tell ${contact} to message me${handle} with the code below.`;
67
- }
68
-
69
26
  // ---------------------------------------------------------------------------
70
27
  // LLM-powered generation
71
28
  // ---------------------------------------------------------------------------
@@ -88,15 +45,32 @@ export async function generateInviteInstruction(params: {
88
45
  */
89
46
  shareUrl?: string;
90
47
  }): Promise<string> {
91
- const channelLabel = channelDisplayLabel(params.channelType);
92
- const fallback = buildFallbackInstruction({
93
- contactName: params.contactName,
94
- channelLabel,
95
- channelHandle: params.channelHandle,
96
- shareUrl: params.shareUrl,
97
- });
48
+ const channelLabel = (() => {
49
+ switch (params.channelType) {
50
+ case "telegram":
51
+ return "Telegram";
52
+ case "email":
53
+ return "Email";
54
+ case "slack":
55
+ return "Slack";
56
+ case "phone":
57
+ return "Voice";
58
+ default:
59
+ return (
60
+ params.channelType.charAt(0).toUpperCase() +
61
+ params.channelType.slice(1)
62
+ );
63
+ }
64
+ })();
65
+ const contact = params.contactName || "the contact";
66
+ const handle = params.channelHandle
67
+ ? ` at ${params.channelHandle}`
68
+ : ` on ${channelLabel}`;
69
+ const fallback = params.shareUrl
70
+ ? `Send ${contact} this link: ${params.shareUrl} — or tell them to message me${handle} with the code below.`
71
+ : `Tell ${contact} to message me${handle} with the code below.`;
98
72
 
99
- const resolved = resolveConfiguredProvider();
73
+ const resolved = await resolveConfiguredProvider();
100
74
  if (!resolved) {
101
75
  log.debug(
102
76
  "No provider available for invite instruction generation, using fallback",
@@ -34,7 +34,6 @@ import {
34
34
  } from "./channel-invite-transport.js";
35
35
  import { generateInviteInstruction } from "./invite-instruction-generator.js";
36
36
  import {
37
- type InviteRedemptionOutcome,
38
37
  redeemInvite as redeemInviteTyped,
39
38
  redeemVoiceInviteCode as redeemVoiceInviteCodeTyped,
40
39
  type VoiceRedemptionOutcome,
@@ -372,25 +371,6 @@ export function redeemIngressInvite(params: {
372
371
  return { ok: true, data: inviteToResponse(inv) };
373
372
  }
374
373
 
375
- // ---------------------------------------------------------------------------
376
- // Typed invite redemption — preferred entry point for new callers
377
- // ---------------------------------------------------------------------------
378
-
379
- export { type InviteRedemptionOutcome } from "./invite-redemption-service.js";
380
- export { type VoiceRedemptionOutcome } from "./invite-redemption-service.js";
381
-
382
- export function redeemIngressInviteTyped(params: {
383
- rawToken: string;
384
- sourceChannel: string;
385
- externalUserId?: string;
386
- externalChatId?: string;
387
- displayName?: string;
388
- username?: string;
389
- assistantId?: string;
390
- }): InviteRedemptionOutcome {
391
- return redeemInviteTyped(params);
392
- }
393
-
394
374
  export function redeemVoiceInviteCode(params: {
395
375
  assistantId?: string;
396
376
  callerExternalUserId: string;
@@ -101,7 +101,7 @@ export async function handleDeleteAttachment(req: Request): Promise<Response> {
101
101
  return new Response(null, { status: 204 });
102
102
  }
103
103
 
104
- export function handleGetAttachment(attachmentId: string): Response {
104
+ function handleGetAttachment(attachmentId: string): Response {
105
105
  const attachment = attachmentsStore.getAttachmentById(attachmentId);
106
106
  if (!attachment) {
107
107
  return httpError("NOT_FOUND", "Attachment not found", 404);
@@ -34,7 +34,7 @@ function getMemoryKindColor(kind: string): string {
34
34
  }
35
35
  }
36
36
 
37
- export function handleGetBrainGraph(): Response {
37
+ function handleGetBrainGraph(): Response {
38
38
  try {
39
39
  const db = getDb();
40
40
 
@@ -148,7 +148,7 @@ export async function handleStartCall(
148
148
  /**
149
149
  * GET /v1/calls/:callSessionId
150
150
  */
151
- export function handleGetCallStatus(callSessionId: string): Response {
151
+ function handleGetCallStatus(callSessionId: string): Response {
152
152
  const result = getCallStatus(callSessionId);
153
153
 
154
154
  if (!result.ok) {