Checkpoints and resume
A session lives only in memory by default. To keep talking after a process restart you need two things: a SessionStore (where to save) and the store / resume fields of createSession (when to save, when to load). After context compaction the original text is no longer in the history, which is why there are also snapshots (checkpoints): a self-contained read-only copy of the complete context at a point in time, which can be listed, restored and deleted.
SessionStore and resume
Section titled “SessionStore and resume”import { createFileSessionStore, createSession } from '@tansr/sdk';
const store = createFileSessionStore({ dir: app.getPath('userData') }); // 目录显式传,恒不隐式扫用户目录
// 落卷:store 在场即自动接管「会话登记 + 每轮落卷」const session = await createSession({ token, baseUrl, store });session.send('记住:发射码是 7-4-1');await session.idle();const savedId = session.sessionId; // 逻辑会话 id(UUID 形制),跨重启凭它取回
// 进程重启后:resume 一等恢复(取回历史 + 配对治理 + 预载)const resumed = await createSession({ token, baseUrl, resume: { sessionId: savedId, store }, // 只恢复;要继续落卷,store 字段同时传 store,});resumed.send('复述发射码'); // 前文事实已在上下文
// 枚举与删除经 store 接口面(list 只有元数据,没有内容)const sessions = await store.list({ limit: 20 }); // SessionRecordMeta[]await store.delete(savedId);Key semantics:
- resume always fails with a structured error: a missing target throws
session_not_found; corrupted storage (a bad line in the middle / a broken hash chain / a bad payload) throwssession_store_corrupted. It never silently starts an empty session. - Pairing governance is built in: on resume, a truncated
tool_callgets an isError placeholder and orphantool_results are cut back to a clean boundary, so the restored history can be sent straight to the model without a 400. The low-levelinitialMessagesreload path does not go through governance. - Crash self-healing:
FileSessionStoreinherits the kernel journal primitives (hash chain / crash truncation repair / file lock); a half-written line after a power cut is truncated automatically and does not count as corruption. - Platform shadow (token tier): each session mints a platform session ULID that travels with every request, and the mapping lands in
SessionRecordMeta.platformSessionId;resumereuses the same one, so cache affinity continues across restarts. The platform-side lease is a 30-day inactivity window, and conversation activity renews it implicitly.
The standard “resume first, create only if that fails” pattern:
import { TansrSdkError, createFileSessionStore, createSession } from '@tansr/sdk';
async function openOrResume(lastId: string | undefined) { if (lastId === undefined) return createSession({ token, baseUrl, store }); try { return await createSession({ token, baseUrl, store, resume: { sessionId: lastId, store } }); } catch (e) { // 只有「确实不在 store」才新建;session_store_corrupted 等恒上抛 if (e instanceof TansrSdkError && e.code === 'session_not_found') return createSession({ token, baseUrl, store }); throw e; }}Your own storage: five methods and two rules
Section titled “Your own storage: five methods and two rules”Implement the five SessionStore methods create / get / list / commit / delete and you can inject it (SQLite, an IndexedDB proxy in the main process, your own backend — all the same shape). Two rules:
- When
commitreceivesrewritten = trueit must rotate the whole volume; never append incrementally by length — a compaction fold or a snapshot restore rewrites the history prefix in place. - Corruption always throws a structured error, never silently.
Two optional members: getHistoryPage?(sessionId, { offset, limit }) (paged reads; when absent, falls back to slicing the full history) and checkpoints?: CheckpointStore (snapshot storage with the same lifecycle as the session; absent = checkpoints not wired).
Cold restore of the attachment admission state (since @tansr/sdk 0.18.0, released 2026-10-02): a Store may declare the optional capability flag attachmentCheckpoints: true, promising to save commit’s meta.attachmentCheckpoint (an opaque value generated by the SDK) atomically in the same write as messages and to return it unchanged from get(); resume then cold-restores the attachment admission state (cumulative injected bytes / rejection count / entries) together with the history. When a Store has actually repaired this history, it sets attachmentReferencesInvalidated: true on the record, and the SDK invalidates the attachment references while keeping the cumulative admission budget. The built-in createFileSessionStore implements it; a custom five-method Store that does not declare the flag behaves exactly as in 0.17 (cold restore conservatively invalidates references and never fabricates missing state). Implementers must not parse, construct or replace this value with client metadata.
File layout and segment sealing
Section titled “File layout and segment sealing”createFileSessionStore({ dir, segmentation? }) writes journal.jsonl (the active segment) + meta.json + attachments/ under <dir>/sessions/<sessionId>/. Long sessions are sealed dynamically into segments/000001.jsonl… + manifest.json once a threshold is reached (enabled by default at 16 MiB / 5000 records; segmentation: { enabled: false } keeps a single volume forever). Old directories (without a manifest) are readable with zero migration; whole-volume rotation always returns to a single active segment, and messages from before a compaction never come back; a crash midway through rotation is completed automatically on the next access, and a half volume is never visible.
The checkpoint directory is one of three choices, all explicit by default: absent = co-located with the session at <sessionDir>/checkpoints/ (disappears together with store.delete); checkpoints: { dir } = a separate directory with its own lifecycle; checkpoints: { store } = your own CheckpointStore (four methods put / get / list / delete).
Media artifacts and storage version 2 (since @tansr/sdk 0.15.0 / @tansr/serve 0.10.0): the artifacts of image / video / speech tools (tool_result.artifact) are kept with the session history, preserved through hot / cold restore and checkpoints alike, and after resume the SessionView can rebuild the tool card preview (setting artifactUnavailable: true when the preview cannot be restored safely); the model still consumes only the original content. The first write of an artifact upgrades the session volume to storage version 2: writers of older versions cannot keep writing an upgraded directory — back up before upgrading, and upgrade every program that writes the same session storage (SDK hosts / tansr serve / the CLI) together; rolling back to an older version requires the original backup or a separate directory. Old text-only histories are not backfilled with media.
Bounded fault-tolerant reads (since 0.15.0, off by default): createFileSessionStore({ recovery: { mode: 'verified-ir', quarantineDir?, onRecovery? } }) quarantines message records that are definitely invalid within the full verification chain, writes the raw evidence outside the session directory (by default the sibling .session-recovery-quarantine) and calls the host back with a SessionRecoveryReport (counts, positions and content fingerprints, no message text); if onRecovery throws, that read fails instead of degrading silently. Before enabling it, check the capability of the installed runtime package with SESSION_RECOVERY_VERSION so an older package cannot silently ignore the option. The default remains strict reading — corruption always throws a structured error.
Manual compaction and snapshots
Section titled “Manual compaction and snapshots”const session = await createSession({ token, baseUrl, store, checkpoints: { dir: path.join(app.getPath('userData'), 'checkpoints'), max: 20 },});
// 手动快照(任何空闲时刻;运行中 throw turn_running)const mark = await session.checkpoint({ label: 'before compaction' });// → CheckpointMeta { checkpointId(ULID,时间有序), trigger: 'manual', label, messageCount, createdAt, cwd, model, … }
// 手动压缩:缺省先落 pre_compaction 快照,再把历史前缀折叠为摘要const result = await session.compact({ instructions: 'keep the launch code' });if (result.status === 'compacted') { console.log(result.compactionId, result.removedRange, result.checkpointId);} else if (result.status === 'rejected') { console.log(result.reason); // 'turn_running' | 'empty_history' | 'not_configured' | 'hook_blocked'} else { console.log(result.reason, result.checkpointId); // failed:已落的快照保留}
for (const cp of await session.listCheckpoints()) console.log(cp.checkpointId, cp.trigger, cp.label, cp.messageCount);
// 恢复:同会话就地换史;缺省先落 pre_restore 快照const restored = await session.restore(mark.checkpointId);if (restored.status === 'restored') console.log(restored.fromMessages, '→', restored.toMessages, restored.preRestoreCheckpointId);
await session.deleteCheckpoint(mark.checkpointId); // 幂等Signatures (the named option types CompactOptions / CheckpointOptions / RestoreOptions are expanded inline; 0.19.0 is identical to 0.18.x):
compact(options?: { instructions?: string; checkpoint?: boolean | { label?: string } }): Promise<CompactResult>;checkpoint(options?: { label?: string }): Promise<CheckpointMeta>;listCheckpoints(): Promise<CheckpointMeta[]>;restore(checkpointId: string, options?: { checkpoint?: boolean }): Promise<RestoreResult>;deleteCheckpoint(checkpointId: string): Promise<void>;Key points:
- Idle-only semantics:
compact/checkpoint/restore/deleteCheckpointare always rejected while a turn is running — no queueing, zero side effects.compact()/restore()express it as the resultrejected: 'turn_running',checkpoint()/deleteCheckpoint()throw a structuredturn_running; retry afterawait session.idle(). - Snapshot switch: when
compact({ checkpoint })is absent,checkpoints.autoBeforeCompact(default true) decides; automatic in-turn compaction takes itspre_compactionsnapshot by the same rule. When checkpoints are not wired,compact()still compacts but takes no snapshot, andrestore()returnsrejected: 'store_not_wired'. - Failure semantics:
not_configured= compaction disabled or the context window unknown;empty_history= the history is empty;failedcarries the kernel reason (nothing_to_compact/summary_malformed/model_error/before_compact_failed…), observable throughturn.error(scope='compaction', recoverable), and snapshots already taken are kept. - Restore: first reconciles the
sessionId(a snapshot from another session is rejected withsession_mismatch), then replaces the history as a whole and emitssession.restored; the store performs a whole-volume rotation. Restoring the same snapshot twice in a row rotates the volume the second time too. When the snapshotcwddiffers from the sessioncwd, the cwd is left unchanged and acwd_mismatch_on_restorenotice is pushed. - Retention: unlimited by default; when
checkpoints.maxis reached the oldest are evicted in time order regardless of trigger. Snapshot files are always written durably (temp + fsync + rename). - Event order:
session.checkpointed → session.compacted;session.checkpointed(pre_restore) → session.restored.
History paging
Section titled “History paging”A long-session UI does not have to fetch everything at once. history({ offset, limit }) pages by message index, and the returned slice always has complete pairs — a start that falls inside a tool-call round is aligned backwards and offset is corrected, the end is aligned forwards (limit is a lower bound).
let cursor = 0;const first = await session.history({ offset: 0, limit: 50 });while (cursor < first.total) { const p = await session.history({ offset: cursor, limit: 50 }); render(p.messages); cursor = p.nextOffset; // 恒以 nextOffset 推进,不要用 offset + messages.length}
const { total } = await session.history({ limit: 0 }); // 只取 totalconst last = await session.history({ offset: Math.max(0, total - 50), limit: 50 }); // 尾页For dormant sessions (no AgentSession started) use readHistoryPage(store, sessionId, { offset, limit }); the getHistoryPage of createFileSessionStore skips through the segment manifest, so the cost depends on the size of the tail, not on the total length of the volume.
fork and snapshot export / import
Section titled “fork and snapshot export / import”session.fork(checkpointId)grows a new session out of a snapshot; the source session is untouched. It needs the sessionstoreto place the new record, otherwise it throwssession_store_not_wired.exportCheckpoint(checkpointId)produces self-contained bytes (formattansr-checkpoint/1, image attachments inlined);importCheckpoint(bytes)verifies the format / schema / attachment sha256 / message shapes, and any failure throwscheckpoint_import_invalid— tampering is rejected with nothing written to disk. The imported item belongs to the target session and can be restored or forked directly.
Relationship to the session service
Section titled “Relationship to the session service”The /v2 surface of @tansr/serve offers the same set of capabilities over HTTP (compact / checkpoints / restore / history paging / fork / export / import) with the semantics described here; since 0.13.0 new integrations call the same operations through the unified call surface /api/sessions/**, with unchanged domain payload bytes. Server-side session retention and physical deletion policy are described in Retention and governance.
Since @tansr/sdk 0.18.0 the sub-entry @tansr/sdk/extensions additionally provides a local archive host (createNodeArchiveHost): it installs a persistent archive handover and an optional materials chain for the session alongside the original SessionStore, open still returns the original AgentSession, and the store / resume / checkpoint semantics on this page are unchanged. It is neither a backup nor multi-device sync (the storage strategy is fixed to single-authorized-source, with replication / synchronization both 'none'). See SDK 2.0 extension entries.
Was this page helpful?
Thanks for your feedback.