import { type Plugin, type PluginInput, type PluginOptions, tool } from "@opencode-ai/plugin" import type { TextPartInput } from "@opencode-ai/sdk" export const id = "opencode-session-restart" type PluginClient = PluginInput["client"] export type RestartPluginOptions = { /** * Static text always prepended to every restart prompt, for agents that do * not define their own `handoff_text`. An agent-defined `handoff_text` * overrides this. Default: none. */ static_text?: string } const DEFAULT_OPTIONS: RestartPluginOptions = {} const MAX_TITLE_LENGTH = 80 function buildHandoff(task: string, notes: string | undefined, staticText: string | undefined, parentSessionID: string) { const state = notes?.trim() return [ "You are continuing work that was restarted into this fresh session with a clean context window.", "", `The full earlier conversation remains readable in the parent session ${parentSessionID} (switched to above this one in the session tree). Do not replay it here — only the standing instructions and state notes below, plus the current on-disk state (working tree, git status, staged or uncommitted changes), are available context.`, ...(staticText ? ["", "STANDING INSTRUCTIONS (always apply):", staticText] : []), "", "STATE NOTES:", state && state.length > 0 ? state : "None provided — orient yourself by inspecting git status, the diff, and open files.", "", "FOCUSED TASK (this session only):", task.trim(), ].join("\n") } async function resolveStaticText( client: PluginClient, currentAgent: string, targetAgent: string | undefined, fallback: string | undefined, ) { const agentName = targetAgent ?? currentAgent try { const cfg = await client.config.get() const agent = cfg.data?.agent?.[agentName] const direct = agent?.handoff_text if (typeof direct === "string" && direct.trim()) return direct const opts = agent?.options as Record | undefined const folded = opts?.handoff_text if (typeof folded === "string" && folded.trim()) return folded } catch { // config lookup failed — fall through to the plugin-level static_text } return fallback } function normalizeModel(value: string) { const slash = value.indexOf("/") if (slash <= 0 || slash === value.length - 1) { throw new Error(`model must be "providerID/modelID", got "${value}"`) } return { providerID: value.slice(0, slash), modelID: value.slice(slash + 1) } } export const server: Plugin = async ({ client }, rawOptions = {}) => { const options: RestartPluginOptions = { ...DEFAULT_OPTIONS, ...(rawOptions as Partial), } return { tool: { session_restart: tool({ description: "Start a fresh child session on this project with a clean context window and prompt it to continue a single focused task, " + "keeping the current session alive with its full history readable in the session tree. " + "Use this when the current session is getting too long or about to compact, so work continues without losing context fidelity. " + "The current session is always left running: after calling this tool, stop working here and hand off — the restarted session continues.", args: { task: tool.schema .string() .describe("The single focused task for the restarted session, written as a fresh prompt."), notes: tool .schema.string() .optional() .describe( "Durable context to carry over: decisions, constraints, files touched, unfinished steps, commands run. Omit to rely on the working tree only.", ), model: tool .schema.string() .optional() .describe('Model for the restarted session as "providerID/modelID". Defaults to this session\'s model.'), agent: tool.schema.string().optional().describe("Agent for the restarted session. Defaults to the current agent."), }, async execute(args, ctx) { const title = args.task.length > MAX_TITLE_LENGTH ? `${args.task.slice(0, MAX_TITLE_LENGTH - 1)}…` : args.task const created = await client.session.create({ body: { title, parentID: ctx.sessionID }, }) const restartedID = created.data?.id if (!restartedID) throw new Error("session.create returned no session id") const staticText = await resolveStaticText(client, ctx.agent, args.agent, options.static_text) const parts: TextPartInput[] = [ { type: "text", text: buildHandoff(args.task, args.notes, staticText, ctx.sessionID) }, ] await client.session.promptAsync({ path: { id: restartedID }, body: { parts, ...(args.model ? { model: normalizeModel(args.model) } : {}), ...(args.agent ? { agent: args.agent } : {}), }, }) return { title: `Restarted in session ${restartedID}`, output: `Started a fresh child session ${restartedID} with a clean context window; it is already running the focused task. The current session stays alive with its full history readable. Switch to session ${restartedID} to continue the work.`, metadata: { restartedSessionID: restartedID, parentSessionID: ctx.sessionID, }, } }, }), }, } } export default { id, server }