@sema-agent/core
Version:
Stateless, task-oriented AI agent core
446 lines (445 loc) • 30.6 kB
TypeScript
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>;
}