@vellumai/assistant 0.8.9-staging.3 → 0.8.9-staging.4
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/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/context-window-manager-compact-retry.test.ts +120 -14
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/daemon/conversation-agent-loop.ts +9 -17
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/plugins/defaults/compaction/window-manager.ts +56 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/chat-completions-provider.ts +19 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/services/published-app-updater.ts +30 -8
- package/src/tools/skills/load.ts +1 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
import { getConfig } from "../config/loader.js";
|
|
34
34
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
35
35
|
import type { ContextWindowConfig } from "../config/types.js";
|
|
36
|
-
import { runEmergencyCompaction } from "../context/compactor.js";
|
|
37
36
|
import {
|
|
38
37
|
derefToolResultReReads,
|
|
39
38
|
postTurnTruncateToolResults,
|
|
@@ -1221,22 +1220,15 @@ export async function runAgentLoopImpl(
|
|
|
1221
1220
|
// compressing history. Falls through to reducer tiers on failure.
|
|
1222
1221
|
{
|
|
1223
1222
|
try {
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
previousEstimatedInputTokens: estimatedTokensAtOverflow,
|
|
1234
|
-
force: true,
|
|
1235
|
-
signal: abortController.signal,
|
|
1236
|
-
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
1237
|
-
nonPersistedPrefixCount:
|
|
1238
|
-
ctx.contextWindowManager.nonPersistedPrefixCount,
|
|
1239
|
-
});
|
|
1223
|
+
const emergencyResult =
|
|
1224
|
+
await ctx.contextWindowManager.emergencyCompact(
|
|
1225
|
+
ctx.messages,
|
|
1226
|
+
{
|
|
1227
|
+
previousEstimatedInputTokens: estimatedTokensAtOverflow,
|
|
1228
|
+
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
1229
|
+
},
|
|
1230
|
+
abortController.signal,
|
|
1231
|
+
);
|
|
1240
1232
|
if (emergencyResult.compacted) {
|
|
1241
1233
|
rlog.info(
|
|
1242
1234
|
{
|
|
@@ -65,6 +65,7 @@ import { getConfig } from "../config/loader.js";
|
|
|
65
65
|
import type { AssistantConfig } from "../config/schema.js";
|
|
66
66
|
import { HOOKS } from "../plugin-api/constants.js";
|
|
67
67
|
import { registerDefaultPlugins } from "../plugins/defaults/index.js";
|
|
68
|
+
import { installPluginRuntime } from "../plugins/external-api.js";
|
|
68
69
|
import { buildExternalPlugin } from "../plugins/external-plugin-loader.js";
|
|
69
70
|
import {
|
|
70
71
|
registerPluginSkills,
|
|
@@ -186,17 +187,21 @@ function getDisabledPluginFlag(
|
|
|
186
187
|
* Bring the plugin layer up during daemon startup. Runs the full sequence in
|
|
187
188
|
* the one order the rest of the system depends on:
|
|
188
189
|
*
|
|
189
|
-
* 1.
|
|
190
|
+
* 1. Install the `globalThis.__vellumPluginRuntime` bridge so plugins can touch
|
|
191
|
+
* it from their module body (see `plugins/external-api.ts` — compiled-binary
|
|
192
|
+
* module identity).
|
|
193
|
+
* 2. Register the first-party defaults so their middleware composes innermost,
|
|
190
194
|
* ahead of any user plugins.
|
|
191
|
-
*
|
|
195
|
+
* 3. Load user plugins from `<workspaceDir>/plugins/*`. A failing user plugin is
|
|
192
196
|
* logged and skipped; `loadUserPlugins()` closes the registration window
|
|
193
197
|
* when it returns, so the defaults must already be registered by then.
|
|
194
|
-
*
|
|
198
|
+
* 4. Run every registered plugin's `init()` via {@link bootstrapPlugins}.
|
|
195
199
|
*
|
|
196
200
|
* Plugin bootstrap is wrapped so a failing plugin cannot block daemon startup —
|
|
197
201
|
* the daemon comes up with degraded plugin functionality instead.
|
|
198
202
|
*/
|
|
199
203
|
export async function initializePlugins(): Promise<void> {
|
|
204
|
+
installPluginRuntime();
|
|
200
205
|
registerDefaultPlugins();
|
|
201
206
|
await loadUserPlugins();
|
|
202
207
|
try {
|
|
@@ -155,6 +155,21 @@ registerHook("app_refresh", (_name, input, _result, { ctx }) => {
|
|
|
155
155
|
notifyAppChanged(ctx, appId, { fileChange: true });
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
// app_update compiles internally (like app_refresh) but emits no events of its
|
|
159
|
+
// own, so without this hook an updated app leaves open surfaces rendering the
|
|
160
|
+
// stale dist and never re-deploys or invalidates the Library. The executor owns
|
|
161
|
+
// the compile; notifyAppChanged only refreshes surfaces and broadcasts.
|
|
162
|
+
registerHook("app_update", (_name, input, _result, { ctx }) => {
|
|
163
|
+
const appId = input.app_id as string | undefined;
|
|
164
|
+
if (!appId) return;
|
|
165
|
+
try {
|
|
166
|
+
addAppConversationId(appId, ctx.conversationId);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
log.warn({ err, appId }, "Failed to track conversation ID on app_update");
|
|
169
|
+
}
|
|
170
|
+
notifyAppChanged(ctx, appId, { fileChange: true });
|
|
171
|
+
});
|
|
172
|
+
|
|
158
173
|
registerHook("voice_config_update", (_name, input) => {
|
|
159
174
|
const setting = input.setting as string | undefined;
|
|
160
175
|
if (!setting) return;
|
|
@@ -105,43 +105,15 @@ mock.module("../../jobs-store.js", () => ({
|
|
|
105
105
|
isMemoryEnabled: () => true,
|
|
106
106
|
}));
|
|
107
107
|
|
|
108
|
-
// ── v3
|
|
108
|
+
// ── v3 follow-up flag mock ──────────────────────────────────────────
|
|
109
109
|
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// mocks let each test (a) toggle the flag, (b) decide what health block the
|
|
113
|
-
// renderer produces, and (c) force the health computation to throw — without
|
|
114
|
-
// materializing a real v3 data dir. The tree/core/page-index loaders are
|
|
115
|
-
// stubbed to inert values; `computeV3Health` is a pass-through whose result is
|
|
116
|
-
// fed to the toggleable `renderV3Health`.
|
|
110
|
+
// A v3 flag being on appends `memory_v3_maintain` to the post-consolidation
|
|
111
|
+
// follow-up fan-out. This mock lets each test toggle the flag.
|
|
117
112
|
let v3FlagOn = false;
|
|
118
113
|
mock.module("../../../config/assistant-feature-flags.js", () => ({
|
|
119
114
|
isAssistantFeatureFlagEnabled: () => v3FlagOn,
|
|
120
115
|
}));
|
|
121
116
|
|
|
122
|
-
let renderedHealth = "";
|
|
123
|
-
let computeThrows = false;
|
|
124
|
-
mock.module("../../../plugins/defaults/memory-v3-shadow/health.js", () => ({
|
|
125
|
-
computeV3Health: () => {
|
|
126
|
-
if (computeThrows) throw new Error("simulated health compute failure");
|
|
127
|
-
return {};
|
|
128
|
-
},
|
|
129
|
-
renderV3Health: () => renderedHealth,
|
|
130
|
-
}));
|
|
131
|
-
|
|
132
|
-
mock.module("../../../plugins/defaults/memory-v3-shadow/tree.js", () => ({
|
|
133
|
-
loadLeafTree: async () => ({ leaves: new Map(), byPage: new Map() }),
|
|
134
|
-
resolveDataDir: () => "/tmp/v3-data-stub",
|
|
135
|
-
}));
|
|
136
|
-
|
|
137
|
-
mock.module("../../../plugins/defaults/memory-v3-shadow/core.js", () => ({
|
|
138
|
-
loadCore: async () => new Set<string>(),
|
|
139
|
-
}));
|
|
140
|
-
|
|
141
|
-
mock.module("../../v2/page-index.js", () => ({
|
|
142
|
-
getPageIndex: async () => ({ entries: [] }),
|
|
143
|
-
}));
|
|
144
|
-
|
|
145
117
|
// ── Workspace pin ───────────────────────────────────────────────────
|
|
146
118
|
let tmpWorkspace: string;
|
|
147
119
|
let previousWorkspaceEnv: string | undefined;
|
|
@@ -211,10 +183,8 @@ beforeEach(() => {
|
|
|
211
183
|
enqueuedJobs.length = 0;
|
|
212
184
|
nextJobIdCounter = 0;
|
|
213
185
|
|
|
214
|
-
// v3
|
|
186
|
+
// v3 follow-up flag default: off.
|
|
215
187
|
v3FlagOn = false;
|
|
216
|
-
renderedHealth = "";
|
|
217
|
-
computeThrows = false;
|
|
218
188
|
});
|
|
219
189
|
|
|
220
190
|
// ---------------------------------------------------------------------------
|
|
@@ -409,69 +379,6 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
409
379
|
});
|
|
410
380
|
});
|
|
411
381
|
|
|
412
|
-
describe("memoryV2ConsolidateJob — v3 health injection", () => {
|
|
413
|
-
beforeEach(() => {
|
|
414
|
-
writeFileSync(
|
|
415
|
-
bufferPath(),
|
|
416
|
-
"- [Apr 27, 9:00 AM] Alice prefers VS Code over Vim.\n",
|
|
417
|
-
);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
test("prepends the health block when a v3 flag is on and the report is actionable", async () => {
|
|
421
|
-
v3FlagOn = true;
|
|
422
|
-
renderedHealth = "memory-v3 health:\n- 2 unassigned slug(s): a, b";
|
|
423
|
-
|
|
424
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
425
|
-
|
|
426
|
-
expect(result.kind).toBe("invoked");
|
|
427
|
-
const prompt = runnerLastArgs?.prompt as string;
|
|
428
|
-
// The health block is PREPENDED, separated by a blank line, with the
|
|
429
|
-
// base consolidation prompt still present underneath it.
|
|
430
|
-
expect(prompt.startsWith(`${renderedHealth}\n\n`)).toBe(true);
|
|
431
|
-
expect(prompt).toContain("memory consolidation");
|
|
432
|
-
expect(prompt).not.toContain(CUTOFF_PLACEHOLDER);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
test("leaves the prompt unchanged when the report is all-green (empty render)", async () => {
|
|
436
|
-
v3FlagOn = true;
|
|
437
|
-
renderedHealth = "";
|
|
438
|
-
|
|
439
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
440
|
-
|
|
441
|
-
expect(result.kind).toBe("invoked");
|
|
442
|
-
const prompt = runnerLastArgs?.prompt as string;
|
|
443
|
-
expect(prompt).not.toContain("memory-v3 health:");
|
|
444
|
-
// Prompt is exactly the resolved consolidation body.
|
|
445
|
-
expect(prompt).toContain("memory consolidation");
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
test("leaves the prompt unchanged when v3 flags are off even if a block would render", async () => {
|
|
449
|
-
v3FlagOn = false;
|
|
450
|
-
renderedHealth = "memory-v3 health:\n- 1 unassigned slug(s): a";
|
|
451
|
-
|
|
452
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
453
|
-
|
|
454
|
-
expect(result.kind).toBe("invoked");
|
|
455
|
-
const prompt = runnerLastArgs?.prompt as string;
|
|
456
|
-
expect(prompt).not.toContain("memory-v3 health:");
|
|
457
|
-
expect(prompt).toContain("memory consolidation");
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
test("falls back to the base prompt when health computation throws", async () => {
|
|
461
|
-
v3FlagOn = true;
|
|
462
|
-
computeThrows = true;
|
|
463
|
-
renderedHealth = "memory-v3 health:\n- should not appear";
|
|
464
|
-
|
|
465
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
466
|
-
|
|
467
|
-
// A health-compute failure must NEVER break consolidation.
|
|
468
|
-
expect(result.kind).toBe("invoked");
|
|
469
|
-
const prompt = runnerLastArgs?.prompt as string;
|
|
470
|
-
expect(prompt).not.toContain("memory-v3 health:");
|
|
471
|
-
expect(prompt).toContain("memory consolidation");
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
|
|
475
382
|
describe("memoryV2ConsolidateJob — concurrent invocations", () => {
|
|
476
383
|
beforeEach(() => {
|
|
477
384
|
writeFileSync(bufferPath(), "- [Apr 27, 9:00 AM] Alice prefers VS Code.\n");
|
|
@@ -186,6 +186,28 @@ describe("writePage + readPage round-trip", () => {
|
|
|
186
186
|
expect(read!.body).toBe(page.body);
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
test("round-trips authored links: frontmatter (memory-v3 edge lane)", async () => {
|
|
190
|
+
const page = makePage({
|
|
191
|
+
frontmatter: {
|
|
192
|
+
edges: [],
|
|
193
|
+
ref_files: [],
|
|
194
|
+
ref_urls: [],
|
|
195
|
+
links: ["bob-handoff — the partner this hands off to"],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
await writePage(workspaceDir, page);
|
|
199
|
+
|
|
200
|
+
// A page using the optional `links:` frontmatter must NOT be rejected by the
|
|
201
|
+
// strict schema (it would otherwise be dropped from memory-v3 entirely), and
|
|
202
|
+
// the curated links must survive the render → read round-trip so the edge
|
|
203
|
+
// graph can parse them back from the rendered frontmatter.
|
|
204
|
+
const read = await readPage(workspaceDir, page.slug);
|
|
205
|
+
expect(read).not.toBeNull();
|
|
206
|
+
expect(read!.frontmatter.links).toEqual([
|
|
207
|
+
"bob-handoff — the partner this hands off to",
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
189
211
|
test("renders frontmatter at the top with --- delimiters", async () => {
|
|
190
212
|
const page = makePage();
|
|
191
213
|
await writePage(workspaceDir, page);
|
|
@@ -63,19 +63,6 @@ import { dirname, join } from "node:path";
|
|
|
63
63
|
|
|
64
64
|
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
65
65
|
import type { AssistantConfig } from "../../config/types.js";
|
|
66
|
-
import { loadCore } from "../../plugins/defaults/memory-v3-shadow/core.js";
|
|
67
|
-
import {
|
|
68
|
-
computeV3Health,
|
|
69
|
-
renderV3Health,
|
|
70
|
-
} from "../../plugins/defaults/memory-v3-shadow/health.js";
|
|
71
|
-
import {
|
|
72
|
-
loadLeafTree,
|
|
73
|
-
resolveDataDir,
|
|
74
|
-
} from "../../plugins/defaults/memory-v3-shadow/tree.js";
|
|
75
|
-
import type {
|
|
76
|
-
LeafPath,
|
|
77
|
-
Slug,
|
|
78
|
-
} from "../../plugins/defaults/memory-v3-shadow/types.js";
|
|
79
66
|
import { runBackgroundJob } from "../../runtime/background-job-runner.js";
|
|
80
67
|
import { getLogger } from "../../util/logger.js";
|
|
81
68
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
@@ -86,7 +73,6 @@ import {
|
|
|
86
73
|
type MemoryJob,
|
|
87
74
|
type MemoryJobType,
|
|
88
75
|
} from "../jobs-store.js";
|
|
89
|
-
import { getPageIndex } from "../v2/page-index.js";
|
|
90
76
|
import { MEMORY_V2_CONSOLIDATION_SOURCE } from "./constants.js";
|
|
91
77
|
import { resolveConsolidationPrompt } from "./prompts/consolidation.js";
|
|
92
78
|
|
|
@@ -96,8 +82,7 @@ const log = getLogger("memory-v2-consolidate");
|
|
|
96
82
|
const JOB_NAME = "memory.consolidate";
|
|
97
83
|
|
|
98
84
|
/**
|
|
99
|
-
* v3 plugin flags. Either being on
|
|
100
|
-
* consolidation prompt and (b) enqueues `memory_v3_maintain` as a
|
|
85
|
+
* v3 plugin flags. Either being on enqueues `memory_v3_maintain` as a
|
|
101
86
|
* post-consolidation follow-up. These gate the v3 plugin itself.
|
|
102
87
|
*/
|
|
103
88
|
const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
|
|
@@ -191,15 +176,10 @@ export async function memoryV2ConsolidateJob(
|
|
|
191
176
|
// it to a regular file under 1 MiB before substitution so a stray path
|
|
192
177
|
// (or a `/dev/zero`-style pseudo-file) cannot exfiltrate megabytes of
|
|
193
178
|
// bytes through the wake hint.
|
|
194
|
-
|
|
195
|
-
// That resolved prompt is the base; `maybePrependV3Health` prepends a
|
|
196
|
-
// freshly computed v3 health block when a v3 flag is on and the tree has
|
|
197
|
-
// actionable drift, leaving it untouched otherwise.
|
|
198
|
-
const basePrompt = resolveConsolidationPrompt(
|
|
179
|
+
const prompt = resolveConsolidationPrompt(
|
|
199
180
|
config.memory.v2.consolidation_prompt_path,
|
|
200
181
|
cutoff,
|
|
201
182
|
);
|
|
202
|
-
const prompt = await maybePrependV3Health(basePrompt, config);
|
|
203
183
|
|
|
204
184
|
const runResult = await runBackgroundJob({
|
|
205
185
|
jobName: JOB_NAME,
|
|
@@ -270,56 +250,6 @@ export async function memoryV2ConsolidateJob(
|
|
|
270
250
|
}
|
|
271
251
|
}
|
|
272
252
|
|
|
273
|
-
/**
|
|
274
|
-
* When a v3 flag is enabled and the v3 tree has actionable structural drift,
|
|
275
|
-
* prepend a rendered health block to `basePrompt` so the consolidation run can
|
|
276
|
-
* fold tree maintenance into its pass. Returns `basePrompt` UNCHANGED when:
|
|
277
|
-
* - neither `memory-v3-shadow` nor `memory-v3-live` is enabled,
|
|
278
|
-
* - the health report is all-green (`renderV3Health` returns ""), or
|
|
279
|
-
* - computing the report throws for any reason.
|
|
280
|
-
*
|
|
281
|
-
* The block is computed here (not baked into the prompt template) so an
|
|
282
|
-
* operator's custom consolidation prompt stays untouched and the block reflects
|
|
283
|
-
* the tree state at run time. Health computation is wrapped so a load/compute
|
|
284
|
-
* failure can never break consolidation — the run proceeds on the base prompt.
|
|
285
|
-
*/
|
|
286
|
-
async function maybePrependV3Health(
|
|
287
|
-
basePrompt: string,
|
|
288
|
-
config: AssistantConfig,
|
|
289
|
-
): Promise<string> {
|
|
290
|
-
const v3Enabled =
|
|
291
|
-
isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config) ||
|
|
292
|
-
isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config);
|
|
293
|
-
if (!v3Enabled) return basePrompt;
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
const pageIndex = await getPageIndex(getWorkspaceDir());
|
|
297
|
-
const pageLeaves = new Map<Slug, LeafPath[]>();
|
|
298
|
-
const allSlugs: Slug[] = [];
|
|
299
|
-
for (const entry of pageIndex.entries) {
|
|
300
|
-
pageLeaves.set(entry.slug, entry.leaves);
|
|
301
|
-
allSlugs.push(entry.slug);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const dataDir = resolveDataDir();
|
|
305
|
-
const [tree, core] = await Promise.all([
|
|
306
|
-
loadLeafTree(dataDir, pageLeaves),
|
|
307
|
-
loadCore(dataDir),
|
|
308
|
-
]);
|
|
309
|
-
|
|
310
|
-
const report = computeV3Health({ tree, allSlugs, core });
|
|
311
|
-
const healthBlock = renderV3Health(report);
|
|
312
|
-
if (healthBlock.length === 0) return basePrompt;
|
|
313
|
-
return `${healthBlock}\n\n${basePrompt}`;
|
|
314
|
-
} catch (err) {
|
|
315
|
-
log.warn(
|
|
316
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
317
|
-
"consolidation: v3 health computation failed; using base prompt",
|
|
318
|
-
);
|
|
319
|
-
return basePrompt;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
253
|
/**
|
|
324
254
|
* Read `memory/buffer.md`. Missing file → empty string so the skip-on-empty
|
|
325
255
|
* branch doesn't have to distinguish "no file" from "blank file".
|
package/src/memory/v2/types.ts
CHANGED
|
@@ -41,6 +41,11 @@ export const ConceptPageFrontmatterSchema = z
|
|
|
41
41
|
ref_urls: z.array(z.string().url()).default([]),
|
|
42
42
|
summary: z.string().optional(),
|
|
43
43
|
leaves: z.array(z.string()).optional(),
|
|
44
|
+
// Optional authored `"<target-slug> — <why>"` cross-links. Curated, first-class
|
|
45
|
+
// edges for the memory-v3 edge lane. Declared here so `.strict()` does not
|
|
46
|
+
// reject a page that uses `links:`, and so `renderPageContent` round-trips the
|
|
47
|
+
// field (the edge graph reads it back from the rendered frontmatter).
|
|
48
|
+
links: z.array(z.string()).optional(),
|
|
44
49
|
})
|
|
45
50
|
.strict();
|
|
46
51
|
|
|
@@ -20,7 +20,9 @@ import type { LLMCallSite } from "../../../config/schemas/llm.js";
|
|
|
20
20
|
import type { ContextWindowConfig } from "../../../config/types.js";
|
|
21
21
|
import {
|
|
22
22
|
type CompactionRunArgs,
|
|
23
|
+
type CompactionRunResult,
|
|
23
24
|
runAssistantDrivenCompaction,
|
|
25
|
+
runEmergencyCompaction,
|
|
24
26
|
} from "../../../context/compactor.js";
|
|
25
27
|
import { estimatePromptTokens } from "../../../context/token-estimator.js";
|
|
26
28
|
import type {
|
|
@@ -118,6 +120,21 @@ export interface ContextWindowCompactOptions {
|
|
|
118
120
|
actorTrustClass?: TrustClass;
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
export interface EmergencyCompactOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The provider-reported estimate at the overflow that triggered recovery.
|
|
126
|
+
* Sizing the emergency summary needs the actual rejected token count, not
|
|
127
|
+
* the manager's pre-send estimate (which under-counted, hence the
|
|
128
|
+
* rejection).
|
|
129
|
+
*/
|
|
130
|
+
previousEstimatedInputTokens: number;
|
|
131
|
+
/**
|
|
132
|
+
* Per-conversation inference-profile override forwarded to the summary
|
|
133
|
+
* call.
|
|
134
|
+
*/
|
|
135
|
+
overrideProfile?: string | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
export interface ContextWindowManagerOptions {
|
|
122
139
|
provider: Provider;
|
|
123
140
|
systemPrompt: string | (() => string);
|
|
@@ -428,6 +445,45 @@ export class ContextWindowManager {
|
|
|
428
445
|
return { ...result, estimatedInputTokens, exhausted: true };
|
|
429
446
|
}
|
|
430
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Emergency mid-turn compaction — summarize everything before the last
|
|
450
|
+
* tool_use/tool_result pair and let the agent continue with
|
|
451
|
+
* `[summary, last_tool_call, last_tool_result]`. Used as a recovery rung
|
|
452
|
+
* after the provider rejects a turn for context overflow. The manager
|
|
453
|
+
* supplies the provider, system prompt, token budget, conversation id, and
|
|
454
|
+
* non-persisted prefix count it already owns; the caller provides only the
|
|
455
|
+
* overflow-specific inputs.
|
|
456
|
+
*/
|
|
457
|
+
async emergencyCompact(
|
|
458
|
+
messages: Message[],
|
|
459
|
+
options: EmergencyCompactOptions,
|
|
460
|
+
signal?: AbortSignal,
|
|
461
|
+
): Promise<CompactionRunResult> {
|
|
462
|
+
if (this.conversationId == null) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
"ContextWindowManager has no conversationId — cannot run emergency compaction",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
return await runEmergencyCompaction({
|
|
469
|
+
conversationId: this.conversationId,
|
|
470
|
+
messages,
|
|
471
|
+
provider: this.provider,
|
|
472
|
+
systemPrompt: this.systemPrompt,
|
|
473
|
+
tools: undefined,
|
|
474
|
+
compaction: this.resolveCompactionConfig(),
|
|
475
|
+
maxInputTokens: this.config.maxInputTokens,
|
|
476
|
+
previousEstimatedInputTokens: options.previousEstimatedInputTokens,
|
|
477
|
+
force: true,
|
|
478
|
+
signal,
|
|
479
|
+
overrideProfile: options.overrideProfile ?? null,
|
|
480
|
+
nonPersistedPrefixCount: this._nonPersistedPrefixCount,
|
|
481
|
+
});
|
|
482
|
+
} finally {
|
|
483
|
+
this.clearSystemPromptCache();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
431
487
|
/**
|
|
432
488
|
* Recompute the prompt-token estimate against the post-compaction
|
|
433
489
|
* message array. The compactor returns a conservative placeholder; the
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
capabilityOrDiskBody,
|
|
5
5
|
type CapabilityResolvers,
|
|
6
|
-
injectCapabilitiesLeaf,
|
|
7
6
|
isCapabilitySlug,
|
|
8
7
|
renderCapabilityContent,
|
|
9
8
|
} from "../capabilities.js";
|
|
10
|
-
import { buildNeedleIndex } from "../needle.js";
|
|
11
|
-
import type { LeafPath, LeafTree, Slug } from "../types.js";
|
|
12
|
-
|
|
13
|
-
function emptyTree(): LeafTree {
|
|
14
|
-
return { leaves: new Map(), byPage: new Map<Slug, LeafPath[]>() };
|
|
15
|
-
}
|
|
16
9
|
|
|
17
10
|
describe("isCapabilitySlug", () => {
|
|
18
11
|
test("true for skill and CLI-command slugs, false otherwise", () => {
|
|
@@ -23,52 +16,6 @@ describe("isCapabilitySlug", () => {
|
|
|
23
16
|
});
|
|
24
17
|
});
|
|
25
18
|
|
|
26
|
-
describe("injectCapabilitiesLeaf", () => {
|
|
27
|
-
test("registers an always-on leaf with the synthetic slugs as members", () => {
|
|
28
|
-
const tree = emptyTree();
|
|
29
|
-
const core = new Set<LeafPath>();
|
|
30
|
-
injectCapabilitiesLeaf(tree, core, ["skills/foo", "cli-commands/bar"]);
|
|
31
|
-
|
|
32
|
-
const leaf = tree.leaves.get(CAPABILITIES_LEAF_PATH);
|
|
33
|
-
expect(leaf?.frontmatter.in_core).toBe(true);
|
|
34
|
-
expect(leaf?.members).toEqual(["skills/foo", "cli-commands/bar"]);
|
|
35
|
-
expect(core.has(CAPABILITIES_LEAF_PATH)).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("unions the leaf into each member's byPage entry without dropping existing leaves", () => {
|
|
39
|
-
const tree = emptyTree();
|
|
40
|
-
tree.byPage.set("skills/foo", ["other/leaf"]);
|
|
41
|
-
injectCapabilitiesLeaf(tree, new Set<LeafPath>(), [
|
|
42
|
-
"skills/foo",
|
|
43
|
-
"cli-commands/bar",
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
expect(tree.byPage.get("skills/foo")).toEqual([
|
|
47
|
-
"other/leaf",
|
|
48
|
-
CAPABILITIES_LEAF_PATH,
|
|
49
|
-
]);
|
|
50
|
-
expect(tree.byPage.get("cli-commands/bar")).toEqual([
|
|
51
|
-
CAPABILITIES_LEAF_PATH,
|
|
52
|
-
]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("is idempotent — re-injecting does not duplicate the byPage entry", () => {
|
|
56
|
-
const tree = emptyTree();
|
|
57
|
-
const core = new Set<LeafPath>();
|
|
58
|
-
injectCapabilitiesLeaf(tree, core, ["skills/foo"]);
|
|
59
|
-
injectCapabilitiesLeaf(tree, core, ["skills/foo"]);
|
|
60
|
-
expect(tree.byPage.get("skills/foo")).toEqual([CAPABILITIES_LEAF_PATH]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("stays always-on even with no installed skills/commands", () => {
|
|
64
|
-
const tree = emptyTree();
|
|
65
|
-
const core = new Set<LeafPath>();
|
|
66
|
-
injectCapabilitiesLeaf(tree, core, []);
|
|
67
|
-
expect(tree.leaves.get(CAPABILITIES_LEAF_PATH)?.members).toEqual([]);
|
|
68
|
-
expect(core.has(CAPABILITIES_LEAF_PATH)).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
19
|
describe("renderCapabilityContent", () => {
|
|
73
20
|
const resolvers: CapabilityResolvers = {
|
|
74
21
|
skill: (slug) =>
|
|
@@ -100,19 +47,25 @@ describe("renderCapabilityContent", () => {
|
|
|
100
47
|
});
|
|
101
48
|
});
|
|
102
49
|
|
|
103
|
-
describe("
|
|
104
|
-
test("a
|
|
105
|
-
|
|
106
|
-
|
|
50
|
+
describe("capabilityOrDiskBody", () => {
|
|
51
|
+
test("a capability slug routes through renderCapabilityContent and never reads disk", async () => {
|
|
52
|
+
let diskReads = 0;
|
|
53
|
+
const readDiskBody = async (): Promise<string> => {
|
|
54
|
+
diskReads += 1;
|
|
55
|
+
return "disk body — should not be read";
|
|
56
|
+
};
|
|
57
|
+
// A capability slug whose production cache has no entry degrades to "" (the
|
|
58
|
+
// `renderCapabilityContent` contract) rather than falling through to the
|
|
59
|
+
// injected disk reader, which must stay untouched.
|
|
60
|
+
expect(await capabilityOrDiskBody("skills/example", readDiskBody)).toBe("");
|
|
61
|
+
expect(diskReads).toBe(0);
|
|
62
|
+
});
|
|
107
63
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
64
|
+
test("a non-capability slug returns the injected disk reader's result", async () => {
|
|
65
|
+
const readDiskBody = async (slug: string): Promise<string> =>
|
|
66
|
+
`disk body for ${slug}`;
|
|
67
|
+
expect(await capabilityOrDiskBody("relationship/vows", readDiskBody)).toBe(
|
|
68
|
+
"disk body for relationship/vows",
|
|
111
69
|
);
|
|
112
|
-
|
|
113
|
-
// By slug-derived title token.
|
|
114
|
-
expect(needle.query("meet", 5)).toContain("skills/meet-join");
|
|
115
|
-
// By summary token.
|
|
116
|
-
expect(needle.query("transcribe", 5)).toContain("skills/meet-join");
|
|
117
70
|
});
|
|
118
71
|
});
|