From 2cac9375d8df4e5cbd7cd9aa6464ffc061afaeaa Mon Sep 17 00:00:00 2001 From: emidab Date: Wed, 16 Sep 2026 12:46:47 +0000 Subject: [PATCH] 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. --- index.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/index.ts b/index.ts index bd79ae4..db319d0 100644 --- a/index.ts +++ b/index.ts @@ -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({ - path: { id: ctx.sessionID }, - body: { providerID, modelID }, - }) + + 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: