@sema-agent/core
Version:
Stateless, task-oriented AI agent core
92 lines (91 loc) • 7.08 kB
TypeScript
/**
* 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>;