import type { Pool as MySqlPool } from "mysql2/promise"; import type { Pool as PgPool } from "pg"; import { type FileHistoryRetentionPolicy, type ExecutionEnv, type FileHistoryStore, type FileHistoryResult, type FileHistoryTrackResult, type FileHistoryRestoreResult, type FileHistoryExport } from "@sema-agent/core"; import { type BlobBackend } from "./blob-backend.js"; import { type SqlDriver } from "./sql-driver.js"; /** The file-history blob domain's SQL table (BlobBackend `table`/MinIO index binding + the A7 assert). */ export declare const FILE_HISTORY_BLOB_TABLE = "file_history_blob"; /** MinIO object-key prefix for this domain (snapshot epoch used "blobs/"; a separate prefix keeps the * two domains' objects disjoint so neither sweep can see the other's keys). */ export declare const FILE_HISTORY_BLOB_KEY_PREFIX = "file-history/"; /** * [ref] twin 保留条款(core 7.0.0 [ref] §片2 的 SQL 孪生)—— 构造期旋钮,键形与 core 内置后端 * `FileFileHistoryStoreOptions.retention` / `FileStorageBackendOptions.fileHistory.retention` **同域同名**。 * 解析与校验**直调** core 的 `resolveFileHistoryRetention`(缺省 100 / `"unbounded"` 唯一不剪拼法 / 坏值 * 同码 `config.retention_policy_invalid` 响亮拒),本店零第二套语义。 */ export interface SqlFileHistoryStoreOptions { retention?: FileHistoryRetentionPolicy; } /** Dual-dialect durable FileHistoryStore. See the file header for posture + dialect-delta ledger. */ export declare class SqlFileHistoryStore implements FileHistoryStore { protected readonly db: SqlDriver; /** RAW native pool — handed to blob-backend.ts (outside the SqlDriver seam, dialect-switches on * its own string). Not used for this class's own statements (those go through `db`). */ protected readonly pool: MySqlPool | PgPool; /** 2c sync blob-face MARKER (codex R2-high): the route/caps predicates gate on THIS, never on a * `typeof putBlob` duck-probe — core's local FileFileHistoryStore carries a PRIVATE synchronous * `putBlob(hash, bytes): void` helper whose name collides with the service face; a name probe * passes there, mutates core-owned blob storage out of contract, then dereferences `.ok` on * undefined. Presence of this literal = the full typed putBlob/getBlob/hasBlobs face exists. */ readonly syncBlobFaces: true; protected readonly blobs: BlobBackend; /** §10 S-6 in-process lease state (design v1 D2: NOT persisted). Keys = `${scope}\x00${entryId}`. */ private readonly spentAttempts; private readonly activeAttempts; /** [ref] twin 保留条款:每 scope 存活的最新 boundary 数;`null` = `"unbounded"`(自剪关闭,GC 归宿主 `reap`)。 * 由 core 的 `resolveFileHistoryRetention` 在构造期裁定 —— 与两只内置后端同一道门、同一句拒绝。 */ private readonly boundaryKeep; constructor(db: SqlDriver, /** RAW native pool — handed to blob-backend.ts (outside the SqlDriver seam, dialect-switches on * its own string). Not used for this class's own statements (those go through `db`). */ pool: MySqlPool | PgPool, blobBackend?: BlobBackend, opts?: SqlFileHistoryStoreOptions); /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */ private q; private bool; private leaseKey; /** PROTECTED RACE SEAM (fault/race-injectable, test-only override): runs between trackEdit's * repeat-touch pre-check and the conditional mint INSERT. Production body is empty. */ protected beforeMint(_scope: string, _keyHash: string): Promise; /** PROTECTED RACE SEAM: runs between trackEdit's repeat-touch pre-check and the observed_later * UPDATE (the annul-vs-observe window codex R1-high named). Production body is empty. */ protected beforeObserve(_scope: string, _keyHash: string): Promise; /** PROTECTED RACE SEAM: runs between snapshot's staging pass (tracked/versions loaded, blobs staged, * publish verdict green) and the publish tx — the capture window another replica's annul/re-mint * can land in (codex R2 ABA case). Production body is empty. */ protected beforePublish(_scope: string): Promise; /** * Per-scope WRITE-SERIALIZATION lock (codex R1-critical): `SELECT … FOR UPDATE` over an EMPTY result * locks nothing on PG, so neither the empty-target law (import/adopt) nor a multi-statement graph * write can be serialized by the data rows alone. Every graph-mutating tx therefore first ensures + * row-locks the scope's sentinel row — one lock, both dialects, and disjoint-key writers can no * longer interleave into a merged/torn graph. The ensure+lock loops because deleteBySession removes * the sentinel: a waiter woken by that delete sees zero rows and must re-ensure. * * Shape A (S-127): the MySQL ensure verb is `ON DUPLICATE KEY UPDATE scope = scope`, NOT `INSERT IGNORE`. * On InnoDB a duplicate-key IGNORE takes an **S** lock on the existing sentinel and the `FOR UPDATE` right * below has to upgrade it to X — two concurrent scope writers each holding an S deadlock (B-013). ON * DUPLICATE takes X on the duplicate key, the same mode the locking read wants, so they serialize instead. */ private lockScope; /** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed; * the caller disambiguates by re-reading (codex R1-high: answering minted:false after the baseline * vanished would let a later touch mint already-modified bytes as pristine). */ private markObserved; trackEdit(scope: string, absPath: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise; /** Why (scope,keyHash) is currently NOT a retractable mint, or undefined when it still is. */ private annulIneligibleReason; annulTrack(scope: string, absPath: string, root: string, verify?: { env: ExecutionEnv; signal?: AbortSignal; }): Promise; /** `forUpdate` = the {@link SqlTxConn.begin} `@contract txn.read-semantics` ③ arm: a collection read that * runs inside a transaction must be a LOCKING read (a plain SELECT is pinned to the transaction's start on * TiDB, so the lock-wait window's commits are invisible — B-022). Pool-level callers pass nothing. */ private loadTracked; /** `forUpdate` — see {@link loadTracked}: inside a transaction this is a collection read, so it locks. */ private loadVersions; /** [ref] twin 保留条款:这个 entry 的键是否**已花掉**(被 reap 剪过)。台账 `file_history_reaped_entry`。 * `forUpdate`(r3-①连带):发布 tx 内的复核必须是**锁定读**——TiDB 悲观事务的普通 SELECT 按文档读 * start_ts,锁等待窗内提交的墓碑会被漏看(池上的预检无此虑:单语句即当前)。 */ private entryReaped; /** adoptScope 的台账携带(InMemory 参照:正常臂 = 源∪目标,空源臂也带)。目标已有的行原样保留。 */ private copyReapedEntries; /** * [ref] twin 保留条款 —— boundary 提交后的自剪 pass(core InMemory `applyBoundaryRetention` 的 SQL 形)。 * * 选择半场**共享** core 的 `fileHistoryBoundariesToKeep`(两只内置后端与本孪生不许在「哪个 boundary 死」上 * 分家;刚提交者恒 pin):本店的 oldest-first 序 = `publish_seq`(发布时在 scope 锁下按 MAX+1 分配, * UNIQUE(scope, publish_seq) 兜底——严格全序,无破平之需;`published_at_ms` 走数据库钟且已降为信息位, * 见 boundary 发布段行注;core 明写序只影响「剪哪一个同样合法的 * boundary」,pin 保证永远不会剪掉本轮的)。`null` = 未超界 ⇒ 零 reap,一轮只付一次 SELECT。 * * 在**提交之后、连接归还之后**跑(reap 自取第二条连接与 scope 锁):自剪失败不失败本轮 —— boundary 已 * 提交、rewind 座照常,只是这一轮没剪;下一次提交的 pass 按当前全集重算,不累积欠账。参照是无声 * `reap(...).catch(() => {})`,本仓走 `recordFailOpen` 留痕三件套(F 类,普查档 §1 第 50 行)。 */ /** PROTECTED RACE SEAM (test-only override): runs at the start of the retention pass, before any of its * reads. Production body is empty. */ protected beforeRetentionPass(_scope: string): Promise; /** PROTECTED RACE SEAM (test-only override): runs after the retention pass has chosen its survivors and * before it deletes. Production body is empty. */ protected duringRetentionPass(_scope: string): Promise; /** PROTECTED RACE SEAM (test-only override): runs inside `restore` between the boundary-header check and * the mapping read. Production body is empty. */ protected duringRestorePlan(_scope: string, _entryId: string): Promise; /** PROTECTED RACE SEAM (test-only override): runs inside the publish tx AFTER `begin()` and BEFORE * the scope lock — the window where another replica's publish can commit while this tx's snapshot is * already pinned. That window is exactly what B-022 rode: a plain SELECT taken after the lock is still * pinned to the transaction's start, so this tx's collection reads are LOCKING reads * ({@link SqlTxConn.begin} → `@contract txn.read-semantics`). Production body is empty. */ protected beforePublishLock(_scope: string): Promise; /** PROTECTED RACE SEAM (test-only override): runs inside `exportOf` between the boundary-header read and * the mapping read. Production body is empty. */ protected duringExportGraph(_scope: string): Promise; private applyBoundaryRetention; /** `forUpdate` — see {@link loadTracked}: the in-transaction re-check must be a LOCKING read, or it just * replays the pool-level pre-check's snapshot and its documented `null` arm becomes unreachable. */ private boundaryExists; snapshot(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise; restore(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise; /** `restore` 的不可变计划:头存在性 + tracked + 版本链 + 边界映射,**scope 锁内一事务**读齐后 COMMIT。 * `null` = 锁内复核发现 boundary 已不在(与预检之间被 reap)。 */ private restorePlan; canRestore(scope: string, entryId: string): Promise; reap(scope: string, keepEntryIds: string[]): Promise; /** The reap BODY, shared by the host-facing `reap` verb and the retention pass — both call it under the * scope lock in the SAME tx that listed `present` (codex r1 [high]: a survivor set chosen outside the lock * is stale by the time it deletes, and a boundary another replica committed in the window would die * under it). Ledger row first, then the drop, then the version cascade — one tx, atomic. */ private reapWithin; /** E21 purge — drop the WHOLE scope (tracked set incl. v1 baselines + markers + boundaries), unlike * `reap(scope, [])` which retains baselines while the scope lives. Blob bytes become orphans and are * collected by the async grace-window sweep (never a synchronous external-store delete on the purge * path). Also the overwrite-dst session-sync wipe seam (import requires an EMPTY target graph). */ deleteBySession(scope: string): Promise; /** Async byte-GC: blobs referenced by NO version row (any scope), past the created_at grace window. * Domain-isolated by construction (this store's blob table only — [ref] D1). Reaper-driven. */ sweepOrphanBlobs(): Promise; /** Upload ONE content-addressed blob ahead of an importHistory (§5 streamed negotiation). Store-level * integrity re-check (the route pre-verifies too — the internal caller might not). Never-throw. */ putBlob(hash: string, bytes: Uint8Array): Promise; getBlob(hash: string): Promise; /** The subset of `hashes` already present — one SQL INDEX probe (the import presence pre-check). */ hasBlobs(hashes: string[]): Promise>; adoptScope(fromScope: string, toScope: string): Promise; private scopeEmpty; /** `forUpdate` — see {@link loadTracked}. Every in-transaction caller passes `true`: the four reads are * one graph, and a graph materialized from a start-of-transaction snapshot disagrees with the LOCKING * emptiness probe that decided to materialize it (that disagreement answered a legal idempotent * re-import as `conflict` — B-022). */ private exportOf; exportHistory(scope: string): Promise; importHistory(scope: string, data: FileHistoryExport, srcGetBlob: (hash: string) => Promise): Promise; } /** MySQL-protocol (TiDB) binding. */ export declare class TiDBFileHistoryStore extends SqlFileHistoryStore { constructor(pool: MySqlPool, blobBackend?: BlobBackend, opts?: SqlFileHistoryStoreOptions); } /** PostgreSQL binding. */ export declare class PgFileHistoryStore extends SqlFileHistoryStore { constructor(pool: PgPool, blobBackend?: BlobBackend, opts?: SqlFileHistoryStoreOptions); } /** * [ref] D1② — A7-recipe upgrade precondition assert (**refuse to start**, never a warn; the form and * rationale follow `assertPermissionRuleTicketSchema` verbatim: this repo ships no ALTER TABLE, and * `CREATE TABLE IF NOT EXISTS` is a no-op on an existing table whose columns predate this build). * * The probe is one always-empty `WHERE 1=0` read per table naming every column this build references. * ONLY a provable missing-column error gets the destructive recreate guidance — everything else * (timeout / reset / permission / missing table) rethrows untouched: advising `DROP TABLE` for a * connection blip is more expensive than the defect this gate exists to stop. Honest cost statement: * dropping these tables loses per-file REWIND history (working-tree convenience state) — session * transcripts and every other durable subsystem are untouched. */ export declare function assertFileHistorySchema(query: (sql: string) => Promise<{ rows: Record[]; }>, dialect: "tidb" | "pg"): Promise; //# sourceMappingURL=file-history-store-sql.d.ts.map