import { type SnapshotStore } from "./hashline-engine/index.ts"; /** * Stable token carried inside every conflict message. * * It lives in the message text, not only on the class, because nothing typed survives the * trip from a tool to a workflow: a subagent result reports `cause`/`error` as strings, a * workflow task result has no error field at all, and a workflow runner reduces a failure to * `err.message`. Matching this token is how a consumer downstream of that flattening tells a * mutation conflict apart from an ordinary tool error. * * It is placed at the start of the message so it survives truncation of the tail as the * message is relayed. Never change it without updating every consumer that matches on it. */ export declare const FILE_MUTATION_CONFLICT_CODE = "FILE_MUTATION_CONFLICT"; /** * Reasons report a {@link FileMutationLiveState} wherever the live target can be described, * which is everywhere except `target_unreadable`: that one exists precisely because the target * could not be read, so claiming a size or a tag for it would be invention. * * Only the reasons that have something to compare against also report a * {@link FileMutationConflictEvidence} diff: `no_prior_observation` never read the file, and * `foreign_snapshot` presented a tag naming content this session has never seen, so in both * cases there is no prior side to diff. */ export type FileMutationConflictReason = /** Live content moved between the mutation being prepared and the write being attempted. */ "changed_before_write" /** Overwrite attempted with no prior read of this file in this session. */ | "no_prior_observation" /** Overwrite attempted, but what this session observed is no longer the live content. */ | "changed_since_observation" /** Exclusive create lost: the target already exists. */ | "target_exists" /** The target no longer exists: removed between preparing the mutation and writing it. */ | "target_missing" /** * The target still exists but can no longer be read: replaced by a directory, made * unreadable, locked, or rewritten as content this reader cannot parse. Distinct from a * content change because there is nothing to diff, and from a deletion because the path is * still occupied. */ | "target_unreadable" /** The presented tag was never minted in this session's snapshot store. */ | "foreign_snapshot"; /** * Correlation only. * * Flora, 2026-08-13: identity "must never grant permission or identify a current owner or * culprit". So this describes the writer being *rejected*, never a winner, and nothing in * this module reads it to decide whether a mutation is admitted. A record of the last Atomic * writer could not prove ownership anyway, because shell commands, external programs, other * Atomic processes and symlink changes all bypass this coordinator. */ export interface MutationRequester { readonly sessionId: string; /** Session that spawned this one, from the child's `supervisor.supervisorSessionId`. */ readonly parentSessionId?: string; readonly workflowRunId?: string; readonly workflowStageId?: string; readonly workflowStageName?: string; readonly subagentRunId?: string; readonly subagentAgent?: string; /** * Position within a fan-out. Needed to tell siblings apart: a child's intercom address is * `agent + runId + index` (`resolveSubagentIntercomTarget`), so a stage that spawns several * children of one agent onto one file yields identical identity without it. */ readonly subagentIndex?: number; /** Opaque per-attempt UUID. Diagnostic only; never an authority. */ readonly attemptId?: string; /** The specific tool call rejected. Narrower than the attempt, which makes many calls. */ readonly toolCallId?: string; } /** * Resolved per execution rather than captured once. * * Mirrors `sessionTempDir` in the session tool registry, which is a thunk "so bash spill * files follow the live transcript session across fork/branch/resume". A requester captured * at tool-construction time would keep a stale session id through exactly those transitions, * which is the relaunch case this feature exists to make legible. * * Takes the tool call id rather than leaving callers to merge it in, because it has a * different lifetime from the rest: session identity belongs to the session, the call id is * an argument of the single `execute` being rejected. */ export type MutationRequesterResolver = (toolCallId: string) => MutationRequester | undefined; /** * What actually diverged, so a conflict can be acted on without a second round trip. * * A reason and a path are not evidence; they say a race happened, not what it did. Whatever acts * on the rejection, a workflow applying its own control behavior or a human reading the error, * needs the divergence itself before it can decide anything. * * The span is computed by trimming the common prefix and suffix rather than comparing line by * line positionally, so inserting one line at the top reports one changed region and not every * following line. */ export interface FileMutationConflictEvidence { /** 1-based line of the first divergence. */ readonly line: number; /** Lines the rejected mutation assumed occupied the divergent span. */ readonly assumedLines: number; /** Lines actually occupying it now. */ readonly foundLines: number; /** First assumed line, clamped. Absent when the span is pure insertion. */ readonly assumed?: string; /** First live line, clamped. Absent when the span is pure deletion. */ readonly found?: string; } /** * The target as it stood at the moment of refusal, independent of any comparison. * * Kept separate from {@link FileMutationConflictEvidence} because the two answer different * questions and only one of them can be unanswerable. A conflict may have nothing to diff * against, as when the session never read the file, but there is still always something worth * reporting about what the mutation was aimed at. Size in particular is the difference between * clobbering an empty placeholder and clobbering four hundred lines of production code. */ export interface FileMutationLiveState { /** Lines currently in the file. Zero when it is empty or does not exist. */ readonly lines: number; /** Tag of the live content, so a rejected tag can be told apart from the real one. */ readonly tag?: string; /** First line, clamped. Absent when the file is missing, empty, or starts with a blank line. */ readonly firstLine?: string; } /** Describe live content, or a missing target when `live` is `undefined`. */ export declare function computeLiveState(live: string | undefined): FileMutationLiveState; /** * Compare what a mutation assumed against what is live, returning `undefined` when they agree. * * Both sides must already share a line-ending convention. `Snapshot.text` is normalized to LF, * so a caller passing raw live bytes on Windows would report every line as divergent. */ export declare function computeConflictEvidence(assumed: string, found: string): FileMutationConflictEvidence | undefined; export interface FileMutationConflictDetails { readonly reason: FileMutationConflictReason; /** Display path, as the model referred to it. */ readonly path: string; /** Symlink-resolved key, from `canonicalMutationKey`. */ readonly canonicalKey: string; readonly requester?: MutationRequester; /** The tag the rejected mutation presented, when it had one. */ readonly presentedTag?: string; /** Absent when the reason carries no comparison, as with `no_prior_observation`. */ readonly evidence?: FileMutationConflictEvidence; /** What the mutation was aimed at. Absent only when it could not be read at all. */ readonly liveState?: FileMutationLiveState; /** * Filesystem error code that classified the conflict, such as `EISDIR` or `EACCES`. For a * target that became unreadable this is the whole diagnosis, since nothing else about the * file can be reported once the read fails. */ readonly causeCode?: string; } /** * Raised instead of a bare `Error` so a conflict is countable rather than merely readable. * The message is the wire format; see {@link FILE_MUTATION_CONFLICT_CODE}. */ export declare class FileMutationConflict extends Error { readonly reason: FileMutationConflictReason; readonly path: string; readonly canonicalKey: string; readonly requester: MutationRequester | undefined; readonly presentedTag: string | undefined; /** Structured alongside the message so a ledger can record it without re-parsing text. */ readonly evidence: FileMutationConflictEvidence | undefined; readonly liveState: FileMutationLiveState | undefined; readonly causeCode: string | undefined; constructor(details: FileMutationConflictDetails); /** * One labeled line per concern, ordered by what must survive truncation: the code, reason, * and path first, then why and what to do, then the divergence and live state, and the * requester identity last. Identity is correlation for logs, never guidance, so it is the * most expendable part of the message for the model reading it. */ static formatMessage(details: FileMutationConflictDetails): string; } /** Whether a thrown value is an exclusive-create collision reported by the filesystem. */ export declare function isExclusiveCreateCollision(error: unknown): boolean; /** * Whether a thrown value says the target is gone. `ENOTDIR` counts because a parent directory * replaced by a file makes the path unreachable for the same reason a deletion does. */ export declare function isMissingTargetError(error: unknown): boolean; /** The `code` a filesystem rejection carries, when it carries one. */ export declare function filesystemErrorCode(error: unknown): string | undefined; /** * Guard for the window between preparing a mutation and committing it, called under the * mutation queue. * * `prepared` is the content the mutation was computed against, which for a hashline edit is * the file as `prepare` read it, NOT what the session originally observed, since `prepare` * re-reads and may have recovered onto drifted content. Naming that honestly matters: this * closes a time-of-check window, it does not prove the session read the file. * * Full text rather than tags: a hashline tag is 16 bits and the engine documents that it can * collide, so it is a label for the model to recognize, never proof of identity. */ export declare function assertLiveMatchesPrepared(args: { readonly canonicalKey: string; readonly path: string; readonly prepared: string; readonly live: string; readonly requester?: MutationRequester; readonly presentedTag?: string; }): void; /** * Guard for `write` overwriting an existing file: this session must already have observed the * exact content it is about to replace. * * Asks the session's own store and never another session's, which is what keeps cross-session * rejection strict. `byHashAndText` answers both halves of the requirement at once, since a * hit means this session recorded exactly this content for this path. * * `storeKey` is the snapshot store's own key convention, the normalized absolute path, which * is NOT symlink-resolved unlike `canonicalKey`. Passing the wrong one makes a symlinked path * miss and report a false `no_prior_observation`. * * The tag is derived here rather than accepted. Unlike the engine's own use of * `byHashAndText`, where the hash is the independent claim a section presented, `write` has no * presented tag: it replaces the whole file. Any tag is therefore just `live` hashed, and * taking it as a parameter only creates a way for the two to disagree silently. */ export declare function assertPriorSessionObservation(args: { readonly canonicalKey: string; readonly storeKey: string; readonly path: string; readonly store: SnapshotStore; readonly live: string; readonly requester?: MutationRequester; }): void; /** * Build a requester from parts already present on a session. Kept pure so it is testable * without constructing an `AgentSession`, and so this module never imports one. */ export declare function buildMutationRequester(parts: { readonly sessionId: string; readonly orchestration?: { readonly kind: string; readonly workflowRunId: string; readonly workflowStageId: string; readonly workflowStageName: string; }; readonly intercom?: { readonly runId: string; readonly agent: string; readonly index?: number; readonly supervisor?: { readonly supervisorSessionId: string; }; }; readonly attemptId?: string; readonly toolCallId?: string; }): MutationRequester; //# sourceMappingURL=file-mutation-coordinator.d.ts.map