رفتن به محتوا

پایدارسازیِ نشست‌ها در ذخیره‌سازیِ بیرونی

به‌صورت پیش‌فرض، SDK رونوشت‌های نشست (session transcripts) را در فایل‌های JSONL زیرِ ~/.claude/projects/ روی فایل‌سیستمِ محلی می‌نویسد. یک آداپتورِ SessionStore به تو اجازه می‌دهد آن رونوشت‌ها را به backendِ خودت — مثلِ S3، Redis یا یک دیتابیس — آینه (mirror) کنی، تا نشستی که روی یک میزبان ساخته شده روی میزبانی دیگر از سر گرفته شود.

دلایلِ رایج برای استفاده از session store:

  • استقرارهای چند‌میزبانه. توابعِ serverless، workerهای خودمقیاس‌شونده و runnerهای CI فایل‌سیستم را به اشتراک نمی‌گذارند. یک storeِ مشترک به هر replica اجازه می‌دهد هر نشستی را از سر بگیرد.
  • ماندگاری. کانتینرهای محلی زودگذرند. یک storeِ پشتیبانی‌شده با S3 یا یک دیتابیس از ری‌استارت‌ها و استقرارهای مجدد جانِ سالم به در می‌برد.
  • انطباق و ممیزی. رونوشت‌ها را در ذخیره‌سازی‌ای که از قبل کنترلش می‌کنی نگه دار، با قواعدِ نگه‌داریِ خودت، رمزنگاری و کنترل‌های دسترسی.

یک SessionStore آبجکتی است با دو متدِ الزامی، append و load، و سه متدِ اختیاری. SDK، append را برای نوشتنِ ورودی‌های رونوشت در حینِ یک query و load را برای بازخوانیِ آن‌ها هنگامِ از سرگیری فرا می‌خواند.

// Exported from @anthropic-ai/claude-agent-sdk as
// SessionStore, SessionKey, SessionStoreEntry.
type SessionKey = {
projectKey: string;
sessionId: string;
subpath?: string;
};
type SessionStore = {
// Required
append(key: SessionKey, entries: SessionStoreEntry[]): Promise<void>;
load(key: SessionKey): Promise<SessionStoreEntry[] | null>;
// Optional
listSessions?(
projectKey: string,
): Promise<Array<{ sessionId: string; mtime: number }>>;
delete?(key: SessionKey): Promise<void>;
listSubkeys?(key: {
projectKey: string;
sessionId: string;
}): Promise<string[]>;
};
# Exported from claude_agent_sdk as
# SessionStore, SessionKey, SessionStoreEntry.
class SessionKey(TypedDict):
project_key: str
session_id: str
subpath: NotRequired[str]
class SessionStore(Protocol):
# Required
async def append(
self, key: SessionKey, entries: list[SessionStoreEntry]
) -> None: ...
async def load(self, key: SessionKey) -> list[SessionStoreEntry] | None: ...
# Optional — omit or raise NotImplementedError
async def list_sessions(
self, project_key: str
) -> list[SessionStoreListEntry]: ...
async def delete(self, key: SessionKey) -> None: ...
async def list_subkeys(self, key: SessionListSubkeysKey) -> list[str]: ...

SessionKey به یک رونوشت اشاره می‌کند. projectKey یک کدگذاریِ پایدار و امن برای فایل‌سیستم از دایرکتوریِ کاری است، sessionId همان UUID نشست است، و subpath وقتی تنظیم می‌شود که ورودی به یک رونوشتِ ساب‌ایجنت یا فایلِ کناری (sidecar) تعلق داشته باشد، نه به مکالمه‌ی اصلی. با subpath مثلِ یک پسوندِ کلیدِ مبهم رفتار کن؛ از چیدمانِ روی‌دیسک پیروی می‌کند، مثلاً subagents/agent-<id>. وقتی subpath تعریف‌نشده باشد، کلید به رونوشتِ اصلی اشاره می‌کند.

متدالزامیکِی فرا خوانده می‌شود
appendبلهپس از هر دسته از ورودی‌های رونوشت که محلی نوشته می‌شود. ورودی‌ها آبجکت‌های امن برای JSON هستند، هر کدام یک خط در JSONL محلی.
loadبلهیک‌بار پیش از spawn شدنِ زیرفرایند، وقتی resume تنظیم شده باشد. اگر نشست ناشناخته است null برگردان.
listSessionsخیرتوسطِ listSessions({ sessionStore }) و توسطِ query()/startup() با continue: true. اگر تعریف‌نشده باشد، آن فراخوانی‌ها خطا می‌دهند.
deleteخیرتوسطِ deleteSession({ sessionStore }). حذفِ کلیدِ اصلی (بدونِ subpath) باید به‌صورتِ آبشاری همه‌ی subkeyهای آن نشست را حذف کند. اگر تعریف‌نشده باشد، حذف یک no-op است، که به backendهای فقط‌افزایشی می‌خورد.
listSubkeysخیردر حینِ از سرگیری، برای کشفِ رونوشت‌های ساب‌ایجنت. اگر تعریف‌نشده باشد، فقط رونوشتِ اصلی بازیابی می‌شود.

SDK یک InMemorySessionStore برای توسعه و تست همراه دارد. مثالِ زیر یک query را با storeِ متصل اجرا می‌کند، session ID را از پیامِ نتیجه می‌گیرد، سپس در یک فراخوانیِ دومِ query() نشست را از store از سر می‌گیرد. فراخوانیِ دوم همان نمونه‌ی store را به‌علاوه‌ی resume پاس می‌دهد، پس SDK رونوشت را به‌جای فایل‌سیستمِ محلی از store بارگذاری می‌کند:

import { query, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";
const store = new InMemorySessionStore();
let sessionId: string | undefined;
for await (const message of query({
prompt: "List the TypeScript files under src/",
options: { sessionStore: store },
})) {
if (message.type === "result") {
sessionId = message.session_id;
}
}
// Resume from the store. The agent has full context from the first call.
for await (const message of query({
prompt: "Summarize what those files do",
options: { sessionStore: store, resume: sessionId },
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
import asyncio
from claude_agent_sdk import (
ClaudeAgentOptions,
InMemorySessionStore,
ResultMessage,
query,
)
store = InMemorySessionStore()
async def main():
session_id = None
async for message in query(
prompt="List the Python files under src/",
options=ClaudeAgentOptions(session_store=store),
):
if isinstance(message, ResultMessage):
session_id = message.session_id
# Resume from the store. The agent has full context from the first call.
async for message in query(
prompt="Summarize what those files do",
options=ClaudeAgentOptions(session_store=store, resume=session_id),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())

query دوم خلاصه‌ای از فایل‌های query اول را چاپ می‌کند، که نشان می‌دهد ایجنت با کانتکستِ کامل از store از سر گرفته شد.

append و load را در برابرِ backendت پیاده‌سازی کن. اگر می‌خواهی listSessions()، deleteSession() و از سرگیریِ ساب‌ایجنت در برابرِ store کار کنند، listSessions، delete و listSubkeys را اضافه کن.

ورودی‌هایی که به append پاس داده می‌شوند از نوعِ SessionStoreEntry هستند (یک آبجکتِ { type: string; ... }). با آن‌ها مثلِ مقادیرِ مبهمِ امن برای JSON رفتار کن: آن‌ها را به ترتیب پایدار کن و از load به همان ترتیب برگردان. load باید ورودی‌هایی برگرداند که با آنچه append شده deep-equal باشند؛ سریال‌سازیِ byte-equal لازم نیست، پس backendهایی مثلِ jsonb در Postgres که کلیدهای آبجکت را بازچینش می‌کنند مشکلی ندارند.

مخزنِ SDKِ TypeScript آداپتورهای مرجعِ قابل‌اجرا برای S3، Redis و Postgres را زیرِ examples/session-stores/ دارد. این‌ها روی npm منتشر نشده‌اند؛ فایلِ src/ موردِ نیازت را در پروژه‌ات کپی کن و کلاینتِ backendِ متناظر را نصب کن.

آداپتورکلاینتِ backendمدلِ ذخیره‌سازی
S3SessionStore@aws-sdk/client-s3یک فایلِ JSONL به ازای هر append()؛ load() فهرست، مرتب و الحاق می‌کند.
RedisSessionStoreioredisیک لیستِ RPUSH/LRANGE به ازای هر رونوشت، به‌علاوه‌ی یک ایندکسِ نشستِ sorted-set.
PostgresSessionStorepgیک ردیف به ازای هر ورودی در یک جدولِ jsonb، مرتب‌شده با BIGSERIAL.

هر آداپتور یک نمونه‌ی کلاینتِ از پیش‌پیکربندی‌شده می‌گیرد، پس تو اعتبارنامه‌ها، TLS، region و pooling را کنترل می‌کنی. مثلاً با S3:

import { query } from "@anthropic-ai/claude-agent-sdk";
import { S3Client } from "@aws-sdk/client-s3";
import { S3SessionStore } from "./S3SessionStore"; // copied from examples/session-stores/s3
const store = new S3SessionStore({
bucket: "my-claude-sessions",
prefix: "transcripts",
client: new S3Client({ region: "us-east-1" }),
});
for await (const message of query({
prompt: "Hello!",
options: { sessionStore: store },
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
// Later, possibly on a different host:
for await (const message of query({
prompt: "Continue where we left off",
options: { sessionStore: store, resume: "previous-session-id" },
})) {
// ...
}

آداپتورت را اعتبارسنجی کن

Section titled “آداپتورت را اعتبارسنجی کن”

هر دو SDK یک مجموعه‌ی conformance همراه دارند که قراردادِ رفتاری‌ای را که append، load و متدهای اختیاری باید برآورده کنند تأیید می‌کند. تست‌های متدهای اختیاری وقتی آن متدها پیاده‌سازی نشده باشند به‌صورت خودکار رد (skip) می‌شوند.

در TypeScript، shared/conformance.ts را از دایرکتوریِ مثال در مجموعه‌تستت کپی کن. در Python، این مجموعه درونِ بسته می‌آید:

import pytest
from claude_agent_sdk.testing import run_session_store_conformance
@pytest.mark.asyncio
async def test_my_store_conformance():
await run_session_store_conformance(MyRedisStore)

store یک آینه است، نه جایگزین. زیرفرایندِ Claude Code همیشه اول روی دیسکِ محلی می‌نویسد؛ سپس SDK هر دسته را به append() فوروارد می‌کند. اگر می‌خواهی نسخه‌ی محلی زودگذر باشد، CLAUDE_CONFIG_DIR را در options.env به یک دایرکتوریِ موقت اشاره بده. چون آینه به نوشتن‌های محلی وابسته است، sessionStore را نمی‌توان با persistSession: false ترکیب کرد؛ اگر هر دو را تنظیم کنی SDK خطا می‌دهد. همچنین اگر با enableFileCheckpointing ترکیب شود خطا می‌دهد، چون blobهای پشتیبانِ تاریخچه‌ی فایل مستقیماً روی دیسکِ محلی نوشته می‌شوند و به store آینه نمی‌شوند.

نوشتن‌های آینه best-effort هستند

Section titled “نوشتن‌های آینه best-effort هستند”

اگر append() رد شود یا تایم‌اوت بخورد، خطا لاگ می‌شود، یک پیامِ { type: "system", subtype: "mirror_error" } به iterator فرستاده می‌شود و query ادامه پیدا می‌کند. رونوشتِ محلی از قبل روی دیسک ماندگار است، پس یک قطعیِ store ایجنت را قطع نمی‌کند یا داده را محلی از دست نمی‌دهد. دسته‌هایی که شکست می‌خورند دوباره تلاش نمی‌شوند، پس اگر باید از دست رفتنِ داده‌ی store را تشخیص دهی، mirror_error را پایش کن.

getSessionMessages زنجیره‌ی پس از فشرده‌سازی را برمی‌گرداند

Section titled “getSessionMessages زنجیره‌ی پس از فشرده‌سازی را برمی‌گرداند”

getSessionMessages({ sessionStore }) زنجیره‌ی پیامِ پیوندی‌ای را برمی‌گرداند که ایجنت هنگامِ از سرگیری می‌بیند. پس از فشرده‌سازیِ خودکار (auto-compaction)، نوبت‌های قبلی با یک خلاصه جایگزین می‌شوند، پس نشستی که storeش ۵۰۳ ورودیِ خام دارد ممکن است از getSessionMessages ۱۸ پیام برگرداند. برای تاریخچه‌ی خامِ کامل — از جمله نوبت‌های پیش از فشرده‌سازی و ورودی‌های متادیتا — مستقیماً store.load(key) را فرا بخوان.

forkSession یک کپیِ byte به byte نیست

Section titled “forkSession یک کپیِ byte به byte نیست”

forkSession({ sessionStore }) ورودی‌های منبع را می‌خواند، هر فیلدِ sessionId را بازنویسی و UUIDهای پیام را remap می‌کند، سپس ورودی‌های تبدیل‌شده را زیرِ یک کلیدِ جدید append می‌کند. یک کپی در سطحِ آداپتور یا میان‌برِ CopyObject، رونوشتی تولید می‌کرد که هنوز به session IDِ قدیمی ارجاع می‌داد، پس SDK از آن استفاده نمی‌کند.

رونوشت‌های ساب‌ایجنت زیرِ subpath: "subagents/agent-<id>" آینه می‌شوند. listSubagents({ sessionStore }) ایجاب می‌کند که آداپتور listSubkeys را پیاده‌سازی کند؛ getSubagentMessages({ sessionStore }) در صورتِ موجود بودن از آن استفاده می‌کند ولی وقتی تعریف‌نشده باشد به subpathِ مستقیم برمی‌گردد. از سرگیری هم listSubkeys را برای بازیابیِ فایل‌های ساب‌ایجنت فرا می‌خواند؛ بدونِ آن، فقط رونوشتِ اصلی materialize می‌شود.

SDK هرگز خودش از store تو چیزی حذف نمی‌کند. نگه‌داری مسئولیتِ آداپتور است: TTLها، سیاست‌های lifecycle برای S3، یا پاک‌سازیِ زمان‌بندی‌شده را بر اساسِ الزاماتِ انطباقت پیاده‌سازی کن. رونوشت‌های محلیِ زیرِ CLAUDE_CONFIG_DIR به‌صورت مستقل توسطِ تنظیمِ cleanupPeriodDays جارو می‌شوند.

توابعِ SDK زیر گزینه‌ی sessionStore را می‌پذیرند و وقتی فراهم شده باشد، به‌جای فایل‌سیستمِ محلی در برابرِ store عمل می‌کنند: