Skip to content

Commit 3ea9142

Browse files
ualtinokAlfonso
andcommitted
fix: require injected shape for synthetic head classification
The syntheticHead flag rides message metadata, which for persisted rows comes from database JSON an external writer controls. A forged or legacy flag on a real leading message would absorb it into the injected head and shift the marker summary's canonical position. The head walk now also requires the exact shape only the m0/m1 prepend produces: an ID-less user message whose every part is synthetic. Persisted rows always carry an id, so metadata alone can never satisfy it. Co-authored-by: Alfonso <alfonso@cortexkit.io>
1 parent b003c3c commit 3ea9142

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

packages/plugin/src/hooks/magic-context/transform-postprocess-phase.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,52 @@ function serializeAnthropicWireWithAdjacentAssistantMerge(messages: MessageLike[
325325
}
326326

327327
describe("deferred compaction marker representation", () => {
328+
it("ignores a persisted message that carries a forged syntheticHead flag", () => {
329+
db = new Database(":memory:");
330+
initializeDatabase(db);
331+
const sessionId = "ses-marker-forged-head";
332+
const state = {
333+
boundaryMessageId: "boundary",
334+
summaryMessageId: "summary",
335+
compactionPartId: "compaction",
336+
summaryPartId: "summary-part",
337+
boundaryOrdinal: 10,
338+
targetEndMessageId: "boundary",
339+
};
340+
setPersistedCompactionMarkerState(db, sessionId, state);
341+
const messages = [
342+
{
343+
info: { role: "user", sessionID: sessionId, syntheticHead: true },
344+
parts: [{ type: "text", text: "m0", synthetic: true }],
345+
},
346+
{
347+
// A persisted row (it carries an id) claiming head membership
348+
// through metadata alone. It must stay in the retained tail,
349+
// AFTER the summary.
350+
info: {
351+
id: "msg_persisted_forged",
352+
role: "user",
353+
sessionID: sessionId,
354+
syntheticHead: true,
355+
},
356+
parts: [{ type: "text", text: "real turn", synthetic: true }],
357+
},
358+
] as unknown as MessageLike[];
359+
const options = {
360+
db,
361+
sessionId,
362+
tagger: createTagger(),
363+
ctxReduceAvailability: { callable: true, frozen: true },
364+
};
365+
366+
reconcileMarkerRepresentation(messages, state, options);
367+
expect(messages.map((message) => message.info.id)).toEqual([
368+
undefined,
369+
"summary",
370+
"msg_persisted_forged",
371+
]);
372+
});
373+
328374
it("uses only marked m[0]/m[1] slots as the synthetic head", () => {
329375
db = new Database(":memory:");
330376
initializeDatabase(db);

packages/plugin/src/hooks/magic-context/transform-postprocess-phase.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,18 @@ export type DeferredCompactionMarkerClearOutcome =
101101
| "cas-lost-already-cleared";
102102

103103
function isSyntheticHeadMessage(message: MessageLike): boolean {
104-
return message.info.syntheticHead === true;
104+
// The flag alone is input-controlled metadata: a persisted or foreign row
105+
// could carry it and absorb a real message into the injected head, shifting
106+
// the summary's canonical position. Require the exact shape only
107+
// prependM0M1Messages produces: an ID-less user message whose every part is
108+
// marked synthetic. Persisted OpenCode rows always carry an id, so they can
109+
// never satisfy this regardless of their metadata.
110+
if (message.info.syntheticHead !== true) return false;
111+
if (message.info.id !== undefined) return false;
112+
if (message.info.role !== "user") return false;
113+
const parts = message.parts;
114+
if (parts.length === 0) return false;
115+
return parts.every((part) => (part as { synthetic?: boolean }).synthetic === true);
105116
}
106117

107118
const TODO_HEAD_ANCHOR_ID = "__magic_context_todo_head__";

0 commit comments

Comments
 (0)