UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

446 lines (445 loc) • 30.6 kB
import type { SubagentEditedFile, SubagentStep } from "../agents/subagent-steps.js"; /** Terminal/live usage rollup persisted on the row (mirror of the task_notification/BCE fields). */ export interface BackgroundAgentUsage { totalTokens?: number; toolUses?: number; durationMs?: number; tokens?: number; turns?: number; costMicroUsd?: number; } /** * One durable background-agent execution row. Field discipline (design/151 §3.1, HIGH-4): revival * re-hydration data is LOOKUP KEYS ONLY (`agentType`/`model`/`name` — the CC 2.1.216 * `.meta.json` sidecar shape) — never a serialized spec and never closures; a tier-3 revival rebuilds * the spec the same way a fresh named-teammate spawn would (roster/agent definition + the reviver's * deployment assembly). */ export interface BackgroundAgentRecord { /** Row key: the registry handle (`a…` domain), stable across restarts. */ handle: string; /** Tenant/principal scope — put-time REQUIRED (assertOwnership parity: a row with neither a real * scope nor an owner would be world-readable through the fallback; the registry writer always has * both). */ scope: string; /** task-scoped: the host taskId · session-scoped: the sessionId (registry owner semantics). */ owner: string; sessionScoped: boolean; /** The child's store session id — the transcript / resume anchor. Absent at put (the child session * is minted AFTER registration); bound via a follow-up CAS update. */ sessionId?: string; /** Spawn-time root linkage — the durable enumeration key for task-scoped rows * ({@link BackgroundAgentStore.listBySession}) and the restart read leg of the access predicate. */ parentSessionId?: string; /** the ROOT host session of the delegation tree (fixed point down the internals * chain; equals parentSessionId at depth 1). The recovery-face enumeration anchor: after a * restart, intermediate spawner sessions are dead ends — "everything under this host session" * keys on THIS field (listBySession matches it as a second arm), not on an alias walk. */ rootSessionId?: string; parentTaskId?: string; name?: string; agentType?: string; description?: string; /** Revival lookup key (CC meta-sidecar parity); populated when known, never required. The team * key is {@link rootSessionId} — one implicit team per session, no column of its own. */ model?: string; /** design/151 §3.1d (HIGH-3): the WRITING process instance id (random per registry instance). A * fallback read of a `running` row whose writer is not the reading instance serves an HONEST * "outcome unknown here" — never a fabricated "running" liveness claim. * S3 revival ORDER OBLIGATION: a cross-process revival must first CAS this field * over to the reviving instance's writerId (the claim), THEN register-with-id — a fresh lane's * first write against a row still carrying the dead process's writerId reads back "foreign" and * poisons itself by design. Claim-CAS first is the only sequence the adjudication admits. */ writerId: string; /** * design/153 §7.2c (r6 H-3) — the monotonic OWNERSHIP GENERATION, bumped by every ownership * transfer (tier-3 claim, 件3 resume claim finalize, the in-process retained-revive claim). A * ROLLBACK is itself a transfer, so every rollback shape mints a NEW epoch too — the reconciliation * sweep's reservation/running rollbacks, the parked-reservation rollback (which additionally syncs * this process's SURVIVING lane forward so its own later terminal write does not foreignize), and * the revival-claim compensation. No rollback restores a pre-claim epoch: an orphaned lane's * authority must never become resumable, and restoring the old generation would let the next claim * by the SAME writer land on the very (writerId, epoch) pair that lane still holds. What a rollback * restores is the pre-claim writerId and row shape — that is what makes the row addressable again; * the fresh generation is what keeps the abandoned lane from speaking for it. The * durable-lane adopt matches * `writerId + writerEpoch` BOTH: a stale lane of the SAME writer (pre-transfer snapshot still in * its chain) reads "foreign" instead of adopting the new rev and replaying its old record — the * lane-self-clobber ABA the epoch exists to close. Optional: legacy rows (undefined) keep the * pre-epoch writerId-only adopt semantics unchanged. */ writerEpoch?: number; spawnedAt: number; updatedAt: number; settledAt?: number; /** design/153 §7.1: `"parked"` = durably suspended on a pending approval checkpoint (neither live * nor terminal) — reap-exempt like `running`; cleared to a real state by resume claim, TaskStop * (checkpoint arbitration first), or the reconciliation sweep (checkpoint expired/missing). */ status: "running" | "parked" | "completed" | "failed" | "killed"; /** Open enum (task-registry `StopSource` mirror). */ stoppedBy?: string; /** The cross-channel completion correlation id * (task-registry `SemaTaskHandle.completionId` mirror). Written alongside `status`/`settledAt` at * the same durable terminal write that lands them, so a cross-restart fallback read (`TaskOutput` * after the writing process is gone) REPLAYS this exact value — there is no second mint point here, * this field only ever receives what the in-memory handle already minted before the write. */ completionId?: string; /** design/144 §1.1 dedup-key continuity (LOW-15): revival cycles re-mint seq FROM the stored value, * so `task_id:status:seq` keys never collide across restarts. */ seq?: number; /** * design/153 §7.1 — the durable-approval checkpoint token this row is parked ON (`status:"parked"` * only; park mint writes it, resume claim / terminal settle clears it). The checkpoint token CAS is * the MASTER arbitration domain (r4 F-05): every parked-row CAS also verifies THIS binding, and the * row never adjudicates a park outcome on its own. A row CAS that loses after the checkpoint was * committed is compensated by EXPIRING the checkpoint (no "committed but undiscoverable" orphans). */ parkedCheckpointToken?: string; /** design/153 §7.1 — when the park landed (epoch ms). Observability + reconciliation ordering only. */ parkedAt?: number; /** design/153 §7.2 — the in-flight resume claim's compensable-ticket id (parked→running CAS writes * it, a successful attach finalize clears it, a pre-attach failure rolls back BY it). A `running` * row still carrying BOTH this and `parkedCheckpointToken` past the claim lease is a stale claim — * the reconciliation sweep rolls it back to `parked`. */ parkClaimId?: string; summary?: string; finalOutput?: string; /** The FULL result (workflow.ts NH-1 shape via {@link BackgroundAgentTaskHandle.resultFull}), * set only when it differs from `finalOutput` — the restart/durable-fallback poll path needs the same * pairing the live handle carries, or a process restart loses the very recoverability this field exists * to preserve. */ finalOutputFull?: string; /** The child's failure text (registry `handle.error` mirror; the reap-stop sentinel never lands here). * The stale-running reap flip ALSO writes its attribution here (`??`-guarded) — * `error` is the field the durable serving face renders (`serveDurableAgentRow`), while `summary` * has no reader on that face, so an attribution written only to `summary` was invisible: a host * crash read as a bare `failed` + "(no result text)". */ error?: string; /** The classification triplet the in-memory handle carries under * the SAME names (`errorCode`/`errorRetryable`/`errorKind`; see RB-386②/RB-405 G3 on * {@link import("./task-registry-shared.js").BackgroundAgentTaskHandle}), written to this row at the * same terminal write that lands `error` — so a cross-restart fallback read * ({@link import("./task-registry-agent.js").serveDurableAgentRowLane}) can render the identical * classification clause the live poll does, instead of the row structurally losing it once the * writing process exits (the residual this field set closes). */ errorCode?: string; errorRetryable?: boolean; errorKind?: string; errorRetryAfterMs?: number; resultIsPartial?: boolean; recentSteps?: SubagentStep[]; editedFiles?: SubagentEditedFile[]; usage?: BackgroundAgentUsage; /** The org scopes the row's most recent leg actually mounted. ABSENT is NOT "unknown ⇒ * unconstrained": the revival reads an absent field as the ZERO verdict (see * `readDurableOrgAdmission` in memory-admission.ts), so a pre-#22 row and a row whose leg * mounted no org plane both admit nothing on the org axis. Written by the leg itself once its * prepare has adjudicated (task-registry `recordBackgroundAgentOrgAdmission`), so every revival * cycle re-records its own — necessarily narrower — verdict. */ admittedOrgScopes?: string[]; /** The explicit org WRITE grant that verdict carried; `null` = the org write face was read-only. * Same absent-reads-as-zero rule as {@link admittedOrgScopes} (absent ⇒ `null`). */ admittedOrgWriteScope?: string | null; /** Optimistic-concurrency revision. The writer always CASes with its OWN held rev (design/151 * MED-8): a foreign row (stale leftover / another writer) has an unknown rev, so the CAS loses * instead of clobbering. Bumped by the store on every successful `update`. */ rev: number; } /** * The terminal payload a REVIVAL must erase from the row it is re-launching, as ONE list. * * A revived handle starts a fresh cycle on an existing row: status flips back to `running`, but every * field describing how the PREVIOUS cycle ended is still sitting there. Left standing, each one * surfaces the old cycle's answer under the new cycle's status — a stale `finalOutputFull` is served * in preference to the fresh `finalOutput` by the durable read face, a stale `errorCode`/`errorKind` * pair renders a "network / retryable" classification on a row that has since completed cleanly, a * stale `completionId` makes two different completions report one correlation id. * * There are TWO revival entry points — the in-process wake and the cross-process tier-3 claim-CAS — * and they maintained two hand-written copies of this list. The copies drifted: `finalOutputFull` and * the error triplet were added to one and not the other, and each addition was a separately-found * defect of the same shape. One list, both callers, so the next field added to the terminal snapshot * can only be forgotten once. * * Every member is optional on {@link BackgroundAgentRecord} (that is what makes them clearable); * identity, lifecycle, concurrency and GOVERNANCE fields are deliberately absent — a revival * inherits those. The org-admission freeze (`admittedOrgScopes` / `admittedOrgWriteScope`) is in * the governance group on purpose: clearing it would hand the next cycle a blank slate, which is * the widening the freeze exists to prevent. */ export declare const REVIVED_ROW_CLEARED_FIELDS: readonly ["settledAt", "stoppedBy", "completionId", "finalOutput", "finalOutputFull", "error", "errorCode", "errorRetryable", "errorKind", "errorRetryAfterMs", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"]; /** Erase {@link REVIVED_ROW_CLEARED_FIELDS} from a record a revival is about to write back. */ export declare function clearRevivedRowTerminalPayload(record: BackgroundAgentRecord): void; export declare function announceTranscriptIntegrityGapOnce(handle: string, scope: string | undefined, sink: ((handle: string, scope: string | undefined) => void) | undefined): void; /** Content-free projection for list reads (design/151 HIGH-1: `summary`/`finalOutput`/`recentSteps` * and friends NEVER ride a list — content is get-by-handle only, behind the full predicate). */ export interface BackgroundAgentRowSummary { handle: string; owner: string; sessionScoped: boolean; sessionId?: string; parentSessionId?: string; rootSessionId?: string; name?: string; agentType?: string; status: BackgroundAgentRecord["status"]; spawnedAt: number; updatedAt: number; settledAt?: number; usage?: BackgroundAgentUsage; } /** Typed store error so callers branch on `code` (mirrors `WorkflowRunStoreError`). */ export declare class BackgroundAgentStoreError extends Error { readonly code: "agent_record.already_exists"; constructor(code: "agent_record.already_exists", message: string); } /** * design/151 §3.1c — the fallback access predicate (S-1 定谳 class). Mirrors the live registry's * `canAccess` axes with ONE remaining durable-only addition: a task-scoped row is readable by its * PARENT SESSION (`parentSessionId` leg) — after a restart the host session is the row's only * legitimate owner (the spawn-time task ids are dead). The live registry still does not grant the * parentSessionId leg (an intermediate session must not steer a sibling turn's running child); * the ROOT arm, however, is now on BOTH faces — RB-236 (2026-07-29) aligned the live `canAccess` * with this predicate's root-session arm, closing the "running grandchild is not_found to * the session that owns the tree" window. Durable rows remain the settled/read/revive face. * Cross-session same-scope reads stay not_found (content-axis ruling). * * Scope is FAIL-CLOSED both ways (`undefined` never matches) — records are put with a real scope. */ export declare function canAccessAgentRecord(record: Pick<BackgroundAgentRecord, "scope" | "owner" | "sessionScoped" | "parentSessionId" | "rootSessionId">, access: { owner?: string; scope?: string; sessionId?: string; }): boolean; /** * The pluggable durable ledger seam. Contract notes for implementations (file/pg): * - `put` is create-once: throw {@link BackgroundAgentStoreError} `already_exists` on a live key — * never silently overwrite (a leftover row from a dead process must not be clobbered by a * fresh-minted colliding handle; the writer treats that as a loud no-durability degrade). * - `update` is a CAS (`WHERE handle=? AND scope=? AND rev=?`): return false on a missing row, a * scope mismatch (multi-tenant isolation — a wrong-scope update must not win), or a rev mismatch. * On success bump `rev` by EXACTLY 1 and persist (全景 S3c F-3: the tier-3 claim derives the * post-claim rev as `expect.rev + 1` for its lane seed and rollback CAS — a larger bump would * silently downgrade every revival to the adopt-heal slow path and every rollback to the * staleRunning sweep; both bundled impls bump by 1, the contract follows them). * - `get`/`listBySession` are RAW row reads keyed by (handle|session, scope) — the full access * predicate ({@link canAccessAgentRecord}) is applied by the ENGINE consumer (registry fallback), * roster-store posture: a store implementation never widens visibility, and a deployment must not * serve raw rows without the predicate. * - `reap` deletes only what the policy names; a `running` OR `parked` row is deleted by NO policy — * the `staleRunningMaxAgeMs` arm first CAS-flips a stale `running` to `failed` (host interrupted), * then it ages out as a terminal row. A `parked` row is FULLY reap-exempt (design/153 §7.1: its * writer does not heartbeat — the child is durably suspended, silence is its normal state; the * checkpoint-bound reconciliation sweep, not row age, owns its cleanup). With no options, reap is * a no-op (retention is always an explicit policy). * Sizing: a LIVE writer renews its running rows' `updatedAt` every * `DURABLE_AGENT_HEARTBEAT_MS` (60s — task-registry writer lease), so row age measures SILENCE — * of the WRITER PROCESS, specifically: a wedged child inside a healthy * process keeps renewing and never goes stale, by design (in-process supervision owns that case). * Set `staleRunningMaxAgeMs` ≥ 10× the heartbeat (≥10min). A premature flip still self-heals: * the live writer's settle reads the row back, adopts the bumped rev (writerId match) and * re-CASes the real terminal truth over the reaper's guess. * ⚠️ Row deletion does NOT release the child SESSION (the store cannot reach the session store) — * the blessed reap path is the registry-side orchestration (design/151 §3.1 MED-7 + * C-2): it conditionally DELETES the row under its re-verified rev first, and only a WINNING * delete releases the transcript session (release-first destroyed a live transcript when a * concurrent revive won the CAS after the release). Calling raw `reap` alone strands transcripts. * Cross-process caveat: a revive's durable `running` transition rides the async * lane — between the in-memory revive and its flush, a DIFFERENT process's sweep still holds a * terminal-looking row whose rev the flush has not yet bumped. In-process sweeps are gated on the * live handle; cross-process revival (S3) must claim-CAS FIRST (see {@link BackgroundAgentRecord.writerId}). */ /** The stale-running reap flip's attribution, written to BOTH `summary` (legacy * slot, kept for any out-of-repo reader) and `error` (the field {@link canAccessAgentRecord}-gated * durable serving actually renders). One exported constant so the two store implementations (in-memory * here, file twin) can never drift apart (RB-74/RB-111 family). Wording carries CC 2.1.220's three * facts (220:950397): what happened (host process gone mid-run), the consequence (in-process state * lost), and the action (check the worktree/output for partial work before assuming the task landed). */ export declare const STALE_RUNNING_REAP_ATTRIBUTION = "the host process was interrupted while this agent was running \u2014 its in-process state was lost. Check its worktree / output file for partial work before assuming the task landed (stale running row reaped)."; /** * REF-D18: the `updateIf` CAS predicate — the SINGLE shape the interface method and both the * In-Memory and File implementations reference (previously each re-declared it inline). Mechanism * finding this merge closes: a `class X implements Interface` clause does NOT catch a missing * optional field when the implementing method re-declares its own narrower inline param type (TS * method-parameter bivariance) — this exact duplicate-signature-drift class already produced a * shipped double-counting bug in this file's family (RB-74, then RB-87/RB-111 fixed it in one * implementation and not the other, same release; see the file-store twin's own comment). */ export interface BackgroundAgentUpdateIfExpect { rev: number; status?: BackgroundAgentRecord["status"]; parkedCheckpointToken?: string | null; parkClaimId?: string | null; } /** REF-D18: `reap`'s retention-policy options — same single-shape rationale as {@link * BackgroundAgentUpdateIfExpect} above. */ export interface BackgroundAgentReapOptions { /** Delete terminal rows settled longer ago than this. Absent = this arm is not applied. Must be a * finite, non-negative number of milliseconds. */ maxAgeMs?: number; /** Keep at most this many terminal rows, newest first. Absent = this arm is not applied. Must be a * finite, non-negative whole number — see {@link assertBackgroundAgentReapOptions} for why a * malformed value here is the dangerous one. */ keep?: number; /** Flip `running` rows untouched for longer than this to a failed terminal. Absent = not applied. * Must be a finite, non-negative number of milliseconds. */ staleRunningMaxAgeMs?: number; } /** knob-hygiene #98 — refuse a malformed reap policy before any row is touched. Delegates to the one * shared validator ({@link assertRetentionPolicy}) both bundled stores and the sibling mailbox / * workflow-run / roster retention seams run through, so no implementation can drift from another. */ export declare function assertBackgroundAgentReapOptions(opts: BackgroundAgentReapOptions | undefined): void; export interface BackgroundAgentStore { put(record: BackgroundAgentRecord): Promise<void>; get(handle: string, scope: string): Promise<BackgroundAgentRecord | null>; update(handle: string, scope: string, record: BackgroundAgentRecord, expect: { rev: number; }): Promise<boolean>; /** * design/153 §7.2c (r6 H-3) — the GUARDED CAS the park/claim lanes require: atomic * `WHERE rev [AND status] [AND parkedCheckpointToken] [AND parkClaimId]`. The plain `update`'s * rev-only guard is NOT enough there (a rev can move for unrelated reasons between read and * write while the park-critical fields changed meaning — reservation, rollback and finalize all * bind to the FIELD state, not just the revision). Same success semantics as `update` (bump rev * by exactly 1). An `expect` field set to a string matches equality; `null` matches ABSENCE. * REQUIRED method (BREAKING for store implementers, announced): a silently-ignoring legacy * implementation would fail OPEN on the exact CAS the approval chain's safety rests on. */ updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: BackgroundAgentUpdateIfExpect): Promise<boolean>; /** Rows whose durable session anchor (`sessionScoped ? owner : parentSessionId`) OR root anchor * (`rootSessionId` — the whole delegation tree under one host session) equals * `sessionId`, newest-first by `spawnedAt`. Content-free projections (HIGH-1). `opts.status` * filters one state; `opts.limit` keeps the newest N after the sort. */ listBySession(scope: string, sessionId: string, opts?: { status?: BackgroundAgentRecord["status"]; limit?: number; }): Promise<BackgroundAgentRowSummary[]>; /** ALL of a scope's rows (content-free, newest-first): the joint-reap orchestration's * enumeration face (`TaskRegistry.reapDurableAgents` — conditional delete first, session release * only after a winning delete). Deployment-op context, not a model-serving face — * model-facing reads stay behind {@link canAccessAgentRecord}. */ listByScope(scope: string, opts?: { status?: BackgroundAgentRecord["status"]; limit?: number; }): Promise<BackgroundAgentRowSummary[]>; /** row-precise removal (the joint-reap deletion arm). Returns false on a miss. * `expect.rev` makes it CONDITIONAL — a live writer's concurrent revival (heartbeat * adopt bumps rev) must beat the reaper's stale snapshot, so the joint reap always deletes with * the rev it just re-verified. */ delete(handle: string, scope: string, expect?: { rev: number; }): Promise<boolean>; /** Enumerate the scopes that currently hold rows, so a deployment-op * retention loop can drive {@link TaskRegistry.reapDurableAgents} per scope without external * bookkeeping (a local backend under multi-tenant use otherwise grows principal-scoped rows * forever). Optional: a store whose deployment tracks scopes elsewhere (pg: SELECT DISTINCT) * may omit it. Deployment-op face, never model-serving. */ listScopes?(): Promise<string[]>; /** Retention sweep for `scope` (explicit policy, see interface notes). Returns rows deleted+flipped. */ reap(scope: string, now: number, opts?: BackgroundAgentReapOptions): Promise<number>; } /** * design/153 §7.1 (r4 F-03) — the parked-row ↔ checkpoint reconciliation sweep. `parked` rows are * reap-exempt (their writer does not heartbeat), so WITHOUT this sweep an expired/removed approval * checkpoint leaves the row parked forever. Deployment-op face (same posture as * `TaskRegistry.reapDurableAgents`): the deployment calls it on its retention cadence with BOTH * stores. Two arms, both master-arbitrated by the checkpoint (never by row age): * 1. parked rows whose checkpoint is EXPIRED or MISSING → CAS to `failed` (honest terminal, error * names the cause). A `pending` or `resolved` checkpoint leaves the row alone (`resolved` = * a resume is mid-flight — the claim path owns it). * 2. stale claims (r4 F-04): `running` rows still carrying BOTH `parkClaimId` and * `parkedCheckpointToken` past `staleClaimMaxAgeMs` (claimed, then the claimer died before the * attach finalize) → CAS BACK to `parked` iff the checkpoint is still pending (redeemable * again); expired/missing → arm-1 shape (failed). * 3. stranded flip-lost orphans (server cross-probe B1, 2026-07-24): `parked` rows with NO claim * whose checkpoint is RESOLVED (the consume flip never landed) → `failed` (approval consumed, * outcome unknown). ⚠️ Arms 2-3 run ONLY when `staleClaimMaxAgeMs` is supplied (review F-5: * a deployment that never passes it never reaps these shapes), and an in-process caller's own * handles are excluded — the stranded arm therefore needs an OUT-OF-PROCESS sweep (or a later * run after the owning process exits) to fire. * Every write is a rev-CAS — a live writer's concurrent transition wins and the sweep just skips. * * SPLIT-BRAIN FENCE (件1): failing a row whose PARKING PROCESS is still alive would * desynchronize its live handle (poll keeps saying parked, TaskStop mis-reports). An in-process * deployment therefore calls {@link import("./task-registry.js").TaskRegistry.reconcileParkedAgents} * (which handles ITS OWN handles live, then delegates here with `excludeWriterId` = its writerId); * calling this bare is correct only where no engine instance is live for the scope. Residual (multi- * replica shared store, ANOTHER live instance's rows): same documented posture as RB-38③ — that * instance's next durable write CAS-fails and poisons its lane loudly rather than silently diverging. */ export declare function reconcileParkedAgents(stores: { agentStore: BackgroundAgentStore; checkpointStore: Pick<import("./checkpoint-store.js").CheckpointStore, "get">; }, scope: string, now: number, opts?: { staleClaimMaxAgeMs?: number; excludeWriterId?: string; excludeHandles?: ReadonlySet<string>; }): Promise<{ failed: number; rolledBack: number; }>; /** * REF-A3 — the query shape both `listBySession` (with {@link sessionId}) and `listByScope` (without it) * are expressed in. Named rather than inline because it crosses a module boundary (the file backend * imports it), and every field's ABSENCE is a defined "no filter" state rather than a missing value. */ export interface BackgroundAgentQuery { /** Tenant partition key, always in the WHERE — a row in another scope is never visible. */ scope: string; /** * Session-anchored enumeration: a row qualifies when its durable anchor * (`sessionScoped ? owner : parentSessionId`) OR its `rootSessionId` (the whole * delegation tree under one host session) equals this. ABSENT ⇒ scope-wide enumeration. */ sessionId?: string; /** Keep only this status. ABSENT ⇒ every status. */ status?: BackgroundAgentRecord["status"]; /** Keep the newest N, applied AFTER the sort. ABSENT ⇒ no cap. */ limit?: number; } /** * REF-A3 — THE query projection for {@link BackgroundAgentStore.listBySession} / * {@link BackgroundAgentStore.listByScope}: filter → sort newest-first by `spawnedAt` → truncate → * content-free summary (HIGH-1). Pure (no store state, no I/O): it takes any row iterable, so the * in-memory backend passes its `Map` values and the file backend passes the shared ledger's. The two * backends held byte-identical hand copies of this before — the same one-of-two-implementations drift * hazard RB-60 / RB-74 / RB-87 / RB-111 each landed on, on this very surface. * * PUBLIC (a serving layer runs the same filter/sort/truncate the bundled backends do, instead of a * sixth hand copy). It is a projection over rows the CALLER supplied — it neither reads a store nor * authorizes anything: the summaries it returns are content-free but still name handles and session * anchors, so a deployment that hands any of them to an external caller applies * {@link canAccessAgentRecord} first (the {@link BackgroundAgentStore} interface notes state the same * obligation for raw rows: a store never widens visibility, and raw rows are not a serving face). */ export declare function queryBackgroundAgents(rows: Iterable<BackgroundAgentRecord>, query: BackgroundAgentQuery): BackgroundAgentRowSummary[]; /** * In-process reference implementation. NOT default-mounted (divergence ② above) — single-instance * deployments and tests wire it explicitly; it does not survive a restart, so with THIS store the * lifecycle flip buys retained transcripts only for the process lifetime (capability presence ≠ * durability — the `SessionStore.fork` doctrine applies verbatim). `structuredClone` on both * boundaries (no aliasing between the caller's live objects and stored rows). */ export declare class InMemoryBackgroundAgentStore implements BackgroundAgentStore { private rows; private key; put(record: BackgroundAgentRecord): Promise<void>; get(handle: string, scope: string): Promise<BackgroundAgentRecord | null>; update(handle: string, scope: string, record: BackgroundAgentRecord, expect: { rev: number; }): Promise<boolean>; updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: BackgroundAgentUpdateIfExpect): Promise<boolean>; listBySession(scope: string, sessionId: string, opts?: { status?: BackgroundAgentRecord["status"]; limit?: number; }): Promise<BackgroundAgentRowSummary[]>; listByScope(scope: string, opts?: { status?: BackgroundAgentRecord["status"]; limit?: number; }): Promise<BackgroundAgentRowSummary[]>; delete(handle: string, scope: string, expect?: { rev: number; }): Promise<boolean>; listScopes(): Promise<string[]>; reap(scope: string, now: number, opts?: BackgroundAgentReapOptions): Promise<number>; }