UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

92 lines (91 loc) • 7.08 kB
/** * The Runner's session-scoped read-file-state CACHE and the licence to inherit from it: session-scoped cache * machinery, not a phase step — the Runner owns the instance for its lifetime, the run internals carry it to the * hands phase, and the hands phase adopts, inherits and seals seats through it. */ import type { ReadFileState } from "../../tools/fs/safety.js"; /** * The Runner's per-session read-file-state seats (CC parity: readFileState is minted * once per REPL session and mutated across turns; it has NO turn-boundary and NO abort clear site — * its only clears are compaction/fork/subagent-cleanup/dispose). sema's map stays per-task (the §11 * ruling-A closure wiring), so the SESSION scope lives here instead: prepare adopts each task's live * map under its sessionId, and the next same-session, same-Runner task INHERITS it (through the same * containment judgment the transcript-replay seed applies — see `inheritSessionReadFileState`). * * This seat is a CACHE, never an authority: * - a durable resume's checkpoint seed stays the sole authority on that leg (the seed was cut * atomically with the leaf; this seat may be older OR newer than it); * - a cross-process continuation misses here and falls back to the transcript replay (rung-2); * - eviction (the LRU cap, a Runner restart) degrades to that same replay — never to wrong state. * * Bounded: an LRU over sessionIds (insertion-order refresh on read), because the SessionStore's own * eviction is not observable from here — a dropped seat costs one transcript replay, nothing else. * The stored value is the task's LIVE map (object identity), so in-turn mutations — tool write-backs, * and above all the compaction clear — are the seat's truth by construction; a snapshot copy * here would resurrect summarized-away reads on the next turn. * * BRANCH BINDING: a sessionId alone does not name a transcript — the same id * can be rebranched (`resumeAt`), rewound out-of-band, or DELETED and re-created (`release()` + the * default create-on-miss acquire), and the seat's map would then vouch for reads the CURRENT branch * never saw. Each seat therefore records `anchorLeafId` — a leaf on the lineage its task produced — * and inheritance must first prove the CURRENT branch still descends from that anchor * (see {@link sessionSeatAnchorHolds}). The anchor is written twice: at ADOPT (the leaf at prepare; * `null` when the branch was still empty) and SEALED at the task's settle to the task's own tail * (`reanchor`). A `null` anchor is NEVER inheritable ("the branch is non-empty now" proves * nothing about WHOSE messages those are — another Runner repopulating a released id, or a session * import, makes a non-empty branch that never contained this seat's reads). An unprovable anchor * falls back to the transcript replay, which reads the current branch by construction — cost, never * wrongness. */ export declare class SessionReadFileStates { private readonly seats; private static readonly CAP; /** The session's seat row, or undefined (cross-process / evicted / first turn) ⇒ caller replays. * The caller must verify the anchor against the session's CURRENT branch before using `state`. */ get(sessionId: string): { state: ReadFileState; anchorLeafId: string | null; } | undefined; /** Adopt THIS task's live map as the session's seat (every hands leg: fresh, continuation, resume). * `anchorLeafId` = the session's leaf as read at THIS prepare (null = the branch was empty; a * null-anchored seat is not inheritable until the settle-time {@link reanchor} names a real leaf). */ adopt(sessionId: string, state: ReadFileState, anchorLeafId: string | null): void; /** The settle-time SEAL: re-anchor the seat to the finished task's own tail, so a * first-task (null-anchor) seat becomes inheritable only through a leaf its task provably wrote. * Identity-guarded: touches the row only while it still holds THIS task's map (a later prepare on * the same session has already replaced the seat and owns its own anchor). */ reanchor(sessionId: string, state: ReadFileState, anchorLeafId: string): void; } /** * Does the session's CURRENT branch still descend from the seat's anchor? * TRUE is the licence to inherit; anything unprovable answers FALSE (the caller then replays the * transcript — the fail-closed direction costs a rebuild, never admits a wrong entry). * * - anchor `null`: NEVER inheritable (r2). The seat was adopted on an empty branch and its task * never sealed (crashed pre-settle, or the leaf read failed) — "the branch is non-empty now" * would also be true of a released-and-repopulated id or an imported log, whose messages are not * this seat's lineage. The settle-time reanchor is what upgrades such a seat to a real leaf. * - anchor non-null: holds iff walking parentIds from the current leaf reaches the anchor within * a bounded number of steps (the walk length is one task's appends in the ordinary continuation). * A rebranch/rewind moved the leaf OFF the anchor's subtree ⇒ the walk misses ⇒ no inherit; a * recreated/foreign log does not contain the anchor ⇒ same answer; a re-IMPORT of the same log * keeps the anchor's lineage ⇒ inheritance stays correct there by the same test. * - a COMPACTION on the walked segment (A-082 idx2): refuses, even though the branch is perfectly * intact. The walked segment is exactly what landed AFTER this seat was anchored, and a * compaction there summarized older Read results OUT of the context the model will be shown — * the very state the compaction clear removes from the live map at the landing site. That clear reaches the seat * only when the SAME Runner did both (the seat IS that map, by object identity); a second Runner * on the shared durable session clears its own map and commits the compaction as an ORDINARY * LINEAR APPEND, which every other test here passes. Inheriting across it would let requireRead * and checkStale both pass on a read whose card the model no longer has — a blind edit from a * summarized-away recollection, the exact failure the compaction clear exists to prevent. Refusing * costs one transcript replay, and the replay reads the POST-compaction branch by construction, * so the fail-closed direction is also the CORRECT state. A compaction at or ABOVE the anchor is * not on the walked segment and does not refuse: the seat that observed it already carries its * clear (P-483-4), and an ordinary foreign turn with no compaction still inherits (G-483-5) — * dropping every seat a foreign Runner touched would restore the cross-turn read-state amnesia this seat-anchor check already ended. */ export declare function sessionSeatAnchorHolds(getEntry: (id: string) => Promise<{ parentId: string | null; type?: string; } | null | undefined>, anchorLeafId: string | null, currentLeafId: string | null): Promise<boolean>;