@vellumai/assistant 0.8.11 → 0.8.12-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +633 -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-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- 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__/secret-fixtures.ts +20 -0
- 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-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- 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-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- 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/bundler/app-compiler.ts +8 -0
- 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 +307 -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 +225 -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/config/seed-inference-profiles.ts +4 -8
- 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/events/tool-audit-listener.ts +40 -9
- 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/__tests__/unparseable-tool-args.test.ts +53 -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-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -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__/conversation-query-routes.test.ts +132 -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 +348 -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/conversation-query-routes.ts +79 -4
- 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/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -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/security/redact-json.ts +61 -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/tools/tool-approval-handler.ts +31 -0
- 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/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -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
|
@@ -141,14 +141,32 @@ const { CUTOFF_PLACEHOLDER, CONSOLIDATION_PROMPT } =
|
|
|
141
141
|
await import("../prompts/consolidation.js");
|
|
142
142
|
|
|
143
143
|
// The handler only reads `config.memory.enabled`, `config.memory.v2.enabled`,
|
|
144
|
-
//
|
|
145
|
-
//
|
|
144
|
+
// `config.memory.v2.consolidation_prompt_path`, and
|
|
145
|
+
// `config.memory.v2.consolidation_max_entries_per_run`, so a minimal
|
|
146
|
+
// stand-in covers those call sites without materializing the full default
|
|
147
|
+
// config.
|
|
146
148
|
const CONFIG = {
|
|
147
149
|
memory: {
|
|
148
150
|
enabled: true,
|
|
149
151
|
v2: { enabled: true, consolidation_prompt_path: null },
|
|
150
152
|
},
|
|
151
153
|
} as Parameters<typeof memoryV2ConsolidateJob>[1];
|
|
154
|
+
|
|
155
|
+
/** CONFIG plus a per-run entry cap for the chunked-cutoff tests. */
|
|
156
|
+
function configWithMaxEntries(
|
|
157
|
+
maxEntries: number | null,
|
|
158
|
+
): Parameters<typeof memoryV2ConsolidateJob>[1] {
|
|
159
|
+
return {
|
|
160
|
+
memory: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
v2: {
|
|
163
|
+
enabled: true,
|
|
164
|
+
consolidation_prompt_path: null,
|
|
165
|
+
consolidation_max_entries_per_run: maxEntries,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
} as Parameters<typeof memoryV2ConsolidateJob>[1];
|
|
169
|
+
}
|
|
152
170
|
const CONFIG_DISABLED = {
|
|
153
171
|
memory: {
|
|
154
172
|
enabled: true,
|
|
@@ -205,6 +223,169 @@ beforeEach(() => {
|
|
|
205
223
|
|
|
206
224
|
// ---------------------------------------------------------------------------
|
|
207
225
|
|
|
226
|
+
describe("memoryV2ConsolidateJob — chunked cutoff (consolidation_max_entries_per_run)", () => {
|
|
227
|
+
const FIVE_ENTRIES = [
|
|
228
|
+
"- [Apr 27, 9:00 AM] Alice prefers VS Code.",
|
|
229
|
+
"- [Apr 27, 9:01 AM] Bob takes his coffee black.",
|
|
230
|
+
"- [Apr 27, 9:02 AM] Carol loves jazz.",
|
|
231
|
+
"- [Apr 27, 9:03 AM] Dave runs marathons.",
|
|
232
|
+
"- [Apr 27, 9:04 AM] Erin paints watercolors.",
|
|
233
|
+
].join("\n");
|
|
234
|
+
|
|
235
|
+
test("buffer over the cap → cutoff is the first over-cap entry's timestamp; overflow deferred", async () => {
|
|
236
|
+
writeFileSync(bufferPath(), FIVE_ENTRIES + "\n");
|
|
237
|
+
|
|
238
|
+
const outcome = await memoryV2ConsolidateJob(
|
|
239
|
+
makeJob(),
|
|
240
|
+
configWithMaxEntries(3),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(outcome.kind).toBe("invoked");
|
|
244
|
+
if (outcome.kind !== "invoked") throw new Error("unreachable");
|
|
245
|
+
expect(outcome.cutoff).toBe("Apr 27, 9:03 AM");
|
|
246
|
+
expect(outcome.deferredEntries).toBe(2);
|
|
247
|
+
expect(runnerCalls).toBe(1);
|
|
248
|
+
expect(runnerLastArgs!.prompt as string).toContain("Apr 27, 9:03 AM");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("buffer at or under the cap → full-buffer cutoff, nothing deferred", async () => {
|
|
252
|
+
writeFileSync(bufferPath(), FIVE_ENTRIES + "\n");
|
|
253
|
+
|
|
254
|
+
const outcome = await memoryV2ConsolidateJob(
|
|
255
|
+
makeJob(),
|
|
256
|
+
configWithMaxEntries(5),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(outcome.kind).toBe("invoked");
|
|
260
|
+
if (outcome.kind !== "invoked") throw new Error("unreachable");
|
|
261
|
+
expect(outcome.deferredEntries).toBe(0);
|
|
262
|
+
// Cutoff is the run-start timestamp, not any buffer entry's.
|
|
263
|
+
expect(outcome.cutoff).not.toBe("Apr 27, 9:03 AM");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("null cap → full buffer processed regardless of size", async () => {
|
|
267
|
+
writeFileSync(bufferPath(), FIVE_ENTRIES + "\n");
|
|
268
|
+
|
|
269
|
+
const outcome = await memoryV2ConsolidateJob(
|
|
270
|
+
makeJob(),
|
|
271
|
+
configWithMaxEntries(null),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(outcome.kind).toBe("invoked");
|
|
275
|
+
if (outcome.kind !== "invoked") throw new Error("unreachable");
|
|
276
|
+
expect(outcome.deferredEntries).toBe(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("cap absent from config (hand-crafted stand-in) → full buffer processed", async () => {
|
|
280
|
+
writeFileSync(bufferPath(), FIVE_ENTRIES + "\n");
|
|
281
|
+
|
|
282
|
+
const outcome = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
283
|
+
|
|
284
|
+
expect(outcome.kind).toBe("invoked");
|
|
285
|
+
if (outcome.kind !== "invoked") throw new Error("unreachable");
|
|
286
|
+
expect(outcome.deferredEntries).toBe(0);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("same-minute burst (every entry shares the over-cap timestamp) → full-buffer cutoff, no wedge", () => {
|
|
290
|
+
// A sweep that wrote >cap entries within one minute: a pulled-back
|
|
291
|
+
// cutoff would defer ALL of them ("timestamp >= cutoff stays") and the
|
|
292
|
+
// size trigger would requeue an identical no-op run forever.
|
|
293
|
+
writeFileSync(
|
|
294
|
+
bufferPath(),
|
|
295
|
+
[
|
|
296
|
+
"- [Apr 27, 9:00 AM] Alice prefers VS Code.",
|
|
297
|
+
"- [Apr 27, 9:00 AM] Bob takes his coffee black.",
|
|
298
|
+
"- [Apr 27, 9:00 AM] Carol loves jazz.",
|
|
299
|
+
"- [Apr 27, 9:00 AM] Dave runs marathons.",
|
|
300
|
+
].join("\n") + "\n",
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return memoryV2ConsolidateJob(makeJob(), configWithMaxEntries(2)).then(
|
|
304
|
+
(outcome) => {
|
|
305
|
+
expect(outcome.kind).toBe("invoked");
|
|
306
|
+
if (outcome.kind !== "invoked") throw new Error("unreachable");
|
|
307
|
+
expect(outcome.deferredEntries).toBe(0);
|
|
308
|
+
expect(outcome.cutoff).not.toBe("Apr 27, 9:00 AM");
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("continuation lines (multi-line entries) don't count as entries or break the cutoff", async () => {
|
|
314
|
+
// The second entry carries embedded newlines: its continuation lines
|
|
315
|
+
// belong to that entry, so the buffer holds 3 entries, not 5 lines.
|
|
316
|
+
// With a cap of 2, the cutoff is the THIRD entry's timestamp.
|
|
317
|
+
writeFileSync(
|
|
318
|
+
bufferPath(),
|
|
319
|
+
[
|
|
320
|
+
"- [Apr 27, 9:00 AM] Alice prefers VS Code.",
|
|
321
|
+
"- [Apr 27, 9:01 AM] Bob shared a snippet:",
|
|
322
|
+
" line two of Bob's multi-line memory",
|
|
323
|
+
" line three of Bob's multi-line memory",
|
|
324
|
+
"- [Apr 27, 9:03 AM] Dave runs marathons.",
|
|
325
|
+
].join("\n") + "\n",
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const outcome = await memoryV2ConsolidateJob(
|
|
329
|
+
makeJob(),
|
|
330
|
+
configWithMaxEntries(2),
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
expect(outcome.kind).toBe("invoked");
|
|
334
|
+
if (outcome.kind !== "invoked") throw new Error("unreachable");
|
|
335
|
+
expect(outcome.cutoff).toBe("Apr 27, 9:03 AM");
|
|
336
|
+
expect(outcome.deferredEntries).toBe(1);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("bracketed continuation lines (checklists, wikilinks) are not counted as entries", async () => {
|
|
340
|
+
// Continuation lines that START with "- [" but don't carry the
|
|
341
|
+
// formatBufferTimestamp shape must not count toward the cap or become
|
|
342
|
+
// the cutoff — "- [ ] task" would otherwise yield a blank-string cutoff.
|
|
343
|
+
writeFileSync(
|
|
344
|
+
bufferPath(),
|
|
345
|
+
[
|
|
346
|
+
"- [Apr 27, 9:00 AM] Alice's project plan:",
|
|
347
|
+
"- [ ] follow up with Bob",
|
|
348
|
+
"- [[meeting-notes]] referenced doc",
|
|
349
|
+
"- [Apr 27, 9:01 AM] Carol loves jazz.",
|
|
350
|
+
].join("\n") + "\n",
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const outcome = await memoryV2ConsolidateJob(
|
|
354
|
+
makeJob(),
|
|
355
|
+
configWithMaxEntries(2),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
expect(outcome.kind).toBe("invoked");
|
|
359
|
+
if (outcome.kind !== "invoked") throw new Error("unreachable");
|
|
360
|
+
// Two real entries, cap 2 → no chunking, full-buffer cutoff.
|
|
361
|
+
expect(outcome.deferredEntries).toBe(0);
|
|
362
|
+
expect(outcome.cutoff).not.toBe(" ");
|
|
363
|
+
expect(outcome.cutoff).not.toBe("[meeting-notes]");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("multi-line entries under the cap → full-buffer cutoff even when raw line count exceeds it", async () => {
|
|
367
|
+
writeFileSync(
|
|
368
|
+
bufferPath(),
|
|
369
|
+
[
|
|
370
|
+
"- [Apr 27, 9:00 AM] Alice shared a snippet:",
|
|
371
|
+
" continuation one",
|
|
372
|
+
" continuation two",
|
|
373
|
+
"- [Apr 27, 9:01 AM] Bob takes his coffee black.",
|
|
374
|
+
].join("\n") + "\n",
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const outcome = await memoryV2ConsolidateJob(
|
|
378
|
+
makeJob(),
|
|
379
|
+
configWithMaxEntries(2),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(outcome.kind).toBe("invoked");
|
|
383
|
+
if (outcome.kind !== "invoked") throw new Error("unreachable");
|
|
384
|
+
expect(outcome.deferredEntries).toBe(0);
|
|
385
|
+
expect(outcome.cutoff).not.toBe("Apr 27, 9:01 AM");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
208
389
|
describe("memoryV2ConsolidateJob — v2 disabled", () => {
|
|
209
390
|
test("returns disabled without invoking the runner when memory.enabled is false", async () => {
|
|
210
391
|
writeFileSync(bufferPath(), "- [Apr 27, 9:00 AM] Alice prefers VS Code.\n");
|
|
@@ -1872,6 +1872,76 @@ describe("injectMemoryV2Block", () => {
|
|
|
1872
1872
|
expect(aliceRow!.ownActivation).toBe(0);
|
|
1873
1873
|
});
|
|
1874
1874
|
|
|
1875
|
+
test("flag-on: saturated index (every page already injected) skips the router LLM call", async () => {
|
|
1876
|
+
// Turn 1 — normal router path injects alice.
|
|
1877
|
+
routerState.nextResult = {
|
|
1878
|
+
selectedSlugs: ["alice-vscode"],
|
|
1879
|
+
failureReason: null,
|
|
1880
|
+
};
|
|
1881
|
+
await injectMemoryV2Block({
|
|
1882
|
+
database: db,
|
|
1883
|
+
conversationId: "conv-router-saturated",
|
|
1884
|
+
currentTurn: 1,
|
|
1885
|
+
recentTurnPairs: [
|
|
1886
|
+
{ assistantMessage: "", userMessage: "Tell me about Alice" },
|
|
1887
|
+
],
|
|
1888
|
+
nowText: "Now",
|
|
1889
|
+
messageId: "msg-1",
|
|
1890
|
+
config: makeConfig({ router: { enabled: true } }),
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
// Mark every page in the index as already injected — the state a
|
|
1894
|
+
// small workspace reaches a few turns into any conversation.
|
|
1895
|
+
const { getPageIndex } = await import("../page-index.js");
|
|
1896
|
+
const index = await getPageIndex(tmpWorkspace);
|
|
1897
|
+
const seeded = await hydrate(db, "conv-router-saturated");
|
|
1898
|
+
await save(db, "conv-router-saturated", {
|
|
1899
|
+
...seeded!,
|
|
1900
|
+
everInjected: index.entries.map((e) => ({ slug: e.slug, turn: 1 })),
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
// Turn 2 — the router must not be called: any selection would dedupe
|
|
1904
|
+
// against everInjected and render nothing.
|
|
1905
|
+
const callsBefore = routerState.callCount;
|
|
1906
|
+
routerState.nextResult = {
|
|
1907
|
+
selectedSlugs: ["bob-coffee"],
|
|
1908
|
+
failureReason: null,
|
|
1909
|
+
};
|
|
1910
|
+
const result = await injectMemoryV2Block({
|
|
1911
|
+
database: db,
|
|
1912
|
+
conversationId: "conv-router-saturated",
|
|
1913
|
+
currentTurn: 2,
|
|
1914
|
+
recentTurnPairs: [{ assistantMessage: "ok", userMessage: "and Bob?" }],
|
|
1915
|
+
nowText: "Now",
|
|
1916
|
+
messageId: "msg-2",
|
|
1917
|
+
config: makeConfig({ router: { enabled: true } }),
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
expect(routerState.callCount).toBe(callsBefore);
|
|
1921
|
+
expect(result.block).toBeNull();
|
|
1922
|
+
expect(result.toInject).toEqual([]);
|
|
1923
|
+
|
|
1924
|
+
// State still advanced; everInjected preserved for future dedupe.
|
|
1925
|
+
const persisted = await hydrate(db, "conv-router-saturated");
|
|
1926
|
+
expect(persisted!.currentTurn).toBe(2);
|
|
1927
|
+
expect(persisted!.messageId).toBe("msg-2");
|
|
1928
|
+
expect(persisted!.everInjected.length).toBe(index.entries.length);
|
|
1929
|
+
|
|
1930
|
+
// Telemetry row stays in router mode with every slug carried over.
|
|
1931
|
+
const row = telemetryState.recordCalls.at(-1) as {
|
|
1932
|
+
mode: string;
|
|
1933
|
+
concepts: Array<{ source: string; status: string }>;
|
|
1934
|
+
};
|
|
1935
|
+
expect(row.mode).toBe("router");
|
|
1936
|
+
expect(row.concepts.length).toBe(index.entries.length);
|
|
1937
|
+
for (const concept of row.concepts) {
|
|
1938
|
+
expect(concept.source).toBe("carry_over");
|
|
1939
|
+
// `finalizeInjection` rewrites the placeholder status: slugs already
|
|
1940
|
+
// in everInjected are reported as in_context.
|
|
1941
|
+
expect(concept.status).toBe("in_context");
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1875
1945
|
test("flag-on: router failure logs warn, writes mode:`errored` telemetry, returns null block", async () => {
|
|
1876
1946
|
routerState.nextResult = {
|
|
1877
1947
|
selectedSlugs: [],
|
|
@@ -126,6 +126,18 @@ describe("readMemoryV2StaticContent", () => {
|
|
|
126
126
|
expect(text.indexOf("## Recent")).toBeLessThan(text.indexOf("## Buffer"));
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
test("excludeBuffer drops the Buffer section but keeps the other three", () => {
|
|
130
|
+
for (const file of MEMORY_FILES) writeMemoryFile(file, `Content ${file}`);
|
|
131
|
+
|
|
132
|
+
const result = readMemoryV2StaticContent({ excludeBuffer: true });
|
|
133
|
+
expect(result).not.toBeNull();
|
|
134
|
+
expect(result!).toContain("## Essentials");
|
|
135
|
+
expect(result!).toContain("## Threads");
|
|
136
|
+
expect(result!).toContain("## Recent");
|
|
137
|
+
expect(result!).not.toContain("## Buffer");
|
|
138
|
+
expect(result!).not.toContain("Content buffer.md");
|
|
139
|
+
});
|
|
140
|
+
|
|
129
141
|
test("omits empty files but keeps populated ones", () => {
|
|
130
142
|
writeMemoryFile("essentials.md", "Alice prefers VS Code.");
|
|
131
143
|
writeMemoryFile("threads.md", "");
|
|
@@ -144,6 +144,12 @@ export type ConsolidationOutcome =
|
|
|
144
144
|
kind: "invoked";
|
|
145
145
|
conversationId: string;
|
|
146
146
|
cutoff: string;
|
|
147
|
+
/**
|
|
148
|
+
* Buffer entries beyond `consolidation_max_entries_per_run` left for a
|
|
149
|
+
* follow-up pass via the pulled-back cutoff. `0` when the whole buffer
|
|
150
|
+
* fit in one run.
|
|
151
|
+
*/
|
|
152
|
+
deferredEntries: number;
|
|
147
153
|
followUpJobIds: string[];
|
|
148
154
|
};
|
|
149
155
|
|
|
@@ -174,15 +180,7 @@ export async function memoryV2ConsolidateJob(
|
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
try {
|
|
177
|
-
// Step 2:
|
|
178
|
-
// (`Mon D, h:mm AM/PM`, see `formatBufferTimestamp`) so the agent's
|
|
179
|
-
// "timestamp ≥ cutoff" check compares like-with-like at minute precision.
|
|
180
|
-
// Same-minute entries land on the next pass — conservative but loss-free.
|
|
181
|
-
// Captured here (not at enqueue time) so late-claimed rows get a fresh
|
|
182
|
-
// cutoff.
|
|
183
|
-
const cutoff = formatBufferTimestamp(new Date());
|
|
184
|
-
|
|
185
|
-
// Step 3: bail on empty buffer. Nothing for the agent to consolidate.
|
|
183
|
+
// Step 2: bail on empty buffer. Nothing for the agent to consolidate.
|
|
186
184
|
// The lock is released in finally below.
|
|
187
185
|
const bufferContent = readBufferContent(bufferPath);
|
|
188
186
|
if (bufferContent.trim().length === 0) {
|
|
@@ -190,6 +188,68 @@ export async function memoryV2ConsolidateJob(
|
|
|
190
188
|
return { kind: "empty_buffer" };
|
|
191
189
|
}
|
|
192
190
|
|
|
191
|
+
// Step 3: capture cutoff. Formatted to match `buffer.md` entry timestamps
|
|
192
|
+
// (`Mon D, h:mm AM/PM`, see `formatBufferTimestamp`) so the agent's
|
|
193
|
+
// "timestamp ≥ cutoff" check compares like-with-like at minute precision.
|
|
194
|
+
// Same-minute entries land on the next pass — conservative but loss-free.
|
|
195
|
+
// Captured here (not at enqueue time) so late-claimed rows get a fresh
|
|
196
|
+
// cutoff.
|
|
197
|
+
//
|
|
198
|
+
// Chunking: when the buffer holds more than
|
|
199
|
+
// `consolidation_max_entries_per_run` entries (a backlog from missed or
|
|
200
|
+
// failed runs), pull the cutoff back to the first over-cap entry's
|
|
201
|
+
// timestamp. The agent's existing "≥ cutoff stays" rule then defers the
|
|
202
|
+
// overflow loss-free, and the `consolidation_max_buffer_lines` size
|
|
203
|
+
// trigger re-fires while the remainder stays over threshold — so one run
|
|
204
|
+
// never has to read an unbounded backlog into context. Entries sharing
|
|
205
|
+
// the over-cap entry's minute are also deferred (conservative).
|
|
206
|
+
//
|
|
207
|
+
// Entries are counted by their timestamped bullet-start lines
|
|
208
|
+
// (`- [Mon D, h:mm AM/PM] …`) rather than raw non-empty lines: a
|
|
209
|
+
// remembered fact can carry embedded newlines, and its continuation
|
|
210
|
+
// lines belong to the preceding entry, not the count.
|
|
211
|
+
let cutoff = formatBufferTimestamp(new Date());
|
|
212
|
+
let deferredEntries = 0;
|
|
213
|
+
const maxEntries = config.memory.v2.consolidation_max_entries_per_run;
|
|
214
|
+
if (maxEntries != null) {
|
|
215
|
+
const entryTimestamps = bufferContent
|
|
216
|
+
.split("\n")
|
|
217
|
+
.map(extractBufferEntryTimestamp)
|
|
218
|
+
.filter((timestamp): timestamp is string => timestamp !== null);
|
|
219
|
+
if (entryTimestamps.length > maxEntries) {
|
|
220
|
+
const overflowTimestamp = entryTimestamps[maxEntries];
|
|
221
|
+
// Same-minute burst guard: timestamps have minute precision, so when
|
|
222
|
+
// even the FIRST entry shares the over-cap entry's timestamp, a
|
|
223
|
+
// pulled-back cutoff would tell the agent to defer every entry
|
|
224
|
+
// ("timestamp ≥ cutoff stays") — zero progress, and the size trigger
|
|
225
|
+
// would requeue the identical run forever. Fall back to the
|
|
226
|
+
// full-buffer cutoff in that case; partial same-minute runs (some
|
|
227
|
+
// earlier entries have older timestamps) still make progress.
|
|
228
|
+
if (entryTimestamps[0] === overflowTimestamp) {
|
|
229
|
+
log.warn(
|
|
230
|
+
{
|
|
231
|
+
bufferEntries: entryTimestamps.length,
|
|
232
|
+
maxEntries,
|
|
233
|
+
overflowTimestamp,
|
|
234
|
+
},
|
|
235
|
+
"consolidation: entire over-cap prefix shares one minute timestamp; processing full buffer to guarantee progress",
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
cutoff = overflowTimestamp;
|
|
239
|
+
deferredEntries = entryTimestamps.length - maxEntries;
|
|
240
|
+
log.info(
|
|
241
|
+
{
|
|
242
|
+
bufferEntries: entryTimestamps.length,
|
|
243
|
+
maxEntries,
|
|
244
|
+
deferredEntries,
|
|
245
|
+
cutoff,
|
|
246
|
+
},
|
|
247
|
+
"consolidation chunked: buffer over per-run cap, overflow deferred to next pass",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
193
253
|
// Step 4: hand off to the centralized background-job runner. The runner
|
|
194
254
|
// bootstraps the conversation, drives `processMessage`, applies the
|
|
195
255
|
// timeout policy, classifies errors, and — because we opt out via
|
|
@@ -225,6 +285,7 @@ export async function memoryV2ConsolidateJob(
|
|
|
225
285
|
jobName: JOB_NAME,
|
|
226
286
|
source: MEMORY_V2_CONSOLIDATION_SOURCE,
|
|
227
287
|
prompt,
|
|
288
|
+
systemHint: "Memory consolidation",
|
|
228
289
|
trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
229
290
|
callSite: "memoryV2Consolidation",
|
|
230
291
|
timeoutMs: CONSOLIDATION_TIMEOUT_MS,
|
|
@@ -272,6 +333,7 @@ export async function memoryV2ConsolidateJob(
|
|
|
272
333
|
{
|
|
273
334
|
conversationId: runResult.conversationId,
|
|
274
335
|
cutoff,
|
|
336
|
+
deferredEntries,
|
|
275
337
|
followUpJobIds,
|
|
276
338
|
},
|
|
277
339
|
"consolidation invoked",
|
|
@@ -280,6 +342,7 @@ export async function memoryV2ConsolidateJob(
|
|
|
280
342
|
kind: "invoked",
|
|
281
343
|
conversationId: runResult.conversationId,
|
|
282
344
|
cutoff,
|
|
345
|
+
deferredEntries,
|
|
283
346
|
followUpJobIds,
|
|
284
347
|
};
|
|
285
348
|
} finally {
|
|
@@ -300,6 +363,27 @@ function readBufferContent(bufferPath: string): string {
|
|
|
300
363
|
}
|
|
301
364
|
}
|
|
302
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Extract the bracketed timestamp from a `buffer.md` entry line
|
|
368
|
+
* (`- [Mon D, h:mm AM/PM] …`, see `formatBufferEntry`). Returned verbatim so
|
|
369
|
+
* it can serve directly as a consolidation cutoff — both sides of the agent's
|
|
370
|
+
* "timestamp ≥ cutoff" comparison then share the exact `formatBufferTimestamp`
|
|
371
|
+
* shape.
|
|
372
|
+
*
|
|
373
|
+
* The bracket contents must match that shape exactly: a remembered fact's
|
|
374
|
+
* continuation lines can themselves start with `- [` (markdown checklists
|
|
375
|
+
* `- [ ] …`, wikilink bullets `- [[…]]`), and counting those as entries
|
|
376
|
+
* would inflate the per-run budget — or worse, hand the agent a garbage
|
|
377
|
+
* cutoff like a blank string. Returns `null` for anything that isn't a real
|
|
378
|
+
* timestamped entry start.
|
|
379
|
+
*/
|
|
380
|
+
function extractBufferEntryTimestamp(line: string): string | null {
|
|
381
|
+
const match = /^\s*-\s*\[([A-Z][a-z]{2} \d{1,2}, \d{1,2}:\d{2} [AP]M)\]/.exec(
|
|
382
|
+
line,
|
|
383
|
+
);
|
|
384
|
+
return match ? match[1] : null;
|
|
385
|
+
}
|
|
386
|
+
|
|
303
387
|
/**
|
|
304
388
|
* Count non-empty lines in `memory/buffer.md`. Used by the scheduler to
|
|
305
389
|
* implement the size-based consolidation trigger. Missing file → 0.
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from "./cli-command-store.js";
|
|
45
45
|
import { getEdgeIndex } from "./edge-index.js";
|
|
46
46
|
import { recordInjectionEvents } from "./injection-events.js";
|
|
47
|
+
import { getPageIndex } from "./page-index.js";
|
|
47
48
|
import { readPage, renderPageContent } from "./page-store.js";
|
|
48
49
|
import { type RouterTurnPair, runRouter } from "./router.js";
|
|
49
50
|
import { getSkillCapability, isSkillSlug } from "./skill-store.js";
|
|
@@ -553,6 +554,58 @@ async function injectViaRouter(args: {
|
|
|
553
554
|
const priorEverInjected: readonly EverInjectedEntry[] =
|
|
554
555
|
priorState?.everInjected ?? [];
|
|
555
556
|
|
|
557
|
+
// Saturated-index bypass: when every page in the index has already been
|
|
558
|
+
// injected on a prior turn of this conversation, the router LLM call is
|
|
559
|
+
// provably useless — whatever it selected would be deduped against
|
|
560
|
+
// `priorEverInjected` below and render nothing. Small workspaces reach
|
|
561
|
+
// this state within the first few turns, after which the per-message
|
|
562
|
+
// router call is pure cost. Skip the call and finalize directly with
|
|
563
|
+
// carry-over telemetry, mirroring the success path's "all selections
|
|
564
|
+
// already injected" outcome. (Trade-off: re-pick EMA events are not
|
|
565
|
+
// recorded from this state; with the whole index attached, injection-
|
|
566
|
+
// score ranking has nothing left to decide for this conversation.)
|
|
567
|
+
const everInjectedSlugSet = new Set(priorEverInjected.map((e) => e.slug));
|
|
568
|
+
const pageIndexForBypass = await getPageIndex(workspaceDir);
|
|
569
|
+
if (
|
|
570
|
+
pageIndexForBypass.entries.length > 0 &&
|
|
571
|
+
pageIndexForBypass.entries.every((e) => everInjectedSlugSet.has(e.slug))
|
|
572
|
+
) {
|
|
573
|
+
log.info(
|
|
574
|
+
{
|
|
575
|
+
conversationId,
|
|
576
|
+
indexSize: pageIndexForBypass.entries.length,
|
|
577
|
+
},
|
|
578
|
+
"memory v2 router skipped — every index page already injected",
|
|
579
|
+
);
|
|
580
|
+
return finalizeInjection({
|
|
581
|
+
workspaceDir,
|
|
582
|
+
database,
|
|
583
|
+
conversationId,
|
|
584
|
+
mode: "router",
|
|
585
|
+
currentTurn,
|
|
586
|
+
messageId,
|
|
587
|
+
priorEverInjected,
|
|
588
|
+
slugsToRender: [],
|
|
589
|
+
telemetryRows: priorEverInjected.map((entry) => ({
|
|
590
|
+
slug: entry.slug,
|
|
591
|
+
finalActivation: 0,
|
|
592
|
+
ownActivation: 0,
|
|
593
|
+
priorActivation: 0,
|
|
594
|
+
simUser: 0,
|
|
595
|
+
simAssistant: 0,
|
|
596
|
+
simNow: 0,
|
|
597
|
+
simUserRerankBoost: 0,
|
|
598
|
+
simAssistantRerankBoost: 0,
|
|
599
|
+
inRerankPool: false,
|
|
600
|
+
spreadContribution: 0,
|
|
601
|
+
source: "carry_over",
|
|
602
|
+
status: "not_injected",
|
|
603
|
+
})),
|
|
604
|
+
config,
|
|
605
|
+
nextStateMap: {},
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
556
609
|
const routerResult = await runRouter({
|
|
557
610
|
workspaceDir,
|
|
558
611
|
recentTurnPairs,
|
|
@@ -571,6 +571,7 @@ Bullets where a list is genuinely a list.
|
|
|
571
571
|
- **\`links\`** are directed see-also references, each annotated: \`"target-slug — why"\`. The annotation is for future-you deciding whether to follow it. Directed means listing B here pulls B toward A's readers — it does not link back.
|
|
572
572
|
- **\`tags\`** are flat labels for the cluster(s) this touches. Index pages also carry \`kind: index\`.
|
|
573
573
|
- **No \`summary:\` field.** The lead is the summary. Writing a good lead IS writing the retrieval surface.
|
|
574
|
+
- **\`current:\`** — optional ONE-LINE live state of the page's subject: open items, deadlines, what's owed or pending, with an as-of date in the text (\`current: "bridge check owed before thursday's dry-run (as of jun 10)"\`). Retrieval renders it on the card, so status-shaped questions ("what's on my plate", "what's pending") can find the page. Maintain it like state, not prose: update it when the state moves, DELETE the field the moment nothing is live — a stale \`current:\` is worse than none. Most pages never carry one. \`threads.md\` remains the cross-page commitments list; \`current:\` is per-page state, not a second threads file.
|
|
574
575
|
|
|
575
576
|
## The card budget (the economic principle)
|
|
576
577
|
|
|
@@ -38,8 +38,17 @@ const MEMORY_V2_STATIC_BLOCKS: readonly MemoryV2StaticBlock[] = [
|
|
|
38
38
|
* Build the v2 static memory block, gated on `config.memory.enabled` and
|
|
39
39
|
* `config.memory.v2.enabled`. Empty/missing files are skipped; returns `null`
|
|
40
40
|
* when either gate is off or every file is empty.
|
|
41
|
+
*
|
|
42
|
+
* `excludeBuffer` drops the `## Buffer` section. The consolidation run sets
|
|
43
|
+
* it: the agent's contract there is the `memory/buffer.md` FILE — it reads,
|
|
44
|
+
* routes, and rewrites it through file tools — so injecting a static snapshot
|
|
45
|
+
* of the same content would duplicate the entire (potentially unbounded)
|
|
46
|
+
* backlog into the turn's context and go stale the moment the agent edits
|
|
47
|
+
* the file.
|
|
41
48
|
*/
|
|
42
|
-
export function readMemoryV2StaticContent(
|
|
49
|
+
export function readMemoryV2StaticContent(
|
|
50
|
+
options: { excludeBuffer?: boolean } = {},
|
|
51
|
+
): string | null {
|
|
43
52
|
let config;
|
|
44
53
|
try {
|
|
45
54
|
config = loadConfig();
|
|
@@ -52,6 +61,9 @@ export function readMemoryV2StaticContent(): string | null {
|
|
|
52
61
|
|
|
53
62
|
const sections: string[] = [];
|
|
54
63
|
for (const { heading, file } of MEMORY_V2_STATIC_BLOCKS) {
|
|
64
|
+
if (options.excludeBuffer === true && file === "memory/buffer.md") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
55
67
|
const content = readPromptFile(getWorkspacePromptPath(file));
|
|
56
68
|
if (!content) continue;
|
|
57
69
|
sections.push(`${heading}\n\n${content}`);
|
|
@@ -283,7 +283,7 @@ function loadRecentMessagesText(nowMs: number): string {
|
|
|
283
283
|
// initial limit (1000) defends against pathological busy windows where a
|
|
284
284
|
// naive scan would touch every recent message. Joining conversations and
|
|
285
285
|
// excluding background/scheduled types keeps automation chatter
|
|
286
|
-
// (heartbeats, filing,
|
|
286
|
+
// (heartbeats, filing, scheduled jobs) out of buffer.md.
|
|
287
287
|
const rows = db
|
|
288
288
|
.select({
|
|
289
289
|
role: messages.role,
|
package/src/memory/v2/types.ts
CHANGED
|
@@ -62,6 +62,11 @@ export const ConceptPageFrontmatterSchema = z
|
|
|
62
62
|
main: z.string().optional(),
|
|
63
63
|
kind: z.string().optional(),
|
|
64
64
|
status: z.string().optional(),
|
|
65
|
+
// One-line live state of the page's subject (open items, deadlines,
|
|
66
|
+
// what's pending), as-of dated by convention. Memory-v3 renders it on the
|
|
67
|
+
// card surface so state-shaped questions can select the page. Distinct
|
|
68
|
+
// from `status:` (the article-model draft marker, e.g. "cc-draft").
|
|
69
|
+
current: z.string().optional(),
|
|
65
70
|
})
|
|
66
71
|
.strict();
|
|
67
72
|
|
package/src/plugin-api/types.ts
CHANGED
|
@@ -314,6 +314,13 @@ export interface PostToolUseContext {
|
|
|
314
314
|
* singular of Codex's `additional_contexts`. `null` means no extra context.
|
|
315
315
|
*/
|
|
316
316
|
additionalContext: string | null;
|
|
317
|
+
/**
|
|
318
|
+
* Model id reported by the provider for the assistant turn that issued
|
|
319
|
+
* this tool call (e.g. `claude-opus-4-8`,
|
|
320
|
+
* `accounts/fireworks/models/kimi-k2p6`). Hooks use it to vary coaching by
|
|
321
|
+
* model family — some models need earlier or firmer steering than others.
|
|
322
|
+
*/
|
|
323
|
+
readonly model: string;
|
|
317
324
|
/**
|
|
318
325
|
* The model's context-window size in tokens. Plugins derive their own
|
|
319
326
|
* character budget from this (e.g. a share of the window) rather than
|