@alfe.ai/openclaw-sync 0.2.10 → 0.3.1

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":"sync-engine.js","names":["log","formatBytes","log"],"sources":["../src/manifest.ts","../src/ignore.ts","../src/retry.ts","../src/uploader.ts","../src/downloader.ts","../src/sync-engine.ts"],"sourcesContent":["/**\n * AlfeSync manifest — local file manifest at `~/.alfe/sync/manifest.json`.\n *\n * Tracks file hashes, sizes, sync timestamps, and storage classes\n * to enable efficient diff-based syncing. Lives under `~/.alfe/sync/`\n * so workspace directories stay clean.\n *\n * `workspacePath` is accepted by every function for consistency with the\n * rest of the package, but the manifest itself is workspace-independent\n * (one agent, one workspace, one manifest).\n */\n\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync, createReadStream } from \"node:fs\";\nimport { createHash } from \"node:crypto\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/**\n * Local state directory for sync — manifest, etc. Lives under `~/.alfe/sync/`\n * so it stays alongside the rest of Alfe's agent state instead of polluting\n * the workspace.\n */\nconst SYNC_STATE_DIR = join(homedir(), \".alfe\", \"sync\");\n\nexport interface ManifestEntry {\n hash: string;\n size: number;\n lastSynced: string;\n storageClass: \"STANDARD\" | \"GLACIER_IR\";\n}\n\nexport interface LocalManifest {\n files: Record<string, ManifestEntry>;\n /**\n * Owner of this manifest. The manifest lives at a machine-global path\n * (`~/.alfe/sync/manifest.json`), so if a machine is re-adopted by a\n * DIFFERENT agent without clearing `~/.alfe/sync/`, the entries describe\n * the previous agent's sync state and must not be trusted for recovery\n * classification. Absent on pre-upgrade manifests — those are trusted\n * (grandfathered: same-agent continuation is overwhelmingly the norm, and\n * distrust would sidecar-spam every diverged file after upgrade).\n */\n agentId?: string;\n}\n\nexport interface RemoteManifest {\n version: 1;\n agentId: string;\n lastSync: string;\n files: Record<\n string,\n {\n hash: string;\n size: number;\n modified: string;\n etag?: string;\n storageClass?: string;\n compressed?: boolean;\n }\n >;\n}\n\nexport interface ManifestDiff {\n /** Files that exist locally but not on remote, or have changed locally */\n toPush: string[];\n /** Files that exist on remote but not locally, or are newer on remote */\n toPull: string[];\n /** Files that exist in both and have diverged (conflict) */\n conflicts: string[];\n /** Files that exist locally but were deleted on remote */\n remoteDeleted: string[];\n}\n\nconst MANIFEST_FILE = \"manifest.json\";\n\n/**\n * Resolve the manifest file path. Lives under `~/.alfe/sync/`, independent\n * of the workspace path — one agent has one manifest.\n */\nfunction manifestPath(): string {\n return join(SYNC_STATE_DIR, MANIFEST_FILE);\n}\n\n/**\n * Read the local manifest. Returns empty manifest if not found.\n *\n * `workspacePath` is accepted for call-site symmetry with the rest of the\n * package but is not used — the manifest path is resolved from\n * `~/.alfe/sync/` regardless of which workspace the call comes from.\n */\nexport async function readManifest(\n workspacePath: string,\n): Promise<LocalManifest> {\n void workspacePath;\n const path = manifestPath();\n if (!existsSync(path)) {\n return { files: {} };\n }\n\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as LocalManifest;\n } catch {\n return { files: {} };\n }\n}\n\n/**\n * Write the local manifest. `workspacePath` is accepted for call-site\n * symmetry but unused (see `readManifest`).\n */\nexport async function writeManifest(\n workspacePath: string,\n manifest: LocalManifest,\n): Promise<void> {\n void workspacePath;\n const path = manifestPath();\n await mkdir(SYNC_STATE_DIR, { recursive: true });\n await writeFile(path, JSON.stringify(manifest, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/**\n * Update a single file entry in the local manifest.\n */\nexport async function updateManifestEntry(\n workspacePath: string,\n relativePath: string,\n entry: ManifestEntry,\n): Promise<void> {\n const manifest = await readManifest(workspacePath);\n manifest.files[relativePath] = entry;\n await writeManifest(workspacePath, manifest);\n}\n\n/**\n * Update many file entries in one read-modify-write pass.\n *\n * `updateManifestEntry` re-reads and rewrites the whole manifest JSON per\n * call — fine for single-file watcher events, quadratic for bulk heals, and\n * lossy when calls overlap. Use this for any batch (e.g. the recovery\n * \"identical → heal\" pass). Optionally stamps the owning agentId.\n */\nexport async function updateManifestEntries(\n workspacePath: string,\n entries: Record<string, ManifestEntry>,\n options: { agentId?: string } = {},\n): Promise<void> {\n const manifest = await readManifest(workspacePath);\n Object.assign(manifest.files, entries);\n if (options.agentId) manifest.agentId = options.agentId;\n await writeManifest(workspacePath, manifest);\n}\n\n/**\n * Remove a file entry from the local manifest.\n */\nexport async function removeManifestEntry(\n workspacePath: string,\n relativePath: string,\n): Promise<void> {\n const manifest = await readManifest(workspacePath);\n const remainingFiles = Object.fromEntries(\n Object.entries(manifest.files).filter(([key]) => key !== relativePath),\n );\n manifest.files = remainingFiles;\n await writeManifest(workspacePath, manifest);\n}\n\n/**\n * Compute SHA-256 hash of a file using streaming (memory-efficient).\n * Returns `sha256:<hex>` format.\n */\nexport async function computeFileHash(filePath: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const hash = createHash(\"sha256\");\n const stream = createReadStream(filePath);\n\n stream.on(\"data\", (chunk) => hash.update(chunk));\n stream.on(\"end\", () => { resolve(`sha256:${hash.digest(\"hex\")}`); });\n stream.on(\"error\", reject);\n });\n}\n\n/**\n * Diff the local manifest against the remote manifest.\n *\n * Returns lists of files to push, pull, and conflicts.\n */\nexport function diffManifests(\n local: LocalManifest,\n remote: RemoteManifest,\n): ManifestDiff {\n const toPush: string[] = [];\n const toPull: string[] = [];\n const conflicts: string[] = [];\n const remoteDeleted: string[] = [];\n\n const localPaths = new Set(Object.keys(local.files));\n const remotePaths = new Set(Object.keys(remote.files));\n\n // Files only in local → push\n for (const path of localPaths) {\n if (!remotePaths.has(path)) {\n toPush.push(path);\n }\n }\n\n // Files only in remote → pull\n for (const path of remotePaths) {\n if (!localPaths.has(path)) {\n toPull.push(path);\n }\n }\n\n // Files in both → compare hashes\n for (const path of localPaths) {\n if (!remotePaths.has(path)) continue;\n\n const localEntry = local.files[path];\n const remoteEntry = remote.files[path];\n\n if (localEntry.hash === remoteEntry.hash) {\n // In sync — nothing to do\n continue;\n }\n\n // Hashes differ — determine direction\n const localSyncTime = new Date(localEntry.lastSynced).getTime();\n const remoteModTime = new Date(remoteEntry.modified).getTime();\n\n if (remoteModTime > localSyncTime) {\n // Remote is newer than our last sync — could be a conflict\n // If local file was also modified (hash differs from what we synced),\n // it's a conflict\n conflicts.push(path);\n } else {\n // Local is newer — push\n toPush.push(path);\n }\n }\n\n // Files that were in our manifest but deleted from remote\n for (const path of localPaths) {\n if (\n local.files[path].lastSynced !== \"\" &&\n !remotePaths.has(path)\n ) {\n remoteDeleted.push(path);\n }\n }\n\n return { toPush, toPull, conflicts, remoteDeleted };\n}\n","// AlfeSync ignore — parse .alfesyncignore (gitignore-style glob matching).\n// Uses micromatch for pattern matching, compatible with .gitignore syntax.\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport micromatch from \"micromatch\";\n\n// Committed default ignore files live next to this module (copied to\n// dist/defaults at build). `common` applies to every runtime; each runtime\n// (openclaw, hermes) adds its own runtime-internal dirs. These are the source\n// of truth for the baseline set — a hardcoded array used to live here.\n//\n// tsdown bundles this module into three entries at different depths\n// (dist/index.js, dist/plugin.js, dist/cli/index.js), so `./defaults` resolves\n// to dist/defaults for the top-level entries but dist/cli/defaults for the CLI.\n// The copy step only writes dist/defaults, so probe both `./defaults` and\n// `../defaults` and use whichever exists. In src (vitest) `./defaults` wins.\nconst MODULE_DIR = fileURLToPath(new URL(\".\", import.meta.url));\nconst DEFAULTS_DIR =\n [join(MODULE_DIR, \"defaults\"), join(MODULE_DIR, \"..\", \"defaults\")].find(existsSync) ??\n join(MODULE_DIR, \"defaults\");\n\n// Parse a committed default ignore file: lines are FULL micromatch globs,\n// used verbatim (no gitignore normalisation). Blank lines and `#` comments\n// are skipped.\nfunction parseDefaultGlobs(text: string): string[] {\n return text\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0 && !l.startsWith(\"#\"));\n}\n\n// Read a committed default file, tolerating a missing file (unknown runtime →\n// no extra patterns). Returns [] on any read error.\nfunction readDefaultGlobs(name: string): string[] {\n const file = join(DEFAULTS_DIR, `${name}.alfesyncignore`);\n try {\n return parseDefaultGlobs(readFileSync(file, \"utf-8\"));\n } catch {\n return [];\n }\n}\n\n// Load-bearing backstop if the committed common file can't be read (a bundler\n// stripped it, or the tsdown copy step regressed). Failing OPEN to \"ignore\n// nothing\" would re-watch node_modules + the runtime caches and resurrect the\n// exact ENOSPC crash-loop this fix exists to prevent — so the crash-prevention\n// entries are duplicated here. Kept intentionally minimal; the committed files\n// remain the source of truth. The `DEFAULT_IGNORES loaded from committed file`\n// test asserts the real file loaded (not this fallback) so a copy/bundle\n// regression fails CI rather than silently degrading in prod.\nconst CRITICAL_FALLBACK_IGNORES = [\n \"**/node_modules/**\",\n \"**/.git/**\",\n \"**/.env\",\n \"**/__pycache__/**\",\n \"npm/**\",\n \"extensions/**\",\n \"plugins/**\",\n \"**/.venv/**\",\n];\n\n// Default ignore patterns — always applied to every runtime. Exported so tests\n// and the docs generator can introspect the list without duplicating it.\n// Sourced from the committed `defaults/common.alfesyncignore`, with a hardcoded\n// fallback so an unreadable file never fails open.\n//\n// Patterns are full micromatch globs. To match a name at any depth use a\n// globstar prefix (two asterisks plus slash). The `matchBase` option is NOT\n// used because it breaks deep globstar/X/globstar matches in micromatch 4.x.\nconst commonGlobs = readDefaultGlobs(\"common\");\nexport const DEFAULT_IGNORES =\n commonGlobs.length > 0 ? commonGlobs : CRITICAL_FALLBACK_IGNORES;\n\n// Cache the per-runtime default set (common + runtime) so repeated\n// loadIgnorePatterns calls don't re-read the committed files.\nconst runtimeDefaultsCache = new Map<string, string[]>();\n\nfunction runtimeDefaults(runtime: string): string[] {\n const cached = runtimeDefaultsCache.get(runtime);\n if (cached) return cached;\n // common first, then runtime-specific; unknown runtime contributes nothing.\n const patterns = [...DEFAULT_IGNORES, ...readDefaultGlobs(runtime)];\n runtimeDefaultsCache.set(runtime, patterns);\n return patterns;\n}\n\n// Normalise a user-supplied gitignore-style pattern into a micromatch glob.\n// Mapping (input -> output):\n// foo -> **\\/foo (no slash: match anywhere)\n// /foo -> foo (leading slash: root-anchored)\n// foo/ -> **\\/foo/** (trailing slash: dir + contents anywhere)\n// path/to/foo -> path/to/foo (internal slash: leave anchored)\n// !pattern -> !<recurse on body> (negation; applied to body)\n// This keeps the historical .alfesyncignore UX (gitignore semantics) while\n// the engine matches on full globs without micromatch's matchBase option.\nexport function normaliseIgnorePattern(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return trimmed;\n if (trimmed.startsWith(\"!\")) return `!${normaliseIgnorePattern(trimmed.slice(1))}`;\n if (trimmed.startsWith(\"/\")) {\n const body = trimmed.slice(1);\n return body.endsWith(\"/\") ? `${body.slice(0, -1)}/**` : body;\n }\n // Decide based on whether the unsuffixed pattern has any internal slashes:\n // foo, *.log, scratch.* → basename-style, prepend **/\n // foo/ → basename-style dir, prepend **/ + suffix /**\n // path/to/foo, path/foo/ → workspace-anchored as written\n const stripped = trimmed.endsWith(\"/\") ? trimmed.slice(0, -1) : trimmed;\n const isAnchored = stripped.includes(\"/\");\n if (trimmed.endsWith(\"/\")) return isAnchored ? `${stripped}/**` : `**/${stripped}/**`;\n return isAnchored ? trimmed : `**/${trimmed}`;\n}\n\n// Load ignore patterns from the committed per-runtime defaults + the\n// workspace `.alfesyncignore`. Defaults (common + runtime-internal dirs) come\n// first; user patterns are appended last (so a `!negation` can un-ignore a\n// default) and run through normaliseIgnorePattern so a gitignore-style `*.log`\n// matches at any depth. `runtime` selects the runtime-specific default file\n// (openclaw / hermes); an unknown runtime falls back to the common set.\nexport async function loadIgnorePatterns(\n workspacePath: string,\n runtime = \"openclaw\",\n): Promise<string[]> {\n const ignoreFile = join(workspacePath, \".alfesyncignore\");\n const patterns = [...runtimeDefaults(runtime)];\n\n if (existsSync(ignoreFile)) {\n try {\n const content = await readFile(ignoreFile, \"utf-8\");\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n // Skip empty lines and comments\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n patterns.push(normaliseIgnorePattern(trimmed));\n }\n } catch {\n // Ignore read errors — use defaults only\n }\n }\n\n return patterns;\n}\n\n// Check if a relative path should be ignored. Negation patterns (!foo)\n// un-ignore a path matched by an earlier positive pattern — gitignore\n// semantics. micromatch.isMatch with an array does NOT honour negation\n// (it OR's all patterns), so we split positive/negative and combine\n// manually: ignored iff any positive matches AND no negative matches.\nexport function shouldIgnore(\n relativePath: string,\n patterns: string[],\n): boolean {\n const positive: string[] = [];\n const negative: string[] = [];\n for (const p of patterns) {\n if (p.startsWith(\"!\")) negative.push(p.slice(1));\n else positive.push(p);\n }\n if (positive.length === 0) return false;\n if (!micromatch.isMatch(relativePath, positive, { dot: true })) return false;\n if (negative.length === 0) return true;\n return !micromatch.isMatch(relativePath, negative, { dot: true });\n}\n\n// Check whether a DIRECTORY's contents are ignored — i.e. whether the watcher\n// / scanner may prune it and never descend. A directory `npm` isn't itself\n// matched by `npm/**` (that glob needs a child segment), so we probe a\n// sentinel child: if `npm/<probe>` is ignored, everything under `npm` is too.\n// Used by the chokidar `ignored` predicate and the workspace walk so a single\n// ignore engine governs both file matching and directory pruning.\nconst IGNORE_DIR_PROBE = \"__alfe_ignore_probe__\";\nexport function shouldIgnoreDir(\n relativeDir: string,\n patterns: string[],\n): boolean {\n if (relativeDir === \"\") return false; // never prune the workspace root\n // Forward-slash join — micromatch globs are posix-style regardless of OS.\n return shouldIgnore(`${relativeDir}/${IGNORE_DIR_PROBE}`, patterns);\n}\n\n// Filter a list of relative paths, removing ignored ones.\nexport function filterIgnored(\n paths: string[],\n patterns: string[],\n): string[] {\n return paths.filter((p) => !shouldIgnore(p, patterns));\n}\n","/**\n * Tiny retry helper used by uploader/downloader. Extracted so both\n * use the same timing and surface the same final error shape.\n */\n\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_BASE_DELAY_MS = 1000;\n\nexport interface RetryOptions {\n maxRetries?: number;\n baseDelayMs?: number;\n}\n\n/**\n * Run `fn` up to `maxRetries + 1` times with exponential backoff\n * (`base * 2^attempt`). Returns the first successful value, or rethrows\n * the last error.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n if (attempt < maxRetries) {\n const delay = baseDelayMs * Math.pow(2, attempt);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(String(lastError));\n}\n","/**\n * AlfeSync uploader — push changed files to S3.\n *\n * Per file:\n * 1. Ask the agent API for a presigned PUT URL\n * 2. PUT bytes directly to S3 (raw fetch — S3 isn't an Alfe API)\n * 3. Tell the agent API the upload completed (`syncConfirmUpload`)\n * 4. Update the local manifest\n *\n * Each step retries with exponential backoff via `withRetry`.\n */\n\nimport { readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nimport { computeFileHash, updateManifestEntries } from \"./manifest.js\";\nimport type { ManifestEntry } from \"./manifest.js\";\nimport { withRetry } from \"./retry.js\";\n\nconst log = createLogger(\"SyncUploader\");\n\nexport interface UploadResult {\n path: string;\n success: boolean;\n hash?: string;\n size?: number;\n error?: string;\n}\n\n/**\n * Prefixes stored as GLACIER_IR on the sync bucket. Shared with the recovery\n * classifier in sync-engine.ts, which must never cut conflict sidecars for\n * archive objects (their presigned GETs fail until restored, so hashing/\n * preserving them is wasted work on a pull that can't succeed anyway).\n */\nexport const ARCHIVE_PREFIXES = [\"sessions/archive/\", \"context/archive/\"] as const;\n\nfunction getStorageClass(relativePath: string): \"STANDARD\" | \"GLACIER_IR\" {\n return ARCHIVE_PREFIXES.some((p) => relativePath.startsWith(p))\n ? \"GLACIER_IR\"\n : \"STANDARD\";\n}\n\nfunction getContentType(relativePath: string): string {\n if (relativePath.endsWith(\".json\")) return \"application/json\";\n if (relativePath.endsWith(\".md\")) return \"text/markdown\";\n if (relativePath.endsWith(\".txt\")) return \"text/plain\";\n if (relativePath.endsWith(\".gz\")) return \"application/gzip\";\n if (relativePath.endsWith(\".yaml\") || relativePath.endsWith(\".yml\")) return \"text/yaml\";\n return \"application/octet-stream\";\n}\n\nasync function uploadOne(\n workspacePath: string,\n relativePath: string,\n client: AgentApiClient,\n): Promise<UploadResult & { entry?: ManifestEntry }> {\n const absolutePath = join(workspacePath, relativePath);\n try {\n return await withRetry(async () => {\n const [hash, fileStat] = await Promise.all([\n computeFileHash(absolutePath),\n stat(absolutePath),\n ]);\n const size = fileStat.size;\n const storageClass = getStorageClass(relativePath);\n const contentType = getContentType(relativePath);\n\n // 1. Presigned PUT URL\n const presigned = await client.syncPresign({\n files: [{ path: relativePath, operation: \"put\", contentType }],\n });\n const url = presigned.urls[0]?.url;\n if (!url) throw new Error(\"No presigned URL returned\");\n\n // 2. Upload bytes directly to S3 (not an Alfe API)\n const fileContent = await readFile(absolutePath);\n const putResponse = await fetch(url, {\n method: \"PUT\",\n headers: { \"Content-Type\": contentType },\n body: fileContent,\n });\n if (!putResponse.ok) {\n throw new Error(\n `S3 PUT failed (${String(putResponse.status)}): ${await putResponse.text()}`,\n );\n }\n\n // 3. Confirm upload via agent API\n await client.syncConfirmUpload({\n filePath: relativePath,\n hash,\n size,\n storageClass,\n });\n\n // 4. Manifest write happens in uploadFiles, once per batch —\n // concurrent per-file read-modify-writes of manifest.json silently\n // drop entries (same fix as downloadFiles).\n const entry: ManifestEntry = {\n hash,\n size,\n lastSynced: new Date().toISOString(),\n storageClass,\n };\n return { path: relativePath, success: true, hash, size, entry };\n });\n } catch (err) {\n return {\n path: relativePath,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Upload many files, batched by `concurrency`.\n */\nexport async function uploadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: AgentApiClient,\n options: { concurrency?: number; quiet?: boolean } = {},\n): Promise<UploadResult[]> {\n const { concurrency = 5, quiet = false } = options;\n const results: UploadResult[] = [];\n\n for (let i = 0; i < relativePaths.length; i += concurrency) {\n const batch = relativePaths.slice(i, i + concurrency);\n const batchResults = await Promise.all(\n batch.map((path) => uploadOne(workspacePath, path, client)),\n );\n const entries: Record<string, ManifestEntry> = {};\n for (const result of batchResults) {\n if (result.entry !== undefined) entries[result.path] = result.entry;\n results.push({\n path: result.path,\n success: result.success,\n hash: result.hash,\n size: result.size,\n error: result.error,\n });\n if (!quiet) {\n if (result.success) {\n log.info(`Uploaded ${result.path} (${formatBytes(result.size ?? 0)})`);\n } else {\n log.error(`Failed to upload ${result.path}: ${result.error ?? \"Unknown error\"}`);\n }\n }\n }\n if (Object.keys(entries).length > 0) {\n await updateManifestEntries(workspacePath, entries);\n }\n }\n\n return results;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${String(bytes)} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n","/**\n * AlfeSync downloader — pull files from S3 to disk.\n *\n * Per file:\n * 1. Ask the agent API for a presigned GET URL\n * 2. GET bytes directly from S3 (raw fetch — S3 isn't an Alfe API)\n * 3. Write to disk\n * 4. Update the local manifest\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nimport { updateManifestEntries } from \"./manifest.js\";\nimport type { ManifestEntry, RemoteManifest } from \"./manifest.js\";\nimport { withRetry } from \"./retry.js\";\n\nconst log = createLogger(\"SyncDownloader\");\n\nexport interface DownloadResult {\n path: string;\n success: boolean;\n size?: number;\n error?: string;\n}\n\nasync function downloadOne(\n workspacePath: string,\n relativePath: string,\n client: AgentApiClient,\n remoteEntry?: RemoteManifest[\"files\"][string],\n): Promise<DownloadResult & { entry?: ManifestEntry }> {\n try {\n return await withRetry(async () => {\n const presigned = await client.syncPresign({\n files: [{ path: relativePath, operation: \"get\" }],\n });\n const url = presigned.urls[0]?.url;\n if (!url) throw new Error(\"No presigned URL returned\");\n\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(\n `S3 GET failed (${String(response.status)}): ${await response.text()}`,\n );\n }\n const buffer = Buffer.from(await response.arrayBuffer());\n\n const absolutePath = join(workspacePath, relativePath);\n await mkdir(dirname(absolutePath), { recursive: true });\n await writeFile(absolutePath, buffer);\n\n const entry: ManifestEntry = {\n hash: remoteEntry?.hash ?? \"\",\n size: buffer.length,\n lastSynced: new Date().toISOString(),\n storageClass:\n (remoteEntry?.storageClass ?? \"STANDARD\") as \"STANDARD\" | \"GLACIER_IR\",\n };\n // Manifest write happens in downloadFiles, once per batch — concurrent\n // per-file read-modify-writes of manifest.json silently drop entries.\n return { path: relativePath, success: true, size: buffer.length, entry };\n });\n } catch (err) {\n return {\n path: relativePath,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\nexport async function downloadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: AgentApiClient,\n remoteManifest?: RemoteManifest,\n options: { concurrency?: number; quiet?: boolean } = {},\n): Promise<DownloadResult[]> {\n const { concurrency = 5, quiet = false } = options;\n const results: DownloadResult[] = [];\n\n for (let i = 0; i < relativePaths.length; i += concurrency) {\n const batch = relativePaths.slice(i, i + concurrency);\n const batchResults = await Promise.all(\n batch.map((path) =>\n downloadOne(workspacePath, path, client, remoteManifest?.files[path]),\n ),\n );\n const entries: Record<string, ManifestEntry> = {};\n for (const result of batchResults) {\n if (result.entry !== undefined) entries[result.path] = result.entry;\n results.push({\n path: result.path,\n success: result.success,\n size: result.size,\n error: result.error,\n });\n if (!quiet) {\n if (result.success) {\n log.info(`Downloaded ${result.path} (${formatBytes(result.size ?? 0)})`);\n } else {\n log.error(`Failed to download ${result.path}: ${result.error ?? \"Unknown error\"}`);\n }\n }\n }\n if (Object.keys(entries).length > 0) {\n await updateManifestEntries(workspacePath, entries);\n }\n }\n\n return results;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${String(bytes)} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n","/**\n * AlfeSync engine — orchestrates push, pull, and full sync.\n *\n * - push(paths[]): upload changed files to S3\n * - pull(): download files newer on remote\n * - fullSync(): bidirectional sync with conflict detection\n *\n * Conflict resolution: when a remote file is newer than the local manifest\n * entry AND the local file has also changed, the local copy is renamed to\n * `<name>.conflict-<timestamp>.<ext>` and the remote version wins.\n */\n\nimport { existsSync } from \"node:fs\";\nimport {\n appendFile,\n copyFile,\n mkdir,\n readdir,\n readFile,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { join, dirname, basename, extname } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient, SyncManifest } from \"@alfe.ai/agent-api-client\";\n\nimport {\n readManifest,\n writeManifest,\n computeFileHash,\n diffManifests,\n removeManifestEntry,\n updateManifestEntries,\n} from \"./manifest.js\";\nimport type { LocalManifest, ManifestEntry, RemoteManifest } from \"./manifest.js\";\nimport { loadIgnorePatterns, filterIgnored, shouldIgnore, shouldIgnoreDir } from \"./ignore.js\";\nimport { uploadFiles, ARCHIVE_PREFIXES } from \"./uploader.js\";\nimport { downloadFiles } from \"./downloader.js\";\n\nconst log = createLogger(\"SyncEngine\");\n\nexport interface SyncResult {\n pushed: number;\n pulled: number;\n conflicts: number;\n errors: number;\n}\n\n/**\n * Above this size a diverged local file is NOT copied to a `.conflict-*`\n * sidecar during recovery (the cloud copy still replaces it). Sidecars\n * double disk usage per file; a multi-GB session archive could fill the VM.\n * The un-preserved overwrite is never silent — it is logged and listed in\n * the RECOVERY report.\n */\nconst MAX_PRESERVE_BYTES = 100 * 1024 * 1024;\n\n/** Marker that keeps the heartbeat recovery nudge idempotent across runs. */\nconst RECOVERY_NUDGE_MARKER = \"<!-- alfe-sync:recovery-nudge -->\";\n\n/**\n * Recovery artifacts: `.conflict-<ts>` sidecars and `RECOVERY-<ts>.md`\n * reports. Anchored to the timestamp shape so legitimate user files like\n * `my.conflict-notes.txt` or `RECOVERY-plan.md` are never misclassified.\n *\n * They seed UP to the cloud so a preserved local version survives even if\n * the VM dies before the agent merges it (retrievable via the dashboard\n * file manager), but they are never AUTO-restored back down and never\n * preserved again — restoring would resurrect copies the agent already\n * cleaned up, and re-preserving would cascade `.conflict`-of-`.conflict`\n * files. Cloud copies are reclaimed when the agent's local cleanup\n * propagates through the watcher; artifacts orphaned by a rebuild before\n * cleanup linger until then (accepted: small, user-visible, deletable).\n * The watcher's delete brake also exempts them so the agent's post-merge\n * cleanup of many sidecars can't trip it.\n *\n * Deliberately NOT in the ignore set: ignored paths don't seed up and would\n * be eligible for pruneIgnored deletion.\n */\nexport function isRecoveryArtifact(relativePath: string): boolean {\n const name = basename(relativePath);\n return (\n /\\.conflict-\\d{4}-\\d{2}-\\d{2}T/.test(name) ||\n /^RECOVERY-\\d{4}-\\d{2}-\\d{2}T.*\\.md$/.test(name)\n );\n}\n\nexport interface SyncEngineOptions {\n workspacePath: string;\n client: AgentApiClient;\n /**\n * Active agent runtime — selects the per-runtime default ignore set used by\n * every scan (push / fullSync / firstRunReconcile). Default: \"openclaw\".\n */\n runtime?: string;\n /**\n * Recovery sidecar size cap override — see MAX_PRESERVE_BYTES. Exists so\n * tests can exercise the over-cap path without multi-hundred-MB fixtures.\n */\n maxPreserveBytes?: number;\n}\n\n/**\n * Construct a sync engine bound to a workspace + agent API client.\n *\n * The client is constructed once in plugin.ts (or the CLI) and passed in,\n * so credentials never leak into multiple places.\n */\nexport function createSyncEngine({\n workspacePath,\n client,\n runtime = \"openclaw\",\n maxPreserveBytes = MAX_PRESERVE_BYTES,\n}: SyncEngineOptions) {\n return {\n workspacePath,\n client,\n runtime,\n\n /**\n * Upload changed files. Pass `paths` for an explicit list, or omit\n * to scan the workspace for anything that drifted from the manifest.\n */\n async push(\n paths?: string[],\n options: { quiet?: boolean; filter?: string } = {},\n ): Promise<SyncResult> {\n const { quiet = false, filter } = options;\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n\n let filesToPush: string[] =\n paths && paths.length > 0\n ? filterIgnored(paths, ignorePatterns)\n : await detectLocalChanges(workspacePath, ignorePatterns);\n\n if (filter) {\n filesToPush = filesToPush.filter((p) => p.startsWith(filter));\n }\n\n if (filesToPush.length === 0) {\n if (!quiet) log.info(\"Nothing to push\");\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n\n if (!quiet) log.info(`Pushing ${String(filesToPush.length)} file(s)`);\n\n const results = await uploadFiles(workspacePath, filesToPush, client, { quiet });\n const pushed = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Push complete: ${String(pushed)} uploaded, ${String(errors)} failed`);\n }\n\n return { pushed, pulled: 0, conflicts: 0, errors };\n },\n\n // Propagate local deletes to the cloud. Called by the watcher's onChanges\n // when chokidar reports `unlink` events (paths that no longer exist on\n // disk). Each path is removed via `client.syncDeleteFile()` then dropped\n // from the local manifest. Wrapped by the caller with a rolling-window\n // rate brake — see makeDeleteBrake() — so a buggy watcher event or a\n // mistaken `rm -rf` can't nuke the cloud copy in one go.\n async pushDeletes(\n paths: string[],\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n if (paths.length === 0) return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n if (!quiet) log.info(`Deleting ${String(paths.length)} file(s) from cloud`);\n\n let deleted = 0;\n let errors = 0;\n for (const path of paths) {\n try {\n await client.syncDeleteFile(path);\n await removeManifestEntry(workspacePath, path);\n deleted++;\n if (!quiet) log.info(`Deleted from cloud: ${path}`);\n } catch (err) {\n errors++;\n if (!quiet) log.error({ err }, `Failed to delete ${path} from cloud`);\n }\n }\n\n if (!quiet) {\n log.info(`Delete complete: ${String(deleted)} deleted, ${String(errors)} failed`);\n }\n // Reuse `pushed` to count successful deletes — same shape, distinct meaning.\n return { pushed: deleted, pulled: 0, conflicts: 0, errors };\n },\n\n /** Download files newer on remote. */\n async pull(options: { quiet?: boolean } = {}): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.syncGetManifest(),\n ]);\n\n const diff = diffManifests(localManifest, remoteManifest);\n if (diff.toPull.length === 0) {\n if (!quiet) log.info(\"Nothing to pull\");\n return { pushed: 0, pulled: 0, conflicts: diff.conflicts.length, errors: 0 };\n }\n\n if (!quiet) log.info(`Pulling ${String(diff.toPull.length)} file(s)`);\n\n const results = await downloadFiles(\n workspacePath,\n [...diff.toPull],\n client,\n remoteManifest,\n { quiet },\n );\n const pulled = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);\n }\n\n return { pushed: 0, pulled, conflicts: diff.conflicts.length, errors };\n },\n\n /**\n * Bidirectional sync. True conflicts (changed on both sides) are saved\n * locally as `.conflict-<ts>` files and the remote version wins.\n */\n async fullSync(options: { quiet?: boolean } = {}): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.syncGetManifest(),\n ]);\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);\n\n const diff = diffManifests(localManifest, remoteManifest);\n const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));\n const remoteOnlyChanges = diff.conflicts.filter((p) => !localChanges.includes(p));\n\n let conflictCount = 0;\n const conflictTimestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n for (const conflictPath of trueConflicts) {\n const outcome = await preserveConflictCopy(\n workspacePath,\n conflictPath,\n conflictTimestamp,\n );\n if (outcome.status === \"vanished\") continue;\n if (outcome.status !== \"too-large\" && !quiet) {\n log.warn(`Conflict: ${conflictPath} — saved as ${outcome.conflictPath}`);\n }\n conflictCount++;\n }\n\n const filesToPush = filterIgnored(\n [...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))],\n ignorePatterns,\n );\n const filesToPull = [\n ...diff.toPull,\n ...remoteOnlyChanges,\n ...trueConflicts,\n ];\n\n const pushResult = { pushed: 0, errors: 0 };\n const pullResult = { pulled: 0, errors: 0 };\n\n if (filesToPush.length > 0) {\n if (!quiet) log.info(`Pushing ${String(filesToPush.length)} file(s)`);\n const results = await uploadFiles(\n workspacePath,\n [...new Set(filesToPush)],\n client,\n { quiet },\n );\n pushResult.pushed = results.filter((r) => r.success).length;\n pushResult.errors = results.filter((r) => !r.success).length;\n }\n\n if (filesToPull.length > 0) {\n if (!quiet) log.info(`Pulling ${String(filesToPull.length)} file(s)`);\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\n );\n pullResult.pulled = results.filter((r) => r.success).length;\n pullResult.errors = results.filter((r) => !r.success).length;\n }\n\n const totalErrors = pushResult.errors + pullResult.errors;\n if (!quiet) {\n log.info(\n `Sync complete: ${String(pushResult.pushed)} pushed, ${String(pullResult.pulled)} pulled, ${String(conflictCount)} conflicts, ${String(totalErrors)} errors`,\n );\n }\n return {\n pushed: pushResult.pushed,\n pulled: pullResult.pulled,\n conflicts: conflictCount,\n errors: totalErrors,\n };\n },\n\n /**\n * First-run reconcile for realtime activation.\n *\n * Distinguishes a genuinely NEW agent (empty cloud state → push-only\n * seed, today's behaviour) from a REBUILD / reconnect (cloud already\n * holds this agent's last-synced state → restore cloud → local, cloud\n * wins, THEN seed only the files that are new locally).\n *\n * Why not `push(undefined)`? On a from-scratch VM rebuild the local\n * manifest is gone and `alfe setup` has just re-seeded the persona\n * templates (SOUL.md, IDENTITY.md, …) onto a fresh disk. With no\n * manifest, `detectLocalChanges()` classifies every seeded template as\n * \"new\", so a blind push uploads those fresh seeds OVER the agent's\n * edited cloud copy — the agent's edits are lost on every rebuild. This\n * is the persona-durability gap this method closes.\n *\n * Why not `fullSync()`? With an empty local manifest it double-counts\n * each on-disk seed as BOTH a push candidate (`detectLocalChanges`) and\n * a pull candidate (remote-only in `diffManifests`, since the local\n * manifest has no entry for it). fullSync pushes before it pulls, so it\n * would still clobber the cloud edit before restoring it locally.\n *\n * Conflict direction: the CLOUD copy is authoritative for anything it\n * holds, but diverged UNSYNCED local content is never silently\n * destroyed — it is preserved next to the restored file as a\n * `.conflict-<ts>` sidecar and indexed in a `RECOVERY-<ts>.md` report\n * the agent is nudged (via HEARTBEAT.md, openclaw only) to reconcile.\n * Known noise: on a fresh-disk rebuild the setup-seeded persona\n * templates diverge from the edited cloud copies, so each gets a small\n * sidecar of the pristine template — bounded, listed in the report, and\n * cleaned up by the agent. The plugin can't tell a seed from an edit\n * without the template registry, and guessing (e.g. mtime windows)\n * would reintroduce exactly the silent-loss hole this closes.\n *\n * Per-path classification (see classifyRestorePaths): files whose local\n * manifest entry matches the cloud hash but whose disk content moved on\n * (`localAhead` — edited while the plugin was down) are PUSHED, not\n * restored; files deleted locally while the plugin was down\n * (`offlineDelete`) are not resurrected; files already matching the\n * cloud are healed into the manifest without a download.\n */\n async firstRunReconcile(\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const remoteManifest = await client.syncGetManifest();\n const remotePaths = Object.keys(remoteManifest.files);\n\n // Fresh agent: nothing in the cloud → seed the workspace up (today's\n // push-only behaviour). There is no prior state to lose, so pushing\n // the setup-seeded templates is correct. Stamp the owning agentId so\n // the provenance guard works from day one — without this, a machine\n // re-adopted by a different agent BEFORE its first rebuild would\n // present an unstamped (grandfathered/trusted) manifest and the guard\n // would never fire.\n if (remotePaths.length === 0) {\n if (!quiet) {\n log.info(\"First-run sync: no cloud state — seeding workspace to cloud\");\n }\n const seedResult = await this.push(undefined, options);\n await updateManifestEntries(workspacePath, {}, {\n agentId: remoteManifest.agentId,\n });\n return seedResult;\n }\n\n // Rebuild / reconnect: the cloud is the source of truth for anything\n // it holds. Restore it over the local state, preserving any diverged\n // unsynced local content as conflict sidecars first.\n if (!quiet) {\n log.info(\n `First-run sync: ${String(remotePaths.length)} file(s) in cloud — restoring cloud state before seeding`,\n );\n }\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n\n // Ignore-filter the restore list so a pre-fix workspace's stale\n // ignored objects (e.g. `**/Cache/**`) that an older buggy ignore\n // engine pushed into the cloud aren't dragged back onto a fresh disk.\n // Recovery artifacts are also excluded — see isRecoveryArtifact.\n const restorePaths = filterIgnored(remotePaths, ignorePatterns).filter(\n (p) => !isRecoveryArtifact(p),\n );\n\n const localManifest = await readManifest(workspacePath);\n // Machine re-adopted by a DIFFERENT agent without clearing\n // `~/.alfe/sync/`? Then the manifest describes the previous agent's\n // sync state: classify as if it were lost (diverged files are\n // preserved; nothing is pushed or skipped on its say-so) and reset it\n // so downloads don't merge fresh entries into stale ones.\n const manifestTrusted =\n localManifest.agentId === undefined ||\n localManifest.agentId === remoteManifest.agentId;\n if (!manifestTrusted) {\n log.warn(\n `Local sync manifest belongs to agent ${localManifest.agentId ?? \"?\"} but cloud state is agent ${remoteManifest.agentId} — treating the manifest as lost`,\n );\n await writeManifest(workspacePath, {\n files: {},\n agentId: remoteManifest.agentId,\n });\n }\n\n const classes = await classifyRestorePaths(\n workspacePath,\n restorePaths,\n remoteManifest.files,\n localManifest,\n manifestTrusted,\n );\n\n // Not a data-displacement event (nothing is overwritten), so per-path\n // detail respects quiet; a one-line summary is always kept.\n for (const p of classes.offlineDeletes) {\n if (!quiet) {\n log.info(`Recovery: ${p} was deleted locally while sync was down — not restoring it`);\n }\n }\n if (classes.offlineDeletes.length > 0) {\n log.info(\n `Recovery: ${String(classes.offlineDeletes.length)} locally-deleted file(s) not restored from cloud`,\n );\n }\n\n // Preserve diverged local content BEFORE the cloud copy overwrites it.\n // These warns are deliberately unconditional (not gated on `quiet`) —\n // they record the only moments local data is displaced.\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const preserved: PreservedRecord[] = [];\n let newlyPreserved = 0;\n for (const { path, diskHash, size } of classes.preserve) {\n const outcome = await preserveConflictCopy(workspacePath, path, timestamp, {\n maxBytes: maxPreserveBytes,\n sourceHash: diskHash,\n });\n if (outcome.status === \"vanished\") continue;\n const record: PreservedRecord = {\n path,\n localHash: diskHash,\n remoteHash: remoteManifest.files[path].hash,\n size,\n };\n if (outcome.status === \"too-large\") {\n record.tooLarge = true;\n newlyPreserved++;\n log.warn(\n `Recovery: ${path} diverged locally but exceeds the sidecar size cap — cloud version will replace it WITHOUT a preserved copy`,\n );\n } else {\n record.conflictPath = outcome.conflictPath;\n if (outcome.status === \"preserved\") newlyPreserved++;\n log.warn(\n `Recovery: preserved local ${path} as ${outcome.conflictPath} — cloud version restored in place`,\n );\n }\n preserved.push(record);\n }\n\n // Recovery report — written under the agent's working dir so the agent\n // actually encounters it, and seeded to cloud (net-new path) for\n // durability across another rebuild. A repeat recovery whose sidecars\n // were all already preserved (crash-loop retry, unresolved prior\n // recovery) doesn't stack another report — unless no report exists at\n // all (crashed before writing it last time).\n let recoveryReportPath: string | null = null;\n const reportNeeded =\n preserved.length > 0 &&\n (newlyPreserved > 0 || !(await recoveryReportExists(workspacePath)));\n if (reportNeeded) {\n recoveryReportPath = `${agentDirPrefix(workspacePath)}RECOVERY-${timestamp}.md`;\n const reportAbsolute = join(workspacePath, recoveryReportPath);\n await mkdir(dirname(reportAbsolute), { recursive: true });\n await writeFile(\n reportAbsolute,\n buildRecoveryReport(preserved, workspacePath),\n \"utf-8\",\n );\n log.warn(\n `Recovery: ${String(preserved.length)} diverged local file(s) — see ${recoveryReportPath}`,\n );\n }\n\n // Restore cloud → local. downloadFiles is used directly (not\n // pullFiles): pullFiles' manifest-hash filter would skip the preserve\n // paths whose manifest entry happens to match the cloud.\n const pullList = [\n ...classes.missing,\n ...classes.pullClean,\n ...classes.preserve.map((p) => p.path),\n ];\n const restore = { pulled: 0, errors: 0 };\n if (pullList.length > 0) {\n if (!quiet) log.info(`Pulling ${String(pullList.length)} file(s)`);\n const results = await downloadFiles(\n workspacePath,\n pullList,\n client,\n remoteManifest,\n options,\n );\n restore.pulled = results.filter((r) => r.success).length;\n restore.errors = results.filter((r) => !r.success).length;\n }\n\n // Heal the manifest for files already matching the cloud (no download\n // needed) and stamp the owning agentId. Single bulk write — the\n // per-entry helper would rewrite the JSON once per file.\n if (\n Object.keys(classes.heal).length > 0 ||\n localManifest.agentId !== remoteManifest.agentId\n ) {\n await updateManifestEntries(workspacePath, classes.heal, {\n agentId: remoteManifest.agentId,\n });\n }\n\n // Seed up files that are genuinely new locally (sidecars and the\n // recovery report are on disk by now, so they ride this push), plus\n // localAhead files — the cloud holds an older copy, so they're not\n // net-new, but local is the only side that advanced since last sync.\n const remoteSet = new Set(remotePaths);\n const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);\n const toSeed = localChanges.filter((p) => !remoteSet.has(p));\n const pushList = [...toSeed, ...classes.localAhead];\n const seed =\n pushList.length > 0\n ? await this.push(pushList, options)\n : { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n // Active self-resolution: nudge the agent to merge the preserved\n // copies. OpenClaw reads HEARTBEAT.md from its agent workspace dir on\n // the periodic heartbeat; hermes has no such convention. The nudge\n // file may already exist in the cloud, so it can't ride the net-new\n // seed — push it explicitly.\n let nudge = { pushed: 0, errors: 0 };\n if (preserved.length > 0 && recoveryReportPath !== null && runtime === \"openclaw\") {\n try {\n const nudgePath = await appendRecoveryNudge(\n workspacePath,\n recoveryReportPath,\n preserved.length,\n );\n if (nudgePath !== null) {\n const pushResult = await this.push([nudgePath], options);\n nudge = { pushed: pushResult.pushed, errors: pushResult.errors };\n }\n } catch (err) {\n log.warn(\n `Recovery: failed to write heartbeat nudge: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n return {\n pushed: seed.pushed + nudge.pushed,\n pulled: restore.pulled,\n conflicts: preserved.length + seed.conflicts,\n errors: restore.errors + seed.errors + nudge.errors,\n };\n },\n\n /**\n * Pull a known list of files (used for relay-driven notifications,\n * where we already know which paths changed remotely).\n */\n async pullFiles(\n paths: string[],\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n if (paths.length === 0) return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n const remoteManifest: SyncManifest = await client.syncGetManifest();\n const localManifest = await readManifest(workspacePath);\n\n const filesToPull = paths.filter((p) => {\n if (!(p in remoteManifest.files)) return false;\n if (!(p in localManifest.files)) return true;\n return localManifest.files[p].hash !== remoteManifest.files[p].hash;\n });\n\n if (filesToPull.length === 0) {\n if (!quiet) log.info(\"All notified files already in sync\");\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n\n if (!quiet) log.info(`Pulling ${String(filesToPull.length)} changed file(s)`);\n\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\n );\n const pulled = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);\n }\n return { pushed: 0, pulled, conflicts: 0, errors };\n },\n\n /**\n * Purge cloud files that match the current (per-runtime) ignore set.\n *\n * Self-heals workspaces whose cloud copy still holds runtime-internal junk\n * (node_modules, the openclaw npm cache, …) that a pre-fix ignore engine\n * uploaded before it was correctly ignored. Bounded to ignored paths only,\n * so it can never delete a legitimately-synced file, and it deletes the\n * cloud copy directly (not via the watcher) so the rolling-window delete\n * brake — which guards against a buggy `rm -rf` — does not apply.\n */\n async pruneIgnored(\n options: { quiet?: boolean; limit?: number } = {},\n ): Promise<SyncResult> {\n // Cap deletes per pass. pushDeletes is serial (one HTTP call per path), so\n // an agent with thousands of pre-fix leaked node_modules objects would\n // otherwise hammer the sync service in one burst. Draining over several\n // activations is fine — the watcher already ignores these paths, so they\n // never come back and the remainder is cleared on the next start.\n const { quiet = false, limit = 1000 } = options;\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n const remoteManifest = await client.syncGetManifest();\n const ignoredPaths = Object.keys(remoteManifest.files).filter((p) =>\n shouldIgnore(p, ignorePatterns),\n );\n if (ignoredPaths.length === 0) {\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n const batch = ignoredPaths.slice(0, limit);\n if (!quiet) {\n const capped = ignoredPaths.length > batch.length\n ? ` (capped at ${String(limit)}; ${String(ignoredPaths.length - batch.length)} remain for next pass)`\n : \"\";\n log.info(`Pruning ${String(batch.length)} ignored file(s) from cloud${capped}`);\n }\n return this.pushDeletes(batch, { quiet });\n },\n\n /**\n * Delete a file locally and from the manifest. Used when the sync relay\n * tells us a file was removed remotely.\n */\n async removeLocalFile(\n filePath: string,\n options: { quiet?: boolean } = {},\n ): Promise<void> {\n const { quiet = false } = options;\n const absolutePath = join(workspacePath, filePath);\n\n try {\n const { unlink } = await import(\"node:fs/promises\");\n if (existsSync(absolutePath)) {\n await unlink(absolutePath);\n if (!quiet) log.info(`Deleted: ${filePath}`);\n }\n } catch (err) {\n if (!quiet) log.error({ err }, `Failed to delete ${filePath}`);\n }\n\n await removeManifestEntry(workspacePath, filePath);\n },\n };\n}\n\nexport type SyncEngine = ReturnType<typeof createSyncEngine>;\n\n/**\n * Walk the workspace and return paths whose hash differs from the manifest\n * (or that are missing from it entirely).\n */\nasync function detectLocalChanges(\n workspacePath: string,\n ignorePatterns: string[],\n): Promise<string[]> {\n const manifest = await readManifest(workspacePath);\n const changed: string[] = [];\n\n const { readdir } = await import(\"node:fs/promises\");\n\n async function walk(dir: string): Promise<void> {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n const relativePath = fullPath\n .slice(workspacePath.length + 1)\n .replace(/\\\\/g, \"/\");\n\n if (entry.isDirectory()) {\n // Prune ignored directories (incl. node_modules and the runtime's\n // internal dirs) via the same ignore engine the watcher uses — one\n // source of truth, no hardcoded name list to drift.\n if (!shouldIgnoreDir(relativePath, ignorePatterns)) await walk(fullPath);\n } else if (entry.isFile()) {\n if (shouldIgnore(relativePath, ignorePatterns)) continue;\n if (!(relativePath in manifest.files)) {\n changed.push(relativePath);\n continue;\n }\n try {\n const currentHash = await computeFileHash(fullPath);\n if (currentHash !== manifest.files[relativePath].hash) {\n changed.push(relativePath);\n }\n } catch {\n // file may have been deleted between listing and hashing\n }\n }\n }\n }\n\n await walk(workspacePath);\n return changed;\n}\n\n/**\n * Relative prefix of the agent's actual working directory. The sync root is\n * the runtime HOME (`~/.openclaw`), but the agent lives and works in\n * `~/.openclaw/workspace/` — recovery artifacts meant for the agent's eyes\n * (RECOVERY report, heartbeat nudge) must land there or it never sees them.\n * Falls back to the sync root when no `workspace/` dir exists (tests, CLI\n * usage against a bare directory).\n */\nfunction agentDirPrefix(workspacePath: string): string {\n return existsSync(join(workspacePath, \"workspace\")) ? \"workspace/\" : \"\";\n}\n\n/** Whether an unresolved RECOVERY-*.md report already sits in the agent dir. */\nasync function recoveryReportExists(workspacePath: string): Promise<boolean> {\n const dir = join(workspacePath, agentDirPrefix(workspacePath));\n const entries = await readdir(dir).catch(() => [] as string[]);\n return entries.some((name) => name.startsWith(\"RECOVERY-\") && name.endsWith(\".md\"));\n}\n\ntype PreserveOutcome =\n | { status: \"preserved\"; conflictPath: string }\n | { status: \"already-preserved\"; conflictPath: string }\n | { status: \"too-large\"; size: number }\n | { status: \"vanished\" };\n\n/**\n * Copy a file aside as `<base>.conflict-<ts><ext>` in the same directory\n * before a remote copy overwrites it. Uses `copyFile` (no buffering — safe\n * for large/binary files).\n *\n * Idempotent: if a sibling sidecar with identical content already exists\n * (crash-loop retry, repeated recovery), no new copy is stacked. Pass\n * `sourceHash` when the caller already hashed the file.\n */\nasync function preserveConflictCopy(\n workspacePath: string,\n relativePath: string,\n timestamp: string,\n options: { maxBytes?: number; sourceHash?: string } = {},\n): Promise<PreserveOutcome> {\n const absolutePath = join(workspacePath, relativePath);\n const ext = extname(relativePath);\n const base = basename(relativePath, ext);\n const dir = dirname(relativePath);\n\n try {\n const fileStat = await stat(absolutePath);\n if (options.maxBytes !== undefined && fileStat.size > options.maxBytes) {\n return { status: \"too-large\", size: fileStat.size };\n }\n\n const sourceHash = options.sourceHash ?? (await computeFileHash(absolutePath));\n const siblings = await readdir(join(workspacePath, dir)).catch(() => [] as string[]);\n for (const name of siblings) {\n if (!name.startsWith(`${base}.conflict-`)) continue;\n if (ext !== \"\" && !name.endsWith(ext)) continue;\n const existingHash = await computeFileHash(join(workspacePath, dir, name)).catch(\n () => null,\n );\n if (existingHash === sourceHash) {\n return {\n status: \"already-preserved\",\n conflictPath: join(dir, name).replace(/\\\\/g, \"/\"),\n };\n }\n }\n\n const conflictName = `${base}.conflict-${timestamp}${ext}`;\n await copyFile(absolutePath, join(workspacePath, dir, conflictName));\n return {\n status: \"preserved\",\n conflictPath: join(dir, conflictName).replace(/\\\\/g, \"/\"),\n };\n } catch {\n // Source vanished between classification and copy — the pull will\n // restore the cloud copy; nothing local left to preserve.\n return { status: \"vanished\" };\n }\n}\n\ninterface RestoreClassification {\n /** Absent locally (or vanished mid-scan) → plain pull. */\n missing: string[];\n /** Deleted locally while the plugin was down (manifest matches cloud) → skip; don't resurrect. */\n offlineDeletes: string[];\n /** Disk already matches the cloud but the manifest doesn't know → heal entries, no download. */\n heal: Record<string, ManifestEntry>;\n /** Local unchanged since last sync, remote advanced → plain pull (not a conflict). */\n pullClean: string[];\n /** Local edited while the plugin was down, cloud unchanged since our last sync → push, never pull. */\n localAhead: string[];\n /** Unsynced local content diverges from the cloud → sidecar, then pull (cloud wins). */\n preserve: { path: string; diskHash: string; size: number }[];\n}\n\nconst CLASSIFY_CONCURRENCY = 8;\n\n/**\n * Classify every cloud-held path for recovery. The destructive decision\n * (overwrite local content) is only ever taken when the DISK hash diverges\n * from the cloud AND the local manifest can't vouch for the disk content —\n * and even then the local bytes are preserved first. The manifest and mtime\n * are used solely to avoid work and to detect local-ahead edits, never to\n * authorize an unpreserved overwrite.\n */\nasync function classifyRestorePaths(\n workspacePath: string,\n restorePaths: string[],\n remoteFiles: RemoteManifest[\"files\"],\n localManifest: LocalManifest,\n manifestTrusted: boolean,\n): Promise<RestoreClassification> {\n const out: RestoreClassification = {\n missing: [],\n offlineDeletes: [],\n heal: {},\n pullClean: [],\n localAhead: [],\n preserve: [],\n };\n\n async function classifyOne(path: string): Promise<void> {\n const remote = remoteFiles[path];\n const entry = manifestTrusted ? localManifest.files[path] : undefined;\n const absolutePath = join(workspacePath, path);\n\n let fileStat = null;\n try {\n fileStat = await stat(absolutePath);\n } catch {\n // missing on disk\n }\n if (!fileStat?.isFile()) {\n if (entry?.hash === remote.hash) out.offlineDeletes.push(path);\n else out.missing.push(path);\n return;\n }\n\n // Stat fast-path: same size and untouched since our last sync → trust\n // the manifest hash without re-reading the file, keeping warm restarts\n // cheap. mtime only ever SKIPS work here — a false \"changed\" costs one\n // hash; a false \"unchanged\" needs a backdated-mtime same-size edit.\n const unchangedSinceSync =\n fileStat.size === entry?.size &&\n fileStat.mtimeMs <= Date.parse(entry.lastSynced);\n\n // Archive objects (GLACIER_IR) are classified on manifest data only —\n // they can be huge, and their presigned GET fails until restored, so\n // hashing or sidecaring them buys nothing.\n const isArchive = ARCHIVE_PREFIXES.some((p) => path.startsWith(p));\n\n if (unchangedSinceSync || isArchive) {\n if (entry?.hash === remote.hash) return; // in sync\n out.pullClean.push(path);\n return;\n }\n\n let diskHash: string;\n try {\n diskHash = await computeFileHash(absolutePath);\n } catch {\n out.missing.push(path); // vanished between stat and hash\n return;\n }\n\n if (diskHash === remote.hash) {\n if (entry?.hash !== remote.hash) {\n out.heal[path] = {\n hash: remote.hash,\n size: fileStat.size,\n lastSynced: new Date().toISOString(),\n storageClass: (remote.storageClass ?? \"STANDARD\") as \"STANDARD\" | \"GLACIER_IR\",\n };\n }\n return;\n }\n if (entry?.hash === diskHash) {\n out.pullClean.push(path);\n return;\n }\n if (entry?.hash === remote.hash) {\n out.localAhead.push(path);\n return;\n }\n out.preserve.push({ path, diskHash, size: fileStat.size });\n }\n\n for (let i = 0; i < restorePaths.length; i += CLASSIFY_CONCURRENCY) {\n await Promise.all(\n restorePaths.slice(i, i + CLASSIFY_CONCURRENCY).map(classifyOne),\n );\n }\n return out;\n}\n\ninterface PreservedRecord {\n path: string;\n /** Absent when the file exceeded the sidecar size cap. */\n conflictPath?: string;\n localHash: string;\n remoteHash: string;\n size: number;\n tooLarge?: boolean;\n}\n\nfunction buildRecoveryReport(\n records: PreservedRecord[],\n workspacePath: string,\n): string {\n const lines: string[] = [\n `# Workspace recovery report`,\n \"\",\n `A sync recovery restored this agent's cloud-saved workspace. The files`,\n `below had local content that differed from the cloud copy. The cloud`,\n `version now lives at the original path; the pre-recovery local version`,\n `was preserved next to it as a \\`.conflict-*\\` sidecar.`,\n \"\",\n `**What to do:** open each sidecar, merge anything worth keeping into the`,\n `live file, then delete the sidecar. Delete this report when done.`,\n \"\",\n `Paths are relative to the sync root: \\`${workspacePath}\\``,\n \"\",\n ];\n const capped = records.filter((r) => r.tooLarge === true);\n const kept = records.filter((r) => r.tooLarge !== true);\n if (kept.length > 0) {\n lines.push(`| File | Preserved copy | Local hash | Cloud hash | Size (bytes) |`);\n lines.push(`| --- | --- | --- | --- | --- |`);\n for (const r of kept) {\n lines.push(\n `| \\`${r.path}\\` | \\`${r.conflictPath ?? \"\"}\\` | \\`${r.localHash}\\` | \\`${r.remoteHash}\\` | ${String(r.size)} |`,\n );\n }\n lines.push(\"\");\n }\n if (capped.length > 0) {\n lines.push(\n `The following diverged files exceeded the sidecar size cap and were`,\n `replaced by the cloud version WITHOUT a preserved copy:`,\n \"\",\n );\n for (const r of capped) {\n lines.push(`- \\`${r.path}\\` (${String(r.size)} bytes, local hash \\`${r.localHash}\\`)`);\n }\n lines.push(\"\");\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Append the recovery nudge to the agent's HEARTBEAT.md (OpenClaw reads it\n * from the agent workspace dir on its periodic heartbeat). Returns the\n * sync-relative path when a nudge was appended, or null when a prior nudge\n * is still pending (idempotency marker present).\n */\nasync function appendRecoveryNudge(\n workspacePath: string,\n recoveryReportPath: string,\n preservedCount: number,\n): Promise<string | null> {\n const prefix = agentDirPrefix(workspacePath);\n const heartbeatPath = `${prefix}HEARTBEAT.md`;\n const absolutePath = join(workspacePath, heartbeatPath);\n\n const existing = existsSync(absolutePath)\n ? await readFile(absolutePath, \"utf-8\")\n : \"\";\n if (existing.includes(RECOVERY_NUDGE_MARKER)) return null;\n\n // Report path shown relative to the agent's working dir when possible.\n const reportForAgent = recoveryReportPath.startsWith(prefix)\n ? recoveryReportPath.slice(prefix.length)\n : recoveryReportPath;\n\n // Reference the report glob, not just the latest report: a later recovery\n // can add another RECOVERY-*.md while this nudge (marker-idempotent, so\n // never re-appended) is still pending — the instruction must cover it.\n const block = [\n \"\",\n RECOVERY_NUDGE_MARKER,\n \"## Workspace recovery needs your attention\",\n \"\",\n `A sync recovery preserved ${String(preservedCount)} pre-recovery local file version(s)`,\n `as \\`.conflict-*\\` sidecars. Read every \\`RECOVERY-*.md\\` report in this directory`,\n `(latest: \\`${reportForAgent}\\`), merge anything worth keeping into the live files,`,\n `then delete the sidecars, the reports, and this section (including its marker`,\n `comment).`,\n \"\",\n ].join(\"\\n\");\n\n await mkdir(dirname(absolutePath), { recursive: true });\n await appendFile(absolutePath, block, \"utf-8\");\n return heartbeatPath;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sync-engine.js","names":["log","formatBytes","log"],"sources":["../src/manifest.ts","../src/ignore.ts","../src/retry.ts","../src/uploader.ts","../src/downloader.ts","../src/sync-engine.ts"],"sourcesContent":["/**\n * AlfeSync manifest — local file manifest at `~/.alfe/sync/manifest.json`.\n *\n * Tracks file hashes, sizes, sync timestamps, and storage classes\n * to enable efficient diff-based syncing. Lives under `~/.alfe/sync/`\n * so workspace directories stay clean.\n *\n * `workspacePath` is accepted by every function for consistency with the\n * rest of the package, but the manifest itself is workspace-independent\n * (one agent, one workspace, one manifest).\n */\n\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync, createReadStream } from \"node:fs\";\nimport { createHash } from \"node:crypto\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/**\n * Local state directory for sync — manifest, etc. Lives under `~/.alfe/sync/`\n * so it stays alongside the rest of Alfe's agent state instead of polluting\n * the workspace.\n */\nconst SYNC_STATE_DIR = join(homedir(), \".alfe\", \"sync\");\n\nexport interface ManifestEntry {\n hash: string;\n size: number;\n lastSynced: string;\n storageClass: \"STANDARD\" | \"GLACIER_IR\";\n}\n\nexport interface LocalManifest {\n files: Record<string, ManifestEntry>;\n /**\n * Owner of this manifest. The manifest lives at a machine-global path\n * (`~/.alfe/sync/manifest.json`), so if a machine is re-adopted by a\n * DIFFERENT agent without clearing `~/.alfe/sync/`, the entries describe\n * the previous agent's sync state and must not be trusted for recovery\n * classification. Absent on pre-upgrade manifests — those are trusted\n * (grandfathered: same-agent continuation is overwhelmingly the norm, and\n * distrust would sidecar-spam every diverged file after upgrade).\n */\n agentId?: string;\n}\n\nexport interface RemoteManifest {\n version: 1;\n agentId: string;\n lastSync: string;\n files: Record<\n string,\n {\n hash: string;\n size: number;\n modified: string;\n etag?: string;\n storageClass?: string;\n compressed?: boolean;\n }\n >;\n}\n\nexport interface ManifestDiff {\n /** Files that exist locally but not on remote, or have changed locally */\n toPush: string[];\n /** Files that exist on remote but not locally, or are newer on remote */\n toPull: string[];\n /** Files that exist in both and have diverged (conflict) */\n conflicts: string[];\n /** Files that exist locally but were deleted on remote */\n remoteDeleted: string[];\n}\n\nconst MANIFEST_FILE = \"manifest.json\";\n\n/**\n * Resolve the manifest file path. Lives under `~/.alfe/sync/`, independent\n * of the workspace path — one agent has one manifest.\n */\nfunction manifestPath(): string {\n return join(SYNC_STATE_DIR, MANIFEST_FILE);\n}\n\n/**\n * Read the local manifest. Returns empty manifest if not found.\n *\n * `workspacePath` is accepted for call-site symmetry with the rest of the\n * package but is not used — the manifest path is resolved from\n * `~/.alfe/sync/` regardless of which workspace the call comes from.\n */\nexport async function readManifest(\n workspacePath: string,\n): Promise<LocalManifest> {\n void workspacePath;\n const path = manifestPath();\n if (!existsSync(path)) {\n return { files: {} };\n }\n\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as LocalManifest;\n } catch {\n return { files: {} };\n }\n}\n\n/**\n * Write the local manifest. `workspacePath` is accepted for call-site\n * symmetry but unused (see `readManifest`).\n */\nexport async function writeManifest(\n workspacePath: string,\n manifest: LocalManifest,\n): Promise<void> {\n void workspacePath;\n const path = manifestPath();\n await mkdir(SYNC_STATE_DIR, { recursive: true });\n await writeFile(path, JSON.stringify(manifest, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/**\n * Update a single file entry in the local manifest.\n */\nexport async function updateManifestEntry(\n workspacePath: string,\n relativePath: string,\n entry: ManifestEntry,\n): Promise<void> {\n const manifest = await readManifest(workspacePath);\n manifest.files[relativePath] = entry;\n await writeManifest(workspacePath, manifest);\n}\n\n/**\n * Update many file entries in one read-modify-write pass.\n *\n * `updateManifestEntry` re-reads and rewrites the whole manifest JSON per\n * call — fine for single-file watcher events, quadratic for bulk heals, and\n * lossy when calls overlap. Use this for any batch (e.g. the recovery\n * \"identical → heal\" pass). Optionally stamps the owning agentId.\n */\nexport async function updateManifestEntries(\n workspacePath: string,\n entries: Record<string, ManifestEntry>,\n options: { agentId?: string } = {},\n): Promise<void> {\n const manifest = await readManifest(workspacePath);\n Object.assign(manifest.files, entries);\n if (options.agentId) manifest.agentId = options.agentId;\n await writeManifest(workspacePath, manifest);\n}\n\n/**\n * Remove a file entry from the local manifest.\n */\nexport async function removeManifestEntry(\n workspacePath: string,\n relativePath: string,\n): Promise<void> {\n const manifest = await readManifest(workspacePath);\n const remainingFiles = Object.fromEntries(\n Object.entries(manifest.files).filter(([key]) => key !== relativePath),\n );\n manifest.files = remainingFiles;\n await writeManifest(workspacePath, manifest);\n}\n\n/**\n * Compute SHA-256 hash of a file using streaming (memory-efficient).\n * Returns `sha256:<hex>` format.\n */\nexport async function computeFileHash(filePath: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const hash = createHash(\"sha256\");\n const stream = createReadStream(filePath);\n\n stream.on(\"data\", (chunk) => hash.update(chunk));\n stream.on(\"end\", () => { resolve(`sha256:${hash.digest(\"hex\")}`); });\n stream.on(\"error\", reject);\n });\n}\n\n/**\n * Diff the local manifest against the remote manifest.\n *\n * Returns lists of files to push, pull, and conflicts.\n */\nexport function diffManifests(\n local: LocalManifest,\n remote: RemoteManifest,\n): ManifestDiff {\n const toPush: string[] = [];\n const toPull: string[] = [];\n const conflicts: string[] = [];\n const remoteDeleted: string[] = [];\n\n const localPaths = new Set(Object.keys(local.files));\n const remotePaths = new Set(Object.keys(remote.files));\n\n // Files only in local → push\n for (const path of localPaths) {\n if (!remotePaths.has(path)) {\n toPush.push(path);\n }\n }\n\n // Files only in remote → pull\n for (const path of remotePaths) {\n if (!localPaths.has(path)) {\n toPull.push(path);\n }\n }\n\n // Files in both → compare hashes\n for (const path of localPaths) {\n if (!remotePaths.has(path)) continue;\n\n const localEntry = local.files[path];\n const remoteEntry = remote.files[path];\n\n if (localEntry.hash === remoteEntry.hash) {\n // In sync — nothing to do\n continue;\n }\n\n // Hashes differ — determine direction\n const localSyncTime = new Date(localEntry.lastSynced).getTime();\n const remoteModTime = new Date(remoteEntry.modified).getTime();\n\n if (remoteModTime > localSyncTime) {\n // Remote is newer than our last sync — could be a conflict\n // If local file was also modified (hash differs from what we synced),\n // it's a conflict\n conflicts.push(path);\n } else {\n // Local is newer — push\n toPush.push(path);\n }\n }\n\n // Files that were in our manifest but deleted from remote\n for (const path of localPaths) {\n if (\n local.files[path].lastSynced !== \"\" &&\n !remotePaths.has(path)\n ) {\n remoteDeleted.push(path);\n }\n }\n\n return { toPush, toPull, conflicts, remoteDeleted };\n}\n","// AlfeSync ignore — parse .alfesyncignore (gitignore-style glob matching).\n// Uses micromatch for pattern matching, compatible with .gitignore syntax.\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport micromatch from \"micromatch\";\n\n// Committed default ignore files live next to this module (copied to\n// dist/defaults at build). `common` applies to every runtime; each runtime\n// (openclaw, hermes) adds its own runtime-internal dirs. These are the source\n// of truth for the baseline set — a hardcoded array used to live here.\n//\n// tsdown bundles this module into three entries at different depths\n// (dist/index.js, dist/plugin.js, dist/cli/index.js), so `./defaults` resolves\n// to dist/defaults for the top-level entries but dist/cli/defaults for the CLI.\n// The copy step only writes dist/defaults, so probe both `./defaults` and\n// `../defaults` and use whichever exists. In src (vitest) `./defaults` wins.\nconst MODULE_DIR = fileURLToPath(new URL(\".\", import.meta.url));\nconst DEFAULTS_DIR =\n [join(MODULE_DIR, \"defaults\"), join(MODULE_DIR, \"..\", \"defaults\")].find(existsSync) ??\n join(MODULE_DIR, \"defaults\");\n\n// Parse a committed default ignore file: lines are FULL micromatch globs,\n// used verbatim (no gitignore normalisation). Blank lines and `#` comments\n// are skipped.\nfunction parseDefaultGlobs(text: string): string[] {\n return text\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0 && !l.startsWith(\"#\"));\n}\n\n// Read a committed default file, tolerating a missing file (unknown runtime →\n// no extra patterns). Returns [] on any read error.\nfunction readDefaultGlobs(name: string): string[] {\n const file = join(DEFAULTS_DIR, `${name}.alfesyncignore`);\n try {\n return parseDefaultGlobs(readFileSync(file, \"utf-8\"));\n } catch {\n return [];\n }\n}\n\n// Load-bearing backstop if the committed common file can't be read (a bundler\n// stripped it, or the tsdown copy step regressed). Failing OPEN to \"ignore\n// nothing\" would re-watch node_modules + the runtime caches and resurrect the\n// exact ENOSPC crash-loop this fix exists to prevent — so the crash-prevention\n// entries are duplicated here. Kept intentionally minimal; the committed files\n// remain the source of truth. The `DEFAULT_IGNORES loaded from committed file`\n// test asserts the real file loaded (not this fallback) so a copy/bundle\n// regression fails CI rather than silently degrading in prod.\nconst CRITICAL_FALLBACK_IGNORES = [\n \"**/node_modules/**\",\n \"**/.git/**\",\n \"**/.env\",\n \"**/__pycache__/**\",\n \"npm/**\",\n \"extensions/**\",\n \"plugins/**\",\n \"**/.venv/**\",\n];\n\n// Default ignore patterns — always applied to every runtime. Exported so tests\n// and the docs generator can introspect the list without duplicating it.\n// Sourced from the committed `defaults/common.alfesyncignore`, with a hardcoded\n// fallback so an unreadable file never fails open.\n//\n// Patterns are full micromatch globs. To match a name at any depth use a\n// globstar prefix (two asterisks plus slash). The `matchBase` option is NOT\n// used because it breaks deep globstar/X/globstar matches in micromatch 4.x.\nconst commonGlobs = readDefaultGlobs(\"common\");\nexport const DEFAULT_IGNORES =\n commonGlobs.length > 0 ? commonGlobs : CRITICAL_FALLBACK_IGNORES;\n\n// Cache the per-runtime default set (common + runtime) so repeated\n// loadIgnorePatterns calls don't re-read the committed files.\nconst runtimeDefaultsCache = new Map<string, string[]>();\n\nfunction runtimeDefaults(runtime: string): string[] {\n const cached = runtimeDefaultsCache.get(runtime);\n if (cached) return cached;\n // common first, then runtime-specific; unknown runtime contributes nothing.\n const patterns = [...DEFAULT_IGNORES, ...readDefaultGlobs(runtime)];\n runtimeDefaultsCache.set(runtime, patterns);\n return patterns;\n}\n\n// Two-tier ignore model. The `forced` tier is the compiled-in runtime-safety\n// base (common + runtime defaults + the CRITICAL_FALLBACK_IGNORES backstop):\n// it is IMMUNE to user negation. The `user` tier is the workspace\n// `.alfesyncignore`, normalised to canonical globs — it is ADDITIVE-ONLY. It\n// may add ignores and negate among its OWN added patterns, but no user pattern\n// (positive or `!`) can un-ignore anything in `forced`.\n//\n// Why forced-immune: the forced base keeps live runtime DBs\n// (state/tasks/flows/browser/memory) and heavy dirs (npm/extensions/plugins/\n// node_modules) out of the synced tree. A workspace `.alfesyncignore` with\n// `!state/**`, `!openclaw.sqlite`, or a broad `!*.sqlite` used to un-ignore the\n// live WAL SQLite → torn-snapshot round-trip → SQLITE_IOERR / \"database disk\n// image is malformed\" crash-loop on cloud-wins restore (Badi Jr incident). The\n// forced tier is a runtime-safety guarantee, not a user preference.\nexport interface IgnoreRules {\n /** Compiled-in runtime-safety base — immune to user negation. */\n forced: string[];\n /** Workspace `.alfesyncignore` — additive-only, may negate among itself. */\n user: string[];\n}\n\n// Coerce the matcher input. A bare `string[]` is treated as forced-only (all\n// patterns immune to negation, no user layer) — this is exactly what the\n// exported `DEFAULT_IGNORES` constant represents, and preserves back-compat for\n// callers/tests that pass a raw default array.\nfunction toRules(input: IgnoreRules | string[]): IgnoreRules {\n return Array.isArray(input) ? { forced: input, user: [] } : input;\n}\n\n// Normalise a user-supplied gitignore-style pattern into a micromatch glob.\n// Mapping (input -> output):\n// foo -> **\\/foo (no slash: match anywhere)\n// /foo -> foo (leading slash: root-anchored)\n// foo/ -> **\\/foo/** (trailing slash: dir + contents anywhere)\n// path/to/foo -> path/to/foo (internal slash: leave anchored)\n// !pattern -> !<recurse on body> (negation; applied to body)\n// This keeps the historical .alfesyncignore UX (gitignore semantics) while\n// the engine matches on full globs without micromatch's matchBase option.\nexport function normaliseIgnorePattern(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return trimmed;\n if (trimmed.startsWith(\"!\")) return `!${normaliseIgnorePattern(trimmed.slice(1))}`;\n if (trimmed.startsWith(\"/\")) {\n const body = trimmed.slice(1);\n return body.endsWith(\"/\") ? `${body.slice(0, -1)}/**` : body;\n }\n // Decide based on whether the unsuffixed pattern has any internal slashes:\n // foo, *.log, scratch.* → basename-style, prepend **/\n // foo/ → basename-style dir, prepend **/ + suffix /**\n // path/to/foo, path/foo/ → workspace-anchored as written\n const stripped = trimmed.endsWith(\"/\") ? trimmed.slice(0, -1) : trimmed;\n const isAnchored = stripped.includes(\"/\");\n if (trimmed.endsWith(\"/\")) return isAnchored ? `${stripped}/**` : `**/${stripped}/**`;\n return isAnchored ? trimmed : `**/${trimmed}`;\n}\n\n// Load the two-tier ignore rules: the committed per-runtime defaults (FORCED,\n// immune to user negation) and the workspace `.alfesyncignore` (USER,\n// additive-only). The forced tier = common + runtime-internal dirs (verbatim\n// globs). The user tier is run through normaliseIgnorePattern so a\n// gitignore-style `*.log` matches at any depth. `runtime` selects the\n// runtime-specific default file (openclaw / hermes); an unknown runtime falls\n// back to the common set.\n//\n// User patterns are NO LONGER merged into the defaults array — that used to let\n// a `!negation` un-ignore a default, which re-opened the live-DB corruption\n// vector. The tiers are kept distinct so `shouldIgnore` can guarantee forced\n// patterns win regardless of anything the user writes.\nexport async function loadIgnorePatterns(\n workspacePath: string,\n runtime = \"openclaw\",\n): Promise<IgnoreRules> {\n const ignoreFile = join(workspacePath, \".alfesyncignore\");\n const forced = [...runtimeDefaults(runtime)];\n const user: string[] = [];\n\n if (existsSync(ignoreFile)) {\n try {\n const content = await readFile(ignoreFile, \"utf-8\");\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n // Skip empty lines and comments\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n user.push(normaliseIgnorePattern(trimmed));\n }\n } catch {\n // Ignore read errors — use defaults only\n }\n }\n\n return { forced, user };\n}\n\n// Check if a relative path should be ignored.\n//\n// Two-tier semantics: ignored iff the FORCED base matches, OR the user tier\n// ignores it. The forced tier is matched FIRST and negation NEVER applies to it\n// — a workspace `.alfesyncignore` can neither positively re-match nor `!`-negate\n// a forced default. The user tier keeps full gitignore semantics WITHIN itself:\n// negation patterns (`!foo`) un-ignore a path matched by an earlier user\n// positive pattern.\n//\n// micromatch.isMatch with an array does NOT honour negation (it OR's all\n// patterns and drops the leading `!`), so we split the USER tier into\n// positive/negative and combine manually: user-ignored iff any user-positive\n// matches AND no user-negative matches. The `matchBase` option is never passed\n// (it breaks deep `**/X/**` matches in micromatch 4.x); `{ dot: true }` is kept\n// so dotfiles are matched.\nexport function shouldIgnore(\n relativePath: string,\n rules: IgnoreRules | string[],\n): boolean {\n const { forced, user } = toRules(rules);\n\n // Forced base wins unconditionally — user negation can't reach it.\n if (forced.length > 0 && micromatch.isMatch(relativePath, forced, { dot: true })) {\n return true;\n }\n\n // User tier: additive-only, gitignore negation within its own patterns.\n const positive: string[] = [];\n const negative: string[] = [];\n for (const p of user) {\n if (p.startsWith(\"!\")) negative.push(p.slice(1));\n else positive.push(p);\n }\n if (positive.length === 0) return false;\n if (!micromatch.isMatch(relativePath, positive, { dot: true })) return false;\n if (negative.length === 0) return true;\n return !micromatch.isMatch(relativePath, negative, { dot: true });\n}\n\n// Check whether a DIRECTORY's contents are ignored — i.e. whether the watcher\n// / scanner may prune it and never descend. A directory `npm` isn't itself\n// matched by `npm/**` (that glob needs a child segment), so we probe a\n// sentinel child: if `npm/<probe>` is ignored, everything under `npm` is too.\n// Used by the chokidar `ignored` predicate and the workspace walk so a single\n// ignore engine governs both file matching and directory pruning.\nconst IGNORE_DIR_PROBE = \"__alfe_ignore_probe__\";\nexport function shouldIgnoreDir(\n relativeDir: string,\n rules: IgnoreRules | string[],\n): boolean {\n if (relativeDir === \"\") return false; // never prune the workspace root\n // Forward-slash join — micromatch globs are posix-style regardless of OS.\n return shouldIgnore(`${relativeDir}/${IGNORE_DIR_PROBE}`, rules);\n}\n\n// Filter a list of relative paths, removing ignored ones.\nexport function filterIgnored(\n paths: string[],\n rules: IgnoreRules | string[],\n): string[] {\n return paths.filter((p) => !shouldIgnore(p, rules));\n}\n","/**\n * Tiny retry helper used by uploader/downloader. Extracted so both\n * use the same timing and surface the same final error shape.\n */\n\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_BASE_DELAY_MS = 1000;\n\nexport interface RetryOptions {\n maxRetries?: number;\n baseDelayMs?: number;\n}\n\n/**\n * Run `fn` up to `maxRetries + 1` times with exponential backoff\n * (`base * 2^attempt`). Returns the first successful value, or rethrows\n * the last error.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n if (attempt < maxRetries) {\n const delay = baseDelayMs * Math.pow(2, attempt);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(String(lastError));\n}\n","/**\n * AlfeSync uploader — push changed files to S3.\n *\n * Per file:\n * 1. Ask the agent API for a presigned PUT URL\n * 2. PUT bytes directly to S3 (raw fetch — S3 isn't an Alfe API)\n * 3. Tell the agent API the upload completed (`syncConfirmUpload`)\n * 4. Update the local manifest\n *\n * Each step retries with exponential backoff via `withRetry`.\n */\n\nimport { readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nimport { computeFileHash, updateManifestEntries } from \"./manifest.js\";\nimport type { ManifestEntry } from \"./manifest.js\";\nimport { withRetry } from \"./retry.js\";\n\nconst log = createLogger(\"SyncUploader\");\n\nexport interface UploadResult {\n path: string;\n success: boolean;\n hash?: string;\n size?: number;\n error?: string;\n}\n\n/**\n * Prefixes stored as GLACIER_IR on the sync bucket. Shared with the recovery\n * classifier in sync-engine.ts, which must never cut conflict sidecars for\n * archive objects (their presigned GETs fail until restored, so hashing/\n * preserving them is wasted work on a pull that can't succeed anyway).\n */\nexport const ARCHIVE_PREFIXES = [\"sessions/archive/\", \"context/archive/\"] as const;\n\nfunction getStorageClass(relativePath: string): \"STANDARD\" | \"GLACIER_IR\" {\n return ARCHIVE_PREFIXES.some((p) => relativePath.startsWith(p))\n ? \"GLACIER_IR\"\n : \"STANDARD\";\n}\n\nfunction getContentType(relativePath: string): string {\n if (relativePath.endsWith(\".json\")) return \"application/json\";\n if (relativePath.endsWith(\".md\")) return \"text/markdown\";\n if (relativePath.endsWith(\".txt\")) return \"text/plain\";\n if (relativePath.endsWith(\".gz\")) return \"application/gzip\";\n if (relativePath.endsWith(\".yaml\") || relativePath.endsWith(\".yml\")) return \"text/yaml\";\n return \"application/octet-stream\";\n}\n\nasync function uploadOne(\n workspacePath: string,\n relativePath: string,\n client: AgentApiClient,\n): Promise<UploadResult & { entry?: ManifestEntry }> {\n const absolutePath = join(workspacePath, relativePath);\n try {\n return await withRetry(async () => {\n const [hash, fileStat] = await Promise.all([\n computeFileHash(absolutePath),\n stat(absolutePath),\n ]);\n const size = fileStat.size;\n const storageClass = getStorageClass(relativePath);\n const contentType = getContentType(relativePath);\n\n // 1. Presigned PUT URL\n const presigned = await client.syncPresign({\n files: [{ path: relativePath, operation: \"put\", contentType }],\n });\n const url = presigned.urls[0]?.url;\n if (!url) throw new Error(\"No presigned URL returned\");\n\n // 2. Upload bytes directly to S3 (not an Alfe API)\n const fileContent = await readFile(absolutePath);\n const putResponse = await fetch(url, {\n method: \"PUT\",\n headers: { \"Content-Type\": contentType },\n body: fileContent,\n });\n if (!putResponse.ok) {\n throw new Error(\n `S3 PUT failed (${String(putResponse.status)}): ${await putResponse.text()}`,\n );\n }\n\n // 3. Confirm upload via agent API\n await client.syncConfirmUpload({\n filePath: relativePath,\n hash,\n size,\n storageClass,\n });\n\n // 4. Manifest write happens in uploadFiles, once per batch —\n // concurrent per-file read-modify-writes of manifest.json silently\n // drop entries (same fix as downloadFiles).\n const entry: ManifestEntry = {\n hash,\n size,\n lastSynced: new Date().toISOString(),\n storageClass,\n };\n return { path: relativePath, success: true, hash, size, entry };\n });\n } catch (err) {\n return {\n path: relativePath,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Upload many files, batched by `concurrency`.\n */\nexport async function uploadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: AgentApiClient,\n options: { concurrency?: number; quiet?: boolean } = {},\n): Promise<UploadResult[]> {\n const { concurrency = 5, quiet = false } = options;\n const results: UploadResult[] = [];\n\n for (let i = 0; i < relativePaths.length; i += concurrency) {\n const batch = relativePaths.slice(i, i + concurrency);\n const batchResults = await Promise.all(\n batch.map((path) => uploadOne(workspacePath, path, client)),\n );\n const entries: Record<string, ManifestEntry> = {};\n for (const result of batchResults) {\n if (result.entry !== undefined) entries[result.path] = result.entry;\n results.push({\n path: result.path,\n success: result.success,\n hash: result.hash,\n size: result.size,\n error: result.error,\n });\n if (!quiet) {\n if (result.success) {\n log.info(`Uploaded ${result.path} (${formatBytes(result.size ?? 0)})`);\n } else {\n log.error(`Failed to upload ${result.path}: ${result.error ?? \"Unknown error\"}`);\n }\n }\n }\n if (Object.keys(entries).length > 0) {\n await updateManifestEntries(workspacePath, entries);\n }\n }\n\n return results;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${String(bytes)} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n","/**\n * AlfeSync downloader — pull files from S3 to disk.\n *\n * Per file:\n * 1. Ask the agent API for a presigned GET URL\n * 2. GET bytes directly from S3 (raw fetch — S3 isn't an Alfe API)\n * 3. Write to disk\n * 4. Update the local manifest\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient } from \"@alfe.ai/agent-api-client\";\n\nimport { updateManifestEntries } from \"./manifest.js\";\nimport type { ManifestEntry, RemoteManifest } from \"./manifest.js\";\nimport { withRetry } from \"./retry.js\";\n\nconst log = createLogger(\"SyncDownloader\");\n\nexport interface DownloadResult {\n path: string;\n success: boolean;\n size?: number;\n error?: string;\n}\n\nasync function downloadOne(\n workspacePath: string,\n relativePath: string,\n client: AgentApiClient,\n remoteEntry?: RemoteManifest[\"files\"][string],\n): Promise<DownloadResult & { entry?: ManifestEntry }> {\n try {\n return await withRetry(async () => {\n const presigned = await client.syncPresign({\n files: [{ path: relativePath, operation: \"get\" }],\n });\n const url = presigned.urls[0]?.url;\n if (!url) throw new Error(\"No presigned URL returned\");\n\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(\n `S3 GET failed (${String(response.status)}): ${await response.text()}`,\n );\n }\n const buffer = Buffer.from(await response.arrayBuffer());\n\n const absolutePath = join(workspacePath, relativePath);\n await mkdir(dirname(absolutePath), { recursive: true });\n await writeFile(absolutePath, buffer);\n\n const entry: ManifestEntry = {\n hash: remoteEntry?.hash ?? \"\",\n size: buffer.length,\n lastSynced: new Date().toISOString(),\n storageClass:\n (remoteEntry?.storageClass ?? \"STANDARD\") as \"STANDARD\" | \"GLACIER_IR\",\n };\n // Manifest write happens in downloadFiles, once per batch — concurrent\n // per-file read-modify-writes of manifest.json silently drop entries.\n return { path: relativePath, success: true, size: buffer.length, entry };\n });\n } catch (err) {\n return {\n path: relativePath,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\nexport async function downloadFiles(\n workspacePath: string,\n relativePaths: string[],\n client: AgentApiClient,\n remoteManifest?: RemoteManifest,\n options: { concurrency?: number; quiet?: boolean } = {},\n): Promise<DownloadResult[]> {\n const { concurrency = 5, quiet = false } = options;\n const results: DownloadResult[] = [];\n\n for (let i = 0; i < relativePaths.length; i += concurrency) {\n const batch = relativePaths.slice(i, i + concurrency);\n const batchResults = await Promise.all(\n batch.map((path) =>\n downloadOne(workspacePath, path, client, remoteManifest?.files[path]),\n ),\n );\n const entries: Record<string, ManifestEntry> = {};\n for (const result of batchResults) {\n if (result.entry !== undefined) entries[result.path] = result.entry;\n results.push({\n path: result.path,\n success: result.success,\n size: result.size,\n error: result.error,\n });\n if (!quiet) {\n if (result.success) {\n log.info(`Downloaded ${result.path} (${formatBytes(result.size ?? 0)})`);\n } else {\n log.error(`Failed to download ${result.path}: ${result.error ?? \"Unknown error\"}`);\n }\n }\n }\n if (Object.keys(entries).length > 0) {\n await updateManifestEntries(workspacePath, entries);\n }\n }\n\n return results;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${String(bytes)} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n","/**\n * AlfeSync engine — orchestrates push, pull, and full sync.\n *\n * - push(paths[]): upload changed files to S3\n * - pull(): download files newer on remote\n * - fullSync(): bidirectional sync with conflict detection\n *\n * Conflict resolution: when a remote file is newer than the local manifest\n * entry AND the local file has also changed, the local copy is renamed to\n * `<name>.conflict-<timestamp>.<ext>` and the remote version wins.\n */\n\nimport { existsSync } from \"node:fs\";\nimport {\n appendFile,\n copyFile,\n mkdir,\n readdir,\n readFile,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { join, dirname, basename, extname } from \"node:path\";\nimport { createLogger } from \"@auriclabs/logger\";\nimport type { AgentApiClient, SyncManifest } from \"@alfe.ai/agent-api-client\";\n\nimport {\n readManifest,\n writeManifest,\n computeFileHash,\n diffManifests,\n removeManifestEntry,\n updateManifestEntries,\n} from \"./manifest.js\";\nimport type { LocalManifest, ManifestEntry, RemoteManifest } from \"./manifest.js\";\nimport { loadIgnorePatterns, filterIgnored, shouldIgnore, shouldIgnoreDir } from \"./ignore.js\";\nimport type { IgnoreRules } from \"./ignore.js\";\nimport { uploadFiles, ARCHIVE_PREFIXES } from \"./uploader.js\";\nimport { downloadFiles } from \"./downloader.js\";\n\nconst log = createLogger(\"SyncEngine\");\n\nexport interface SyncResult {\n pushed: number;\n pulled: number;\n conflicts: number;\n errors: number;\n}\n\n/**\n * Above this size a diverged local file is NOT copied to a `.conflict-*`\n * sidecar during recovery (the cloud copy still replaces it). Sidecars\n * double disk usage per file; a multi-GB session archive could fill the VM.\n * The un-preserved overwrite is never silent — it is logged and listed in\n * the RECOVERY report.\n */\nconst MAX_PRESERVE_BYTES = 100 * 1024 * 1024;\n\n/** Marker that keeps the heartbeat recovery nudge idempotent across runs. */\nconst RECOVERY_NUDGE_MARKER = \"<!-- alfe-sync:recovery-nudge -->\";\n\n/**\n * Recovery artifacts: `.conflict-<ts>` sidecars and `RECOVERY-<ts>.md`\n * reports. Anchored to the timestamp shape so legitimate user files like\n * `my.conflict-notes.txt` or `RECOVERY-plan.md` are never misclassified.\n *\n * They seed UP to the cloud so a preserved local version survives even if\n * the VM dies before the agent merges it (retrievable via the dashboard\n * file manager), but they are never AUTO-restored back down and never\n * preserved again — restoring would resurrect copies the agent already\n * cleaned up, and re-preserving would cascade `.conflict`-of-`.conflict`\n * files. Cloud copies are reclaimed when the agent's local cleanup\n * propagates through the watcher; artifacts orphaned by a rebuild before\n * cleanup linger until then (accepted: small, user-visible, deletable).\n * The watcher's delete brake also exempts them so the agent's post-merge\n * cleanup of many sidecars can't trip it.\n *\n * Deliberately NOT in the ignore set: ignored paths don't seed up and would\n * be eligible for pruneIgnored deletion.\n */\nexport function isRecoveryArtifact(relativePath: string): boolean {\n const name = basename(relativePath);\n return (\n /\\.conflict-\\d{4}-\\d{2}-\\d{2}T/.test(name) ||\n /^RECOVERY-\\d{4}-\\d{2}-\\d{2}T.*\\.md$/.test(name)\n );\n}\n\nexport interface SyncEngineOptions {\n workspacePath: string;\n client: AgentApiClient;\n /**\n * Active agent runtime — selects the per-runtime default ignore set used by\n * every scan (push / fullSync / firstRunReconcile). Default: \"openclaw\".\n */\n runtime?: string;\n /**\n * Recovery sidecar size cap override — see MAX_PRESERVE_BYTES. Exists so\n * tests can exercise the over-cap path without multi-hundred-MB fixtures.\n */\n maxPreserveBytes?: number;\n}\n\n/**\n * Construct a sync engine bound to a workspace + agent API client.\n *\n * The client is constructed once in plugin.ts (or the CLI) and passed in,\n * so credentials never leak into multiple places.\n */\nexport function createSyncEngine({\n workspacePath,\n client,\n runtime = \"openclaw\",\n maxPreserveBytes = MAX_PRESERVE_BYTES,\n}: SyncEngineOptions) {\n return {\n workspacePath,\n client,\n runtime,\n\n /**\n * Upload changed files. Pass `paths` for an explicit list, or omit\n * to scan the workspace for anything that drifted from the manifest.\n */\n async push(\n paths?: string[],\n options: { quiet?: boolean; filter?: string } = {},\n ): Promise<SyncResult> {\n const { quiet = false, filter } = options;\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n\n let filesToPush: string[] =\n paths && paths.length > 0\n ? filterIgnored(paths, ignorePatterns)\n : await detectLocalChanges(workspacePath, ignorePatterns);\n\n if (filter) {\n filesToPush = filesToPush.filter((p) => p.startsWith(filter));\n }\n\n if (filesToPush.length === 0) {\n if (!quiet) log.info(\"Nothing to push\");\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n\n if (!quiet) log.info(`Pushing ${String(filesToPush.length)} file(s)`);\n\n const results = await uploadFiles(workspacePath, filesToPush, client, { quiet });\n const pushed = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Push complete: ${String(pushed)} uploaded, ${String(errors)} failed`);\n }\n\n return { pushed, pulled: 0, conflicts: 0, errors };\n },\n\n // Propagate local deletes to the cloud. Called by the watcher's onChanges\n // when chokidar reports `unlink` events (paths that no longer exist on\n // disk). Each path is removed via `client.syncDeleteFile()` then dropped\n // from the local manifest. Wrapped by the caller with a rolling-window\n // rate brake — see makeDeleteBrake() — so a buggy watcher event or a\n // mistaken `rm -rf` can't nuke the cloud copy in one go.\n async pushDeletes(\n paths: string[],\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n if (paths.length === 0) return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n if (!quiet) log.info(`Deleting ${String(paths.length)} file(s) from cloud`);\n\n let deleted = 0;\n let errors = 0;\n for (const path of paths) {\n try {\n await client.syncDeleteFile(path);\n await removeManifestEntry(workspacePath, path);\n deleted++;\n if (!quiet) log.info(`Deleted from cloud: ${path}`);\n } catch (err) {\n errors++;\n if (!quiet) log.error({ err }, `Failed to delete ${path} from cloud`);\n }\n }\n\n if (!quiet) {\n log.info(`Delete complete: ${String(deleted)} deleted, ${String(errors)} failed`);\n }\n // Reuse `pushed` to count successful deletes — same shape, distinct meaning.\n return { pushed: deleted, pulled: 0, conflicts: 0, errors };\n },\n\n /** Download files newer on remote. */\n async pull(options: { quiet?: boolean } = {}): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.syncGetManifest(),\n ]);\n\n const diff = diffManifests(localManifest, remoteManifest);\n if (diff.toPull.length === 0) {\n if (!quiet) log.info(\"Nothing to pull\");\n return { pushed: 0, pulled: 0, conflicts: diff.conflicts.length, errors: 0 };\n }\n\n if (!quiet) log.info(`Pulling ${String(diff.toPull.length)} file(s)`);\n\n const results = await downloadFiles(\n workspacePath,\n [...diff.toPull],\n client,\n remoteManifest,\n { quiet },\n );\n const pulled = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);\n }\n\n return { pushed: 0, pulled, conflicts: diff.conflicts.length, errors };\n },\n\n /**\n * Bidirectional sync. True conflicts (changed on both sides) are saved\n * locally as `.conflict-<ts>` files and the remote version wins.\n */\n async fullSync(options: { quiet?: boolean } = {}): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const [localManifest, remoteManifest] = await Promise.all([\n readManifest(workspacePath),\n client.syncGetManifest(),\n ]);\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);\n\n const diff = diffManifests(localManifest, remoteManifest);\n const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));\n const remoteOnlyChanges = diff.conflicts.filter((p) => !localChanges.includes(p));\n\n let conflictCount = 0;\n const conflictTimestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n for (const conflictPath of trueConflicts) {\n const outcome = await preserveConflictCopy(\n workspacePath,\n conflictPath,\n conflictTimestamp,\n );\n if (outcome.status === \"vanished\") continue;\n if (outcome.status !== \"too-large\" && !quiet) {\n log.warn(`Conflict: ${conflictPath} — saved as ${outcome.conflictPath}`);\n }\n conflictCount++;\n }\n\n const filesToPush = filterIgnored(\n [...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))],\n ignorePatterns,\n );\n const filesToPull = [\n ...diff.toPull,\n ...remoteOnlyChanges,\n ...trueConflicts,\n ];\n\n const pushResult = { pushed: 0, errors: 0 };\n const pullResult = { pulled: 0, errors: 0 };\n\n if (filesToPush.length > 0) {\n if (!quiet) log.info(`Pushing ${String(filesToPush.length)} file(s)`);\n const results = await uploadFiles(\n workspacePath,\n [...new Set(filesToPush)],\n client,\n { quiet },\n );\n pushResult.pushed = results.filter((r) => r.success).length;\n pushResult.errors = results.filter((r) => !r.success).length;\n }\n\n if (filesToPull.length > 0) {\n if (!quiet) log.info(`Pulling ${String(filesToPull.length)} file(s)`);\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\n );\n pullResult.pulled = results.filter((r) => r.success).length;\n pullResult.errors = results.filter((r) => !r.success).length;\n }\n\n const totalErrors = pushResult.errors + pullResult.errors;\n if (!quiet) {\n log.info(\n `Sync complete: ${String(pushResult.pushed)} pushed, ${String(pullResult.pulled)} pulled, ${String(conflictCount)} conflicts, ${String(totalErrors)} errors`,\n );\n }\n return {\n pushed: pushResult.pushed,\n pulled: pullResult.pulled,\n conflicts: conflictCount,\n errors: totalErrors,\n };\n },\n\n /**\n * First-run reconcile for realtime activation.\n *\n * Distinguishes a genuinely NEW agent (empty cloud state → push-only\n * seed, today's behaviour) from a REBUILD / reconnect (cloud already\n * holds this agent's last-synced state → restore cloud → local, cloud\n * wins, THEN seed only the files that are new locally).\n *\n * Why not `push(undefined)`? On a from-scratch VM rebuild the local\n * manifest is gone and `alfe setup` has just re-seeded the persona\n * templates (SOUL.md, IDENTITY.md, …) onto a fresh disk. With no\n * manifest, `detectLocalChanges()` classifies every seeded template as\n * \"new\", so a blind push uploads those fresh seeds OVER the agent's\n * edited cloud copy — the agent's edits are lost on every rebuild. This\n * is the persona-durability gap this method closes.\n *\n * Why not `fullSync()`? With an empty local manifest it double-counts\n * each on-disk seed as BOTH a push candidate (`detectLocalChanges`) and\n * a pull candidate (remote-only in `diffManifests`, since the local\n * manifest has no entry for it). fullSync pushes before it pulls, so it\n * would still clobber the cloud edit before restoring it locally.\n *\n * Conflict direction: the CLOUD copy is authoritative for anything it\n * holds, but diverged UNSYNCED local content is never silently\n * destroyed — it is preserved next to the restored file as a\n * `.conflict-<ts>` sidecar and indexed in a `RECOVERY-<ts>.md` report\n * the agent is nudged (via HEARTBEAT.md, openclaw only) to reconcile.\n * Known noise: on a fresh-disk rebuild the setup-seeded persona\n * templates diverge from the edited cloud copies, so each gets a small\n * sidecar of the pristine template — bounded, listed in the report, and\n * cleaned up by the agent. The plugin can't tell a seed from an edit\n * without the template registry, and guessing (e.g. mtime windows)\n * would reintroduce exactly the silent-loss hole this closes.\n *\n * Per-path classification (see classifyRestorePaths): files whose local\n * manifest entry matches the cloud hash but whose disk content moved on\n * (`localAhead` — edited while the plugin was down) are PUSHED, not\n * restored; files deleted locally while the plugin was down\n * (`offlineDelete`) are not resurrected; files already matching the\n * cloud are healed into the manifest without a download.\n */\n async firstRunReconcile(\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n\n const remoteManifest = await client.syncGetManifest();\n const remotePaths = Object.keys(remoteManifest.files);\n\n // Fresh agent: nothing in the cloud → seed the workspace up (today's\n // push-only behaviour). There is no prior state to lose, so pushing\n // the setup-seeded templates is correct. Stamp the owning agentId so\n // the provenance guard works from day one — without this, a machine\n // re-adopted by a different agent BEFORE its first rebuild would\n // present an unstamped (grandfathered/trusted) manifest and the guard\n // would never fire.\n if (remotePaths.length === 0) {\n if (!quiet) {\n log.info(\"First-run sync: no cloud state — seeding workspace to cloud\");\n }\n const seedResult = await this.push(undefined, options);\n await updateManifestEntries(workspacePath, {}, {\n agentId: remoteManifest.agentId,\n });\n return seedResult;\n }\n\n // Rebuild / reconnect: the cloud is the source of truth for anything\n // it holds. Restore it over the local state, preserving any diverged\n // unsynced local content as conflict sidecars first.\n if (!quiet) {\n log.info(\n `First-run sync: ${String(remotePaths.length)} file(s) in cloud — restoring cloud state before seeding`,\n );\n }\n\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n\n // Ignore-filter the restore list so a pre-fix workspace's stale\n // ignored objects (e.g. `**/Cache/**`) that an older buggy ignore\n // engine pushed into the cloud aren't dragged back onto a fresh disk.\n // Recovery artifacts are also excluded — see isRecoveryArtifact.\n const restorePaths = filterIgnored(remotePaths, ignorePatterns).filter(\n (p) => !isRecoveryArtifact(p),\n );\n\n const localManifest = await readManifest(workspacePath);\n // Machine re-adopted by a DIFFERENT agent without clearing\n // `~/.alfe/sync/`? Then the manifest describes the previous agent's\n // sync state: classify as if it were lost (diverged files are\n // preserved; nothing is pushed or skipped on its say-so) and reset it\n // so downloads don't merge fresh entries into stale ones.\n const manifestTrusted =\n localManifest.agentId === undefined ||\n localManifest.agentId === remoteManifest.agentId;\n if (!manifestTrusted) {\n log.warn(\n `Local sync manifest belongs to agent ${localManifest.agentId ?? \"?\"} but cloud state is agent ${remoteManifest.agentId} — treating the manifest as lost`,\n );\n await writeManifest(workspacePath, {\n files: {},\n agentId: remoteManifest.agentId,\n });\n }\n\n const classes = await classifyRestorePaths(\n workspacePath,\n restorePaths,\n remoteManifest.files,\n localManifest,\n manifestTrusted,\n );\n\n // Not a data-displacement event (nothing is overwritten), so per-path\n // detail respects quiet; a one-line summary is always kept.\n for (const p of classes.offlineDeletes) {\n if (!quiet) {\n log.info(`Recovery: ${p} was deleted locally while sync was down — not restoring it`);\n }\n }\n if (classes.offlineDeletes.length > 0) {\n log.info(\n `Recovery: ${String(classes.offlineDeletes.length)} locally-deleted file(s) not restored from cloud`,\n );\n }\n\n // Preserve diverged local content BEFORE the cloud copy overwrites it.\n // These warns are deliberately unconditional (not gated on `quiet`) —\n // they record the only moments local data is displaced.\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const preserved: PreservedRecord[] = [];\n let newlyPreserved = 0;\n for (const { path, diskHash, size } of classes.preserve) {\n const outcome = await preserveConflictCopy(workspacePath, path, timestamp, {\n maxBytes: maxPreserveBytes,\n sourceHash: diskHash,\n });\n if (outcome.status === \"vanished\") continue;\n const record: PreservedRecord = {\n path,\n localHash: diskHash,\n remoteHash: remoteManifest.files[path].hash,\n size,\n };\n if (outcome.status === \"too-large\") {\n record.tooLarge = true;\n newlyPreserved++;\n log.warn(\n `Recovery: ${path} diverged locally but exceeds the sidecar size cap — cloud version will replace it WITHOUT a preserved copy`,\n );\n } else {\n record.conflictPath = outcome.conflictPath;\n if (outcome.status === \"preserved\") newlyPreserved++;\n log.warn(\n `Recovery: preserved local ${path} as ${outcome.conflictPath} — cloud version restored in place`,\n );\n }\n preserved.push(record);\n }\n\n // Recovery report — written under the agent's working dir so the agent\n // actually encounters it, and seeded to cloud (net-new path) for\n // durability across another rebuild. A repeat recovery whose sidecars\n // were all already preserved (crash-loop retry, unresolved prior\n // recovery) doesn't stack another report — unless no report exists at\n // all (crashed before writing it last time).\n let recoveryReportPath: string | null = null;\n const reportNeeded =\n preserved.length > 0 &&\n (newlyPreserved > 0 || !(await recoveryReportExists(workspacePath)));\n if (reportNeeded) {\n recoveryReportPath = `${agentDirPrefix(workspacePath)}RECOVERY-${timestamp}.md`;\n const reportAbsolute = join(workspacePath, recoveryReportPath);\n await mkdir(dirname(reportAbsolute), { recursive: true });\n await writeFile(\n reportAbsolute,\n buildRecoveryReport(preserved, workspacePath),\n \"utf-8\",\n );\n log.warn(\n `Recovery: ${String(preserved.length)} diverged local file(s) — see ${recoveryReportPath}`,\n );\n }\n\n // Restore cloud → local. downloadFiles is used directly (not\n // pullFiles): pullFiles' manifest-hash filter would skip the preserve\n // paths whose manifest entry happens to match the cloud.\n const pullList = [\n ...classes.missing,\n ...classes.pullClean,\n ...classes.preserve.map((p) => p.path),\n ];\n const restore = { pulled: 0, errors: 0 };\n if (pullList.length > 0) {\n if (!quiet) log.info(`Pulling ${String(pullList.length)} file(s)`);\n const results = await downloadFiles(\n workspacePath,\n pullList,\n client,\n remoteManifest,\n options,\n );\n restore.pulled = results.filter((r) => r.success).length;\n restore.errors = results.filter((r) => !r.success).length;\n }\n\n // Heal the manifest for files already matching the cloud (no download\n // needed) and stamp the owning agentId. Single bulk write — the\n // per-entry helper would rewrite the JSON once per file.\n if (\n Object.keys(classes.heal).length > 0 ||\n localManifest.agentId !== remoteManifest.agentId\n ) {\n await updateManifestEntries(workspacePath, classes.heal, {\n agentId: remoteManifest.agentId,\n });\n }\n\n // Seed up files that are genuinely new locally (sidecars and the\n // recovery report are on disk by now, so they ride this push), plus\n // localAhead files — the cloud holds an older copy, so they're not\n // net-new, but local is the only side that advanced since last sync.\n const remoteSet = new Set(remotePaths);\n const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);\n const toSeed = localChanges.filter((p) => !remoteSet.has(p));\n const pushList = [...toSeed, ...classes.localAhead];\n const seed =\n pushList.length > 0\n ? await this.push(pushList, options)\n : { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n // Active self-resolution: nudge the agent to merge the preserved\n // copies. OpenClaw reads HEARTBEAT.md from its agent workspace dir on\n // the periodic heartbeat; hermes has no such convention. The nudge\n // file may already exist in the cloud, so it can't ride the net-new\n // seed — push it explicitly.\n let nudge = { pushed: 0, errors: 0 };\n if (preserved.length > 0 && recoveryReportPath !== null && runtime === \"openclaw\") {\n try {\n const nudgePath = await appendRecoveryNudge(\n workspacePath,\n recoveryReportPath,\n preserved.length,\n );\n if (nudgePath !== null) {\n const pushResult = await this.push([nudgePath], options);\n nudge = { pushed: pushResult.pushed, errors: pushResult.errors };\n }\n } catch (err) {\n log.warn(\n `Recovery: failed to write heartbeat nudge: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n return {\n pushed: seed.pushed + nudge.pushed,\n pulled: restore.pulled,\n conflicts: preserved.length + seed.conflicts,\n errors: restore.errors + seed.errors + nudge.errors,\n };\n },\n\n /**\n * Pull a known list of files (used for relay-driven notifications,\n * where we already know which paths changed remotely).\n */\n async pullFiles(\n paths: string[],\n options: { quiet?: boolean } = {},\n ): Promise<SyncResult> {\n const { quiet = false } = options;\n if (paths.length === 0) return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n\n const remoteManifest: SyncManifest = await client.syncGetManifest();\n const localManifest = await readManifest(workspacePath);\n\n const filesToPull = paths.filter((p) => {\n if (!(p in remoteManifest.files)) return false;\n if (!(p in localManifest.files)) return true;\n return localManifest.files[p].hash !== remoteManifest.files[p].hash;\n });\n\n if (filesToPull.length === 0) {\n if (!quiet) log.info(\"All notified files already in sync\");\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n\n if (!quiet) log.info(`Pulling ${String(filesToPull.length)} changed file(s)`);\n\n const results = await downloadFiles(\n workspacePath,\n filesToPull,\n client,\n remoteManifest,\n { quiet },\n );\n const pulled = results.filter((r) => r.success).length;\n const errors = results.filter((r) => !r.success).length;\n\n if (!quiet) {\n log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);\n }\n return { pushed: 0, pulled, conflicts: 0, errors };\n },\n\n /**\n * Purge cloud files that match the current (per-runtime) ignore set.\n *\n * Self-heals workspaces whose cloud copy still holds runtime-internal junk\n * (node_modules, the openclaw npm cache, …) that a pre-fix ignore engine\n * uploaded before it was correctly ignored. Bounded to ignored paths only,\n * so it can never delete a legitimately-synced file, and it deletes the\n * cloud copy directly (not via the watcher) so the rolling-window delete\n * brake — which guards against a buggy `rm -rf` — does not apply.\n */\n async pruneIgnored(\n options: { quiet?: boolean; limit?: number } = {},\n ): Promise<SyncResult> {\n // Cap deletes per pass. pushDeletes is serial (one HTTP call per path), so\n // an agent with thousands of pre-fix leaked node_modules objects would\n // otherwise hammer the sync service in one burst. Draining over several\n // activations is fine — the watcher already ignores these paths, so they\n // never come back and the remainder is cleared on the next start.\n const { quiet = false, limit = 1000 } = options;\n const ignorePatterns = await loadIgnorePatterns(workspacePath, runtime);\n const remoteManifest = await client.syncGetManifest();\n const ignoredPaths = Object.keys(remoteManifest.files).filter((p) =>\n shouldIgnore(p, ignorePatterns),\n );\n if (ignoredPaths.length === 0) {\n return { pushed: 0, pulled: 0, conflicts: 0, errors: 0 };\n }\n const batch = ignoredPaths.slice(0, limit);\n if (!quiet) {\n const capped = ignoredPaths.length > batch.length\n ? ` (capped at ${String(limit)}; ${String(ignoredPaths.length - batch.length)} remain for next pass)`\n : \"\";\n log.info(`Pruning ${String(batch.length)} ignored file(s) from cloud${capped}`);\n }\n return this.pushDeletes(batch, { quiet });\n },\n\n /**\n * Delete a file locally and from the manifest. Used when the sync relay\n * tells us a file was removed remotely.\n */\n async removeLocalFile(\n filePath: string,\n options: { quiet?: boolean } = {},\n ): Promise<void> {\n const { quiet = false } = options;\n const absolutePath = join(workspacePath, filePath);\n\n try {\n const { unlink } = await import(\"node:fs/promises\");\n if (existsSync(absolutePath)) {\n await unlink(absolutePath);\n if (!quiet) log.info(`Deleted: ${filePath}`);\n }\n } catch (err) {\n if (!quiet) log.error({ err }, `Failed to delete ${filePath}`);\n }\n\n await removeManifestEntry(workspacePath, filePath);\n },\n };\n}\n\nexport type SyncEngine = ReturnType<typeof createSyncEngine>;\n\n/**\n * Walk the workspace and return paths whose hash differs from the manifest\n * (or that are missing from it entirely).\n */\nasync function detectLocalChanges(\n workspacePath: string,\n ignorePatterns: IgnoreRules,\n): Promise<string[]> {\n const manifest = await readManifest(workspacePath);\n const changed: string[] = [];\n\n const { readdir } = await import(\"node:fs/promises\");\n\n async function walk(dir: string): Promise<void> {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n const relativePath = fullPath\n .slice(workspacePath.length + 1)\n .replace(/\\\\/g, \"/\");\n\n if (entry.isDirectory()) {\n // Prune ignored directories (incl. node_modules and the runtime's\n // internal dirs) via the same ignore engine the watcher uses — one\n // source of truth, no hardcoded name list to drift.\n if (!shouldIgnoreDir(relativePath, ignorePatterns)) await walk(fullPath);\n } else if (entry.isFile()) {\n if (shouldIgnore(relativePath, ignorePatterns)) continue;\n if (!(relativePath in manifest.files)) {\n changed.push(relativePath);\n continue;\n }\n try {\n const currentHash = await computeFileHash(fullPath);\n if (currentHash !== manifest.files[relativePath].hash) {\n changed.push(relativePath);\n }\n } catch {\n // file may have been deleted between listing and hashing\n }\n }\n }\n }\n\n await walk(workspacePath);\n return changed;\n}\n\n/**\n * Relative prefix of the agent's actual working directory. The sync root is\n * the runtime HOME (`~/.openclaw`), but the agent lives and works in\n * `~/.openclaw/workspace/` — recovery artifacts meant for the agent's eyes\n * (RECOVERY report, heartbeat nudge) must land there or it never sees them.\n * Falls back to the sync root when no `workspace/` dir exists (tests, CLI\n * usage against a bare directory).\n */\nfunction agentDirPrefix(workspacePath: string): string {\n return existsSync(join(workspacePath, \"workspace\")) ? \"workspace/\" : \"\";\n}\n\n/** Whether an unresolved RECOVERY-*.md report already sits in the agent dir. */\nasync function recoveryReportExists(workspacePath: string): Promise<boolean> {\n const dir = join(workspacePath, agentDirPrefix(workspacePath));\n const entries = await readdir(dir).catch(() => [] as string[]);\n return entries.some((name) => name.startsWith(\"RECOVERY-\") && name.endsWith(\".md\"));\n}\n\ntype PreserveOutcome =\n | { status: \"preserved\"; conflictPath: string }\n | { status: \"already-preserved\"; conflictPath: string }\n | { status: \"too-large\"; size: number }\n | { status: \"vanished\" };\n\n/**\n * Copy a file aside as `<base>.conflict-<ts><ext>` in the same directory\n * before a remote copy overwrites it. Uses `copyFile` (no buffering — safe\n * for large/binary files).\n *\n * Idempotent: if a sibling sidecar with identical content already exists\n * (crash-loop retry, repeated recovery), no new copy is stacked. Pass\n * `sourceHash` when the caller already hashed the file.\n */\nasync function preserveConflictCopy(\n workspacePath: string,\n relativePath: string,\n timestamp: string,\n options: { maxBytes?: number; sourceHash?: string } = {},\n): Promise<PreserveOutcome> {\n const absolutePath = join(workspacePath, relativePath);\n const ext = extname(relativePath);\n const base = basename(relativePath, ext);\n const dir = dirname(relativePath);\n\n try {\n const fileStat = await stat(absolutePath);\n if (options.maxBytes !== undefined && fileStat.size > options.maxBytes) {\n return { status: \"too-large\", size: fileStat.size };\n }\n\n const sourceHash = options.sourceHash ?? (await computeFileHash(absolutePath));\n const siblings = await readdir(join(workspacePath, dir)).catch(() => [] as string[]);\n for (const name of siblings) {\n if (!name.startsWith(`${base}.conflict-`)) continue;\n if (ext !== \"\" && !name.endsWith(ext)) continue;\n const existingHash = await computeFileHash(join(workspacePath, dir, name)).catch(\n () => null,\n );\n if (existingHash === sourceHash) {\n return {\n status: \"already-preserved\",\n conflictPath: join(dir, name).replace(/\\\\/g, \"/\"),\n };\n }\n }\n\n const conflictName = `${base}.conflict-${timestamp}${ext}`;\n await copyFile(absolutePath, join(workspacePath, dir, conflictName));\n return {\n status: \"preserved\",\n conflictPath: join(dir, conflictName).replace(/\\\\/g, \"/\"),\n };\n } catch {\n // Source vanished between classification and copy — the pull will\n // restore the cloud copy; nothing local left to preserve.\n return { status: \"vanished\" };\n }\n}\n\ninterface RestoreClassification {\n /** Absent locally (or vanished mid-scan) → plain pull. */\n missing: string[];\n /** Deleted locally while the plugin was down (manifest matches cloud) → skip; don't resurrect. */\n offlineDeletes: string[];\n /** Disk already matches the cloud but the manifest doesn't know → heal entries, no download. */\n heal: Record<string, ManifestEntry>;\n /** Local unchanged since last sync, remote advanced → plain pull (not a conflict). */\n pullClean: string[];\n /** Local edited while the plugin was down, cloud unchanged since our last sync → push, never pull. */\n localAhead: string[];\n /** Unsynced local content diverges from the cloud → sidecar, then pull (cloud wins). */\n preserve: { path: string; diskHash: string; size: number }[];\n}\n\nconst CLASSIFY_CONCURRENCY = 8;\n\n/**\n * Classify every cloud-held path for recovery. The destructive decision\n * (overwrite local content) is only ever taken when the DISK hash diverges\n * from the cloud AND the local manifest can't vouch for the disk content —\n * and even then the local bytes are preserved first. The manifest and mtime\n * are used solely to avoid work and to detect local-ahead edits, never to\n * authorize an unpreserved overwrite.\n */\nasync function classifyRestorePaths(\n workspacePath: string,\n restorePaths: string[],\n remoteFiles: RemoteManifest[\"files\"],\n localManifest: LocalManifest,\n manifestTrusted: boolean,\n): Promise<RestoreClassification> {\n const out: RestoreClassification = {\n missing: [],\n offlineDeletes: [],\n heal: {},\n pullClean: [],\n localAhead: [],\n preserve: [],\n };\n\n async function classifyOne(path: string): Promise<void> {\n const remote = remoteFiles[path];\n const entry = manifestTrusted ? localManifest.files[path] : undefined;\n const absolutePath = join(workspacePath, path);\n\n let fileStat = null;\n try {\n fileStat = await stat(absolutePath);\n } catch {\n // missing on disk\n }\n if (!fileStat?.isFile()) {\n if (entry?.hash === remote.hash) out.offlineDeletes.push(path);\n else out.missing.push(path);\n return;\n }\n\n // Stat fast-path: same size and untouched since our last sync → trust\n // the manifest hash without re-reading the file, keeping warm restarts\n // cheap. mtime only ever SKIPS work here — a false \"changed\" costs one\n // hash; a false \"unchanged\" needs a backdated-mtime same-size edit.\n const unchangedSinceSync =\n fileStat.size === entry?.size &&\n fileStat.mtimeMs <= Date.parse(entry.lastSynced);\n\n // Archive objects (GLACIER_IR) are classified on manifest data only —\n // they can be huge, and their presigned GET fails until restored, so\n // hashing or sidecaring them buys nothing.\n const isArchive = ARCHIVE_PREFIXES.some((p) => path.startsWith(p));\n\n if (unchangedSinceSync || isArchive) {\n if (entry?.hash === remote.hash) return; // in sync\n out.pullClean.push(path);\n return;\n }\n\n let diskHash: string;\n try {\n diskHash = await computeFileHash(absolutePath);\n } catch {\n out.missing.push(path); // vanished between stat and hash\n return;\n }\n\n if (diskHash === remote.hash) {\n if (entry?.hash !== remote.hash) {\n out.heal[path] = {\n hash: remote.hash,\n size: fileStat.size,\n lastSynced: new Date().toISOString(),\n storageClass: (remote.storageClass ?? \"STANDARD\") as \"STANDARD\" | \"GLACIER_IR\",\n };\n }\n return;\n }\n if (entry?.hash === diskHash) {\n out.pullClean.push(path);\n return;\n }\n if (entry?.hash === remote.hash) {\n out.localAhead.push(path);\n return;\n }\n out.preserve.push({ path, diskHash, size: fileStat.size });\n }\n\n for (let i = 0; i < restorePaths.length; i += CLASSIFY_CONCURRENCY) {\n await Promise.all(\n restorePaths.slice(i, i + CLASSIFY_CONCURRENCY).map(classifyOne),\n );\n }\n return out;\n}\n\ninterface PreservedRecord {\n path: string;\n /** Absent when the file exceeded the sidecar size cap. */\n conflictPath?: string;\n localHash: string;\n remoteHash: string;\n size: number;\n tooLarge?: boolean;\n}\n\nfunction buildRecoveryReport(\n records: PreservedRecord[],\n workspacePath: string,\n): string {\n const lines: string[] = [\n `# Workspace recovery report`,\n \"\",\n `A sync recovery restored this agent's cloud-saved workspace. The files`,\n `below had local content that differed from the cloud copy. The cloud`,\n `version now lives at the original path; the pre-recovery local version`,\n `was preserved next to it as a \\`.conflict-*\\` sidecar.`,\n \"\",\n `**What to do:** open each sidecar, merge anything worth keeping into the`,\n `live file, then delete the sidecar. Delete this report when done.`,\n \"\",\n `Paths are relative to the sync root: \\`${workspacePath}\\``,\n \"\",\n ];\n const capped = records.filter((r) => r.tooLarge === true);\n const kept = records.filter((r) => r.tooLarge !== true);\n if (kept.length > 0) {\n lines.push(`| File | Preserved copy | Local hash | Cloud hash | Size (bytes) |`);\n lines.push(`| --- | --- | --- | --- | --- |`);\n for (const r of kept) {\n lines.push(\n `| \\`${r.path}\\` | \\`${r.conflictPath ?? \"\"}\\` | \\`${r.localHash}\\` | \\`${r.remoteHash}\\` | ${String(r.size)} |`,\n );\n }\n lines.push(\"\");\n }\n if (capped.length > 0) {\n lines.push(\n `The following diverged files exceeded the sidecar size cap and were`,\n `replaced by the cloud version WITHOUT a preserved copy:`,\n \"\",\n );\n for (const r of capped) {\n lines.push(`- \\`${r.path}\\` (${String(r.size)} bytes, local hash \\`${r.localHash}\\`)`);\n }\n lines.push(\"\");\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Append the recovery nudge to the agent's HEARTBEAT.md (OpenClaw reads it\n * from the agent workspace dir on its periodic heartbeat). Returns the\n * sync-relative path when a nudge was appended, or null when a prior nudge\n * is still pending (idempotency marker present).\n */\nasync function appendRecoveryNudge(\n workspacePath: string,\n recoveryReportPath: string,\n preservedCount: number,\n): Promise<string | null> {\n const prefix = agentDirPrefix(workspacePath);\n const heartbeatPath = `${prefix}HEARTBEAT.md`;\n const absolutePath = join(workspacePath, heartbeatPath);\n\n const existing = existsSync(absolutePath)\n ? await readFile(absolutePath, \"utf-8\")\n : \"\";\n if (existing.includes(RECOVERY_NUDGE_MARKER)) return null;\n\n // Report path shown relative to the agent's working dir when possible.\n const reportForAgent = recoveryReportPath.startsWith(prefix)\n ? recoveryReportPath.slice(prefix.length)\n : recoveryReportPath;\n\n // Reference the report glob, not just the latest report: a later recovery\n // can add another RECOVERY-*.md while this nudge (marker-idempotent, so\n // never re-appended) is still pending — the instruction must cover it.\n const block = [\n \"\",\n RECOVERY_NUDGE_MARKER,\n \"## Workspace recovery needs your attention\",\n \"\",\n `A sync recovery preserved ${String(preservedCount)} pre-recovery local file version(s)`,\n `as \\`.conflict-*\\` sidecars. Read every \\`RECOVERY-*.md\\` report in this directory`,\n `(latest: \\`${reportForAgent}\\`), merge anything worth keeping into the live files,`,\n `then delete the sidecars, the reports, and this section (including its marker`,\n `comment).`,\n \"\",\n ].join(\"\\n\");\n\n await mkdir(dirname(absolutePath), { recursive: true });\n await appendFile(absolutePath, block, \"utf-8\");\n return heartbeatPath;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-sync",
3
- "version": "0.2.10",
3
+ "version": "0.3.1",
4
4
  "description": "AlfeSync — agent workspace backup and sync skill for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "commander": "^13.0.0",
22
22
  "micromatch": "^4.0.8",
23
23
  "ws": "^8.18.0",
24
- "@alfe.ai/agent-api-client": "0.10.0",
24
+ "@alfe.ai/agent-api-client": "0.11.0",
25
25
  "@alfe.ai/config": "0.3.0"
26
26
  },
27
27
  "peerDependencies": {