@sema-agent/core
Version:
Stateless, task-oriented AI agent core
88 lines (87 loc) • 6.66 kB
TypeScript
/**
* PrepareTask's session acquire+reconcile phase. Acquire the session
* and, when resuming, reconcile any interrupted (orphan) tool call before the turn. Both are retried
* together on a F2 optimistic-lock conflict: a concurrent writer on another instance can move the
* leaf during reconcile's append, and the fix can't be applied to a now-stale in-memory view — so we
* drop it and **re-wake**. Bounded; exhaustion → the conflict propagates and the task fails with
* `errorCode="conflict"` for the caller to retry. The retry loop's WHOLENESS is the T4 invariant
* (resume-at validation + rebranch + reconcile re-run as one unit against the fresh view) — pinned
* in `prepare-task-phase-pins`.
*
* Ownership: on SUCCESS the acquired session's ownership transfers whole
* to the caller (the driver's forgetOnThrow / the Runner's finish own the cleanup from then on);
* every failure path INSIDE this phase drops its own cached view first (`forget`, never `release` —
* audit B-17: release deletes the history on the default store), so a throw here never strands a
* cached view and never hands out a half-acquired resource.
*
* Throws pass through unchanged: `resume.session_not_found` (requireExisting fail-loud mapping),
* the `resume_at.*` target-validation refusals, and — after the bounded re-wake budget — the
* session conflict itself. A throw after a `resumeAt` branch landed first writes that branch back
* (`settleBranchWriteBack`); on success the branch is handed over with the session.
*/
import { StoredSession } from "../session.js";
import type { AcquiredSession, SessionStore } from "../session.js";
import { type RecoveredOrphan } from "../session-reconcile.js";
import type { TaskSpec, ToolEffect } from "../types.js";
import type { PrepareResume } from "./contracts.js";
import { type ResumeAtBranch } from "./branch-writeback.js";
export interface PrepareAcquireReconcileInput {
/** borrowed-mutable (service port) — the deployment's session store. This phase's verbs on it are
* the acquire/forget half of the session resource protocol: `acquire` per attempt, `forget` to
* drop a stale cached view on a lost CAS and on the throw path (never `release` — B-17). */
sessions: SessionStore;
/** borrowed-readonly — the REBOUND spec's four session-addressing fields (窄输入: exactly what the
* slice reads; a new read here must widen this Pick). `sessionId`/`requireExistingSession` drive
* the acquire, `resumeAt`/`resumeAtMode` the §E18 rebranch validation. */
spec: Pick<TaskSpec, "sessionId" | "requireExistingSession" | "resumeAt" | "resumeAtMode">;
/** borrowed-readonly — the durable-resume seats this phase reads: `leafId` (rewind to the
* suspension point BEFORE reconcile) and `suspendedBatch` (wake-reconcile SKIPS those calls —
* they are resumed, not crash-interrupted). Absent for a fresh/non-durable run. */
resume: Pick<PrepareResume, "leafId" | "suspendedBatch"> | undefined;
/** borrowed-readonly here — the safety scan's effect registry (its mutation-owner table lives on
* {@link import("./prepare-safety-scan.js").PrepareSafetyScanResult}); reconcile classifies an
* orphaned call's retry-safety by it. */
toolEffects: Map<string, ToolEffect>;
/** borrowed-readonly — subagent transcript persistence: the trusted spawner's placement
* declaration for THIS run's session ({@link RunInternals.sessionPlacement}), forwarded verbatim
* into the create-form acquire so the store creates the child's transcript into its subagent
* partition. Never combined with `requireExistingSession` semantics: the require-existing form
* re-opens and placement is first-write immutable, so it rides only the create-capable arm. */
placement?: import("../session.js").SessionPlacement;
/** borrowed-readonly — the durable-park TOPOLOGY gap sentence for this
* deployment (`durableParkGapFor`), or `undefined` when the topology is whole / does not apply.
* Read ONLY on the fail-loud missing-session path below, where it turns a symptom into a named
* absent seat; nothing in the phase's control flow depends on it. Computed by the driver (which
* holds `deps`) rather than here, keeping this slice's input the narrow Pick its contract says. */
durableParkGap?: string;
/** borrowed-readonly — the incident channel for this phase's own cleanup legs: a `resumeAt` branch it could not write back, a forget that did not settle. */
report?: (err: Error, sessionId: string) => void;
}
/** The phase's outputs (four-class form). All six are fresh bindings —
* the driver destructures them into consts, so a consumer moved ahead of this call is a lexical
* error, which is the static guarantee the extraction keeps. */
export interface PrepareAcquireReconcileResult {
/** owned (resource handover) — the store's acquired session. Handover happens ONLY on return:
* every throw inside the phase already forgot its own cached view. */
acquired: AcquiredSession;
/** owned — the conflict-proxy-wrapped view over `acquired`'s storage (engine-internal: prepare
* always wraps storage in the concrete built-in — the epoch pin needs its
* append/getPromptEpoch). Aliases `acquired.session`'s storage through the proxy. */
session: StoredSession;
/** owned cell — flipped to `hit: true` by the conflict-detecting proxy installed inside `session`
* whenever a RUN-time write loses the optimistic lock (identity is contract: the proxy closes
* over this exact object; the run loop reads it into `errorCode="conflict"`). */
conflictRef: {
hit: boolean;
};
/** owned — scan-1/A5: what the wake/crash reconcile closed, handed to the run loop so it can mint
* the matching stream frames (see `Prepared.wakeRecovered`). Written by the acquire attempt that
* SUCCEEDS (a conflict-losing attempt re-wakes and re-derives the whole set from the fresh
* branch), and stays empty for a fresh session / a clean one with nothing to reconcile. */
wakeRecovered: RecoveredOrphan[];
/** owned (handed over with the session) — the `resumeAt` branch this phase wrote, absent when it wrote
* none; the driver's throw cleanup writes it back on any later prepare refusal. */
branch: ResumeAtBranch | undefined;
}
/** The B-3 phase body — the acquire+reconcile slice, verbatim (see the module header for the contract). */
export declare function prepareAcquireReconcile(input: PrepareAcquireReconcileInput): Promise<PrepareAcquireReconcileResult>;