From 4b0ea74bc24fb7839be65ac114dfac449e25b5de Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:06:24 +0100 Subject: [PATCH 1/5] feat: add session.before_complete hook --- packages/opencode/src/session/prompt.ts | 19 +++++++++++++++++++ packages/plugin/src/index.ts | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0d3d25feb8de..0bcdede67a9b 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -622,6 +622,25 @@ export namespace SessionPrompt { continue } SessionCompaction.prune({ sessionID }) + + // Allow plugins to optionally resume this session before it completes + const sessionInfo = await Session.get(sessionID) + const beforeCompleteOutput = { resumePrompt: undefined as string | undefined } + await Plugin.trigger( + "session.before_complete", + { sessionID, parentSessionID: sessionInfo?.parentID }, + beforeCompleteOutput, + ) + + if (beforeCompleteOutput.resumePrompt) { + log.info("resuming session", { sessionID }) + delete state()[sessionID] + await prompt({ + sessionID, + parts: [{ type: "text", text: beforeCompleteOutput.resumePrompt }], + }) + } + for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user") continue const queued = state()[sessionID]?.callbacks ?? [] diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index e57eff579e63..85a386bb3a13 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -215,4 +215,9 @@ export interface Hooks { input: { sessionID: string; messageID: string; partID: string }, output: { text: string }, ) => Promise + + "session.before_complete"?: ( + input: { sessionID: string; parentSessionID?: string }, + output: { resumePrompt?: string }, + ) => Promise } From eae0f965cd3b2e01962b7d7b8388998965121377 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:00:42 +0100 Subject: [PATCH 2/5] semantics changes --- packages/opencode/src/session/prompt.ts | 12 ++++-------- packages/plugin/src/index.ts | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0bcdede67a9b..b0b5e303a44a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -625,19 +625,15 @@ export namespace SessionPrompt { // Allow plugins to optionally resume this session before it completes const sessionInfo = await Session.get(sessionID) - const beforeCompleteOutput = { resumePrompt: undefined as string | undefined } - await Plugin.trigger( - "session.before_complete", - { sessionID, parentSessionID: sessionInfo?.parentID }, - beforeCompleteOutput, - ) + const beforeIdleOutput = { resumePrompt: undefined as string | undefined } + await Plugin.trigger("session.before.idle", { sessionID, parentSessionID: sessionInfo?.parentID }, beforeIdleOutput) - if (beforeCompleteOutput.resumePrompt) { + if (beforeIdleOutput.resumePrompt) { log.info("resuming session", { sessionID }) delete state()[sessionID] await prompt({ sessionID, - parts: [{ type: "text", text: beforeCompleteOutput.resumePrompt }], + parts: [{ type: "text", text: beforeIdleOutput.resumePrompt }], }) } diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 85a386bb3a13..ca3f960ff55e 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -216,7 +216,7 @@ export interface Hooks { output: { text: string }, ) => Promise - "session.before_complete"?: ( + "session.before.idle"?: ( input: { sessionID: string; parentSessionID?: string }, output: { resumePrompt?: string }, ) => Promise From 7425a1e5796a344487dba55da4b4e0a62e5ee454 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:27:45 +0100 Subject: [PATCH 3/5] add agent and model --- packages/opencode/src/session/prompt.ts | 12 +++++++++++- packages/plugin/src/index.ts | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b0b5e303a44a..bf78a3433c8e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -266,6 +266,8 @@ export namespace SessionPrompt { using _ = defer(() => cancel(sessionID)) let step = 0 + let sessionAgent: string | undefined + let sessionModel: { providerID: string; modelID: string } | undefined const session = await Session.get(sessionID) while (true) { SessionStatus.set(sessionID, { type: "busy" }) @@ -291,6 +293,8 @@ export namespace SessionPrompt { } if (!lastUser) throw new Error("No user message found in stream. This should never happen.") + sessionAgent = lastUser.agent + sessionModel = lastUser.model if ( lastAssistant?.finish && !["tool-calls", "unknown"].includes(lastAssistant.finish) && @@ -626,13 +630,19 @@ export namespace SessionPrompt { // Allow plugins to optionally resume this session before it completes const sessionInfo = await Session.get(sessionID) const beforeIdleOutput = { resumePrompt: undefined as string | undefined } - await Plugin.trigger("session.before.idle", { sessionID, parentSessionID: sessionInfo?.parentID }, beforeIdleOutput) + await Plugin.trigger( + "session.before.idle", + { sessionID, parentSessionID: sessionInfo?.parentID, agent: sessionAgent, model: sessionModel }, + beforeIdleOutput, + ) if (beforeIdleOutput.resumePrompt) { log.info("resuming session", { sessionID }) delete state()[sessionID] await prompt({ sessionID, + agent: sessionAgent, + model: sessionModel, parts: [{ type: "text", text: beforeIdleOutput.resumePrompt }], }) } diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index ca3f960ff55e..fa9719d24201 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -217,7 +217,12 @@ export interface Hooks { ) => Promise "session.before.idle"?: ( - input: { sessionID: string; parentSessionID?: string }, + input: { + sessionID: string + parentSessionID?: string + agent?: string + model?: { providerID: string; modelID: string } + }, output: { resumePrompt?: string }, ) => Promise } From 91d5554a2b9ddd7d8bb6d60abaeb4032b277e510 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Mon, 19 Jan 2026 01:47:21 +0100 Subject: [PATCH 4/5] Revert "add agent and model" This reverts commit 7425a1e5796a344487dba55da4b4e0a62e5ee454. --- packages/opencode/src/session/prompt.ts | 12 +----------- packages/plugin/src/index.ts | 7 +------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index bf78a3433c8e..b0b5e303a44a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -266,8 +266,6 @@ export namespace SessionPrompt { using _ = defer(() => cancel(sessionID)) let step = 0 - let sessionAgent: string | undefined - let sessionModel: { providerID: string; modelID: string } | undefined const session = await Session.get(sessionID) while (true) { SessionStatus.set(sessionID, { type: "busy" }) @@ -293,8 +291,6 @@ export namespace SessionPrompt { } if (!lastUser) throw new Error("No user message found in stream. This should never happen.") - sessionAgent = lastUser.agent - sessionModel = lastUser.model if ( lastAssistant?.finish && !["tool-calls", "unknown"].includes(lastAssistant.finish) && @@ -630,19 +626,13 @@ export namespace SessionPrompt { // Allow plugins to optionally resume this session before it completes const sessionInfo = await Session.get(sessionID) const beforeIdleOutput = { resumePrompt: undefined as string | undefined } - await Plugin.trigger( - "session.before.idle", - { sessionID, parentSessionID: sessionInfo?.parentID, agent: sessionAgent, model: sessionModel }, - beforeIdleOutput, - ) + await Plugin.trigger("session.before.idle", { sessionID, parentSessionID: sessionInfo?.parentID }, beforeIdleOutput) if (beforeIdleOutput.resumePrompt) { log.info("resuming session", { sessionID }) delete state()[sessionID] await prompt({ sessionID, - agent: sessionAgent, - model: sessionModel, parts: [{ type: "text", text: beforeIdleOutput.resumePrompt }], }) } diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index fa9719d24201..ca3f960ff55e 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -217,12 +217,7 @@ export interface Hooks { ) => Promise "session.before.idle"?: ( - input: { - sessionID: string - parentSessionID?: string - agent?: string - model?: { providerID: string; modelID: string } - }, + input: { sessionID: string; parentSessionID?: string }, output: { resumePrompt?: string }, ) => Promise } From 57d99342d570c255f3b105861c10ea14d62c81ad Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Mon, 19 Jan 2026 02:36:47 +0100 Subject: [PATCH 5/5] cleaner agent/model consistency on resume --- packages/opencode/src/session/prompt.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b0b5e303a44a..f46d3db8211d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -631,8 +631,12 @@ export namespace SessionPrompt { if (beforeIdleOutput.resumePrompt) { log.info("resuming session", { sessionID }) delete state()[sessionID] + const resumeAgent = await lastAgent(sessionID) + const resumeModel = await lastModel(sessionID) await prompt({ sessionID, + agent: resumeAgent, + model: resumeModel, parts: [{ type: "text", text: beforeIdleOutput.resumePrompt }], }) } @@ -655,6 +659,13 @@ export namespace SessionPrompt { return Provider.defaultModel() } + async function lastAgent(sessionID: string) { + for await (const item of MessageV2.stream(sessionID)) { + if (item.info.role === "user" && item.info.agent) return item.info.agent + } + return Agent.defaultAgent() + } + async function resolveTools(input: { agent: Agent.Info model: Provider.Model