@vellumai/assistant 0.9.0 → 0.9.1-staging.1

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 (222) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +4 -4
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
  12. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  13. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  14. package/openapi.yaml +135 -59
  15. package/package.json +2 -1
  16. package/scripts/sync-llm-catalog.ts +6 -15
  17. package/scripts/sync-web-search-catalog.ts +3 -11
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
  19. package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
  20. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  21. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  22. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  23. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  24. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  25. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  26. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  27. package/src/__tests__/btw-routes.test.ts +6 -8
  28. package/src/__tests__/checker.test.ts +0 -3
  29. package/src/__tests__/config-loader-backfill.test.ts +103 -6
  30. package/src/__tests__/config-watcher.test.ts +0 -18
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  32. package/src/__tests__/credential-broker.test.ts +449 -1
  33. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  34. package/src/__tests__/credential-prompt-route.test.ts +3 -4
  35. package/src/__tests__/credential-routes.test.ts +360 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -13
  37. package/src/__tests__/dynamic-page-surface.test.ts +101 -1
  38. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  39. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  40. package/src/__tests__/identity-routes.test.ts +0 -189
  41. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  42. package/src/__tests__/invite-redemption-service.test.ts +4 -4
  43. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  44. package/src/__tests__/llm-catalog-parity.test.ts +0 -22
  45. package/src/__tests__/llm-resolver.test.ts +49 -24
  46. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  47. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  48. package/src/__tests__/persona-resolver.test.ts +11 -14
  49. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  50. package/src/__tests__/registry.test.ts +2 -7
  51. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  52. package/src/__tests__/schedule-routes.test.ts +0 -30
  53. package/src/__tests__/schedule-tools.test.ts +2 -18
  54. package/src/__tests__/skill-execute-input.test.ts +46 -1
  55. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  56. package/src/__tests__/subagent-tools.test.ts +116 -0
  57. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  58. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  59. package/src/__tests__/token-manager.test.ts +519 -0
  60. package/src/__tests__/tool-executor.test.ts +0 -79
  61. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  62. package/src/__tests__/trusted-contact-verification.test.ts +6 -6
  63. package/src/__tests__/voice-invite-redemption.test.ts +2 -2
  64. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  65. package/src/__tests__/workspace-greetings.test.ts +152 -0
  66. package/src/agent/loop.ts +25 -5
  67. package/src/api/README.md +6 -6
  68. package/src/api/responses/conversation-message.ts +2 -4
  69. package/src/api/responses/home.ts +0 -4
  70. package/src/approvals/guardian-request-resolvers.ts +2 -2
  71. package/src/calls/relay-access-wait.ts +1 -1
  72. package/src/calls/voice-session-bridge.ts +2 -2
  73. package/src/cli/commands/plugins.ts +143 -2
  74. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  75. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
  76. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
  77. package/src/cli/lib/diff-plugin.ts +346 -0
  78. package/src/cli/lib/install-from-github.ts +105 -17
  79. package/src/cli/lib/merge-plugin-tree.ts +228 -0
  80. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  81. package/src/cli/lib/upgrade-plugin.ts +270 -10
  82. package/src/cli/program.ts +0 -2
  83. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  84. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  85. package/src/config/bundled-skills/workflows/SKILL.md +0 -1
  86. package/src/config/bundled-tool-registry.ts +2 -7
  87. package/src/config/call-site-defaults.ts +12 -2
  88. package/src/config/feature-flag-registry.json +1 -17
  89. package/src/config/inference-profile-validation.ts +26 -0
  90. package/src/config/loader.ts +4 -0
  91. package/src/config/profile-order.ts +28 -0
  92. package/src/config/schemas/elevenlabs.ts +0 -1
  93. package/src/config/schemas/platform.ts +0 -8
  94. package/src/config/seed-inference-profiles.ts +25 -20
  95. package/src/contacts/contact-store.ts +87 -96
  96. package/src/contacts/contacts-write.ts +5 -21
  97. package/src/context/compactor.ts +2 -2
  98. package/src/credential-execution/process-manager.ts +55 -14
  99. package/src/credential-execution/prompted-credential.ts +2 -3
  100. package/src/daemon/config-watcher.ts +0 -4
  101. package/src/daemon/conversation-agent-loop.ts +15 -4
  102. package/src/daemon/conversation-slash.ts +2 -23
  103. package/src/daemon/conversation-tool-setup.ts +11 -3
  104. package/src/daemon/conversation.ts +2 -0
  105. package/src/daemon/handlers/config-channels.ts +20 -16
  106. package/src/daemon/handlers/config-slack-channel.ts +2 -3
  107. package/src/daemon/lifecycle.ts +0 -7
  108. package/src/daemon/message-types/conversations.ts +3 -3
  109. package/src/daemon/message-types/sync.ts +0 -1
  110. package/src/daemon/orphan-reaper.test.ts +0 -19
  111. package/src/daemon/orphan-reaper.ts +2 -24
  112. package/src/daemon/server.ts +0 -10
  113. package/src/home/relationship-state.ts +2 -4
  114. package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
  115. package/src/memory/bookmark-crud.ts +1 -2
  116. package/src/memory/db-init.ts +7 -17
  117. package/src/memory/embedding-backend.ts +23 -0
  118. package/src/memory/embedding-billing-breaker.ts +96 -0
  119. package/src/memory/jobs-store.ts +25 -13
  120. package/src/memory/jobs-worker.ts +52 -0
  121. package/src/memory/memory-retrospective-constants.ts +4 -4
  122. package/src/memory/memory-retrospective-job.ts +19 -227
  123. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
  124. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
  125. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  126. package/src/memory/migrations/index.ts +1 -0
  127. package/src/memory/migrations/run-migrations.ts +41 -0
  128. package/src/memory/migrations/validate-migration-state.ts +1 -1
  129. package/src/memory/schema/contacts.ts +0 -4
  130. package/src/messaging/providers/slack/adapter.ts +1 -1
  131. package/src/notifications/adapters/shared.ts +29 -0
  132. package/src/notifications/adapters/slack.ts +5 -32
  133. package/src/notifications/adapters/telegram.ts +2 -20
  134. package/src/notifications/broadcaster.ts +10 -1
  135. package/src/notifications/home-feed-side-effect.ts +4 -3
  136. package/src/notifications/notification-utils.ts +17 -19
  137. package/src/notifications/types.ts +7 -0
  138. package/src/oauth/AGENTS.md +5 -24
  139. package/src/plugin-api/constants.ts +1 -1
  140. package/src/plugin-api/index.ts +6 -1
  141. package/src/plugin-api/model-profiles.ts +33 -0
  142. package/src/plugin-api/types.ts +50 -2
  143. package/src/plugins/defaults/index.ts +25 -0
  144. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
  145. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
  146. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  147. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  148. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  149. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  150. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
  151. package/src/prompts/persona-resolver.ts +2 -2
  152. package/src/runtime/AGENTS.md +0 -1
  153. package/src/runtime/actor-trust-resolver.ts +12 -44
  154. package/src/runtime/btw-sidechain.ts +3 -6
  155. package/src/runtime/channel-approval-types.ts +18 -45
  156. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  157. package/src/runtime/channel-verification-service.ts +4 -3
  158. package/src/runtime/invite-redemption-service.ts +3 -3
  159. package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
  160. package/src/runtime/routes/app-routes.ts +1 -1
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  162. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  163. package/src/runtime/routes/btw-routes.ts +1 -27
  164. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  165. package/src/runtime/routes/conversation-routes.ts +2 -2
  166. package/src/runtime/routes/credential-routes.ts +40 -16
  167. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  168. package/src/runtime/routes/identity-routes.ts +1 -296
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
  170. package/src/runtime/routes/plugins-routes.ts +171 -5
  171. package/src/runtime/routes/schedule-routes.ts +0 -22
  172. package/src/runtime/routes/workflow-routes.test.ts +4 -43
  173. package/src/runtime/routes/workflow-routes.ts +0 -28
  174. package/src/runtime/routes/workspace-greetings.ts +55 -0
  175. package/src/runtime/sync/resource-sync-events.ts +1 -11
  176. package/src/schedule/inference-profile.ts +2 -14
  177. package/src/subagent/manager.ts +6 -0
  178. package/src/subagent/types.ts +6 -0
  179. package/src/tools/AGENTS.md +3 -3
  180. package/src/tools/browser/browser-execution.ts +1 -1
  181. package/src/tools/network/web-search-error.ts +1 -1
  182. package/src/tools/permission-checker.ts +1 -1
  183. package/src/tools/schedule/create.ts +3 -9
  184. package/src/tools/schedule/update.ts +2 -10
  185. package/src/tools/side-effects.ts +2 -17
  186. package/src/tools/skills/execute.ts +34 -0
  187. package/src/tools/subagent/spawn.ts +34 -9
  188. package/src/tools/tool-approval-handler.ts +1 -3
  189. package/src/tools/tool-manifest.ts +0 -2
  190. package/src/tools/ui-surface/definitions.ts +39 -1
  191. package/src/tools/workflows/run-workflow.test.ts +8 -18
  192. package/src/util/platform.ts +2 -2
  193. package/src/workflows/capabilities.ts +2 -3
  194. package/src/workflows/run-manager.test.ts +0 -25
  195. package/src/workflows/run-manager.ts +2 -24
  196. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  197. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  198. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  199. package/src/__tests__/credential-vault.test.ts +0 -1706
  200. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  201. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  202. package/src/cli/commands/__tests__/task.test.ts +0 -914
  203. package/src/cli/commands/task.ts +0 -771
  204. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  205. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  206. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  207. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  208. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  209. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  210. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  211. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  212. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  213. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  214. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  215. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  216. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  217. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  218. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  219. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  220. package/src/memory/preloaded-apps.ts +0 -116
  221. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  222. package/src/tools/credentials/vault.ts +0 -712
@@ -27,18 +27,11 @@ import {
27
27
  setCachedEmptyStateGreeting,
28
28
  } from "./empty-state-greeting-cache.js";
29
29
  import { BadRequestError, ServiceUnavailableError } from "./errors.js";
30
- import {
31
- getCachedIntro,
32
- readWorkspaceGreetings,
33
- setCachedIntro,
34
- } from "./identity-intro-cache.js";
35
30
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
31
+ import { readWorkspaceGreetings } from "./workspace-greetings.js";
36
32
 
37
33
  const log = getLogger("btw-routes");
38
34
 
39
- /** Conversation key used by the client for identity intro generation. */
40
- const IDENTITY_INTRO_KEY = "identity-intro";
41
-
42
35
  /** Conversation key used by the client for empty-state greeting generation. */
43
36
  const GREETING_KEY = "greeting";
44
37
 
@@ -72,15 +65,6 @@ async function handleBtw({
72
65
 
73
66
  const trimmedContent = content.trim();
74
67
 
75
- // ----- Cached identity intro fast-path -----
76
- if (conversationKey === IDENTITY_INTRO_KEY) {
77
- const fastText = getCachedIntro()?.greetings[0];
78
- if (fastText) {
79
- log.debug("Returning identity intro fast-path");
80
- return streamText(fastText);
81
- }
82
- }
83
-
84
68
  // ----- Empty-state greeting fast-path -----
85
69
  // User-authored `## Greetings` win; otherwise replay a cached greeting when
86
70
  // one is fresh within the configurable TTL (`ui.emptyStateGreetingCacheTtlMs`,
@@ -126,7 +110,6 @@ async function handleBtw({
126
110
  start(controller) {
127
111
  (async () => {
128
112
  try {
129
- const isIntroRequest = conversationKey === IDENTITY_INTRO_KEY;
130
113
  const isGreeting = conversationKey === GREETING_KEY;
131
114
  const result = await runBtwSidechain({
132
115
  content: effectiveContent,
@@ -153,15 +136,6 @@ async function handleBtw({
153
136
  );
154
137
  }
155
138
 
156
- if (isIntroRequest && result.text) {
157
- try {
158
- setCachedIntro([result.text]);
159
- log.debug("Cached identity intro text");
160
- } catch {
161
- // Non-fatal — next request will regenerate.
162
- }
163
- }
164
-
165
139
  if (isGreeting && result.text) {
166
140
  // setCachedEmptyStateGreeting is a no-op when the TTL is 0.
167
141
  setCachedEmptyStateGreeting(result.text);
@@ -72,7 +72,7 @@ const log = getLogger("conversation-compaction-routes");
72
72
  * `responseBody` (below) is the source-of-truth for the generated
73
73
  * OpenAPI client type the frontend imports
74
74
  * (`ConversationsByIdCompactionGetResponse` in
75
- * `apps/web/src/generated/daemon/types.gen`); regenerate the client with
75
+ * `clients/web/src/generated/daemon/types.gen`); regenerate the client with
76
76
  * `bun run openapi-ts` after changing the schema.
77
77
  *
78
78
  * `null` means the value isn't known for the underlying row — either the
@@ -944,8 +944,8 @@ export function handleListMessages({
944
944
  ...(mergedMessageIds.length > 0 ? { mergedMessageIds } : {}),
945
945
  ...(m.clientMessageId ? { clientMessageId: m.clientMessageId } : {}),
946
946
  role: m.role,
947
- // Flat plain-text body the legacy Swift client reads directly; see the
948
- // `content` field on ConversationMessageSchema for why this must stay.
947
+ // Flat plain-text body; see the `content` field on
948
+ // ConversationMessageSchema for why this must stay.
949
949
  content: text,
950
950
  timestamp: new Date(displayTimestamp).toISOString(),
951
951
  attachments: msgAttachments,
@@ -141,7 +141,9 @@ interface CredentialLookup {
141
141
  * Resolve a credential lookup from service+field or UUID.
142
142
  * Throws BadRequestError when neither is provided or the UUID is not found.
143
143
  */
144
- function resolveCredentialLookup(body: Record<string, unknown>): CredentialLookup {
144
+ function resolveCredentialLookup(
145
+ body: Record<string, unknown>,
146
+ ): CredentialLookup {
145
147
  const { service, field, id } = body as {
146
148
  service?: string;
147
149
  field?: string;
@@ -243,8 +245,9 @@ async function handleCredentialsInspect({ body }: RouteHandlerArgs) {
243
245
  }
244
246
 
245
247
  const lookup = resolveCredentialLookup(body);
246
- const { value: secret, unreachable } =
247
- await getSecureKeyResultAsync(lookup.storageKey);
248
+ const { value: secret, unreachable } = await getSecureKeyResultAsync(
249
+ lookup.storageKey,
250
+ );
248
251
 
249
252
  if (!lookup.metadata && (secret == null || secret.length === 0)) {
250
253
  if (unreachable) {
@@ -290,8 +293,9 @@ async function handleCredentialsReveal({ body }: RouteHandlerArgs) {
290
293
  }
291
294
 
292
295
  const lookup = resolveCredentialLookup(body);
293
- const { value: secret, unreachable } =
294
- await getSecureKeyResultAsync(lookup.storageKey);
296
+ const { value: secret, unreachable } = await getSecureKeyResultAsync(
297
+ lookup.storageKey,
298
+ );
295
299
 
296
300
  if (secret == null || secret.length === 0) {
297
301
  if (unreachable) {
@@ -372,11 +376,19 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
372
376
 
373
377
  assertMetadataWritable();
374
378
 
379
+ // The Slack user_token only grants read access to channels the bot isn't a
380
+ // member of; Socket Mode itself runs on the bot + app tokens. Deleting just
381
+ // the user_token must leave the oauth_connection intact — disconnecting the
382
+ // provider would flap the integration's connected state until the next sync.
383
+ // Every other step (secret + metadata removal, not-found handling) is the
384
+ // same as a normal credential delete.
385
+ const preserveOAuthConnection =
386
+ service === "slack_channel" && field === "user_token";
387
+
375
388
  const key = credentialKey(service, field);
376
389
  const existing = await getSecureKeyAsync(key);
377
- const deleteResult = existing != null
378
- ? await deleteSecureKeyAsync(key)
379
- : "not-found";
390
+ const deleteResult =
391
+ existing != null ? await deleteSecureKeyAsync(key) : "not-found";
380
392
 
381
393
  if (deleteResult === "error") {
382
394
  throw new InternalError(
@@ -388,10 +400,12 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
388
400
 
389
401
  // Clean up OAuth connection (best-effort).
390
402
  let oauthResult: "disconnected" | "not-found" | "error" = "not-found";
391
- try {
392
- oauthResult = await disconnectOAuthProvider(service);
393
- } catch {
394
- // Best-effort — OAuth tables may not exist yet
403
+ if (!preserveOAuthConnection) {
404
+ try {
405
+ oauthResult = await disconnectOAuthProvider(service);
406
+ } catch {
407
+ // Best-effort — OAuth tables may not exist yet
408
+ }
395
409
  }
396
410
 
397
411
  if (oauthResult === "error") {
@@ -437,8 +451,12 @@ export const ROUTES: RouteDefinition[] = [
437
451
  search: z.string().optional().describe("Filter by substring match"),
438
452
  }),
439
453
  responseBody: z.object({
440
- credentials: z.array(z.unknown()).describe("Local credentials with metadata"),
441
- managedCredentials: z.array(z.unknown()).describe("Platform-managed credentials"),
454
+ credentials: z
455
+ .array(z.unknown())
456
+ .describe("Local credentials with metadata"),
457
+ managedCredentials: z
458
+ .array(z.unknown())
459
+ .describe("Platform-managed credentials"),
442
460
  }),
443
461
  handler: handleCredentialsList,
444
462
  },
@@ -507,8 +525,14 @@ export const ROUTES: RouteDefinition[] = [
507
525
  field: z.string().describe("Field name (e.g. client_secret)"),
508
526
  value: z.string().describe("Secret value to store"),
509
527
  label: z.string().optional().describe("Human-friendly label"),
510
- description: z.string().optional().describe("What this credential is used for"),
511
- allowedTools: z.array(z.string()).optional().describe("Tool names that may use this credential"),
528
+ description: z
529
+ .string()
530
+ .optional()
531
+ .describe("What this credential is used for"),
532
+ allowedTools: z
533
+ .array(z.string())
534
+ .optional()
535
+ .describe("Tool names that may use this credential"),
512
536
  }),
513
537
  responseBody: z.object({
514
538
  credentialId: z.string(),
@@ -8,8 +8,7 @@
8
8
  * the greeting regenerates on every request. This is the knob a workspace
9
9
  * sets to always receive a fresh greeting.
10
10
  *
11
- * Storage uses the existing `memory_checkpoints` table (simple key-value
12
- * store), mirroring {@link ./identity-intro-cache.ts}.
11
+ * Storage uses the existing `memory_checkpoints` table.
13
12
  */
14
13
 
15
14
  import { getConfig } from "../../config/loader.js";
@@ -8,13 +8,9 @@ import { availableParallelism, cpus, totalmem } from "node:os";
8
8
  import { z } from "zod";
9
9
 
10
10
  import { getCpuLimit, getIsPlatform } from "../../config/env-registry.js";
11
- import { resolveCallSiteConfig } from "../../config/llm-resolver.js";
12
- import { getConfig } from "../../config/loader.js";
13
11
  import { parseIdentityFields } from "../../daemon/handlers/identity.js";
14
12
  import { getProfilerRuntimeStatus } from "../../daemon/profiler-run-store.js";
15
13
  import { getMaxMigrationVersion } from "../../memory/migrations/registry.js";
16
- import { buildSystemPrompt } from "../../prompts/system-prompt.js";
17
- import { getConfiguredProvider } from "../../providers/provider-send-message.js";
18
14
  import { getCesClient } from "../../security/secure-keys.js";
19
15
  import {
20
16
  getDiskUsageInfo,
@@ -27,15 +23,8 @@ import { resolveHatchedAtReadOnly } from "../../workspace/hatched-date.js";
27
23
  import { WORKSPACE_MIGRATIONS } from "../../workspace/migrations/registry.js";
28
24
  import { getLastWorkspaceMigrationId } from "../../workspace/migrations/runner.js";
29
25
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
30
- import { runBtwSidechain } from "../btw-sidechain.js";
31
26
  import { NotFoundError } from "./errors.js";
32
- import {
33
- getCachedIntro,
34
- readWorkspaceGreetings,
35
- readWorkspaceIdentityIntro,
36
- setCachedIntro,
37
- } from "./identity-intro-cache.js";
38
- import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
27
+ import type { RouteDefinition } from "./types.js";
39
28
 
40
29
  interface MemoryInfo {
41
30
  currentMb: number;
@@ -405,256 +394,6 @@ function resolveIdentityCreatedAt(identityPath: string): string | undefined {
405
394
  return resolveHatchedAtReadOnly(identityPath);
406
395
  }
407
396
 
408
- const FALLBACK_GREETINGS = [
409
- "What are we working on?",
410
- "I'm here whenever you need me.",
411
- "What's on your mind?",
412
- "Ready when you are.",
413
- ];
414
-
415
- const GENERATED_GREETING_LIMIT = 5;
416
- const GREETING_GENERATION_TIMEOUT_MS = 10_000;
417
- const GREETING_GENERATION_FAILURE_COOLDOWN_MS = 60_000;
418
- const EMPTY_STATE_GREETING_CALLSITE = "emptyStateGreeting" as const;
419
- const EXPLICIT_TIME_OF_DAY_PATTERN =
420
- /\b(?:morning|afternoon|evening|tonight|midnight|noon|sunrise|sunset)\b/i;
421
-
422
- type IdentityIntroSource = "workspace" | "cache" | "fallback";
423
-
424
- interface IdentityIntroResponse {
425
- greetings: string[];
426
- text: string;
427
- source: IdentityIntroSource;
428
- refreshing: boolean;
429
- }
430
-
431
- let greetingGenerationInFlight: Promise<void> | null = null;
432
- let lastGreetingGenerationFailureAt = 0;
433
-
434
- function identityIntroResponse(
435
- greetings: string[],
436
- source: IdentityIntroSource,
437
- refreshing = false,
438
- ): IdentityIntroResponse {
439
- return {
440
- greetings,
441
- text: greetings[0] ?? "",
442
- source,
443
- refreshing,
444
- };
445
- }
446
-
447
- function getIdentityIntro({
448
- queryParams = {},
449
- }: RouteHandlerArgs = {}): IdentityIntroResponse {
450
- const localTimeContext = buildLocalTimeContext(queryParams);
451
-
452
- // 1. User-defined greetings from SOUL.md `## Greetings`
453
- const workspaceGreetings = readWorkspaceGreetings();
454
- if (workspaceGreetings) {
455
- return identityIntroResponse(workspaceGreetings, "workspace");
456
- }
457
-
458
- // 2. Cached LLM-generated greetings
459
- const cached = getCachedIntro();
460
- if (cached) {
461
- return identityIntroResponse(cached.greetings, "cache");
462
- }
463
-
464
- // 3. Identity intro tagline from `## Identity Intro` in IDENTITY.md
465
- // (written during onboarding by BOOTSTRAP.md instructions)
466
- const identityIntro = readWorkspaceIdentityIntro();
467
- if (identityIntro) {
468
- // Still trigger background generation so the next request gets
469
- // LLM-generated greetings instead of the static tagline.
470
- const refreshing = triggerEmptyStateGreetingGeneration(localTimeContext);
471
- return identityIntroResponse(
472
- [identityIntro, ...FALLBACK_GREETINGS],
473
- "workspace",
474
- refreshing,
475
- );
476
- }
477
-
478
- // 4. Trigger fresh generation without blocking the empty-state UI.
479
- const refreshing = triggerEmptyStateGreetingGeneration(localTimeContext);
480
-
481
- // 5. Generic fallback only when generation is unavailable.
482
- return identityIntroResponse(FALLBACK_GREETINGS, "fallback", refreshing);
483
- }
484
-
485
- function buildLocalTimeContext(
486
- queryParams: Record<string, string>,
487
- ): string | null {
488
- const rawHour = queryParams.localHour;
489
- const rawMinute = queryParams.localMinute;
490
- if (rawHour === undefined || rawMinute === undefined) {
491
- return null;
492
- }
493
-
494
- const hour = Number(rawHour);
495
- const minute = Number(rawMinute);
496
- if (
497
- !Number.isInteger(hour) ||
498
- !Number.isInteger(minute) ||
499
- hour < 0 ||
500
- hour > 23 ||
501
- minute < 0 ||
502
- minute > 59
503
- ) {
504
- return null;
505
- }
506
-
507
- const period =
508
- hour >= 5 && hour < 12
509
- ? "morning"
510
- : hour >= 12 && hour < 17
511
- ? "afternoon"
512
- : hour >= 17 && hour < 21
513
- ? "evening"
514
- : "late night";
515
- const paddedHour = String(hour).padStart(2, "0");
516
- const paddedMinute = String(minute).padStart(2, "0");
517
- return `${period} (${paddedHour}:${paddedMinute})`;
518
- }
519
-
520
- function triggerEmptyStateGreetingGeneration(
521
- localTimeContext: string | null,
522
- ): boolean {
523
- if (greetingGenerationInFlight) {
524
- return true;
525
- }
526
-
527
- if (
528
- lastGreetingGenerationFailureAt > 0 &&
529
- Date.now() - lastGreetingGenerationFailureAt <
530
- GREETING_GENERATION_FAILURE_COOLDOWN_MS
531
- ) {
532
- return false;
533
- }
534
-
535
- greetingGenerationInFlight = new Promise<void>((resolve) => {
536
- queueMicrotask(() => {
537
- void generateEmptyStateGreetings(localTimeContext)
538
- .then((greetings) => {
539
- lastGreetingGenerationFailureAt = greetings === null ? Date.now() : 0;
540
- })
541
- .finally(() => {
542
- greetingGenerationInFlight = null;
543
- resolve();
544
- });
545
- });
546
- });
547
-
548
- return true;
549
- }
550
-
551
- async function generateEmptyStateGreetings(
552
- localTimeContext: string | null,
553
- ): Promise<string[] | null> {
554
- try {
555
- const provider = await getConfiguredProvider(EMPTY_STATE_GREETING_CALLSITE);
556
- if (!provider) {
557
- return null;
558
- }
559
-
560
- const resolved = resolveCallSiteConfig(
561
- EMPTY_STATE_GREETING_CALLSITE,
562
- getConfig().llm,
563
- );
564
- const systemPrompt = buildSystemPrompt({
565
- excludeBootstrap: true,
566
- excludeCustomPrefix: true,
567
- });
568
- const localTimeInstruction = localTimeContext
569
- ? ` Current user-local time for subtle tone only: ${localTimeContext}.`
570
- : "";
571
- const result = await runBtwSidechain({
572
- content:
573
- `Generate ${GENERATED_GREETING_LIMIT} short first-person greeting options for the empty new-chat screen. ` +
574
- "Use the assistant identity, voice, and relationship guidance from IDENTITY.md and SOUL.md. " +
575
- "Each greeting should feel personal and inviting, not like a generic assistant introduction. " +
576
- "Return only a JSON array of strings. No markdown, keys, or explanation. " +
577
- "Generated greetings are cached for 4 hours, so do not mention the current time " +
578
- "or use explicit time-of-day words like morning, afternoon, evening, or tonight." +
579
- localTimeInstruction,
580
- provider,
581
- systemPrompt,
582
- messages: [],
583
- tools: [],
584
- callSite: EMPTY_STATE_GREETING_CALLSITE,
585
- maxTokens: resolved.maxTokens,
586
- timeoutMs: GREETING_GENERATION_TIMEOUT_MS,
587
- });
588
-
589
- const greetings = parseGeneratedGreetings(result.text);
590
- if (greetings.length === 0) {
591
- return null;
592
- }
593
-
594
- setCachedIntro(greetings);
595
- return greetings;
596
- } catch (err) {
597
- getLogger("identity").warn(
598
- { err },
599
- "Failed to generate empty-state greetings",
600
- );
601
- return null;
602
- }
603
- }
604
-
605
- function parseGeneratedGreetings(text: string): string[] {
606
- const cleaned = text
607
- .trim()
608
- .replace(/^```(?:json)?\s*/i, "")
609
- .replace(/\s*```$/i, "")
610
- .trim();
611
-
612
- try {
613
- const parsed = JSON.parse(cleaned) as unknown;
614
- if (Array.isArray(parsed)) {
615
- return normalizeGeneratedGreetings(parsed);
616
- }
617
- if (
618
- parsed &&
619
- typeof parsed === "object" &&
620
- Array.isArray((parsed as { greetings?: unknown }).greetings)
621
- ) {
622
- return normalizeGeneratedGreetings(
623
- (parsed as { greetings: unknown[] }).greetings,
624
- );
625
- }
626
- } catch {
627
- // Fall through to line parsing for non-JSON model output.
628
- }
629
-
630
- return normalizeGeneratedGreetings(cleaned.split("\n"));
631
- }
632
-
633
- function normalizeGeneratedGreetings(values: unknown[]): string[] {
634
- const greetings: string[] = [];
635
- const seen = new Set<string>();
636
-
637
- for (const value of values) {
638
- if (typeof value !== "string") continue;
639
- const greeting = value
640
- .trim()
641
- .replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, "")
642
- .replace(/^["'`]+|["'`]+$/g, "")
643
- .trim();
644
- if (!greeting) continue;
645
- if (EXPLICIT_TIME_OF_DAY_PATTERN.test(greeting)) continue;
646
-
647
- const key = greeting.toLowerCase();
648
- if (seen.has(key)) continue;
649
- seen.add(key);
650
- greetings.push(greeting);
651
-
652
- if (greetings.length >= GENERATED_GREETING_LIMIT) break;
653
- }
654
-
655
- return greetings;
656
- }
657
-
658
397
  // ---------------------------------------------------------------------------
659
398
  // Zod schemas for profiler health metadata
660
399
  // ---------------------------------------------------------------------------
@@ -787,38 +526,4 @@ export const ROUTES: RouteDefinition[] = [
787
526
  createdAt: z.string().optional(),
788
527
  }),
789
528
  },
790
- {
791
- operationId: "identity_intro",
792
- endpoint: "identity/intro",
793
- method: "GET",
794
- policy: {
795
- requiredScopes: ["settings.read"],
796
- allowedPrincipalTypes: ACTOR_PRINCIPALS,
797
- },
798
- handler: getIdentityIntro,
799
- summary: "Get identity greetings",
800
- description:
801
- "Returns greetings sourced from SOUL.md, the generated cache, or generic fallbacks while background generation refreshes the cache.",
802
- tags: ["identity"],
803
- queryParams: [
804
- {
805
- name: "localHour",
806
- schema: { type: "integer", minimum: 0, maximum: 23 },
807
- description:
808
- "Optional client-local hour of day used only when refreshing generated greetings.",
809
- },
810
- {
811
- name: "localMinute",
812
- schema: { type: "integer", minimum: 0, maximum: 59 },
813
- description:
814
- "Optional client-local minute used only when refreshing generated greetings.",
815
- },
816
- ],
817
- responseBody: z.object({
818
- greetings: z.array(z.string()),
819
- text: z.string(),
820
- source: z.enum(["workspace", "cache", "fallback"]),
821
- refreshing: z.boolean(),
822
- }),
823
- },
824
529
  ];
@@ -166,7 +166,7 @@ export async function enforceIngressAcl(
166
166
  if (canonicalSenderId) {
167
167
  const contactResult = findContactChannel({
168
168
  channelType: sourceChannel,
169
- externalUserId: canonicalSenderId,
169
+ address: canonicalSenderId,
170
170
  externalChatId: conversationExternalId,
171
171
  });
172
172
  resolvedMember = contactResult