import { type Plugin, type PluginOptions, tool } from "@opencode-ai/plugin" import type { TextPartInput } from "@opencode-ai/sdk" export const id = "opencode-session-restart" export type RestartPluginOptions = { /** * Abort the current session once the new session has been started. * Freezes the old context so the restarted session is the single active one. * Default: true. */ abort_current: boolean /** * Reject the synthetic "continue" turn opencode appends after compaction, * forcing a human (or the restart tool) to decide what happens next. * Default: true. */ disable_compaction_continue: boolean } const DEFAULT_OPTIONS: RestartPluginOptions = { abort_current: true, disable_compaction_continue: true, } const MAX_TITLE_LENGTH = 80 function buildHandoff(task: string, notes: string | undefined) { const state = notes?.trim() return [ "You are continuing work that was restarted into this fresh session with a clean context window.", "", "Do not reconstruct or repeat the earlier conversation. Only the notes below and the current on-disk state (working tree, git status, staged or uncommitted changes) are available context.", "", "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") } 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. " + "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 aborted by default: after calling this tool, stop working and hand off.", 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."), abort_current: tool .schema.boolean() .optional() .describe("Abort this session after restarting. Defaults to true."), }, async execute(args, ctx) { const abortCurrent = args.abort_current ?? options.abort_current 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 parts: TextPartInput[] = [{ type: "text", text: buildHandoff(args.task, args.notes) }] await client.session.promptAsync({ path: { id: restartedID }, body: { parts, ...(args.model ? { model: normalizeModel(args.model) } : {}), ...(args.agent ? { agent: args.agent } : {}), }, }) if (abortCurrent) { await client.session.abort({ path: { id: ctx.sessionID } }).catch(() => undefined) } 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. Switch to that session to continue.`, metadata: { restartedSessionID: restartedID, parentSessionID: ctx.sessionID, abort_current: abortCurrent, }, } }, }), }, "experimental.compaction.autocontinue": async (_input, output) => { if (options.disable_compaction_continue) { output.enabled = false } }, } } export default { id, server }