import { type UsageRow } from "../usage-analytics.js"; import type { TaskStatus } from "@sema-agent/core"; import type { RunRecord, SessionSummary, RunEvent, PersistedTaskResult, LatestRunRef } from "./store-contracts.js"; import type { LedgerEventType } from "../trace/ledger-events.js"; /** * [ref] P0 — the local lane's stand-in for the SQL twins' checkpoint-table JOIN. The file/memory run stores * cannot JOIN `checkpoint` (separate store, separate files), so the suspended-run reapers were NO-OPs — which * left a durable HITL park whose approval was never answered holding the task_active claim FOREVER (the * "suspended run locks the session" incident: every later submit 409s and no reaper path releases it). * The backend assembly (store-backend.ts local lane, where checkpoint + run store are built side by side) * injects this probe so the reapers get the SAME two predicates the SQL twins express as EXISTS subqueries: * - hasPending = EXISTS (checkpoint c WHERE c.session_id = ? AND c.status = 'pending') — the [ref] D-D * NOT-EXISTS-pending guard: a row whose checkpoint is STILL pending is owned by the checkpoint-state-driven * sweeps and must NEVER be time-reaped (else two-runs-one-session, see run-store-sql.ts reapSuspended doc). * - hasExpired = EXISTS (checkpoint ce WHERE ce.session_id = ? AND ce.status = 'expired') — the * failSuspendedWithExpiredCheckpoint driver (checkpoint-STATE-driven, not time-driven). * Probe ABSENT (old assembly / no checkpoint store wired) ⇒ the reapers stay honest NO-OPs — without the * pending-guard there is no safe way to distinguish "abandoned park" from "operator still deciding". */ export interface RunStoreCheckpointProbe { hasPending(sessionId: string): Promise; hasExpired(sessionId: string): Promise; } export declare class MemoryRunStore { private readonly runs; /** sessionId → taskId — the single-active-run claim (the `task_active` table). */ private readonly active; /** taskId → events (kept seq-sorted on read). */ private readonly events; /** [ref] the checkpoint-table stand-in (see {@link RunStoreCheckpointProbe}); absent ⇒ reapers NO-OP. */ private checkpointProbe?; /** [ref] wire the checkpoint probe (backend assembly, where checkpoint + run store are built together). */ setCheckpointProbe(probe: RunStoreCheckpointProbe): void; private toRecord; /** Create a run iff the session has no active run (the task_active unique-key claim). */ createRun(taskId: string, sessionId: string, owner: string | null, instanceId: string, meta?: { jobId?: string | null; source?: string | null; objectivePreview?: string | null; }): Promise<{ ok: true; } | { ok: false; activeTaskId: string; }>; requestCancel(taskId: string, owner: string | null): Promise; isCancelRequested(taskId: string, owner: string | null): Promise; requestPreempt(taskId: string, owner: string | null): Promise; isPreemptRequested(taskId: string, owner: string | null): Promise; heartbeat(taskId: string, owner: string | null): Promise; appendEvent(taskId: string, seq: number, type: LedgerEventType, data: unknown): Promise; maxSeq(taskId: string): Promise; retainedFrom(taskId: string): Promise; getEvents(taskId: string, afterSeq: number): Promise; /** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review, * [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */ setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise; /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard). */ setSuspended(taskId: string): Promise; /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */ setNeedsReview(taskId: string): Promise; /** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags. * [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */ markResuming(taskId: string, instanceId: string): Promise; getActiveTaskId(sessionId: string): Promise; getRun(taskId: string): Promise; /** Runs newest-first, keyset on (createdAt, taskId) DESC; optional exact-match filters (owner is exact, like the SQL). */ listRuns(opts: { status?: string; jobId?: string; source?: string; owner?: string; cursor?: { createdAt: string; taskId: string; }; limit: number; }): Promise; /** * S-297 —— 这个会话**最近一条腿**的窄指针({@link SqlRunStore.latestRunForSession} 的孪生:同一个排序口径 * `createdAt DESC, taskId DESC`,与本店 {@link MemoryRunStore.listSessions} 挑 `rn=1` 那一行的比较器逐字同一份)。 */ latestRunForSession(sessionId: string): Promise; /** DISTINCT sessions newest-first by last activity (the CC /resume picker): latest run's preview/status + first/last/count. */ listSessions(opts: { owner?: string; includeUnowned?: boolean; cursor?: { lastActivityAt: string; sessionId: string; }; limit: number; q?: string; }): Promise; /** * E21 (§0.5 delete) — purge the runs ledger for one session, scoped by `session_id` ALONE (per-row owner is the * submitting principal and diverges from the session owner — full 判据 see the SQL twin's head-note, F-A); * aborts (returns {active}) if a run is live. * * `expectedSessionOwner` 是 SQL 双生的**化身围栏**入参(事务内复核 `session_meta.owner`,防「A 验权后 A' 删完、 * 别人用同一 uuid 重新登记」那一形)。本孪生**执行不了**它:local 车道的会话表在另一只店里,这只账本没有 * session_meta 可读 —— 参数只作契约对齐(签名不齐会让 RunStore 联合类型的调用点直接不可调)。local 的替代 * 围栏是两条:①协调器在本调用**紧前**复读一次 canonical owner(不齐即整条中止);②数据根的独占 BootLock * 保证一个数据根恒一个进程,交错只可能发生在同进程的 await 缝里。这条不对称是**明写**的,不是遗漏。 */ deleteBySession(sessionId: string, expectedSessionOwner: string | null): Promise<{ removed: number; } | { active: string; }>; /** 用量扫窗——in-memory 腿:遍历窗内行,投影同 usage-analytics 纯函数(SQL 孪生 parity)。 */ usageScan(fromMs: number, toMs: number, opts?: { owner?: string; limit?: number; }): Promise<{ rows: UsageRow[]; truncated: boolean; }>; sourceSummary(sinceMs: number): Promise>; /** Fail stale `running` rows (crashed-run backstop) + release task_active for any now-terminal row. */ reapStale(olderThanMs: number): Promise; /** [ref] shared tail of both suspended-run reapers: flip one parked row → failed('approval.expired'), the * in-memory twin of the SQL UPDATE (`error`/`error_code` set directly — no result blob to derive from). */ private failExpiredPark; /** Release the claim for TERMINAL rows only — suspended/needs_review keep it (the `DELETE ta … WHERE tr.status * NOT IN ('running','suspended','needs_review')` twin, shared by reapStale + the [ref] suspended reapers). */ private releaseTerminalClaims; /** * [ref] Durable F4 expiry — the local twin of TiDBRunStore.reapSuspended, driven by the injected checkpoint * probe instead of a SQL JOIN (formerly a NO-OP, which was one of the five closed doors in the "suspended run * locks the session forever" incident). Semantics verbatim from the SQL twin: suspended ∧ updatedAt < cutoff * ∧ NOT probe.hasPending(session) → failed 'approval expired before decision' / error_code 'approval.expired' * + release the task_active claim. The NOT-pending clause is the [ref] D-D blocker guard * (SQL 孪生现在的真身:`run-store-sql.ts` 的 `reapSuspended`;`tidb-run-store.ts` 该壳文件已删(5.0.0)): a row whose checkpoint is STILL pending belongs to the checkpoint-state * sweeps (SLA deny-sweep / failSuspendedWithExpiredCheckpoint) — time-reaping it here would orphan a model * leg on a now-unlocked session. Probe absent ⇒ honest NO-OP (never kill a park without the guard). */ reapSuspended(olderThanMs: number): Promise; /** * [ref] [ref] §3 inv#3 crash-safe backstop — the local twin of TiDBRunStore.failSuspendedWithExpiredCheckpoint * (checkpoint-STATE-driven, not time-driven; runs UNCONDITIONALLY every reaper tick, no APPROVAL_TIMEOUT_SEC gate). * Semantics verbatim from the SQL twin: (suspended ∨ needs_review) ∧ probe.hasExpired(session) ∧ NOT * probe.hasPending(session) → same fail + claim release. The NOT-pending clause covers a re-suspended session * that minted a NEW pending gate (wq64gmm5e: an abandoned needs_review park leaks exactly like a suspended one). */ failSuspendedWithExpiredCheckpoint(): Promise; } //# sourceMappingURL=memory-run-store.d.ts.map