Documentation Index
Fetch the complete documentation index at: https://mintlify.com/QwenLM/qwen-code/llms.txt
Use this file to discover all available pages before exploring further.
Session Checkpointing and Recovery
Qwen Code supports session checkpointing, allowing you to save conversation state and resume sessions later. This is essential for long-running tasks, recovery from errors, and maintaining context across multiple work sessions.
Overview
Checkpointing provides:
- Session Persistence: Save conversation history and state
- Resume Capability: Continue sessions after interruption
- Chat Compression: Compact long conversations while preserving context
- Token Optimization: Reduce token usage in resumed sessions
- UI Telemetry: Restore metrics and statistics on resume
Session Storage
Session Directory
Sessions are stored in:
~/.qwen/sessions/<session-id>/
Each session directory contains:
~/.qwen/sessions/<uuid>/
├── conversation.jsonl # Line-delimited JSON conversation log
├── checkpoint-<tag>.json # Named checkpoints
└── metadata.json # Session metadata
From packages/core/src/core/logger.ts, conversations are stored as JSONL:
{"role":"user","parts":[{"text":"Hello"}]}
{"role":"model","parts":[{"text":"Hi there!"}]}
{"role":"user","parts":[{"text":"Create a function"}]}
Checkpoint Operations
Creating Checkpoints
From packages/core/src/core/logger.ts:328:
async saveCheckpoint(conversation: Content[], tag: string): Promise<void> {
if (!this.initialized) {
this.debugLogger.error(
'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.',
);
return;
}
// Always save with the new encoded path.
const path = this._checkpointPath(tag);
try {
await fs.writeFile(path, JSON.stringify(conversation, null, 2), 'utf-8');
} catch (error) {
this.debugLogger.error('Error writing to checkpoint file:', error);
}
}
Checkpoint Path Encoding (from logger.ts:286):
private _checkpointPath(tag: string): string {
if (!tag.length) {
throw new Error('No checkpoint tag specified.');
}
if (!this.qwenDir) {
throw new Error('Checkpoint file path not set.');
}
// Encode the tag to handle all special characters safely.
const encodedTag = encodeTagName(tag);
return path.join(this.qwenDir, `checkpoint-${encodedTag}.json`);
}
Tags are URL-encoded to handle special characters:
# Tag: "pre-refactor"
~/.qwen/sessions/<uuid>/checkpoint-pre-refactor.json
# Tag: "before:api/changes"
~/.qwen/sessions/<uuid>/checkpoint-before%3Aapi%2Fchanges.json
Loading Checkpoints
From logger.ts:344:
async loadCheckpoint(tag: string): Promise<Content[]> {
if (!this.initialized) {
this.debugLogger.error(
'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.',
);
return [];
}
const path = await this._getCheckpointPath(tag);
try {
const fileContent = await fs.readFile(path, 'utf-8');
const parsedContent = JSON.parse(fileContent);
if (!Array.isArray(parsedContent)) {
this.debugLogger.warn(
`Checkpoint file at ${path} is not a valid JSON array. Returning empty checkpoint.`,
);
return [];
}
return parsedContent as Content[];
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === 'ENOENT') {
// This is okay, it just means the checkpoint doesn't exist in either format.
return [];
}
this.debugLogger.error(
`Failed to read or parse checkpoint file ${path}:`,
error,
);
return [];
}
}
Deleting Checkpoints
From logger.ts:377:
async deleteCheckpoint(tag: string): Promise<boolean> {
if (!this.initialized || !this.qwenDir) {
this.debugLogger.error(
'Logger not initialized or checkpoint file path not set. Cannot delete checkpoint.',
);
return false;
}
let deletedSomething = false;
// 1. Attempt to delete the new encoded path.
const newPath = this._checkpointPath(tag);
try {
await fs.unlink(newPath);
deletedSomething = true;
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code !== 'ENOENT') {
this.debugLogger.error(
`Failed to delete checkpoint file ${newPath}:`,
error,
);
throw error; // Rethrow unexpected errors
}
// It's okay if it doesn't exist.
}
// 2. Attempt to delete the old raw path for backward compatibility.
const oldPath = path.join(this.qwenDir!, `checkpoint-${tag}.json`);
if (newPath !== oldPath) {
try {
await fs.unlink(oldPath);
deletedSomething = true;
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code !== 'ENOENT') {
this.debugLogger.error(
`Failed to delete checkpoint file ${oldPath}:`,
error,
);
throw error; // Rethrow unexpected errors
}
// It's okay if it doesn't exist.
}
}
return deletedSomething;
}
Checking Checkpoint Existence
From logger.ts:426:
async checkpointExists(tag: string): Promise<boolean> {
if (!this.initialized) {
throw new Error(
'Logger not initialized. Cannot check for checkpoint existence.',
);
}
const path = await this._getCheckpointPath(tag);
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
Session Resume
Command Line Usage
# Start new session
qwen "Create a web server"
# Session ID: abc-123-def
# Resume later
qwen --resume abc-123-def
# Resume with new input
qwen --resume abc-123-def "Add authentication"
Resume Process
From packages/core/src/services/sessionService.ts:581:
/**
* Builds the model-facing chat history (Content[]) from a reconstructed
* conversation. This keeps UI history intact while applying chat compression
* checkpoints for the API history used on resume.
*
* Strategy:
* - Find the latest system/chat_compression record (if any).
* - Use its compressedHistory snapshot as the base history.
* - Append all messages after that checkpoint (skipping system records).
* - If no checkpoint exists, return the linear message list (message field only).
*/
export function buildApiHistoryFromConversation(
conversation: ConversationRecord,
options: BuildApiHistoryOptions = {},
): Content[] {
const { stripThoughtsFromHistory = true } = options;
const { messages } = conversation;
let lastCompressionIndex = -1;
let compressedHistory: Content[] | undefined;
messages.forEach((record, index) => {
if (record.type === 'system' && record.subtype === 'chat_compression') {
const payload = record.systemPayload as
| ChatCompressionRecordPayload
| undefined;
if (payload?.compressedHistory) {
lastCompressionIndex = index;
compressedHistory = payload.compressedHistory;
}
}
});
if (compressedHistory && lastCompressionIndex >= 0) {
const baseHistory: Content[] = structuredClone(compressedHistory);
// Append everything after the compression record (newer turns)
for (let i = lastCompressionIndex + 1; i < messages.length; i++) {
const record = messages[i];
if (record.type === 'system') continue;
if (record.message) {
baseHistory.push(structuredClone(record.message as Content));
}
}
if (stripThoughtsFromHistory) {
return baseHistory
.map(stripThoughtsFromContent)
.filter((content): content is Content => content !== null);
}
return baseHistory;
}
// Fallback: return linear messages as Content[]
const result = messages
.map((record) => record.message)
.filter((message): message is Content => message !== undefined)
.map((message) => structuredClone(message));
if (stripThoughtsFromHistory) {
return result
.map(stripThoughtsFromContent)
.filter((content): content is Content => content !== null);
}
return result;
}
Session Data Structure
From packages/core/src/services/chatRecordingService.ts:102:
/**
* Stored payload for chat compression checkpoints. This allows us to rebuild the
* effective chat history on resume while keeping the original UI-visible history.
*/
export interface ChatCompressionRecordPayload {
/**
* Compressed conversation history snapshot. Used as base when building API
* resume reconstruction.
*/
compressedHistory: Content[];
/**
* Original token count before compression (for tracking metrics).
*/
oldTokenCount?: number;
/**
* New token count after compression.
*/
newTokenCount?: number;
/**
* Copy of the raw Content[] array sent to the compression model/prompt for
* the CLI (without IDs). Stored as plain objects for replay on resume.
*/
rawInputHistory?: Content[];
}
Chat Compression
Compression Triggers
From packages/core/src/config/config.ts, chat compression activates based on:
export interface ChatCompressionSettings {
contextPercentageThreshold?: number; // Default: 70%
}
When prompt tokens exceed 70% of model’s context window, compression is triggered.
Compression Process
From packages/core/src/core/prompts.ts:356:
const COMPRESS_SYSTEM_PROMPT = `
You are a chat history compression agent. Your job is to distill a long conversation into a concise XML snapshot.
When the conversation history grows too large, you will be invoked to distill the entire history into a concise, structured XML snapshot. This snapshot is CRITICAL, as it will become the agent's *only* memory of the past. The agent will resume its work based solely on this snapshot. All crucial details, plans, errors, and user directives MUST be preserved.
Guidelines:
- Preserve all important context, decisions, and state
- Remove redundant explanations and verbose responses
- Keep technical details and error messages
- Maintain chronological order of significant events
- Include user preferences and directives
Output format:
<conversation_snapshot>
<context>
(Essential background and setup)
</context>
<progress>
(Key accomplishments and current state)
</progress>
<pending>
(Unfinished tasks and next steps)
</pending>
<errors>
(Important errors and their resolutions)
</errors>
</conversation_snapshot>
`;
Compression Benefits
- Token Reduction: 50-80% reduction in prompt tokens
- Cost Savings: Lower API costs for long sessions
- Performance: Faster response times with smaller context
- Context Retention: Important information preserved
Compression Example
Before compression (10,000 tokens):
[
{"role": "user", "parts": [{"text": "Create a web server"}]},
{"role": "model", "parts": [{"text": "I'll help you create..."}]},
{"role": "user", "parts": [{"text": "Add routing"}]},
{"role": "model", "parts": [{"text": "Let me add routing..."}]},
// ... 50+ more turns ...
{"role": "user", "parts": [{"text": "Fix the bug"}]},
{"role": "model", "parts": [{"text": "I'll investigate..."}]}
]
After compression (2,500 tokens):
[
{
"role": "user",
"parts": [{
"text": "<conversation_snapshot>\n<context>\nCreating Express.js web server with routing, authentication, and database integration.\n</context>\n<progress>\n- Set up Express server on port 3000\n- Implemented JWT authentication\n- Connected to PostgreSQL database\n- Created 5 API endpoints\n</progress>\n<pending>\n- Fix CORS error in production\n- Add rate limiting\n- Write integration tests\n</pending>\n<errors>\n- Database connection timeout (resolved: increased pool size)\n- JWT verification failing (resolved: fixed secret key)\n</errors>\n</conversation_snapshot>"
}]
},
// Recent uncompressed messages continue here
{"role": "user", "parts": [{"text": "Fix the bug"}]},
{"role": "model", "parts": [{"text": "I'll investigate..."}]}
]
UI Telemetry Persistence
Recording Telemetry
From packages/core/src/services/chatRecordingService.ts:414:
/**
* Records a UI telemetry event for replaying metrics on resume.
*/
recordUiTelemetryEvent(uiEvent: UiTelemetryEvent): void {
const record: MessageRecord = {
uuid: randomUUID(),
parentUuid: this.getCurrentParentUuid(),
timestamp: Date.now(),
type: 'system',
subtype: 'ui_telemetry',
role: 'system',
systemPayload: {
uiEvent
}
};
this.appendMessage(record);
}
Replaying Telemetry
From sessionService.ts:648:
/**
* Replays stored UI telemetry events to rebuild metrics when resuming a session.
* Also restores the last prompt token count from the best available source.
*/
export function replayUiTelemetryFromConversation(
conversation: ConversationRecord,
): void {
uiTelemetryService.reset();
for (const record of conversation.messages) {
if (record.type !== 'system' || record.subtype !== 'ui_telemetry') {
continue;
}
const payload = record.systemPayload as
| UiTelemetryRecordPayload
| undefined;
const uiEvent = payload?.uiEvent;
if (uiEvent) {
uiTelemetryService.addEvent(uiEvent);
}
}
const resumePromptTokens = getResumePromptTokenCount(conversation);
if (resumePromptTokens !== undefined) {
uiTelemetryService.setLastPromptTokenCount(resumePromptTokens);
}
}
This restores:
- Token counts (prompt, cached, completion)
- Request counts
- Model usage statistics
- Timing information
Git Integration
Checkpointing can integrate with Git for version control.
From packages/core/src/services/gitService.ts:32:
/**
* The Git repository is used to support checkpointing.
*/
async initializeRepo(): Promise<void> {
try {
// Check if Git is available
if (!await this.isGitInstalled()) {
throw new Error(
'Checkpointing is enabled, but Git is not installed. Please install Git or disable checkpointing to continue.',
);
}
// Initialize or verify repository
await this.ensureGitRepo();
} catch (error) {
throw new Error(
`Failed to initialize checkpointing: ${error instanceof Error ? error.message : 'Unknown error'}. Please check that Git is working properly or disable checkpointing.`,
);
}
}
From gitService.ts:110:
async createCheckpointSnapshot(message: string): Promise<void> {
try {
await this.gitCommit(message);
} catch (error) {
throw new Error(
`Failed to create checkpoint snapshot: ${error instanceof Error ? error.message : 'Unknown error'}. Checkpointing may not be working properly.`,
);
}
}
Each checkpoint can create a Git commit:
# Automatic commits during session
git log --oneline
abc123 Checkpoint: All checks passed. This is a stable checkpoint.
def456 Checkpoint: API endpoints implemented
ghi789 Checkpoint: Database schema updated
SDK Integration
TypeScript SDK
From packages/sdk-typescript/src/types/types.ts:37:
/**
* Equivalent to CLI's --resume flag.
*/
resume?: string;
/**
* Session ID for the current query.
* When resume is provided, this should match the resume ID.
*/
sessionId?: string;
Usage:
import { createQuery } from '@qwen-code/sdk-typescript';
// Start new session
const query1 = await createQuery({
prompt: "Create a function",
sessionId: "my-session-123"
});
// Resume later
const query2 = await createQuery({
prompt: "Add error handling",
resume: "my-session-123" // Resumes previous session
});
From packages/sdk-typescript/src/query/createQuery.ts:46:
const sessionId = options.resume ?? options.sessionId ?? randomUUID();
Process Transport
From packages/sdk-typescript/src/transport/ProcessTransport.ts:263:
if (this.options.resume) {
// Add --resume flag
args.push('--resume', this.options.resume);
}
The SDK automatically passes --resume to the CLI process.
Best Practices
When to Create Checkpoints
-
Before Major Changes:
qwen --checkpoint "before-refactor" "Refactor the API"
-
After Milestones:
# After completing a feature
qwen --checkpoint "feature-complete" "Tests passing"
-
Before Risky Operations:
qwen --checkpoint "before-migration" "Migrate database"
Session Naming
Use descriptive session IDs:
# Good: Descriptive
qwen --session-id "api-refactor-2024-03" "Refactor API"
qwen --session-id "bug-fix-auth-issue" "Fix auth bug"
# Bad: Generic
qwen --session-id "session1" "Do stuff"
qwen --session-id "test" "Test things"
Use clear, meaningful tags:
// Good tags
"pre-refactor"
"all-tests-passing"
"stable-v1.0"
"before:database/migration"
// Bad tags
"checkpoint1"
"temp"
"test"
Troubleshooting
Session Not Found
Problem: Error: Session <id> not found
Solution:
# List available sessions
ls ~/.qwen/sessions/
# Verify session exists
ls ~/.qwen/sessions/<session-id>/
# Check for conversation file
cat ~/.qwen/sessions/<session-id>/conversation.jsonl
Checkpoint Load Failed
Problem: Checkpoint returns empty array
Causes:
- Checkpoint file doesn’t exist
- Invalid JSON format
- File corruption
Solution:
# Check checkpoint file
ls -la ~/.qwen/sessions/<session-id>/checkpoint-*.json
# Validate JSON
jq . ~/.qwen/sessions/<session-id>/checkpoint-<tag>.json
# Restore from backup if corrupted
cp ~/.qwen/sessions/<session-id>/conversation.jsonl.bak \
~/.qwen/sessions/<session-id>/conversation.jsonl
Git Initialization Failed
Problem: Checkpointing fails with Git errors
Solution:
# Verify Git is installed
git --version
# Initialize Git in session directory
cd ~/.qwen/sessions/<session-id>/
git init
# Or disable Git integration
qwen config set checkpointing.useGit false
Token Count Mismatch
Problem: Resumed session shows incorrect token counts
Solution: Token counts are restored from compression checkpoints or last usage metadata. If incorrect:
// Telemetry is replayed on resume
// Check for compression checkpoint
const hasCompression = conversation.messages.some(
r => r.type === 'system' && r.subtype === 'chat_compression'
);
if (!hasCompression) {
// No compression checkpoint - using fallback
// Token count from last assistant message
}
Advanced Topics
Custom Checkpoint Storage
import { Logger } from '@qwen-code/qwen-code-core';
const logger = new Logger('custom-path');
// Save to custom location
await logger.saveCheckpoint(conversation, 'milestone-1');
// Load from custom location
const restored = await logger.loadCheckpoint('milestone-1');
Checkpoint Migration
# Migrate old checkpoint format to new
for file in ~/.qwen/sessions/*/checkpoint-*.json; do
if [[ -f "$file" ]]; then
# Validate and re-save with encoding
jq . "$file" > "$file.tmp" && mv "$file.tmp" "$file"
fi
done
Programmatic Resume
import { SessionService } from '@qwen-code/qwen-code-core';
const sessionService = new SessionService();
// Load session
const resumedData = await sessionService.loadSession(sessionId);
// Rebuild API history
const apiHistory = buildApiHistoryFromConversation(
resumedData.conversation,
{ stripThoughtsFromHistory: true }
);
// Replay telemetry
replayUiTelemetryFromConversation(resumedData.conversation);
// Continue session
const response = await contentGenerator.generateContent({
contents: [...apiHistory, newUserMessage]
});
Source Code References
- Logger (checkpoints):
packages/core/src/core/logger.ts:286-430
- Session service:
packages/core/src/services/sessionService.ts:581-680
- Chat recording:
packages/core/src/services/chatRecordingService.ts:33,102,414
- Git integration:
packages/core/src/services/gitService.ts:32,110
- SDK types:
packages/sdk-typescript/src/types/types.ts:37-45
- Compression prompts:
packages/core/src/core/prompts.ts:356