import type { AcquiredSession, PlacedSessionRow, RetentionDeclaration, SessionPlacementRecord, SessionStore, SessionTreeEntry } from "@sema-agent/core"; import type { SessionListItem } from "../security.js"; import type { StagingHandle } from "../session-sync-kernel.js"; import { type PlacementAcquireOpts } from "./session-placement.js"; export interface CachingOptions { /** Idle seconds a woken session is kept warm. 0 disables caching (pure passthrough). */ ttlSec: number; sweepIntervalMs?: number; /** S25 (SILENT-FALLBACK P1): called when a cross-instance write (import/replace) evicts a WARM entry — * the affinity-contract violation signal that previously had zero fingerprint. */ onConflictEvict?: (sessionId: string) => void; } /** * Warm/sticky cache in front of a durable {@link SessionStore} (Option A). * * Keeps a woken session in memory and **reuses the same storage instance across turns**, so a hot * session is not re-read from TiDB every short turn (treats the cattle like a warm pet while it's * active). Idle sessions expire after `ttlSec`; `release` evicts immediately. * * ⚠️ **Session affinity is REQUIRED** when this cache is enabled (`SESSION_CACHE_TTL_SEC>0`): route a * given sessionId to the same instance. Under affinity the cached storage is the sole writer, so its * in-memory leaf is always current and consistent with TiDB (write-through; F2 CAS still applies). * Without affinity a concurrent write from another instance makes this cache stale; that surfaces as * a `SessionError("conflict")` on the next append — the async run driver calls `evict()` on conflict * (defense-in-depth) and the entry also expires after `ttlSec`, but those are backstops, not the * guarantee. If you cannot guarantee affinity, set `SESSION_CACHE_TTL_SEC=0` to disable the cache. * * Orthogonal to the F3 branch-floor change: floor bounds the *cold wake* size; this cache removes * *repeated* re-reads of an active session. */ export declare class CachingSessionStore implements SessionStore { private readonly inner; private readonly cache; private readonly pending; private readonly ttlMs; private readonly timer?; /** Delegated tenant-ownership ops, present only if the inner store exposes them (the TiDB store does). */ readonly ownerOf?: (sessionId: string) => Promise; readonly register?: (sessionId: string, owner: string | null) => Promise; /** Delegated SessionRepo seam (§0.5) — list/fork/delete operate on the DURABLE tables directly (they bypass the * warm cache by design; a fork/delete must read/write durable state, not a possibly-stale cached view). */ readonly listSessions?: (opts: { owner?: string; includeUnowned?: boolean; cursor?: { lastActivityAt: string; sessionId: string; }; limit: number; q?: string; }) => Promise; readonly fork?: (sourceId: string, owner: string | null) => Promise; readonly deleteSession?: (sessionId: string, owner: string | null) => Promise; /** K-5c (core 1.155 SessionStore.noteTaskRun seam) — core's Runner calls it at runTask START to record the latest * run per session (→ listSessions `lastRunId`). MUST be forwarded: the local backend's Runner store is THIS cache * wrapper, so without forwarding `noteTaskRun?.()` silently no-ops and lastRunId stays null (review BLOCKER). Pure * pass-through to the inner store's in-mem map — no cache interaction. (TiDB/PG don't implement it → not bound.) * Return type matches core's `SessionStore.noteTaskRun?(): void | Promise` (review: don't narrow to void). * [ref](core 5.65 增量批):第三参 = engine runId。绑定用的是 `.bind(inner)`(整函数),运行期本就 * 三参全转 —— 这里把**声明**对齐到 core 的三参形,免得读这行类型的人以为缝上只有两参(fork-routing * 的两参形正是在运行期真丢过第三参的那个病,同批修)。内店(LocalSessionStore)今日仍是两参消费, * runId 落地候 sessions 列表 wire 键的 spec 节拍(见本批提货报告件1b)。 */ readonly noteTaskRun?: (sessionId: string, taskId: string, runId?: string) => void | Promise; /** Write-once auto-title (SQL twins + local sidecar carry it; pure pass-through). */ readonly setTitleIfNull?: (sessionId: string, title: string) => Promise; readonly probeTitle?: (sessionId: string) => Promise<"none" | "untitled" | "titled">; /** E18 — delegate the leaf read straight to inner (a cache-bypassing single SELECT; the resume-at turn-capture * reads the DURABLE leaf, not a possibly-stale cached view — same discipline as the §0.5 repo ops above). */ readonly getLeafId?: (sessionId: string) => Promise; /** [ref] 六轮复审:owner+leafId 原子快照透传(cache-bypassing,getLeafId 同姿势)。 */ readonly getHead?: (sessionId: string) => Promise<{ owner: string | null; leafId: string | null; } | undefined>; /** 2c session-sync — delegated export/import seam, DURABLE (bypasses the warm cache exactly like * list/fork: an export reads the full durable log, an import writes durable state). */ readonly exportEntries?: (sessionId: string) => Promise; /** 2c session-sync P1d-α (PULL streaming) — delegated ids-only projection + keyset-paged entry STREAM, DURABLE * (read straight from the durable log, bypassing the warm cache, like exportEntries). The stream's per-batch query * discipline (one pooled query per page) lives in the inner store; this wrapper just forwards. */ readonly listEntryIds?: (sessionId: string) => Promise; readonly exportEntriesStream?: (sessionId: string, opts?: { afterSeq?: number; batchSize?: number; }) => Promise | null>; /** 轴A #1(1.254):core B-17 forget 面(evict 本地 + 转发 inner 可选面)。 */ forget?: (sessionId: string) => void | Promise; readonly importEntries?: (sessionId: string, owner: string | null, entries: SessionTreeEntry[]) => Promise; /** [ref](cli 取证)2c session-sync P1d-β — delegated STAGED import seam. 此前 staging 族不在本类的逐方法 * 转发白名单里 ⇒ 默认部署(SESSION_CACHE_TTL_SEC=300 把真店包进本类)PUSH Phase A 恒 501 * `capability.session_store_required`,而 capabilities().sessionSync 谓词三项(backend/exportEntries/ * fileHistoryStore)都被转发了 ⇒ 照报 true(能力面谎)。转发形:handle 的 `commit` 是唯一写真 id 的 * 时刻,成功后逐出该 id 的暖 handle + S25 指纹(importEntries/replaceEntries 同纪律);`abort` 只丢 * staging 行、不碰真 id ⇒ 不逐出。 */ readonly beginImportStaging?: (realSessionId: string, token: string) => StagingHandle; /** Staging 清扫面(kernel `ImportStagingStore.sweepStagingSessions`)。当下 reaper 走 `backend.session()` * 裸店、不经本包装,但 security.ts 的 OwnerAwareSessionStore 契约列了它 —— 经 sessionStorage 消费的 * 未来调用方不该静默 no-op,与 beginImportStaging 同批补齐(同一类白名单缺口)。 */ readonly sweepStagingSessions?: () => Promise; /** 2c session-sync (§7/§8) — delegated IDEMPOTENT replace; like importEntries it writes durable state under * `sessionId`, so a stale warm handle for that id is evicted on success (next acquire re-wakes from the new log). */ readonly replaceEntries?: (sessionId: string, owner: string | null, entries: SessionTreeEntry[]) => Promise; /** * [ref] 车1 —— 内层店的 `retention` **声明**(core `RetentionDeclaring`)。 * * 🔴 为什么这一格必须转发,而且方向比别的转发更要命:生产默认 `SESSION_CACHE_TTL_SEC=300` 会把真店 * 包进本类,于是 boot 递给 `assertRetentionCapability` 的就是**包装层**。core 对缺席是 fail-closed * 读 `"none"` —— 不转发 ⇒ 一个真 managed 的 SQL 部署在 locked policy 下**被拒启**,而拒启文案会指着 * 一只其实完全合格的店(「stores that cannot delete」)。转发是**只读快照**而不是 getter:声明是店的 * 构造期常量,一个会变的声明本身就是缺陷。缺席则不设(保持 fail-closed 的缺席读法)。 */ readonly retention?: RetentionDeclaration; /** * [ref] C18 —— 内层店的 **placement 声明**转发。 * * 🔴 这一格与 {@link retention} 是同一条教训的同一族(而且更狠):生产默认 `SESSION_CACHE_TTL_SEC=300` * 把真店包进本类,于是 boot 递给 core 的就是**包装层**。core 的铸造门判「row store ∧ **session 店声明**」 * ⇒ 不转发 = 整条 subagent 转录持久化在默认部署上**静默不生效**(placement 参数被合法忽略、子代转录 * 退回普通会话、声明的 durable tier 变成一句空话),而且没有任何一处会报错。 * 只读快照而非 getter:声明是店的构造期常量(会变的声明本身就是缺陷)。 */ readonly placements?: SessionStore["placements"]; /** [ref]:placed 分区的两只读面(reap 分区腿 / 准入探针)——同款转发理由:reaper 与准入判据拿到的 * 是包装层,不转发 = 分区腿静默不可用、暖命中的 claim 关门静默失效。缺席则不设(诚实降级)。 */ readonly listPlaced?: (kind: "subagent", opts?: { olderThanMs?: number; scope?: string; }) => Promise; readonly placementOf?: (sessionId: string) => Promise; constructor(inner: SessionStore, opts: CachingOptions); acquire(sessionId?: string, opts?: PlacementAcquireOpts): Promise; touch(sessionId: string): Promise; release(sessionId: string): Promise; /** Drop a session from the warm cache (e.g. after a cross-instance write conflict). */ evict(sessionId: string): void; get size(): number; dispose(): void; private store; private sweep; } //# sourceMappingURL=caching-session-store.d.ts.map