Skip to content

Commit d2e0a6f

Browse files
author
Tehan
committed
docs(external-memory): clarify profile slice merges into user-profile, not external-memory block
1 parent 5b95697 commit d2e0a6f

3 files changed

Lines changed: 95 additions & 28 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/// <reference types="bun-types" />
2+
3+
import { describe, expect, test } from "bun:test";
4+
import { Database } from "../../shared/sqlite";
5+
import { closeQuietly } from "../../shared/sqlite-helpers";
6+
import { LATEST_MIGRATION_VERSION, runMigrations } from "./migrations";
7+
import { initializeDatabase } from "./storage-db";
8+
9+
function columnNames(db: Database, table: string): string[] {
10+
return (db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).map(
11+
(column) => column.name,
12+
);
13+
}
14+
15+
describe("migration v39 — external recall snapshot + m[0] marker", () => {
16+
test("adds external recall columns to session_meta on a fresh DB, idempotently", () => {
17+
const db = new Database(":memory:");
18+
try {
19+
initializeDatabase(db);
20+
runMigrations(db);
21+
runMigrations(db);
22+
23+
const columns = columnNames(db, "session_meta");
24+
expect(columns).toContain("external_recall_json");
25+
expect(columns).toContain("external_recall_state");
26+
expect(columns).toContain("external_recall_at");
27+
expect(columns).toContain("cached_m0_external_recall_hash");
28+
expect(
29+
db
30+
.prepare("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
31+
.get(),
32+
).toEqual({ version: LATEST_MIGRATION_VERSION });
33+
} finally {
34+
closeQuietly(db);
35+
}
36+
});
37+
38+
test("external recall columns store and round-trip nulls (default absent state)", () => {
39+
const db = new Database(":memory:");
40+
try {
41+
initializeDatabase(db);
42+
runMigrations(db);
43+
44+
db.prepare("INSERT INTO session_meta (session_id, harness) VALUES (?, ?)").run(
45+
"ses_v39",
46+
"test-harness",
47+
);
48+
const row = db
49+
.prepare(
50+
"SELECT external_recall_json, external_recall_state, external_recall_at, cached_m0_external_recall_hash FROM session_meta WHERE session_id = ?",
51+
)
52+
.get("ses_v39") as Record<string, unknown>;
53+
54+
expect(row.external_recall_json).toBeNull();
55+
expect(row.external_recall_state).toBeNull();
56+
expect(row.external_recall_at).toBeNull();
57+
expect(row.cached_m0_external_recall_hash).toBeNull();
58+
} finally {
59+
closeQuietly(db);
60+
}
61+
});
62+
});

packages/plugin/src/hooks/magic-context/inject-compartments.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,16 @@ function renderExternalItem(item: ExternalRecallSliceItem): string {
14081408
: `- ${escapeXmlContent(item.content)}`;
14091409
}
14101410

1411+
/** Body of the <external-memory> block. INTENTIONALLY merges only the
1412+
* `project` and `global` slices — the `profile` slice is omitted here by
1413+
* design and is rendered separately into the <user-profile> block (see
1414+
* `renderUserProfileBlock(..., externalRecall?.profile ?? [])` in `renderM0`).
1415+
* Duplicating profile lines into <external-memory> would be redundant
1416+
* (the model would see the same recall content twice) and would defeat
1417+
* the user-profile budget trim that lives on that block. Note that the
1418+
* sibling `renderExternalDeltaLines` (m[1] delta path) DOES include the
1419+
* profile slice — there profile lines reconcile into <user-profile> at
1420+
* the next HARD fold, per its own comment. */
14111421
function renderExternalLines(snapshot: ExternalRecallSnapshot): string[] {
14121422
const items: ExternalRecallSliceItem[] = [...snapshot.project, ...snapshot.global];
14131423
const lines: string[] = [];
@@ -1504,6 +1514,11 @@ export function renderM0(args: {
15041514
}): string {
15051515
const sections: string[] = [];
15061516
if (args.projectDocs.length > 0) sections.push(args.projectDocs);
1517+
// The external-recall PROFILE slice is merged HERE (into <user-profile>),
1518+
// not into <external-memory>. `renderExternalLines` deliberately emits
1519+
// only project + global — see its JSDoc for the rationale. Splitting the
1520+
// merge this way lets the user-profile budget trim govern recall-derived
1521+
// profile lines without inflating the sibling <external-memory> block.
15071522
const userProfile = renderUserProfileBlock(
15081523
trimUserMemoriesToBudget(
15091524
args.userProfileBaseline,

packages/plugin/src/tools/ctx-search/tools.ts

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -184,34 +184,24 @@ function createCtxSearchTool(deps: CtxSearchToolDeps): ToolDefinition {
184184
const gitCommitsEnabled =
185185
embeddingSnapshot?.gitCommitEnabled ?? deps.gitCommitsEnabled ?? false;
186186

187-
const results = await unifiedSearch(
188-
deps.db,
189-
searchSessionId,
190-
projectPath,
191-
query,
192-
{
193-
limit: normalizeLimit(args.limit),
194-
memoryEnabled,
195-
embeddingEnabled,
196-
embedQuery: async (text, signal) => {
197-
const result = await embedTextForProject(
198-
projectPath,
199-
text,
200-
signal,
201-
"query",
202-
);
203-
return result?.vector ?? null;
204-
},
205-
isEmbeddingRuntimeEnabled: () => embeddingEnabled === true,
206-
readMessages: deps.readMessages,
207-
maxMessageOrdinal: messageOrdinalCutoff,
208-
gitCommitsEnabled,
209-
sources: normalizeSources(args.sources),
210-
visibleMemoryIds,
211-
// Explicit agent search → enable literal-probe multi-query
212-
// recall for symbol/command/path lookups. Auto-search hints
213-
// (the hot path) leave this off to protect their latency.
214-
explicitSearch: true,
187+
const results = await unifiedSearch(deps.db, searchSessionId, projectPath, query, {
188+
limit: normalizeLimit(args.limit),
189+
memoryEnabled,
190+
embeddingEnabled,
191+
embedQuery: async (text, signal) => {
192+
const result = await embedTextForProject(projectPath, text, signal, "query");
193+
return result?.vector ?? null;
194+
},
195+
isEmbeddingRuntimeEnabled: () => embeddingEnabled === true,
196+
readMessages: deps.readMessages,
197+
maxMessageOrdinal: messageOrdinalCutoff,
198+
gitCommitsEnabled,
199+
sources: normalizeSources(args.sources),
200+
visibleMemoryIds,
201+
// Explicit agent search → enable literal-probe multi-query
202+
// recall for symbol/command/path lookups. Auto-search hints
203+
// (the hot path) leave this off to protect their latency.
204+
explicitSearch: true,
215205
// External bank resolution: basename is the human-readable label the
216206
// engine uses as a bank template parameter, NOT a key. Project
217207
// identity (resolveProjectPath's output) is the key.

0 commit comments

Comments
 (0)