diff --git a/packages/opencode/src/session/checkpoint.ts b/packages/opencode/src/session/checkpoint.ts index b5d34bac4..8f5fe42d3 100644 --- a/packages/opencode/src/session/checkpoint.ts +++ b/packages/opencode/src/session/checkpoint.ts @@ -91,17 +91,13 @@ function autonomousLoopReminder(): string { ].join("\n") } -function stopReminder(focusTaskID: string | undefined): string { - const taskHint = focusTaskID - ? `Consult this session's tasks/${focusTaskID}/progress.md head section.` - : "Consult the most recently active task's progress.md head section." +function idleReminder(): string { return [ "", - "The previous assistant turn ended with a stop. Before stopping again,", - taskHint, - "Compare the Task spec to the latest Progress entries. If the task is", - "incomplete, proceed to the next concrete step. Only stop when the spec", - "is genuinely satisfied or you need user input you cannot infer.", + "The previous assistant turn ended with a stop. If it fulfilled the user's request,", + "do not reopen or re-verify completed work — wait for the user's next input.", + "Only continue if you were genuinely mid-task: consult the most recently active task's", + "progress.md head section and proceed only when a concrete next step is required.", "", ].join("\n") } @@ -1504,21 +1500,24 @@ export const layer: Layer.Layer< "Recent messages are preserved verbatim below — the assistant turn (and any tool results) you'll see is real history, not pseudo-content. Continue your task by responding to the most recent state.", ) lines.push("") + const info = opts?.lastMessageInfo + const done = info?.role === "assistant" && info.finish === "stop" lines.push( - "Resume directly. Do not acknowledge this memory dump, do not recap, do not preface with \"I'll continue\" or similar. Pick up the last task as if the break never happened.", + done + ? "The last assistant turn ended with a stop. If it fulfilled the user's request, do not reopen or re-verify completed work — wait for the user's next input." + : 'Resume directly. Do not acknowledge this memory dump, do not recap, do not preface with "I\'ll continue" or similar. Pick up the last task as if the break never happened.', ) // Section 11: tail-aware system reminder. Picks the appropriate nudge // based on how the preserved tail ends: tool-calls → continue loop, - // stop → check task spec before stopping again, tool → process results, - // user → no addendum needed. - const info = opts?.lastMessageInfo + // stop → idle framing (do not reopen completed work), tool → process + // results, user → no addendum needed. if (info) { const reminder = (() => { switch (info.role) { case "assistant": if (info.finish === "tool-calls") return autonomousLoopReminder() - return stopReminder(undefined) + return idleReminder() case "tool": return toolResultContinueReminder() case "user": diff --git a/packages/opencode/test/session/checkpoint-rebuild-v3.test.ts b/packages/opencode/test/session/checkpoint-rebuild-v3.test.ts index 294db3311..936b0c7ed 100644 --- a/packages/opencode/test/session/checkpoint-rebuild-v3.test.ts +++ b/packages/opencode/test/session/checkpoint-rebuild-v3.test.ts @@ -175,7 +175,7 @@ describe("renderRebuildContext v3", () => { ), ) - it.live("appends stopReminder when lastMessageInfo is assistant+stop", () => + it.live("uses idle framing when lastMessageInfo is assistant+stop", () => provideTmpdirInstance(() => Effect.gen(function* () { const cp = yield* SessionCheckpoint.Service @@ -186,6 +186,7 @@ describe("renderRebuildContext v3", () => { const out = yield* cp.renderRebuildContext(sess.id, { lastMessageInfo: { role: "assistant", finish: "stop" } }) expect(out).toContain("The previous assistant turn ended with a stop") + expect(out).toContain("wait for the user") expect(out).toContain("progress.md head section") }), ), diff --git a/packages/opencode/test/session/checkpoint-render-verify.test.ts b/packages/opencode/test/session/checkpoint-render-verify.test.ts index 475ddc1b8..843e26468 100644 --- a/packages/opencode/test/session/checkpoint-render-verify.test.ts +++ b/packages/opencode/test/session/checkpoint-render-verify.test.ts @@ -292,6 +292,62 @@ Next: implement renderRebuildContext 9-section render in src/session/checkpoint. ), ) + it.live("renderRebuildContext uses idle framing when lastMessageInfo is assistant/stop", () => + provideTmpdirInstance(() => + Effect.gen(function* () { + const cp = yield* SessionCheckpoint.Service + const session = yield* Session.Service + const memory = yield* Memory.Service + + const sess = yield* session.create({ title: "idle framing stop" }) + + const root = yield* memory.root() + const sessDir = path.join(root, "sessions", sess.id) + + yield* Effect.promise(async () => { + await fs.mkdir(sessDir, { recursive: true }) + await fs.writeFile( + path.join(sessDir, "checkpoint.md"), + `Topic: Idle framing fixture\n\n### Execution context\n- minimal seed\n`, + ) + }) + + const out = yield* cp.renderRebuildContext(sess.id, { lastMessageInfo: { role: "assistant", finish: "stop" } }) + + expect(out).not.toContain("Pick up the last task as if the break never happened") + expect(out).toContain("wait for the user") + }), + ), + ) + + it.live("renderRebuildContext keeps resume framing for assistant/tool-calls", () => + provideTmpdirInstance(() => + Effect.gen(function* () { + const cp = yield* SessionCheckpoint.Service + const session = yield* Session.Service + const memory = yield* Memory.Service + + const sess = yield* session.create({ title: "resume framing tool-calls" }) + + const root = yield* memory.root() + const sessDir = path.join(root, "sessions", sess.id) + + yield* Effect.promise(async () => { + await fs.mkdir(sessDir, { recursive: true }) + await fs.writeFile( + path.join(sessDir, "checkpoint.md"), + `Topic: Resume framing fixture\n\n### Execution context\n- minimal seed\n`, + ) + }) + + const out = yield* cp.renderRebuildContext(sess.id, { lastMessageInfo: { role: "assistant", finish: "tool-calls" } }) + + expect(out).toContain("Pick up the last task as if the break never happened") + expect(out).toContain("autonomous task") + }), + ), + ) + it.live("renderRebuildContext omits autonomous addendum when opts undefined", () => provideTmpdirInstance(() => Effect.gen(function* () {