Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
61380fa
feat(memory-v3): tree-node on-disk format + node store (#31971)
velissa-ai May 25, 2026
be5eb0f
feat(memory-v3): config schema + cheap/capable LLM call sites (#31972)
velissa-ai May 25, 2026
32394ad
feat(memory-v3): curated edge-expansion lane (#31973)
velissa-ai May 25, 2026
5aa678b
feat(memory-v3): write-path job types + config (no behavior) (#31974)
velissa-ai May 25, 2026
7bd5de2
feat(memory-v3): gate decision (ready/more) + final selection (#31975)
velissa-ai May 25, 2026
5df253f
feat(memory-v3): tree index with DAG adjacency + cache (#31976)
velissa-ai May 25, 2026
00d7812
feat(memory-v3): always-on scouts over the v2 substrate (#31977)
velissa-ai May 25, 2026
eabe526
feat(memory-v3): compose node index from children + routing hints (#3…
velissa-ai May 25, 2026
6df7704
feat(memory-v3): fast filter judging dense hits (sticky bypass) (#31979)
velissa-ai May 25, 2026
a1218c9
feat(memory-v3): parallel-fan-out traversal with cycle/visited guards…
velissa-ai May 25, 2026
21f0087
feat(memory-v3): tree validator (orphans, cycles, dangling refs, fres…
velissa-ai May 25, 2026
94ad287
feat(memory-v3): scout-seeded tree-walk descent driver (#31982)
velissa-ai May 25, 2026
41a3bf4
feat(memory-v3): assistant memory v3 validate/tree CLI + routes (#31983)
velissa-ai May 25, 2026
da0fcec
feat(memory-v3): retrieval loop (scouts->filter->tree->edges->gate) (…
velissa-ai May 25, 2026
ec915b0
feat(memory-v3): consolidation drains shared buffer into tree + maint…
velissa-ai May 25, 2026
01e1c81
feat(memory-v3): v3 Retriever as comparand #2 in the compare harness …
velissa-ai May 25, 2026
5ef2bbc
feat(memory-v3): pass-1->pass-2 co-activation logging (#31987)
velissa-ai May 25, 2026
9279659
feat(memory-v3): weighted, decaying auto-edge learning job (#31988)
velissa-ai May 25, 2026
93da857
feat(memory-v3): live shadow via memoryRetrieval middleware (inject v…
velissa-ai May 25, 2026
afa8d28
fix(memory-v3): null-safe shadow gate when memory.v3 config is absent
May 25, 2026
700bdd5
fix(memory-v3): add route policies for memory/v3/validate + tree
May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions assistant/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11697,6 +11697,48 @@ paths:
type: object
properties: {}
additionalProperties: false
/v1/memory/v3/tree:
post:
operationId: memory_v3_tree_post
summary: Return a serializable view of the memory v3 tree DAG (read-only)
description:
Returns the v3 tree root id plus every node and its ordered child refs (page:/node:) as a JSON-serializable
projection of the in-memory TreeIndex. Read-only; the CLI uses it to print an indented tree with shared-DAG
re-entries marked.
tags:
- memory
responses:
"200":
description: Successful response
requestBody:
required: true
content:
application/json:
schema:
type: object
properties: {}
additionalProperties: false
/v1/memory/v3/validate:
post:
operationId: memory_v3_validate_post
summary: Validate the memory v3 tree structure (read-only)
description:
Read-only structural validation of the hand-authored v3 tree DAG. Reports dangling child refs, orphan
pages, cycles, stale compositional indexes, and unknown edge targets. Writes nothing and runs no LLM — operators
dry-run it while the v2 → v3 migration is in flight.
tags:
- memory
responses:
"200":
description: Successful response
requestBody:
required: true
content:
application/json:
schema:
type: object
properties: {}
additionalProperties: false
/v1/messages:
get:
operationId: messages_get
Expand Down
86 changes: 77 additions & 9 deletions assistant/src/__tests__/llm-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { describe, expect, test } from "bun:test";

import { z } from "zod";

import { resolveCallSiteConfig, resolveDefaultProfileKey } from "../config/llm-resolver.js";
import {
resolveCallSiteConfig,
resolveDefaultProfileKey,
} from "../config/llm-resolver.js";
import { type LLMCallSite, LLMSchema } from "../config/schemas/llm.js";

const fullDefault = {
Expand Down Expand Up @@ -690,13 +693,28 @@ describe("resolveCallSiteConfig", () => {
});

const callSites: LLMCallSite[] = [
"mainAgent", "subagentSpawn", "heartbeatAgent", "filingAgent",
"compactionAgent", "analyzeConversation", "callAgent",
"memoryExtraction", "memoryConsolidation", "memoryRetrieval",
"memoryRouter", "recall", "conversationSummarization",
"commitMessage", "conversationStarters", "replySuggestion",
"conversationTitle", "identityIntro", "emptyStateGreeting",
"notificationDecision", "interactionClassifier", "inference",
"mainAgent",
"subagentSpawn",
"heartbeatAgent",
"filingAgent",
"compactionAgent",
"analyzeConversation",
"callAgent",
"memoryExtraction",
"memoryConsolidation",
"memoryRetrieval",
"memoryRouter",
"recall",
"conversationSummarization",
"commitMessage",
"conversationStarters",
"replySuggestion",
"conversationTitle",
"identityIntro",
"emptyStateGreeting",
"notificationDecision",
"interactionClassifier",
"inference",
];

for (const cs of callSites) {
Expand Down Expand Up @@ -778,7 +796,10 @@ describe("resolveCallSiteConfig", () => {
provider_connection: "anthropic-managed",
},
profiles: {
fireworks: { provider: "fireworks", model: "accounts/fireworks/models/kimi-k2p5" },
fireworks: {
provider: "fireworks",
model: "accounts/fireworks/models/kimi-k2p5",
},
},
activeProfile: "fireworks",
});
Expand Down Expand Up @@ -874,3 +895,50 @@ describe("resolveDefaultProfileKey", () => {
);
});
});

describe("memory v3 call sites resolve through the standard resolver", () => {
const llm = LLMSchema.parse({
default: fullDefault,
profiles: {
balanced: { provider: "anthropic", model: "claude-sonnet-4-7" },
"cost-optimized": {
provider: "anthropic",
model: "claude-haiku-4-5-20251001",
},
},
});

test("memoryV3Filter and memoryV3Descent resolve to the cost-optimized profile", () => {
expect(resolveDefaultProfileKey("memoryV3Filter", llm)).toBe(
"cost-optimized",
);
expect(resolveDefaultProfileKey("memoryV3Descent", llm)).toBe(
"cost-optimized",
);
expect(resolveCallSiteConfig("memoryV3Filter", llm).model).toBe(
"claude-haiku-4-5-20251001",
);
expect(resolveCallSiteConfig("memoryV3Descent", llm).model).toBe(
"claude-haiku-4-5-20251001",
);
});

test("memoryV3Gate resolves to the balanced (capable) profile", () => {
expect(resolveDefaultProfileKey("memoryV3Gate", llm)).toBe("balanced");
expect(resolveCallSiteConfig("memoryV3Gate", llm).model).toBe(
"claude-sonnet-4-7",
);
});

test("v3 call sites are addressable as call-site override keys", () => {
const overridden = LLMSchema.parse({
default: fullDefault,
callSites: {
memoryV3Gate: { model: "claude-opus-4-7" },
},
});
expect(resolveCallSiteConfig("memoryV3Gate", overridden).model).toBe(
"claude-opus-4-7",
);
});
});
164 changes: 164 additions & 0 deletions assistant/src/cli/commands/__tests__/memory-v3-render.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { describe, expect, test } from "bun:test";

import type {
MemoryV3TreeResult,
MemoryV3ValidateResult,
} from "../../../runtime/routes/memory-v3-routes.js";
import {
renderTree,
renderValidationReport,
reportHasDefects,
} from "../memory-v3-render.js";

function cleanReport(): MemoryV3ValidateResult {
return {
danglingChildRefs: [],
danglingChildRefCount: 0,
orphanPages: [],
orphanPageCount: 0,
cycles: [],
cycleCount: 0,
staleIndex: [],
staleIndexCount: 0,
unknownEdgeTargets: [],
unknownEdgeTargetCount: 0,
};
}

describe("memory v3 — renderValidationReport", () => {
test("renders 'none' for every empty category", () => {
const out = renderValidationReport(cleanReport());
expect(out).toContain("Memory v3 Tree Validation");
expect(out).toContain("Dangling child refs: none");
expect(out).toContain("Orphan pages: none");
expect(out).toContain("Cycles: none");
expect(out).toContain("Stale index: none");
expect(out).toContain("Unknown edge targets: none");
});

test("renders counts and offending ids for each defect category", () => {
const report: MemoryV3ValidateResult = {
danglingChildRefs: [{ node: "people", ref: "ghost", kind: "node" }],
danglingChildRefCount: 1,
orphanPages: ["stray-page"],
orphanPageCount: 1,
cycles: [{ from: "a", to: "b" }],
cycleCount: 1,
staleIndex: [
{ node: "root", child: "people", nodeMtimeMs: 1, childMtimeMs: 2 },
],
staleIndexCount: 1,
unknownEdgeTargets: [{ from: "p1", to: "missing" }],
unknownEdgeTargetCount: 1,
};
const out = renderValidationReport(report);
expect(out).toContain("Dangling child refs: 1");
expect(out).toContain("people → node:ghost");
expect(out).toContain("Orphan pages: 1");
expect(out).toContain("- stray-page");
expect(out).toContain("Cycles: 1");
expect(out).toContain("a → b");
expect(out).toContain("Stale index: 1");
expect(out).toContain("root (older than child people)");
expect(out).toContain("Unknown edge targets: 1");
expect(out).toContain("p1 → missing");
});
});

describe("memory v3 — reportHasDefects", () => {
test("false for a clean report", () => {
expect(reportHasDefects(cleanReport())).toBe(false);
});

test("true when any single category is non-empty", () => {
const report = cleanReport();
report.orphanPageCount = 1;
report.orphanPages = ["x"];
expect(reportHasDefects(report)).toBe(true);
});
});

describe("memory v3 — renderTree", () => {
test("renders an indented tree descending node and page children", () => {
const view: MemoryV3TreeResult = {
root: "_root",
nodes: [
{
id: "_root",
children: [
{ kind: "node", ref: "people" },
{ kind: "page", ref: "overview" },
],
},
{
id: "people",
children: [{ kind: "page", ref: "alice" }],
},
],
};
const out = renderTree(view);
expect(out).toBe(
["node:_root", " node:people", " page:alice", " page:overview"].join(
"\n",
),
);
});

test("marks a shared DAG sub-node as a re-entry rather than re-expanding", () => {
const view: MemoryV3TreeResult = {
root: "_root",
nodes: [
{
id: "_root",
children: [
{ kind: "node", ref: "a" },
{ kind: "node", ref: "b" },
],
},
{ id: "a", children: [{ kind: "node", ref: "shared" }] },
{ id: "b", children: [{ kind: "node", ref: "shared" }] },
{ id: "shared", children: [{ kind: "page", ref: "leaf" }] },
],
};
const out = renderTree(view);
// First reach under `a` expands; second reach under `b` is a marked re-entry.
expect(out).toContain(" node:a\n node:shared\n page:leaf");
expect(out).toContain("node:shared (↑ already shown)");
// The leaf page is expanded exactly once.
expect(out.match(/page:leaf/g)?.length).toBe(1);
});

test("bounds output on a cycle instead of looping forever", () => {
const view: MemoryV3TreeResult = {
root: "_root",
nodes: [
{ id: "_root", children: [{ kind: "node", ref: "a" }] },
{ id: "a", children: [{ kind: "node", ref: "_root" }] },
],
};
const out = renderTree(view);
expect(out).toContain("node:_root (↑ already shown)");
});

test("flags a child ref whose target node is missing", () => {
const view: MemoryV3TreeResult = {
root: "_root",
nodes: [{ id: "_root", children: [{ kind: "node", ref: "ghost" }] }],
};
const out = renderTree(view);
expect(out).toContain("node:ghost (missing)");
});

test("lists nodes unreachable from the root", () => {
const view: MemoryV3TreeResult = {
root: "_root",
nodes: [
{ id: "_root", children: [] },
{ id: "floating", children: [] },
],
};
const out = renderTree(view);
expect(out).toContain("Unreachable nodes (1):");
expect(out).toContain("- node:floating");
});
});
Loading
Loading