import type { SessionKey, SessionStoreEntry } from "@yanlinglabs/winter-agent-sdk"; import { RuntimeSdkError } from "../errors.js"; import type { SharedSessionStore } from "./wiring.js"; /** One transcript found under a local-write root, with the store key it mirrors to. */ export interface LocalTranscript { path: string; key: SessionKey; } /** * Every transcript under a recorded local-write root. * * NOT A GLOB: exactly these shapes are reconciled, each under the store key claude's own import gives it * (`importSessionToStore`; the resume materialization writes every subkey back to the same path), and * everything else in that tree — the vendor's settings, its caches, its lock files — is out of scope by * construction rather than by a filter someone has to maintain: * * `/.jsonl` — the session's own transcript; * * `//subagents//agent-.jsonl` — a subagent's (nested for a workflow's agents); * * `//subagents//journal.jsonl`, `` non-empty — a workflow run's journal (review * N-1; claude's `journal` key, subpath `subagents//journal`); * * `//world.jsonl` — the session's own journal (claude's `sessionJournal` key, name `world`). * The two journals are plain appends of uuid-less lines, so they are compared byte for byte (`sameRecord`). * A missing root is an empty list, not an error: a session that never spawned has nothing to mirror. */ export declare function scanLocalWriteRoot(root: string): LocalTranscript[]; /** WS-05 §12 step 4's four possible answers. */ export type TailComparison = { kind: "match"; lines: number; } | { kind: "canonical-behind"; lines: number; missing: SessionStoreEntry[]; } /** The canonical file disagrees with the local root at a line both of them have. */ | { kind: "diverged"; atLine: number; reason: string; } /** The canonical file has entries the local root does not — they are not the same history. */ | { kind: "canonical-ahead"; extra: number; }; /** * WS-05 §12 step 4: "compare canonical tail vs the recorded local-write root". * * The canonical file MUST be a prefix of the local one. It is a mirror: the local write happens first * (WS-14 §5), so the canonical side can only ever be equal or behind. Anything else is a statement * that the two files are not the same history, and this function says so rather than repairing it. */ export declare function compareTranscriptTail(args: { localPath: string; canonicalLines: readonly string[]; isDecoration?: (uuid: string) => boolean; }): TailComparison; /** * I6: is the local copy a record-for-record PREFIX of the canonical lines? The one question * `canonical-ahead` leaves open (`compareTranscriptTail` answers it before comparing a single line): * a local copy that is a prefix holds nothing the canonical file lacks — the canonical history simply * moved on after it (a resume through staging, a continuation on the other leg) — while one that is * not holds a line no append can place. */ export declare function localIsCanonicalPrefix(args: { localPath: string; canonicalLines: readonly string[]; isDecoration?: (uuid: string) => boolean; }): boolean; export interface TranscriptReconcileOutcome { key: SessionKey; localPath: string; comparison: TailComparison; appended: number; /** Set when a `judge` decided this transcript's fate instead of the comparison (I6). */ verdict?: "level" | "excluded"; /** Why a judged transcript was excluded. */ reason?: string; /** Set when a tail WAS appended and the re-read still disagreed: how many entries the append carried. */ attempted?: number; } export interface ReconcileReport { root: string; /** `reconciled` — every transcript now matches. `diverged` — at least one cannot be, safely. */ status: "reconciled" | "nothing-to-do" | "diverged"; transcripts: TranscriptReconcileOutcome[]; appended: number; /** The sessions whose health was cleared, i.e. those that were behind and are now level. */ cleared: SessionKey[]; } /** A reconciliation that cannot proceed because the canonical store's own state forbids it. */ export declare class TranscriptReconcileError extends RuntimeSdkError { readonly root: string; constructor(root: string, reason: string); } /** * I6: a per-transcript verdict, asked BEFORE anything is appended. `reconcile` — the ordinary path * (append a behind tail; a match is level); `level` — nothing to append and nothing lost (a proven * canonical-ahead prefix); `{ exclude }` — unprovable: never appended, never level, the session's flag * stays. A judge that throws excludes that transcript. */ export type TranscriptJudge = (transcript: LocalTranscript, comparison: TailComparison) => Promise<"reconcile" | "level" | { exclude: string; }>; export interface TranscriptReconcilerInput { shared: SharedSessionStore; /** Restricts reconciliation to one session; omitted = every transcript under the root. */ only?: { projectKey: string; sessionId: string; }; /** Recovery's per-transcript proof (WS-21 §3.8, I6). Absent = every transcript takes the ordinary path. */ judge?: TranscriptJudge; } /** * Reconciles the canonical store against one local-write root. * * THE ORDER IS DELIBERATE: settle first (so the comparison is not racing the mirror's own pending * batch), then compare, then append only the suffix, then settle again and re-read to confirm. A * reconciliation that reported success without re-reading would be reporting its own intention. */ export declare function reconcileLocalWriteRoot(root: string, input: TranscriptReconcilerInput): Promise; /** * The shape of WS-14 §6 rule 3's reconcile hook, as `src/official/spawn-proxy.ts` declares it. * * DECLARED STRUCTURALLY HERE rather than imported, for the same reason the seams declare the official * module structurally: this lane must not depend on that lane's module graph to be buildable. * * ASSIGNABLE IN ONE DIRECTION, AND THAT IS THE DIRECTION PRODUCTION USES (review r4, N14 — the * previous sentence claimed "mutually assignable", which is measurably false). This hook accepts LESS * than `SpawnObservation` carries, so Lane C's `createTranscriptReconciler().hook` drops straight into * `createSupervisedSpawnProxy({ reconcile })`; the reverse fails, because a `TranscriptReconcile` is * handed the whole observation (`processIdentity`, `command`, `args`) and this type does not promise * to supply it. Nothing needs the reverse. `test/store/reconcile.test.ts` now actually pins the * forward direction — the comment promised a test that did not exist, which is how a structural * declaration drifts from the thing it mirrors without anything noticing. */ export type TranscriptReconcileHook = (input: { observation: { root: { configDir: string; }; }; exit: { code: number | null; signal: string | null; }; }) => Promise; export interface TranscriptReconciler { /** Reconcile one recorded local-write root. */ reconcile(root: string, only?: { projectKey: string; sessionId: string; }): Promise; /** The collaborator handed to `createSupervisedSpawnProxy({ reconcile })`. */ hook: TranscriptReconcileHook; /** Every report this reconciler produced, newest last — the supervised-run evidence. */ readonly reports: readonly ReconcileReport[]; } /** * Builds the reconciler and its proxy hook. * * THE HOOK NEVER THROWS. WS-14 §6 rule 3 delays the synthetic `exit` until reconciliation completes; * a hook that threw would either strand the process or (as Lane A's proxy actually does) be caught and * the exit forwarded anyway — and in both readings the FAILURE would be the thing that disappears. So * a failed reconciliation is recorded as a `diverged` report, which leaves the session's * `repair-required` flag standing and the handoff blocked. That is the outcome §5 asks for. */ export declare function createTranscriptReconciler(input: TranscriptReconcilerInput): TranscriptReconciler; /** * WS-14 §5's forbidden door, guarded. * * A HOST STILL HAS `importSessionToStore()` — it is the official SDK's own function, and the router * cannot take it away. What the router CAN do is give the host one call site that refuses to run it * while the mirror is unhealthy, so the ban is a mechanism rather than a sentence in a document. */ export declare function guardedImportSessionToStore(args: { shared: SharedSessionStore; key: SessionKey; importer: () => Promise; }): Promise; /** * The canonical transcript's own path under a Winter home. * * THE SAME SHAPE AS A LOCAL-WRITE ROOT'S, and that is not a coincidence: WS-05 §6's key mapping is the * vendor's own `/projects//.jsonl` layout, which is what makes the * canonical file and the local file comparable line for line in the first place. */ export declare function canonicalTranscriptPath(winterHome: string, key: SessionKey): string; /** The transcript file a local-write root holds for one session key, whether or not it exists. */ export declare function localTranscriptPath(root: string, key: SessionKey): string; /** * Is this key one of claude's JOURNALS (a workflow run's `subagents//journal`, or the session's own * `world`) rather than a transcript? A journal is written verbatim — never through the claude-ready fold * — so it is proved against the canonical lines themselves, byte for byte. */ export declare function isJournalKey(key: SessionKey): boolean; export declare function isTranscriptPath(path: string, kind?: "session" | "subagent"): boolean; /** Whether a path exists and is a regular file — used by the probes' byte-for-byte assertions. */ export declare function isFile(path: string): boolean;