import { TheokitAgentError } from '@theokit/sdk/errors'; /** * Bounding an injected registry remover, in one place. * * ## Why this file exists, written after it was needed * * `deleteSession` and `runTranscriptGC` both take a `removeFromRegistry` and both must await it — * the only agent registry in the ecosystem is `Agent.delete(id): Promise`. The plan named this * file (*"session/gc/registry-remover.ts (NEW) — the shared awaiting helper"*) and the first * implementation skipped it, putting the bound inside `deleteSession` and leaving the sweep with a * bare `await`. * * The divergence was not theoretical. A remover that never settled hung `runTranscriptGC` * indefinitely — not one session, every session after it, with no error, no timeout and no output. * The single-session path was already tested against exactly that; the sweep, which is the path that * runs unattended over a whole project, was not. That is `system-design-guardrails.md` § G12 in its * most expensive form: one rule, two call sites, and the one nobody was watching was the one that * mattered. */ /** * The registry did not answer in time. * * T2.2 — this used to mean "you passed a Promise to a synchronous seam". The seam now awaits, so the * only thing left that a caller cannot fix by awaiting is a registry that does not respond. */ declare class SessionRegistryRemoverError extends TheokitAgentError { readonly name = "SessionRegistryRemoverError"; readonly sessionId: string; constructor(sessionId: string, timeoutMs: number); } /** * M71 — the session LIFECYCLE vocabulary: list, delete, protect, fork. * * ## Why this module exists, and what it deliberately is not * * The store is fully supplied — 29 pass-throughs under `/persistence` cover paths, atomic writes, * locks and artifact classification. What had no home was the vocabulary ABOVE them: listing, * deleting with a live-session guard, forking, going back before a turn. * * The named risk was that absorbing lifecycle reads as owning the store (G2). The separation is * real and this module is where it is drawn: **owning the vocabulary is not owning the store.** * Nothing here writes a transcript. Every file operation is either a delete of something the caller * named, or a delegation to an SDK primitive. The knowledge added is which files are protected and * what "delete a session" means — decisions, not storage. * * ## The asymmetry that made the gap a trap * * `Agent.delete` clears the REGISTRY ENTRY and never touches the transcript on disk. A consumer had * to discover that by measuring. {@link deleteSession} is the answer: it returns * `{ registryOutcome, transcriptRemoved }` so the two are impossible to confuse, and a caller that * wanted both and got one can see it. */ /** Raised when a lifecycle operation is refused because the session is live. */ /** * `removeFromRegistry` returned a thenable — the delete was refused rather than half-performed. * * The seam is synchronous by contract because `deleteSession` is. A caller whose registry is async * (which, measured, is every real one) awaits its own removal and then calls this with the outcome. */ declare class SessionInUseError extends TheokitAgentError { readonly sessionId: string; /** Why it is protected — a writer lease, the resumable pointer, or being the most recent. */ readonly reason: string; /** * What the registry half reported before the refusal, or `undefined` when it never ran. * * Set only when the session became protected DURING the registry removal — the re-check fires * after the await, so that half has already been attempted while the transcript stays. The * caller needs it: retrying a removal that is already done reports "nothing to remove", which * reads as a failure and is not one. * * Carries the OUTCOME rather than a boolean since #675. `unconfirmed` must not be advertised as * "already removed" — that is the assertion the issue is about, and telling a caller not to * retry a half that may never have happened is the expensive direction to be wrong in. */ readonly registryOutcome?: RegistryOutcome | undefined; readonly name = "SessionInUseError"; constructor(sessionId: string, /** Why it is protected — a writer lease, the resumable pointer, or being the most recent. */ reason: string, /** * What the registry half reported before the refusal, or `undefined` when it never ran. * * Set only when the session became protected DURING the registry removal — the re-check fires * after the await, so that half has already been attempted while the transcript stays. The * caller needs it: retrying a removal that is already done reports "nothing to remove", which * reads as a failure and is not one. * * Carries the OUTCOME rather than a boolean since #675. `unconfirmed` must not be advertised as * "already removed" — that is the assertion the issue is about, and telling a caller not to * retry a half that may never have happened is the expensive direction to be wrong in. */ registryOutcome?: RegistryOutcome | undefined); } /** One session as the lifecycle vocabulary sees it. */ interface SessionSummary { /** * The session id read from the transcript's first record, or `undefined` when the record could * not be read. It is NEVER derived from the filename: on `@theokit/sdk` 5.x the name is a one-way * hash of the id, so a name-derived value would name no session at all (usetheokit/theokit#668). */ readonly id: string | undefined; /** Where {@link SessionSummary.id} came from — so a caller can tell absence from a value. */ readonly idSource: 'transcript' | 'unavailable'; /** Absolute path of the transcript file. */ readonly transcript: string; /** Last modification, for recency ordering. */ readonly modifiedAt: Date; } /** * Sessions with a transcript under `cwd`'s project directory, most recent first. * * Uses `classifySessionArtifact` rather than matching file names here: the SDK owns what a file in a * project directory IS, and a second matcher would answer differently the day the layout changes — * silently, on a path whose purpose is deleting things. */ declare function listSessions(cwd: string, root?: string): SessionSummary[]; /** * Transcripts that must NOT be collected: the resumable pointer's target, the most recent session, * and anything holding a writer lease. * * Three reasons, deliberately not collapsed into a boolean. A caller reporting "skipped 4 sessions" * is far less useful than one reporting why each was skipped, and retention policy is exactly where * an operator asks that question. */ /** * RENAMED from `protectedTranscripts` by usetheokit/theokit#668, and the rename IS the fix for the * second half of that issue. * * The keys changed from session ids to transcript paths, and the signature did not: * `Map` before and `Map` after. A consumer that mapped the keys * forward through `transcriptPath` — the correct thing to do when they were ids — went on compiling * and started double-mapping, so its protection array matched nothing and its delete guard passed. * Measured on TheoCode against this very build: `deleteSession` collected a session with a LIVE * writer lease, and no type could have caught it. * * A silent break that loses data is worse than a loud one. The old name is GONE rather than aliased, * so a caller finds out at compile time instead of at delete time. Aliasing it would have preserved * exactly the silence this rename exists to remove. */ declare function protectedTranscriptPaths(cwd: string, root?: string): Map; /** * The transcript a session id would occupy, for keying protection. * * Protection runs id → PATH and never the reverse, because that is the direction with a function: * `transcriptPath` is total on both majors, while 5.x's naming is a one-way hash. Keying on the id * meant a transcript nobody could read could not be matched against anything, so a session someone * had DECLARED protected became collectable (usetheokit/theokit#668). */ declare function transcriptOf(id: string, cwd: string, root?: string): string; /** What `deleteSession` did, per store. */ /** * What the injected registry remover actually told us — REPLACES the `registryRemoved` boolean * (usetheokit/theokit#675). * * There are three outcomes and a boolean could carry two, so the third was folded into `true`: * * - `removed` the remover reported that it removed an entry * - `nothing-to-remove` the remover reported that there was none — a report, and an ordinary one * - `unconfirmed` the remover ran, resolved, and said NOTHING * - `failed` it threw or timed out; see `registryError` * - `not-attempted` no remover was supplied, so the registry was never touched * * `not-attempted` and `unconfirmed` are deliberately not the same value. "Nobody asked" and "we * asked and got silence" lead a caller to opposite actions, and the old boolean said `false` to * both. * * `unconfirmed` is the one that matters, and it is the common case rather than an exotic one. * `Agent.delete` returns `Promise`: measured in `@theokit/sdk@4.52.1` and confirmed by the * SDK's changelog for `5.3.1`, it mutates an in-memory map and — unlike `Agent.list` — never * hydrates from disk, so in any freshly started process it resolves having removed nothing and * throws nothing. Reporting that as a removal was this package asserting something no version below * 5.3.1 gives it any way to know. * * `5.3.1` fixes the BEHAVIOUR and not the signature: `Agent.delete` still returns `Promise`, * so `unconfirmed` stays the honest answer on every version this package admits. Raising the floor * would make the removal actually happen; it would not make it reportable, which is why the two are * separate decisions. * * A caller that needs certainty verifies from its own side; this package deliberately does not own * the registry (see `gc/registry-remover.ts`). `Agent.list` hydrates from disk — which `delete` * never does, and that asymmetry IS the upstream defect — so it is the read that can answer. */ type RegistryOutcome = 'removed' | 'nothing-to-remove' | 'unconfirmed' | 'failed' | 'not-attempted'; interface DeleteSessionResult { /** * What the registry remover reported. RENAMED from `registryRemoved` by #675: the meaning * changed, and a boolean under the old name would have kept `if (result.registryRemoved)` * compiling while silently flipping which branch it took. */ readonly registryOutcome: RegistryOutcome; /** Whether the transcript file was removed. */ readonly transcriptRemoved: boolean; /** * Why the registry removal failed, when it did. * * Kept SEPARATE from `registryOutcome` on purpose: collapsing the outcomes into one value is * exactly how the original silent success hid. A caller that only reads the outcome sees * `failed` and can still surface the reason. */ readonly registryError?: unknown; } interface DeleteSessionOptions { readonly cwd: string; readonly root?: string; /** Delete even when protected. The refusal is the default because the damage is unrecoverable. */ readonly force?: boolean; /** * Remove the registry entry too. Injected (DIP) — the registry is the runtime's, not ours. * * T2.2 — MAY be async. `Agent.delete(id): Promise` is the only agent registry in this * ecosystem, so a sync-only seam could be satisfied honestly by nobody. Returning `false` means * "no entry to remove" and is not a failure; THROWING (or rejecting) is, and it stops the delete * with the transcript still on disk. */ readonly removeFromRegistry?: (sessionId: string) => unknown; /** * Ceiling on the injected remover. A registry that never answers must not hang a sweep; the * timeout surfaces as `registryError` and the transcript is left alone. */ readonly registryTimeoutMs?: number; } /** * Delete a session, refusing by default when it is protected. * * The two stores are reported separately because they genuinely are separate — `Agent.delete` * touches only the registry, and a caller that assumed otherwise would leave the transcript behind * believing it gone. Returning one boolean would rebuild that confusion inside this function. * * @throws {SessionInUseError} when the session is protected and `force` is not set. */ declare function deleteSession(sessionId: string, options: DeleteSessionOptions): Promise; /** * Fork `srcId` into `newId`, keeping everything BEFORE the `nth` user turn. * * Delegates the copy to the SDK's `forkTranscript` — the truncation rule is the SDK's knowledge and * re-deriving it here would give two answers to "where does a turn begin". * * `nth` is 1-based, matching how a person counts turns out loud. A 0-based index here would be a * silent off-by-one on a destructive-feeling operation. */ declare function forkBeforeUserTurn(srcId: string, newId: string, nth: number, options: { readonly cwd: string; readonly root?: string; }): { readonly transcript: string; readonly recordIndex: number; readonly selectedText: string; }; /** Where the pointer for `cwd` lives. */ declare function sessionPointerPath(cwd: string, root?: string): string; /** * The session id to resume for `cwd`, or a freshly minted one when there is nothing to resume. * * Never rejects. A missing, empty or unreadable pointer all mean the same thing to the caller — * "start a new session" — and distinguishing them would only give the caller three ways to write * the same branch. * * @param mintId how to create an id when there is nothing to resume. Injected (DIP) so the caller * owns id shape and the tests are deterministic — a `crypto.randomUUID()` baked in here would make * every assertion about the fresh path unwritable. */ declare function loadOrCreateSessionId(cwd: string, mintId: () => string, root?: string): { readonly sessionId: string; readonly resumed: boolean; }; /** * Point `cwd` at `sessionId`, atomically. * * Delegates to `atomicWriteText`, so a reader never observes a half-written pointer. * * The first draft reached for `atomicWriteTempTarget` instead, which was a misreading worth * recording: that function does not GENERATE a temp name, it RECOGNISES one and answers which final * file it belongs to. It is the collector's question ("is this scratch, and whose?"), not the * writer's. Its return type — `string | undefined` — is what said so, and the typechecker refused * the misuse before it could ship. * * Returns `{ persisted }` instead of throwing (see the module docblock). The failure is reported, * not hidden. * * ASYNC because `atomicWriteText` is. The first draft called it synchronously and returned * `{ persisted: true }` before the bytes landed — so the function reported a success that had not * happened yet, and a read immediately after found nothing. The tests caught it; a caller would * have caught it as an intermittently missing `--continue`. */ declare function persistSessionId(cwd: string, sessionId: string, root?: string): Promise<{ readonly persisted: boolean; }>; /** Absolute path of the project directory for `cwd`, under the transcript root. */ /** * Where every project's transcripts live under `root`. * * The `projects` segment was written in three places — twice in this file, and once in the closest * consumer, which restated it as `join(transcriptRoot(), 'projects')` to enumerate every project for * a GC sweep. Three owners of one layout fact. * * The failure mode is what makes it worth a function rather than a comment: the consumer guards its * enumeration with `existsSync(root) ? readdir(root) : []`, so a segment that no longer matches does * not throw — it returns an empty list. The sweep then finds nothing, deletes nothing, and reports * success. A wrong path that throws is a bug report; a wrong path that returns nothing is a * collector that quietly stopped collecting. */ declare function projectsRoot(root?: string): string; declare function projectDirFor(cwd: string, root?: string): string; /** * Record that `cwd` is the project behind its encoded directory. * * Idempotent and best-effort by design: this is an index, and a machine that cannot write it should * still be able to run an agent. A throw here would turn a missing optimisation into a failed run. */ declare function recordProjectDir(cwd: string, root?: string): void; /** * The cwd behind an encoded project directory name, or `undefined` when this layer never saw it. * * `undefined` is NOT "the project is gone". It is "no sidecar here", which a project created before * this index existed also produces. A caller that treated the two as the same would delete live * projects — which is precisely why the distinction is stated rather than left to the reader. */ declare function resolveProjectDir(encodedName: string, root?: string): string | undefined; /** * Whether `encodedName` is the encoding of `cwd`. * * The verification that makes the index trustworthy rather than merely convenient: a sidecar can be * stale (the directory was renamed) and re-encoding is the cheap way to notice. It is also the whole * of the `classifyProject` oracle the milestone offered as the alternative — kept as a companion to * the index rather than a replacement, because it can only confirm a cwd the caller already has. */ declare function projectDirMatches(encodedName: string, cwd: string): boolean; /** Raised when a retention knob is below its floor. Refusing beats clamping — see invariant 1. */ /** * The injected protection provider could not answer, so the run refuses to collect anything. * * Its OWN class and not a `GCFloorError`: that one means "a retention knob is below its floor", a * caller mistake fixed by passing a different number. This one means "we cannot know which sessions * are live" — different cause, different remedy, and collapsing them would make a diagnostic lie. * Not retryable: the provider is the caller's, and the same call reproduces the same failure. */ declare class GCProtectionUnavailableError extends TheokitAgentError { readonly reason: unknown; readonly name = "GCProtectionUnavailableError"; constructor(reason: unknown); } declare class GCFloorError extends TheokitAgentError { readonly field: 'keepLast' | 'maxAgeDays'; readonly received: number; readonly name = "GCFloorError"; constructor(field: 'keepLast' | 'maxAgeDays', received: number); } /** A transcript the policy would collect. */ interface GCCandidate { /** `undefined` when the transcript could not be read — see `SessionSummary.id` (#668). */ readonly id: string | undefined; readonly transcript: string; readonly modifiedAt: Date; } /** A transcript the policy keeps, and why. */ interface GCKept { /** `undefined` when the transcript could not be read — see `SessionSummary.id` (#668). */ readonly id: string | undefined; /** Human-readable protection reason — recency, policy, lease, or unreadable age. */ readonly reason: string; } interface TranscriptGCOptions { readonly cwd: string; /** Newest N sessions always survive. Floor: 1 — a project with none has nothing to `--continue`. */ readonly keepLast: number; /** Only sessions older than this may be collected. Floor: 1. */ readonly maxAgeDays: number; readonly root?: string; /** Injected clock (DIP) — a live `Date.now()` makes every age assertion depend on run speed. */ readonly now?: number; /** * Extra sessions this framework cannot see are live. * * `protectedTranscriptPaths` derives protection from THIS framework's pointer convention. A consumer * whose live-session registry lives elsewhere gets an INERT guard — silently, inside a guard that * deletes user transcripts. That is the shape of the consumer's own PS-002: declared, wired, never * called, and therefore reading as protection while protecting nothing. * * ADDITIVE ONLY. The returned ids are unioned with the built-in ones and can never remove them: a * seam that could unprotect a session the framework knows is live would be a deletion vector, not * a safety net. * * A provider that THROWS refuses the whole run — matching `GCFloorError`'s refuse-don't-clamp * posture, and deliberately not reproducing the consumer's fail-open bug where an EACCES on the * pointer read became "there is no live session". */ readonly protectedIds?: () => ReadonlyMap; } interface TranscriptGCPlan { readonly cwd: string; readonly root: string; readonly candidates: readonly GCCandidate[]; readonly kept: readonly GCKept[]; } declare function planTranscriptGC(options: TranscriptGCOptions): TranscriptGCPlan; /** What one candidate's removal did, or why it did not. */ interface GCError { /** `undefined` when the failure IS that no id could be read (#668). */ readonly id: string | undefined; readonly message: string; } interface RunTranscriptGCResult { readonly dryRun: boolean; /** Ids that were (or, in a dry run, would be) removed. */ readonly removed: readonly string[]; /** * Transcripts collected whose session id could NOT be read, by path. * * A separate list rather than a second meaning inside `removed`: that one answers "which sessions * did I collect", this one answers "which files did I collect that I could not name". Folding them * together would put two kinds of string in one array and make every consumer guess which it held * (usetheokit/theokit#668). */ readonly orphaned: readonly string[]; /** One entry per candidate that failed — the rest still ran. */ readonly errors: readonly GCError[]; } declare function runTranscriptGC(plan: TranscriptGCPlan, options: { readonly apply: boolean; /** Same additive, fail-closed contract as on the plan — see `TranscriptGCOptions.protectedIds`. */ readonly protectedIds?: () => ReadonlyMap; /** * T2.2 — remove the registry entry as well as the transcript file. * * Without it this function deleted transcripts and left the agent registry pointing at files * that no longer exist. Nothing repaired that: GC works FROM transcripts, so an entry whose * transcript is gone is never seen again. `deleteSession` has had this seam; the sweep did not. * * Same contract as `deleteSession`: may be async (`Agent.delete` is the only registry there is), * runs BEFORE the unlink, and a throw leaves the transcript on disk for the next sweep rather * than producing an entry no run can repair. */ readonly removeFromRegistry?: (sessionId: string) => unknown; /** * How long to wait for the registry per session before giving up on it and keeping the * transcript. Absent means wait indefinitely — the pre-existing behaviour, kept as the default * so adding the bound cannot change what a current caller experiences. */ readonly registryTimeoutMs?: number; }): Promise; /** * A sentence explaining an empty session list, or `undefined` when there is nothing true to say. * * Returns `undefined` in every case where the hint would be noise or a guess: sessions were found, * the root was not overridden, the override equals the previous root, the previous root cannot be * read, or it holds no projects. A hint that fires spuriously is worse than none — people stop * reading the ones that matter. * * @param found - how many sessions the caller's own listing returned. * @param previousRoot - the root to look in, typically the default before the override. * @param env - injected so a test needs no global mutation; defaults to the real environment. */ declare function transcriptRootHint(found: number, previousRoot: string, env?: NodeJS.ProcessEnv): string | undefined; /** * The state of one thread's stored history (usetheokit/theokit#399). * * ## Why three values and not two * * An application reading a transcript must catch: a brand-new thread has no file yet, and raising * there would 500 the first turn of every conversation. That catch is mandatory — and it swallows * every other read failure with it. A transcript that exists and cannot be parsed came back as the * same empty, successful, warm greeting as a thread that never had one. * * `unreadable` is that third case, taken back out. It is the same shape `liveness-oracle.ts` uses * one file over, for the same reason its docstring gives: a caller acts on the answer, and "I could * not tell" must not be spelled like "there is nothing". * * ## What `absent` does NOT mean * * It does not separate a LOST conversation from a NEW one, and nothing in this package can. The * thread id is minted client-side (`client/agent-client.ts`) and nothing records that it was ever * issued, so on disk a garbage id and an unused one are the same fact: no file. * * That distinction belongs to whoever knows where the id CAME FROM. An application that restored an * id from storage and finds `absent` is looking at a loss and can say so; the same `absent` for an * id the client just minted is a new conversation. The knowledge exists — it is simply not here. * * Making the server able to answer it would take a record of every id ever issued, with tombstones * that survive the transcript's deletion, which is unbounded growth plus a retention policy * `transcript-gc.ts` deliberately leaves to the application. That is a product decision, not a * missing function, and it is not taken here. */ type ThreadHistoryState = /** The transcript exists and parsed. */ 'present' /** No transcript. A new thread, or one whose id was never used — see above. */ | 'absent' /** A transcript exists and could not be read. NOT the same as having none. */ | 'unreadable'; interface ThreadHistory { readonly state: ThreadHistoryState; /** The rows, when `present`. Empty otherwise — never a partial read presented as a whole one. */ readonly messages: readonly Record[]; /** * Why, when `unreadable`. * * Carried because the alternative is an operator reading "unreadable" and going to the logs for * something this call already knows. */ readonly reason?: string; } interface ReadThreadHistoryOptions { /** The project directory the transcript is filed under. */ readonly cwd: string; /** Transcript root. Defaults to the SDK's, which honours `THEOKIT_HOME`. */ readonly root?: string; } /** Read one thread's stored history, distinguishing "none" from "could not read". */ declare function readThreadHistory(sessionId: string, options: ReadThreadHistoryOptions): ThreadHistory; /** * Does the project behind `projects//` still exist? * * The question is hard because of a decision this package's layout made: * `encodeProjectDir(cwd)` is `cwd.replace(/[^a-zA-Z0-9]/g, '-')`, a one-way street. `/a/b` and * `/a-b` produce the same name, so a directory name cannot be turned back into a path — it can only * be CHECKED against candidates. Every product that retains or garbage-collects transcripts has to * answer this, and until now each one wrote the search itself: the consumer's 188 lines, whose own * docstring measured 13.269 project directories, ~3.200 falling through to filesystem search, and * ~64M syscalls without a shared budget. * * ## What is injected, and why exactly that * * `candidatePaths` — which directories are even candidates is PRODUCT policy (workspaces, ignore * rules, mounted volumes). This module must not guess it. It returns REAL ABSOLUTE PATHS, and the * name says so because the previous one (`listProjects`) did not: the only consumer's function of * that name returns ENCODED DIRECTORY NAMES, and wiring one to the other classified 6 of 6 live * projects `dead`. * * `fs` — so the budget is countable and the caller can supply a stat that matches its own retry and * timeout posture. Every call is one operation. * * What is NOT injected is the encoding, because that is the thing this package owns and the whole * reason the question exists. * * ## Two properties do the work * * **The result is three-valued and `undetermined` is not a soft `dead`.** Callers DELETE on `dead`. * Every way of failing to find out — budget spent, unreadable directory, enumeration threw — * resolves to `undetermined`, because deleting on "could not tell" is data loss and the fail-safe * direction is not symmetric here (`rules/error-handling.md`). * * **The budget is shared across the whole sweep, not per project.** A bound that resets each * iteration is not a bound; that is precisely what produced the 64M figure. */ /** * The filesystem, as this module needs it. Every call is ONE operation against the budget, and any * of them may throw (EACCES) — a throw is a third outcome, never "absent". */ interface FsSeam { /** * Does `path` exist? **Three-valued**: `undefined` means "could not determine", and it is NOT the * same answer as `false`. * * The third state is in the RETURN TYPE rather than in prose because that is the only place an * adapter author reliably reads it. The consumer's scar (B-020) is exactly this: its adapter * mapped every `statSync` failure to `false`, and since the verdict branches on that value, a cwd * that exists but cannot be stat-ed — EACCES on a non-traversable parent, ENOTDIR mid-path, EMFILE * under a wide sweep — was classified DEAD, which is a deletion. A signature of `=> boolean` * invites `try { return existsSync(p) } catch { return false }`, which reintroduces it silently. * * ENOENT is the only errno that means absence. Everything else is `undefined`. Throwing is also * accepted and treated as `undefined`, so an adapter that does neither still cannot cause a * deletion. */ exists: (path: string) => boolean | undefined; /** Entry names directly under `dir`. Used to find a transcript to read the recorded cwd from. */ listEntries: (dir: string) => readonly string[]; /** The first line of `file`. The transcript's first record carries the `cwd` it was written in. */ firstLine: (file: string) => string; } type Liveness = 'alive' | 'dead' | 'undetermined'; interface LivenessVerdict { readonly liveness: Liveness; /** Why — carried on every verdict, so an operator reading a GC log is never left guessing. */ readonly reason: string; /** * The cwd this verdict is ABOUT — present on `alive` and `dead`, absent on `undetermined`. * * `alive` reports the member of the collision class that was found to EXIST, not the first one * read: the class can hold a gone path and a live one, and sending a caller's registry lookup to * the gone sibling would defeat the point. `dead` reports the recorded cwd that was checked and * found missing. `undetermined` established no path, so the field is absent rather than an empty * string a caller might mistake for one. * * Added because the verdict was dropping the one fact a caller acts on. This function PROBES the * path to decide `alive` — it has it in hand at the moment it returns — and kept only a prose * `reason`. The consumer this was absorbed from uses the resolved cwd to consult the agent * registry and the resumable pointer for that project (`all-sessions.ts:161,175`), so a verdict * without it could not replace the function it exists to replace. Recovering it by string-matching * `reason` would be the fragile coupling this module exists to remove: a sentence is not an API. */ readonly cwd?: string; } interface ClassifyProjectsOptions { /** * Where `projects//` lives, so the recorded-cwd read can find a transcript. * Use {@link projectsRoot} rather than joining the segment by hand — that segment had three * owners once, and a wrong one makes every project look empty rather than erroring. */ projectsRoot: string; /** * REAL ABSOLUTE PATHS that might be the project — not encoded directory names. * * The name is explicit because the previous one was not, and the ambiguity was a defect rather * than a documentation gap: the only consumer's `listProjects` returns ENCODED NAMES (it keeps * classification in a separate injected seam), so wiring the two together fed encoded names to a * function expecting paths. Measured 2026-08-16: 6 of 6 live projects classified `dead`, on the * path where the caller DELETES. * * PRODUCT policy: which directories are even candidates (workspaces, ignore rules, mounted * volumes) is not this module's to guess. It is also only a HEURISTIC — see the fall-through in * `searchPool`, which is why exhausting it can never prove absence. */ candidatePaths: () => readonly string[]; /** Total filesystem operations allowed for the ENTIRE sweep. */ budget: number; fs: FsSeam; /** How many transcripts to read per project before giving up on the recorded cwd. Default 3. */ transcriptSamples?: number; } /** * REMOVED 2026-08-16 — `likelyPath`, which turned every `-` back into `/` and was documented as * "correct for the overwhelming majority of real paths". It is correct for no path containing a * hyphen, which is most of them: * * encode('/home/op/Projetos/theo/theokit-framework') * → '-home-op-Projetos-theo-theokit-framework' * likelyPath(that) * → '/home/op/Projetos/theo/theokit/framework' ← not the input * * The encoding is lossy on purpose (`/a/b` and `/a-b` collide), so no string transform can invert * it. What replaces it is not a better guess but the actual answer: the transcript records the cwd * it was written in, and reading it costs one line of one file. */ /** * Raised when `budget` cannot bound anything. Refusing beats clamping, for the same reason * `transcript-gc.ts` states as its invariant 1: an operator who asked for a policy must not be * silently given a different one. */ declare class LivenessBudgetError extends TheokitAgentError { constructor(budget: number); } /** * Classify each encoded project directory. Every input appears in the output: a missing key would * read to a caller as "not dead", which is safe only by accident. */ declare function classifyProjects(encoded: readonly string[], opts: ClassifyProjectsOptions): Map; export { type ClassifyProjectsOptions, type DeleteSessionOptions, type DeleteSessionResult, type FsSeam, type GCCandidate, type GCError, GCFloorError, type GCKept, GCProtectionUnavailableError, type Liveness, LivenessBudgetError, type LivenessVerdict, type ReadThreadHistoryOptions, type RegistryOutcome, type RunTranscriptGCResult, SessionInUseError, SessionRegistryRemoverError, type SessionSummary, type ThreadHistory, type ThreadHistoryState, type TranscriptGCOptions, type TranscriptGCPlan, classifyProjects, deleteSession, forkBeforeUserTurn, listSessions, loadOrCreateSessionId, persistSessionId, planTranscriptGC, projectDirFor, projectDirMatches, projectsRoot, protectedTranscriptPaths, readThreadHistory, recordProjectDir, resolveProjectDir, runTranscriptGC, sessionPointerPath, transcriptOf, transcriptRootHint };