Skip to content

Commit 866812a

Browse files
committed
fix: prevent cache busts from note nudge injection and sticky reminder clearing
- Note nudges now append to latest user message instead of depending on assistant nudge placements (which don't exist at low context usage) - Sticky reminder clearing gated behind isCacheBustingPass so defer passes don't remove anchored reminder text from cached user messages - Removed dead appendNudgeToLastAssistant (assistant-target footgun) - Split sticky reminder test into defer (keeps) and execute (clears)
1 parent 73ffae5 commit 866812a

2 files changed

Lines changed: 69 additions & 25 deletions

File tree

src/hooks/magic-context/transform-nudge-cache.test.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -491,14 +491,68 @@ describe("createTransform nudge cache handling", () => {
491491
});
492492
});
493493

494-
it("clears sticky turn reminder when messages contain a recent ctx_reduce call", async () => {
495-
//#given
496-
useTempDataHome("context-transform-sticky-suppress-by-message-");
497-
const scheduler: Scheduler = { shouldExecute: mock(() => "defer" as const) };
494+
it("keeps sticky turn reminder on cache-safe defer pass even when ctx_reduce is in recent messages", async () => {
495+
//#given — scheduler defers, so this is a cache-safe pass
496+
useTempDataHome("context-transform-sticky-keep-on-defer-");
498497
const db = openDatabase();
499498
const transform = createTransform({
500499
tagger: createTagger(),
501-
scheduler,
500+
scheduler: { shouldExecute: mock(() => "defer" as const) },
501+
contextUsageMap: new Map<string, { usage: ContextUsage; updatedAt: number }>([
502+
[
503+
"ses-1",
504+
{ usage: { percentage: 41, inputTokens: 80_000 }, updatedAt: Date.now() },
505+
],
506+
]),
507+
nudger: () => null,
508+
db,
509+
nudgePlacements: createNudgePlacementStore(db),
510+
flushedSessions: new Set<string>(),
511+
lastHeuristicsTurnId: new Map<string, string>(),
512+
clearReasoningAge: 50,
513+
protectedTags: 0,
514+
autoDropToolAge: 1000,
515+
});
516+
517+
setPersistedStickyTurnReminder(
518+
db,
519+
"ses-1",
520+
'\n\n<instruction name="ctx_reduce_turn_cleanup">sticky reminder</instruction>',
521+
);
522+
523+
const messages: TestMessage[] = [
524+
{
525+
info: { id: "m-user", role: "user", sessionID: "ses-1" },
526+
parts: [{ type: "text", text: "user prompt" }],
527+
},
528+
{
529+
info: { id: "m-assistant", role: "assistant" },
530+
parts: [
531+
{ type: "text", text: "assistant response" },
532+
{
533+
type: "tool-invocation" as "text",
534+
callID: "reduce-call" as unknown as undefined,
535+
toolName: "ctx_reduce",
536+
} as unknown as TestMessage["parts"][0],
537+
],
538+
},
539+
];
540+
541+
//#when
542+
await transform({}, { messages });
543+
544+
//#then — reminder stays in DB and keeps being reinjected (cache-safe: no removal)
545+
expect(firstText(messages[0]!)).toContain("sticky reminder");
546+
expect(getPersistedStickyTurnReminder(db, "ses-1")).not.toBeNull();
547+
});
548+
549+
it("clears sticky turn reminder on cache-busting execute pass when ctx_reduce is in recent messages", async () => {
550+
//#given — scheduler executes, so this pass already busts cache
551+
useTempDataHome("context-transform-sticky-clear-on-execute-");
552+
const db = openDatabase();
553+
const transform = createTransform({
554+
tagger: createTagger(),
555+
scheduler: { shouldExecute: mock(() => "execute" as const) },
502556
contextUsageMap: new Map<string, { usage: ContextUsage; updatedAt: number }>([
503557
[
504558
"ses-1",
@@ -521,7 +575,6 @@ describe("createTransform nudge cache handling", () => {
521575
'\n\n<instruction name="ctx_reduce_turn_cleanup">sticky reminder</instruction>',
522576
);
523577

524-
// Messages include a ctx_reduce tool call — agent already reduced
525578
const messages: TestMessage[] = [
526579
{
527580
info: { id: "m-user", role: "user", sessionID: "ses-1" },
@@ -543,7 +596,7 @@ describe("createTransform nudge cache handling", () => {
543596
//#when
544597
await transform({}, { messages });
545598

546-
//#then — reminder should be cleared from DB and NOT injected
599+
//#then — reminder cleared from DB and NOT injected (cache already busting)
547600
expect(firstText(messages[0]!)).not.toContain("sticky reminder");
548601
expect(getPersistedStickyTurnReminder(db, "ses-1")).toBeNull();
549602
});

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

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ import {
1919
renderCompartmentInjection,
2020
} from "./inject-compartments";
2121
import { getNoteNudgeText } from "./note-nudger";
22-
import {
23-
appendSupplementalNudgeToAssistant,
24-
canAppendSupplementalNudgeToAssistant,
25-
} from "./nudge-injection";
2622
import type { NudgePlacementStore } from "./nudge-placement-store";
2723
import type { ContextNudge } from "./nudger";
2824
import {
@@ -293,11 +289,15 @@ export function runPostTransformPhase(args: RunPostTransformPhaseArgs): void {
293289

294290
const pendingUserTurnReminder = getPersistedStickyTurnReminder(args.db, args.sessionId);
295291
if (pendingUserTurnReminder) {
296-
if (args.hasRecentReduceCall) {
292+
// Only clear the reminder when the pass is already cache-busting (execute/flush).
293+
// Clearing on a cache-safe pass would remove text from an anchored user message,
294+
// changing cached content and busting the Anthropic prompt-cache prefix.
295+
const isCacheBustingPass = isExplicitFlush || shouldApplyPendingOps || shouldRunHeuristics;
296+
if (args.hasRecentReduceCall && isCacheBustingPass) {
297297
clearPersistedStickyTurnReminder(args.db, args.sessionId);
298298
sessionLog(
299299
args.sessionId,
300-
"sticky turn reminder cleared — ctx_reduce found in recent messages",
300+
"sticky turn reminder cleared — ctx_reduce found in recent messages (cache-busting pass)",
301301
);
302302
} else {
303303
if (pendingUserTurnReminder.messageId) {
@@ -355,19 +355,10 @@ export function runPostTransformPhase(args: RunPostTransformPhaseArgs): void {
355355
args.nudgePlacements.clear(args.sessionId);
356356
}
357357

358-
const canInjectDeferredNoteNudge = canAppendSupplementalNudgeToAssistant(
359-
args.messages,
360-
args.nudgePlacements,
361-
args.sessionId,
362-
);
363358
const deferredNoteText = getNoteNudgeText(args.db, args.sessionId);
364-
if (deferredNoteText && canInjectDeferredNoteNudge) {
365-
appendSupplementalNudgeToAssistant(
366-
args.messages,
367-
`\n\n<instruction name="deferred_notes">${deferredNoteText}</instruction>`,
368-
args.nudgePlacements,
369-
args.sessionId,
370-
);
359+
if (deferredNoteText) {
360+
const noteInstruction = `\n\n<instruction name="deferred_notes">${deferredNoteText}</instruction>`;
361+
appendReminderToLatestUserMessage(args.messages, noteInstruction);
371362
}
372363
} else {
373364
args.nudgePlacements.clear(args.sessionId);

0 commit comments

Comments
 (0)