/** * Desktop-daemon hook for the sync-reconciliation audit (US-004). * * WHY THIS LIVES IN THE RUNNER * ---------------------------- * The HQ Sync desktop app does not run a sync pass itself — it spawns * `hq-sync-runner` and consumes its ndjson. So "after a successful sync pass * the desktop daemon uploads a manifest" IS a runner concern: the runner is * the only process that knows which scopes were synced, which of them finished * CLEANLY, where the HQ root and the state dir are, and how to mint a JWT. * * WHAT THIS OWNS (and nothing more) * --------------------------------- * - picking the scopes that earned an upload (clean pass only); * - resolving the installation/machine identity; * - the authed transport (base URL + bearer); * - turning each pass result into one additive ndjson event. * * Everything that makes an upload SAFE — the kill switch, the 24h per-scope * throttle, sequencing, chunking, the delta base, the 404 soft skip and the * upload wall clock — belongs to `runManifestUploadPass` and is deliberately * NOT re-derived here. * * FAILURE POSTURE * --------------- * This is a tail step: it runs AFTER `all-complete` has been emitted, and it * can never change the runner's exit code or any pre-existing event. An audit * that can break sync is strictly worse than no audit, so every call site is * wrapped and every outcome — including a throwing or hanging transport — ends * as a diagnostic plus (where a pass ran) one `manifest-upload` event. */ import { type ManifestUploadPassStatus, type RunManifestUploadPassOptions, type RunManifestUploadPassResult } from "../manifest/upload-manifest.js"; import type { RunnerTarget } from "./sync-runner-planning.js"; /** * Wall-clock bound on the tree WALK for one scope. * * Separate from the seam's upload budget (a slow disk and a black-holed socket * fail for unrelated reasons). * * Was 30s, which measured as far too tight: on a real 690k-file personal vault * the walk expired after ~179k files, and (before the truncation guard landed) * uploaded the resulting empty manifest as a complete one. The stat walk * benches at well under 2s per 70k files, so ~690k files wants ~20s of pure * stat — but a cold page cache, a spinning disk or a busy laptop can multiply * that several times over, and the cost of over-budgeting is nil (the walk * yields to the event loop throughout and stops the moment it finishes) while * the cost of under-budgeting is a scope that can NEVER complete a pass. * 90s gives roughly 4x headroom over the measured target. */ export declare const RUNNER_MANIFEST_WALK_WALL_MS = 90000; /** * Bound on the ENTIRE post-sync manifest phase, across every scope. * * The per-scope budgets already bound each pass, but a wide fanout multiplies * them. This is the promise the runner makes to its supervisor: the tail step * is over within a bounded time no matter how many companies are in the plan. */ export declare const RUNNER_MANIFEST_PHASE_WALL_MS = 240000; /** * One scope's outcome, as the desktop app and the logs see it. * * Numbers and fixed tokens only — never a path, filename, or error message. */ export interface ManifestUploadEventPayload { /** Company slug, or `"personal"` for the personal vault leg. */ company: string; scope: "personal" | "company"; status: ManifestUploadPassStatus; mode?: "full" | "delta"; chunkCount?: number; uploadedChunks?: number; /** Failure CLASS token (never a message). Present only for `failed`. */ errorKind?: string; } export interface RunPostSyncManifestUploadsOptions { hqRoot: string; stateDir: string; /** The fanout plan, in fanout order. */ targets: readonly RunnerTarget[]; /** Slugs whose leg returned cleanly with `aborted: false` and no error. */ completedCompanySlugs: ReadonlySet; /** Vault API base URL (the runner's `DEFAULT_VAULT_API_URL`). */ apiBaseUrl: string; getAccessToken: () => Promise; /** Client name/version stamped on every request, like the vault client. */ clientInfo?: { name: string; version: string; }; /** One `manifest-upload` ndjson event per scope that actually ran a pass. */ emitEvent: (payload: ManifestUploadEventPayload) => void; reportDiagnostic?: (diagnostic: { event: string; message: string; err?: unknown; context?: Record; }) => void; /** Injected env — tests pin it; production reads `process.env`. */ env?: NodeJS.ProcessEnv; /** Injected clock (epoch ms), forwarded to the pass. Tests pin it. */ now?: number; /** Test seam: stands in for `runManifestUploadPass`. */ runPass?: (options: RunManifestUploadPassOptions) => Promise; /** Test seam: stands in for the global `fetch`. */ fetchImpl?: typeof fetch; /** Test seam: stands in for `getOrCreateMachineId`. */ resolveMachineId?: (hqRoot: string) => string; /** Test seam: stands in for `readDesktopInstallationId`. */ resolveInstallationId?: (options: { stateDir: string; }) => string | undefined; } /** * Run one manifest upload pass per cleanly-synced scope. * * Never throws, never rejects: the caller is a tail step whose only contract * is that the runner's exit code and event stream are unchanged by it. */ export declare function runPostSyncManifestUploads(options: RunPostSyncManifestUploadsOptions): Promise; //# sourceMappingURL=sync-runner-manifest.d.ts.map