@vellumai/assistant 0.8.11 → 0.8.12-staging.2

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 (244) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +633 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -1,52 +1,64 @@
1
1
  /**
2
- * Background refresh timer for LLM-generated home page content.
2
+ * On-demand revalidation for LLM-generated home page content.
3
3
  *
4
- * Keeps the personalized greeting and assistant-generated suggestion
5
- * prompts warm in their caches so the GET handler never triggers LLM
6
- * calls or database writes (see `src/runtime/AGENTS.md` — GET handler
7
- * idempotency rule).
4
+ * The personalized greeting and assistant-generated suggestion prompts
5
+ * are served from caches (see `home-greeting-cache.ts` and
6
+ * `suggested-prompts.ts`). The GET handler stays read-only; when a
7
+ * client fetches the home feed it calls `revalidateHomeContentInBackground()`
8
+ * fire-and-forget, which regenerates whichever caches are stale and
9
+ * publishes a `home_feed_updated` event when fresh content lands so
10
+ * connected clients refetch and the personalized content swaps in.
8
11
  *
9
- * Call `startHomeContentRefresh()` once during daemon startup; the
10
- * timer handles periodic re-generation automatically.
12
+ * This keeps LLM cost proportional to actual Home usage: nothing
13
+ * generates at daemon startup or on a timer, and the per-cache TTLs
14
+ * (4 hours each) bound how often a regeneration can happen.
11
15
  */
12
16
 
17
+ import { buildAssistantEvent } from "../runtime/assistant-event.js";
18
+ import { assistantEventHub } from "../runtime/assistant-event-hub.js";
13
19
  import { getLogger } from "../util/logger.js";
14
20
  import { refreshPersonalizedGreeting } from "./home-greeting.js";
15
21
  import { refreshAssistantSuggestedPrompts } from "./suggested-prompts.js";
16
22
 
17
23
  const log = getLogger("home-content-refresh");
18
24
 
19
- const REFRESH_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
25
+ let inFlight: Promise<void> | null = null;
20
26
 
21
- let refreshTimer: ReturnType<typeof setInterval> | null = null;
22
-
23
- async function refreshAll(): Promise<void> {
24
- await Promise.all([
27
+ async function revalidateAll(): Promise<void> {
28
+ const [greetingRefreshed, promptsRefreshed] = await Promise.all([
25
29
  refreshPersonalizedGreeting(),
26
30
  refreshAssistantSuggestedPrompts(),
27
31
  ]);
28
- }
29
32
 
30
- /**
31
- * Start periodic background refresh of home page LLM content.
32
- * Runs an initial generation immediately (fire-and-forget) and
33
- * schedules re-generation every 30 minutes.
34
- */
35
- export function startHomeContentRefresh(): void {
36
- void refreshAll().catch((err) =>
37
- log.warn({ err }, "Initial home content refresh failed"),
38
- );
33
+ if (!greetingRefreshed && !promptsRefreshed) {
34
+ return;
35
+ }
39
36
 
40
- refreshTimer = setInterval(() => {
41
- void refreshAll().catch((err) =>
42
- log.warn({ err }, "Periodic home content refresh failed"),
43
- );
44
- }, REFRESH_INTERVAL_MS);
37
+ await assistantEventHub.publish(
38
+ buildAssistantEvent({
39
+ type: "home_feed_updated",
40
+ updatedAt: new Date().toISOString(),
41
+ newItemCount: 0,
42
+ }),
43
+ );
45
44
  }
46
45
 
47
- export function stopHomeContentRefresh(): void {
48
- if (refreshTimer) {
49
- clearInterval(refreshTimer);
50
- refreshTimer = null;
46
+ /**
47
+ * Regenerate stale home content in the background. Returns immediately;
48
+ * callers (the home feed GET handler) must not await generation. Both
49
+ * refreshers no-op when their cache is fresh, so calling this on every
50
+ * feed fetch is cheap — at most one generation per cache TTL window.
51
+ * Concurrent calls share a single in-flight revalidation.
52
+ */
53
+ export function revalidateHomeContentInBackground(): void {
54
+ if (inFlight) {
55
+ return;
51
56
  }
57
+ inFlight = revalidateAll()
58
+ .catch((err) => {
59
+ log.warn({ err }, "Home content revalidation failed");
60
+ })
61
+ .finally(() => {
62
+ inFlight = null;
63
+ });
52
64
  }
@@ -79,14 +79,21 @@ export function getCachedHomeGreeting(): string | null {
79
79
  }
80
80
  }
81
81
 
82
- export function setCachedHomeGreeting(text: string): void {
82
+ /**
83
+ * Persist a freshly generated greeting. Returns `true` when the cache
84
+ * write landed; `false` on failure so callers don't report fresh content
85
+ * that the next read cannot serve.
86
+ */
87
+ export function setCachedHomeGreeting(text: string): boolean {
83
88
  try {
84
89
  const hash = computeIdentityContentHash();
85
90
  const now = String(Date.now());
86
91
  setMemoryCheckpoint(CHECKPOINT_KEY_TEXT, text);
87
92
  setMemoryCheckpoint(CHECKPOINT_KEY_HASH, hash);
88
93
  setMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP, now);
94
+ return true;
89
95
  } catch {
90
96
  // Cache write failure is non-fatal — next request will regenerate.
97
+ return false;
91
98
  }
92
99
  }
@@ -6,9 +6,11 @@
6
6
  * caches the result for 4 hours (busted when identity files change).
7
7
  *
8
8
  * The GET handler reads only from cache (`getPersonalizedGreeting`).
9
- * Generation runs in the background via `refreshPersonalizedGreeting`,
10
- * called at daemon startup and periodically by the home-content
11
- * refresh timer.
9
+ * Generation runs on demand via `refreshPersonalizedGreeting`, invoked
10
+ * fire-and-forget by the home-content revalidation coordinator when a
11
+ * client fetches the home feed and the cache is stale (see
12
+ * `home-content-refresh.ts`). Nothing generates at daemon startup or on
13
+ * a timer — LLM cost is only incurred when a user actually views Home.
12
14
  */
13
15
 
14
16
  import { resolveCallSiteConfig } from "../config/llm-resolver.js";
@@ -37,13 +39,14 @@ export function getPersonalizedGreeting(): string | null {
37
39
 
38
40
  /**
39
41
  * Generate a personalized greeting via LLM and write it to cache.
40
- * No-ops when the cache is still fresh. Intended for background
41
- * invocation (daemon startup / periodic refresh), not the GET path.
42
+ * No-ops when the cache is still fresh. Intended for fire-and-forget
43
+ * background invocation, not the GET path. Returns `true` when a new
44
+ * greeting was generated and cached.
42
45
  */
43
- export async function refreshPersonalizedGreeting(): Promise<void> {
46
+ export async function refreshPersonalizedGreeting(): Promise<boolean> {
44
47
  const cached = getCachedHomeGreeting();
45
48
  if (cached) {
46
- return;
49
+ return false;
47
50
  }
48
51
 
49
52
  try {
@@ -52,7 +55,7 @@ export async function refreshPersonalizedGreeting(): Promise<void> {
52
55
 
53
56
  const provider = await getConfiguredProvider("homeGreeting");
54
57
  if (!provider) {
55
- return;
58
+ return false;
56
59
  }
57
60
 
58
61
  const systemPrompt = buildSystemPrompt({
@@ -77,9 +80,10 @@ export async function refreshPersonalizedGreeting(): Promise<void> {
77
80
 
78
81
  const text = result.text.trim();
79
82
  if (text) {
80
- setCachedHomeGreeting(text);
83
+ return setCachedHomeGreeting(text);
81
84
  }
82
85
  } catch (err) {
83
86
  log.warn({ err }, "Failed to generate personalized home greeting");
84
87
  }
88
+ return false;
85
89
  }
@@ -3,13 +3,25 @@
3
3
  *
4
4
  * Returns an array of `SuggestedPrompt` items shown at the top of the
5
5
  * Home page as conversation starters. All prompts are generated by the
6
- * assistant based on the user's connected services and context —
7
- * read from an in-memory cache in the GET path; generation runs in
8
- * the background via `refreshAssistantSuggestedPrompts`.
6
+ * assistant based on the user's connected services and context — read
7
+ * from a checkpoint-backed cache in the GET path. Generation runs on
8
+ * demand via `refreshAssistantSuggestedPrompts`, invoked fire-and-forget
9
+ * by the home-content revalidation coordinator when a client fetches the
10
+ * home feed and the cache is stale (see `home-content-refresh.ts`).
11
+ * Nothing generates at daemon startup or on a timer.
12
+ *
13
+ * The cache persists in the `memory_checkpoints` table so a daemon
14
+ * restart does not force a regeneration. OAuth connect/disconnect paths
15
+ * invalidate it explicitly so suggestions track integration state.
9
16
  */
10
17
 
11
18
  import { resolveCallSiteConfig } from "../config/llm-resolver.js";
12
19
  import { getConfig } from "../config/loader.js";
20
+ import {
21
+ deleteMemoryCheckpoint,
22
+ getMemoryCheckpoint,
23
+ setMemoryCheckpoint,
24
+ } from "../memory/checkpoints.js";
13
25
  import { buildSystemPrompt } from "../prompts/system-prompt.js";
14
26
  import { getConfiguredProvider } from "../providers/provider-send-message.js";
15
27
  import { buildAssistantEvent } from "../runtime/assistant-event.js";
@@ -22,14 +34,46 @@ import type { SuggestedPrompt } from "./feed-types.js";
22
34
  const log = getLogger("suggested-prompts");
23
35
 
24
36
  const LLM_SUGGESTIONS_TIMEOUT_MS = 5_000;
25
- const LLM_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
37
+ const LLM_CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
38
+
39
+ const CHECKPOINT_KEY_JSON = "home:suggested_prompts:json";
40
+ const CHECKPOINT_KEY_TIMESTAMP = "home:suggested_prompts:cached_at";
26
41
 
27
42
  // ---------------------------------------------------------------------------
28
- // In-memory cache for LLM-generated suggestions
43
+ // Checkpoint-backed cache for LLM-generated suggestions
29
44
  // ---------------------------------------------------------------------------
30
45
 
31
- let cachedLLMPrompts: SuggestedPrompt[] = [];
32
- let cachedLLMPromptsAt = 0;
46
+ function readCachedPrompts(): SuggestedPrompt[] | null {
47
+ try {
48
+ const json = getMemoryCheckpoint(CHECKPOINT_KEY_JSON);
49
+ const timestampStr = getMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP);
50
+ if (!json || !timestampStr) {
51
+ return null;
52
+ }
53
+ const cachedAt = Number(timestampStr);
54
+ if (isNaN(cachedAt) || Date.now() - cachedAt > LLM_CACHE_TTL_MS) {
55
+ return null;
56
+ }
57
+ const parsed = JSON.parse(json);
58
+ if (!Array.isArray(parsed)) {
59
+ return null;
60
+ }
61
+ return parsed as SuggestedPrompt[];
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function writeCachedPrompts(prompts: SuggestedPrompt[]): boolean {
68
+ try {
69
+ setMemoryCheckpoint(CHECKPOINT_KEY_JSON, JSON.stringify(prompts));
70
+ setMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP, String(Date.now()));
71
+ return true;
72
+ } catch {
73
+ // Cache write failure is non-fatal — the next refresh regenerates.
74
+ return false;
75
+ }
76
+ }
33
77
 
34
78
  /**
35
79
  * Return cached assistant-generated prompts. No LLM calls happen in
@@ -37,22 +81,23 @@ let cachedLLMPromptsAt = 0;
37
81
  * background refresh populates the cache.
38
82
  */
39
83
  export async function getSuggestedPrompts(): Promise<SuggestedPrompt[]> {
40
- if (Date.now() - cachedLLMPromptsAt < LLM_CACHE_TTL_MS) {
41
- return [...cachedLLMPrompts];
42
- }
43
- return [];
84
+ return readCachedPrompts() ?? [];
44
85
  }
45
86
 
46
87
  /**
47
- * Drops the in-memory suggestion cache so the next background refresh
48
- * regenerates prompts with current integration state.
88
+ * Drops the suggestion cache so the next on-demand refresh regenerates
89
+ * prompts with current integration state.
49
90
  *
50
91
  * Called from OAuth connect/disconnect paths so suggestions reflect the
51
- * new state within one reload instead of waiting for the 30-minute TTL.
92
+ * new state within one reload instead of waiting for the TTL.
52
93
  */
53
94
  export function invalidateAssistantSuggestedPromptsCache(): void {
54
- cachedLLMPrompts = [];
55
- cachedLLMPromptsAt = 0;
95
+ try {
96
+ deleteMemoryCheckpoint(CHECKPOINT_KEY_JSON);
97
+ deleteMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP);
98
+ } catch {
99
+ // Invalidation failure is non-fatal — the TTL still bounds staleness.
100
+ }
56
101
  assistantEventHub
57
102
  .publish(
58
103
  buildAssistantEvent({
@@ -70,21 +115,29 @@ export function invalidateAssistantSuggestedPromptsCache(): void {
70
115
  }
71
116
 
72
117
  /**
73
- * Generate LLM-based suggestion prompts and write them to the in-memory
74
- * cache. No-ops when the cache is still fresh. Intended for background
75
- * invocation (daemon startup / periodic refresh), not the GET path.
118
+ * Generate LLM-based suggestion prompts and write them to the cache.
119
+ * No-ops when the cache is still fresh. Intended for fire-and-forget
120
+ * background invocation, not the GET path. Returns `true` when a new
121
+ * batch was generated and cached.
76
122
  */
77
- export async function refreshAssistantSuggestedPrompts(): Promise<void> {
78
- if (Date.now() - cachedLLMPromptsAt < LLM_CACHE_TTL_MS) {
79
- return;
123
+ export async function refreshAssistantSuggestedPrompts(): Promise<boolean> {
124
+ if (readCachedPrompts() !== null) {
125
+ return false;
80
126
  }
81
127
 
82
128
  try {
83
129
  const llmPrompts = await generateAssistantPrompts();
84
- cachedLLMPrompts = llmPrompts;
85
- cachedLLMPromptsAt = Date.now();
130
+ // Cache empty results too — an empty or unparseable LLM response
131
+ // would otherwise leave the cache unpopulated and every Home feed
132
+ // GET would re-trigger generation until the TTL window closed.
133
+ // Only report success when the cache write landed AND there is new
134
+ // content — otherwise the coordinator would publish
135
+ // home_feed_updated for content the next GET cannot serve.
136
+ const wrote = writeCachedPrompts(llmPrompts);
137
+ return wrote && llmPrompts.length > 0;
86
138
  } catch (err) {
87
139
  log.warn({ err }, "Failed to refresh assistant suggested prompts");
140
+ return false;
88
141
  }
89
142
  }
90
143
 
@@ -1,123 +1,117 @@
1
1
  /**
2
2
  * Unit tests for the trust rule IPC proxy routes.
3
- *
4
- * Covers:
5
- * - trust_rules_list: no params, tool filter, include_all, origin filter
6
- * - error path: non-OK gateway response surfaces body .error message
7
3
  */
8
4
 
9
5
  import { beforeEach, describe, expect, mock, test } from "bun:test";
10
6
 
11
- // ---------------------------------------------------------------------------
12
- // Mocks — must be defined before importing the module under test
13
- // ---------------------------------------------------------------------------
14
-
15
- mock.module("../../config/env.js", () => ({
16
- getGatewayInternalBaseUrl: () => "http://localhost:7822",
17
- }));
18
-
19
- type MockResponse = {
20
- ok: boolean;
21
- status: number;
22
- json: () => Promise<unknown>;
7
+ type IpcCall = {
8
+ method: string;
9
+ params?: Record<string, unknown>;
23
10
  };
24
11
 
25
- let mockFetchResponse: MockResponse = {
26
- ok: true,
27
- status: 200,
28
- json: async () => ({ rules: [] }),
29
- };
30
-
31
- let capturedFetchCalls: Array<{ url: string; init?: RequestInit }> = [];
12
+ let ipcCalls: IpcCall[] = [];
13
+ let ipcResult: unknown = { rules: [] };
14
+ let ipcError: Error | undefined;
32
15
 
33
- const mockFetch = mock(async (url: string, init?: RequestInit) => {
34
- capturedFetchCalls.push({ url, init });
35
- return mockFetchResponse;
36
- });
16
+ const ipcCallPersistentMock = mock(
17
+ async (method: string, params?: Record<string, unknown>) => {
18
+ ipcCalls.push({ method, params });
19
+ if (ipcError) throw ipcError;
20
+ return ipcResult;
21
+ },
22
+ );
37
23
 
38
- global.fetch = mockFetch as unknown as typeof fetch;
39
-
40
- // ---------------------------------------------------------------------------
41
- // Import module under test AFTER mocks are set up
42
- // ---------------------------------------------------------------------------
24
+ mock.module("../gateway-client.js", () => ({
25
+ ipcCallPersistent: ipcCallPersistentMock,
26
+ }));
43
27
 
44
28
  import { ROUTES as trustRuleRoutes } from "../../runtime/routes/trust-rules-routes.js";
45
29
 
46
- // ---------------------------------------------------------------------------
47
- // Helpers
48
- // ---------------------------------------------------------------------------
49
-
50
30
  function findRoute(method: string) {
51
31
  const route = trustRuleRoutes.find((r) => r.operationId === method);
52
32
  if (!route) throw new Error(`Route not found: ${method}`);
53
33
  return route;
54
34
  }
55
35
 
56
- // ---------------------------------------------------------------------------
57
- // Tests
58
- // ---------------------------------------------------------------------------
59
-
60
36
  describe("trustRuleRoutes", () => {
61
37
  beforeEach(() => {
62
- capturedFetchCalls = [];
63
- mockFetchResponse = {
64
- ok: true,
65
- status: 200,
66
- json: async () => ({ rules: [] }),
67
- };
68
- mockFetch.mockClear();
38
+ ipcCalls = [];
39
+ ipcResult = { rules: [] };
40
+ ipcError = undefined;
41
+ ipcCallPersistentMock.mockClear();
69
42
  });
70
43
 
71
44
  describe("trust_rules_list", () => {
72
- test("no params → GET /v1/trust-rules (no query string)", async () => {
45
+ test("no params calls trust_rules_list with empty params", async () => {
73
46
  const route = findRoute("trust_rules_list");
74
47
  await route.handler({ body: {} });
75
48
 
76
- expect(capturedFetchCalls).toHaveLength(1);
77
- expect(capturedFetchCalls[0].url).toBe(
78
- "http://localhost:7822/v1/trust-rules",
79
- );
80
- expect(capturedFetchCalls[0].init).toBeUndefined();
49
+ expect(ipcCalls).toEqual([{ method: "trust_rules_list", params: {} }]);
81
50
  });
82
51
 
83
- test("{ tool: 'bash' } → appends ?tool=bash", async () => {
52
+ test("{ tool: 'bash' } is forwarded", async () => {
84
53
  const route = findRoute("trust_rules_list");
85
54
  await route.handler({ body: { tool: "bash" } });
86
55
 
87
- expect(capturedFetchCalls[0].url).toBe(
88
- "http://localhost:7822/v1/trust-rules?tool=bash",
89
- );
56
+ expect(ipcCalls).toEqual([
57
+ { method: "trust_rules_list", params: { tool: "bash" } },
58
+ ]);
90
59
  });
91
60
 
92
- test("{ include_all: true } → appends ?include_all=true", async () => {
61
+ test("{ include_all: true } is forwarded", async () => {
93
62
  const route = findRoute("trust_rules_list");
94
63
  await route.handler({ body: { include_all: true } });
95
64
 
96
- expect(capturedFetchCalls[0].url).toBe(
97
- "http://localhost:7822/v1/trust-rules?include_all=true",
98
- );
65
+ expect(ipcCalls).toEqual([
66
+ { method: "trust_rules_list", params: { include_all: true } },
67
+ ]);
99
68
  });
100
69
 
101
- test("{ origin: 'user_defined' } → appends ?origin=user_defined", async () => {
70
+ test("{ origin: 'user_defined' } is forwarded", async () => {
102
71
  const route = findRoute("trust_rules_list");
103
72
  await route.handler({ body: { origin: "user_defined" } });
104
73
 
105
- expect(capturedFetchCalls[0].url).toBe(
106
- "http://localhost:7822/v1/trust-rules?origin=user_defined",
107
- );
74
+ expect(ipcCalls).toEqual([
75
+ {
76
+ method: "trust_rules_list",
77
+ params: { origin: "user_defined" },
78
+ },
79
+ ]);
80
+ });
81
+
82
+ test("returns the parsed gateway IPC response", async () => {
83
+ ipcResult = {
84
+ rules: [
85
+ {
86
+ id: "rule-123",
87
+ tool: "bash",
88
+ pattern: "echo hello",
89
+ risk: "low",
90
+ description: "Allow echo hello",
91
+ origin: "user_defined",
92
+ userModified: false,
93
+ deleted: false,
94
+ createdAt: "2026-01-01T00:00:00.000Z",
95
+ updatedAt: "2026-01-01T00:00:00.000Z",
96
+ },
97
+ ],
98
+ };
99
+
100
+ const route = findRoute("trust_rules_list");
101
+ const result = await route.handler({ body: {} });
102
+
103
+ expect(result).toEqual(ipcResult);
108
104
  });
109
105
  });
110
106
 
111
107
  describe("error path", () => {
112
- test("non-OK response surfaces body .error message", async () => {
113
- mockFetchResponse = {
114
- ok: false,
115
- status: 404,
116
- json: async () => ({ error: "Not found" }),
117
- };
108
+ test("gateway IPC error is propagated", async () => {
109
+ ipcError = new Error("Gateway IPC call timed out");
118
110
 
119
111
  const route = findRoute("trust_rules_list");
120
- await expect(route.handler({ body: {} })).rejects.toThrow("Not found");
112
+ await expect(route.handler({ body: {} })).rejects.toThrow(
113
+ "Gateway IPC call timed out",
114
+ );
121
115
  });
122
116
  });
123
117
  });
@@ -66,8 +66,8 @@ export async function resolveImageGenCredentials(opts: {
66
66
  function providerKeyHint(provider: ImageGenProvider): string {
67
67
  switch (provider) {
68
68
  case "gemini":
69
- return "No Gemini API key configured. Please set your Gemini API key in Settings > Models & Services.";
69
+ return "No Gemini API key configured. Please set your Gemini API key in Settings → Models & Services.";
70
70
  case "openai":
71
- return "No OpenAI API key configured. Please set your OpenAI API key in Settings > Models & Services.";
71
+ return "No OpenAI API key configured. Please set your OpenAI API key in Settings → Models & Services.";
72
72
  }
73
73
  }