@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
|
@@ -22,87 +22,90 @@ import type { WorkspaceMigration } from "./types.js";
|
|
|
22
22
|
// - Already have thinking enabled (idempotent)
|
|
23
23
|
// - Are source: "user" (user-created profiles are untouched)
|
|
24
24
|
// - Have a non-managed, non-absent source (unknown origin)
|
|
25
|
-
// -
|
|
25
|
+
// - Resolve to a non-Anthropic provider (adaptive thinking is
|
|
26
|
+
// Anthropic-specific). A profile with no explicit provider inherits
|
|
27
|
+
// llm.default.provider, so the check falls back to that — with a
|
|
28
|
+
// completely absent llm.default.provider treated as Anthropic, matching
|
|
29
|
+
// migration 052's own `?? "anthropic"` default. This keeps the legacy
|
|
30
|
+
// non-Anthropic empty `{}` shells seeded by migration 052 off the repair.
|
|
31
|
+
//
|
|
32
|
+
// Note: the live startup path re-runs the same repair after the platform
|
|
33
|
+
// overlay merge — see assistant/src/workspace/adaptive-thinking-repair.ts.
|
|
34
|
+
// This file keeps its own frozen copy of the logic because migration modules
|
|
35
|
+
// are self-contained snapshots and must never be imported by other code.
|
|
26
36
|
|
|
27
37
|
const ADAPTIVE_THINKING = { enabled: true, streamThinking: true } as const;
|
|
28
38
|
const TARGET_PROFILES = ["balanced", "quality-optimized"] as const;
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
* The function is idempotent: profiles that already have thinking enabled are skipped.
|
|
40
|
-
*/
|
|
41
|
-
export function repairAdaptiveThinkingOnManagedProfiles(
|
|
42
|
-
workspaceDir: string,
|
|
43
|
-
): void {
|
|
44
|
-
const configPath = join(workspaceDir, "config.json");
|
|
45
|
-
if (!existsSync(configPath)) return;
|
|
40
|
+
export const enableAdaptiveThinkingManagedProfilesMigration: WorkspaceMigration =
|
|
41
|
+
{
|
|
42
|
+
id: "097-enable-adaptive-thinking-managed-profiles",
|
|
43
|
+
description:
|
|
44
|
+
"Enable adaptive thinking on managed balanced and quality-optimized profiles",
|
|
45
|
+
run(workspaceDir: string): void {
|
|
46
|
+
const configPath = join(workspaceDir, "config.json");
|
|
47
|
+
if (!existsSync(configPath)) return;
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
let config: Record<string, unknown>;
|
|
50
|
+
try {
|
|
51
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
52
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
53
|
+
config = raw as Record<string, unknown>;
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
const llm = readObject(config.llm);
|
|
59
|
+
if (llm === null) return;
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const profiles = readObject(llm.profiles);
|
|
62
|
+
if (profiles === null) return;
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
// Profiles without an explicit provider inherit llm.default.provider at
|
|
65
|
+
// resolution time; an absent llm.default.provider resolves to Anthropic.
|
|
66
|
+
const defaultBlock = readObject(llm.default);
|
|
67
|
+
const defaultProvider =
|
|
68
|
+
typeof defaultBlock?.provider === "string"
|
|
69
|
+
? defaultBlock.provider
|
|
70
|
+
: "anthropic";
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
const profile = readObject(profiles[name]);
|
|
66
|
-
if (profile === null) continue;
|
|
72
|
+
let changed = false;
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// of the canonical managed names (which TARGET_PROFILES already guarantees)
|
|
72
|
-
// and the provider is Anthropic (or absent). Explicit `source: "user"`
|
|
73
|
-
// profiles are always skipped.
|
|
74
|
-
if (profile.source === "user") continue;
|
|
75
|
-
if (profile.source !== undefined && profile.source !== "managed") continue;
|
|
76
|
-
if (
|
|
77
|
-
typeof profile.provider === "string" &&
|
|
78
|
-
profile.provider !== "anthropic"
|
|
79
|
-
) {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
74
|
+
for (const name of TARGET_PROFILES) {
|
|
75
|
+
const profile = readObject(profiles[name]);
|
|
76
|
+
if (profile === null) continue;
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
// Only patch managed Anthropic profiles.
|
|
79
|
+
// Legacy profiles created before the `source` metadata field was
|
|
80
|
+
// introduced have source=undefined. Treat these as managed when the
|
|
81
|
+
// profile name is one of the canonical managed names (which
|
|
82
|
+
// TARGET_PROFILES already guarantees) and the effective provider —
|
|
83
|
+
// explicit, or inherited from llm.default — is Anthropic. Explicit
|
|
84
|
+
// `source: "user"` profiles are always skipped.
|
|
85
|
+
if (profile.source === "user") continue;
|
|
86
|
+
if (profile.source !== undefined && profile.source !== "managed") {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const effectiveProvider =
|
|
90
|
+
typeof profile.provider === "string"
|
|
91
|
+
? profile.provider
|
|
92
|
+
: defaultProvider;
|
|
93
|
+
if (effectiveProvider !== "anthropic") continue;
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
95
|
+
// Skip if thinking is already enabled.
|
|
96
|
+
const thinking = readObject(profile.thinking);
|
|
97
|
+
if (thinking !== null && thinking.enabled === true) continue;
|
|
91
98
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
}
|
|
99
|
+
profile.thinking = { ...ADAPTIVE_THINKING };
|
|
100
|
+
profiles[name] = profile;
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
run(workspaceDir: string): void {
|
|
105
|
-
repairAdaptiveThinkingOnManagedProfiles(workspaceDir);
|
|
104
|
+
if (changed) {
|
|
105
|
+
llm.profiles = profiles;
|
|
106
|
+
config.llm = llm;
|
|
107
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
108
|
+
}
|
|
106
109
|
},
|
|
107
110
|
down(_workspaceDir: string): void {
|
|
108
111
|
// Forward-only.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const MIGRATION_ID = "098-remove-stale-updates-bulletin-file";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Delete `<workspace>/UPDATES.md`.
|
|
10
|
+
*
|
|
11
|
+
* Release notes used to be appended to the file by workspace migrations and
|
|
12
|
+
* processed by a background conversation dispatched at daemon startup. That
|
|
13
|
+
* feature has been removed — nothing consumes the file's contents anymore,
|
|
14
|
+
* and it was system-written bulletin material, so it is deleted outright.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent: `rmSync` with `force` is a no-op when the file is absent.
|
|
17
|
+
*/
|
|
18
|
+
export const removeStaleUpdatesBulletinFileMigration: WorkspaceMigration = {
|
|
19
|
+
id: MIGRATION_ID,
|
|
20
|
+
description:
|
|
21
|
+
"Delete leftover UPDATES.md release-note bulletins (processing job removed)",
|
|
22
|
+
|
|
23
|
+
run(workspaceDir: string): void {
|
|
24
|
+
rmSync(join(workspaceDir, "UPDATES.md"), { force: true });
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
down(_workspaceDir: string): void {
|
|
28
|
+
// Forward-only: the removed content was pending release-note bulletins
|
|
29
|
+
// for a feature that no longer exists.
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Add `disableCache: true` to existing on-disk entries for the one-shot LLM
|
|
8
|
+
* call sites whose shipped defaults now opt out of prompt caching.
|
|
9
|
+
*
|
|
10
|
+
* The resolver prefers an explicit `llm.callSites.<id>` entry over
|
|
11
|
+
* CALL_SITE_DEFAULTS, and earlier migrations (054 recall, 072 replySuggestion,
|
|
12
|
+
* and platform overlays for the others) seeded such entries. Without this
|
|
13
|
+
* migration, upgraded workspaces would keep paying prompt-cache writes on
|
|
14
|
+
* call sites whose prompts never produce a cache read — only fresh installs
|
|
15
|
+
* would pick up the new shipped default.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent and customization-preserving: an entry that already carries a
|
|
18
|
+
* `disableCache` key (either value — the user may have opted back in) is
|
|
19
|
+
* left untouched, and only the one key is added to entries we patch. Entries
|
|
20
|
+
* that don't exist on disk are skipped — those workspaces already resolve
|
|
21
|
+
* the shipped default.
|
|
22
|
+
*/
|
|
23
|
+
const ONE_SHOT_CALL_SITES = [
|
|
24
|
+
"recall",
|
|
25
|
+
"replySuggestion",
|
|
26
|
+
"homeGreeting",
|
|
27
|
+
"homeSuggestedPrompts",
|
|
28
|
+
"conversationTitle",
|
|
29
|
+
"memoryConsolidation",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export const disableCacheOneShotCallsitesMigration: WorkspaceMigration = {
|
|
33
|
+
id: "099-disable-cache-one-shot-callsites",
|
|
34
|
+
description:
|
|
35
|
+
"Add disableCache: true to existing one-shot call-site entries so upgraded workspaces match the new shipped defaults",
|
|
36
|
+
run(workspaceDir: string): void {
|
|
37
|
+
if (process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH) return;
|
|
38
|
+
|
|
39
|
+
const configPath = join(workspaceDir, "config.json");
|
|
40
|
+
if (!existsSync(configPath)) return;
|
|
41
|
+
|
|
42
|
+
let config: Record<string, unknown> = {};
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
45
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
46
|
+
config = raw as Record<string, unknown>;
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const llm = readObject(config.llm);
|
|
52
|
+
if (llm === null) return;
|
|
53
|
+
const callSites = readObject(llm.callSites);
|
|
54
|
+
if (callSites === null) return;
|
|
55
|
+
|
|
56
|
+
let changed = false;
|
|
57
|
+
for (const callSite of ONE_SHOT_CALL_SITES) {
|
|
58
|
+
const entry = readObject(callSites[callSite]);
|
|
59
|
+
if (entry === null) continue;
|
|
60
|
+
if ("disableCache" in entry) continue;
|
|
61
|
+
entry.disableCache = true;
|
|
62
|
+
callSites[callSite] = entry;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
if (!changed) return;
|
|
66
|
+
|
|
67
|
+
llm.callSites = callSites;
|
|
68
|
+
config.llm = llm;
|
|
69
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
70
|
+
},
|
|
71
|
+
down(_workspaceDir: string): void {
|
|
72
|
+
// Forward-only.
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function readObject(value: unknown): Record<string, unknown> | null {
|
|
77
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return value as Record<string, unknown>;
|
|
81
|
+
}
|
|
@@ -95,6 +95,8 @@ import { seedAvatarManifestMigration } from "./094-seed-avatar-manifest.js";
|
|
|
95
95
|
import { bumpHeartbeatInterval30mTo60mMigration } from "./095-bump-heartbeat-interval-30m-to-60m.js";
|
|
96
96
|
import { reduceQualityProfileEffortMigration } from "./096-reduce-quality-profile-effort.js";
|
|
97
97
|
import { enableAdaptiveThinkingManagedProfilesMigration } from "./097-enable-adaptive-thinking-managed-profiles.js";
|
|
98
|
+
import { removeStaleUpdatesBulletinFileMigration } from "./098-remove-stale-updates-bulletin-file.js";
|
|
99
|
+
import { disableCacheOneShotCallsitesMigration } from "./099-disable-cache-one-shot-callsites.js";
|
|
98
100
|
import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
|
|
99
101
|
import type { WorkspaceMigration } from "./types.js";
|
|
100
102
|
|
|
@@ -201,4 +203,6 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
|
|
|
201
203
|
bumpHeartbeatInterval30mTo60mMigration,
|
|
202
204
|
reduceQualityProfileEffortMigration,
|
|
203
205
|
enableAdaptiveThinkingManagedProfilesMigration,
|
|
206
|
+
removeStaleUpdatesBulletinFileMigration,
|
|
207
|
+
disableCacheOneShotCallsitesMigration,
|
|
204
208
|
];
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* When loadConfig()/loadRawConfig() quarantines a corrupt config.json, it
|
|
3
|
-
* appends a bulletin to <workspace>/UPDATES.md so the background update-
|
|
4
|
-
* bulletin job picks up the event inside a background-only conversation.
|
|
5
|
-
* The agent decides whether to surface the quarantine to the user — it's
|
|
6
|
-
* agent-visible context, not a push notification.
|
|
7
|
-
*
|
|
8
|
-
* The bulletin is keyed on the quarantine filename via an HTML marker so
|
|
9
|
-
* repeated appends for the same quarantine are idempotent — per the
|
|
10
|
-
* Release-Update-Hygiene rule in the root AGENTS.md, idempotency at both the
|
|
11
|
-
* runner level AND an in-file marker is required to close the crash-mid-append
|
|
12
|
-
* window.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
existsSync,
|
|
17
|
-
mkdirSync,
|
|
18
|
-
readdirSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
rmSync,
|
|
21
|
-
writeFileSync,
|
|
22
|
-
} from "node:fs";
|
|
23
|
-
import { basename, join } from "node:path";
|
|
24
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
_appendQuarantineBulletin,
|
|
28
|
-
invalidateConfigCache,
|
|
29
|
-
loadConfig,
|
|
30
|
-
} from "../config/loader.js";
|
|
31
|
-
import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Helpers
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
|
|
38
|
-
const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
|
|
39
|
-
const UPDATES_PATH = join(WORKSPACE_DIR, "UPDATES.md");
|
|
40
|
-
|
|
41
|
-
function ensureTestDir(): void {
|
|
42
|
-
const dirs = [
|
|
43
|
-
WORKSPACE_DIR,
|
|
44
|
-
join(WORKSPACE_DIR, "data"),
|
|
45
|
-
join(WORKSPACE_DIR, "data", "memory"),
|
|
46
|
-
join(WORKSPACE_DIR, "data", "memory", "knowledge"),
|
|
47
|
-
join(WORKSPACE_DIR, "data", "logs"),
|
|
48
|
-
];
|
|
49
|
-
for (const dir of dirs) {
|
|
50
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function resetWorkspace(): void {
|
|
55
|
-
for (const name of readdirSync(WORKSPACE_DIR)) {
|
|
56
|
-
rmSync(join(WORKSPACE_DIR, name), { recursive: true, force: true });
|
|
57
|
-
}
|
|
58
|
-
ensureTestDir();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function listQuarantinedFiles(): string[] {
|
|
62
|
-
return readdirSync(WORKSPACE_DIR).filter((name) =>
|
|
63
|
-
/^config\.json\.corrupt-.+\.json$/.test(name),
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// Tests
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
describe("config-quarantine UPDATES.md bulletin", () => {
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
resetWorkspace();
|
|
74
|
-
setStorePathForTesting(join(WORKSPACE_DIR, "keys.enc"));
|
|
75
|
-
invalidateConfigCache();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
afterEach(() => {
|
|
79
|
-
setStorePathForTesting(null);
|
|
80
|
-
invalidateConfigCache();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("writes a bulletin with the quarantine marker when config.json is corrupt", () => {
|
|
84
|
-
writeFileSync(CONFIG_PATH, '{"provider": "anthropic", "mo');
|
|
85
|
-
|
|
86
|
-
loadConfig();
|
|
87
|
-
|
|
88
|
-
const [quarantinedName] = listQuarantinedFiles();
|
|
89
|
-
expect(quarantinedName).toBeDefined();
|
|
90
|
-
const quarantinedPath = join(WORKSPACE_DIR, quarantinedName);
|
|
91
|
-
|
|
92
|
-
expect(existsSync(UPDATES_PATH)).toBe(true);
|
|
93
|
-
const body = readFileSync(UPDATES_PATH, "utf-8");
|
|
94
|
-
|
|
95
|
-
// Heading must match the wording the background update-bulletin job
|
|
96
|
-
// relays to the user.
|
|
97
|
-
expect(body).toContain("## Config was reset to defaults");
|
|
98
|
-
// Quarantine file path is what the user cats to recover.
|
|
99
|
-
expect(body).toContain(quarantinedPath);
|
|
100
|
-
// Idempotency marker is shape-exact, basename-keyed (not full path).
|
|
101
|
-
expect(body).toContain(`<!-- config-quarantine:${quarantinedName} -->`);
|
|
102
|
-
expect(body).toMatch(
|
|
103
|
-
/<!-- config-quarantine:config\.json\.corrupt-.+\.json -->/,
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("two successive quarantines append two distinct bulletins (not overwrite)", () => {
|
|
108
|
-
// First corruption round.
|
|
109
|
-
writeFileSync(CONFIG_PATH, '{"partial": ');
|
|
110
|
-
loadConfig();
|
|
111
|
-
invalidateConfigCache();
|
|
112
|
-
|
|
113
|
-
const firstQuarantined = listQuarantinedFiles();
|
|
114
|
-
expect(firstQuarantined).toHaveLength(1);
|
|
115
|
-
const firstBody = readFileSync(UPDATES_PATH, "utf-8");
|
|
116
|
-
const firstMarker = `<!-- config-quarantine:${firstQuarantined[0]} -->`;
|
|
117
|
-
expect(firstBody).toContain(firstMarker);
|
|
118
|
-
|
|
119
|
-
// Loader wrote a fresh default config.json after quarantine; corrupt it
|
|
120
|
-
// again. Sleep briefly to guarantee a different ISO-timestamp millisecond
|
|
121
|
-
// component and therefore a distinct quarantine filename.
|
|
122
|
-
const untilDifferentMs = Date.now() + 5;
|
|
123
|
-
while (Date.now() < untilDifferentMs) {
|
|
124
|
-
/* spin */
|
|
125
|
-
}
|
|
126
|
-
writeFileSync(CONFIG_PATH, "still not json");
|
|
127
|
-
loadConfig();
|
|
128
|
-
|
|
129
|
-
const quarantined = listQuarantinedFiles().sort();
|
|
130
|
-
expect(quarantined).toHaveLength(2);
|
|
131
|
-
|
|
132
|
-
const body = readFileSync(UPDATES_PATH, "utf-8");
|
|
133
|
-
for (const name of quarantined) {
|
|
134
|
-
expect(body).toContain(`<!-- config-quarantine:${name} -->`);
|
|
135
|
-
}
|
|
136
|
-
// Two "Config was reset to defaults" sections — appended, not overwritten.
|
|
137
|
-
const headingMatches = body.match(/## Config was reset to defaults/g) ?? [];
|
|
138
|
-
expect(headingMatches).toHaveLength(2);
|
|
139
|
-
// The earlier marker is still present (append semantics).
|
|
140
|
-
expect(body).toContain(firstMarker);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("idempotent: pre-existing marker for the same quarantine filename skips the append", () => {
|
|
144
|
-
// Simulate a crash-mid-append: UPDATES.md already contains a marker for
|
|
145
|
-
// a specific quarantine filename. A follow-up call referencing the same
|
|
146
|
-
// filename must leave the file untouched.
|
|
147
|
-
const quarantineName = "config.json.corrupt-2026-04-20T12-00-00.000Z.json";
|
|
148
|
-
const quarantinePath = join(WORKSPACE_DIR, quarantineName);
|
|
149
|
-
const preexisting =
|
|
150
|
-
`## Config was reset to defaults\n\n` +
|
|
151
|
-
`Pre-existing bulletin for ${quarantinePath}.\n\n` +
|
|
152
|
-
`<!-- config-quarantine:${quarantineName} -->\n`;
|
|
153
|
-
writeFileSync(UPDATES_PATH, preexisting, "utf-8");
|
|
154
|
-
// Also create the quarantine file on disk so the helper sees an
|
|
155
|
-
// environment consistent with a prior successful rename.
|
|
156
|
-
writeFileSync(quarantinePath, "{ not json", "utf-8");
|
|
157
|
-
|
|
158
|
-
_appendQuarantineBulletin(CONFIG_PATH, quarantinePath);
|
|
159
|
-
|
|
160
|
-
const after = readFileSync(UPDATES_PATH, "utf-8");
|
|
161
|
-
expect(after).toBe(preexisting);
|
|
162
|
-
// Exactly one marker present — no duplicate was appended.
|
|
163
|
-
expect(
|
|
164
|
-
after.match(
|
|
165
|
-
new RegExp(`<!-- config-quarantine:${quarantineName} -->`, "g"),
|
|
166
|
-
)?.length,
|
|
167
|
-
).toBe(1);
|
|
168
|
-
expect(basename(quarantinePath)).toBe(quarantineName);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("valid config.json does not create UPDATES.md (regression guard)", () => {
|
|
172
|
-
writeFileSync(
|
|
173
|
-
CONFIG_PATH,
|
|
174
|
-
JSON.stringify({ provider: "anthropic", model: "claude-opus-4-7" }),
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
loadConfig();
|
|
178
|
-
|
|
179
|
-
expect(listQuarantinedFiles()).toHaveLength(0);
|
|
180
|
-
expect(existsSync(UPDATES_PATH)).toBe(false);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("appends (does not overwrite) when UPDATES.md already has unrelated content", () => {
|
|
184
|
-
const priorContent =
|
|
185
|
-
`## Some earlier bulletin\n\n` +
|
|
186
|
-
`Unrelated prior content from a previous migration.\n\n` +
|
|
187
|
-
`<!-- release-note-id:unrelated-note -->\n`;
|
|
188
|
-
writeFileSync(UPDATES_PATH, priorContent, "utf-8");
|
|
189
|
-
|
|
190
|
-
writeFileSync(CONFIG_PATH, "{oops");
|
|
191
|
-
loadConfig();
|
|
192
|
-
|
|
193
|
-
const body = readFileSync(UPDATES_PATH, "utf-8");
|
|
194
|
-
// Prior content preserved verbatim at the start.
|
|
195
|
-
expect(body.startsWith(priorContent)).toBe(true);
|
|
196
|
-
// New bulletin appended.
|
|
197
|
-
expect(body).toContain("## Config was reset to defaults");
|
|
198
|
-
expect(body).toMatch(
|
|
199
|
-
/<!-- config-quarantine:config\.json\.corrupt-.+\.json -->/,
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
});
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { v4 as uuid } from "uuid";
|
|
4
|
-
|
|
5
|
-
mock.module("../util/logger.js", () => ({
|
|
6
|
-
getLogger: () =>
|
|
7
|
-
new Proxy({} as Record<string, unknown>, {
|
|
8
|
-
get: () => () => {},
|
|
9
|
-
}),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
import { maybeEnqueueConversationStartersJob } from "../memory/conversation-starters-cadence.js";
|
|
13
|
-
import { getSqlite } from "../memory/db-connection.js";
|
|
14
|
-
import { initializeDb } from "../memory/db-init.js";
|
|
15
|
-
initializeDb();
|
|
16
|
-
|
|
17
|
-
function clearTables() {
|
|
18
|
-
getSqlite().run("DELETE FROM memory_graph_nodes");
|
|
19
|
-
getSqlite().run("DELETE FROM memory_jobs");
|
|
20
|
-
getSqlite().run("DELETE FROM memory_checkpoints");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function insertMemoryNode(scopeId = "default") {
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
getSqlite().run(
|
|
26
|
-
`INSERT INTO memory_graph_nodes (
|
|
27
|
-
id, content, type, created, last_accessed, last_consolidated,
|
|
28
|
-
emotional_charge, fidelity, confidence, significance,
|
|
29
|
-
stability, reinforcement_count, last_reinforced,
|
|
30
|
-
source_conversations, source_type, scope_id
|
|
31
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'vivid', 0.8, 0.5, 14, 0, ?, '[]', 'inferred', ?)`,
|
|
32
|
-
[
|
|
33
|
-
uuid(),
|
|
34
|
-
"test statement",
|
|
35
|
-
"semantic",
|
|
36
|
-
now,
|
|
37
|
-
now,
|
|
38
|
-
now,
|
|
39
|
-
'{"valence":0,"intensity":0.1,"decayCurve":"linear","decayRate":0.05,"originalIntensity":0.1}',
|
|
40
|
-
now,
|
|
41
|
-
scopeId,
|
|
42
|
-
],
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function setCheckpoint(key: string, value: string) {
|
|
47
|
-
getSqlite().run(
|
|
48
|
-
`INSERT OR REPLACE INTO memory_checkpoints (key, value, updated_at) VALUES (?, ?, ?)`,
|
|
49
|
-
[key, value, Date.now()],
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getPendingJobs(): Array<{ type: string }> {
|
|
54
|
-
return getSqlite()
|
|
55
|
-
.prepare(
|
|
56
|
-
`SELECT type FROM memory_jobs WHERE type = 'generate_conversation_starters' AND status = 'pending'`,
|
|
57
|
-
)
|
|
58
|
-
.all() as Array<{ type: string }>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
beforeEach(() => {
|
|
62
|
-
clearTables();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("maybeEnqueueConversationStartersJob", () => {
|
|
66
|
-
test("no-op when zero memory nodes", () => {
|
|
67
|
-
maybeEnqueueConversationStartersJob("default");
|
|
68
|
-
expect(getPendingJobs()).toHaveLength(0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("enqueues when threshold exceeded (<=10 nodes, threshold=1)", () => {
|
|
72
|
-
insertMemoryNode();
|
|
73
|
-
insertMemoryNode();
|
|
74
|
-
|
|
75
|
-
maybeEnqueueConversationStartersJob("default");
|
|
76
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("no-op when delta below threshold", () => {
|
|
80
|
-
for (let i = 0; i < 5; i++) insertMemoryNode();
|
|
81
|
-
|
|
82
|
-
// Set checkpoint to current count — no new items since last gen
|
|
83
|
-
setCheckpoint("conversation_starters:item_count_at_last_gen:default", "5");
|
|
84
|
-
|
|
85
|
-
maybeEnqueueConversationStartersJob("default");
|
|
86
|
-
expect(getPendingJobs()).toHaveLength(0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("uses higher threshold for larger memory counts (>50 nodes, threshold=10)", () => {
|
|
90
|
-
for (let i = 0; i < 55; i++) insertMemoryNode();
|
|
91
|
-
|
|
92
|
-
// Set checkpoint so delta is only 4 (below threshold of 10)
|
|
93
|
-
setCheckpoint(
|
|
94
|
-
"conversation_starters:item_count_at_last_gen:default",
|
|
95
|
-
"51",
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
maybeEnqueueConversationStartersJob("default");
|
|
99
|
-
expect(getPendingJobs()).toHaveLength(0);
|
|
100
|
-
|
|
101
|
-
// Add more to exceed threshold
|
|
102
|
-
for (let i = 0; i < 6; i++) insertMemoryNode();
|
|
103
|
-
|
|
104
|
-
maybeEnqueueConversationStartersJob("default");
|
|
105
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("dedup prevents double-enqueue", () => {
|
|
109
|
-
insertMemoryNode();
|
|
110
|
-
insertMemoryNode();
|
|
111
|
-
|
|
112
|
-
maybeEnqueueConversationStartersJob("default");
|
|
113
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
114
|
-
|
|
115
|
-
// Call again — should not create a second job
|
|
116
|
-
maybeEnqueueConversationStartersJob("default");
|
|
117
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("enqueues when active memory count drops below the last-generation checkpoint", () => {
|
|
121
|
-
// Start with 5 nodes and set checkpoint to 5
|
|
122
|
-
for (let i = 0; i < 5; i++) insertMemoryNode();
|
|
123
|
-
setCheckpoint("conversation_starters:item_count_at_last_gen:default", "5");
|
|
124
|
-
|
|
125
|
-
// Simulate pruning: mark 3 nodes as gone (reducing totalActive to 2)
|
|
126
|
-
const ids = (
|
|
127
|
-
getSqlite()
|
|
128
|
-
.prepare(`SELECT id FROM memory_graph_nodes LIMIT 3`)
|
|
129
|
-
.all() as Array<{ id: string }>
|
|
130
|
-
).map((r) => r.id);
|
|
131
|
-
for (const id of ids) {
|
|
132
|
-
getSqlite().run(
|
|
133
|
-
`UPDATE memory_graph_nodes SET fidelity = 'gone' WHERE id = ?`,
|
|
134
|
-
[id],
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// The checkpoint is now ahead of the active memory count. This should
|
|
139
|
-
// enqueue a refresh immediately so stale starters can recover.
|
|
140
|
-
maybeEnqueueConversationStartersJob("default");
|
|
141
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("scopes are independent", () => {
|
|
145
|
-
insertMemoryNode("scope-a");
|
|
146
|
-
insertMemoryNode("scope-b");
|
|
147
|
-
|
|
148
|
-
maybeEnqueueConversationStartersJob("scope-a");
|
|
149
|
-
maybeEnqueueConversationStartersJob("scope-b");
|
|
150
|
-
|
|
151
|
-
const jobs = getSqlite()
|
|
152
|
-
.prepare(
|
|
153
|
-
`SELECT payload FROM memory_jobs WHERE type = 'generate_conversation_starters' AND status = 'pending'`,
|
|
154
|
-
)
|
|
155
|
-
.all() as Array<{ payload: string }>;
|
|
156
|
-
|
|
157
|
-
expect(jobs).toHaveLength(2);
|
|
158
|
-
const payloads = jobs.map((j) => JSON.parse(j.payload).scopeId).sort();
|
|
159
|
-
expect(payloads).toEqual(["scope-a", "scope-b"]);
|
|
160
|
-
});
|
|
161
|
-
});
|