import { BaseSessionStorage, type PromptEpochDescriptor, type SessionMetadata, type SessionTreeEntry, type SessionWriteOptions } from "@sema-agent/core"; import type { Pool } from "mysql2/promise"; export declare const SESSION_EVENT_LATEST_BY_TYPE_SQL: { readonly floor: "SELECT /*+ USE_INDEX(session_event, idx_session_event_type) */ payload FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1"; readonly epochAnchor: "SELECT /*+ USE_INDEX(session_event, idx_session_event_type) */ seq, payload FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100"; }; /** * TiDB-backed {@link BaseSessionStorage} — the durable L1 event log for one session. * * - `appendEntry` / `setLeafId` persist under an **atomic compare-and-set on `leaf_id`** (core's F2 * optimistic-lock seam): the conditional UPDATE's only predicate is `leaf_id <=> ?`, so a write lands * only if the branch leaf is still what the entry was built against. Two stateless runners that both * woke the same session can't silently fork — the loser gets `SessionError("conflict")` (catch with * `isSessionConflict`), with the `(session_id, seq)` primary key as the backstop for a racing writer * that grabbed the same seq. ([ref] 件2 更正:`leaf_seq` **不是**判据 —— 它是无条件的 `SET leaf_seq = * leaf_seq + 1` 计数器,在同一事务内重读出来给事件铸 `seq`,从不进 WHERE。) * - The in-memory base tree is the wake-time snapshot; every write is also mirrored into it so the * live harness keeps reading a consistent view. Wake is **bounded** to `[floor .. leaf]` (F3, * core 1.6.0 branch-floor seam); older history is covered by the last compaction's summary. */ export declare class TiDBSessionStorage extends BaseSessionStorage { private readonly pool; private readonly sessionId; private readonly floorSeq; constructor(pool: Pool, sessionId: string, metadata: SessionMetadata, entries: SessionTreeEntry[], leafId: string | null, floorEntryId?: string | null, floorSeq?: number); /** * [ref] S3 [ref] pull seam — `SessionStorage.getEpochAnchor?()`: the nearest epoch carrier BELOW the * bounded-wake floor. core's `Session.getPromptEpoch()` (≥1.305) calls this only when the in-window * branch walk finds no carrier — i.e. a rewind/resumeAt cut inside the kept tail, where the compaction * restatement is a DESCENDANT of the cut and the root pin sits under the floor. Semantics per core's * ruling: highest-seq valid carrier below the floor (not an ancestor-chain walk — fallback of last * resort); malformed rows are skipped (#13: fall back to the older valid pin — the write gate refuses * malformed carriers, so these only exist via out-of-band writes); none ⇒ undefined = a genuine * pre-epoch session (core mints legacy_migration, the correct meaning). core re-normalizes whatever we * return, so this stays honest even if the two sides ever disagree on strictness. */ getEpochAnchor(): Promise; /** * Rebuild a session from its event log (= wake). Returns null if the session does not exist. * * F3 bounded load: only `[floor .. leaf]` is read, where floor = the last compaction's * `firstKeptEntryId` (everything older is represented by that compaction's summary, so it never * enters context). No compaction yet → floor=null → full load (the session is still short). * * This is the durable, bounded-READ equivalent of core's `boundedTail(pathEntries)` (1.42.0): that * helper trims a FULL-loaded path in memory, whereas we compute the floor seq first and only `SELECT * seq >= floorSeq` — so the head never leaves cold storage. Our sessions are LINEAR (no fork), so * seq-order == root→leaf path-order and our `seq >= floorSeq` tail is byte-identical to * `boundedTail(path).tail`; the resulting floored `BaseSessionStorage.buildContext()` matches a * full-tree wake (core's `session-floor.test.ts`). If branching is ever added, switch to `boundedTail`. */ static wake(pool: Pool, sessionId: string): Promise; /** The bounded-window floor = last compaction's firstKeptEntryId (and its seq). */ private static computeFloor; /** Sub-floor lookups (e.g. a label target) aren't in the loaded tail — fall back to the durable log. */ getEntry(id: string): Promise; appendEntry(entry: SessionTreeEntry, opts?: SessionWriteOptions): Promise; setLeafId(leafId: string | null, opts?: SessionWriteOptions): Promise; /** * Atomically: CAS the meta leaf (+bump leaf_seq), then append the event at the new seq. * The `(session_id, seq)` primary key is the backstop — a racing writer that grabbed the same * seq makes our INSERT fail with a duplicate-key, which we translate to a conflict too. */ private persist; } //# sourceMappingURL=tidb-session-storage.d.ts.map