This guide walks you through the SDK's session persistence capabilitiesโhow to pause work, resume it later, and manage sessions in production environments.
When you create a session, the Copilot CLI maintains conversation history, tool state, and planning context. By default, this state lives in memory and disappears when the session ends. With persistence enabled, you can resume sessions across restarts, container migrations, or even different client instances.
flowchart LR
A[๐ Create] --> B[โก Active] --> C[๐พ Paused] --> D[๐ Resume]
D --> B
| State | What happens |
|---|---|
| Create | session_id assigned |
| Active | Send prompts, tool calls, responses |
| Paused | State saved to disk |
| Resume | State loaded from disk |
The key to resumable sessions is providing your own session_id. Without one, the SDK generates a random ID and the session can't be resumed later.
import { CopilotClient } from "copilot-sdk-supercharged";
const client = new CopilotClient();
// Create a session with a meaningful ID
const session = await client.createSession({
sessionId: "user-123-task-456",
model: "gpt-5.2-codex",
});
// Do some work...
await session.sendAndWait({ prompt: "Analyze my codebase" });
// Session state is automatically persisted
// You can safely close the client
from copilot import CopilotClient
client = CopilotClient()
await client.start()
# Create a session with a meaningful ID
session = await client.create_session({
"session_id": "user-123-task-456",
"model": "gpt-5.2-codex",
})
# Do some work...
await session.send_and_wait({"prompt": "Analyze my codebase"})
# Session state is automatically persisted
ctx := context.Background()
client := copilot.NewClient(nil)
// Create a session with a meaningful ID
session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
SessionID: "user-123-task-456",
Model: "gpt-5.2-codex",
})
// Do some work...
session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Analyze my codebase"})
// Session state is automatically persisted
using GitHub.Copilot.SDK;
var client = new CopilotClient();
// Create a session with a meaningful ID
var session = await client.CreateSessionAsync(new SessionConfig
{
SessionId = "user-123-task-456",
Model = "gpt-5.2-codex",
});
// Do some work...
await session.SendAndWaitAsync(new MessageOptions { Prompt = "Analyze my codebase" });
// Session state is automatically persisted
Laterโminutes, hours, or even daysโyou can resume the session from where you left off.
flowchart LR
subgraph Day1["Day 1"]
A1[Client A:<br/>createSession] --> A2[Work...]
end
A2 --> S[(๐พ Storage:<br/>~/.copilot/session-state/)]
S --> B1
subgraph Day2["Day 2"]
B1[Client B:<br/>resumeSession] --> B2[Continue]
end
// Resume from a different client instance (or after restart)
const session = await client.resumeSession("user-123-task-456");
// Continue where you left off
await session.sendAndWait({ prompt: "What did we discuss earlier?" });
# Resume from a different client instance (or after restart)
session = await client.resume_session("user-123-task-456")
# Continue where you left off
await session.send_and_wait({"prompt": "What did we discuss earlier?"})
ctx := context.Background()
// Resume from a different client instance (or after restart)
session, _ := client.ResumeSession(ctx, "user-123-task-456", nil)
// Continue where you left off
session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What did we discuss earlier?"})
// Resume from a different client instance (or after restart)
var session = await client.ResumeSessionAsync("user-123-task-456");
// Continue where you left off
await session.SendAndWaitAsync(new MessageOptions { Prompt = "What did we discuss earlier?" });
When resuming a session, you can optionally reconfigure many settings. This is useful when you need to change the model, update tool configurations, or modify behavior.
| Option | Description |
|---|---|
model | Change the model for the resumed session |
systemMessage | Override or extend the system prompt |
availableTools | Restrict which tools are available |
excludedTools | Disable specific tools |
provider | Re-provide BYOK credentials (required for BYOK sessions) |
reasoningEffort | Adjust reasoning effort level |
streaming | Enable/disable streaming responses |
workingDirectory | Change the working directory |
configDir | Override configuration directory |
mcpServers | Configure MCP servers |
customAgents | Configure custom agents |
skillDirectories | Directories to load skills from |
disabledSkills | Skills to disable |
infiniteSessions | Configure infinite session behavior |
// Resume with a different model
const session = await client.resumeSession("user-123-task-456", {
model: "claude-sonnet-4", // Switch to a different model
reasoningEffort: "high", // Increase reasoning effort
});
When using your own API keys, you must re-provide the provider configuration when resuming. API keys are never persisted to disk for security reasons.
// Original session with BYOK
const session = await client.createSession({
sessionId: "user-123-task-456",
model: "gpt-5.2-codex",
provider: {
type: "azure",
endpoint: "https://my-resource.openai.azure.com",
apiKey: process.env.AZURE_OPENAI_KEY,
deploymentId: "my-gpt-deployment",
},
});
// When resuming, you MUST re-provide the provider config
const resumed = await client.resumeSession("user-123-task-456", {
provider: {
type: "azure",
endpoint: "https://my-resource.openai.azure.com",
apiKey: process.env.AZURE_OPENAI_KEY, // Required again
deploymentId: "my-gpt-deployment",
},
});
Session state is saved to ~/.copilot/session-state/{sessionId}/:
~/.copilot/session-state/
โโโ user-123-task-456/
โโโ checkpoints/ # Conversation history snapshots
โ โโโ 001.json # Initial state
โ โโโ 002.json # After first interaction
โ โโโ ... # Incremental checkpoints
โโโ plan.md # Agent's planning state (if any)
โโโ files/ # Session artifacts
โโโ analysis.md # Files the agent created
โโโ notes.txt # Working documents
| Data | Persisted? | Notes |
|---|---|---|
| Conversation history | โ Yes | Full message thread |
| Tool call results | โ Yes | Cached for context |
| Agent planning state | โ Yes | plan.md file |
| Session artifacts | โ Yes | In files/ directory |
| Provider/API keys | โ No | Security: must re-provide |
| In-memory tool state | โ No | Tools should be stateless |
Choose session IDs that encode ownership and purpose. This makes auditing and cleanup much easier.
| Pattern | Example | Use Case |
|---|---|---|
โ abc123 | Random IDs | Hard to audit, no ownership info |
โ
user-{userId}-{taskId} | user-alice-pr-review-42 | Multi-user apps |
โ
tenant-{tenantId}-{workflow} | tenant-acme-onboarding | Multi-tenant SaaS |
โ
{userId}-{taskId}-{timestamp} | alice-deploy-1706932800 | Time-based cleanup |
Benefits of structured IDs:
function createSessionId(userId: string, taskType: string): string {
const timestamp = Date.now();
return `${userId}-${taskType}-${timestamp}`;
}
const sessionId = createSessionId("alice", "code-review");
// โ "alice-code-review-1706932800000"
import time
def create_session_id(user_id: str, task_type: str) -> str:
timestamp = int(time.time())
return f"{user_id}-{task_type}-{timestamp}"
session_id = create_session_id("alice", "code-review")
# โ "alice-code-review-1706932800"
const sessions = await client.listSessions();
console.log(`Found ${sessions.length} sessions`);
for (const session of sessions) {
console.log(`- ${session.sessionId} (created: ${session.createdAt})`);
}
async function cleanupExpiredSessions(maxAgeMs: number) {
const sessions = await client.listSessions();
const now = Date.now();
for (const session of sessions) {
const age = now - new Date(session.createdAt).getTime();
if (age > maxAgeMs) {
await client.deleteSession(session.sessionId);
console.log(`Deleted expired session: ${session.sessionId}`);
}
}
}
// Clean up sessions older than 24 hours
await cleanupExpiredSessions(24 * 60 * 60 * 1000);
When a task completes, destroy the session explicitly rather than waiting for timeouts:
try {
// Do work...
await session.sendAndWait({ prompt: "Complete the task" });
// Task complete - clean up
await session.destroy();
} catch (error) {
// Clean up even on error
await session.destroy();
throw error;
}
The CLI has a built-in 30-minute idle timeout. Sessions without activity are automatically cleaned up:
flowchart LR
A["โก Last Activity"] --> B["โณ 25 min<br/>timeout_warning"] --> C["๐งน 30 min<br/>destroyed"]
Listen for idle events to know when work completes:
session.on("session.idle", (event) => {
console.log(`Session idle for ${event.idleDurationMs}ms`);
});
Best for: Strong isolation, multi-tenant environments, Azure Dynamic Sessions.
flowchart LR
subgraph Users[" "]
UA[User A] --> CA[CLI A]
UB[User B] --> CB[CLI B]
UC[User C] --> CC[CLI C]
end
CA --> SA[(Storage A)]
CB --> SB[(Storage B)]
CC --> SC[(Storage C)]
Benefits: โ Complete isolation | โ Simple security | โ Easy scaling
Best for: Internal tools, trusted environments, resource-constrained setups.
flowchart LR
UA[User A] --> CLI
UB[User B] --> CLI
UC[User C] --> CLI
CLI[๐ฅ๏ธ Shared CLI] --> SA[Session A]
CLI --> SB[Session B]
CLI --> SC[Session C]
Requirements:
// Application-level access control for shared CLI
async function resumeSessionWithAuth(
client: CopilotClient,
sessionId: string,
currentUserId: string
): Promise<Session> {
// Parse user from session ID
const [sessionUserId] = sessionId.split("-");
if (sessionUserId !== currentUserId) {
throw new Error("Access denied: session belongs to another user");
}
return client.resumeSession(sessionId);
}
For serverless/container deployments where containers can restart or migrate:
The session state directory must be mounted to persistent storage:
# Azure Container Instance example
containers:
- name: copilot-agent
image: my-agent:latest
volumeMounts:
- name: session-storage
mountPath: /home/app/.copilot/session-state
volumes:
- name: session-storage
azureFile:
shareName: copilot-sessions
storageAccountName: myaccount
flowchart LR
subgraph Before["Container A"]
CLI1[CLI + Session X]
end
CLI1 --> |persist| Azure[(โ๏ธ Azure File Share)]
Azure --> |restore| CLI2
subgraph After["Container B (restart)"]
CLI2[CLI + Session X]
end
Session survives container restarts!
For workflows that might exceed context limits, enable infinite sessions with automatic compaction:
const session = await client.createSession({
sessionId: "long-workflow-123",
infiniteSessions: {
enabled: true,
backgroundCompactionThreshold: 0.80, // Start compaction at 80% context
bufferExhaustionThreshold: 0.95, // Block at 95% if needed
},
});
Note: Thresholds are context utilization ratios (0.0-1.0), not absolute token counts. See the Compatibility Guide for details.
| Limitation | Description | Mitigation |
|---|---|---|
| BYOK re-authentication | API keys aren't persisted | Store keys in your secret manager; provide on resume |
| Writable storage | ~/.copilot/session-state/ must be writable | Mount persistent volume in containers |
| No session locking | Concurrent access to same session is undefined | Implement application-level locking or queue |
| Tool state not persisted | In-memory tool state is lost | Design tools to be stateless or persist their own state |
The SDK doesn't provide built-in session locking. If multiple clients might access the same session:
// Option 1: Application-level locking with Redis
import Redis from "ioredis";
const redis = new Redis();
async function withSessionLock<T>(
sessionId: string,
fn: () => Promise<T>
): Promise<T> {
const lockKey = `session-lock:${sessionId}`;
const acquired = await redis.set(lockKey, "locked", "NX", "EX", 300);
if (!acquired) {
throw new Error("Session is in use by another client");
}
try {
return await fn();
} finally {
await redis.del(lockKey);
}
}
// Usage
await withSessionLock("user-123-task-456", async () => {
const session = await client.resumeSession("user-123-task-456");
await session.sendAndWait({ prompt: "Continue the task" });
});
| Feature | How to Use |
|---|---|
| Create resumable session | Provide your own sessionId |
| Resume session | client.resumeSession(sessionId) |
| BYOK resume | Re-provide provider config |
| List sessions | client.listSessions() |
| Delete session | client.deleteSession(sessionId) |
| Destroy active session | session.destroy() |
| Containerized deployment | Mount ~/.copilot/session-state/ to persistent storage |
Can you improve this documentation? These fine people already did:
Patrick Nikoletich, Jeremiah Isaacson & Adrien FriggeriEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| โ | Move to previous article |
| โ | Move to next article |
| Ctrl+/ | Jump to the search field |