@vellumai/assistant 0.8.11 → 0.8.12-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.
- package/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +126 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +219 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +2 -1
- package/src/providers/openai/responses-provider.ts +5 -1
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/plugins-routes.ts +21 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ─── Mocks ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
let greetingRefreshed = false;
|
|
6
|
+
let promptsRefreshed = false;
|
|
7
|
+
let greetingCalls = 0;
|
|
8
|
+
let promptsCalls = 0;
|
|
9
|
+
let greetingGate: Promise<void> = Promise.resolve();
|
|
10
|
+
|
|
11
|
+
mock.module("../home-greeting.js", () => ({
|
|
12
|
+
refreshPersonalizedGreeting: async () => {
|
|
13
|
+
greetingCalls++;
|
|
14
|
+
await greetingGate;
|
|
15
|
+
return greetingRefreshed;
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
mock.module("../suggested-prompts.js", () => ({
|
|
20
|
+
refreshAssistantSuggestedPrompts: async () => {
|
|
21
|
+
promptsCalls++;
|
|
22
|
+
return promptsRefreshed;
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
|
|
27
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
28
|
+
assistantEventHub: { publish: publishSpy },
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
mock.module("../../runtime/assistant-event.js", () => ({
|
|
32
|
+
buildAssistantEvent: (e: unknown) => e,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module("../../util/logger.js", () => ({
|
|
36
|
+
getLogger: () =>
|
|
37
|
+
new Proxy({} as Record<string, unknown>, {
|
|
38
|
+
get: () => () => {},
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const { revalidateHomeContentInBackground } =
|
|
43
|
+
await import("../home-content-refresh.js");
|
|
44
|
+
|
|
45
|
+
async function settle(): Promise<void> {
|
|
46
|
+
// Let the fire-and-forget revalidation chain run to completion.
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Tests ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
describe("revalidateHomeContentInBackground", () => {
|
|
54
|
+
test("publishes home_feed_updated when content was refreshed", async () => {
|
|
55
|
+
publishSpy.mockClear();
|
|
56
|
+
greetingRefreshed = true;
|
|
57
|
+
promptsRefreshed = false;
|
|
58
|
+
|
|
59
|
+
revalidateHomeContentInBackground();
|
|
60
|
+
await settle();
|
|
61
|
+
|
|
62
|
+
expect(publishSpy).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(publishSpy.mock.calls[0]?.[0]).toMatchObject({
|
|
64
|
+
type: "home_feed_updated",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("does not publish when both caches were fresh", async () => {
|
|
69
|
+
publishSpy.mockClear();
|
|
70
|
+
greetingRefreshed = false;
|
|
71
|
+
promptsRefreshed = false;
|
|
72
|
+
|
|
73
|
+
revalidateHomeContentInBackground();
|
|
74
|
+
await settle();
|
|
75
|
+
|
|
76
|
+
expect(publishSpy).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("concurrent calls share a single in-flight revalidation", async () => {
|
|
80
|
+
greetingCalls = 0;
|
|
81
|
+
promptsCalls = 0;
|
|
82
|
+
greetingRefreshed = false;
|
|
83
|
+
promptsRefreshed = false;
|
|
84
|
+
|
|
85
|
+
let release!: () => void;
|
|
86
|
+
greetingGate = new Promise((resolve) => {
|
|
87
|
+
release = resolve;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
revalidateHomeContentInBackground();
|
|
91
|
+
revalidateHomeContentInBackground();
|
|
92
|
+
revalidateHomeContentInBackground();
|
|
93
|
+
|
|
94
|
+
release();
|
|
95
|
+
greetingGate = Promise.resolve();
|
|
96
|
+
await settle();
|
|
97
|
+
|
|
98
|
+
expect(greetingCalls).toBe(1);
|
|
99
|
+
expect(promptsCalls).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a completed run allows a later revalidation", async () => {
|
|
103
|
+
greetingCalls = 0;
|
|
104
|
+
greetingRefreshed = false;
|
|
105
|
+
promptsRefreshed = false;
|
|
106
|
+
|
|
107
|
+
revalidateHomeContentInBackground();
|
|
108
|
+
await settle();
|
|
109
|
+
revalidateHomeContentInBackground();
|
|
110
|
+
await settle();
|
|
111
|
+
|
|
112
|
+
expect(greetingCalls).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -4,6 +4,25 @@ import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
|
4
4
|
|
|
5
5
|
const mockIntegrationSummary = "Gmail ✓ | Slack ✓ | Twilio ✗ | Telegram ✗";
|
|
6
6
|
let mockSidechainText = "";
|
|
7
|
+
let sidechainCalls = 0;
|
|
8
|
+
|
|
9
|
+
// In-memory stand-in for the memory_checkpoints table so the cache
|
|
10
|
+
// round-trips without a real database.
|
|
11
|
+
const checkpointStore = new Map<string, string>();
|
|
12
|
+
let checkpointWritesShouldThrow = false;
|
|
13
|
+
|
|
14
|
+
mock.module("../../memory/checkpoints.js", () => ({
|
|
15
|
+
getMemoryCheckpoint: (key: string) => checkpointStore.get(key) ?? null,
|
|
16
|
+
setMemoryCheckpoint: (key: string, value: string) => {
|
|
17
|
+
if (checkpointWritesShouldThrow) {
|
|
18
|
+
throw new Error("synthetic checkpoint write failure");
|
|
19
|
+
}
|
|
20
|
+
checkpointStore.set(key, value);
|
|
21
|
+
},
|
|
22
|
+
deleteMemoryCheckpoint: (key: string) => {
|
|
23
|
+
checkpointStore.delete(key);
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
7
26
|
|
|
8
27
|
mock.module("../../schedule/integration-status.js", () => ({
|
|
9
28
|
formatIntegrationSummary: async () => mockIntegrationSummary,
|
|
@@ -41,7 +60,10 @@ mock.module("../../prompts/system-prompt.js", () => ({
|
|
|
41
60
|
}));
|
|
42
61
|
|
|
43
62
|
mock.module("../../runtime/btw-sidechain.js", () => ({
|
|
44
|
-
runBtwSidechain: async () =>
|
|
63
|
+
runBtwSidechain: async () => {
|
|
64
|
+
sidechainCalls++;
|
|
65
|
+
return { text: mockSidechainText };
|
|
66
|
+
},
|
|
45
67
|
}));
|
|
46
68
|
|
|
47
69
|
mock.module("../../runtime/assistant-event.js", () => ({
|
|
@@ -62,8 +84,10 @@ const {
|
|
|
62
84
|
|
|
63
85
|
describe("getSuggestedPrompts", () => {
|
|
64
86
|
afterEach(() => {
|
|
87
|
+
checkpointWritesShouldThrow = false;
|
|
65
88
|
invalidateAssistantSuggestedPromptsCache();
|
|
66
89
|
mockSidechainText = "";
|
|
90
|
+
sidechainCalls = 0;
|
|
67
91
|
});
|
|
68
92
|
|
|
69
93
|
test("returns empty array before cache is populated", async () => {
|
|
@@ -98,12 +122,17 @@ describe("getSuggestedPrompts", () => {
|
|
|
98
122
|
expect(await getSuggestedPrompts()).toEqual([]);
|
|
99
123
|
});
|
|
100
124
|
|
|
101
|
-
test("handles empty LLM response gracefully", async () => {
|
|
125
|
+
test("handles empty LLM response gracefully and caches the empty result", async () => {
|
|
102
126
|
mockSidechainText = "";
|
|
103
127
|
|
|
104
|
-
await refreshAssistantSuggestedPrompts();
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(false);
|
|
129
|
+
expect(await getSuggestedPrompts()).toEqual([]);
|
|
130
|
+
|
|
131
|
+
// The empty result is cached: the next refresh within the TTL must
|
|
132
|
+
// not hit the LLM again (prevents a generation loop on every Home
|
|
133
|
+
// feed GET while the model returns nothing usable).
|
|
134
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(false);
|
|
135
|
+
expect(sidechainCalls).toBe(1);
|
|
107
136
|
});
|
|
108
137
|
|
|
109
138
|
test("handles malformed LLM response gracefully", async () => {
|
|
@@ -113,4 +142,56 @@ describe("getSuggestedPrompts", () => {
|
|
|
113
142
|
const prompts = await getSuggestedPrompts();
|
|
114
143
|
expect(prompts).toEqual([]);
|
|
115
144
|
});
|
|
145
|
+
|
|
146
|
+
test("refresh reports whether new content was generated", async () => {
|
|
147
|
+
mockSidechainText = JSON.stringify([
|
|
148
|
+
{ label: "Do stuff", prompt: "Do stuff for me" },
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(true);
|
|
152
|
+
// Cache is fresh — second refresh must not hit the LLM again.
|
|
153
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(false);
|
|
154
|
+
expect(sidechainCalls).toBe(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("refresh reports false when the cache write fails", async () => {
|
|
158
|
+
mockSidechainText = JSON.stringify([
|
|
159
|
+
{ label: "Do stuff", prompt: "Do stuff for me" },
|
|
160
|
+
]);
|
|
161
|
+
checkpointWritesShouldThrow = true;
|
|
162
|
+
|
|
163
|
+
// The generation succeeded but nothing was cached, so the caller must
|
|
164
|
+
// not announce fresh content (clients would refetch into a cache miss
|
|
165
|
+
// and re-trigger generation on every Home load).
|
|
166
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(false);
|
|
167
|
+
expect(await getSuggestedPrompts()).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("refresh regenerates after invalidation", async () => {
|
|
171
|
+
mockSidechainText = JSON.stringify([
|
|
172
|
+
{ label: "Do stuff", prompt: "Do stuff for me" },
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
await refreshAssistantSuggestedPrompts();
|
|
176
|
+
invalidateAssistantSuggestedPromptsCache();
|
|
177
|
+
|
|
178
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(true);
|
|
179
|
+
expect(sidechainCalls).toBe(2);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("cache survives across module state (checkpoint-backed)", async () => {
|
|
183
|
+
mockSidechainText = JSON.stringify([
|
|
184
|
+
{ label: "Persisted", prompt: "I came from the checkpoint store" },
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
await refreshAssistantSuggestedPrompts();
|
|
188
|
+
// Simulate the read path a fresh daemon would take: the prompts come
|
|
189
|
+
// back from the checkpoint store, not module-local memory.
|
|
190
|
+
expect(checkpointStore.get("home:suggested_prompts:json")).toContain(
|
|
191
|
+
"Persisted",
|
|
192
|
+
);
|
|
193
|
+
const prompts = await getSuggestedPrompts();
|
|
194
|
+
expect(prompts).toHaveLength(1);
|
|
195
|
+
expect(prompts[0]!.label).toBe("Persisted");
|
|
196
|
+
});
|
|
116
197
|
});
|
|
@@ -1,52 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* On-demand revalidation for LLM-generated home page content.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
* timer
|
|
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
|
-
|
|
25
|
+
let inFlight: Promise<void> | null = null;
|
|
20
26
|
|
|
21
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
41
|
-
* invocation
|
|
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<
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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 =
|
|
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
|
-
//
|
|
43
|
+
// Checkpoint-backed cache for LLM-generated suggestions
|
|
29
44
|
// ---------------------------------------------------------------------------
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
41
|
-
return [...cachedLLMPrompts];
|
|
42
|
-
}
|
|
43
|
-
return [];
|
|
84
|
+
return readCachedPrompts() ?? [];
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
/**
|
|
47
|
-
* Drops the
|
|
48
|
-
*
|
|
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
|
|
92
|
+
* new state within one reload instead of waiting for the TTL.
|
|
52
93
|
*/
|
|
53
94
|
export function invalidateAssistantSuggestedPromptsCache(): void {
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
74
|
-
*
|
|
75
|
-
* invocation
|
|
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<
|
|
78
|
-
if (
|
|
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
|
-
|
|
85
|
-
|
|
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
|
|