diff --git a/assistant/src/config/schemas/__tests__/memory-v2.test.ts b/assistant/src/config/schemas/__tests__/memory-v2.test.ts index 5ca74e76bba..acdbc7dbb26 100644 --- a/assistant/src/config/schemas/__tests__/memory-v2.test.ts +++ b/assistant/src/config/schemas/__tests__/memory-v2.test.ts @@ -224,6 +224,11 @@ describe("MemoryV3ConfigSchema", () => { denseQuota: { activeDomain: 30, offDomain: 8 }, lanes: { hot: true, sparse: true, dense: true, tree: true, edges: true }, ks: [5, 10, 25, 50], + write: { + enabled: false, + consolidateIntervalMs: 3600000, + coactivation: false, + }, }); }); @@ -287,6 +292,30 @@ describe("MemoryV3ConfigSchema", () => { test("rejects non-number ks entries", () => { expect(() => MemoryV3ConfigSchema.parse({ ks: ["a"] })).toThrow(); }); + + test("parses the write subtree to safe off defaults when omitted", () => { + const parsed = MemoryV3ConfigSchema.parse({}); + expect(parsed.write).toEqual({ + enabled: false, + consolidateIntervalMs: 3600000, + coactivation: false, + }); + }); + + test("accepts a partial write override and defaults the rest", () => { + const parsed = MemoryV3ConfigSchema.parse({ write: { enabled: true } }); + expect(parsed.write).toEqual({ + enabled: true, + consolidateIntervalMs: 3600000, + coactivation: false, + }); + }); + + test("rejects a non-integer write.consolidateIntervalMs", () => { + expect(() => + MemoryV3ConfigSchema.parse({ write: { consolidateIntervalMs: 1.5 } }), + ).toThrow(); + }); }); describe("MemoryConfigSchema integration with v3 block", () => { @@ -298,6 +327,11 @@ describe("MemoryConfigSchema integration with v3 block", () => { expect(parsed.v3.passCap).toBe(3); expect(parsed.v3.lanes.dense).toBe(true); expect(parsed.v3.ks).toEqual([5, 10, 25, 50]); + expect(parsed.v3.write).toEqual({ + enabled: false, + consolidateIntervalMs: 3600000, + coactivation: false, + }); }); test("leaves pre-existing configs (no v3 key) otherwise unchanged", () => { diff --git a/assistant/src/config/schemas/memory-v2.ts b/assistant/src/config/schemas/memory-v2.ts index 11360a89e31..433267cc05e 100644 --- a/assistant/src/config/schemas/memory-v2.ts +++ b/assistant/src/config/schemas/memory-v2.ts @@ -491,6 +491,38 @@ export const MemoryV3ConfigSchema = z .describe( "Evaluation top-K cutoffs the v3 loop reports metrics at (e.g. recall@K).", ), + write: z + .object({ + enabled: z + .boolean({ error: "memory.v3.write.enabled must be a boolean" }) + .default(false) + .describe( + "Whether v3 consolidation owns the shared-buffer drain + tree build. Off by default — v2 consolidation stays the sole buffer-drainer. Does NOT introduce a separate buffer.", + ), + consolidateIntervalMs: z + .number({ + error: "memory.v3.write.consolidateIntervalMs must be a number", + }) + .int("memory.v3.write.consolidateIntervalMs must be an integer") + .default(3600000) + .describe( + "Interval, in milliseconds, between scheduled v3 consolidation runs once the v3 write path owns the drain. Default 1 hour.", + ), + coactivation: z + .boolean({ error: "memory.v3.write.coactivation must be a boolean" }) + .default(false) + .describe( + "Whether v3 consolidation learns co-activation edges during the tree build. Off by default; consumed by a later PR.", + ), + }) + .default({ + enabled: false, + consolidateIntervalMs: 3600000, + coactivation: false, + }) + .describe( + "Memory v3 write-path configuration. All default-off scaffolding — controls whether v3 consolidation owns the shared-buffer drain + tree build. Consumed by later PRs.", + ), }) .default({ enabled: false, @@ -501,6 +533,11 @@ export const MemoryV3ConfigSchema = z denseQuota: { activeDomain: 30, offDomain: 8 }, lanes: { hot: true, sparse: true, dense: true, tree: true, edges: true }, ks: [5, 10, 25, 50], + write: { + enabled: false, + consolidateIntervalMs: 3600000, + coactivation: false, + }, }) .describe( "Memory v3 — multi-lane bounded-descent retrieval. Additive scaffolding, disabled by default.", diff --git a/assistant/src/memory/__tests__/jobs-store-job-classes.test.ts b/assistant/src/memory/__tests__/jobs-store-job-classes.test.ts index 7950e93758b..09fc33779b9 100644 --- a/assistant/src/memory/__tests__/jobs-store-job-classes.test.ts +++ b/assistant/src/memory/__tests__/jobs-store-job-classes.test.ts @@ -1,6 +1,24 @@ import { describe, expect, test } from "bun:test"; -import { EMBED_JOB_TYPES, SLOW_LLM_JOB_TYPES } from "../jobs-store.js"; +import { + EMBED_JOB_TYPES, + type MemoryJobType, + SLOW_LLM_JOB_TYPES, +} from "../jobs-store.js"; + +describe("memory v3 job types", () => { + test("the v3 job-type literals are members of MemoryJobType", () => { + // Compile-time assignability is enforced by `tsc --noEmit`; the runtime + // assertion keeps the literals visible to the test runner. These types are + // inert scaffolding until their handlers land in later PRs. + const v3JobTypes: MemoryJobType[] = [ + "memory_v3_consolidate", + "memory_v3_index_maintenance", + "memory_v3_edge_learning", + ]; + expect(new Set(v3JobTypes).size).toBe(3); + }); +}); describe("memory job classes", () => { test("EMBED_JOB_TYPES and SLOW_LLM_JOB_TYPES are disjoint", () => { diff --git a/assistant/src/memory/jobs-store.ts b/assistant/src/memory/jobs-store.ts index 930f20cdad6..fd672f75e88 100644 --- a/assistant/src/memory/jobs-store.ts +++ b/assistant/src/memory/jobs-store.ts @@ -44,6 +44,9 @@ export type MemoryJobType = | "memory_v2_migrate" | "memory_v2_reembed" | "memory_v2_activation_recompute" + | "memory_v3_consolidate" + | "memory_v3_index_maintenance" + | "memory_v3_edge_learning" | "memory_retrospective"; export const EMBED_JOB_TYPES: MemoryJobType[] = [