Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 32 additions & 0 deletions packages/plugin/src/hooks/magic-context/degraded-reanchor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,38 @@ describe("Layer B — degraded-mode re-anchor (#264)", () => {
expect(pass2Messages[1].info.id).toBe("msg_y");
});

it("does not inherit another database's degraded count for the same session id", () => {
// The degraded count gates a byte-CHANGING re-anchor. If it leaks across
// stores sharing a session id, store B re-anchors on its FIRST degraded
// pass because it inherited store A's episode.
const makeVisible = (): MessageLike[] => [
userMessage("msg_c1_end", "compartment one end"),
userMessage("msg_x", "x"),
];

// Store A: one degraded bust pass (count = 1, below the threshold).
seedTwoCompartments();
const storeAPass = prepareCompartmentInjection(db, SESSION_ID, makeVisible(), true);
expect(storeAPass?.compartmentEndMessageId).toBeNull();

// Store B: independent database, same session id, its FIRST degraded pass.
const storeA = db;
const storeB = makeContextDb();
try {
db = storeB;
seedTwoCompartments();
const messages = makeVisible();
const storeBPass = prepareCompartmentInjection(storeB, SESSION_ID, messages, true);
// Still pass 1 for THIS store: no re-anchor, nothing spliced.
expect(storeBPass?.compartmentEndMessageId).toBeNull();
expect(storeBPass?.skippedVisibleMessages).toBe(0);
expect(messages.length).toBe(2);
} finally {
db = storeA;
closeQuietly(storeB);
}
});

it("does NOT re-anchor before the degraded-pass threshold", () => {
seedTwoCompartments();
const makeVisible = (): MessageLike[] => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,41 @@ describe("prepareCompartmentInjection — empty compartments fallback", () => {
});
});

describe("prepareCompartmentInjection — cross-database cache isolation", () => {
it("does not replay a block rendered from a different database", () => {
// The injection cache is process-global and keyed by session id alone,
// while the value it holds is rendered FROM a database. Two independent
// stores that share a session id must not see each other's blocks.
const first = makeDb();
try {
insertMemory(first, {
projectPath: PROJECT_PATH,
category: "CONSTRAINTS",
content: "MEMORY-ONLY-IN-FIRST-DATABASE",
});
const populated = prepareCompartmentInjection(
first,
SESSION_ID,
[userMessage("m1", "hi")],
true,
PROJECT_PATH,
);
expect(populated?.block).toContain("MEMORY-ONLY-IN-FIRST-DATABASE");
} finally {
closeQuietly(first);
}

// Second store: same session id, no memories, and a DEFER pass — the
// path that replays the cached injection.
db = makeDb();
const messages: MessageLike[] = [userMessage("m1", "hi")];
const replayed = prepareCompartmentInjection(db, SESSION_ID, messages, false, PROJECT_PATH);

expect(replayed?.block ?? "").not.toContain("MEMORY-ONLY-IN-FIRST-DATABASE");
expect(replayed).toBeNull();
});
});

describe("prepareCompartmentInjection — workspace memory sharing", () => {
it("renders only explicitly shared foreign memory categories", () => {
db = makeDb();
Expand Down
30 changes: 22 additions & 8 deletions packages/plugin/src/hooks/magic-context/inject-compartments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ export interface PreparedCompartmentInjection {
*/
const INJECTION_CACHE_MAX = 100;
type InjectionCacheEntry =
| { kind: "empty"; compartmentEndMessageId: string; renderedBytes: number }
| { kind: "populated"; injection: PreparedCompartmentInjection };
| { db: Database; kind: "empty"; compartmentEndMessageId: string; renderedBytes: number }
| { db: Database; kind: "populated"; injection: PreparedCompartmentInjection };

const injectionCache = new BoundedSessionMap<InjectionCacheEntry>(INJECTION_CACHE_MAX);

Expand Down Expand Up @@ -319,11 +319,24 @@ export function prepareCompartmentInjection(
// On defer (cache-safe) passes, replay the cached injection result so that
// historian publications between passes do not bust the prompt-cache prefix.
const cached = injectionCache.get(sessionId);
if (!isCacheBusting && cached) {
if (cached.kind === "empty") {
if (cached && cached.db !== db) {
// Session ids are unique in production, but tests and explicit database
// paths can reuse one across independent stores. Never replay a block
// rendered from a different database into the current session.
//
// clearInjectionCache (not a bare delete) so the degraded-mode re-anchor
// bookkeeping is dropped with it: that count gates a byte-CHANGING
// re-anchor, so inheriting another store's episode could re-anchor early
// — the same cross-store leak, on the path where it costs more.
clearInjectionCache(sessionId);
}
const usableCached = cached?.db === db ? cached : undefined;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

if (!isCacheBusting && usableCached) {
if (usableCached.kind === "empty") {
return null;
}
const prepared = cached.injection;
const prepared = usableCached.injection;
if (prepared.compartmentEndMessageId === null) {
sessionLog(
sessionId,
Expand Down Expand Up @@ -418,6 +431,7 @@ export function prepareCompartmentInjection(
// Nothing to inject if we have no compartments, no facts, and no memories
if (compartments.length === 0 && facts.length === 0 && !memoryBlock) {
injectionCache.set(sessionId, {
db,
kind: "empty",
compartmentEndMessageId: "",
renderedBytes: 0,
Expand Down Expand Up @@ -462,7 +476,7 @@ export function prepareCompartmentInjection(
memoryCount,
rebuiltFromDb: true,
};
injectionCache.set(sessionId, { kind: "populated", injection: result });
injectionCache.set(sessionId, { db, kind: "populated", injection: result });
return result;
}

Expand Down Expand Up @@ -527,7 +541,7 @@ export function prepareCompartmentInjection(
memoryCount,
rebuiltFromDb: true,
};
injectionCache.set(sessionId, { kind: "populated", injection: result });
injectionCache.set(sessionId, { db, kind: "populated", injection: result });
return result;
}

Expand Down Expand Up @@ -614,7 +628,7 @@ export function prepareCompartmentInjection(
if (needsFreshMaterialization) {
result.needsFreshMaterialization = true;
}
injectionCache.set(sessionId, { kind: "populated", injection: result });
injectionCache.set(sessionId, { db, kind: "populated", injection: result });
return result;
}

Expand Down
Loading