@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
@@ -1,85 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- rmSync,
6
- writeFileSync,
7
- } from "node:fs";
8
- import { join } from "node:path";
9
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
-
11
- import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
12
- import type { AssistantConfig } from "../../config/schema.js";
13
- import { getAppsDir } from "../app-store.js";
14
- import {
15
- ACTIVATION_FLOW_FLAG,
16
- PERSONAL_PAGE_APP_ID,
17
- PERSONAL_PAGE_ARM,
18
- seedPreloadedApps,
19
- } from "../preloaded-apps.js";
20
-
21
- // The flag resolver reads the override cache and registry; the config
22
- // argument is part of the signature but not consulted for resolution.
23
- const config = {} as AssistantConfig;
24
-
25
- function definitionPath(): string {
26
- return join(getAppsDir(), `${PERSONAL_PAGE_APP_ID}.json`);
27
- }
28
-
29
- function appDir(): string {
30
- return join(getAppsDir(), PERSONAL_PAGE_APP_ID);
31
- }
32
-
33
- beforeEach(() => {
34
- setOverridesForTesting({});
35
- });
36
-
37
- afterEach(() => {
38
- setOverridesForTesting({});
39
- rmSync(definitionPath(), { force: true });
40
- rmSync(appDir(), { recursive: true, force: true });
41
- });
42
-
43
- describe("seedPreloadedApps gating", () => {
44
- test("does not seed on the control arm", async () => {
45
- setOverridesForTesting({ [ACTIVATION_FLOW_FLAG]: "control" });
46
- await seedPreloadedApps(config);
47
- expect(existsSync(definitionPath())).toBe(false);
48
- expect(existsSync(appDir())).toBe(false);
49
- });
50
-
51
- test("does not seed on other treatment arms", async () => {
52
- setOverridesForTesting({ [ACTIVATION_FLOW_FLAG]: "variant-a" });
53
- await seedPreloadedApps(config);
54
- expect(existsSync(definitionPath())).toBe(false);
55
- });
56
-
57
- test("does not seed when the flag resolves to the registry default", async () => {
58
- // No override → declared default "control".
59
- await seedPreloadedApps(config);
60
- expect(existsSync(definitionPath())).toBe(false);
61
- });
62
-
63
- test("leaves an already-seeded, compiled app untouched", async () => {
64
- setOverridesForTesting({ [ACTIVATION_FLOW_FLAG]: PERSONAL_PAGE_ARM });
65
-
66
- const sentinelDefinition = JSON.stringify({
67
- id: PERSONAL_PAGE_APP_ID,
68
- userEdited: true,
69
- });
70
- mkdirSync(join(appDir(), "dist"), { recursive: true });
71
- writeFileSync(definitionPath(), sentinelDefinition, "utf-8");
72
- writeFileSync(
73
- join(appDir(), "dist", "index.html"),
74
- "<!-- compiled -->",
75
- "utf-8",
76
- );
77
-
78
- await seedPreloadedApps(config);
79
-
80
- // Seeding must not re-copy the template or rewrite the definition once
81
- // the app exists — its content is considered user-owned.
82
- expect(readFileSync(definitionPath(), "utf-8")).toBe(sentinelDefinition);
83
- expect(existsSync(join(appDir(), "src"))).toBe(false);
84
- });
85
- });
@@ -1,116 +0,0 @@
1
- /**
2
- * Seeds preloaded apps into the workspace apps directory at daemon startup.
3
- *
4
- * A preloaded app ships as plain source files under
5
- * `src/config/preloaded-apps/<dirName>/` and is copied into
6
- * `data/apps/<dirName>/` with a fixed, well-known app id, then compiled.
7
- * The assistant later populates it by overwriting designated content files
8
- * (e.g. the personal page's `src/profile-data.ts`) and calling app_refresh —
9
- * it never authors the layout itself.
10
- *
11
- * Seeding is idempotent and deliberately conservative: once the definition
12
- * JSON exists the app is considered user-owned (its content may have been
13
- * populated) and the template is never re-copied. A missing `dist/` triggers
14
- * a recompile only.
15
- *
16
- * Seeding is gated on the activation-flow experiment: only assistants in the
17
- * personal-page treatment arm get the app. The gate is evaluated once per
18
- * daemon startup — an assistant moved into the arm later picks it up on its
19
- * next restart, and an already-seeded app is never removed when the
20
- * assistant leaves the arm (it is user-owned by then).
21
- */
22
-
23
- import { cpSync, existsSync, writeFileSync } from "node:fs";
24
- import { join } from "node:path";
25
-
26
- import { compileApp } from "../bundler/app-compiler.js";
27
- import {
28
- getAssistantFeatureFlagValue,
29
- initFeatureFlagOverrides,
30
- } from "../config/assistant-feature-flags.js";
31
- import type { AssistantConfig } from "../config/schema.js";
32
- import { resolveBundledDir } from "../util/bundled-asset.js";
33
- import { getLogger } from "../util/logger.js";
34
- import { getAppsDir } from "./app-store.js";
35
-
36
- const log = getLogger("preloaded-apps");
37
-
38
- /** Well-known id (and dirName) of the personal landing page app. */
39
- export const PERSONAL_PAGE_APP_ID = "personal-page";
40
-
41
- /** Multivariate activation-flow experiment flag (scope "both"). */
42
- export const ACTIVATION_FLOW_FLAG = "experiment-activation-flow-2026-06-03";
43
-
44
- /** Arm of the activation-flow experiment that gets the preseeded page. */
45
- export const PERSONAL_PAGE_ARM = "personal-page";
46
-
47
- export async function seedPreloadedApps(
48
- config: AssistantConfig,
49
- ): Promise<void> {
50
- // The lifecycle hook fires this early in startup, possibly before the
51
- // gateway override fetch has completed. Await it here (no-op when already
52
- // cached; bounded retries otherwise) so the arm read below sees the
53
- // platform-assigned value rather than the registry default.
54
- await initFeatureFlagOverrides();
55
-
56
- const arm = getAssistantFeatureFlagValue(ACTIVATION_FLOW_FLAG, config);
57
- if (arm !== PERSONAL_PAGE_ARM) return;
58
-
59
- await seedPersonalPageApp();
60
- }
61
-
62
- async function seedPersonalPageApp(): Promise<void> {
63
- const appsDir = getAppsDir();
64
- const definitionPath = join(appsDir, `${PERSONAL_PAGE_APP_ID}.json`);
65
- const appDir = join(appsDir, PERSONAL_PAGE_APP_ID);
66
- const alreadySeeded = existsSync(definitionPath);
67
-
68
- if (alreadySeeded && existsSync(join(appDir, "dist", "index.html"))) {
69
- return;
70
- }
71
-
72
- if (!alreadySeeded) {
73
- const templateDir = resolveBundledDir(
74
- import.meta.dirname ?? __dirname,
75
- "../config/preloaded-apps/personal-page",
76
- "preloaded-apps/personal-page",
77
- );
78
- if (!existsSync(join(templateDir, "src"))) {
79
- log.warn(
80
- { templateDir },
81
- "Personal page template not found — skipping preloaded app seeding",
82
- );
83
- return;
84
- }
85
-
86
- cpSync(join(templateDir, "src"), join(appDir, "src"), { recursive: true });
87
- // Multifile (formatVersion 2) apps keep an empty root index.html; the
88
- // real entrypoint is src/index.html compiled into dist/. Matches the
89
- // on-disk shape produced by createApp().
90
- writeFileSync(join(appDir, "index.html"), "", "utf-8");
91
-
92
- const now = Date.now();
93
- const definition = {
94
- id: PERSONAL_PAGE_APP_ID,
95
- name: "Your Page",
96
- description:
97
- "A personal landing page the assistant fills in with what it learns about you.",
98
- icon: "✨",
99
- schemaJson: "{}",
100
- createdAt: now,
101
- updatedAt: now,
102
- formatVersion: 2,
103
- dirName: PERSONAL_PAGE_APP_ID,
104
- };
105
- writeFileSync(definitionPath, JSON.stringify(definition, null, 2), "utf-8");
106
- log.info({ appDir }, "Seeded personal page app");
107
- }
108
-
109
- const result = await compileApp(appDir);
110
- if (!result.ok) {
111
- log.warn(
112
- { errors: result.errors },
113
- "Personal page app failed to compile after seeding",
114
- );
115
- }
116
- }
@@ -1,172 +0,0 @@
1
- /**
2
- * Caching layer for identity greetings.
3
- *
4
- * Greetings are sourced from (in priority order):
5
- * 1. A `## Greetings` section in SOUL.md (user-defined bullet list)
6
- * 2. A cached greetings array (populated by the empty-state greeting callsite)
7
- * 3. A generic fallback when generation is unavailable
8
- *
9
- * Cache invalidation is intentionally TTL-only. User-authored SOUL.md
10
- * greetings are read before cache, so manual overrides still take priority
11
- * without coupling cache validity to prompt-file edits.
12
- *
13
- * Storage uses the existing `memory_checkpoints` table (simple key-value store).
14
- */
15
-
16
- import { existsSync, readFileSync } from "node:fs";
17
-
18
- import {
19
- getMemoryCheckpoint,
20
- setMemoryCheckpoint,
21
- } from "../../memory/checkpoints.js";
22
- import { getWorkspacePromptPath } from "../../util/platform.js";
23
-
24
- // ---------------------------------------------------------------------------
25
- // Constants
26
- // ---------------------------------------------------------------------------
27
-
28
- const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
29
-
30
- const CHECKPOINT_KEY_GREETINGS = "identity:intro:greetings";
31
- const CHECKPOINT_KEY_TIMESTAMP = "identity:intro:cached_at";
32
-
33
- // ---------------------------------------------------------------------------
34
- // Helpers
35
- // ---------------------------------------------------------------------------
36
-
37
- /** Read a workspace prompt file, returning empty string if missing. */
38
- function readWorkspaceFile(name: string): string {
39
- try {
40
- const path = getWorkspacePromptPath(name);
41
- if (!existsSync(path)) return "";
42
- return readFileSync(path, "utf-8");
43
- } catch {
44
- return "";
45
- }
46
- }
47
-
48
- function parseIdentityIntroSection(content: string): string | null {
49
- let inSection = false;
50
-
51
- for (const line of content.split("\n")) {
52
- const trimmed = line.trim();
53
- if (/^#+\s/.test(trimmed)) {
54
- inSection = trimmed.toLowerCase().includes("identity intro");
55
- continue;
56
- }
57
- if (inSection && trimmed.length > 0) {
58
- return trimmed;
59
- }
60
- }
61
-
62
- return null;
63
- }
64
-
65
- /**
66
- * Read the explicit `## Identity Intro` section from workspace prompt files.
67
- *
68
- * BOOTSTRAP.md instructs the assistant to write this section in IDENTITY.md.
69
- * SOUL.md remains a fallback for older workspaces that stored the intro there.
70
- */
71
- export function readWorkspaceIdentityIntro(): string | null {
72
- return (
73
- parseIdentityIntroSection(readWorkspaceFile("IDENTITY.md")) ??
74
- parseIdentityIntroSection(readWorkspaceFile("SOUL.md"))
75
- );
76
- }
77
-
78
- /**
79
- * Parse the `## Greetings` section from SOUL.md. Returns bullet items as an
80
- * array of strings, or `null` if the section is missing or empty.
81
- */
82
- export function parseGreetingsSection(content: string): string[] | null {
83
- let inSection = false;
84
- let sectionLevel: number | null = null;
85
- const greetings: string[] = [];
86
-
87
- for (const line of content.split("\n")) {
88
- const trimmed = line.trim();
89
- const heading = /^(#{1,6})\s+(.+?)\s*#*$/.exec(trimmed);
90
- if (heading) {
91
- const level = heading[1]!.length;
92
- const title = heading[2]!.trim();
93
- if (inSection) {
94
- if (sectionLevel !== null && level <= sectionLevel) break;
95
- continue;
96
- }
97
- if (level === 2 && /^greetings$/i.test(title)) {
98
- inSection = true;
99
- sectionLevel = level;
100
- }
101
- continue;
102
- }
103
- if (!inSection) continue;
104
- const bullet = /^(?:[-*+]\s+|\d+[.)]\s+)(.+)$/.exec(trimmed);
105
- const greeting = bullet?.[1]?.trim();
106
- if (greeting) {
107
- greetings.push(greeting);
108
- }
109
- }
110
-
111
- return greetings.length > 0 ? greetings : null;
112
- }
113
-
114
- /**
115
- * Read user-defined greetings from the `## Greetings` section of SOUL.md.
116
- */
117
- export function readWorkspaceGreetings(): string[] | null {
118
- const soulContent = readWorkspaceFile("SOUL.md");
119
- if (!soulContent) return null;
120
- return parseGreetingsSection(soulContent);
121
- }
122
-
123
- // ---------------------------------------------------------------------------
124
- // Public API
125
- // ---------------------------------------------------------------------------
126
-
127
- export interface CachedIntro {
128
- greetings: string[];
129
- }
130
-
131
- /**
132
- * Retrieve the cached greetings array if it exists and is within the TTL
133
- * window.
134
- *
135
- * Returns `null` when the cache is missing or expired.
136
- */
137
- export function getCachedIntro(): CachedIntro | null {
138
- try {
139
- const raw = getMemoryCheckpoint(CHECKPOINT_KEY_GREETINGS);
140
- const timestampStr = getMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP);
141
-
142
- if (!raw || !timestampStr) return null;
143
-
144
- // TTL check
145
- const cachedAt = Number(timestampStr);
146
- if (isNaN(cachedAt) || Date.now() - cachedAt > CACHE_TTL_MS) return null;
147
-
148
- // Parse stored value — handles both JSON array and legacy single string
149
- let greetings: string[];
150
- try {
151
- const parsed = JSON.parse(raw);
152
- greetings = Array.isArray(parsed) ? parsed : [raw];
153
- } catch {
154
- greetings = [raw];
155
- }
156
-
157
- return { greetings };
158
- } catch {
159
- return null;
160
- }
161
- }
162
-
163
- /** Store a greetings array in the cache along with the current timestamp. */
164
- export function setCachedIntro(greetings: string[]): void {
165
- try {
166
- const now = String(Date.now());
167
- setMemoryCheckpoint(CHECKPOINT_KEY_GREETINGS, JSON.stringify(greetings));
168
- setMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP, now);
169
- } catch {
170
- // Cache write failure is non-fatal — next request will regenerate.
171
- }
172
- }