fix: avoid deadlock by not awaiting summarize

The summarize request parks server-side on the current run's done until
the loop executing this tool finishes, so awaiting it never resolves.
Fire it without awaiting, poll for the persisted compaction message, then
wait for the handoff prompt to appear before returning.
This commit is contained in:
emidab 2026-09-16 12:46:47 +00:00
parent f991c4d0f8
commit 2cac9375d8

View file

@ -78,6 +78,24 @@ async function resolveCompactModel(
return { providerID: model.providerID, modelID: model.modelID }
}
const HANDOFF_MARKER = "Work was restarted in this session with a clean context window."
async function waitFor(
client: PluginClient,
sessionID: string,
test: (messages: { info: import("@opencode-ai/sdk").Message; parts: import("@opencode-ai/sdk").Part[] }[]) => boolean,
timeoutMs = 30000,
intervalMs = 250,
) {
const deadline = Date.now() + timeoutMs
while (true) {
const page = await client.session.messages({ path: { id: sessionID } })
if (test(page.data ?? [])) return
if (Date.now() >= deadline) throw new Error(`timed out after ${timeoutMs}ms waiting for the session to change`)
await new Promise((resolve) => setTimeout(resolve, intervalMs))
}
}
export const server: Plugin = async ({ client }, rawOptions = {}) => {
const options: RestartPluginOptions = {
...DEFAULT_OPTIONS,
@ -111,24 +129,46 @@ export const server: Plugin = async ({ client }, rawOptions = {}) => {
},
async execute(args, ctx) {
const { providerID, modelID } = await resolveCompactModel(client, ctx.sessionID, args.model)
await client.session.summarize({
void client.session
.summarize({
path: { id: ctx.sessionID },
body: { providerID, modelID },
})
.catch(() => {
// The summarize request parks server-side until the running loop
// finishes; the compaction message it writes is what matters, and it
// is created synchronously before that wait. Errors here are
// surface-level (the loop below still processes the compaction).
})
await waitFor(client, ctx.sessionID, (messages) =>
messages.some(
(entry) =>
entry.info.role === "user" &&
entry.parts.some((part) => part.type === "compaction"),
),
)
const staticText = await resolveStaticText(client, ctx.agent, args.agent, options.static_text)
const parts: TextPartInput[] = [
{ type: "text", text: buildHandoff(args.task, args.notes, staticText) },
]
const handoff = buildHandoff(args.task, args.notes, staticText)
await client.session.promptAsync({
path: { id: ctx.sessionID },
body: {
parts,
parts: [{ type: "text", text: handoff } as TextPartInput],
...(args.model ? { model: normalizeModel(args.model) } : {}),
...(args.agent ? { agent: args.agent } : {}),
},
})
await waitFor(client, ctx.sessionID, (messages) =>
messages.some(
(entry) =>
entry.info.role === "user" &&
entry.parts.some((part) => part.type === "text" && part.text?.includes(HANDOFF_MARKER)),
),
)
return {
title: "Session context cleared",
output: