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
27 changes: 13 additions & 14 deletions packages/opencode/src/session/checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
"<system-reminder>",
"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.",
"</system-reminder>",
].join("\n")
}
Expand Down Expand Up @@ -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":
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/test/session/checkpoint-rebuild-v3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}),
),
Expand Down
56 changes: 56 additions & 0 deletions packages/opencode/test/session/checkpoint-render-verify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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* () {
Expand Down
Loading