import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ExecutionOutcomeRecordWord, type GateOutcome, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core"; import { type CheckpointAskCandidate, type DecidedApprovalRecord, type PendingCheckpoint, type PendingRowBinding } from "./checkpoint-store-sql.js"; export interface LocalCheckpointStoreOptions { /** taskId join for the pending card (the local FileRunStore's `getActiveTaskId`). Absent ⇒ `taskId: null`. */ getActiveTaskId?: (sessionId: string) => Promise; /** E21 owner guard (the LocalSessionStore's `ownerOf`). Absent ⇒ owner guard degrades to null-owner-only * (a non-null owner with no owner source purges NOTHING — fail-closed, mirrors the SQL EXISTS guard). */ ownerOf?: (sessionId: string) => Promise; } export declare class LocalCheckpointStore { /** [ref] 车1:托管留存声明 —— **诚实的 none**(与 local session 店同一条判据:本店的行没有按期删除的腿; * 留存三方法只实现在 SQL 聚合店上)。locked policy + 本店 ⇒ core 的门拒启,那是契约的正确行为。 */ readonly retention: import("@sema-agent/core").RetentionDeclaration; /** * `CheckpointStore.durability` 声明([ref] 欠账,[ref] 件5)——**如实按内核判**:本类的内核是 core 的 * `FileCheckpointStore`(crash-safe ledger + snapshot,自身声明 `"durable"`),本地 scope 注册表也落 * 在同一个盘上目录里 ⇒ 进程重启后 park 全部还在、还赎得回,`"durable"` 是诚实读数而不是抬举。 * * 它与「session store 的 local 形声明 durable 就拒启」不矛盾:那条判据管的是**声明 vs 介质**,而这里 * 的介质就是盘。真正的 process-local 形(`InMemoryCheckpointStore`)由 core 自己声明 `"process-local"`。 */ readonly durability: StoreDurability; /** * `CheckpointStore.fidelity` 声明(core 5.17.0 [ref] 提货批 [ref])——**如实按内核判**,与 durability * 同一条判据:本类的读写内核是 core 的 `FileCheckpointStore`,它自己声明 `"json"`(盘上是 JSON 行), * 包装层只加 scope/session 索引与 ctx 附属文件,不改这条 round-trip 的宽度 ⇒ 抄内核的读数是诚实的。 * * ⚠️ 包装类**不继承**被包装者的声明(本类是委派不是子类),所以这一格必须自己写。它与内核那一格 * 是否仍然一致由测试盯着(`wiring-governance-operator.test.ts` 的 [ref] 组直接读内核那一格对表)—— * 内核哪天改宽度,包装层的声明会被那条钉当场揪出来,而不是靠人记得同步。 */ readonly fidelity: StoreFidelity; /** * `CheckpointStore.redecision` 声明(core 5.22.0 F-012 L2/L3;[ref] clay 裁放行)——**如实按内核判**, * 与 durability/fidelity 同一条判据:内核 core `FileCheckpointStore` **自己**声明 `{ reopen: true }`, * 本类的 {@link LocalCheckpointStore.reopen} 纯委派给它 ⇒ 抄内核的读数是诚实的。包装类**不继承**被 * 包装者的声明(委派不是子类),所以这一格必须自己写。`validatingLease` 不声明(内核也没声明)。 * 翻向钉:`wiring-governance-operator.test.ts`(修前恰红,与本声明同 commit)。 */ readonly redecision: { readonly reopen: true; }; /** * `CheckpointStore.execution` 声明(core 7.7.0 [ref];S-160)——**必填**,与上面三格同一条判据:**如实 * 按内核判**。内核 core `FileCheckpointStore` 自己声明 `{ outcome: true }`,并把执行记录落成 ledger 的 * 一个新事件种类(`execution`,与 resolve 同款「先 append+fsync 再翻内存」的崩溃序), * {@link LocalCheckpointStore.recordExecutionOutcome} 与 {@link LocalCheckpointStore.reopen} 都是纯委派 * ⇒ 抄内核的读数是诚实的。包装类**不继承**被包装者的声明(委派不是子类),所以这一格必须自己写。 * * ⚠️ 升级序的诚实披露(DEPLOY-PREREQS 同句):ledger 里一旦出现 `execution` 事件,**7.7.0 之前的 * binary 会拒绝重放这个目录**(响亮,不是静默跳过);而压缩把事件折进快照行之后,那个旧 binary 读到 * 一个它不认识的字段、且它的 `reopen` 不认这条记录 —— 跨记录行回滚不受支持。 */ readonly execution: { readonly outcome: true; }; private readonly inner; private readonly scopesPath; private readonly sessionTokensPath; private readonly ctxDir; private readonly tmpDir; private readonly scopes; /** [ref] sessionId → every token ever put for it — the service-side index that stands in for the SQL twins' * `WHERE checkpoint.session_id = ?` on NON-pending rows (core's File store enumerates pending only via * listByScope; get(token) is the sole read for resolved/expired rows, so cross-status session queries need * this registry). Registered BEFORE the inner put (same crash direction as the scopes registry: a stale * index entry is a harmless empty lookup; an unindexed EXPIRED row would silently disable the * failSuspendedWithExpiredCheckpoint leg — the bad direction). Persisted so a restart keeps the * suspend→crash→expire-later lifecycle visible. Entries are never dropped while the inner row exists * (a `resolved` row can be reopen()ed back to pending — pruning it would be wrong). */ private readonly sessionTokens; private readonly opts; constructor(root: string, opts?: LocalCheckpointStoreOptions); /** Persist the session→tokens index (whole-file atomic replace — puts are per-suspend, low frequency). */ private persistSessionTokens; put(token: CheckpointToken, cp: Checkpoint): Promise; get(token: CheckpointToken): Promise; resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise; reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise; /** core 7.7.0 [ref](S-160):执行判词的 CAS —— 语义、四词、幂等的结构相等与冲突抛全在被包的内核里 * (它自己走 core 的共享判词 `executionRecordDisposition`,ledger 事件 `execution` 是它的 commit 半场), * 本 wrapper 只做转发。服务扩展面(运维队列/会话索引)与这条记录无关:它不改 status,也不新增可枚举行。 */ recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise; /** core 5.14.0([ref]):入参是 `PendingSteerInput`(text/trusted + 可选 actor/inputId/priority),不是 * 两键单座 —— 形参窄成 `{text,trusted}` 会**静默丢掉**署名与幂等键(TS 的方法参数是双变的,窄形不 * 报错,只是把三个可选键从类型上抹掉,调用点再也传不进来)。队列语义与边界全在被包的 core * `FileCheckpointStore` 里,本 wrapper 只做转发。 */ setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise; expire(token: CheckpointToken, scope: string): Promise; reap(scope: string, cutoff: number): Promise; listByScope(scope: string): Promise; close(): void; /** C4-B:注册表损坏时从**数据本体**懒重建 scope 集(core 1.424 `listScopes?` = 「有 ≥1 条 pending 的 * distinct scope 集」,与 listByScope 可见面对齐 —— 恰好是本注册表要的内容)。一次性:重建后回写 * sidecar,恢复正常路径。seam 缺席(第三方旧 inner)⇒ 保持 C 案的可见降级,不装恢复了。 */ private scopeRegistryCorrupt; private ensureScopesRebuilt; /** All still-pending checkpoints across the registered scopes (summary + full row via get). */ private pendings; /** Operator approval queue — the same NON-secret card as the SQL twins (never the token). */ listPending(scope?: string): Promise; /** * [ref]:与 SQL 孪生**同判**的 session 键读口 —— `binding.gateKinds` 过滤门型、序恒「最新优先,token 兜平局」 * ({@link pendingRowPrecedes} 是两 twin 共用的判据;语义全文见 SQL 侧 `PendingRowBinding` 顶注)。 * 修前是「扫描序里第一条 sessionId 相等的」:`pendings()` 按 scope 注册表 + `listByScope` 顺序走,同 * session 两条 pending 时答哪条取决于 sidecar 里 scope 的插入序 —— 与 SQL 无序 `LIMIT 1` 同病。 */ findPendingTokenBySession(sessionId: string, scope?: string, binding?: PendingRowBinding): Promise; /** [ref]:候选集 → 读口该答的那一条(过滤 sessionId + 门型,再取「最新优先」)。 */ private pickPendingRow; /** [ref] read-only probe face: does this session have an EXPIRED checkpoint row? The local twin of the SQL * `EXISTS (SELECT 1 FROM checkpoint ce WHERE ce.session_id = ? AND ce.status = 'expired')` that drives the * run stores' failSuspendedWithExpiredCheckpoint. Scans the session→token index (get(token) is the only * cross-status read core's File store offers); an index entry whose inner row vanished is pruned lazily. * Backed by a persisted registry, so it stays honest — never a guessed "true". * ⚠️ 存量限制(codex M2,如实):索引随 put 增量建立,无升级迁移——本版本部署前已存在的 checkpoint 不在 * 索引里,其 expired 对本探针不可见(core File store 的 listByScope 是 pending-only,无法枚举历史行回填)。 * 旧存量锁死的恢复把手 = cancel verb([ref]①,不依赖本索引);增量行全覆盖。 */ hasExpiredBySession(sessionId: string): Promise; /** * [ref] 件1 —— SQL 孪生 `findDecidedApprovalsForBinding` 的本地形(判据、词表、失败方向逐条同源; * 那处顶注是唯一真源,这里只记两处**本地形自己的**差别)。 * * · 枚举面走 [ref] 的 `sessionTokens` 索引 —— 与 `hasExpiredBySession` 同一条路(core 的 File store * 只能 pending-only 枚举,跨态读只有 `get(token)`),所以它那条**存量限制**在这里逐字同样成立: * 索引建立之前 park 的老行对本口不可见 ⇒ 回放判别不出 ⇒ 调用方逐字回落 404(保守方向)。 * · **没有排序**:本口交出全部命中行,由调用方按「判词一致才回放」判(理由见 SQL 孪生顶注的 * codex R1-[medium] 四段——正是为了消掉「local 只有 createdAt、SQL 有 decided_at_ms」这条排序键 * 分岔)。同理回放的 wire 形**刻意不投** `decidedAtMs`:core 的 `Checkpoint` 上没有那个字段, * 投了就是「SQL 车道有、local 车道没有」的键集分岔。 */ findDecidedApprovalsForBinding(sessionId: string, boundCallId: string): Promise; /** * S-384 —— 对账收敛器**判据 1**(身份三元组 ∧ hash 双等 ⇒ `bindBatch`)的读口在 local 车道的形。 * 语义、判别纪律、`unparseable` 的含义,唯一属主 = SQL 孪生 `findCheckpointCandidatesForAsk` 的顶注与 * `CheckpointAskCandidate` 的字段注;这里只记**本形自己的三处差别**。 * * 🔴 **为什么必须有这一口**(不是锦上添花):S-384 让 local 车道有了持久 ask 账,于是收敛器在这条车道上 * 第一次真的有行可扫。缺这一口 ⇒ 判据 1 结构性不命中 ⇒ **窗到期 park 的 ask 行永远停在 `PARKING`**, * 而那个态在 wire 上是有后果的:`POST /v1/tool-approvals/:id/respond` 的 `PARKING` 臂答的是 * `202 parking_in_progress + retryAfterMs`(它的前提是「`bindBatch` 毫秒级就到」),在一台绑不了的机器上 * 那句「再试一次」**永远不会兑现**;run 跑到终局后判据 2 还会把它收成 * `DENIED(routing_failure_fail_closed)` ⇒ 迟到答拿到的判别位是 `denied` —— 对一条**人真的批准过**的 ask * 撒谎。两条都是相对 7.80.2 的回归,根因同一个:**判据 1 的读口只有 SQL 孪生有**。修在源头 = 把这一口 * 补齐,而不是给下游那两处各加一条「这台机器绑不了」的特判。 * * 形差(逐条,如实): * · **枚举面**走 [ref] 的 `sessionTokens` 索引(与 `hasExpiredBySession` / `findDecidedApprovalsForBinding` * 同一条路),所以它那条**存量限制**在这里逐字同样成立:索引建立之前 put 的老行对本口不可见 ⇒ 判据 1 * 不命中 ⇒ 行落判据 ②③④⑤(保守方向,与读口缺席时的既有降级同向)。 * · **没有列,所以没有列/blob 一致性门**:SQL 孪生的两道矛盾门(`tool_call_id` 与 `bound_input_hash` * 的列 vs blob)在本形结构上不存在 —— 本形只有 blob 这一个真源,谈不上两份互相矛盾。判别力因此**不弱**: * 那两道门防的是「外部改库 / schema 偏斜让列与 blob 分家」,而本形没有可分家的第二份。 * · **没有行级版本闸**:本类 `peekScopeByToken` 顶注已亲核过这条事实 —— 本形的 `get()` 没有行级版本闸, * 唯一的读不动形是账本重放级(整店同时读不动)。所以 SQL 孪生那道 `version > MAX_SUPPORTED` 的 * `unparseable` 臂在这里没有对应物;本口的 `unparseable` 只由**窄读器**判(blob 形不合)。 * · **窄读器是同一只** `pendingActionToolCallId`(与 SQL 孪生共用,零第二份手抄):身份三元组的第一维 * `sourceTaskId`、`boundInputHash`、以及「读不出 vs 确定不匹配」的三态,全部由它一家判。 */ findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise; /** * [ref]:按 token 的 scope 读 —— 与 SQL 孪生**同一条契约**(`undefined` 无行 / `null` 匿名 / 属主串), * 语义与存在理由的唯一属主 = SQL 侧 `peekScopeByToken` 顶注。 * * ⚠️ 形差如实记:SQL 孪生那口是真「轻读」(只 SELECT scope 一列),本形没有列可选 —— core 的 File 店 * 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级版本闸(唯一的 * 读不动形是账本重放级 = 整店同时读不动,`findPendingTokenBySession` 先抛),所以「行体读得出而 scope * 读不出」这一支在本形结构上不存在。哪天本形补了行级闸,这里要跟着改成不碰行体的读。 */ peekScopeByToken(token: CheckpointToken): Promise; /** `undefined` = no pending row; else the owner scope (local scopes are always strings — never null). * [ref]:与 `findPendingTokenBySession` 同一个 {@link pickPendingRow}(同 binding ⇒ 同一行)。 */ peekPendingScope(sessionId: string, binding?: PendingRowBinding): Promise; /** * [ref] D-D global expiry sweep — same kind-split as the TiDB twin: the deadline branch abort-expires * every kind EXCEPT a human/irreversible_ask approval gate (those get the graceful resolve-DENY via * listExpiredApprovalGates), with the AskUserQuestion carve-back (denying a question is incoherent → expire); * the terminal_at_ms branch (derived read-time via the SAME formula the TiDB put stamps) abort-expires ANY kind. */ reapExpired(cutoff: number): Promise; /** [ref] D-D SLA deny-sweep input — pending human/irreversible_ask gates past deadline (excl. AskUserQuestion). */ listExpiredApprovalGates(cutoff: number, limit?: number): Promise>; putCtx(sessionId: string, ctx: unknown): Promise; getCtx(sessionId: string): Promise; /** GC ctx files whose lifecycle is over — same three guards as the SQL twins (stale ∧ no pending ∧ no active run). */ reapCtx(cutoff: number): Promise; /** * E21 session purge — functional contract: a deleted session has no resumable future. Expires every pending * gate of the session (all scopes) + unlinks its ctx file. Owner guard mirrors the SQL EXISTS guard: with an * `ownerOf` source the owner must null-safe-match; without one, only an ownerless call (owner=null) purges. * Returns the number of gates expired. (Terminal ledger history stays on disk — local transcripts posture.) */ deleteBySession(sessionId: string, owner: string | null): Promise; } //# sourceMappingURL=local-checkpoint-store.d.ts.map