import fs from 'node:fs/promises' import path from 'node:path' import type { EthagentIdentity } from '../../storage/config.js' import { atomicWriteText } from '../../storage/atomicWrite.js' import type { ContinuityFiles } from './envelope.js' import { continuityVaultRef, ensureContinuityVault, writeAgentCardFile, writeContinuityFiles, type PrivateContinuityFile, } from './storage.js' type PrivateContinuityHistorySnapshot = { version: 1 id: string createdAt: string file: PrivateContinuityFile filePath: string existedBefore: boolean previousContent: string previousFiles?: ContinuityFiles previousAgentCard?: string changeSummary: string identity: { address: string ownerAddress?: string chainId?: number identityRegistryAddress?: string agentId?: string } sessionId?: string turnId?: string promptSnippet?: string checkpointLabel?: string } type RecordPrivateContinuityHistoryInput = { identity: EthagentIdentity file: PrivateContinuityFile filePath: string existedBefore: boolean previousContent: string previousFiles?: ContinuityFiles previousAgentCard?: string changeSummary: string createdAt?: string sessionId?: string turnId?: string promptSnippet?: string checkpointLabel?: string } function privateContinuityHistoryPath(identity: EthagentIdentity): string { return path.join(continuityVaultRef(identity).dir, '.history.jsonl') } export async function recordPrivateContinuityHistorySnapshot( input: RecordPrivateContinuityHistoryInput, ): Promise { await ensureContinuityVault(input.identity) const createdAt = input.createdAt ?? new Date().toISOString() const snapshot: PrivateContinuityHistorySnapshot = { version: 1, id: `${createdAt}:${input.file}`.replaceAll('\\', '/'), createdAt, file: input.file, filePath: path.resolve(input.filePath), existedBefore: input.existedBefore, previousContent: input.previousContent, ...(input.previousFiles ? { previousFiles: input.previousFiles } : {}), ...(input.previousAgentCard !== undefined ? { previousAgentCard: input.previousAgentCard } : {}), changeSummary: input.changeSummary, identity: { address: input.identity.address, ...(input.identity.ownerAddress ? { ownerAddress: input.identity.ownerAddress } : {}), ...(input.identity.chainId ? { chainId: input.identity.chainId } : {}), ...(input.identity.identityRegistryAddress ? { identityRegistryAddress: input.identity.identityRegistryAddress } : {}), ...(input.identity.agentId ? { agentId: input.identity.agentId } : {}), }, ...(input.sessionId ? { sessionId: input.sessionId } : {}), ...(input.turnId ? { turnId: input.turnId } : {}), ...(input.promptSnippet ? { promptSnippet: normalizeSnippet(input.promptSnippet) } : {}), ...(input.checkpointLabel ? { checkpointLabel: normalizeSnippet(input.checkpointLabel) } : {}), } await fs.appendFile(privateContinuityHistoryPath(input.identity), `${JSON.stringify(snapshot)}\n`, { encoding: 'utf8', mode: 0o600, }) return snapshot } export async function listPrivateContinuityHistory( identity: EthagentIdentity, limit = 30, ): Promise { let raw: string try { raw = await fs.readFile(privateContinuityHistoryPath(identity), 'utf8') } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [] throw error } const snapshots: PrivateContinuityHistorySnapshot[] = [] for (const line of raw.split('\n')) { const trimmed = line.trim() if (!trimmed) continue try { snapshots.push(JSON.parse(trimmed) as PrivateContinuityHistorySnapshot) } catch { continue } } return snapshots.reverse().slice(0, limit) } export async function restorePrivateContinuityHistorySnapshot( identity: EthagentIdentity, snapshotId: string, ): Promise { const snapshot = (await listPrivateContinuityHistory(identity, 500)) .find(item => item.id === snapshotId) if (!snapshot) throw new Error('Private continuity checkpoint was not found') if (snapshot.previousFiles) { await writeContinuityFiles(identity, snapshot.previousFiles) if (snapshot.previousAgentCard !== undefined) { await writeAgentCardFile(identity, snapshot.previousAgentCard) } const sync = await import('../../cli/sync.js') await sync.pushVaultSoulMemoryToHarness(identity).catch(() => undefined) return snapshot } await ensureContinuityVault(identity) if (snapshot.existedBefore) { await atomicWriteText(snapshot.filePath, ensureTrailingNewline(snapshot.previousContent), { mode: 0o600 }) } else { await fs.rm(snapshot.filePath, { force: true }) } if (snapshot.previousAgentCard !== undefined) { await writeAgentCardFile(identity, snapshot.previousAgentCard) } const sync = await import('../../cli/sync.js') await sync.pushVaultSoulMemoryToHarness(identity).catch(() => undefined) return snapshot } function normalizeSnippet(input: string): string { const normalized = input.replace(/\s+/g, ' ').trim() return normalized.length <= 120 ? normalized : `${normalized.slice(0, 117)}...` } function ensureTrailingNewline(value: string): string { return value.endsWith('\n') ? value : `${value}\n` }