Skip to content

Keep sessionStart hook additionalContext after compaction #5225

Description

@naoki1213mj

Describe the feature or problem you'd like to solve

After compaction, the model no longer receives the additionalContext that sessionStart hooks returned, and nothing restores it until the session is resumed.

Proposed solution

Any one of these would solve it:

  1. When compaction replaces the history, keep the message that carries the sessionStart context, as the runtime already keeps the system message.
  2. Run sessionStart again after compaction with source: "compact" and inject its additionalContext. Claude Code does this, so plugins that register SessionStart with a compact matcher would work on Copilot unchanged.
  3. Add a postCompact hook whose additionalContext reaches the compacted context. Preserve durable context across repeated compactions #4441 proposes something close to this.

If the current behavior stays, please state in the hooks reference that sessionStart context does not survive compaction.

Plugins and repositories use sessionStart to inject rules that should hold for the whole session. Today the rules hold only until the first compaction, and neither the user nor the agent is told that they are gone.

Example prompts or workflows

  1. A plugin injects routing rules at session start, such as which workflow to load for multi-file changes, and expects them to hold for the whole session. Today, automatic compaction in a long autopilot session drops them, and the agent stops following them.
  2. A sessionStart hook injects the models the user approved for subagents, and a preToolUse hook denies any other model. After compaction the agent no longer sees the list, picks another model, and the hook denies the call.
  3. A repository hook injects ticket constraints at session start. After the user runs /compact, the constraints are gone until the session is resumed.

Additional context

The current behavior matches the hooks reference. It says sessionStart fires when "A new or resumed session begins", lists its source values as "startup", "resume", and "new", and says the runtime does not process preCompact output. So I'm filing this as a feature request, not a bug.

What I measured

I pointed a session at a local mock OpenAI-compatible server through a BYOK provider and recorded which markers each model request contained. A sessionStart hook returned one marker in additionalContext, and copilot-instructions.md in the config directory held another.

Request sessionStart context copilot-instructions.md
Turn 1 yes yes
Compaction summary request yes yes
Turn 2, after compaction no yes
Turn 3, after resume yes yes
  • sessionStart ran with source: "new" and then source: "resume". It did not run for compaction.
  • The runtime sends the hook context as a separate user message right after the system message. The first request after compaction contains only the system message, the summary message, and the new prompt.
  • A plugin command hook and an SDK onSessionStart callback behaved the same. So did manual compaction and threshold-triggered background compaction.
  • Runtime 1.0.99-2 and 1.0.100-1 on macOS arm64 gave the same result. The release notes through 1.0.103-0 don't mention a change.
  • The summarizer may paraphrase the hook text into the summary, but the original text is gone.

The GitHub Copilot app shows the same pattern in normal use. The app shuts a session down about 10 minutes after the last turn and resumes it on the next message. That resume is the only point where sessionStart runs again, so after each compaction the context stays missing until the next idle resume.

Both workarounds I know of have costs. Copying the rules into ~/.copilot/copilot-instructions.md works because that file stays in the system message, but every user has to edit a personal file, and the copy goes stale when the plugin changes. A userPromptTransformed hook could append the rules to every prompt, but the docs say the replaced content is stored in session history, so the rules would repeat on every turn. I haven't tried this one.

References, checked 2026-10-22

Repro script

Requires Node.js. Set COPILOT_SDK to the SDK's index.js. I used the copy bundled with the GitHub Copilot app. Set COPILOT_RUNTIME to a copilot runtime binary or to the index.js of the @github/copilot package. The session uses a local mock model server and a temporary config directory, and the script deletes the directory at the end.

// Shows whether sessionStart additionalContext reaches the model after compaction.
//
// Usage:
//   COPILOT_SDK=/path/to/copilot-sdk/index.js \
//   COPILOT_RUNTIME=/path/to/copilot \
//   node repro-sessionstart-compaction.mjs
//
// COPILOT_RUNTIME is a runtime binary or the index.js of the @github/copilot package.
// The session talks to a local mock model server, so the script makes no real model calls.
// It writes only to a temporary COPILOT_HOME, which it deletes at the end.

import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";

const { COPILOT_SDK, COPILOT_RUNTIME } = process.env;
if (!COPILOT_SDK || !COPILOT_RUNTIME) {
	console.error("Set COPILOT_SDK to the SDK's index.js and COPILOT_RUNTIME to the copilot runtime.");
	process.exit(2);
}
const { CopilotClient, RuntimeConnection, approveAll } = await import(pathToFileURL(COPILOT_SDK).href);

const HOOK_MARKER = "HOOK-MARKER-7Q2";
const INSTRUCTIONS_MARKER = "INSTRUCTIONS-MARKER-4K9";
const SUMMARY = [
	"<overview>A short test conversation.</overview>",
	"<history>1. The user sent a test message.</history>",
	"<work_done>None.</work_done>",
	"<technical_details>None.</technical_details>",
	"<important_files>None.</important_files>",
	"<next_steps>Wait for the next message.</next_steps>",
	"<checkpoint_title>Test checkpoint</checkpoint_title>",
].join("\n");

const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sessionstart-compaction-"));
const home = path.join(tmp, "home");
const work = path.join(tmp, "work");
fs.mkdirSync(home);
fs.mkdirSync(work);
fs.writeFileSync(path.join(home, "copilot-instructions.md"), `${INSTRUCTIONS_MARKER}\n`);

// A mock OpenAI-compatible server that records which markers each chat request contains.
const requests = [];
let step = "setup";
const server = http.createServer((req, res) => {
	let raw = "";
	req.on("data", (chunk) => (raw += chunk));
	req.on("end", () => {
		if (req.method !== "POST") {
			res.writeHead(200, { "Content-Type": "application/json" });
			res.end(JSON.stringify({ object: "list", data: [{ id: "mock-model", object: "model", owned_by: "mock" }] }));
			return;
		}
		const body = JSON.parse(raw);
		const messages = JSON.stringify(body.messages);
		const isSummary = messages.includes("Please create a detailed summary");
		requests.push({
			request: isSummary ? "compaction summary request" : step,
			"sessionStart context": messages.includes(HOOK_MARKER) ? "yes" : "no",
			"copilot-instructions.md": messages.includes(INSTRUCTIONS_MARKER) ? "yes" : "no",
		});
		const text = isSummary ? SUMMARY : "OK";
		const created = Math.floor(Date.now() / 1000);
		const usage = { prompt_tokens: 1000, completion_tokens: 20, total_tokens: 1020 };
		if (!body.stream) {
			res.writeHead(200, { "Content-Type": "application/json" });
			res.end(JSON.stringify({ id: "mock", object: "chat.completion", created, model: "mock-model", choices: [{ index: 0, message: { role: "assistant", content: text }, finish_reason: "stop" }], usage }));
			return;
		}
		res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
		const send = (chunk) => res.write(`data: ${JSON.stringify(chunk)}\n\n`);
		send({ id: "mock", object: "chat.completion.chunk", created, model: "mock-model", choices: [{ index: 0, delta: { role: "assistant", content: text }, finish_reason: null }] });
		send({ id: "mock", object: "chat.completion.chunk", created, model: "mock-model", choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage });
		res.end("data: [DONE]\n\n");
	});
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address();

const sources = [];
const sessionConfig = {
	model: "mock-model",
	provider: { type: "openai", wireApi: "completions", baseUrl: `http://127.0.0.1:${port}/v1`, apiKey: "mock", maxPromptTokens: 120000, maxOutputTokens: 4096 },
	onPermissionRequest: approveAll,
	workingDirectory: work,
	hooks: {
		onSessionStart: async (input) => {
			sources.push(input.source);
			return { additionalContext: `${HOOK_MARKER} (source: ${input.source})` };
		},
	},
};
const newClient = () =>
	new CopilotClient({
		connection: RuntimeConnection.forStdio({ path: COPILOT_RUNTIME }),
		baseDirectory: home,
		workingDirectory: work,
		logLevel: "error",
	});

let client = newClient();
try {
	await client.start();
	const session = await client.createSession(sessionConfig);
	step = "turn 1";
	await session.sendAndWait({ prompt: "First message." }, 60000);
	step = "manual compaction";
	await session.rpc.history.compact({ trigger: "manual" });
	step = "turn 2, after compaction";
	await session.sendAndWait({ prompt: "Second message." }, 60000);
	await session.disconnect();
	await client.stop();

	client = newClient();
	await client.start();
	const resumed = await client.resumeSession(session.sessionId, sessionConfig);
	step = "turn 3, after resume";
	await resumed.sendAndWait({ prompt: "Third message." }, 60000);
	await resumed.disconnect();
} finally {
	await client.stop().catch(() => {});
	server.close();
	fs.rmSync(tmp, { recursive: true, force: true });
}

console.log(`sessionStart ran with source: ${sources.join(", ")}`);
console.table(requests);
process.exit(0);

Activity

  1. added
    area:context-memoryContext window, memory, compaction, checkpoints, and instruction loading
    area:pluginsPlugin system, marketplace, hooks, skills, extensions, and custom agents
    and removed on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:context-memoryContext window, memory, compaction, checkpoints, and instruction loadingarea:pluginsPlugin system, marketplace, hooks, skills, extensions, and custom agents

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions