@langchain/langgraph-checkpoint-redis 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.cjs","names":["sortedObj: Record<string, any>","sorted: Record<string, any>","BaseCheckpointSaver","prevCheckpointData: any","prevCheckpointId: string | null","jsonDoc: any","pendingWrites: Array<[string, string, any]> | undefined","queryParts: string[]","error: any","limit","writeKeys: string[]","zaddArgs: Record<string, number>","pendingWrites: Array<[string, string, any]>","sanitized: any"],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n _newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(metadata),\n checkpoint_ts: Date.now(),\n has_writes: \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Channel values are stored inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n queryParts.push(`(@${key}:{${value}})`);\n } else if (typeof value === \"number\") {\n queryParts.push(`(@${key}:[${value} ${value}])`);\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n yield this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n yield this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `write_keys_zset:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to create checkpoint tuple from json document\n private createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): CheckpointTuple {\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata: jsonDoc.metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n writeDoc.value,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shallow.cjs","names":["BaseCheckpointSaver","escapeRediSearchTagValue"],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n _newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(metadata),\n checkpoint_ts: Date.now(),\n has_writes: \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Deserialize checkpoint using serde to restore LangChain objects\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n // Escape both key and value to prevent RediSearch query injection\n const escapedKey = escapeRediSearchTagValue(key);\n const escapedValue = escapeRediSearchTagValue(value);\n queryParts.push(`(@${escapedKey}:{${escapedValue}})`);\n } else if (typeof value === \"number\") {\n // Escape key to prevent injection; numbers don't need value escaping\n const escapedKey = escapeRediSearchTagValue(key);\n queryParts.push(`(@${escapedKey}:[${value} ${value}])`);\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `write_keys_zset:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to create checkpoint tuple from json document\n private async createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): Promise<CheckpointTuple> {\n // Deserialize metadata using serde\n const metadata = (await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.metadata)\n )) as CheckpointMetadata;\n\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n // Deserialize write value using serde to restore LangChain objects\n const deserializedValue = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(writeDoc.value)\n );\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n deserializedValue,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -3,8 +3,8 @@ import { RunnableConfig } from "@langchain/core/runnables";
3
3
 
4
4
  //#region src/shallow.d.ts
5
5
  interface TTLConfig {
6
- defaultTTL?: number; // TTL in minutes
7
- refreshOnRead?: boolean; // Whether to refresh TTL when reading
6
+ defaultTTL?: number;
7
+ refreshOnRead?: boolean;
8
8
  }
9
9
  /**
10
10
  * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.
@@ -29,13 +29,10 @@ declare class ShallowRedisSaver extends BaseCheckpointSaver {
29
29
  putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;
30
30
  deleteThread(threadId: string): Promise<void>;
31
31
  end(): Promise<void>;
32
- // Helper method to add searchable metadata fields
33
32
  private addSearchableMetadataFields;
34
- // Helper method to create checkpoint tuple from json document
35
33
  private createCheckpointTuple;
36
34
  private applyTTL;
37
35
  private loadPendingWrites;
38
- // Helper method to check metadata filter matches
39
36
  private checkMetadataFilterMatch;
40
37
  private cleanupOldCheckpoint;
41
38
  private sanitizeMetadata;
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.d.cts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport declare class ShallowRedisSaver extends BaseCheckpointSaver {\n private client;\n private ttlConfig?;\n constructor(client: any, ttlConfig?: TTLConfig);\n static fromUrl(url: string, ttlConfig?: TTLConfig): Promise<ShallowRedisSaver>;\n get(config: RunnableConfig): Promise<Checkpoint | undefined>;\n put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise<RunnableConfig>;\n getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;\n list(config: RunnableConfig | null, options?: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }): AsyncGenerator<CheckpointTuple>;\n putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;\n deleteThread(threadId: string): Promise<void>;\n end(): Promise<void>;\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields;\n // Helper method to create checkpoint tuple from json document\n private createCheckpointTuple;\n private applyTTL;\n private loadPendingWrites;\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch;\n private cleanupOldCheckpoint;\n private sanitizeMetadata;\n private ensureIndexes;\n}\n//# sourceMappingURL=shallow.d.ts.map"],"mappings":";;;;UAEiBQ,SAAAA;;EAAAA,aAAS,CAAA,EAAA,OAAA,CAAA,CAAA;AAa1B;;;;;;;;;;AAM4CN,cANvBO,iBAAAA,SAA0BT,mBAAAA,CAMHE;UAAsBE,MAAAA;UAAkCH,SAAAA;aAA0BM,CAAAA,MAAAA,EAAAA,GAAAA,EAAAA,SAAAA,CAAAA,EAHrFC,SAGqFD;SAARG,OAAAA,CAAAA,GAAAA,EAAAA,MAAAA,EAAAA,SAAAA,CAAAA,EAF1EF,SAE0EE,CAAAA,EAF9DA,OAE8DA,CAFtDD,iBAEsDC,CAAAA;KACjGH,CAAAA,MAAAA,EAFLA,cAEKA,CAAAA,EAFYG,OAEZH,CAFoBL,UAEpBK,GAAAA,SAAAA,CAAAA;KAAyBF,CAAAA,MAAAA,EAD9BE,cAC8BF,EAAAA,UAAAA,EADFH,UACEG,EAAAA,QAAAA,EADoBD,kBACpBC,EAAAA,YAAAA,EADsDJ,eACtDI,CAAAA,EADwEK,OACxEL,CADgFE,cAChFF,CAAAA;UAARK,CAAAA,MAAAA,EAAjBH,cAAiBG,CAAAA,EAAAA,OAAAA,CAAQL,eAARK,GAAAA,SAAAA,CAAAA;MACrBH,CAAAA,MAAAA,EAAAA,cAAAA,GAAAA,IAAAA,EAAAA,QAAAA,EAAiCJ,qBAAjCI,GAAAA;IAAiCJ,MAAAA,CAAAA,EACjCC,kBADiCD;MAE1CQ,cADSP,CACMC,eADND,CAAAA;WACMC,CAAAA,MAAAA,EACDE,cADCF,EAAAA,MAAAA,EACuBC,YADvBD,EAAAA,EAAAA,MAAAA,EAAAA,MAAAA,CAAAA,EACwDK,OADxDL,CAAAA,IAAAA,CAAAA;cAAfM,CAAAA,QAAAA,EAAAA,MAAAA,CAAAA,EAE4BD,OAF5BC,CAAAA,IAAAA,CAAAA;KACcJ,CAAAA,CAAAA,EAEXG,OAFWH,CAAAA,IAAAA,CAAAA;;UAAyDG,2BAAAA;;UAEpEA,qBAAAA;UAboCV,QAAAA"}
1
+ {"version":3,"file":"shallow.d.cts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\n}\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport declare class ShallowRedisSaver extends BaseCheckpointSaver {\n private client;\n private ttlConfig?;\n constructor(client: any, ttlConfig?: TTLConfig);\n static fromUrl(url: string, ttlConfig?: TTLConfig): Promise<ShallowRedisSaver>;\n get(config: RunnableConfig): Promise<Checkpoint | undefined>;\n put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise<RunnableConfig>;\n getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;\n list(config: RunnableConfig | null, options?: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }): AsyncGenerator<CheckpointTuple>;\n putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;\n deleteThread(threadId: string): Promise<void>;\n end(): Promise<void>;\n private addSearchableMetadataFields;\n private createCheckpointTuple;\n private applyTTL;\n private loadPendingWrites;\n private checkMetadataFilterMatch;\n private cleanupOldCheckpoint;\n private sanitizeMetadata;\n private ensureIndexes;\n}\n//# sourceMappingURL=shallow.d.ts.map"],"mappings":";;;;UAEiBQ,SAAAA;;EAAAA,aAAS,CAAA,EAAA,OAAA;AAa1B;;;;;;;;;;AAM4CN,cANvBO,iBAAAA,SAA0BT,mBAAAA,CAMHE;UAAsBE,MAAAA;UAAkCH,SAAAA;aAA0BM,CAAAA,MAAAA,EAAAA,GAAAA,EAAAA,SAAAA,CAAAA,EAHrFC,SAGqFD;SAARG,OAAAA,CAAAA,GAAAA,EAAAA,MAAAA,EAAAA,SAAAA,CAAAA,EAF1EF,SAE0EE,CAAAA,EAF9DA,OAE8DA,CAFtDD,iBAEsDC,CAAAA;KACjGH,CAAAA,MAAAA,EAFLA,cAEKA,CAAAA,EAFYG,OAEZH,CAFoBL,UAEpBK,GAAAA,SAAAA,CAAAA;KAAyBF,CAAAA,MAAAA,EAD9BE,cAC8BF,EAAAA,UAAAA,EADFH,UACEG,EAAAA,QAAAA,EADoBD,kBACpBC,EAAAA,YAAAA,EADsDJ,eACtDI,CAAAA,EADwEK,OACxEL,CADgFE,cAChFF,CAAAA;UAARK,CAAAA,MAAAA,EAAjBH,cAAiBG,CAAAA,EAAAA,OAAAA,CAAQL,eAARK,GAAAA,SAAAA,CAAAA;MACrBH,CAAAA,MAAAA,EAAAA,cAAAA,GAAAA,IAAAA,EAAAA,QAAAA,EAAiCJ,qBAAjCI,GAAAA;IAAiCJ,MAAAA,CAAAA,EACjCC,kBADiCD;MAE1CQ,cADSP,CACMC,eADND,CAAAA;WACMC,CAAAA,MAAAA,EACDE,cADCF,EAAAA,MAAAA,EACuBC,YADvBD,EAAAA,EAAAA,MAAAA,EAAAA,MAAAA,CAAAA,EACwDK,OADxDL,CAAAA,IAAAA,CAAAA;cAAfM,CAAAA,QAAAA,EAAAA,MAAAA,CAAAA,EAE4BD,OAF5BC,CAAAA,IAAAA,CAAAA;KACcJ,CAAAA,CAAAA,EAEXG,OAFWH,CAAAA,IAAAA,CAAAA;UAAwBD,2BAAAA;UAAiCI,qBAAAA;UAC3CA,QAAAA;UACzBA,iBAAAA;UAboCV,wBAAAA;EAAmB,QAAA,oBAAA"}
package/dist/shallow.d.ts CHANGED
@@ -3,8 +3,8 @@ import { RunnableConfig } from "@langchain/core/runnables";
3
3
 
4
4
  //#region src/shallow.d.ts
5
5
  interface TTLConfig {
6
- defaultTTL?: number; // TTL in minutes
7
- refreshOnRead?: boolean; // Whether to refresh TTL when reading
6
+ defaultTTL?: number;
7
+ refreshOnRead?: boolean;
8
8
  }
9
9
  /**
10
10
  * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.
@@ -29,13 +29,10 @@ declare class ShallowRedisSaver extends BaseCheckpointSaver {
29
29
  putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;
30
30
  deleteThread(threadId: string): Promise<void>;
31
31
  end(): Promise<void>;
32
- // Helper method to add searchable metadata fields
33
32
  private addSearchableMetadataFields;
34
- // Helper method to create checkpoint tuple from json document
35
33
  private createCheckpointTuple;
36
34
  private applyTTL;
37
35
  private loadPendingWrites;
38
- // Helper method to check metadata filter matches
39
36
  private checkMetadataFilterMatch;
40
37
  private cleanupOldCheckpoint;
41
38
  private sanitizeMetadata;
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.d.ts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport declare class ShallowRedisSaver extends BaseCheckpointSaver {\n private client;\n private ttlConfig?;\n constructor(client: any, ttlConfig?: TTLConfig);\n static fromUrl(url: string, ttlConfig?: TTLConfig): Promise<ShallowRedisSaver>;\n get(config: RunnableConfig): Promise<Checkpoint | undefined>;\n put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise<RunnableConfig>;\n getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;\n list(config: RunnableConfig | null, options?: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }): AsyncGenerator<CheckpointTuple>;\n putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;\n deleteThread(threadId: string): Promise<void>;\n end(): Promise<void>;\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields;\n // Helper method to create checkpoint tuple from json document\n private createCheckpointTuple;\n private applyTTL;\n private loadPendingWrites;\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch;\n private cleanupOldCheckpoint;\n private sanitizeMetadata;\n private ensureIndexes;\n}\n//# sourceMappingURL=shallow.d.ts.map"],"mappings":";;;;UAEiBQ,SAAAA;;EAAAA,aAAS,CAAA,EAAA,OAAA,CAAA,CAAA;AAa1B;;;;;;;;;;AAM4CN,cANvBO,iBAAAA,SAA0BT,mBAAAA,CAMHE;UAAsBE,MAAAA;UAAkCH,SAAAA;aAA0BM,CAAAA,MAAAA,EAAAA,GAAAA,EAAAA,SAAAA,CAAAA,EAHrFC,SAGqFD;SAARG,OAAAA,CAAAA,GAAAA,EAAAA,MAAAA,EAAAA,SAAAA,CAAAA,EAF1EF,SAE0EE,CAAAA,EAF9DA,OAE8DA,CAFtDD,iBAEsDC,CAAAA;KACjGH,CAAAA,MAAAA,EAFLA,cAEKA,CAAAA,EAFYG,OAEZH,CAFoBL,UAEpBK,GAAAA,SAAAA,CAAAA;KAAyBF,CAAAA,MAAAA,EAD9BE,cAC8BF,EAAAA,UAAAA,EADFH,UACEG,EAAAA,QAAAA,EADoBD,kBACpBC,EAAAA,YAAAA,EADsDJ,eACtDI,CAAAA,EADwEK,OACxEL,CADgFE,cAChFF,CAAAA;UAARK,CAAAA,MAAAA,EAAjBH,cAAiBG,CAAAA,EAAAA,OAAAA,CAAQL,eAARK,GAAAA,SAAAA,CAAAA;MACrBH,CAAAA,MAAAA,EAAAA,cAAAA,GAAAA,IAAAA,EAAAA,QAAAA,EAAiCJ,qBAAjCI,GAAAA;IAAiCJ,MAAAA,CAAAA,EACjCC,kBADiCD;MAE1CQ,cADSP,CACMC,eADND,CAAAA;WACMC,CAAAA,MAAAA,EACDE,cADCF,EAAAA,MAAAA,EACuBC,YADvBD,EAAAA,EAAAA,MAAAA,EAAAA,MAAAA,CAAAA,EACwDK,OADxDL,CAAAA,IAAAA,CAAAA;cAAfM,CAAAA,QAAAA,EAAAA,MAAAA,CAAAA,EAE4BD,OAF5BC,CAAAA,IAAAA,CAAAA;KACcJ,CAAAA,CAAAA,EAEXG,OAFWH,CAAAA,IAAAA,CAAAA;;UAAyDG,2BAAAA;;UAEpEA,qBAAAA;UAboCV,QAAAA"}
1
+ {"version":3,"file":"shallow.d.ts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\n}\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport declare class ShallowRedisSaver extends BaseCheckpointSaver {\n private client;\n private ttlConfig?;\n constructor(client: any, ttlConfig?: TTLConfig);\n static fromUrl(url: string, ttlConfig?: TTLConfig): Promise<ShallowRedisSaver>;\n get(config: RunnableConfig): Promise<Checkpoint | undefined>;\n put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise<RunnableConfig>;\n getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;\n list(config: RunnableConfig | null, options?: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }): AsyncGenerator<CheckpointTuple>;\n putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;\n deleteThread(threadId: string): Promise<void>;\n end(): Promise<void>;\n private addSearchableMetadataFields;\n private createCheckpointTuple;\n private applyTTL;\n private loadPendingWrites;\n private checkMetadataFilterMatch;\n private cleanupOldCheckpoint;\n private sanitizeMetadata;\n private ensureIndexes;\n}\n//# sourceMappingURL=shallow.d.ts.map"],"mappings":";;;;UAEiBQ,SAAAA;;EAAAA,aAAS,CAAA,EAAA,OAAA;AAa1B;;;;;;;;;;AAM4CN,cANvBO,iBAAAA,SAA0BT,mBAAAA,CAMHE;UAAsBE,MAAAA;UAAkCH,SAAAA;aAA0BM,CAAAA,MAAAA,EAAAA,GAAAA,EAAAA,SAAAA,CAAAA,EAHrFC,SAGqFD;SAARG,OAAAA,CAAAA,GAAAA,EAAAA,MAAAA,EAAAA,SAAAA,CAAAA,EAF1EF,SAE0EE,CAAAA,EAF9DA,OAE8DA,CAFtDD,iBAEsDC,CAAAA;KACjGH,CAAAA,MAAAA,EAFLA,cAEKA,CAAAA,EAFYG,OAEZH,CAFoBL,UAEpBK,GAAAA,SAAAA,CAAAA;KAAyBF,CAAAA,MAAAA,EAD9BE,cAC8BF,EAAAA,UAAAA,EADFH,UACEG,EAAAA,QAAAA,EADoBD,kBACpBC,EAAAA,YAAAA,EADsDJ,eACtDI,CAAAA,EADwEK,OACxEL,CADgFE,cAChFF,CAAAA;UAARK,CAAAA,MAAAA,EAAjBH,cAAiBG,CAAAA,EAAAA,OAAAA,CAAQL,eAARK,GAAAA,SAAAA,CAAAA;MACrBH,CAAAA,MAAAA,EAAAA,cAAAA,GAAAA,IAAAA,EAAAA,QAAAA,EAAiCJ,qBAAjCI,GAAAA;IAAiCJ,MAAAA,CAAAA,EACjCC,kBADiCD;MAE1CQ,cADSP,CACMC,eADND,CAAAA;WACMC,CAAAA,MAAAA,EACDE,cADCF,EAAAA,MAAAA,EACuBC,YADvBD,EAAAA,EAAAA,MAAAA,EAAAA,MAAAA,CAAAA,EACwDK,OADxDL,CAAAA,IAAAA,CAAAA;cAAfM,CAAAA,QAAAA,EAAAA,MAAAA,CAAAA,EAE4BD,OAF5BC,CAAAA,IAAAA,CAAAA;KACcJ,CAAAA,CAAAA,EAEXG,OAFWH,CAAAA,IAAAA,CAAAA;UAAwBD,2BAAAA;UAAiCI,qBAAAA;UAC3CA,QAAAA;UACzBA,iBAAAA;UAboCV,wBAAAA;EAAmB,QAAA,oBAAA"}
package/dist/shallow.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { escapeRediSearchTagValue } from "./utils.js";
1
2
  import { BaseCheckpointSaver, uuid6 } from "@langchain/langgraph-checkpoint";
2
3
  import { createClient } from "redis";
3
4
 
@@ -114,8 +115,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
114
115
  return saver;
115
116
  }
116
117
  async get(config) {
117
- const tuple = await this.getTuple(config);
118
- return tuple?.checkpoint;
118
+ return (await this.getTuple(config))?.checkpoint;
119
119
  }
120
120
  async put(config, checkpoint, metadata, _newVersions) {
121
121
  await this.ensureIndexes();
@@ -160,19 +160,16 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
160
160
  const threadId = config.configurable?.thread_id;
161
161
  const checkpointNs = config.configurable?.checkpoint_ns ?? "";
162
162
  const checkpointId = config.configurable?.checkpoint_id;
163
- if (!threadId) return void 0;
163
+ if (!threadId) return;
164
164
  const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;
165
165
  const jsonDoc = await this.client.json.get(key);
166
- if (!jsonDoc) return void 0;
167
- if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) return void 0;
166
+ if (!jsonDoc) return;
167
+ if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) return;
168
168
  if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) await this.applyTTL(key);
169
- const checkpoint = {
170
- ...jsonDoc.checkpoint,
171
- channel_values: jsonDoc.checkpoint.channel_values || {}
172
- };
169
+ const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
173
170
  let pendingWrites;
174
171
  if (jsonDoc.has_writes === "true") pendingWrites = await this.loadPendingWrites(jsonDoc.thread_id, jsonDoc.checkpoint_ns, jsonDoc.checkpoint_id);
175
- return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
172
+ return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
176
173
  }
177
174
  async *list(config, options) {
178
175
  await this.ensureIndexes();
@@ -184,8 +181,14 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
184
181
  } else {
185
182
  const queryParts = [];
186
183
  if (options?.filter) {
187
- for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") queryParts.push(`(@${key}:{${value}})`);
188
- else if (typeof value === "number") queryParts.push(`(@${key}:[${value} ${value}])`);
184
+ for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") {
185
+ const escapedKey = escapeRediSearchTagValue(key);
186
+ const escapedValue = escapeRediSearchTagValue(value);
187
+ queryParts.push(`(@${escapedKey}:{${escapedValue}})`);
188
+ } else if (typeof value === "number") {
189
+ const escapedKey = escapeRediSearchTagValue(key);
190
+ queryParts.push(`(@${escapedKey}:[${value} ${value}])`);
191
+ }
189
192
  }
190
193
  if (queryParts.length === 0) queryParts.push("*");
191
194
  const query = queryParts.join(" ");
@@ -212,24 +215,20 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
212
215
  if (options?.filter) {
213
216
  if (!this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)) continue;
214
217
  }
215
- const checkpoint = {
216
- ...jsonDoc.checkpoint,
217
- channel_values: jsonDoc.checkpoint.channel_values || {}
218
- };
219
- yield this.createCheckpointTuple(jsonDoc, checkpoint);
218
+ const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
219
+ yield await this.createCheckpointTuple(jsonDoc, checkpoint);
220
220
  yieldCount++;
221
221
  }
222
222
  } catch (error) {
223
223
  if (error.message?.includes("no such index")) {
224
- const pattern = `checkpoint:*:*:shallow`;
225
- const keys = await this.client.keys(pattern);
224
+ const keys = await this.client.keys(`checkpoint:*:*:shallow`);
226
225
  if (keys.length === 0) return;
227
226
  keys.sort().reverse();
228
227
  const seenThreads = /* @__PURE__ */ new Set();
229
228
  let yieldCount = 0;
230
- const limit$1 = options?.limit ?? 10;
229
+ const limit = options?.limit ?? 10;
231
230
  for (const key of keys) {
232
- if (yieldCount >= limit$1) break;
231
+ if (yieldCount >= limit) break;
233
232
  const jsonDoc = await this.client.json.get(key);
234
233
  if (!jsonDoc) continue;
235
234
  const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;
@@ -238,11 +237,8 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
238
237
  if (options?.filter) {
239
238
  if (!this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)) continue;
240
239
  }
241
- const checkpoint = {
242
- ...jsonDoc.checkpoint,
243
- channel_values: jsonDoc.checkpoint.channel_values || {}
244
- };
245
- yield this.createCheckpointTuple(jsonDoc, checkpoint);
240
+ const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
241
+ yield await this.createCheckpointTuple(jsonDoc, checkpoint);
246
242
  yieldCount++;
247
243
  }
248
244
  return;
@@ -290,8 +286,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
290
286
  if (this.ttlConfig?.defaultTTL) await this.applyTTL(...writeKeys, zsetKey);
291
287
  }
292
288
  const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;
293
- const checkpointExists = await this.client.exists(checkpointKey);
294
- if (checkpointExists) {
289
+ if (await this.client.exists(checkpointKey)) {
295
290
  const currentDoc = await this.client.json.get(checkpointKey);
296
291
  if (currentDoc) {
297
292
  currentDoc.has_writes = "true";
@@ -320,7 +315,8 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
320
315
  if ("writes" in metadata) jsonDoc.writes = typeof metadata.writes === "object" ? JSON.stringify(metadata.writes) : metadata.writes;
321
316
  if ("score" in metadata) jsonDoc.score = metadata.score;
322
317
  }
323
- createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
318
+ async createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
319
+ const metadata = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.metadata));
324
320
  return {
325
321
  config: { configurable: {
326
322
  thread_id: jsonDoc.thread_id,
@@ -328,7 +324,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
328
324
  checkpoint_id: jsonDoc.checkpoint_id
329
325
  } },
330
326
  checkpoint,
331
- metadata: jsonDoc.metadata,
327
+ metadata,
332
328
  parentConfig: jsonDoc.parent_checkpoint_id ? { configurable: {
333
329
  thread_id: jsonDoc.thread_id,
334
330
  checkpoint_ns: jsonDoc.checkpoint_ns,
@@ -346,15 +342,18 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
346
342
  async loadPendingWrites(threadId, checkpointNs, checkpointId) {
347
343
  const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
348
344
  const writeKeys = await this.client.zRange(zsetKey, 0, -1);
349
- if (writeKeys.length === 0) return void 0;
345
+ if (writeKeys.length === 0) return;
350
346
  const pendingWrites = [];
351
347
  for (const writeKey of writeKeys) {
352
348
  const writeDoc = await this.client.json.get(writeKey);
353
- if (writeDoc) pendingWrites.push([
354
- writeDoc.task_id,
355
- writeDoc.channel,
356
- writeDoc.value
357
- ]);
349
+ if (writeDoc) {
350
+ const deserializedValue = await this.serde.loadsTyped("json", JSON.stringify(writeDoc.value));
351
+ pendingWrites.push([
352
+ writeDoc.task_id,
353
+ writeDoc.channel,
354
+ deserializedValue
355
+ ]);
356
+ }
358
357
  }
359
358
  return pendingWrites;
360
359
  }
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.js","names":["sortedObj: Record<string, any>","sorted: Record<string, any>","prevCheckpointData: any","prevCheckpointId: string | null","jsonDoc: any","pendingWrites: Array<[string, string, any]> | undefined","queryParts: string[]","error: any","limit","writeKeys: string[]","zaddArgs: Record<string, number>","pendingWrites: Array<[string, string, any]>","sanitized: any"],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n _newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(metadata),\n checkpoint_ts: Date.now(),\n has_writes: \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Channel values are stored inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n queryParts.push(`(@${key}:{${value}})`);\n } else if (typeof value === \"number\") {\n queryParts.push(`(@${key}:[${value} ${value}])`);\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n yield this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n yield this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `write_keys_zset:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to create checkpoint tuple from json document\n private createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): CheckpointTuple {\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata: jsonDoc.metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n writeDoc.value,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shallow.js","names":[],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n _newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(metadata),\n checkpoint_ts: Date.now(),\n has_writes: \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Deserialize checkpoint using serde to restore LangChain objects\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n // Escape both key and value to prevent RediSearch query injection\n const escapedKey = escapeRediSearchTagValue(key);\n const escapedValue = escapeRediSearchTagValue(value);\n queryParts.push(`(@${escapedKey}:{${escapedValue}})`);\n } else if (typeof value === \"number\") {\n // Escape key to prevent injection; numbers don't need value escaping\n const escapedKey = escapeRediSearchTagValue(key);\n queryParts.push(`(@${escapedKey}:[${value} ${value}])`);\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `write_keys_zset:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to create checkpoint tuple from json document\n private async createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): Promise<CheckpointTuple> {\n // Deserialize metadata using serde\n const metadata = (await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.metadata)\n )) as CheckpointMetadata;\n\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n // Deserialize write value using serde to restore LangChain objects\n const deserializedValue = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(writeDoc.value)\n );\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n deserializedValue,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}