Skip to content

Commit 75d19f9

Browse files
committed
feat(plugin): replay current todos sticky block
1 parent 3095cf7 commit 75d19f9

2 files changed

Lines changed: 251 additions & 0 deletions

File tree

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {
22
type ContextDatabase,
33
clearPersistedStickyTurnReminder,
4+
clearPersistedTodoBlock,
45
getPendingOps,
56
getPersistedStickyTurnReminder,
7+
getPersistedTodoBlock,
68
getStrippedPlaceholderIds,
79
getTopNBySize,
810
setPersistedStickyTurnReminder,
11+
setPersistedTodoBlock,
912
setStrippedPlaceholderIds,
1013
updateSessionMeta,
1114
} from "../../features/magic-context/storage";
@@ -52,6 +55,7 @@ import {
5255
type TagTarget,
5356
truncateErroredTools,
5457
} from "./transform-operations";
58+
import { renderTodoBlock } from "./todo-view";
5559
import { logTransformTiming } from "./transform-stage-logger";
5660

5761
interface RunPostTransformPhaseArgs {
@@ -705,6 +709,51 @@ export async function runPostTransformPhase(args: RunPostTransformPhaseArgs): Pr
705709
markNoteNudgeDelivered(args.db, args.sessionId, noteInstruction, anchoredMessageId);
706710
}
707711

712+
// Todo state synthesis mirrors sticky replay above, but only re-renders
713+
// from the raw snapshot on cache-busting passes. Defer passes replay the
714+
// persisted text byte-for-byte at the same anchor to preserve cache stability.
715+
if (args.fullFeatureMode) {
716+
const stickyTodoBlock = getPersistedTodoBlock(args.db, args.sessionId);
717+
if (isCacheBustingPass) {
718+
const todoBlock = renderTodoBlock(args.sessionMeta.lastTodoState);
719+
if (todoBlock === null) {
720+
if (stickyTodoBlock) {
721+
clearPersistedTodoBlock(args.db, args.sessionId);
722+
}
723+
} else if (stickyTodoBlock?.text === todoBlock) {
724+
const replayed = appendReminderToUserMessageById(
725+
args.messages,
726+
stickyTodoBlock.messageId,
727+
stickyTodoBlock.text,
728+
);
729+
if (!replayed) {
730+
const anchoredMessageId = appendReminderToLatestUserMessage(
731+
args.messages,
732+
todoBlock,
733+
);
734+
if (anchoredMessageId) {
735+
setPersistedTodoBlock(args.db, args.sessionId, todoBlock, anchoredMessageId);
736+
} else {
737+
clearPersistedTodoBlock(args.db, args.sessionId);
738+
}
739+
}
740+
} else {
741+
const anchoredMessageId = appendReminderToLatestUserMessage(args.messages, todoBlock);
742+
if (anchoredMessageId) {
743+
setPersistedTodoBlock(args.db, args.sessionId, todoBlock, anchoredMessageId);
744+
} else {
745+
clearPersistedTodoBlock(args.db, args.sessionId);
746+
}
747+
}
748+
} else if (stickyTodoBlock) {
749+
appendReminderToUserMessageById(
750+
args.messages,
751+
stickyTodoBlock.messageId,
752+
stickyTodoBlock.text,
753+
);
754+
}
755+
}
756+
708757
// Auto-search hint — append a vague-recall fragment hint to the latest
709758
// user message when experimental.auto_search is enabled and search
710759
// returns a high-confidence match. Gated behind fullFeatureMode: subagent
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/// <reference types="bun-types" />
2+
3+
import { afterEach, describe, expect, it, mock } from "bun:test";
4+
import { mkdtempSync, rmSync } from "node:fs";
5+
import { tmpdir } from "node:os";
6+
import { join } from "node:path";
7+
import type { Scheduler } from "../../features/magic-context/scheduler";
8+
import {
9+
closeDatabase,
10+
getPersistedTodoBlock,
11+
openDatabase,
12+
setPersistedTodoBlock,
13+
updateSessionMeta,
14+
} from "../../features/magic-context/storage";
15+
import { createTagger } from "../../features/magic-context/tagger";
16+
import type { ContextUsage } from "../../features/magic-context/types";
17+
import { stripTagPrefix } from "./tag-content-primitives";
18+
import { createNudgePlacementStore, createTransform } from "./transform";
19+
20+
type TestMessage = {
21+
info: { id: string; role: string; sessionID?: string };
22+
parts: Array<{ type: "text"; text: string }>;
23+
};
24+
25+
const TODO_BLOCK =
26+
"\n\n<current-todos>\n- [in_progress] Implement todo synthesis\n- [pending] Review tests\n</current-todos>";
27+
28+
const tempDirs: string[] = [];
29+
const originalXdgDataHome = process.env.XDG_DATA_HOME;
30+
31+
afterEach(() => {
32+
closeDatabase();
33+
process.env.XDG_DATA_HOME = originalXdgDataHome;
34+
for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true });
35+
tempDirs.length = 0;
36+
});
37+
38+
function useTempDataHome(prefix: string): void {
39+
const dir = mkdtempSync(join(tmpdir(), prefix));
40+
tempDirs.push(dir);
41+
process.env.XDG_DATA_HOME = dir;
42+
}
43+
44+
function firstText(message: TestMessage): string {
45+
return stripTagPrefix(message.parts[0]?.text ?? "");
46+
}
47+
48+
function makeMessages(): TestMessage[] {
49+
return [
50+
{
51+
info: { id: "m-user", role: "user", sessionID: "ses-1" },
52+
parts: [{ type: "text", text: "user prompt" }],
53+
},
54+
{
55+
info: { id: "m-assistant", role: "assistant" },
56+
parts: [{ type: "text", text: "assistant response" }],
57+
},
58+
];
59+
}
60+
61+
function makeTwoTurnMessages(): TestMessage[] {
62+
return [
63+
{
64+
info: { id: "m-user-1", role: "user", sessionID: "ses-1" },
65+
parts: [{ type: "text", text: "first user prompt" }],
66+
},
67+
{
68+
info: { id: "m-assistant-1", role: "assistant" },
69+
parts: [{ type: "text", text: "assistant response" }],
70+
},
71+
{
72+
info: { id: "m-user-2", role: "user", sessionID: "ses-1" },
73+
parts: [{ type: "text", text: "second user prompt" }],
74+
},
75+
];
76+
}
77+
78+
function createTodoTransform(scheduler: Scheduler) {
79+
const db = openDatabase();
80+
const transform = createTransform({
81+
tagger: createTagger(),
82+
scheduler,
83+
contextUsageMap: new Map<string, { usage: ContextUsage; updatedAt: number }>([
84+
["ses-1", { usage: { percentage: 41, inputTokens: 80_000 }, updatedAt: Date.now() }],
85+
]),
86+
nudger: () => null,
87+
db,
88+
nudgePlacements: createNudgePlacementStore(db),
89+
historyRefreshSessions: new Set<string>(),
90+
pendingMaterializationSessions: new Set<string>(),
91+
lastHeuristicsTurnId: new Map<string, string>(),
92+
clearReasoningAge: 50,
93+
protectedTags: 0,
94+
autoDropToolAge: 1000,
95+
});
96+
return { db, transform };
97+
}
98+
99+
describe("todo state synthesis transform", () => {
100+
it("appends rendered todo block on cache-busting execute pass", async () => {
101+
useTempDataHome("context-transform-todo-execute-");
102+
const { db, transform } = createTodoTransform({ shouldExecute: mock(() => "execute") });
103+
updateSessionMeta(db, "ses-1", {
104+
lastTodoState: JSON.stringify([
105+
{ content: "Implement todo synthesis", status: "in_progress", priority: "high" },
106+
{ content: "Review tests", status: "pending", priority: "medium" },
107+
]),
108+
});
109+
const messages = makeMessages();
110+
111+
await transform({}, { messages });
112+
113+
expect(firstText(messages[0]!)).toBe(`user prompt${TODO_BLOCK}`);
114+
expect(getPersistedTodoBlock(db, "ses-1")).toEqual({ text: TODO_BLOCK, messageId: "m-user" });
115+
});
116+
117+
it("replays persisted todo block byte-identically on defer pass", async () => {
118+
useTempDataHome("context-transform-todo-defer-");
119+
const { db, transform } = createTodoTransform({ shouldExecute: mock(() => "defer") });
120+
setPersistedTodoBlock(db, "ses-1", TODO_BLOCK, "m-user");
121+
const messages = makeMessages();
122+
123+
await transform({}, { messages });
124+
125+
expect(firstText(messages[0]!)).toBe(`user prompt${TODO_BLOCK}`);
126+
expect(getPersistedTodoBlock(db, "ses-1")).toEqual({ text: TODO_BLOCK, messageId: "m-user" });
127+
});
128+
129+
it("re-renders and re-anchors when todo state changes on execute pass", async () => {
130+
useTempDataHome("context-transform-todo-state-change-");
131+
const { db, transform } = createTodoTransform({ shouldExecute: mock(() => "execute") });
132+
setPersistedTodoBlock(
133+
db,
134+
"ses-1",
135+
"\n\n<current-todos>\n- [pending] Old\n</current-todos>",
136+
"m-user-1",
137+
);
138+
updateSessionMeta(db, "ses-1", {
139+
lastTodoState: JSON.stringify([{ content: "New", status: "pending", priority: "high" }]),
140+
});
141+
const messages = makeTwoTurnMessages();
142+
143+
await transform({}, { messages });
144+
145+
expect(firstText(messages[0]!)).not.toContain("<current-todos>");
146+
expect(firstText(messages[2]!)).toBe(
147+
"second user prompt\n\n<current-todos>\n- [pending] New\n</current-todos>",
148+
);
149+
expect(getPersistedTodoBlock(db, "ses-1")?.messageId).toBe("m-user-2");
150+
});
151+
152+
it("keeps unchanged todo block anchored to the original user message", async () => {
153+
useTempDataHome("context-transform-todo-anchor-stable-");
154+
const { db, transform } = createTodoTransform({ shouldExecute: mock(() => "execute") });
155+
setPersistedTodoBlock(db, "ses-1", TODO_BLOCK, "m-user-1");
156+
updateSessionMeta(db, "ses-1", {
157+
lastTodoState: JSON.stringify([
158+
{ content: "Implement todo synthesis", status: "in_progress", priority: "high" },
159+
{ content: "Review tests", status: "pending", priority: "medium" },
160+
]),
161+
});
162+
const messages = makeTwoTurnMessages();
163+
164+
await transform({}, { messages });
165+
166+
expect(firstText(messages[0]!)).toBe(`first user prompt${TODO_BLOCK}`);
167+
expect(firstText(messages[2]!)).toBe("second user prompt");
168+
expect(getPersistedTodoBlock(db, "ses-1")).toEqual({ text: TODO_BLOCK, messageId: "m-user-1" });
169+
});
170+
171+
it("clears sticky todo block when current state is empty", async () => {
172+
useTempDataHome("context-transform-todo-clear-");
173+
const { db, transform } = createTodoTransform({ shouldExecute: mock(() => "execute") });
174+
setPersistedTodoBlock(db, "ses-1", TODO_BLOCK, "m-user");
175+
updateSessionMeta(db, "ses-1", {
176+
lastTodoState: JSON.stringify([{ content: "Done", status: "completed", priority: "high" }]),
177+
});
178+
const messages = makeMessages();
179+
180+
await transform({}, { messages });
181+
182+
expect(firstText(messages[0]!)).toBe("user prompt");
183+
expect(getPersistedTodoBlock(db, "ses-1")).toBeNull();
184+
});
185+
186+
it("skips todo synthesis for subagent sessions", async () => {
187+
useTempDataHome("context-transform-todo-subagent-");
188+
const { db, transform } = createTodoTransform({ shouldExecute: mock(() => "execute") });
189+
updateSessionMeta(db, "ses-1", {
190+
isSubagent: true,
191+
lastTodoState: JSON.stringify([
192+
{ content: "Subagent work", status: "pending", priority: "high" },
193+
]),
194+
});
195+
const messages = makeMessages();
196+
197+
await transform({}, { messages });
198+
199+
expect(firstText(messages[0]!)).toBe("user prompt");
200+
expect(getPersistedTodoBlock(db, "ses-1")).toBeNull();
201+
});
202+
});

0 commit comments

Comments
 (0)