From a563fafdcf41d0a4c0712188467c3c9a5a2de4c1 Mon Sep 17 00:00:00 2001 From: emidab Date: Wed, 16 Sep 2026 08:54:25 +0000 Subject: [PATCH] feat: keep current session alive on restart by default --- README.md | 13 +++++++------ index.ts | 23 +++++++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 99f920e..b455636 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ opencode plugin that lets the agent restart a session into a fresh child session with a clean context window and a focused prompt, dodging context compaction loss. -When a session runs long enough to trigger compaction, opencode summarizes the conversation into a prompt and keeps going — fidelity you did not choose. This plugin gives the agent a first-class escape hatch: start a new child session on the same project, prompt it with a curated fresh prompt, and (by default) freeze the old session. The single focused task continues in a fresh window. +When a session runs long enough to trigger compaction, opencode summarizes the conversation into a prompt and keeps going — fidelity you did not choose. This plugin gives the agent a first-class escape hatch: start a new child session on the same project, prompt it with a curated fresh prompt, and (by default) keep the current session alive so its full history stays readable. The single focused task continues in a fresh window. ## Install @@ -18,7 +18,7 @@ or from a local clone: ```json { - "plugin": [["/path/to/opencode-session-restart", { "abort_current": true }]] + "plugin": [["/path/to/opencode-session-restart", { "disable_compaction_continue": true }]] } ``` @@ -27,8 +27,8 @@ or from a local clone: The plugin registers one tool, `session_restart`, that the agent can call when the session is getting long: - Creates a child session (via the SDK `session.create` with `parentID`), so it shows up in the session tree under the original. -- Immediately prompts it (via `session.promptAsync`) with the focused `task` plus optional carry-over `notes` and a fresh-start preamble that tells the new session not to reconstruct the old conversation and to orient itself from the working tree. -- Aborts the current session by default, so only the restarted session stays active. +- Immediately prompts it (via `session.promptAsync`) with the focused `task` plus optional carry-over `notes` and a fresh-start preamble that points at the parent session for the full earlier conversation and tells the new session to orient itself from the working tree. +- Leaves the current session running by default, so its full history stays visible and readable in the session tree. Optionally `abort_current` freezes it instead. - Returns the new session ID so the agent can tell you where to switch. ## Static handoff text @@ -65,12 +65,13 @@ Either way the text is prepended to the handoff prompt under a `STANDING INSTRUC | `notes` | string | — | Durable context to carry over: decisions, constraints, files touched, unfinished steps. Omit to rely on the working tree only. | | `model` | string | current | `"providerID/modelID"` for the restarted session. | | `agent` | string | current | Agent for the restarted session. | -| `abort_current` | boolean | `true` | Abort this session after restarting. | +| `abort_current` | boolean | `false` | Abort this session after restarting. Default leaves the current session alive with its history readable. | ### Notes on behavior - The new session keeps the fresh prompt as its user-turn text and immediately starts working. On-disk state (git status, staged or uncommitted changes) carries over because it is the same project directory. -- `abort_current` freezes the original session after the new one is started. Because abort cancels the current agent turn, the tool's returned text may not be delivered to the old session — the side effects (create + prompt) complete first. +- By default the original session is left running and its full transcript remains in the session tree — the clean context lives in the child session. The handoff prompt includes the parent session ID so both you and the model know where to read the earlier conversation. +- `abort_current: true` freezes the original session after the new one is started. Because abort cancels the current agent turn, the tool's returned text may not be delivered to the old session — the side effects (create + prompt) complete first. - `disable_compaction_continue` (default `true`) rejects the synthetic "continue" turn opencode would append after a compaction, so a compaction that does slip through never silently continues. Set it to `false` to keep stock behavior. ## Development diff --git a/index.ts b/index.ts index 8dc60d6..5a5d1fd 100644 --- a/index.ts +++ b/index.ts @@ -8,8 +8,8 @@ type PluginClient = PluginInput["client"] 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. + * Default: false — the current session stays warm and its full history + * remains readable in the session tree. */ abort_current: boolean /** @@ -27,18 +27,18 @@ export type RestartPluginOptions = { } const DEFAULT_OPTIONS: RestartPluginOptions = { - abort_current: true, + abort_current: false, disable_compaction_continue: true, } const MAX_TITLE_LENGTH = 80 -function buildHandoff(task: string, notes: string | undefined, staticText: string | undefined) { +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.", "", - "Do not reconstruct or repeat the earlier conversation. 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.", + `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] : []), @@ -90,9 +90,10 @@ export const server: Plugin = async ({ client }, rawOptions = {}) => { 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. " + + "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 aborted by default: after calling this tool, stop working and hand off.", + "The current session is left running by default: after calling this tool, stop working here and hand off — the restarted session continues.", args: { task: tool.schema .string() @@ -111,7 +112,7 @@ export const server: Plugin = async ({ client }, rawOptions = {}) => { abort_current: tool .schema.boolean() .optional() - .describe("Abort this session after restarting. Defaults to true."), + .describe("Abort this session after restarting. Defaults to false — the current session stays alive."), }, async execute(args, ctx) { const abortCurrent = args.abort_current ?? options.abort_current @@ -124,7 +125,9 @@ export const server: Plugin = async ({ client }, rawOptions = {}) => { 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) }] + const parts: TextPartInput[] = [ + { type: "text", text: buildHandoff(args.task, args.notes, staticText, ctx.sessionID) }, + ] await client.session.promptAsync({ path: { id: restartedID }, body: { @@ -140,7 +143,7 @@ export const server: Plugin = async ({ client }, rawOptions = {}) => { 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.`, + 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,