Skip to content
Open
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
65 changes: 65 additions & 0 deletions packages/pi-plugin/src/clone-inheritance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import {
getSourceContents,
getTagsBySession,
} from "@magic-context/core/features/magic-context/storage";
import {
addNativeReasoningIds,
getNativeReasoningIds,
getNativeToolInputs,
saveNativeToolInputs,
} from "@magic-context/core/features/magic-context/storage-native-replay";
import { replayCavemanCompression } from "@magic-context/core/hooks/magic-context/caveman-cleanup";
import type { TagTarget } from "@magic-context/core/hooks/magic-context/tag-messages";
import type { Database } from "@magic-context/core/shared/sqlite";
Expand Down Expand Up @@ -648,6 +654,65 @@ describe("Pi clone state inheritance", () => {
expect(JSON.parse(row.images)).toEqual(["u1"]);
});

it("carries only retained native tool and reasoning decisions into a remapped clone", () => {
const database = db();
const retainedInput =
'{"path":"src/retained.ts","range":{"start":10,"end":40},"marker":"[truncated]"}';
seedTag(database, {
tagNumber: 1,
messageId: "call-retained",
type: "tool",
ownerId: "assistant-retained",
});
seedTag(database, {
tagNumber: 2,
messageId: "call-outside",
type: "tool",
ownerId: "assistant-outside",
});
saveNativeToolInputs(
database,
"source",
new Map([
["call-retained", retainedInput],
["call-outside", '{"path":"src/outside.ts"}'],
]),
);
addNativeReasoningIds(database, "source", [
"assistant-retained",
"assistant-outside",
]);

const filter: CloneSessionStateFilter = {
resolveBoundaryOrdinal: () => undefined,
includeTag: (tag) =>
tag.type === "tool" && tag.toolOwnerMessageId === "assistant-retained",
includeMessageId: (id) => id === "assistant-retained",
mapMessageId: (id) =>
id === "assistant-retained"
? "clone-assistant-retained"
: id === "call-retained"
? "clone-call-retained"
: id,
selectPendingPiMarker: () => null,
};

const result = copySessionStateForClone(
database,
"source",
"clone",
filter,
);

expect(result.tagsCopied).toBe(1);
expect(getNativeToolInputs(database, "clone")).toEqual(
new Map([["clone-call-retained", retainedInput]]),
);
expect(getNativeReasoningIds(database, "clone")).toEqual(
new Set(["clone-assistant-retained"]),
);
});

it("leaves every m0/m1 cache field fresh so the first pass hard-materializes", () => {
const database = db();
seedCompartment(database, { sequence: 1, startId: "u1", endId: "a1" });
Expand Down
107 changes: 105 additions & 2 deletions packages/pi-plugin/src/context-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3619,11 +3619,83 @@ describe("registerPiContextHandler", () => {
}
});

it("replays an inline-only watermark after restart without fresh age cleanup", async () => {
const db = createTestDb();
const sessionId = "ses-inline-reasoning-watermark";
try {
const fake = createFakePi();
registerPiContextHandler(fake.pi as never, {
db,
heuristics: { clearReasoningAge: 1 },
scheduler: { executeThresholdPercentage: 80 },
});
let handler = fake.handlers.get("context") as (
event: { messages: never[] },
ctx: never,
) => Promise<{ messages: never[] } | undefined>;
const runPass = async (percent: number, newUser = false) => {
const messages = [
userMessage("first", 1),
assistantMessage(
"Keep <think>stale private thought</think> visible",
2,
),
userMessage("second", 3),
assistantMessage("latest answer", 4),
];
const entryIds = [
"entry-u1",
"entry-inline",
"entry-u2",
"entry-latest",
];
if (newUser) {
messages.push(userMessage("new request", 5));
entryIds.push("entry-new");
}
const result = await handler({ messages: messages as never[] }, {
...fakeContext(sessionId, process.cwd(), entryIds, messages as never),
getContextUsage: () => ({
tokens: percent * 1_000,
percent,
contextWindow: 100_000,
}),
} as never);
if (!result) throw new Error("expected transformed messages");
return result.messages;
};

const executed = await runPass(90);
expect(textOf(executed[1])).toContain("Keep visible");
expect(textOf(executed[1])).not.toContain("stale private thought");
updateSessionMeta(db, sessionId, {
lastResponseTime: Date.now(),
cacheTtl: "59m",
lastContextPercentage: 1,
lastInputTokens: 1_000,
});
clearContextHandlerSession(sessionId);
registerPiContextHandler(fake.pi as never, {
db,
heuristics: { clearReasoningAge: 100 },
scheduler: { executeThresholdPercentage: 80 },
});
handler = fake.handlers.get("context") as typeof handler;
const replayed = await runPass(1, true);
expect(textOf(replayed[1])).toBe(textOf(executed[1]));
} finally {
clearContextHandlerSession(sessionId);
closeQuietly(db);
}
});

it("restores reasoning bytes when the durable watermark write fails", async () => {
const db = createTestDb();
const sessionId = "ses-reasoning-watermark-failure";
let watermarkWriteAttempted = false;
const restorePersistence =
contextHandlerInternals.setReasoningWatermarkPersistenceForTests(() => {
watermarkWriteAttempted = true;
throw new Error("faulted reasoning watermark write");
});
try {
Expand All @@ -3642,6 +3714,16 @@ describe("registerPiContextHandler", () => {
{
role: "assistant",
timestamp: 2,
providerPayload: {
type: "openaiResponsesHistory",
dt: true,
items: [
{
type: "reasoning",
encrypted_content: "durable native reasoning",
},
],
},
content: [
{
type: "thinking",
Expand All @@ -3664,23 +3746,44 @@ describe("registerPiContextHandler", () => {
messages as never,
),
getContextUsage: () => ({
tokens: 70_000,
percent: 70,
tokens: 90_000,
percent: 90,
contextWindow: 100_000,
}),
model: {
id: "test-codex",
api: "openai-codex-responses",
provider: "openai-codex",
contextWindow: 100_000,
compat: {
requiresReasoningContentForAllAssistantTurns: false,
requiresReasoningContentForToolCalls: false,
},
},
} as never);
if (!result) throw new Error("expected transformed messages");
return result.messages;
};

const first = await runPass();
const second = await runPass();
expect(watermarkWriteAttempted).toBe(true);
const firstThinking = (first[1] as { content: Record<string, unknown>[] })
.content[0];
expect(firstThinking).toMatchObject({
thinking: "durable secret",
thinkingSignature: "sig",
});
expect(first[1]).toMatchObject({
providerPayload: {
items: [
{
type: "reasoning",
encrypted_content: "durable native reasoning",
},
],
},
});
expect(JSON.stringify(second)).toBe(JSON.stringify(first));
expect(
getOrCreateSessionMeta(db, sessionId).clearedReasoningThroughTag,
Expand Down
66 changes: 65 additions & 1 deletion packages/pi-plugin/src/context-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ import {
pruneAutoSearchHintDecisions,
pruneNoteNudgeAnchors,
} from "@magic-context/core/features/magic-context/storage-meta-persisted";
import {
getNativeReasoningIds,
getNativeToolInputs,
} from "@magic-context/core/features/magic-context/storage-native-replay";
import { getSourceContents } from "@magic-context/core/features/magic-context/storage-source";
import {
createTagger,
Expand Down Expand Up @@ -223,6 +227,11 @@ import {
prepareCachedM0M1PiReplay,
trimPiMessagesToCachedBoundary,
} from "./inject-compartments-pi";
import { canClearNativeReasoning } from "./native-replay-pi";
import {
applyNativeReasoningReplayPi,
applyNativeToolInputReplayPi,
} from "./native-replay-state-pi";
import { hasVisibleNoteReadCallPi } from "./note-visibility-pi";
import {
resolvePiUsableContextLimit,
Expand Down Expand Up @@ -3086,6 +3095,7 @@ export function registerPiContextHandler(
clearReasoningAge:
options.heuristics?.clearReasoningAge ??
DEFAULT_CLEAR_REASONING_AGE,
nativeReasoningMayClear: canClearNativeReasoning(ctx.model),
},
canUseEmptySentinels,
temporalAwareness: options.injection?.temporalAwareness === true,
Expand Down Expand Up @@ -4529,6 +4539,7 @@ interface RunPipelineArgs {
*/
reasoningClearing?: {
clearReasoningAge: number;
nativeReasoningMayClear: boolean;
};
/** True only when the active provider filters empty sentinel content safely. */
canUseEmptySentinels: boolean;
Expand Down Expand Up @@ -4631,7 +4642,10 @@ function captureReasoningMutationRollback(
}> = [];
for (const raw of messages) {
if (!raw || typeof raw !== "object") continue;
const message = raw as { role?: unknown; content?: unknown };
const message = raw as {
role?: unknown;
content?: unknown;
};
if (message.role !== "assistant" || !Array.isArray(message.content))
continue;
for (const rawPart of message.content) {
Expand Down Expand Up @@ -5642,6 +5656,7 @@ async function runPipeline(args: RunPipelineArgs): Promise<RunPipelineResult> {
// materialization passes where heuristics DO run — leaving reasoning on the
// wire on a pass that already dropped tools (inconsistent + a missed
// same-pass mutation). shouldRunHeuristics is the broader, correct set.
let reasoningPersistenceFailed = false;
if (args.reasoningClearing && shouldRunHeuristics && routineCleanupApplied) {
const rollbackReasoning = captureReasoningMutationRollback(workingMessages);
try {
Expand Down Expand Up @@ -5687,6 +5702,7 @@ async function runPipeline(args: RunPipelineArgs): Promise<RunPipelineResult> {
executedWorkThisPass = true;
}
} catch (err) {
reasoningPersistenceFailed = true;
// Never ship cleared reasoning unless replay state persisted. Restoring the
// pre-cleanup parts keeps this pass byte-stable and lets the next execute
// pass retry the watermark write.
Expand Down Expand Up @@ -5816,6 +5832,54 @@ async function runPipeline(args: RunPipelineArgs): Promise<RunPipelineResult> {
const tTranscriptCommit = performance.now();
transcript.commit();
logTransformTiming(args.sessionId, "transcriptCommit", tTranscriptCommit);

// Legacy drop/watermark state does not authorize first native activation.
// Use committed canonical inputs, then publish only persisted native decisions.
let nativeInputs: ReadonlyMap<string, string> | undefined;
let nativeReasoningIds: ReadonlySet<string> | undefined;
try {
// Validate both lanes before either can publish replay or activation.
nativeInputs = getNativeToolInputs(args.db, args.sessionId);
nativeReasoningIds = getNativeReasoningIds(args.db, args.sessionId);
} catch (error) {
sessionLog(
args.sessionId,
`native replay state unavailable; retaining native history (continuing): ${error instanceof Error ? error.message : String(error)}`,
);
}
if (nativeInputs !== undefined && nativeReasoningIds !== undefined) {
const nativeInputsApplied = applyNativeToolInputReplayPi(
{
db: args.db,
sessionId: args.sessionId,
messages: args.messages,
changes: transcript.getToolInputChanges(),
canApply: isCacheBustingPass,
},
nativeInputs,
);
const nativeReasoningApplied = args.reasoningClearing
? applyNativeReasoningReplayPi(
{
db: args.db,
sessionId: args.sessionId,
messages: args.messages,
messageIdToMaxTag,
stableId: stableIdResolver,
localWatermark: args.sessionMeta.clearedReasoningThroughTag ?? 0,
clearReasoningAge: args.reasoningClearing.clearReasoningAge,
omissionAllowed: args.reasoningClearing.nativeReasoningMayClear,
canApply: isCacheBustingPass && !reasoningPersistenceFailed,
detectAged: shouldRunHeuristics && routineCleanupApplied,
},
nativeReasoningIds,
)
: 0;
if (nativeInputsApplied > 0 || nativeReasoningApplied > 0) {
heuristicOrReasoningDidMutate = true;
executedWorkThisPass = true;
}
}
if (toolReclaimApplicationOpportunity) {
advanceToolReclaimWatermarkToCurrentMax(args.db, args.sessionId);
}
Expand Down
Loading