@sema-agent/core
Version:
Stateless, task-oriented AI agent core
2,918 lines • 204 kB
TypeScript
/**
* The prepare CONTRACTS — the types that cross between `prepareTask` and the phase modules it drives.
*
* WHY THEY LIVE HERE. Every one of these was declared inside `prepare-task.ts`, the file that PRODUCES
* them, so any module needing to name one had to import its own driver. Thirteen such edges had
* accumulated by the time the dependency-direction gate was written: six phases reaching up for a
* contract type, the vocabulary file reaching up for two more through inline `import()` type
* positions, a phase reaching sideways for a pure path helper, and the orchestrator and its own
* consumer naming each other. A contract belongs BELOW both the producer and the consumer; that is
* all this file is.
*
* It declares TYPES ONLY — nothing is emitted, so it sits on the vocabulary floor beside `types.ts`
* without adding a runtime edge anywhere. The blocks below are verbatim moves, in their original
* order, JSDoc included, so the move stays auditable as a move.
*/
import type { HumanReviewGateRow } from "../task-result.js";
import type { RepairBundle } from "../../agents/repair-loop.js";
import type { SubagentRetainLedger } from "../../agents/retain-ledger.js";
import type { BrainCallGuardrailRef } from "../../brain/timeout.js";
import type { ActiveWorktreeSession, AgentHarness, AgentMessage, AgentTool, AnnouncedListingSnapshotInput, ExecutionEnv, ThinkingLevel } from "../../internal/harness.js";
import type { Model } from "../../internal/llm.js";
import type { WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
import type { ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
import type { CwdRef, ContainmentFace } from "../../tools/fs/index.js";
/** A leg's WRITE face: the face in force (read per call, following the mode's face) and whether a seat or a clamp DECLARED the fence (a checkpoint row records it; a resume keeps it). */
export interface WriteFaceSeat {
readonly inForce: () => ContainmentFace;
readonly fenced: boolean;
}
import type { MaterializedA2a } from "../a2a.js";
import type { CompactionForkContext, CompactionPhaseDurations, MaybeCompactOptions, RapidRefillState } from "../auto-compaction.js";
import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
import type { AutoModeDecider, AutoModeDenialTracker, DenialLimitFallbackFace } from "../auto-mode.js";
import type { SettlementKind } from "../gate-outcome.js";
import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
import type { ActorAssertion, Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
import type { ClearedProjectionLedger, MicroCompactMachine, OccurrenceIndex } from "../context-edit.js";
import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
import type { MaterializedMcp } from "../mcp.js";
import type { MemoryEngine } from "../memory-engine/engine.js";
import type { ResolvedSkillListingBudget } from "../skill-listing-budget.js";
import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
import type { SessionPermissionRules } from "../session-policy-store.js";
import type { SafeNotifier } from "../safe-notify.js";
import type { RecoveredOrphan } from "../session-reconcile.js";
import type { SessionStore, StoredSession } from "../session.js";
import type { TaskOutcome } from "../task-outcome.js";
import type { ModelPricing } from "../pricing.js";
import type { PushQueue } from "../push-queue.js";
import type { SystemInjectionPriority, TaskNotificationPayload } from "../task-notification.js";
import type { OnAsk, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
import type { RemoteExecutionEnv, SnapshotId, WorkspaceHandle } from "../remote-env.js";
import type { ToolDisclosureManifest } from "../trace.js";
import type { AgentDefinition, Brain, CompactOutcome, ModelRef, ModelRoles, NestedUsageAccum, PermissionMode, RunnerDeps, ShellGateDoctrine, TaskEvent, TaskEventIdentity, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
import type { PermissionModeController } from "../mode-policy.js";
import type { WiringManifest } from "../wiring-manifest.js";
import type { SkillGateSeats, SkillGrantLane, SkillGrantRecord, SkillGrantSource } from "../skill-grant.js";
import type { BudgetAxis } from "./assemble-result.js";
import type { GitStatusLaneRef } from "./git-status-frame.js";
import type { SessionReadFileStates } from "./session-read-file-states.js";
import type { LegModeCeiling, SessionModeSeats } from "./session-mode-seats.js";
import type { BlockedRef, OutputRef, SkillListingEntry } from "./synthetic-tools.js";
import type { createAttachmentState, WriteFamily } from "./turn-attachments.js";
import type { cacheFamilyOf } from "./usage-accounting.js";
/**
* The frozen task-start snapshot of the caller's tool-face control arrays: produced by the
* config-doors phase, read by the hands/read-face phase. Named here rather than reached for as
* `PrepareConfigDoorsResult["toolFaceSnapshot"]` — an indexed access into a sibling phase's Result
* is a sideways dependency wearing a type's clothes.
*/
export interface ToolFaceSnapshot {
exclude: readonly string[] | undefined;
defer: readonly string[] | undefined;
alwaysLoad: readonly string[] | undefined;
/** The model-gate restore selector ({@link TaskSpec.restoreGatedTools}), fourth
* seat of the same frozen task-start snapshot: the gate decision and the delegation carrier
* read THIS, never the live spec. */
restoreGated: readonly string[] | true | undefined;
}
/**
* The once-per-session announcement ledger's OPERATOR SINK as the phases see it: the
* `RunnerDeps.onError` signature, deduplicated per (code, text) across the session. The ledger itself
* (load, digests, settle) lives in announce-once-ledger.ts; a phase that announces once invokes only
* this member, so the member's shape is declared here — below every phase — rather than reached for
* sideways as the ledger module's type.
*/
export interface AnnounceOnceSink {
onError: NonNullable<RunnerDeps["onError"]>;
}
/** Static per-tool irreversibility tier, resolved at prepare time by the safety scan. */
export type IrreversibilityTier = Map<string, "never" | "maybe" | "always">;
/** Declared reversibility probes by tool name, collected by the safety scan on the same pass. */
export type ReversibilityProbes = Map<string, NonNullable<TaskSpec["tools"]>[number]["reversibilityProbe"]>;
/**
* The prepare-FAILURE cleanup stack. A phase that acquires a resource (an execution env, an MCP
* client, an A2A client) pushes its release the moment the handle exists, so a throw further down
* cannot leak it — before this the driver could only see a half-acquired handle by hoisting a `let`
* to the top of the function and hoping every acquisition site remembered to assign it.
*
* IT IS ARMED ONLY ON THE FAILURE PATH. The driver calls {@link RollbackStack.commit} before it
* builds `Prepared`: past that point the stack is empty and every resource's cleanup belongs to the
* Runner's finish/teardown, which is where a SUCCESSFUL run's resources have always been released.
* The catch path calls {@link RollbackStack.unwindAll}, which runs the cleanups in reverse order,
* each exactly once, none of them able to stop another — a bounded settle, the same posture the
* teardown leg already uses. NEVER call unwind on the success path.
*
* The detach guard for an owned execution env and the worktree-isolation check stay with the DRIVER: a phase pushes, it does not destroy, because the driver's reference is
* the authoritative one and a phase-local rebind could never reach it.
*/
export interface RollbackStack {
/** Register one cleanup for the failure path. Called immediately after the handle is acquired. */
push(cleanup: () => void | Promise<void>): void;
/** Disarm: prepare succeeded, so ownership passes to `Prepared` and the stack is emptied. */
commit(): void;
/** Failure path only — run every registered cleanup in reverse order, each once, none blocking another. */
unwindAll(): Promise<void>;
}
/**
* The run's view of deployment usage governance, built once at prepare when
* `RunnerDeps.usageWindows` + `usageWindowStore` are both wired (absent otherwise, so every consumer is
* an explicit `!== undefined` test and an ungoverned deployment executes not one extra line).
*
* Both methods are called SEQUENTIALLY by the run loop (entry check, then one commit per turn boundary,
* then one final commit) — the delta bookkeeping in `commit` assumes that and is not a concurrency
* primitive.
*/
export interface UsageGovernance {
/** The ledger key this run is governed under — the principal, or the shared global key. */
readonly key: string;
/** Does any governed window carry a MONEY ceiling? Read by the run loop's pricing seats: only a
* cost-governing deployment has to treat an unevaluable price table as unpriced spend. */
readonly governsCost: boolean;
/** Ms the caller must wait before ANY window would admit work again, or `undefined` when none is
* exhausted as of `now`. Reads the ledger; propagates a store failure (an unreadable ceiling must not
* read as an open one). */
check(now: number): Promise<number | undefined>;
/**
* Charge whatever of the run's cumulative spend has not been charged yet. Takes the run's CUMULATIVE
* totals rather than deltas so no caller can double-charge by calling twice, and so a caller that skips
* a boundary loses nothing.
*
* `cumulativeCostMicroUsd` is the MONEY half (integer micro-USD, `stats.costMicroUsd`). Pass `undefined`
* when the run's spend has no cost figure at all — a deployment governing a
* `maxCostUsd` window then REFUSES here rather than charging the fabricated 0 that would let the ceiling
* silently stop applying. A token-only deployment ignores the argument entirely.
*/
commit(cumulativeTokens: number, cumulativeCostMicroUsd: number | undefined, now: number): Promise<void>;
}
/**
* The run's turn-start file-history BOUNDARY seat. `begin(entryId)` starts the capture
* for the turn's own initial entry (first-wins: exactly one boundary per turn incarnation, DV-4);
* `settle()` awaits the in-flight capture's settle and is the caller's LEASE CLOSE (awaited before
* the first tool executes) as well as the finish-tail durability await. The capture is bounded by a
* 30s timeout whose abort FENCES the attempt (the store's `boundaryPublishVerdict` guarantees a
* timed-out capture never publishes); failure is disclosed via onError(phase:"rewind") and NEVER
* blocks the turn — a turn whose capture failed simply has no boundary (the reference behaves the
* same), and there is no mid-turn re-capture (the store's spent lease refuses it with zero reads).
*/
export interface FileHistoryBoundarySeat {
begin(entryId: string): void;
settle(): Promise<void>;
}
/**
* The commit-side facts of the run's ONE durable pause: the terminal cause the result assembles
* ({@link import("../types.js").PausedCause} — gate, token, checkpoint id, restore mode), the COMMITTED scope
* (read verbatim by the reap/cancel path — never re-derived from the spec, which the cross-scope re-scope
* may no longer match), and the id of the tool call the park is holding (only a `tool_approval` pending
* action parks ON a call; absent, never guessed, for the kinds that hold none).
*/
/** See {@link Prepared.undeliveredContinuationRef} — `parkedSteers` is in ISSUE order, and the wake message rides it with no `seq` (no store minted one). */
export interface UndeliveredContinuation {
parkedSteers: Array<Omit<PendingSteerEntry, "seq"> & {
seq?: number;
}>;
orphanTasks: Array<{
id: string;
description?: string;
}>;
}
export interface CommittedPause {
cause: import("../types.js").PausedCause;
scope: string;
gatedCallId?: string;
}
/** The session's date anchor as prepare read it off the branch. The system-prefix date is frozen per
* SEGMENT — from session start, or from the latest landed compaction, to the next one. */
export interface DateAnchorRead {
/** The anchor written for the current segment; undefined ⇒ the leg renders today and anchors it. */
current: {
prefixDate: string;
announcedDate: string;
} | undefined;
/** The current segment: the nearest compaction entry's id, null before the first compaction. */
segment: string | null;
}
/**
* One engine frame whose commit waits for the receipt of the message carrying it.
*
* @contract frames.commit_on_receipt — an engine-minted frame that rides a user message (the git-status
* frame, an `agent_listing`, `skills_listing` or `date_change` frame) is recorded as announced ONLY when the run loop's
* `message_end` walk sees a persisted user entry whose text equals the carrying text it was registered
* with; every receipt with that text settles on that one entry. Until then nothing is recorded — a
* message that never reaches the transcript (a pre-append failure, a suppressed or undrained steer, a
* leg that ends first) leaves the frame unannounced, and the next boundary or leg renders it again.
* The error asymmetry is one-directional by design: a lost receipt costs a duplicate frame, never a
* fact the model was recorded as told and was not.
*/
export interface FrameReceipt {
/** The carrying message's exact text (its first text block) — the receipt's match key. */
text: string;
/** Which frame this receipt settles. A boundary collector must not render a listing whose receipt is
* still pending (the frame is queued, not yet in the transcript). */
source: "git_status" | "agent_listing" | "skills_listing" | "date_change" | "output_style";
/** Runs inside the `message_end` walk (synchronous, never awaited): advances the run-local record
* and parks any session write for the next serialization point. */
commit: (entryId: string) => void;
}
/** The leg's frame-receipt ledger — see {@link FrameReceipt}. `listingSnapshotOwed` is the parked
* announced-listing snapshot write (a session append from inside the walk would race the loop's own
* next transcript append); the boundary's end and the prompt's settle flush it. */
export interface FrameReceiptLedger {
pending: FrameReceipt[];
listingSnapshotOwed: boolean;
}
/**
* The one closure that stops a run leg for a classifier denial-limit fallback NOBODY JUDGED — the
* synchronous gate's headless arms and the durable resume station both call it, and both reach it
* through {@link Prepared.stopForDenialLimit}. Declared here, beside the seat it fills
* ({@link Prepared.gateStopRef}), so the two stations name one type rather than two.
*
* `settlement` is the kind that ENDED a parked fallback's wait, and is present only on the DURABLE
* station: the synchronous one has no settlement to report (no approver was reached at all), and that
* absence is what selects its clause of the shared sentence.
*/
export type StopForDenialLimit = (info: {
toolName: string;
toolCallId: string;
fallback: DenialLimitFallbackFace;
settlement?: SettlementKind;
}) => void;
/** The gate's seat inputs for one call (the static seat or the `Skill` tool's per-call reader, the run's skill and allow-list rows). */
export type GateSeatInputsFor = (calledName: string, wireName: string) => import("../mode-policy.js").PermissionSeatInputs & SkillGateSeats & import("../allow-entry.js").AllowEntryGateSeats;
export interface Prepared {
harness: AgentHarness;
/** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
* and the epoch pin path needs its `appendPromptEpoch`/`getPromptEpoch`). Public consumer faces take
* the `Session` contract instead. */
session: StoredSession;
sessionId: string;
/**
* THIS INVOCATION's own identity, minted once at the top of {@link prepareTask} and never
* rewritten. The third id of the trio, and the only one the engine owns outright:
* - `spec.taskId` — the HOST's task identity (absent whenever the host names nothing);
* - `sessionId` — the CONVERSATION's identity (shared by every run that continues it);
* - `runId` — THIS call of the engine.
*
* The gap it closes: the repo-wide `spec.taskId ?? sessionId` spelling answers the same value for
* two runs of one session whenever the host supplies no task id, so every per-run account keyed on
* it FOLDED two runs into one row (two runs each losing one steer read as one run losing one).
* A per-run account must key on this; a per-TASK fact (trace `taskId`, the host's own correlation
* handle) must keep the task spelling — the two are different questions and the station sweep
* judged each one.
*
* Minted here (never at the Runner's stream entry) so that a standalone `prepareTask` exercise gets
* a real id too, and so the mint has exactly one site. A leg whose prepare THROWS never returns a
* `Prepared` at all — but the id still reaches its caller through the `runIdSink` carrier, because
* prepare emits run-scoped disclosures of its own and each must have a terminal that names it.
*/
runId: string;
/** The (owner, scope) key this run's registry-facing tools register rows under — (host task id, mount domain), minted once at prepare; the teardown sweeps read it and never re-derive it. */
registryAccess: {
readonly owner: string;
readonly scope: string;
};
/** The session's reminder provenance mark this run mints under (adopted
* through the prepare adoption ladder: checkpoint seed → trusted fork channel → session entry →
* fresh mint). Every engine-authored `<system-reminder>` open tag in the run carries it, and the
* system prompt's Harness declaration names it. Always present on a completed prepare. */
reminderMark: string;
/** The leg's reminder-disclosure trigger counters (mutated by the
* disclosure outlets at tool-execute time; read once at result assembly into
* `stats.mechanisms.reminderDisclosures` when any key is non-zero). Always present. */
reminderDisclosureCounts: import("../reminder-disclosure.js").ReminderDisclosureCounts;
/** Whether the full shell is on this leg's roster (the inherited-gate phase's bit, asserted against the mount). */
fullShellReachable: boolean;
/** The run's approval-posture facts as prepared — the effective permission mode and the ask seam's effective
* answer. The posture composition renders them into the engine's own marked reminders; no gate decision keys on them. */
approvalPosture: import("../auto-mode-directive.js").ApprovalPostureFacts;
/** The output-style frame body, rendered once at prepare from the screened `TaskSpec.outputStyle` (absent = no seat). */
outputStyleFrame?: string;
/** The leg's live permission-mode seat: the effective mode every mode consumer reads, the steer verb writes. */
permissionMode: PermissionModeController;
/** The ISOLATION-AWARE working-tree root for this task (a worktree's cwd when `isolation: "worktree"`, else
* `deps.rootPath ?? executionEnv.cwd`) — the same value the hands/LSP/policy/restore use. The Runner's
* rewind/snapshot path MUST key off THIS, not `deps.rootPath`, or a worktree-isolated turn snapshots the base
* repo (CORE-1~9 audit MAJOR). */
taskRootPath: string;
model: Model;
/** Effective thinking level (explicit `spec.thinking` or the resolved role's default). */
thinking?: ThinkingLevel;
compModel?: Model;
mcp: MaterializedMcp;
/** The run handle's `reconnectMcpServer` implementation — {@link MaterializedMcp.reconnect} plus the
* live-roster application and the operator disclosure, which are runner concerns and not the MCP
* handle's. Present only when this leg DECLARED MCP servers; the stream verb's seat stays unbound
* otherwise and it refuses typed. */
mcpRedial?: (server: string) => Promise<import("../mcp.js").McpReconnectResult>;
/** Present only when `spec.a2a` declared peers — the run loop owes it the same end-of-task dispose
* it owes {@link mcp} (no sockets are held, but a disposed mount is what makes a post-task tool
* call refuse loudly instead of dialing a peer on a dead task's behalf). */
a2a?: MaterializedA2a;
blockedRef: BlockedRef;
/** Holds the structured output once `submit_output` is called (when `spec.outputSchema` is set). */
outputRef: OutputRef;
/** Fires when the task aborts (timeout / max turns / end). Passed to `ToolPolicy.check` so a
* pending human-approval gate is released instead of hanging past the deadline (F4). */
abortController: AbortController;
/** Flipped if any session write lost the optimistic lock during the run → `errorCode = "conflict"`. */
conflictRef: {
hit: boolean;
};
/** Tool-call ids the gate blocked (policy/hook/plan-mode deny) or suspended —
* populated only while a consumer is wired (per-tool post hooks or postToolBatch). The runner's
* batch collector DELETES on match (its tool_execution_end is the only end-event a blocked call
* emits; the tool_result-side delete in prepare-task never fires for immediate results). */
blockedToolCalls: Set<string>;
/**
* WHAT ended the approval a gated call was waiting on and WHOSE settlement it
* was, keyed by tool-call id: written ONLY by the tool gate, at the one exit where an ask resolved,
* and read once when that call's `tool_end` frame is minted (the reader deletes on read; a call the
* gate never settled has no entry, and an entry never names neither fact).
*
* ONE record rather than two parallel maps because they are one observation: an attribution without
* the settlement kind beside it is unreadable ("alice" — approved? her window elapsed?), and two maps
* keyed alike are two chances to drain one and leak the other.
*
* It is a sideband and not a field on the tool RESULT because a result is not a trustworthy carrier
* for this: `details` is arbitrary tool-authored data that post-tool hooks may also replace, so a
* failing tool could stamp a settlement on itself and tell a consumer's audit view that a person
* approved something nobody was ever shown. Provenance has to travel on a channel only the
* adjudicating layer can write. Same reason the entries are keyed by CALL id: the gate adjudicated
* that exact call, and the frame that reads it is that call's own.
*/
gateOutcomes: Map<string, import("../gate-outcome.js").GateOutcome>;
/**
* The parent-thread human-rejection halt fact (see `maybeHumanRejectionHalt`): present from the
* moment a bare human rejection halts the turn's batch until the run ends or the NEXT provider
* request begins (user input continuing the run clears it). Consumers: the runner's stop gate
* (suppress natural-end pushback / final-verify injection — engine continuations must not restart
* a run a person just stopped), the turn-boundary engine steers (same reason), and the result
* stamp (`TaskResult.haltedOnUserRejection` — a human-halted run must not read as an ordinary
* completion). Engine-owned sideband, same trust reasoning as `approvalSettlement` above.
*/
batchHaltRef: {
current?: {
rejectedToolCallId: string;
};
};
/** Summed usage of nested sub-runs (sub-agents) spawned by this task's tools. */
nestedStats: NestedUsageAccum;
/** Prepare-time rewind disclosures (conversation-only branch / no snapshot backend / no file
* env), echoed verbatim onto `TaskResult.rewindNotes`. Present only when there is something to say. */
rewindNotes?: NonNullable<TaskResult["rewindNotes"]>;
/** The run's edited-file ledger read face — what its OWN hands landed, as `TaskResult.editedFiles`
* (undefined when nothing landed: the key is absent, never an empty array). A LIVE reader rather
* than a snapshot, so the throw-path backstop terminal reports the same ledger the ordinary
* assembly would. Always present on Prepared; independent of whether a fileHistoryStore is wired. */
editedFilesSnapshot: () => TaskResult["editedFiles"];
/** The run's turn-start boundary seat (present iff the run records file history into a wired store and
* the run mounts a real fs env). runtask calls begin() at the first committed user entry and
* awaits settle() at the lease close + the finish tail. */
fileHistoryBoundary?: FileHistoryBoundarySeat;
/** The run's RESOLVED read face for the result observation seat
* (`TaskResult.effectiveReadFace`): `carrierReadFace()`'s value at prepare completion — the hands
* block's single resolution, or the hands-less legs' resolver run (live spec-time facts, with the
* checkpoint seed folded stricter-wins where one exists). Every leg that completes prepare has a
* read posture now — this is what its delegation subtree is clamped by even where no faces mount. */
effectiveReadFace?: ContainmentFace;
/** owned — the same observation on the WRITE seat (`TaskResult.effectiveWriteFace`). */
effectiveWriteFace?: ContainmentFace;
/** The normalized deny ADDITIONS in force (deployment ∪ task ∪ checkpoint seed; built-ins
* excluded), echoed on `TaskResult.effectiveReadDenyPatterns`. Present iff non-empty; a defensive
* copy (the wide-scope working array stays the engine's own). */
effectiveReadDenyPatterns?: readonly import("../../tools/fs/read-deny.js").NormalizedReadDenyEntry[];
/** The memory-visibility observation prepareMemory minted (echoed on
* `TaskResult.effectiveMemoryScopes`). Always present on a completed prepare (the memory-less
* states are their own values); the seat is optional only so a Prepared shape without the phase
* cannot fabricate one. */
effectiveMemoryScopes?: import("../types.js").EffectiveMemoryScopes;
/** The per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
* reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
cwdRef?: CwdRef;
/** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
* built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
* The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
* the CELL, not the lane: unbound means "no run body has claimed this band", and emissions no-op. */
toolProgressRef: {
current?: ToolProgressLane;
};
/** The run's advertised-writable-directory seat (present with hands): `roots` is what the prompt advertised as writable and the fence admitted. Read live wherever a policy is adjudicated off a resumed leg, so the write gate's exemption domain is that same list. */
advertisedWritableDirs?: Pick<import("./advertised-writable-dirs.js").AdvertisedWritableDirs, "roots">;
/** The ACTIVE EnterWorktree session ref (mounted with the worktree tools; undefined without
* real write hands). runtask's settle write reads `current` for the workspace-state entry. */
worktreeSessionRef?: {
current?: ActiveWorktreeSession;
};
/** The settle-write base — the canonical root + what the cwd/worktree were RESTORED to at
* prepare. runtask diffs the live refs against this at settle and appends a `workspace_state` entry
* when the state changed (skipped on a durable suspend — the checkpoint lane owns that state). */
workspaceStateSettle?: {
rootCanonical: string;
restoredHandsCwd?: string;
restoredWorktreeDir?: string;
baselineUnknown?: boolean;
};
/** The settle-time session-seat SEAL (see prepare-hands-readface): re-anchors
* the per-session read-state seat to the entry id THIS run last committed (its own
* message_committed tail — provenance, never a shared-leaf re-read). The driver calls it once at
* the run's settle, every terminal (suspend included), under the session lock; undefined argument
* (no commits) is a no-op. Synchronous, never throws. Absent ⇔ hands-less / no seat channel. */
sealReadStateSeat?: (ownCommittedTailEntryId: string | undefined) => void;
/** The DENY-NARROWING layers (session rules, inherited ancestor rules, the frozen constraint projections; deny-only).
* Re-checked on RESUME before an approved pending tool executes, so a rule tightened during the suspend still applies. */
denyNarrowingPolicy?: ToolPolicy;
/** The run's skill grant lane: its deny rows (a skill's `disallowed-tools`) are re-read on RESUME at the same station as the
* person's deny rows, on the bytes that will run. */
skillGrants: SkillGrantLane;
/** The skills this leg loads (normalized, past the load gate; empty when none) — the one list its `Skill` tool activates from, its park binds and the resumed execution boundary judges a parked Skill approval against. */
skillSpecs: readonly SkillGrantSource[];
/** The person's own persisted-rule lane (deny/ask rows), re-adjudicated on RESUME at the same
* station as the deny-narrowing layers: a deny row added while the call was parked vetoes the execution
* (`deniedBy: "persisted_rule"`), and so does a call the lane could not read against the rows (re-issue and
* re-approve); an ask row does not (the approval is its answer), and a store that could not be read
* contributes no row. Absent when no rule store is wired. */
persistedRuleLane?: {
adjudicate: (req: ToolCallRequest) => Promise<import("../hooks.js").PersistedRuleAnswer>;
};
/** The deployment's caller policy, re-checked on a durable resume when the bytes to execute are not the bytes the gate saw. */
basePolicyForResumeEdit?: ToolPolicy;
approvedCallScreen?: (call: {
toolCallId: string;
toolName: string;
args: unknown;
decidedBy: "person" | "sla_timeout" | "policy";
}) => Promise<{
readonly refused: string;
} | {
readonly args: unknown;
readonly admit: () => void;
}>;
/** The leg's shell READ STATION, re-run on a durable RESUME before an approved shell call executes: the parked leg's
* admission died with that leg, so the resumed bytes are judged (and admitted for their pre-spawn re-establishment)
* on THIS leg — a deny row found now vetoes, an operator's edit is judged on the `rejudge` seat. */
shellReadCheck?: import("../../tools/fs/shell-read-check.js").ShellReadCheck;
/** The ORG adjudication face, re-resolved on a durable RESUME before an approved
* pending call executes. The resume path bypasses the harness gate by design (a human already
* adjudicated the checkpointed call), which is exactly where org policy skew is most likely: the
* suspend may have outlived the snapshot revision that was current when it was minted. Present only
* on a governed deployment. */
permissionRuleOrg?: {
adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
};
/** Removes the `spec.signal` abort listener on task end (else a long-lived signal leaks listeners). */
releaseSignal: () => void;
/** The leg has stopped: its children's mode ceiling moves to the session's current leg (called once its loop settled). */
endModeCeiling: () => void;
/**
* End-of-leg sweep for questions this leg's gate routed to a person. Called once by the
* run loop at task end, on every exit path. A binding that is still present means a person ANSWERED and
* the call never executed to collect it (aborted, batch torn down, loop threw): that answer is disclosed
* through the deployment's error sink rather than dropped, because "a human answered and it vanished" is
* precisely the failure this whole path exists to remove — it must not reappear inside the mechanism
* that removes it. Idempotent, never throws.
*/
settleContentAskBindings: () => ReadonlyArray<{
deliveryId: string;
toolCallId: string;
}>;
/** Per-task prefix-cache-break detector. The fingerprint is mutated when deferred tools
* materialize — see `cacheFingerprint`. */
cacheBreakDetector?: CacheBreakDetector;
/** The fingerprinted prefix. `systemPrompt` is stable; `tools` is REFRESHED in place when a deferred
* tool is materialized (placeholder→full schema), so the cache-break detector sees the real tool set. */
cacheFingerprint?: {
systemPrompt: string;
tools: ToolFingerprintInput[];
};
/**
* 提示词主权批 — the labelled composition of the assembled system prompt, emitted as the
* `prompt.assembled` trace event at task start. `constitution` says who owned the safety layer:
* `"core"` (structural, the default), `"replaced"` (provider set `replaceAll` — deliberate opt-out),
* `"provider-assembled"` (migration guard: a historic provider returned a full prompt, passed through
* un-doubled + onError hint). Deployments/tests assert on this to make "which blocks were in the
* prompt" a runtime fact instead of archaeology.
*/
/**
* This leg's EFFECTIVE wiring manifest (built once in prepare from resolved
* facts through the same derivation as the static half). runtask emits it as the
* `wiring_manifest` TaskEvent after the prepare consume point and before the leg's first
* model/tool interaction. Host/operator plane — never enters model context.
*/
wiringManifest: WiringManifest;
/**
* This leg's frozen identity envelope, minted ONCE in prepare beside the wiring
* manifest (same leg derivation, one mint — {@link mintHookInvocationIdentity}). Every hook
* station runtask drives (stop/stopFailure/userPromptSubmit/postToolBatch, the compaction
* wrapper) and the 件B delegation-lifecycle observer read THIS object; prepare's own stations
* (the tool gate, the post-tool contexts) close over the same const. Always present — a prepared
* leg always knows its identity.
*/
hookIdentity: HookInvocationIdentity;
/**
* This session's DURABLE box, present iff this leg mounted the session-box drain (a top-level leg
* over a cross-process-safe mailbox with a grammatical session id). The notification lane binds it
* to the park seat ({@link import("../task-notification.js").PendingSessionNotifications.bindSessionBox})
* so a child's uplink frame parked for this session while it is idle lands in the box the next run
* drains — one mount judgment, the drain and the park read it alike. Absent ⇒ in-process parking.
*/
sessionBox?: import("../task-notification.js").SessionBoxBinding;
/**
* #1036 — the leg's TWO SETTLEMENT LEDGERS, one cell shared by the three beats that fill and
* read them: the harness's transcript-append sink writes receipts, the compaction commit beat
* writes effects, and the session-box drain's settlement half-pass reads both and clears what it
* consumed. Always present (the cell is minted with the leg); empty on a run with no session box,
* because nothing registers an interest and the receipt sink only records awaited keys.
*/
frameSettlement: import("../frame-settlement.js").FrameSettlement;
/**
* #1036 — the session-box drain's BOUNDARY PASS, called by the turn-boundary lane as its tail
* (in a `finally`, so every early exit of the boundary still settles). Present iff this leg bound
* the drain. `continues` = this leg will issue another request after this boundary, which is what
* decides whether the pass's NEW-delivery half runs; the settlement half runs either way.
*
* It is a tail call and not a `turn_boundary` hook of the drain's own because the ORDER matters and
* a hook's position is registration-order luck: settlement must run AFTER this boundary's
* compaction, or a record whose frame the compaction just summarized away is already acked (deleted
* from the box) by the time anyone could notice.
*/
peerBoundaryRound?: (o: {
continues: boolean;
}) => Promise<void>;
/**
* The per-invocation TIME BOUND every hook seat of this leg runs under (`Hooks.timeoutMs`, already
* validated — a garbage value was refused to the default and disclosed ONCE, here, rather than on
* every tool call). Published for the same reason `hookIdentity` is: the seats live in two files, and
* a number each station re-derived would be two answers to one wiring question — including two
* chances to re-refuse the same bad value.
*/
hookTimeoutMs: number;
promptManifest: {
constitution: "core" | "replaced" | "provider-assembled";
blocks: Array<{
id: string;
chars: number;
hash: string;
}>;
/** Manifest v2 (stage S1, additive): the compiled-IR section view (slot/carrier/cadence/
* cacheClass per section). `contentHash` is the
* digest-tiering face: present ONLY on operator-declared sections (the typed
* `stableBlocks` lane, owner "deployment") as an UNSALTED full sha256 of the section text —
* the center-publish ↔ manifest end-to-end reconciliation anchor. Core-native sections (env
* facts, memory tail, user role text) stay salted-only: a plain hash of a short guessable
* block is a dictionary-preimage surface for trace readers ([E]). */
sections?: Array<{
id: string;
slot: string;
carrier: string;
cadence: string;
cacheClass: string;
chars: number;
hash: string;
contentHash?: string;
}>;
/** S2 (additive): the mounted tools plane — contract identity, durable-replay aliases,
* presentation-invariant shape digest and full wire digest per tool (initial mount face). */
tools?: ToolManifestRow[];
/** The nine-element cache identity — digests only, no prompt
* text; backfilled once the final wire tool list exists. Live post-refresh truth rides
* `prepared.turnSnapshot`. */
snapshot?: {
cacheIdentity: string;
elements: import("../../prompt-assembly/turn-snapshot.js").CacheIdentityElements;
};
/** The run's lowering record (version/wire form/known
* intentional divergences — run-static v1, one brain serves every call). */
lowering?: import("../../prompt-assembly/turn-snapshot.js").LoweringRecord;
/** The RESOLVED deferred-tool disclosure strategy for this leg and which
* seat chose it. Absent when nothing is deferred (there is no disclosure to describe). */
toolDisclosure?: ToolDisclosureManifest;
};
/** This run's provider-declared prompt sections (epoch declaration axis),
* threaded to every compaction-boundary epoch selection so boundary re-pins hash the same
* artifact identity as the prepare-time pin. Empty on declaration-free runs. */
epochDeclaredSections: import("../../prompt-assembly/epoch.js").EpochDeclaredSections;
/** The LIVE nine-element identity face (refreshed by deferred materialization and
* the deferred-materialization adoption swap); the manifest holds the prepare-time initial copy. */
turnSnapshot?: import("../../prompt-assembly/turn-snapshot.js").TurnPromptSnapshot;
/** The compaction-boundary center-adoption seam (undefined = no source
* wired or session not center-pinned). Returns the maybeCompact `centerAdoption` fragment or
* undefined; `apply` runs post-CAS only (auto-compaction owns the ordering). Both
* `epochDeclaredSections` and `promptOverheadTokens` on THIS object are mutated by a successful
* adoption (read them at call time, never capture early). */
centerCompactionCandidate?: () => {
centerArtifactDigest?: string;
sourceRevision?: string;
clear?: true;
declaredSections: import("../../prompt-assembly/epoch.js").EpochDeclaredSections;
overheadTokensAfter: number;
apply: (committedArtifactDigest?: string) => void;
} | undefined;
/** Deferred-tool disclosure: the monotonic set of activated deferred-tool names. Empty
* (and no `tool_search` injected) when nothing is deferred. Mutated by BOTH disclosure lanes across
* the run — the injected `tool_search` and the direct-call placeholder lane (default ON) —
* under ONE shared activation critical section, plus resume seeding at prepare. A leg on which the
* model never calls `tool_search` is therefore NOT quiescent. */
activeTools: Set<string>;
/** The DECLARED deferred set (populated by `classifyDeferred`, ghost names already pruned).
* Present iff the task has deferred tools at all; `deferredToolNames − activeTools` is the
* "undiscovered" census the tool_search_usage_reminder lane enumerates. Frozen at prepare time: a
* name never leaves the deferred set, it only moves into `activeTools`. */
deferredToolNames?: ReadonlySet<string>;
/** The deferral census seat, present iff {@link deferredToolNames} is. */ toolDisclosure?: import("./deferral-census.js").ToolDisclosureSeat;
/** The RESOLVED materialization strategy this run runs under (`true` = "static":
* activation never swaps the placeholder; the real schema's only in-context carrier is the
* ToolSearch result text). Consumed by the tools_delta boundary renderer, whose "full schemas are
* loaded" wording is a statement about the tools block and is only true under swap. `false` when
* nothing is deferred (the knob is inert then). */
toolMaterializeStatic: boolean;
/** Is the direct-call lane mounted on this run's placeholders (`TaskSpec.deferSelfResolve`
* not disabled; default ON)? The SAME fact `createToolSearchTool` receives as `directCallEnabled`,
* surfaced here so every model-facing face that describes what happens to a call on a
* still-deferred tool reads ONE value — under the direct lane a schema-valid call executes rather
* than failing, so an absolute failure claim is only true for the opt-out posture. `false` when
* nothing is deferred (no placeholders, so the fact is inert). */
deferDirectCall: boolean;
/** Is THIS deferred name on the static face? False under `"swap"`, and false under
* `"static"` for a tool whose declaration the result-text carrier cannot hold (the per-tool
* exemption: it materializes into the tools block instead). Read at call time — the roster is
* mutated in place by MCP refresh, so renderability is a live fact. Absent when nothing is
* deferred; a caller with no accessor has no deferred family to describe. */
staticFaceFor?: (name: string) => boolean;
/**
* The MemoryEngine session. Present when `deps.memoryBackend` + `spec.memory.enabled`
* hold AND the engine mount succeeded: a materialize failure without a `config.memory_*` code is
* fail-open (reported via `deps.onError`, the task runs memory-less), leaving this absent even though
* both flags hold.
* `harvest` is the swallow-guarded boundary hook (task terminal in runtask + the checkpoint mint
* point in commitSuspendSaga): it runs the FULL gate set (containment/secret/caps/deletion fuse),
* commits entry patches to the backend, self-heals the derived index, and re-baselines (a second
* harvest of an unchanged session yields zero patches). It NEVER throws.
* S2-B (O-F7/C-F3): the report is NOT discarded — `harvest` hands it to the Runner-owned
* `deps.onMemoryHarvestReport` callback (with the boundary phase), and the engine itself enqueues
* the announcement-queue entry at its harvest tail (drained into the NEXT session's first inject —
* 时机①; the r1 in-run attachments lane was cut, O-F2/C-F4). `phase` defaults to "terminal" (the
* runtask call site passes nothing); the checkpoint-mint call site passes "checkpoint".
*/
memoryEngineSession?: {
/** The WRITE-plane pair (dual roots collapse to one session face; these are the
* plane that owns the write gate — the read-only plane is internal to `inject`/`harvest`). */
engine: MemoryEngine;
handle: MemorySessionHandle;
/** Merged injection across all planes (single-root sessions: identical to engine.inject(handle)). */
inject: () => import("../memory-engine/engine.js").MemoryInjection;
harvest: (phase?: "checkpoint" | "terminal") => Promise<HarvestReport | undefined>;
/** End this mount's session leg WITHOUT a harvest: the run that mounted it will never reach its
* terminal harvest (a prepare that failed after the mount). Never throws. */
release: () => void;
/**
* The session's capture opt-out face (the `TaskStream.optOutMemoryCapture`
* verb's target; host-API only — no tool face reads it, §2.6). `flip` re-adjudicates the
* entitlement at call time (fresh caps resolve), mints the one-way record, runs the §2.3
* boundary sweep, and delivers the effectiveness notice; its typed refusals are the
* `memory.capture_optout_*` family. `optedOut` is the live state read (declared / standing /
* flipped) — a GENUINE-record read: a store fault answers false here and TRUE on
* `indeterminate` instead: the fault must reach the delegation floor
* as its own third state, never as either boolean; an irreversible record is never minted off
* an unreadable state, and an outage must not spawn un-floored children. Present whenever
* the memory session mounted.
*/
captureOptOut?: {
/** Dual form: a sync capture store answers synchronously; a Promise-form store answers a Promise. `await` is correct on either arm. */
optedOut: () => boolean | Promise<boolean>;
indeterminate: () => boolean | Promise<boolean>;
flip: (reason?: string) => Promise<{
outcome: "created" | "existed";
}>;
};
/**
* The session's ONE-WAY pollution face. `markPolluted` fires when an
* external-content-class tool is invoked (the prepare-time tool wrap below is the caller);
* durable + in-process, no unmark exists at any layer. `polluted` is what the harvest legs read
* (they consult it at harvest time themselves — this accessor serves observers/tests).
*/
pollution: {
polluted: () => {
at: number;
reason: string;
cause?: import("../memory-engine/types.js").MemoryOriginCause;
} | undefined;
/** `cause` is the structured mechanical fact the mark records (and the
* origin marker minted off it carries): `"observed"` for a witnessed external event,
* `"static"` for the capability over-approximation, `"derived"` for recall-taint. Absent
* folds to `"observed"` at mint time (the honest floor). */
markPolluted: (reason: string, cause?: import("../memory-engine/types.js").MemoryOriginCause) => void;
};
/** The task's content-safety config (normalized memory spec): the allowlist for
* UNDECLARED tools and the strict execution-class upgrade. Consumed by the tool wrap only. */
contentSafety: {
trustedTools: ReadonlySet<string>;
execIsExternalContent: boolean;
};
/**
* The delegation-settlement handle: the control-plane coordinates a
* settlement writer needs, as PURE DATA. Consumers (the subagent background leg's write-ahead
* + terminal observation, the tool wrap's sync unattestable row) can outlive this prepared
* leg, so they rebuild their write handle from these fields alone — never from the live
* engine/session objects above. Present only under `memoryProvenance: "carry"` (the default):
* an `"off"` deployment keeps the pre-336 accepted-cost posture byte-level.
*/
settlement?: {
/** The WRITE plane's control-plane dir (the pollution-marker/lineage sidecar home). */
controlDir: string;
sessionId: string;
provenance: "carry";
};
/**
* The Read-tool recall-taint judgment, present only under
* `memoryProvenance: "carry"`. True ⇔ the delivered ABSOLUTE path sits inside a mounted memory
* plane and its head bytes carry a committed external-origin marker; the tool wrap then marks
* the session derived (same seat and cause as the memory_get propagation). Never throws;
* relative paths and unreadable files answer false (named residuals beside the Bash channel).
*/
recallTaint?: {
judgeDeliveredPath: (absPath: string) => boolean;
};
};
/** A per-task env minted by `RunnerDeps.executionEnvFactory` (the remote seam) that THIS task owns
* and the Runner must tear down on task end. Undefined when the env came from a (caller-owned) static
* `deps.executionEnv` or the stub — those outlive the task and must NOT be destroyed here. */
ownedEnv?: ExecutionEnv;
/** The run's ONE committed durable pause ({@link CommittedPause}): a mutable holder the commit-side
* publisher (`publishCommittedSuspend`) writes exactly once per run when a park commits — an approval
* or content ask, a resource slice, a platform pause, a plan or dry-run review — and the run loop reads
* to assemble the result's `terminal: { kind: "paused", … }`. WHICH pause it is is the cause's own
* `gate.kind` (a registry row), not the holder it landed in: the two same-shaped holders that used to
* split the approval family from the review family, and the commit-side routing between them, are
* gone. `current` is absent until a park commits. */
pausedRef: {
current?: CommittedPause;
};
/** A resumed leg's continuation content that has NOT reached the transcript yet: EVERY input frame the
* prompt carries (parked steers, the caller's wake message) and the orphaned background tasks whose obituary
* rides it. Set before the prompt, emptied at delivery; a re-park carries it forward (the saga folds the
* orphans in, the adoption re-parks the frames), and what no re-park carries the undrained account announces. */
undeliveredContinuationRef: {
current: UndeliveredContinuation;
};
/** Ruled 2026-08-05 (matrix ruling arm A): set true by the resume engine when this leg EXECUTES the
* approved pending call — the restart-loop cap then counts from a fresh base (consecutive
* no-progress suspends only). See `suspendChainBase`. */
suspendProgressRef: {
executedApproved: boolean;
};
/** The remote-workspace lifecycle failures this run hit, appended in call order and echoed
* verbatim on `TaskResult.remoteEnvFailures`. A shared array (not a per-call return) because a suspend
* refusal is reported through the deployment's `onError` side channel and the run then continues or
* stops by its own rules — without this collector the caller's result kept no trace that a durable
* suspend was even attempted, let alone which of the eleven codes refused it. Empty unless something
* failed. Resume-leg failures do NOT land here (that leg throws out of prepare before a `Prepared`
* exists) — they ride the thrown error's `remoteEnvFailure` carrier instead. */
remoteEnvFailures: NonNullable<TaskResult["remoteEnvFailures"]>;
/** Set when a suspend was REFUSED because the task already suspended `maxSuspends`
* times (a resume/restart loop) — the run is aborted and assembles as `failed`/`suspend.loop` instead
* of minting yet another checkpoint. */
suspendLoopRef: {
hit: boolean;
};
/** Opt-in resource-slice suspend. Present (≠ undefined) ONLY when the task opted in
* (`spec.resourceSuspend`) AND it is eligible to suspend durably (a checkpoint store, durable tool
* results, and a remote — or static caller-owned, never per-task-stub — env). The run loop calls it at a
* CLEAN turn boundary when a resource limit (turns/budget/walltime) was hit: it mints a `resource_limit`
* checkpoint + pauses the workspace + stops the loop cleanly (NOT abort). Returns true iff it committed a
* resumable checkpoint (publishes the committed pause); false ⇒ caller falls through to normal limit handling.
* `sliceSpend` (Slice 4) is THIS slice's cost/tokens/turns, debited onto the cross-slice ledger. */
suspendForResource?: (reason: ResourceLimitReason, sliceSpend: {
costMicroUsd: number;
tokens: number;
turns: number;
walltimeMs: number;
}) => Promise<boolean>;
/** The PLATFORM-cause suspend — the same commit saga as {@link suspendForResource},
* exposed on the INFRASTRUCTURE alone (checkpoint store + durable tool results + remote/no owned env),
* WITHOUT the `spec.resourceSuspend` opt-in. The run loop calls it at a clean turn boundary when the
* execution environment is about to be reclaimed (`env_lifetime`) or a deployment usage window is
* exhausted (`usage_window`) — causes the task did not choose and cannot ask differently for, so a
* deployment that CAN keep the work has no reason to be asked whether it wants to. `hint.resumeAfterMs`
* (usage windows only) rides the gate and extends the checkpoint deadline. Returns true iff it committed
* a resumable checkpoint; false ⇒ the caller stops the run LOUDLY with the cause's terminal code. */
suspendForPlatformLimit?: (reason: PlatformLimitReason, sliceSpend: {
costMicroUsd: number;
tokens: number;
turns: number;
walltimeMs: number;
}, hint?: {
resumeAfterMs: number;
}) => Promise<boolean>;
/** Epoch ms at which this run must stop and checkpoint because the EXECUTION
* ENVIRONMENT's declared lifetime is about to expire (`ExecutionEnv.lifetimeMs` minus
* {@link ENV_LIFETIME_SUSPEND_MARGIN_MS}). Undefined ⇒ the env declared no lifetime (every local env,
* and any adapter on an unbounded host) or declared one the engine could not anchor — in both cases the
* boundary check is dead code. Epoch, not monotonic: the anchor
* comes from the PLATFORM (an env can be older than this process), which is a wall-clock fact and the
* one domain both sides can name. */
envLifetimeSuspendAt?: number;
/** Deployment usage governance for this run, or undefined when the deployment wired
* none (or wired windows without a ledger, which is reported and not enforced). */
usageGovernance?: UsageGovernance;
/** 1.296 件2b — epoch ms at which the loop ISSUED the current provider call (stamped by the
* per-call stall-watchdog closure, consumed+cleared by runtask's brain.call trace row as
* `callStartedAt`). Always present. */
callIssuedAtRef: {
current?: number;
};
/** Was the run's WORK cut by a stop? Two observation points, one record, read by the answer-settled
* verdict (`answerSettled`) beside the harness's own undrained-input account:
* - `continuation` — the loop decided to CONTINUE past its last assistant turn and the abort caught it
* before that continuation dispatched. Written by the loop-trace sink from the loop's own steps: a
* `continue` step (a drained steer / follow-up, a truncated-output or thinking-only or degenerate-output
* nudge, a next tool turn) sets it; the terminal step clears it unless the loop exited at its
* pre-dispatch abort guard (`aborted_before_stream`) — the one exit where `final` is still the PREVIOUS
* turn's message. A clean "stop" the loop itself had decided was not the end is not a settled answer.
* - `abortedBeforeTurnEnd` — the run's abort was already in force when a turn ENDED (written by the
* `turn_end` handler before the turns cap fires its own abort). The ruling's line is the last
* `turn_end`: a stop that landed before it landed on work — even when the brain ignored the signal
* and delivered a full "stop" answer through it (the loop then ends at its own decision, so the
* continuation bit never sets); a stop that landed after it landed on the tail.
* Always present; both false on a run nothing stopped. */
workCutRef: {
continuation: boolean;
abortedBeforeTurnEnd: boolean;
};
/** Records the FIRST brain call this run's outer guardrail gave up on (see
* {@link import("../../brain/timeout.js").withBrainCallGuardrail}). Always present; `timedOut`
* stays absent unless the guardrail fired. The run loop reads it AFTER the loop settles and gives
* the task the typed terminal — the harness turns a loop throw into an error assistant message, so
* without this the cause would reach the caller only as the generic `provider.error`. */
brainCallGuardrailRef: BrainCallGuardrailRef;
/**
* The tool gate's own TYPED STOP: set (once) when the classifier denial limit fell back to a person
* and nobody judged it — no approver to fall back to (headless), or a DURABLE park whose decide
* settled with a word the nobody-judged table reads as such — together with the run abort. The run
* loop adopts it as the terminal `threw` (`TaskResult.errorCode` = the error's `code`,
* `errorMessage` = its sentence) the same way it adopts the brain-call guardrail's — a loop that
* ended because THIS lane aborted it must report the cause, not the consequence. The abort-result
* details seam reads it too, so the aborted call's own `tool_end` carries the code. `undefined` ⇒ no
* gate stop happened.
*/
gateStopRef: {
terminal?: Error & {
code: string;
};
};
/** The ONE closure that fills {@link Prepared.gateStopRef} (minted by `createDenialLimitStop`),
* carried on `Prepared` so the DURABLE resume station reaches the same seat, the same sentence
* composer and the same notice as the synchronous gate's arms — the second station of one rule, not
* a second rule. */
stopForDenialLimit: StopForDenialLimit;
/** Set by a tool calling `ctx.requestReview()` (the first-party `present_plan` tool, CC
* ExitPlanMode parity): the run loop honors it at the next CLEAN turn boundary by minting a `plan_review`
* checkpoint. `{ pending }` is set (with an optional reason) the moment a tool requests review; the boundary
* reads + clears it. First request in a batch wins (idempotent). */
reviewRequestRef: {
pending?: {
reason?: string;
};
};
/** Present (≠ undefined) ONLY when a `checkpointStore` is wired (the deployment can pause). The
* run loop calls it at a CLEAN turn boundary when `reviewRequestRef.pending` is set: it mints a `plan_review`
* checkpoint (`status:"needs_review"` by its registry row) + pauses the workspace + aborts the loop, reusing
* the SAME commit saga as the human/resource suspends. Returns true iff it committed a resumable checkpoint;
* false ⇒ the request could not be honored (caller drops it and continues). */
suspendForReview?: (reason?: string) => Promise<boolean>;
/** The prior cross-slice {@link ResourceLedger} (from the resumed checkpoint), so the run
* loop can size this slice's effective budget = `min(maxCostUsd, remaining)`. Undefined on the first slice
* (or a non-resource task). */
resourceLedger?: ResourceLedger;
/** A mutable holder the run loop populates (right after `stats` exists) so the
* human/irreversible_ask suspend can debit THIS leg's live cumulative spend onto the durable approval
* ledger it attaches (the resource-slice path passes `sliceSpend` explicitly; this event-driven gate has
* no such arg, so it reads the live spend here). Read at suspend time; absent ⇒ this leg's spend is not
* debited (the prior ledger still rides for the cross-leg READ). */
liveSpendRef: {
get?: () => {
costMicroUsd: number;
tokens: number;
turns: number;
walltimeMs: number;
};
};
/** The per-task human-review accumulator (synchronous `resolveAsk` waits this leg + the carried
* prior-leg burden seeded from the resumed checkpoint). The run loop ADDS the durable-resume latency
* (`now() − cp.suspendedAt`) on a resume, then surfaces it as `stats.humanReview` at assembly (omitted when
* empty). **Budget-EXCLUDED** — never folded into cost/the budget gate. */
/** borrowed-mutable — the hands' read-state map (the SAME `Map` object the toolkit and the context
* lane hold; absent without hands). Read by the provider-rejection recovery arm so its stale-result
* clears retire the entries those transmissions fed, through the same landing the frontier pass uses. */
readFileState?: import("../../tools/fs/safety.js").ReadFileState;
humanReviewRef: {
count: number;
totalWaitMs: number;
gates: HumanReviewGateRow[];
};
/** The injectable wall-clock the run loop uses for the durable-resume human-review latency
* (`humanLatencyMs = now() − cp.suspendedAt`), so it reads the SAME clock as the suspend-side `suspendedAt`. */
now: () => number;
/** The FULL resolved tool list (real tools, never deferred placeholders) so the
* resume engine can execute a previously-suspended pending tool call directly (it bypasses the gate —
* the human already adjudicated it). Same wrapping (offload + ctx) the harness runs with.
*
* ONE array, mutated in place; identity is the contract (every closure that resolves a name at run time
* reads this exact array). The array is PRIVATE to the `RosterBuilder` (tool-roster.ts) —
* every station below writes through its verbs (`mount` / `mountAll` / `unmountWhere` / `replaceAt` /
* `replaceDomain`) and this field is the builder's read face. WRITER TABLE — the only
* stations that write it, in prepare order:
* · caps-and-workflow — MINTS it (`spec.tools` through the ctx wrap + the large-result wrapper), then pushes
* ReportBlocked (enableBlockedReport ≠ false), ReportFindings (no caller tool of that name/alias),
* ExitPlanMode + EnterPlanMode (enablePlanMode ∧ the plan-review face), Workflow (self-orchestration active
* ∧ runner self ∧ hard sandbox ∧ governance baseline);
* · the driver — pushes StructuredOutput (outputSchema);
* · protocol-tools — pushes the MCP tools (through the remote wrapper), RefreshMcpTools (servers declared),
* the A2A tools; at RUN time its refresh closure splices a server's name-prefix domain out and back in;
* · hands mount — pushes the hands band (env wired);
* · delegation-surface — pushes TaskOutput/TaskStop (background ∨ workflow door), SendMessage (door ∨
* parentNotify ∨ peer lane, ∧ runner self ∧ no shadow), AgentTranscript (door ∧ runner self ∧ no shadow),
* ListAgents (peer lane ∧ built-in SendMessage ∧ mountable), Monitor (background), EnterWorktree +
* ExitWorktree (tracked cwd), ReadToolResult (offload store);
* · question-face — pushes AskUserQuestion (the mount decision);
* · the driver — pushes CronCreate/CronDelete/CronList (write hands; inert without a daemon);
* · lsp — pushes the LSP tool (manager wired);
* · project-context — pushes Skill (manifest), the shared-memory pair and the memory-engine tools;
* · defer-classify — SPLICES OUT the excluded names and the retracted pair (the only removals at prepare);
* · memory-engine-session — REPLACES entries in place with the content-origin wrappers when it arms (a mounted
* memory session or a recorder channel); the refresh seam re-runs the same pass through `contentOriginWrapRef`
* after a re-splice (identity-tracked, so a wrapper is never wrapped again).
* tool-disclosure and listings only READ it (`harnessTools` is a derived list; placeholders never enter here). */
tools: readonly AgentTool[];
/**
* This leg's minted TOOL ROSTER (the same object `wiringManifest.tools` carries):
* one row per mounted instance in wire order. Minted ONCE at the prompt-assembly station (the same
* pass that projects the prompt manifest's tool rows, so the two agree by construction); the park
* station reads the gated call's identity off it, the exemption probe hands it to the host.
*/
toolRoster: import("../tool-roster.js").ToolRoster;
/**
* The run-time delta seat: every roster change after prepare (the RefreshMcpTools
* seam) re-mints the roster here and publishes a `ToolRosterDelta`. The run loop subscribes at the
* point it emits `wiring_manifest` and forwards each delta as a `tool_roster_delta` event; until it
* does, deltas are buffered in order.
*/
toolRosterDeltas: import("../tool-roster.js").ToolRosterDeltaSeat;
/**
* The leg's `tool_end.structured` projector: the engine card set ∪ the caller rows' declared
* `renderHints.resultCards`; an undeclared `details.type` is dropped and announced ONCE per leg
* (`config.tool_card_undeclared`). The ONE card port: every tool_end body projection (the live loop, the resumed
* batch, the reconcile-recovered orphan) takes it as a parameter; no module-level card set exists to fall back to.
*/
structuredProjector: (result: unknown) => unknown;
/** Name→effect map for every tool this task can call. Used by the abort-path orphan
* reconcile to make interrupted tool_results effect-aware (read/idempotent = safe to
* repeat; write/unknown = outcome unknown). Unknown names default to `write` (conservative). */
toolEffects: Map<string, ToolEffect>;
/** scan-1/A5 — the orphans the WAKE/CRASH reconcile closed while preparing this run (empty on a fresh or
* clean session). The run loop replays them onto the stream at run open as synthetic `tool_end` +
* `message_committed` frames, the same pair the live-abort leg mints at run close: a previous PROCESS
* died holding those calls, so this run's stream is the only place a consumer can ever learn they ended.
* Not the live-abort set — that one is reconciled inside the run and never passes through here. */
wakeRecovered: RecoveredOrphan[];
/** Fixed per-request prompt overhead (system prompt + tool schemas, ≈chars/4 tokens). Fed to
* `maybeCompact.overheadTokens` so the compaction trigger stays accurate in the anchor-less
* regime (custom Brains that don't report usage). */
promptOverheadTokens: number;
/** Accessor for the MAIN lane's latest real brain request (systemPrompt/messages/
* tools snapshot, recorded at the harness's provider seam just before each main-loop call). Fed
* to `maybeCompact.forkContext` on all three compaction lanes so the summary request can FORK the
* already-paid main prefix (CC form). `undefined` until the run's first main-loop call. Summary
* calls themselves never pass through the recorded seam (they ride the separate compaction-brain
* wrapper), so the snapshot is never polluted by a summary request. */
lastBrainContext: () => CompactionForkContext | undefined;
/** Narrow workspace reader for compaction working-file attachments (LONGRUN-2): reads a task file
* via the SAME env the hands ran against (so remote/k8s/E2B tasks read the container's tree, not
* the control plane's). Present only when the hands are enabled — without an env there is no
* workspace to re-read. null = unreadable (deleted/binary/transport error); callers skip it.
* `{ withheld }` = the target matches the sensitive-path read deny list — the
* attachment is deliberately withheld and the consumer must SAY so (an annotation, never a silent
* skip: a silently missing attachment reads as "file gone", which is a different fact). */
readTaskFile?: (path: string) => Promise<string | null | {
withheld: {
pattern: string;
};
}>;
/** CC post-compact restore parity (2026-07-03): the task's READ files, most recent
* first (from the hands' readFileState `lastReadAt` stamps). The compaction working-file
* attachment prefers this over the modified set — CC restores what the model RECENTLY READ,
* including untouched reference files. Present only with hands (same gate as readTaskFile).
* Seeded entries are excluded — their content lives in the system-prompt lane, which
* compaction never touches, so re-attaching them into the summary would be a duplicate. */
recentlyReadFiles?: () => string[];
/** Canonicalizes a raw (often relative, model-typed) path the
* SAME way the hands toolkit does (`resolveKey` against the same containment root `readTaskFile`
* uses), so `maybeCompact`'s kept-tail/instruction-source exclusions compare paths in the SAME
* coordinate `recentlyReadFiles()` already uses — without this a plain string match silently never
* fires for the common relative-vs-canonical case. Present only with hands (same gate as
* readTaskFile); absent there is no containment root to resolve against, and every path source is
* already in the same raw coordinate anyway. */
normalizeAttachmentPath?: (raw: string) => Promise<string>;
/** Recognizes the hands Read tool's own dedup-stub markers
* ({@link isReadDedupStubResult}), so `maybeCompact`'s kept-tail scan can tell a SUBSTANTIVE Read
* result from a stub hit whose original full transmission may already be summarized away. Present
* only with hands (same gate as readTaskFile). */
isDedupStubResult?: (resultText: string) => boolean;
/** Wired to `MaybeCompactOptions.onApplied` by every compaction lane (CC 220 clears readFileState at its
* compaction landing site, @388663). Drops the non-seeded read-state entries
* (the summary just replaced the Read results the dedup stubs point at) and re-registers the files
* that were re-attached WHOLE. Present only with hands (same gate as readTaskFile).
* `preserveReadState` carries the kept-tail-visible files that were
* deliberately skipped for re-attachment — their existing entry must survive the clear too (same
* reasoning as a seeded entry: the model's view of the file did not change).
*
* SCOPE — read "every compaction lane" as exactly that and no wider. The request-layer
* stale-result pass takes a Read body off the wire with no compaction at all, and retires its own
* entries at its own landing site (`prepare-context-lane.ts`'s `landStaleResultClears`) through the
* SAME function this seat calls, on its `tool-results-cleared` arm: ONE retirement mechanism, two
* ways for content to leave. Taken as a claim about every SHRINK mechanism this sentence was true
* of the lanes it named and silently false of the one it did not. */
onCompactionApplied?: (attachedComplete: ReadonlyArray<{
path: string;
content: string;
}>, preserveReadState?: ReadonlyArray<string>) => void;
/** The live diagnostics lane (present only when the gate passed — manager w/ registry +
* write hands + not opted out). `registry` is drained by runtask at turn boundaries; `nudge` is
* called (fire-and-forget) after each successful edit/write so the language server re-analyzes. */
lspDiagnostics?: {
registry: import("../lsp-diagnostics.js").LspDiagnosticsRegistry;
nudge: (rawPath: string) => void;
/** This run's key into the registry's delivered set (the registry is DEPLOYMENT-scoped — one per
* `NodeLspManager` — so "already delivered" has to be qualified by run). runtask passes it to every
* `drain` and calls `releaseRun` with it at the run's terminal. */
runIdent: string;
};
/** The ctx `requestStopAfterTurn` primitive's run-local mirror (one-way, set when a tool asked for a
* clean stop after the current turn). The harness latch itself is private to the loop; the turn
* boundary reads THIS to know that no next request follows — a boundary bundle collected there
* would be steered into a queue the loop never drains, and every one-shot announcement in it
* (listing frames, the once-per-session MCP arms) would be committed as delivered while it was not. */
stopRequestedRef: {
current: boolean;
};
/** Whether prepare's ONE read of the branch's announced snapshot succeeded (see
* `AnnounceOnceLedger.recovered`). False ⇒ the run loop must not flush the once-per-session arms
* as a whole-snapshot append on this leg (it would replace the branch's record with a partial
* one); the leg's announcements then simply repeat next run. */
announcedSnapshotRecovered: boolean;
/** The date-flip detector seam: `legDate` = the date frozen
* into this leg's system prefix; `today()` = the boundary-time LOCAL date (user-zone-bound closure).
* The run loop feeds both to the `date_change` attachment producer; the prefix itself never
* re-renders mid-leg (cache preservation, CC parity). Present only when the prompt carries a date.
* `anchor` = the session's date anchor as prepare read it (undefined ⇒ the branch was unreadable
* and the leg writes none); the settle lane persists it when it is new or the announced date moved. */
dateChange?: {
legDate: string;
today: () => string;
anchor: DateAnchorRead | undefined;
};
/** [A2] C-4 (design-A §4) — the loadProjectMemory snapshot's declared instruction sources
* (`ProjectMemoryLoad.instructionSources`): the probe input AND the lane's per-path baseline
* hashes for the run loop's `instructions_change` attachment. Present only when the deployment's
* loader declared a non-empty list; the lane additionally requires
* `RunnerDeps.probeInstructionSources` — either absent ⇒ zero probe calls, byte-identical. */
instructionSources?: ReadonlyArray<{
path: string;
contentHash: string | null;
}>;
/** The RAW instruction-file text this leg's `loadProjectMemory` returned
* (`ProjectMemoryLoad.content`), for the compaction lanes' `contextInstructionFiles` seat.
*
* Which bytes, exactly: the string prepare-task hands `composeMemoryBlock(projectMem, "project")`
* — pre-compose, unfenced, straight from the host seam. The two neighbours are deliberately NOT
* candidates: the assembled `memoryBlock` carries the memory ENGINE's layers (bytes the model
* itself authored in-band this session through the memory tools) and the seat's contract is
* host/systemPrompt tier; the COMPOSED project block would arrive at a mint that neutralizes the
* whole authority family, `user_memory`/`scope` included, so the engine would rewrite the tags of
* its own composition. Raw text in, one fence at the mint.
*
* Present only when the deployment wired the loader AND it answered non-blank — the same
* predicate that composes the project layer, so the summarizer's copy and the main prompt's copy
* can never disagree about whether this deployment HAS instruction files. Absent ⇒ every
* compaction lane omits the key entirely. */
projectInstructionContent?: string;
/** The `workflow_size_guideline_change` lane's seam, the {@link dateChange} shape one lane over:
* `legGuideline` = the RESOLVED guideline this leg's Workflow tool card was built with (the card is
* a per-mount snapshot, so it never re-renders mid-leg — the model learns a retune from the tail
* frame instead); `current()` = the boundary-time resolved guideline, read live off
* `RunnerDeps.workflowLimits` so a deployment retuning it mid-run is observable without a re-prepare.
* Present only when the Workflow tool is actually MOUNTED — a run with no workflow card has no
* guideline to change. The direct-construction face (`RunWorkflowToolDeps.sizeGuideline`) is out of
* scope by construction: this mount never passes it, so the deps channel is the lane's only source. */
workflowSizeGuideline?: {
legGuideline: WorkflowSizeGuideline;
current: () => WorkflowSizeGuideline;
};
/** Boundary-time external-change scan over the ≤`maxFiles` most-recently-READ
* files. Stats each via `env.fileInfo` and reports paths whose `mtimeMs` moved past the recorded
* `lastReadAt` + 2s epsilon (CC getChangedFiles shape: readFileState needs NO new field; the agent's
* own write-backs refresh `lastReadAt`, so self-edits are immune). ENOENT evicts the readFileState
* entry (CC evict-only-on-ENOENT — transient stat failures skip, never evict) and is echoed in
* `evicted` so the caller drops its per-path dedup state in lockstep (LOW-9). Present only with
* hands; the run loop calls it ONLY when `spec.attachments.changedFiles` opted in (OFF ⇒ zero stat). */
detectExternalChanges?: (maxFiles: number) => Promise<{
changed: Array<{
path: string;
mtimeMs: number;
}>;
evicted: string[];
}>;
/** G1 通告层 — deferred tools MATERIALIZED (rematerialized) but not yet announced at a
* turn boundary. Appended by the rematerialize diff (newly-activated names only — the announced set
* is seeded with prepare-time actives INCLUDING resume-reseeded ones, so a resume never replays);
* DRAINED by the run loop only when the `tools_delta` attachment actually survived the byte cap.
* Present when the task has deferred tools at all — OR when a declared MCP server failed
* to connect, so the failure is announceable even on a task whose every MCP tool went missing with it.
*
* The same frame's MCP arms, filled by the SAME rematerialize seam / materialize-time
* projection and drained by the same intact-survival predicate:
* - `pendingRemoved`: previously-ANNOUNCED tool names that left the live roster (a RefreshMcpTools
* re-splice whose server withdrew them). CC `removedNames`.
* - `pendingReadded`: names in `pendingRemoved`'s history that came back on a later refresh. CC
* `readdedNames`.
* - `pendingFailed`: declared servers whose connect failed at materialize (`statuses`), name/error
* neutralized + bounded at intake, MINUS the pairs this session's record already announced (CC
* `failedMcpServers` compared against the previous attachment — a serving layer runs prepare once
* per user message, so the per-run set alone re-announced every message). */
toolsDeltaRef?: {
pending: string[];
pendingRemoved: string[];
pendingReadded: string[];
pendingFailed: Array<{
name: string;
error?: string;
}>;
};
/** G1 通告层续批 (CC `agent_listing_delta` parity) — the mounted delegation tool's agent-type roster
* (read off `ToolSpec.agentListing`, filled by createSubagentTool), plus the tool's mounted name for
* the CC-verbatim headers. [c209-C]: this seam is now the roster's ONLY model-facing carrier —
* the run loop delivers the initial full frame ON THE FIRST USER TURN (prompt-adjacent) and
* boundary drift deltas after. `models` (Q4, read off `ToolSpec.agentModels`) rides the initial
* frame's tail line. `seedAnnounced` = a durable-resume leg: the run loop seeds the producer's
* announced set from the checkpoint's `announcedListings.agents` name-set when present (drift
* since suspend IS delta-announced), else the MED-3② transcript probe (delivered ⇒ seeded as
* the current entries; unconfirmed ⇒ the resume leg re-announces the initial listing). Present
* only when such a tool is mounted AND its roster is non-empty. */
agentListing?: {
entries: ReadonlyArray<{
name: string;
description: string;
}>;
toolName: string;
seedAnnounced: boolean;
models?: readonly string[];
budget: ResolvedSkillListingBudget;
};
/** [c209-C] skills_listing counterpart of {@link agentListing} — the normalized skills METADATA
* projection (names/descriptions/attachment paths, never bodies; the exact list `createSkillTool`
* serves). Initial full `<skills>` frame on the first user turn, drift deltas at boundaries,
* same seeding contract. Present only when `spec.skills` is non-empty (post 1MB-gate). */
skillsListing?: {
entries: ReadonlyArray<SkillListingEntry>;
seedAnnounced: boolean;
budget: ResolvedSkillListingBudget;
};
/** [c209-C] Q5 — the run loop's live mirror of the listing frames' announced NAME-SETS (updated at
* every commit point: first-frame delivery, intact boundary survival, resume seed). Read by the
* suspend-time checkpoint serializer (`CheckpointState.announcedListings`) so a resume leg can
* diff the then-current roster/skills against what the model actually saw. Always present (empty
* object when nothing announced / no listing faces mounted). [c209-C] R2 C7: `models` mirrors the
* ANNOUNCED model catalog the same way (advanced only when a committed frame carried one), so a
* catalog change across a suspend is re-announced as a "Models available…" drift line.
* `mcpFailed` / `advisories` are the once-per-SESSION arms of the same snapshot (see
* announce-once-ledger.ts): seeded at prepare from the branch's record, advanced by the run loop
* on intact delivery of the tools_delta `failedServers` arm / the mcp_dropped_tools frame. */
announcedListingsRef: AnnouncedListingSnapshotInput;
/** The frame-receipt ledger — the ONE commit point of every engine frame that rides a user message
* (the git-status frame, the agent and skills listing frames). Always present; per leg. */
frameReceipts: FrameReceiptLedger;
/** env-tail migration — the git-status frame lane's run-local state: this leg's
* resolved frame (probe outcome rendered + hashed at prepare), the announced `(kind, hash)`
* mirror the checkpoint serializer reads, the trim-protection slot the request-build context
* handler matches on, and the re-assert closure the compaction landing + boundary retry call.
* Always present (empty object on a hands-less leg — the lane is then out of scope). */
gitStatusRef: GitStatusLaneRef;
/** The posture composition's run-local half (the newest carrier's protection slot; the row last composed for). */
postureFrameRef: import("../auto-mode-directive.js").PostureFrameRef;
/** G1 通告层 — narrow post-compact getter over the process task registry: THIS run's visible
* pending/running background tasks (same owner/scope/session identity the TaskOutput/TaskStop tools
* use), as a bounded display projection (id/description/status — never handles/env/abort). Called by
* the run loop ONLY when `spec.attachments.backgroundTasks` opted in AND a compaction just landed. */
listBackgroundTasks: () => Array<{
id: string;
description?: string;
status: string;
}>;
/** The parent-run subagent-retain ledger (present ONLY when `spec.retainSubagentSessions`
* is enabled). The Runner disposes it (abort in-flight resumes + unpin + release every retained child
* session) in the task's terminal `finally` — same UNCONDITIONAL posture as the background-agent reap:
* retain is NOT durable (a suspend leg's in-memory ledger cannot survive a re-prepare), so releasing on
* every exit path is hygiene, never a loss. */
subagentRetain?: SubagentRetainLedger;
/** Run-scoped consecutive-`summaryProvider`-reuse counter, OWNED by the Runner and
* SHARED across both compaction call sites (within-task turn boundary + `finish()`), so the
* `maxConsecutiveProviderReuse` drift guard is enforced over the whole task — incremented when a
* compaction reused the provider's summary, reset to 0 on a real (LLM) summary. */
compactionReuseRef: {
consecutive: number;
};
/** Trim→compaction pressure propagation (16k live sawtooth root cause): set by the
* context hook when `trimToBudget` actually DROPPED messages from a request view (request-only trim
* + usage-anchor mismatch deflates the next boundary's estimate → the trigger and floor are both
* deceived → full-size request spikes alternate with trimmed troughs). The next turn boundary's
* `maybeCompact` consumes it as `force: true` (bypasses the auto threshold AND the §25.2 anti-thrash
* floor — "the request layer was forced to drop history" is direct evidence compaction is overdue).
* One-shot: cleared on consumption; a failed compaction does NOT re-arm it (existing breaker path).
* Content-only clears (`clearStaleToolResults`) never set it — only real message drops do. */
trimPressureRef: {
droppedMessages: boolean;
};
/** The microCompact machine state this run: the selected clearing
* machine, the cleared-projection ledger (request-view application, durable decisions — see
* `context-edit.ts`'s ledger note; per-run in-memory, so durable resume / `resumeAt` rebuilds
* start EMPTY by construction), the last request's projection seat (what the provider actually
* saw — the MC-R rejection arm computes its candidates and savings on THIS view, never on the
* raw session rebuild), the MC-R knob, and the slice-3 arm-B seat. */
microCompact: PreparedMicroCompact;
}
/** See {@link Prepared.microCompact}. */
export interface PreparedMicroCompact {
/** The frontier-machine selection — `"off"` = no proactive frontier clearing (the machine
* instead gets its one blocking-point shot, slice-3 arm A). */
machine: MicroCompactMachine;
/** MC-R: one-shot clear-and-retry on a provider input-too-long rejection.
* Default true since the slice-3 flip. */
clearOnRejection: boolean;
/** The in-turn forced-compaction seat: runtask wires a closure
* that runs the SAME forced-compaction pass the prompt-too-long recovery uses (gates included)
* and answers whether a compaction landed in the session. The context hook calls it when the
* pre-send estimate breaks the guard budget and then returns `adoptSessionRebuild` so the
* harness adopts the reduced transcript. `signal` is the TURN-scoped abort of the request
* build (r3): a turn interrupt must be able to cut the summary call short instead of waiting
* it out. `anchoredEstimate` is the chain's own trigger coordinate — the seat consults the
* §25.2 anti-thrash floor against it (an ineffective landing must not be repeated per request
* build; the chain's arm C owns the bounded fallback). Unwired (pure-prepare callers) ⇒ arm B
* declines and the chain falls to the trim last resort — same posture as a compaction-disabled
* run. */
inTurnCompactionRef: {
current?: (signal?: AbortSignal, anchoredEstimate?: number) => Promise<boolean>;
};
ledger: ClearedProjectionLedger;
projectionRef: {
current?: {
/** The FINAL projected view of the last provider request (post trim/sweep). */
messages: AgentMessage[];
/** Occurrence coordinates of that view (object-identity first, unambiguous-group fallback). */
keyOf: OccurrenceIndex["keyOf"];
};
};
/** The same offload persist seat the frontier machine uses (write-once, idempotent), so MC-R
* clears compose identical markers. Absent when no offload store is configured. */
offloadPersist?: (toolCallId: string, fullText: string) => string;
}
/**
* Resume inputs threaded into {@link prepareTask} to continue a suspended task. The Runner
* builds it from the persisted {@link Checkpoint}: rewind the branch to the suspension leaf, skip the
* suspended batch during wake-reconcile, and re-seed the §4.bis per-task correctness state so the
* resumed run is in the **same state space** it suspended in.
*/
export interface PrepareResume {
/** The session leaf to rewind to (the suspension point) BEFORE reconcile — discards the abort's
* off-branch "Operation aborted" writes so the resume engine resolves the pending batch cleanly. */
leafId: string;
/** Batch tool-call ids of the suspended turn — wake-reconcile SKIPS these (they are resumed, not
* crash-interrupted; closing them with `[INTERRUPTED]` would DESTROY the suspended batch, §15.2 #7). */
suspendedBatch: ReadonlySet<string>;
/** The §4.bis correctness-state snapshot to re-seed (activeTools / outputRef / nestedStats /
* readFileState). */
seed: CheckpointState;
/** The decision this resume carries (the gate word and, for a review, approve / edit / reject) — read by the mode seed
* (`modeRequestAfterResume`: an approved or edited `plan_review` exits a recorded plan to its pre-plan word). Absent on a prepare that resumes nothing. */
outcome?: ResumeRun["outcome"];
/** How many times this task already suspended (the resumed checkpoint's
* `suspendCount`). The next suspend mints `priorSuspendCount + 1`; past `maxSuspends` it fails
* (`suspend.loop`) instead of re-suspending. Absent/0 ⇒ no prior suspends. */
priorSuspendCount?: number;
/** The cross-slice {@link ResourceLedger} carried by the resumed `resource_limit`
* checkpoint (cumulative spend + the frozen human totals). The next slice's effective budget is
* `min(maxCostUsd, totalBudget − spent)`, and its own suspend debits onto this. Absent ⇒ the first slice. */
priorLedger?: ResourceLedger;
/** The accumulated human-review burden carried by the resumed checkpoint
* ({@link import("../checkpoint-store.js").Checkpoint.humanReview}) — the gates resolved up to and including
* the suspend BEFORE this one. Seeds the per-task accumulator so the resumed leg ADDS this suspend's own
* latency (`now() − cp.suspendedAt`) on top, reporting the WHOLE chain's burden. Absent ⇒ no prior human time. */
priorHumanReview?: {
count: number;
totalWaitMs: number;
gates: HumanReviewGateRow[];
};
/** When the suspend ran with a remote workspace, the {@link CheckpointState.workspaceHandle}
* to restore — prepare rebuilds the per-task env via `deps.executionEnvFactory` then `resumeVM(snapshotId)`
* + `postResumeInit()` (instead of running on a fresh, empty env). Threaded HERE (not via `ResumeTaskConfig`)
* so the factory stays a deployment-level `RunnerDeps` capability and never pollutes `TaskSpec` — preserving
* the "untrusted caller can't inject an env" red line ({@link import("../remote-env.js").ExecutionEnvFactory}'s
* own contract states it: "Lives on `RunnerDeps` (deployment-level) — NOT on `TaskSpec`"). */
workspaceHandle?: import("../remote-env.js").WorkspaceHandle;
/**
* The call id of the CONTENT ask whose answer an operator's approval was spent on, when
* that is what this leg redeems. Scoped as an id, not a leg-wide flag: an `unavailable` outcome on
* THAT call must become a coded failure (the approval bought a question nobody answered), while a NEW
* question raised later on the same leg had no approval spent on it and keeps the ordinary
* continuation. Absent when the resumed pending action is not a question.
*/
redeemedContentAskCallId?: string;
/** Digest of that question's batch — the id alone can repeat, so the pairing is what keeps a LATER
* question from inheriting the claim that an operator approved it. */
redeemedContentAskQuestionsHash?: string;
/** true iff this resume will EXECUTE an approved pending tool (`tool_approval`
* checkpoint × an `allow` winner). The divergent-restore fail-closed guard keys on THIS, not on the
* batch being non-empty: a deny winner executes nothing, so a workspace-root divergence must not
* wedge the refusal (it proceeds under the path-state rebase and records the deny). */
executesApprovedAction?: boolean;
}
/**
* Parent effective-policy inheritance (tighten-only, {@link RunInternals.inheritedGate}): the spawning
* chain's already-evaluated FINAL gate, split into two halves —
*
* - **Data half** (`ancestorRules`, `shellGate`, `permissionMode`): serializable snapshots. Each ancestor's per-session
* permission rules ride as (sessionId, principal, rev, rules) so the child's prepare can re-read them
* LIVE (rev-monotonic — a lagging replica never loosens the snapshot) and re-compile them against the
* CHILD's own env/root/effects as extra deny-narrowing layers. `shellGate` is the operator floor
* (`"always"`): a child inherits it and cannot lift it. `permissionMode` is the ceiling a child's own
* declaration is clamped under.
* - **Opaque half** (`parentConstraints`): each ancestor's live caller `ToolPolicy` plus its FROZEN
* `onAsk`, ancestors first — and, for a hook-wired ancestor, a SECOND entry: its PreToolUse screening
* face folded into a `ToolPolicy` (`createPreToolUseConstraintPolicy`), carrying its own frozen
* approver/mandate/env axes (the `preToolUse`/`hookEnv` fields below). Folded AFTER the child's own caller policy and BEFORE the trailing
* deny-narrowing re-check, each wrapper resolving its own `ask` via the ancestor's frozen approver —
* so a parent `ask` never widens into a child auto-allow, and a parent `allow{updatedInput}` rewrite
* is still re-checked by the child's narrowing layers. NOT serializable: a durable resume must
* re-supply it via `resumeStream(..., internals)` (see {@link CheckpointState.inheritedGate}).
*
* Chain assembly is single-sited in prepareTask's ctx injection (`inheritedGateForChildren`): upstream
* chain + THIS task's own contribution (its session-rule snapshot, its RESOLVED caller policy —
* `spec.toolPolicy ?? deps.toolPolicy`, the same slot its own gate enforces — + frozen onAsk, its
* effective shellGate, and — when this task resolved a PreToolUse hook — a screening entry for that
* hook, deduped by full installation so a deps-level hook is consulted once per call at any depth).
* Depth-N delegation is therefore a linear chain, each layer evaluated at most once per tool call.
*/
export interface InheritedGate {
/** Serializable ancestor session-rule snapshots (data half), ancestors first. */
ancestorRules?: ReadonlyArray<{
sessionId: string;
principal?: string;
rev: number;
gen?: number;
rules: SessionPermissionRules;
}>;
/** The chain's shell floor (`"always"`) — a child inherits it; its own spec cannot lift it. */
shellGate?: ShellGateDoctrine;
/**
* The chain's EFFECTIVE permission mode at spawn (data half, session-wide like every mode): the CEILING a
* child's own declaration is clamped under (`TaskSpec.permissionMode` absent ⇒ inherit; declared ⇒ adopted only
* where the rank table admits it, else folded and disclosed). Persists on the checkpoint's data half
* (`CheckpointState.inheritedGate.permissionMode`) so a redemption in another process resumes under the same
* declared mode — and re-judges the deployment's deny bits for its own leg.
*/
permissionMode?: PermissionMode;
/**
* The parent's live mode ceiling (opaque half; never serialized): the parent leg's mode while that leg runs, its
* SESSION's current leg after it — the child's effective mode is DERIVED from it on every read, so a parent's steer
* reaches a running child on the child's next arbitration without a forwarding frame, and the session's next leg
* reaches a child that outlived the leg that spawned it (a session-scoped background child). `sessionId` is the
* parent's, the address of the user notice a folded child declaration is announced on. `hold` (the engine's own
* ceiling): the child is re-judged at each move of the ceiling, and a fold that produces is announced as at prepare.
*/
permissionModeCeiling?: {
readonly current: PermissionMode;
readonly sessionId: string;
readonly hold?: LegModeCeiling["hold"];
};
/**
* Org-memory admission freeze (ruled 2026-08-05): the spawning chain's FROZEN admitted org-scope
* set — every org memory scope the parent actually mounted (deployment-origin + admitted request).
* A delegated child's REQUEST-origin org scopes must be a subset (intersection ≠ request ⇒ the
* child's prepare refuses, `memory.admission_denied`); deployment-origin scopes circumvent the freeze
* (operator authority is deployment-wide). ALWAYS emitted by chain assembly (empty array = parent
* mounted no org plane), so an ABSENT field discriminates an older-shape chain — which the child
* side reads fail-closed as the empty set. Serializable (plain strings): persists on the
* checkpoint's data half and folds seed ∩ live on resume (tighten-only).
*/
admittedOrgScopes?: readonly string[];
/**
* The MONOTONIC org-governance provenance bit: `true` when any leg of this
* tree ran with an org-admission surface configured (resolver / deployment scope list) or
* inherited the bit. It never clears once set (a governed tree stays governed), and it counts as
* governance evidence at the admission door EVEN when every admitted set en route is empty — a
* governed parent's org-less child, resumed on a surface-less worker, must still refuse
* org-shaped non-v2 scopes instead of mounting them as opaque keys. Absent on trees that never
* had a governance surface (their opaque posture is untouched). Serializable; persists on checkpoints.
*/
orgAdmissionGoverned?: true;
/** Live ancestor caller-policy constraints (opaque half), ancestors first. `policy` is the ancestor's
* RESOLVED caller policy (`spec.toolPolicy ?? deps.toolPolicy` — the same slot the ancestor itself
* enforces, so a deps-level baseline is inherited too).
*
* ⚖️ A-005.13 (ruled with the F-012 family, 2026-08-09): the ancestor's per-run BUDGET LEDGER does
* NOT travel this chain — by design, not by omission. An inherited policy's `check` receives the
* CHILD run's own `ToolCallRequest.budget` snapshot (the child's durable resource ledger), so a
* SELF-LIMITING ancestor policy ("allow N escalations, then ask") sees a fresh ledger in each
* delegated child rather than a continuation of the ancestor's counts. Freezing/snapshotting the
* ancestor ledger was considered and rejected: a frozen ledger goes stale the moment the ancestor
* keeps running (a stale ledger is a NEW wrongness surface, not a fix), and the durable resource
* ledger's identity is per-run by contract. A deployment wanting cross-delegation budget coherence
* carries it in its own policy state (the closure travels the chain intact — a closure-counter
* policy DOES aggregate across the tree, since every layer evaluates the same instance).
*
* `onAsk` is the ancestor's frozen
* `spec.onAsk ?? deps.onAsk` — an ancestor `ask` resolves at that ancestor's own approver
* (headless ⇒ deny), never at the child's. `durableMandate` is frozen at chain-assembly time when the
* ancestor ran under a durable-approval regime that would actually PARK its own plain asks — a
* `forceDurableGate` entitlement, or `durableApproval` with NO live onAsk frozen (with a live approver
* the ancestor's own plain asks resolve synchronously there, so a descendant's ask resolves at that
* same frozen `onAsk` — no mandate). It is a CONSERVATIVE descendant-side mandate: whether or not a given ask would actually
* have parked at the ancestor (the regime may be scoped), the durable-park semantics cannot be
* reconstructed in a delegated child's context — so a wrapper seeing `ask` under this flag denies
* fail-closed (tighten-only holds) instead of resolving synchronously. */
parentConstraints?: ReadonlyArray<{
policy: ToolPolicy;
onAsk?: OnAsk;
durableMandate?: boolean;
/** The CONTENT-ask twin of `durableMandate`, frozen from the QUESTION seat (`spec.onQuestion ??
* deps.onQuestion`) on the same rule: a `forceDurableGate` entitlement, or `durableApproval` with no
* LIVE question face frozen (absent, or the reserved resume placeholder — neither can answer). A
* wrapper seeing an `ask` on the question tool consults THIS bit, never the permission one: the two
* asks are answered by two different faces, and a run with a live `onQuestion` and no permission
* approver can answer a delegated question perfectly well. When it IS set the delegated question is
* marked unresolvable and takes the child gate's park / honest-refusal leg, exactly as an inherited
* permission ask does under `durableMandate`. */
contentMandate?: boolean;
/**
* F-012 — the ancestor's FROZEN auto-mode classifier: the same decider instance the
* ancestor's own gate consults for a surviving ask (`ToolGateInput.autoMode`). Without it the
* ancestor's ask DECISION CHAIN did not travel — the wrappers resolved an inherited ask straight
* at the frozen approver, so an ask the ancestor's classifier would have BLOCKED executed in the
* child on the frozen approver's allow. The wrapper runs it BEFORE the frozen approver, in the
* ancestor's own gate order (classifier → approver): `block` ⇒ deny (`decisionReason:"classifier"`),
* `allow` ⇒ the classifier's auto-allow (the frozen approver is not consulted), anything else
* falls through to the approver chain. Never consulted for the reserved question tool nor under a
* durable mandate (a marked call must reach the park with no synchronous decision-maker between).
*/
autoMode?: {
decider: AutoModeDecider;
/**
* The ancestor's per-run DENIAL-LIMIT tracker, frozen beside its decider (same owner). The
* wrapper arms count the frozen classifier's blocks on it and, at a bound, resolve the fallback
* ask at the frozen approver instead of denying (`requiresRealApproval` set, sandbox admission
* excluded). Live-only, like the decider: a cross-process redemption starts a fresh count.
*/
denialTracking?: AutoModeDenialTracker;
/**
* The SERIALIZABLE criteria half of this classifier (assembly inputs + knobs + the
* deployment's settings epoch), present when the arming deployment opted in
* (`RunnerDeps.autoMode.persistArming`). A durable park records it on the chain entry so a
* CROSS-PROCESS redemption can rebuild an equivalent decider over its own model leg instead of
* answering `unavailable` forever; the constraint-chain digest binds it, so a resume re-supplies
* exactly what the row carries. It never affects THIS process's decisions — the live `decider` is
* the one every gate consults, here and on every descendant.
*/
arming?: AutoModeArmingRecipe;
/**
* The ancestor's live mode ceiling (opaque, never serialized; the chain's `permissionModeCeiling`): the classifier
* answers the ancestor's asks only while this reads `auto` — else they go to the frozen approver. Absent (a
* re-supplied entry) ⇒ the classifier answers as recorded.
*/
liveMode?: {
readonly current: PermissionMode;
};
};
/**
* Set ONLY on an entry whose `policy` is the folded form of an ancestor's PreToolUse screening face
* (issue #33 — the face used to stop at the task that installed it, so a call the ancestor had
* refused executed one level down). It carries the raw callback for ONE purpose: identity. A
* deployment-level face (`RunnerDeps.hooks`) is resolved by every task in the tree on its own, so
* each descendant's gate already runs it in phase 1 — folding it AGAIN as an inherited constraint
* would consult the same callback twice per call at depth 1 and once more per level below. Two
* identity tests keep that at exactly one consultation per call: chain assembly does not re-append a
* face already on the chain, and a descendant substitutes a pass-through for the entry naming the
* very callback its own gate will run. A per-task face (`TaskSpec.hooks`) is a different function in
* the descendant (or absent), so it folds and travels.
*
* Identity is the only sound test here, and it is deliberately the CONSERVATIVE one: an assembly that
* hands each task a freshly bound wrapper over one underlying callback (the `runSpec` mux does this)
* produces two distinct function objects, so the descendant both folds the ancestor's entry and runs
* its own — the callback is consulted twice rather than skipped. Erring toward a second screening,
* never toward a missed one, is what makes an identity miss a cost rather than a hole.
*/
preToolUse?: Hooks["preToolUse"];
/**
* The environment this screening face was installed against — the source of the `HookEnvCapabilities`
* handed to it, compared BY REFERENCE, `undefined` when the installation had no env face. Part of the
* installation's identity for the same reason the frozen approver is (HIGH): a face that
* resolves paths through `ctx.env` answers a different question in a different environment, so an
* ancestor running against the deployment's tree and a descendant running in an isolated worktree are
* two screenings, not one. A deployment with a per-task `executionEnvFactory` therefore has its face
* consulted once per environment on the chain rather than once overall — which is the point: the
* ancestor's environment is the only place the ancestor's verdict can be reproduced.
*
* ⚠️ RECORDED BOUNDARY (delegation provenance): the approver half of the key compares
* through {@link askApproverIdentity}, which by its own contract answers "the same decision-maker",
* collapsing a `withDelegationProvenance` wrapper onto the approver it forwards to. Two installations
* whose approvers differ ONLY in the provenance they attach therefore count as one. Raw reference
* equality is not an available alternative: a delegated child's approver is ALWAYS a fresh wrapper
* over its parent's, so raw equality would collapse nothing and a deployment-level face would be
* consulted once per ancestor level on every leaf call. The exposure is bounded on the side that
* matters — the pass-through arm hands the frame to the DESCENDANT's own gate, whose approver carries
* the descendant's own provenance (strictly more context, never less). What a chain-assembly collapse
* can drop is one intermediate installation's provenance on an ask: an approver that DECIDES on
* `req.delegation` would see the surviving installation's rather than both.
*/
hookEnv?: unknown;
}>;
}
/** The runtime feature flags the stable prompt's harness-context block gates its sections on
* (claim only what the task has). Spread into the assembly's StablePromptContext
* by the prompt-assembly phase, which re-derives the two orchestration flags against the post-exclusion roster. */
export interface PromptFeatureFlags {
policyEnabled: boolean;
hooksEnabled: boolean;
isolationEnabled: boolean;
reminderMark: string;
readFaceOpen: boolean;
writeFaceOpen: boolean;
orchestrationEnabled: boolean;
orchestrationDeferred: boolean;
promptProfile: "simple" | "classic";
fableMitigations: boolean;
goalEnabled: boolean;
awarenessEnabled: boolean;
worktreeIsolated: boolean;
withinTaskCompactionEnabled: boolean;
isSubagent: boolean;
}
/**
* A TRUSTED, run-scoped internal channel into {@link prepareTask}, set
* ONLY by a trusted CORE caller (`runRepairLoop` via the Runner's internal `runTaskStream` arg) — NEVER from
* a {@link TaskSpec} field (the untrusted-caller surface). It is the live-state counterpart
* of {@link PrepareResume.seed}: where `resume.seed` re-seeds correctness state RESTORED from a checkpoint,
* this carries the LIVE per-task state the Runner cannot otherwise see (it lives in the caller's closure).
*
* Originally it carried only the repair loop's live {@link RepairBundle}: `runRepairLoop` is a thin composition
* OVER `runner.runTask`, so when an orthogonal durable suspend (resource/HITL) interleaves a repair attempt,
* the bundle (failureTrace/diagnostics/attemptCount/oracleTier) sits in the loop's closure and was being lost
* — the minted checkpoint serialized `repairBundle: undefined`. Threading it here lets
* {@link prepareTask}'s `serializeCheckpointState` source the LIVE bundle so a resume re-seeds `attemptCount`
* MONOTONICALLY. Mirrors how `nestedStats`/`resume.seed` thread trusted
* run-scoped internals through the Runner without touching `TaskSpec`.
*
* @contract prepare.deps-read-stable — the `RunnerDeps`, `RunInternals` and `ExecutionEnv` handles a host
* passes into {@link prepareTask} are READ-STABLE for the duration of the call: prepare reads them at the
* points its phases need them, and a host that mutates them between microtasks while prepare is in flight
* is outside the contract (the Runner is constructed over its deps; a mid-prepare rewrite is a race the host
* made). Phase boundaries are scheduling points — an `await` on a phase that carries its own awaits adds a
* microtask between that phase's last continuation and the driver's next read — and such a host can observe
* them; the engine does not defend against it. The untrusted surface is {@link TaskSpec}, whose pre-first-
* await latch is a separate, pinned guarantee (prepare-task-phase-pins T1).
*/
export interface RunInternals {
/** The live repair bundle from a `runRepairLoop` attempt in flight (attemptCount>0). Serialized onto a
* checkpoint minted MID-attempt so a resume re-seeds it; undefined for any non-repair run. */
repairBundle?: RepairBundle;
/**
* The ENGINE deliberately stripped the spawn turn's per-request `onQuestion`
* face from this leg's spec (a long-lived background/retained/revived child must not hold a
* callback torn down with the turn that spawned it). Minted ONLY by the subagent lanes'
* `stripSpawnTurnQuestionFace` helper, PAIR-PRODUCED with the strip itself — never inferred from
* spec shape. Consumed by the wiring manifest (`question.wired: "stripped_bg_lane"`) and by the
* interaction-posture door (the strip is core's correct design, not a configuration lie, so an
* `"interactive"` tree's engine-stripped legs are exempt). TRUST POSTURE (ruled 2026-08-05):
* RunInternals is a public trusted-caller parameter, so this flag is an honesty channel, not a
* security gate — a caller forging it is a deployment lying to itself; resource-face safety
* stays with its own fail-closed mechanisms. INHERITS DOWN THE TREE: a stripped leg's descendants are faceless by
* the same engine act (the trusted ctx carries it as `ToolExecuteContext.questionFaceStripped`, the Workflow mount as
* `parentQuestionFaceStripped`), so the posture-door exemption travels with the facelessness it explains; a leg
* that strips on its own mints the same flag.
*/
questionFaceStripped?: true;
/**
* The SPAWNING run's resolved interaction posture, carried
* into engine-built children over the trusted internals channel (never copied onto the child
* SPEC — the §8.3 rule). The child's door resolves `spec ?? THIS ?? deps`, so a root's per-run
* posture override governs its whole engine-built tree instead of every child falling back to
* the deps-level default. Filled by the delegation lanes' `childInternals` and the workflow
* spawn attribution; rides `internalsSnapshot`, so retained/revived legs keep it.
*/
parentInteractionPosture?: "interactive" | "headless";
/**
* The trusted post-consume hook for the PARKED-RESUME drive:
* called by `resumeStream` after its resolve CAS WON (the token is consumed — the master
* arbitration is decided) and BEFORE the resumed leg starts. The parked-resume caller uses it to
* flip the reserved row `parked→running` (guarded CAS) + adopt the live handle/lane — the ONLY
* legal site for that flip (a pre-consume flip would let TaskStop hit the plain running arm and
* circumvent the checkpoint arbitration entirely). A THROW here aborts the resume (propagates out of
* `resumeStream`): the checkpoint is already consumed, so the caller's compensation
* (`rollbackParkedClaim`) reads `resolved` and settles the honest failed/outcome-unknown terminal
* — never a silent run without an adopted row. Deployment/trusted-caller channel only, mirroring
* every other RunInternals field.
*/
afterCheckpointResolve?: () => Promise<void>;
/**
* Parent effective-policy inheritance (tighten-only): the spawning parent's already-evaluated FINAL
* gate, threaded into a child task so the child inherits it as ADDITIONAL constraint layers — a child
* can only ever be narrowed by this, never widened (its own session rules / skill rows / caller policy
* still apply in full). Filled ONLY by core delegation callers (`createSubagentTool`'s execute via
* `ToolExecuteContext.inheritedGateForChildren`, the workflow spawn legs) — NEVER a {@link TaskSpec}
* field (the untrusted-caller surface), mirroring `inheritedSkillGrants`'s ctx-injection posture.
* Absent ⇒ no inheritance (a top-level task; fully backward-compatible).
*/
inheritedGate?: InheritedGate;
/**
* The delegation RUNTIME-PROVENANCE channel. Minted by the spawning delegation
* tool when the parent chain is armed (the parent mounts a memory session, or is itself recording
* for ITS parent): `ref` is the child's monotonic aggregate — the child's recorder writes into it
* as the run progresses (live faces read the current bits; the terminal attestation reduces from
* it); `contentSafety` is the chain's FROZEN classification snapshot — the child may narrow it
* with its own config, never widen. Trusted internals chain only, same posture as
* {@link inheritedGate}. Absent ⇒ the child records nothing (its deliveries then read `unknown`,
* and every judgment falls back to the static floor — fail-closed by construction; whether the
* floor's verdict MARKS the judging session follows that run's deployment evidence standard,
* {@link RunnerDeps.memoryDelegationEvidence}).
*/
delegationProvenance?: {
ref: {
current: import("../memory-engine/delegation-provenance.js").DelegationProvenanceAggregate;
};
contentSafety: import("../memory-engine/delegation-provenance.js").DelegationContentSafety;
};
/**
* The SESSION-scoped org-admission freeze for a SAME-SESSION continuation
* that is NOT a checkpoint resume: a retained background child revived through the in-process resume
* leg. The checkpoint plane already freezes the session's own verdict
* ({@link InheritedGate.ownAdmittedOrgScopes}); this leg has no checkpoint, so without a carrier it
* re-adjudicated from scratch and a resolver whose answer WIDENED between the two legs remounted a
* tenant layer the session had already lost.
*
* A REF, not a value: the revival replays a spread COPY of the spawn-time internals, so a plain field
* could only ever carry the value that existed at spawn (always `undefined`). The prepare reads
* `current` as its `priorOwnVerdict` — intersected with any checkpoint seed, so the fold can only ever
* narrow — and writes this leg's own verdict back, which is ⊆ the prior one by construction.
*
* TRUSTED internal, filled by `createSubagentTool`'s `childInternals` (one ref per spawned child, never
* shared across generations — a child's own children build their own). Absent ⇒ no session freeze from
* this channel, byte-identical to a deployment with no governance surface.
*/
ownOrgAdmissionRef?: {
current: import("../memory-admission.js").OwnOrgAdmissionVerdict | undefined;
};
/**
* This run's LATE-BOUND peer-identity carrier (a REF, same family and reason as
* {@link ownOrgAdmissionRef}: revival replays a spread copy of spawn-time internals, and the axes
* a run wears are born at different points — a* handle at registration, session/task at prepare's
* session acquisition). Created at internals assembly (delegation lanes; runtask normalizes one
* in for a top-level run); axes recorded through the single monotonic `addAxis`; the canonical
* key freezes at the first recorded axis (h→s→t by call-site ordering). Read by the SendMessage
* mount (sender key + hop token) and paired into children as {@link parentPeerRef}.
*/
peerSelfRef?: import("../../agents/peer-admission.js").PeerSelfRef;
/**
* This run's INBOUND peer-chain ref: overwritten at the harness consumption boundary
* with the chain of the peer message the model just consumed (enqueue-time writes would leak a
* not-yet-seen message's chain into outbound sends); seeded by the L3/L4 wake legs; empty
* otherwise. Not checkpointed in v1 (recorded honest boundary: a cross-process resume forwards
* from an empty chain — fail-open on loop suppression only).
*/
peerInboundChainRef?: import("../../agents/peer-admission.js").PeerInboundChainRef;
/**
* The PARENT run's peer-identity ref, PAIR-MINTED with {@link parentNotify} at every
* trust point that binds the uplink callback (spawn assembly, the resume face's replacement): the
* callback is opaque and the parent AXES deliberately keep the original spawner on the revive arm
* while the callback points at the WAKER — so the uplink's admission identity can only travel
* with the binding itself. Consumed by the SendMessage mount as `uplinkRecipient`.
*/
parentPeerRef?: import("../../agents/peer-admission.js").PeerSelfRef;
/**
* 🔴 The workflow **nesting depth** for this run, a TRUSTED
* cross-process channel (worker/script can NEVER set it — it is not a {@link TaskSpec} field nor a
* `run_workflow` tool param). When a deployment initiates a workflow on behalf of a parent run that is
* itself inside a workflow (e.g. service's `/v1/workflows`), it threads `workflowDepth = parentDepth + 1`
* into `startWorkflow`/`runWorkflow` so the one-level nesting guard fires across the process boundary.
* In-process nesting needs nothing here — the engine's `AsyncLocalStorage` propagates depth automatically.
* Mirrors how `repairBundle`/`inheritedSkillGrants` thread trusted run-scoped internals the Runner cannot
* see from `spec`. Consumed by the `run_workflow` tool wiring (S8c), not by `prepareTask` itself.
*/
workflowDepth?: number;
/**
* The host's DECISIONS for parked workflow-agent rows (`wa*` rows with `status:"parked"`), a TRUSTED
* run channel like `workflowDepth` (never a `TaskSpec` field, never a tool argument — a model cannot decide
* an approval). A host that decided a workflow child's parked checkpoint launches the run that re-invokes
* `Workflow({resumeFromRunId})` with the decision here; the Workflow tool applies the entries naming the
* resumed run (`RunWorkflowOptions.parkedResume`) and the resume drives the parked child on instead of
* re-parking. `inheritedGate` is the cross-process re-supply of the checkpoint's opaque parent constraints
* (the same seat the background revival's `reviveClaim.parkedResume` takes). Consumed by the `run_workflow`
* tool wiring, not by `prepareTask` itself.
*/
workflowParkedResume?: ReadonlyArray<{
runId: string;
token: CheckpointToken;
outcome?: ResumeOutcome;
inheritedGate?: InheritedGate;
}>;
/**
* Set ONLY by the Agent tool's fork route (`Agent(subagent_type:"fork")`, a core caller) on the
* child it spawns: this run IS a forked child. `prepareTask` threads it to tool ctx as `insideFork` so the
* child's own Agent tool refuses a nested fork (nesting guard — mirrors CC's "fork is not available inside a
* forked worker"; a fork can still delegate via `Agent`, just not fork again). TRUSTED internal (NOT a
* `TaskSpec` field — the untrusted-caller surface), mirrors `inheritedSkillGrants`.
*/
insideFork?: boolean;
/**
* The PARENT's reminder provenance mark, set ONLY by the Agent tool's
* fork route on the child it spawns ("one declaration, one mark": a fork runs under the parent's
* byte-identical system-prompt declaration, so its own engine mints must carry the PARENT's mark;
* a spawn/clone context mints its own). Verified through the mint home's verify port at adoption
* (`isValidReminderMark` — an unrecognized value re-mints, fail-safe); the forked SESSION's own
* `reminder_mark` entry is the primary carrier (a store fork copies committed history), this
* channel is the in-process belt over it. TRUSTED run-scoped channel (NOT a {@link TaskSpec}
* field), mirroring `insideFork`.
*/
reminderMark?: string;
/**
* Set UNCONDITIONALLY by every core spawn path (`createSubagentTool`'s
* `childInternals` — shared by all four spawn legs sync/steer/background/fork, its persisted
* `internalsSnapshot` for revive, and the workflow orchestrator's `spawnAttribution`), regardless
* of whether a NAMEABLE `parentTaskId`/`parentToolCallId` exists. A directly-started workflow (no
* launching tool call, e.g. cron-triggered) has neither of those — "no id is ever fabricated" for
* attribution — but its spawned agents are still delegated children for consent-notice purposes.
* Drives `isSubagent` below: do NOT use `parentTaskId` presence alone as the child-ness signal,
* that under-covers exactly this case. TRUSTED run-scoped channel (NOT a {@link TaskSpec} field),
* mirroring `insideFork`.
*/
isDelegatedChild?: boolean;
/**
* Subagent transcript persistence — the child transcript session's PLACEMENT declaration, minted
* by the background delegation lane (the trusted spawner: it knows the a* handle, scope and root)
* and forwarded verbatim by this prepare's session acquire, so the session store CREATES the
* child's transcript into its declared subagent partition (see {@link SessionStore.placements}).
* Deliberately a TRUSTED internals seat and never a {@link TaskSpec} key: a public key would let
* any caller push arbitrary sessions into the partition and poison store-side retention/
* enumeration. Absent (sync children, forks, observers, every non-delegated run) ⇒ the acquire
* carries no placement — byte-identical to before.
*/
sessionPlacement?: import("../session.js").SessionPlacement;
/**
* The DEFAULT role-base persona for a DELEGATED child, threaded by
* `createSubagentTool`'s execute (a core caller) when neither an agent-definition `systemPrompt` nor the
* delegation tool's `opts.systemPrompt` names one. It sits at the BOTTOM of the role-base chain —
* `spec.systemPrompt ?? resolvedRole.systemPrompt ?? internals.defaultSystemPrompt` — so a deployment's
* `roles.subagent.systemPrompt` / `roles.default.systemPrompt` preset still wins (pre-G1 semantics: an
* unset child systemPrompt let the role preset apply; G1's first cut put SUBAGENT_PROMPT at spec level
* and silently shadowed the preset). Only when NO preset resolves does the lean SUBAGENT_PROMPT (CC 198
* general-purpose persona, pretty.js:419977) replace the full DEFAULT_SYSTEM_PROMPT constitution base.
* TRUSTED run-scoped channel (NOT a {@link TaskSpec} field), mirroring `insideFork`.
*/
defaultSystemPrompt?: string;
/**
* The catalog WORD that chose the model this engine-spawned child was handed on its spec.
*
* The two halves of one fact travel on two channels on purpose: the spec carries the RESOLVED `Model`
* object, so nothing re-resolves between the spawn's judgement and this child's prepare (the model the
* spawn frames name is the model that runs); this carries the word that object came from, so the child
* is not PINNED by having been handed an object — its own children inherit the word in turn, and the
* deployment's per-model window overlay (keyed by word) still reaches it. Absent ⇒ no word exists for
* this child's model (a caller-pinned object, a `select` criteria), and such a leg is deliberately never
* re-resolved and never overlaid. Trusted channel, never a `TaskSpec` field: it is the engine's own
* statement about a model the engine itself resolved, and a caller cannot author it.
*/
inheritedModelRef?: ModelRef;
/**
* GOAL MODE flag, a TRUSTED internal channel set ONLY by `runGoal` (a core caller),
* NEVER a {@link TaskSpec} field. It drives `featureFlags.goalEnabled` → injects `GOAL_COMPLETION_GUIDANCE`.
* Why internal (not a public `TaskSpec.goalMode`): the guidance promises "declaring done STOPS iteration and
* surfaces" — a promise only `runGoal`'s loop makes real. A public field would let a caller inject that
* prompt with no loop behind it (§6.3 honesty violation). `runGoal` injects the `declare_done` tool itself;
* this flag only governs the PROMPT (injection-ownership split).
*/
goalMode?: boolean;
/**
* The skill grant rows the PARENT task held at the moment it spawned this child (its `allowed-tools` / `disallowed-tools`
* rows of the run, whatever wrote them), captured by `createSubagentTool`'s `execute` from the parent's
* `ctx.activeSkillGrants()` and seeded into this child's table as `inherited` rows: they hold until THIS child ends
* (the parent's run ending ends the parent's rows, not this copy), beside the child's own activations.
*
* A TRUSTED, run-scoped channel (never a {@link TaskSpec} field — TaskSpec is the untrusted-caller surface), in the
* posture `repairBundle` / `resume.seed` keep. A capture the delegation could not read refuses the spawn rather than
* seeding nothing (it carries refusals too). Absent or empty ⇒ nothing inherited.
*/
inheritedSkillGrants?: readonly SkillGrantRecord[];
/** The parent's request allow entries at the spawn (`ctx.activeRequestAllowedTools()`), seeded as this child's own request rows until it ends — the channel of {@link inheritedSkillGrants}. */
inheritedAllowedTools?: readonly string[];
/**
* When this task runs as a SUB-AGENT spawned under a parent task's
* tool call, the spawning tool's `ToolExecuteContext.toolCallId`. The Runner stamps it onto this task's
* stream content events as {@link TaskEvent.parentToolCallId} so a consumer can attribute the child's live
* content to the delegation subtree WITHOUT core merging the child stream into the parent (lightweight
* message-identity, not stream-merge). A TRUSTED, run-scoped channel filled by a core caller
* (`createSubagentTool`'s `execute`) — NEVER a {@link TaskSpec} field (the untrusted-caller surface),
* mirroring `inheritedSkillGrants`/`workflowDepth`. Absent for a top-level (non-delegated) task.
*/
parentToolCallId?: string;
/**
* A SUBAGENT's human display NAME, threaded at spawn so the
* child's `task_progress` ticks carry a readable label (a Fleet child row otherwise shows the raw `taskId`).
* Filled by `createSubagentTool`'s `execute` = the explicit `taskName`, else the selected agent-type
* (`AgentDefinition.name`). TRUSTED run-scoped channel (NOT a {@link TaskSpec} field), mirroring
* `parentToolCallId`. Absent for a top-level run / a bare delegation with neither label — the child's
* `task_progress` then carries NO `name` (it keeps its taskId; it deliberately does NOT fall back to the raw
* objective, which could leak a delegated secret). Untrusted (`taskName` is model-chosen) →
* the consumer sanitizes via `inlineUntrusted` at emit.
*/
agentName?: string;
/**
* The EXPLICIT `Agent({name})` spawn identity — set
* ONLY when the spawn carried a `name` parameter, unlike {@link agentName} (a DISPLAY label:
* description or agent-type, present on nearly every spawn). Teammate semantics (hierarchy clamp,
* teammate addendum, uplink attribution) key on THIS field; keying on the display label made
* every described child a "teammate" (clamp over-wide, addendum over-composed).
*/
explicitAgentName?: string;
/**
* The RESOLVED subagent type this child was spawned as (`"general-purpose"` / `"fork"` / a roster
* definition's name — the same word the fleet row's `agentType` column carries; never the
* description). Read by the hook identity envelope (`identity.agentType`) so a process-level hook
* seat can tell WHAT kind of child it is serving without parsing display text. Trusted run-scoped
* channel (NOT a {@link TaskSpec} field); absent on a top-level run and on a workflow-lane agent.
*/
spawnAgentType?: string;
/**
* The SPAWNING run's taskId, threaded at spawn (from the parent's
* `ToolExecuteContext.taskId`) so this child's `task_progress` ticks carry `parentTaskId`. Lets a UI build the
* live nested-agent tree directly (child.parentTaskId === parent.taskId) at any depth. TRUSTED run-scoped
* channel (NOT a {@link TaskSpec} field), mirroring `parentToolCallId`. Absent for a top-level run.
*/
parentTaskId?: string;
/** The spawning run's sessionId (paired with parentTaskId; see
* ToolExecuteContext.parentSessionId). */
parentSessionId?: string;
/**
* The memory-capture opt-out FLOOR: TRUE ⇔ the spawning session was under an
* opt-out when this child spawned. The child then runs opted-out itself (its prepare mints its
* own one-way record, reason naming the floor) — NOT re-adjudicated against the child's
* entitlement (the floor is the parent's already-granted opt-out tightening the tree; a
* re-adjudication that could answer "no" would be a loosening door). Trusted internals channel
* on purpose (the parentInteractionPosture law): never a child-spec field, so no chosen
* AgentDefinition and no spec surgery can shed it; rides `internalsSnapshot`, so retained
* revivals keep it. Grandchildren inherit through each generation's own live seat (an opted-out
* child forwards TRUE to its children in turn — monotone by construction).
*/
memoryCaptureFloor?: true;
/**
* The floor's THIRD state: the spawning session's
* capture state was INDETERMINATE at spawn (its record store faulted at the getter read).
* Neither boolean is honest there — no floor runs a possibly-recorded lineage captured, a coined
* floor mints an IRREVERSIBLE record off an unreadable state — so the lane carries the fault
* itself and the child's prepare resolves it against the live record query (found ⇒ floor;
* still faulting ⇒ the child runs indeterminate — mechanical restrictions, no mint; readable
* and clean ⇒ clean). Same trusted-channel law as {@link memoryCaptureFloor}; mutually
* exclusive with it at the spawn site (the floor wins when the state IS known).
*/
memoryCaptureFloorIndeterminate?: true;
/** The spawning session's write-plane control dir (the coordinate its capture
* opt-out record is keyed under), so a child on a DIFFERENT memory plane can still run the
* record-query leg against the parent's own carrier. Trusted chain, rides internalsSnapshot. */
memoryCaptureQueryDir?: string;
/** The ancestor chain's capture coordinates (root first, one
* row per generation, appended by each spawner's ctx seat). The child's harvest closures walk
* every row, so a mid-run flip ANYWHERE up the tree suppresses every in-flight descendant.
* Trusted chain; rides internalsSnapshot. */
memoryCaptureAncestors?: ReadonlyArray<{
sessionId: string;
controlDir?: string;
}>;
/**
* The FLEET-task kind of this run, declared by the lane that spawned it and stamped onto every
* `task_progress` tick the run mints (`TaskEvent`'s `taskType`). A consumer merging progress ticks
* with task notifications into one ledger could otherwise only key on "it showed up on the fleet
* stream, so it must be an agent" — an inference that has already produced a row for a task that
* was never an agent. Same vocabulary as `TaskNotificationPayload.task_type`, so the merged ledger
* has ONE type axis rather than two spellings of one.
*
* Set by the BACKGROUND delegation lanes (plain + fork), whose runs own a registry `a*` row, and by
* both of the workflow orchestrator's spawn legs. A SYNCHRONOUS delegated child declares nothing —
* it has no fleet row of any kind, so absence is a fact about the run rather than a gap in the
* stamping. TRUSTED run-scoped channel (never a {@link TaskSpec} field).
*/
delegationTaskType?: import("../types.js").DelegationTaskType;
/**
* The registry row's stop-cycle generation this run executes as (fresh spawn = 1, a
* revival's bumped counter), threaded by the BACKGROUND delegation lanes from the registry's own
* `cycleSeq` so every `task_progress` tick the run mints carries it as `seq` (same axis as
* `TaskNotificationPayload.seq` / `BackgroundChildEvent.seq`). Absent for runs with no `a*` row
* (sync children, workflow agents, top-level) — same absence-is-a-fact posture as
* {@link delegationTaskType} above. TRUSTED run-scoped channel (never a TaskSpec field).
*/
cycleSeq?: number;
/** The ROOT host session of the whole delegation tree (fixed point: the
* spawner passes its own `ctx.rootSessionId ?? ctx.sessionId`, so depth 1 gets the host session
* and every deeper level inherits it verbatim). `parentSessionId` is the IMMEDIATE spawner —
* after a restart those intermediate sessions are dead ends, and a recovery face enumerating
* "everything under this host session" needs the root anchor, not an alias walk. */
rootSessionId?: string;
/**
* The run tree's PLACEMENT root: the fixed point a target-bound env factory keys
* its placement lookup on ({@link import("../remote-env.js").ExecutionEnvFactoryContext.placementRootSessionId}).
* A SEPARATE axis from {@link rootSessionId} deliberately: that field means "member of this host
* session's DELEGATION tree" and is consumed by the registry access/recovery faces — cascade rungs
* and verification legs are intentionally NOT members of that tree (independent cold re-runs in
* their own sessions), so widening `rootSessionId` to cover them would corrupt the recovery faces'
* reading. This member says only "place me where this session was placed". Producers: the
* orchestration entries (`runCascade` / `runWithVerification` family) after their first leg's
* sessionId receipt, and — C12 — the delegation/workflow spawn chains, which re-thread a parent's
* EXPLICIT value verbatim into child internals (ToolExecuteContext.placementRoot → childInternals;
* workflow deps → shared internals base), so every descendant of a placed leg keeps the fixed
* point; prepare's mint reads it first (`placementRoot ?? rootSessionId ?? sessionId`). Absent
* everywhere else — the delegation lanes' `rootSessionId` fixed point then becomes the placement
* root through the middle segment, unchanged. DURABLE: the suspend mint stamps the resolved fixed point as
* `CheckpointState.placementRootSessionId`, and a bare durable resume restores it into this
* member (live internals win when re-supplied; a CONTRADICTING re-supply refuses pre-CAS,
* `resume.placement_mismatch`). TRUSTED run-scoped channel (never a {@link TaskSpec} field).
*/
placementRoot?: string;
/**
* The REGISTRY SCOPE this run's own background row lives in: the domain its registry-facing
* tools (TaskOutput / TaskStop / SendMessage / AgentTranscript / Monitor, and the announce listing)
* must mount in to see it. Filled by the delegation tool at spawn, which is the party that CHOSE the
* domain when it registered the row — `ctx.principal` when the spawning run has one, else the
* delegation mount's declared `background.scope`, and on a revival the claimed row's own scope.
*
* Why this is not just `principal`: a deployment may declare its domain at MOUNT time and run its
* tasks without `TaskSpec.principal`. The row then lands in the declared domain while the child runs
* with no principal at all, and `principal ?? "default"` sends every one of its registry-facing tools
* to a domain the row is not in — scope is fail-closed on both access predicates, so such a child
* cannot see its own row, its siblings, or the children it spawns. Carrying the domain as its own
* trusted axis keeps `principal` the IDENTITY it is: the identity also keys the durable
* approval / checkpoint namespace (`checkpointScopeOf`), the runtime-capability lookup and the MCP
* principal header, and a mount's registry scope has no business moving any of those (an unattended
* safety park must not migrate into the caller's opt-in approval bucket).
*
* TRUSTED channel (never a {@link TaskSpec} field, never a model argument), like the parentage axes
* above. When a deployment sets both, they agree by construction: with a principal present the
* delegation tool resolves this axis TO that principal.
*/
registryScope?: string;
/** The spawning run's ADOPTED center artifact, threaded down the
* trusted internals chain so every child in the tree composes the SAME closure (a child must
* never spontaneously adopt the live candidate while its parent runs an older pin — that is the
* exact mixed-version state the epoch exists to prevent). Resolved by-digest through the
* verified store at child prepare; a miss is the same fail-loud `prompt.snapshot_unavailable`. */
parentCenterArtifactDigest?: string;
/** Publish provenance companion of {@link parentCenterArtifactDigest} (audit only). */
parentCenterSourceRevision?: string;
/** R2 双形轴 — parent's resolved prompt profile, inherited unless the child spec overrides. */
promptProfile?: "simple" | "classic";
/**
* An OPT-IN, DISPLAY-ONLY event sink a deployment sets on the TOP run to
* receive a subagent's live `task_progress` ticks (which otherwise stay in the child's ISOLATED stream). Threaded
* recursively down the delegation tree (via `ctx.forwardEvent`), so every nested subagent's ticks bubble to the
* SAME sink. The Runner's ctx wrapper forwards `task_progress` always; when the run's spec sets
* `forwardSubagentEvents: true` it ALSO forwards the child's content events (`text_delta` / `text_end` /
* `reasoning_delta` / `tool_start` / `tool_end` — the subagent viewing pane, carrying the same UNTRUSTED-RAW/consumer-must-redact
* contract as the main stream's tool events). Either way the child stream is NEVER merged into the parent's
* MODEL context (this is purely a render channel). Absent unless the deployment opted in. CONTAINED at
* one door: every engine site feeding this seat goes through `forwardSafely` (`forward-sink.ts`), so a
* sink that throws — or, since an `async` function satisfies this `=> void` type, one that returns a
* REJECTING promise — drops its frame and nothing else, never awaited
* (`@contract runtask.forward_sink_never_faults_the_run`).
*/
onForwardEvent?: (event: TaskEvent) => void;
/**
* The run's OWN top-level `status` TaskEvent stream (brain liveness: rate-limit/retry/
* reconnect/circuit-open), offered to the internals holder beside the queue. The queue alone was
* enough for a direct `runTask` caller (the TaskStream carries these frames), but a COMPOSITION
* entry (verify/cascade) drains its inner legs' queues itself — without this seat, an inner leg's
* retry disclosure died inside the gate and the wire showed a silent stall. Fed the SAME frame
* object the queue receives, at the same moment; contained by the run's safe notifier (a throwing
* sink is swallowed, first failure per site disclosed, never faults the leg). Subagent
* frames still ride {@link onForwardEvent} — this seat is ONLY the run's own status type.
*/
onStatusEvent?: (event: Extract<TaskEvent, {
type: "status";
}>) => void;
/** The run-local per-tool-call detach hub. runtask creates it and exposes
* `TaskStream.detach(toolCallId)`; the hands Bash tool threads `signalFor(toolCallId)` into env.exec. */
detachHub?: import("../tool-detach.js").ToolDetachHub;
/** The run's forwarding-failure bookkeeping for {@link onForwardEvent} — ENGINE-owned and opaque,
* minted once per run by `Runner.runTaskStream` and carried by every later COPY of this object (the
* prepare re-spread, a placement-root rebind, a child's snapshot), which is what makes "announced once
* per run" true across the lanes that forward. A caller value is overridden like the refs beside it;
* absent ⇒ the door mints a per-object one, so containment never depends on the threading. */
forwardScope?: import("./forward-sink.js").ForwardSinkScope;
/**
* The Runner's per-session read-file-state seats (CC parity: readFileState is
* session-scoped). Always set by the Runner's own prepare call (overriding any caller value, like
* the peer refs beside it); absent on a standalone prepareTask, where the transcript-replay rung
* covers alone. A CACHE, never an authority — see {@link SessionReadFileStates}. Trusted internals
* channel, same posture as every other field here.
*/
sessionReadStates?: SessionReadFileStates;
/**
* The Runner's per-session permission-mode seats ({@link SessionModeSeats}): the chain a leg hands its children
* carries its SESSION's seat as the children's ceiling, and each leg binds itself to that seat once its prepare
* commits — so a session-scoped background child follows the session's current leg. Always set by the Runner's own
* prepare call (overriding any caller value); absent on a standalone prepareTask, which mints a seat of its own.
*/
sessionModeSeats?: SessionModeSeats;
/** Trusted system-injection sink (`Runner.runLocked` wires it to the live TaskStream queue plus the
* current harness lanes; never a public TaskSpec field), ADDRESSED TO THE SESSION rather than to this
* run: a producer wired here (a background shell, a watch, a delegation settle) keeps the closure long
* after the run that mounted it ended, and its frame is then routed to whatever run the SESSION has
* live — at that run's next turn boundary, the seat a steer lands on; only a session with NO live run parks it. */
onTaskNotification?: (notification: TaskNotificationPayload,
/** Injection tier (the ladder is LIVE): "next" = the running turn's next boundary
* (arrival order, consecutive frames batch); "later" = the run's would-otherwise-stop seat
* (never folded into work in progress); "now" = class-head + earliest natural boundary on this
* lane (interrupt authority belongs to the steer face, never to notifications). Internal
* producers declare their tier explicitly (§3.7 census — completion-class lanes are "next");
* the parameterless default "later" serves the external verb's omitting callers only. */
opts?: {
priority?: import("../task-notification.js").SystemInjectionPriority;
}) => void;
/**
* The PARENT run's notification injector (its runtask-wrapped
* `injectTaskNotification`), threaded into a CHILD's internals at spawn time so the child's
* SendMessage("main") uplink lands in the parent's queue at a turn boundary (the CC
* "delivered automatically" parent half). DELIBERATELY a separate field from
* {@link RunInternals.onTaskNotification}: that one is "inject into the run's own SESSION" (runtask
* wraps it as upstream-observer + session route), and reusing it for the child would tee every
* internal child notification (grandchild completions, monitor events) into the parent — double delivery.
*/
parentNotify?: (notification: TaskNotificationPayload, opts?: {
priority?: import("../task-notification.js").SystemInjectionPriority;
}) => unknown;
/**
* The ONE address `SendMessage("main")` names for this child — the session that hosts this child's
* lifecycle — paired with {@link parentNotify} at the same mint, three arms:
* · a fresh spawn: the SPAWNER's session, whose live injector is `parentNotify`;
* · a revival by that tree's ROOT (or any top-level session): the REVIVER's session, its injector;
* · a revival by a SIBLING child, or by a host verb with no session: the row's ROOT session, and
* NO `parentNotify` — a sibling is a peer the child addresses by name, not its main, and there is
* no live callback into the root from either reviver. Such a child's "main" send reads the
* honest not-deliverable text; its terminal still reaches the root through the root-anchor arm.
* The uplink receipt names this id so the model knows WHICH conversation "main" is. Absent on a
* top-level run. Trusted run-scoped channel (never a {@link TaskSpec} field).
*/
uplinkSessionId?: string;
/**
* The PARENT run's subagent-retain ledger, threaded to a CHILD so its
* SendMessage can continue a RETAINED SIBLING (the sibling's retain entry lives on the parent's
* ledger — without this, sibling resolution succeeds but delivery always reads not-retained).
* TRUSTED chain; read-only use (the sibling leg resumes through the same fenced resume face).
*/
parentRetainLedger?: import("../../agents/retain-ledger.js").SubagentRetainLedger;
/**
* Hands THIS run's notification injector back to the SPAWNER once the lane is
* live (runtask calls it with its wrapped `injectTaskNotification`). The spawner stores it on the
* child's registry handle so a parent/sibling SendMessage can deliver TO the RUNNING child at its
* next turn boundary (CC's in-memory `pendingMessages` pedestal, anchors/2.1.212
* messaging-runtime.md §2.5). The injector outlives its run safely: once this lane is over the frame
* is routed to whatever run the SESSION has live, and only a session with no live run parks it
* (PendingSessionNotifications) — exactly CC's durable-mailbox posture for an idle teammate (§2.4).
* TRUSTED chain (core spawner only); never reachable from TaskSpec.
*/
onNotifyInjectorReady?: (inject: (notification: TaskNotificationPayload, opts?: {
priority?: import("../task-notification.js").SystemInjectionPriority;
}) => Promise<import("../task-notification.js").NotificationInjectionOutcome>) => void;
/**
* Per-task ISOLATION hint, a TRUSTED run-scoped channel filled ONLY by a core caller
* (the workflow's `ctx.agent` when the SCRIPT passed `{ isolation: "worktree" }` as an OPTION) — NEVER a
* {@link TaskSpec} field (the untrusted-caller surface). Forwarded to
* {@link ExecutionEnvFactory} via {@link ExecutionEnvFactoryContext.isolation} so the trusted control-plane
* factory mints a git-worktree-rooted env for this agent; and it makes root resolution use the worktree
* env's own cwd (the worktree dir), bypassing `deps.rootPath`. Isolate-ONLY: the runtime never merges
* — the orchestrator script reads each worktree's result and decides verify/merge in userland.
* FAIL-CLOSED: a request the deployment cannot honor (no factory, or an observably non-isolated env)
* throws at prepare time — the child never starts; there is no silent fallback to the shared tree.
*/
isolation?: "worktree";
/**
* Sub-agent cwd inheritance (CC parity, 2026-07-03): the PARENT task's effective working root,
* filled ONLY by core delegation callers (the workflow's `ctx.agent` / `createSubagentTool`'s execute —
* NEVER a {@link TaskSpec} field). Forwarded to {@link ExecutionEnvFactory} via
* {@link ExecutionEnvFactoryContext.parentCwd} so a single-user/TOC factory can root the child env at the
* parent's cwd instead of an empty per-task sandbox. `isolation: "worktree"` wins over this when both set.
*/
parentCwd?: string;
/**
* The spawning run's file-history LINEAGE — the scope it records first-touch edits into and the
* TREE those records' keys are minted against (the canonical root spelling + the filesystem
* identity of {@link fileHistoryFilesystemIdentity}) — threaded VERBATIM by core delegation
* callers from {@link import("../types.js").ToolExecuteContext.fileHistoryLineage} (NEVER a
* {@link TaskSpec} field). {@link resolveFileHistoryScope} is the ONE reading: this run records
* into the lineage's scope iff its own tree coordinates BOTH equal the lineage's, and then
* re-exposes the SAME triple on its own ctx, so every same-tree descendant of a root session — at
* any depth — lands in the root session's scope (the fixed point), while a descendant on another
* tree (worktree isolation, explicit `cwd`, a fresh per-task sandbox) becomes the root of its own
* subtree's history. Absent on a top-level run, on a run that records no file history, and on a
* tier-3 revival (the reviver's lineage says nothing about the revived row's tree).
*/
fileHistoryLineage?: {
scope: string;
root: string;
fs: string;
};
/**
* [c209-D] — the EXPLICIT Agent.cwd request, distinct from the best-effort `parentCwd`
* inheritance hint above: inheritance may be silently ignored by a factory (or absent without one),
* but an explicit cwd the model asked for MUST either take effect or fail loud. prepareTask enforces
* the contract: no `executionEnvFactory` ⇒ throw `config.cwd_unsupported`; after env creation the
* env's actual cwd must canonically equal this path or the task fails `config.cwd_not_honored`; the
* task root follows the env's cwd (never `deps.rootPath`) when set. Filled ONLY by core delegation
* callers (ctx-injection posture, same as `parentCwd`).
*/
requestedCwd?: string;
/**
* Subagent steer verb (dogfood finding 2026-07-03, "中途插话"): the host run's opt-in
* SUBAGENT-STEER-HANDLE sink. When set, `createSubagentTool` runs each child via `runTaskStream`
* and emits a steer handle here (the model never sees the handle — same host-context-isolation
* posture as `onWorkflowAgentSpawn`). A deployment registers it by `taskId` to route a human steer
* into the running child (fenced-marker semantics matching the workflow agent handle). Threaded to
* the tool ctx as {@link ToolExecuteContext.onSubagentSpawn} and recursively down the delegation
* tree. Absent ⇒ children run non-steerable (prior behavior, zero overhead).
* SCOPE: only SYNC delegations emit a handle — a
* `run_in_background` child does not (poll/stop it via TaskOutput/TaskStop); wiring the background
* lane is a recorded follow-up, not an oversight a deployment should discover at runtime.
*/
onSubagentSpawn?: (handle: import("../../agents/subagent.js").SubagentSteerHandle) => void;
/**
* A TRUSTED run-scoped tool-ACTIVITY sink, filled ONLY by a core caller (the workflow's
* `ctx.agent`, to render a per-agent "last N tool calls" drill-down). Called synchronously at each tool start +
* end with structural data (name/phase/ids) — NEVER args/output (those carry untrusted/host data). NEVER a
* {@link TaskSpec} field. Absent ⇒ no activity capture (default).
*
* Reaches activity on FRESH and RESUMED runs alike — the durable-resume entry (`resumeStream`) threads
* `internals` too (see its parent-constraint re-supply snapshot), so a resumed leg's SUBSEQUENT tool calls
* hit this sink. One real boundary remains: the resume's already-approved pending call itself is
* executed by `applyResumeDecision`'s own callback, outside the frame-minting harness, so THAT one call
* emits no activity.
*/
onActivity?: (activity: ToolActivity) => void;
/**
* A TRUSTED run-scoped WORKSPACE-observation sink, filled ONLY by a core caller
* (the workflow's `ctx.agent` / `ctx.agentStream`, to record an isolated agent's worktree directory on the
* persisted run record). NEVER a {@link TaskSpec} field — same posture as {@link onActivity}.
*
* WHY the engine must hand this out: for `isolation: "worktree"` the worktree path is minted INSIDE
* {@link RunnerDeps.executionEnvFactory} and lands only on the per-task env's `cwd`; no runner-outward face
* (TaskEvent union / TaskStream / TaskResult) carries it back. An orchestrator therefore could not record
* WHERE its isolated agent worked — the recovery path after an interrupted run had to enumerate the
* worktrees directory and guess, which is exactly the contradiction recorded against the
* "isolate-only, userland decides verify/merge" intent.
*
* Called at most ONCE per prepared run with the task's FINAL working root (see {@link ResolvedWorkspace}),
* after the durable-resume restore may have re-rooted it. Observe-only: a throwing sink is swallowed (an
* observation must never fault a prepare that already minted a workspace).
*
* Like {@link onActivity}, this seat rides `internals` on FRESH and RESUMED runs alike (`resumeStream`
* threads it too) — a resumed leg's restore re-fires the observation with the settled root.
*/
onWorkspaceResolved?: (workspace: ResolvedWorkspace) => void;
/**
* A TRUSTED run-scoped WIRING-MANIFEST sink, filled ONLY by a core caller and NEVER a {@link
* TaskSpec} field — the posture of {@link onActivity} and {@link onWorkspaceResolved} beside it.
* Handed the very object this leg's `wiring_manifest` frame carries, at the moment that frame is
* pushed: ONCE per prepared leg, after prepare and before the leg's first model or tool
* interaction. Rides `internals` on FRESH and RESUMED legs alike, because the mint sits in the
* run body both entries share.
*
* WHY a seat, when the frame already rides the stream: a COMPOSITION entry (the verify gate, the
* cascade) drains its inner legs' queues ITSELF, so the host holds no TaskStream for any of them
* and an inner leg's manifest dies inside the gate — the hole {@link onStatusEvent} was opened
* for, one frame family over. A direct `runTask` caller needs neither seat: its own stream
* carries both frames.
*
* `leg` is the LEG IDENTITY, and the engine mints NONE of it. It is present only where a
* composition labelled the leg it dispatched: the verify entries hand each leg a per-dispatch
* copy of this object whose sink closes over that leg's identity, so a consumer can tell the
* implementation leg from pass 2's verifier without inferring it from arrival order (the
* ordering is all the sibling sinks have — see the attribution paragraph on `verifyCompleted`).
* ABSENT therefore means "nobody labelled this leg", never "implementation": a plain `runTask`,
* a workflow agent call and a cascade rung all reach this sink with no identity, and reading one
* in would be the engine fabricating a fact it does not hold. NOT the same axis as the manifest's
* OWN `leg` field ({@link WiringManifest.leg}, `root` / `child` / `resume`), which is a fact about
* how the ENGINE reached this run; this one is a fact about the composition that dispatched it, and
* a resumed fix leg carries both at once — `manifest.leg.kind === "resume"` and `leg.kind === "fix"`
* are both true of it. `pass` is 1-based over the gate's verification rounds: a `verifier` leg
* carries the one it verifies, a `fix` leg the one whose findings it answers, and the entry
* implementation leg carries 0 — before the first. The axis is relative to the gate INVOCATION,
* not to the session's history: a resume entry's work leg is THAT invocation's implementation leg
* (`impl`, pass 0) even when the checkpoint it resumes was minted inside a previous invocation's
* fix pass, because the invocation's own pass counter (`VerificationResult.rounds`) restarts with
* it — one axis for both fields, rather than a leg that reads "fix pass 1" inside a result whose
* rounds begin at 1 again.
*
* Observe-only and failure-isolated through the SAME per-run notifier as {@link onActivity} (one
* isolation scope for every sink on this object, so a broken sink is counted and disclosed once,
* not once per site family): a throw — or an `async` sink returning a rejecting promise — is
* contained and never faults the leg whose manifest it was handed.
*/
onWiringManifest?: (manifest: WiringManifest, leg?: {
readonly kind: "impl" | "verifier" | "fix";
readonly round: number;
}) => void;
}
/**
* The payload of {@link RunInternals.onWorkspaceResolved}: the working root this task actually
* runs on, plus whether that root is the task's OWN isolated workspace.
*
* `isolated: true` means "`cwd` is this agent's own git worktree": it is reported only when
* {@link RunInternals.isolation} was requested AND the fail-closed worktree enforcement accepted the env, so a
* consumer may treat the path as private to this agent (safe to diff / merge / remove in userland). A
* non-isolated run reports its plain root with `isolated: false` — the observation face stays complete, while
* the honest non-claim keeps a consumer from mistaking a SHARED tree for its own worktree.
*/
export interface ResolvedWorkspace {
/** The task's effective working root — the same value every fence / LSP / prompt consumer uses. */
cwd: string;
/** True only for an accepted `isolation: "worktree"` request (⇒ `cwd` is this agent's own worktree). */
isolated: boolean;
/** True when the task's execution env is REMOTE: `cwd` then names a path INSIDE the remote
* target (a per-task sandbox on the shape-(d) exemption leg, e.g. `/workspace`), which does not exist —
* or names an unrelated directory — on the host. `isolated && !remote` is the only combination under
* which `cwd` is a host path a consumer may diff / merge / remove. */
remote: boolean;
}
/**
* The resolver's answer, with the WITHHELD cause beside the value.
*
* The old signature returned a bare `unknown`, which collapsed three different worlds into one
* `undefined`: "this tool declares no preview at all", "the projection threw / could not be
* serialized", and "the projection declined to speak for these args". The middle one is the case an
* approval surface must be told about — a person is being asked to approve something whose display
* projection could NOT be produced — and it was indistinguishable from the other two.
*
* `withheld` names the cause; it is ORTHOGONAL to `preview` presence, deliberately: the oversize arm
* still ships its truncation stub (a surface that renders it tells the person the size, which is more
* useful than nothing), and the stub alone never carried the fact that the real projection is missing.
*/
export interface ResolvedApprovalPreview {
/** The display projection, when one could be produced (or the oversize truncation stub). */
preview?: unknown;
/** Present ⇔ the tool DECLARED a preview and the gate could not present a usable one:
* `"oversize"` — produced but past the 16KiB display bound (the stub rides `preview`);
* `"unavailable"` — the projection threw, or its value has no JSON serialization.
* ABSENT covers both "no preview declared" and "the projection returned nothing to show" — neither
* is a withholding, and claiming one would make every preview-less tool look suppressed. */
withheld?: "oversize" | "unavailable";
}
/** The in-stream ask lane: the six closures the park closure and the gate station consume. Built as ONE value so
* "the lane exists" is one fact, not six independently-undefined seats. */
export interface AskLane {
/** The shared run+call signal composition, bound to this task's run signal once. */
composedCallSignal: (callSignal: AbortSignal | undefined) => AbortSignal;
/** The abort-bound policy check, or undefined when no policy is wired (→ the gate reads allow). */
adjudicate: ((req: ToolCallRequest, callSignal?: AbortSignal) => Promise<PermissionResult>) | undefined;
/** The approval display projection over the live roster (alias-aware; oversize/unavailable named). */
approvalPreviewOf: (toolName: string, args: unknown) => ResolvedApprovalPreview;
/** THE synchronous ask mint: resolves a surviving `ask` through the live seat, books the ledger. */
resolveAskBound: (decision: PermissionResult, req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ResolvedAsk>;
/** The pre-wrapped deny observer, or undefined when the deployment wired no `permissionDenied` hook. */
notifyPermissionDenied: ((payload: PermissionDeniedPayload) => Promise<void>) | undefined;
/** The deployment-facing half of a crashing PreToolUse hook (swallow-guarded). */
notifyHookError: (err: unknown) => void;
}
/** How the put-commit + put-failure saga ended (see `commitSuspendSaga` inside the phase for each arm's meaning). */
export type SuspendSagaOutcome = {
tag: "committed";
} | {
tag: "cut";
} | {
tag: "absent";
reason?: string;
} | {
tag: "unknown";
reason: string;
} | {
tag: "compensation_failed";
reason: string;
};
/** The durable settlement machinery the boundary parks and the park closure consume. Built as ONE value so "the saga
* exists" is one fact, not eight independently-undefined seats. */
export interface SuspendSaga {
/** THIS leg's spend so far in micro-USD (own + nested delta), read at mint time. */
inFlightSpendMicroUsd: () => number;
/** The §4.bis correctness-state whitelist, serialized from the live values at suspend. */
serializeCheckpointState: (workspaceHandle: WorkspaceHandle | undefined, parkedSpendMicroUsd?: number) => SerializedCheckpointState;
/** The shared paused-VM compensation, bound to this task's failure ledger and error face. */
compensatePausedVM: (remoteEnv: RemoteExecutionEnv, snapshotId: SnapshotId) => Promise<{
ok: true;
} | {
ok: false;
reason: string;
}>;
/** The put-commit + put-failure split — the point of no return, tagged. */
commitSuspendSaga: (token: CheckpointToken, cp: Checkpoint, remoteEnv: RemoteExecutionEnv | undefined, remoteHandle: WorkspaceHandle | undefined, cutSignal?: AbortSignal) => Promise<SuspendSagaOutcome>;
/** The shared suspend-loop-cap guard (safety state first, diagnostic second). */
suspendLoopCapHit: (count: number, cap: number | undefined, detail: string) => boolean;
/** The `suspendVM` target by CAPABILITY, or undefined. */
suspendableEnv: RemoteExecutionEnv | undefined;
/** The park-only remote target (non-suspendable, externally durable), or undefined. */
parkOnlyRemoteEnv: RemoteExecutionEnv | undefined;
/** The park-only identity capture (snapshotId stripped, `restoreMode: "park_only"`). */
parkOnlyHandle: (env: RemoteExecutionEnv) => WorkspaceHandle;
}
/** The park closure and the content-ask routing closure the gate station hands to `runToolGate`. Built as ONE value so
* "the park lane's closures exist" is one fact; `suspendAsk` stays optional INSIDE it because the park predicate (a store,
* and something to park for) is a narrower condition than the gate machinery's activation. */
export interface ParkAsk {
/** The gate's content-ask routing closure (total by construction; answers `unavailable` with no live face). */
resolveContentAsk: NonNullable<ToolGateInput["resolveContentAsk"]>;
/** The durable park closure (twelve positional seats then the structural carry), or undefined when the park
* lane is not armed (the gate then keeps the synchronous onAsk path). */
suspendAsk: ToolGateInput["suspendAsk"];
}
/**
* `runLocked`'s per-run mutable state, EXPLICIT. These fields started as loose
* `let`/`const` bindings in the R2 declaration hub; the extraction kept every assignment at its
* original site and order (binding-form-only rewrite — the event-snapshot pin held the run
* byte-identical), and later work has since added fields directly here (see the per-field notes).
* Grouped by lifecycle semantics per the B15 pre-recon partition. Function members start as inert
* placeholders and are assigned at their ORIGINAL declaration sites (the original code could not
* call them earlier either — TDZ proved it). Not on the npm face; this extraction is what enabled
* createTurnBoundary / createHarnessHandlers (both are run lanes of their own).
*
* WRITERS, by group (the driver mints the zero value, initial-run-state.ts, and hands it to every
* lane as a borrowed-mutable seat): `telemetry` / `degrade` / `limits` / `budget` initial values and function
* members — the telemetry-and-budget-seats lane; `counters` / `attach` initial values — the attachment-seats lane
* (the clock lane zeroes `counters.walltimeSyncBackstopFired`; the tool-mount facts only read); the per-turn writes — the harness-handlers lane
* (`turn`, `counters`, `limits`, `budget`), the turn-boundary lane (`counters`, `attach`, `budget`) and the
* recovery lane (`counters.compactionFloor`, `attach.attachState`); the settle-time reads stay the driver's.
*/
export interface RunState {
telemetry: {
cacheFamily: ReturnType<typeof cacheFamilyOf>;
pricing: ModelPricing;
/** Whether the CURRENT serving model has a configured price table (a
* `RunnerDeps.pricing` entry or a `Model.cost` declaration). When neither exists,
* `modelCostToPricing(undefined)` yields an all-zero table and every computed cost is a
* fabricated 0 — indistinguishable from "declared free". This flag keeps the two apart so
* the DISCLOSURE faces (trace frames, public stats) can go absent instead of lying; the
* internal budget coordinate (`stats.costMicroUsd`) stays numeric either way. */
pricingConfigured: boolean;
/** Sticky — some spend was ACCOUNTED while `pricingConfigured` was false (a mid-task
* model switch can flip pricedness, so per-call state isn't enough for the task total). Once
* true, the task-level cost total is an undercount and its faces are omitted. */
unpricedSpend: boolean;
tracer: TaskSpec["tracer"];
taskId: string;
/** The run's engine-minted identity, so the task.start/task.end pair carries it from ONE
* seat (the same place taskId is published). Empty only before the run body assigns it. */
runId: string;
taskStart: number;
taskStartMonotonic: number;
cacheBreakReported: boolean;
};
degrade: {
degradeToModel: Model | undefined;
degraded: TaskResult["degraded"];
outputErrorStreak: number;
outputInvalid: boolean;
recordDegraded: (info: NonNullable<TaskResult["degraded"]>, toModel?: Model) => void;
};
limits: {
turnsExceeded: boolean;
budgetHit: "exceeded" | "precall" | undefined;
/** WHICH budget axis tripped, set at the same instant as {@link budgetHit} so the
* terminal can name it (`limits.max_tokens_exceeded` vs `limits.max_cost_exceeded`). Tokens
* outrank cost when both are over at the same boundary (the documented attribution order). */
budgetAxis: BudgetAxis | undefined;
/** The TYPED terminal an EXTERNAL cause (a dying execution environment, an
* exhausted governance window) recorded when it could not suspend the run durably. Set together with
* the abort it triggers, and adopted as the run's `threw` once the loop settles, so the caller gets
* `env.lifetime_expired` / `usage.window_exhausted` instead of the anonymous error an abort produces.
* Same carrier shape as `brainCallGuardrailRef.timedOut`, and for the same reason: the harness turns a
* loop throw into an error assistant message that strips the error object. */
platformTerminal: (Error & {
code?: string;
retryAfterMs?: number;
}) | undefined;
outputRetryCap: number;
effectiveMaxTurns: number | undefined;
};
budget: {
remainingMicroUsd: number | undefined;
maxCostMicroUsd: number | undefined;
/** Tokens still available on the cross-slice allocation (`resourceSuspend.totalTokens`
* minus everything earlier slices spent), or `undefined` when no total is set. */
remainingTokens: number | undefined;
/** This slice's EFFECTIVE token ceiling = `min(limits.maxTokens, remainingTokens)`. The exact mirror
* of `maxCostMicroUsd`. `undefined` = no token ceiling; `0` is a real, exhausted ceiling. */
maxTokensWindow: number | undefined;
/** The axis that is over its ceiling right now, or `undefined` when neither is — a predicate that
* ALSO carries the attribution, so no caller can set `budgetHit` without an axis. */
overBudget: () => BudgetAxis | undefined;
streamCancel: boolean;
callOutputChars: number;
lastStreamBudgetCheck: number;
projectedOverBudget: () => BudgetAxis | undefined;
};
turn: {
callStartAt: number | undefined;
firstTokenAt: number | undefined;
turnUsage: {
inputTokens: number;
totalInputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheWriteTokens: number;
costMicroUsd: number;
} | undefined;
turnUsageMissing: boolean;
turnStopReason: string | undefined;
/** The PROVIDER-REPORTED model name of this turn's LAST assistant message, when it differed from the
* requested id — surfaced on `turn_end.responseModel`, reset with the rest of the per-turn
* bookkeeping. Same shape as `turnStopReason`: last-message-wins, absent by default. */
turnResponseModel: string | undefined;
lastTurnHadToolCalls: boolean;
/** B15 三期使能:postToolBatch 的本回合聚集批(每回合边界消费并重置 — 回合语义,故归 turn)。 */
toolBatch: PostToolBatchCall[];
};
counters: {
/** How many LIMIT-APPROACH frames have been injected this run (0..2). Also the
* one-shot latch itself: each frame fires when the count is still below its index. */
approachNoticesSent: number;
/** B15 三期使能:R9 引用的 R3/压缩区 latch(walltime 收尾 latch — preemptIgnoredReported 同族)。 */
walltimeSyncBackstopFired: boolean;
/** B15 三期终件补遗:压缩地板/背压 latch — rs.counters.compactionFloor 跨 drainManualCompact(写)/R9(读写)/收尾(读)三方共享,rs.counters.trimForceBackoff 为 R9 跨调用 latch(同款只改绑定不改时机)。 */
compactionFloor: number;
trimForceBackoff: boolean;
/** Landed compactions (boundary + prompt-too-long lanes) and the last one's kind — the `TaskResult.compaction` source (the breaker state is read off the breaker itself). */
compactionPasses: number;
compactionKind?: import("../task-notification.js").CompactionKind;
repetitionCuts: number;
repetitionSpared: number;
repetitionEvents: Array<{
turn: number;
action: "cut" | "spared";
rule: "char-run" | "unit-loop";
period: number;
reps: number;
segment: string;
}>;
REPETITION_EVENTS_CAP: number;
preemptIgnoredReported: boolean;
wroteThisRun: boolean;
finalVerifyInjections: number;
groundingSignalPreR9: boolean;
groundingSignalPostR9: boolean;
cadenceTurns: number;
};
attach: {
attachmentsCfg: TaskSpec["attachments"];
agentListingOn: boolean;
skillsListingOn: boolean;
attachState: ReturnType<typeof createAttachmentState> | undefined;
dateState: {
announcedDate: string;
} | undefined;
instrProbe: RunnerDeps["probeInstructionSources"];
instrState: {
lastAnnouncedHash: Map<string, string | null>;
} | undefined;
/** The Workflow tool card's advisory size guideline as last ANNOUNCED to the model,
* plus the live boundary-time read (Prepared.workflowSizeGuideline). Same deliberate posture as
* `dateState`/`instrState`: NOT an AttachmentState member, one field, zero reducers. */
sizeGuidelineState: {
announcedGuideline: WorkflowSizeGuideline;
current: () => WorkflowSizeGuideline;
} | undefined;
/** B15 三期使能:boundary 注入计数(→ stats.mechanisms.attachmentsInjected;attachState 注入族语义,故归 attach)。 */
attachmentsInjected: number;
};
}
/** One accepted live steer's REPLAY identity, the twin of the parked
* queue's `samePendingSteerPayload`. `payload` is the exact string handed to the harness, so the text,
* the speaker envelope and the trust framing all fold into it; `trusted` is kept SEPARATELY from those
* framed bytes so a text that spells its own `<system-reminder>` wrapper can never read as the trusted
* arm's byte-equal twin. `actor` carries the attribution the ledger frame reports. */
export interface AcceptedSteerInput {
payload: string;
trusted: boolean;
/** The NORMALIZED priority the call delivered under (absent input ⇒ "next"),
* the replay identity's sixth axis: same id + same text at a DIFFERENT tier is not a replay —
* a `next` input re-sent as `now` answering "idempotent success" without the interrupt would be
* a disposition lie, so it refuses `duplicate_input_id` like any other identity mismatch. */
priority: SystemInjectionPriority;
actor?: ActorAssertion;
}
/**
* Config re-supplied to {@link Runner.resume}. A suspended task's tools / model / policy /
* hooks cannot be reconstructed from a checkpoint token (the session stores neither tool implementations
* nor the hand band), so the caller's trusted control plane re-supplies the same {@link TaskSpec} it ran
* with — minus the conversation bits: `sessionId` comes from the checkpoint and `objective` is replaced by
* an internally-generated continuation, so both are omitted.
*/
export type ResumeTaskConfig = Omit<TaskSpec, "objective" | "sessionId">;
/** Resume plan threaded from {@link Runner.resume} into the shared run loop. */
export interface ResumeRun {
cp: Checkpoint;
/**
* Did the APPROVED ACTION actually begin executing?
*
* The reopen compensation used to key on an error-code whitelist (`resume.env_failed` /
* `resume.session_not_found` / `resume.tool_unavailable`). That is a proxy for the thing that actually
* matters, and every time a NEW way to exit without running the action appeared, the whitelist did not
* know about it: a caller's abort landing anywhere between the CAS and `tool.execute` — across
* `SessionStore.acquire` (pluggable, cross-network on a durable backend), the MCP reconnect, the env
* rebuild, `resumeVM`, tool materialization — produced a `failed` result with NO errorCode, no reopen,
* and a human approval consumed for work that never happened. Two rounds of fixes each
* moved that window rather than closing it, because both extended the whitelist.
*
* This binds the compensation to the FACT instead: set the instant before `tool.execute` for the gated
* call (the same `onExecuteStart` signal the orphan-reconcile split already trusts). Every terminal —
* throw OR returned result — asks one question: was the checkpoint consumed while this stayed false?
*/
pendingActionStarted?: boolean;
/** Validated against `cp.gate.kind` at the resume entry: human/irreversible_ask→`policy_ask`,
* resource_limit→`resource_limit`, needs_review→`dry_run_review`,
* plan_review→`plan_review`. The gate-match guard in `resumeStream` enforces the
* correlation. `wake` is the NON-GATE arm — only a checkpoint awaiting NO gate
* decision passes the resume entry with it (gate purity, `wake.gate_pending`); the run loop skips
* `applyResumeDecision` for it (no pending action to resolve) and re-enters via the continuation +
* pendingSteer tail alone. */
outcome: Extract<ResumeOutcome, {
gate: "policy_ask" | "resource_limit" | "dry_run_review" | "plan_review" | "wake";
}>;
/** The wake's own operator message, validated (`validatePendingSteer`) at the
* resume entry and carried SEPARATELY from the checkpoint's parked `pendingSteer`: a message-bearing
* wake of a checkpoint that ALSO holds a parked steer must deliver BOTH (park order: parked first,
* wake message second), each under its own trusted framing — the old merge-into-the-slot shape
* silently DISPLACED the parked (undelivered) supervisor steer. Wake outcomes only. */
wakeMessage?: Omit<PendingSteerEntry, "seq">;
/** The userPromptSubmit screen's `additionalContext` for {@link wakeMessage},
* captured at the resume ENTRY (the message is screened once, pre-CAS, on the resuming process's
* hook) and delivered by the drain as the engine's own reminder AHEAD of the wake frame — carrying
* it forward is what keeps the hook single-run (re-screening at the drain would be the double-run
* §4.3-3 reserves for the cross-process parked leg). Present only when a wake message passed a
* screen that supplied context. */
wakeMessageHookContext?: string;
/** Compensation hook: called iff the resumed run fails with `resume.env_failed` (post-CAS
* workspace `resumeVM` failed) OR `resume.tool_unavailable` (P-7: the approved tool vanished) — in both
* the CAS already consumed the checkpoint but the pending action never ran. `resumeStream` supplies a
* closure that reopens the checkpoint (`resolved → pending`) so a retry re-resumes the SAME suspended work
* instead of losing it to a forced "re-initiate" (reopen-by-reason): the `reason` is
* recorded on the reopened row so the next re-resume validates per reason — an `env_failed` reopen must
* replay the persisted winner (a system retry of the approved action), while a `tool_unavailable` reopen
* lets a human re-decide with the tool present (a fresh decision is allowed — preserves P-7). */
onEnvRestoreFailed?: (reason: ReopenReason) => Promise<void>;
/** The row's execution-record verb, bound to THIS resume's token and scope by `resumeStream` (the
* same closure form as {@link onEnvRestoreFailed}, but REQUIRED: the store contract requires the
* verb, so there is no store this seat could be absent for). The pending-call resolver calls it
* exactly once per resolution, right after it emits the resolved call's `tool_end` frame, with the
* {@link import("../gate-outcome.js").GateOutcome} that frame carried — every disposition, the
* executed allow included (see `CheckpointRow.executionOutcome`: absence means unknown, never
* allowed). The word it answers is read through `EXECUTION_RECORD_LEAVES_ROW_UNRECORDED`; a
* conflict throw propagates (an engine defect, not a store condition). */
recordExecutionOutcome: (gate: import("../gate-outcome.js").GateOutcome) => Promise<ExecutionOutcomeRecordWord>;
/** Set in the run body right after `applyResumeDecision` completes: the negative-
* decision twin of `pendingActionStarted`. A reject/deny consumes its gate BY BEING DELIVERED, and
* this bit is the delivery fact — every throw-arm cause (prepare failure, `session_not_found`, a
* pre-delivery abort) and the walltime-exhausted settle fire BEFORE it is set, so an undelivered
* negative decision still reopens as `env_failed` (the retry replays the persisted decision; it
* never re-asks), closing the loss class the first cut reopened. */
decisionDelivered?: boolean;
}
/**
* What {@link TaskStream.destroy} needs to reap a SUSPENDED task. Captured (stashed) onto
* the stream handle the moment a run assembles `status:"suspended"` — `finish()` deliberately left the
* paused env + committed checkpoint alive for resume, so without this stash they'd be unreachable (the
* `Prepared`, and its `ownedEnv`, are GC'd once the run promise settles). `env` is `undefined` for a
* process-local suspend (no remote env to reap — only the checkpoint is expired).
*/
export interface SuspendReap {
env: Prepared["ownedEnv"];
token: CheckpointToken;
scope: string;
store: CheckpointStore;
/** The suspended task's session — for onError correlation only (the `token` is a capability and must
* NEVER be logged, checkpoint-store.ts §6). */
sessionId: string;
}
/** B15 三期终件 — R9(回合边界全域:压缩/attachments 注入/提醒节律/预算判决)工厂。
* 正文自 runLocked 逐字搬迁(this 面经 runnerHooks 闭包打包,B13 #coreView 同款);模块级符号
* 同文件直见不经 deps;可变运行态全在 rs(B15 二期),deps 全部为 const 稳定引用。 */
export interface TurnBoundaryDeps {
spec: TaskSpec;
queue: PushQueue<TaskEvent>;
/** The run loop's own latch — read for `userHalted` (the public `TaskStream.halt()` verb, which sets
* the harness stop latch directly, not through the ctx primitive `Prepared.stopRequestedRef`
* mirrors): a halted boundary has no next request either, so the bundle is not collected there. */
loopLatch: {
readonly userHalted: boolean;
};
manualCompactRef: {
requested: boolean;
waiters: Array<{
resolve: (outcome: CompactOutcome) => void;
signal?: AbortSignal;
instructions?: string;
}>;
};
todoToolMounted: boolean;
taskToolsMounted: boolean;
walltimeMonotonicDeadline: number | undefined;
timeout: {
fired: boolean;
latenessMs?: number;
clear: () => void;
};
ident: () => {
eventId: string;
parentToolCallId?: string;
sourceTaskId?: string;
};
postToolBatchHook: ((batch: PostToolBatchCall[], meta?: {
injectedThisTurn: "final_verification" | "finalize";
}, ctx?: import("../hooks.js").PostToolBatchContext) => PostToolBatchResult | undefined | Promise<PostToolBatchResult | undefined>) | undefined;
batchArgs: Map<string, unknown> | undefined;
compactionBrain: Brain;
withinTaskCompaction: () => boolean;
compactionBreaker: {
failures: number;
};
windowSafetyOptions: (mainModel: Model) => Pick<MaybeCompactOptions, "fallbackBudget" | "onWindowSafety">;
rapidRefill: RapidRefillState;
drainManualCompact: (outcome: CompactOutcome) => void;
runnerHooks: {
onError: RunnerDeps["onError"];
};
/** The Runner's deployment deps, read LIVE ({@link RunnerDepsSeat}): the compaction seams (compaction-seams.ts) the
* boundary's compaction pass reaches down for read the summary provider, the hooks slot and the error sink through it
* on every pass — the three Runner methods that used to sit on `runnerHooks` as delegates. */
runner: RunnerDepsSeat;
}
/** R5(harness 事件处理器族)的依赖包。全部为 runLocked 内声明顺序早于
* 工厂调用点的 const 稳定引用 + 三个 runLocked 形参;this 面(deps.onError)经 runnerHooks 打包
* (B13 #coreView / R9 TurnBoundaryDeps 同款)。*/
/** What a PRODUCER of a `tool_progress` frame knows: the FACTS it observed. The frame minus everything only
* the run supplies — its discriminator, its {@link TaskEventIdentity} stamp, and the elapsed time, which the
* lane computes from its own per-call origin. Derived from the union member, so seat and frame cannot drift. */
export type ToolProgressBody = Omit<Extract<TaskEvent, {
type: "tool_progress";
}>, "type" | "elapsedTimeSeconds" | keyof TaskEventIdentity>;
/** The run's TOOL-PROGRESS lane: the ONE mint of `tool_progress` plus the heartbeat clock over the calls
* in flight. Declared on the FLOOR because both registry-side readers sit above its implementation (the
* driver builds it, the harness-handler lane drives it) and the Prepared seat that carries it to the tool
* band is here too. `createToolProgressLane` (tool-progress-lane.ts, layer 1) is its one implementation. */
export interface ToolProgressLane {
/** Offer ONE frame — from a tool that observes its own progress (the shell's per-second tick) or from
* the lane's own heartbeat. The lane stamps the elapsed time, DROPS the offer when it would land inside
* the cadence floor, and resets that call's beat clock when it does not. NEVER THROWS: a tool calls this
* from inside an environment output callback, where a throw kills the command it is describing. */
emit(body: ToolProgressBody): void;
/** A tool call entered execution: it joins the heartbeat's in-flight set and its clock starts, stamped
* by the LANE (one clock owns one number — see its module header on why that clock is monotonic). */
noteStart(toolCallId: string, toolName: string): void;
/** A tool call settled: it leaves the in-flight set. */
noteEnd(toolCallId: string): void;
/** Drop every in-flight call and disarm the timer (turn end / teardown): no beating into the next turn. */
clear(): void;
}
export interface HarnessHandlersDeps {
spec: TaskSpec;
queue: PushQueue<TaskEvent>;
internals: RunInternals | undefined;
ident: () => {
eventId: string;
parentToolCallId?: string;
sourceTaskId?: string;
};
parentToolCallId: string | undefined;
subagentName: string | undefined;
pushContent: (e: TaskEvent) => void;
emitCommitted: (entryId: string, role: "user" | "assistant" | "toolResult", toolCallId?: string) => void;
startedToolCallIds: Set<string>;
toolStartAt: Map<string, number>;
/** The run's tool-progress lane: the handlers tell it when a call enters and leaves execution, and
* clear it with the rest of the per-call bookkeeping at turn end. */
toolProgress: ToolProgressLane;
writeFamilyOf: (name: string) => WriteFamily | undefined;
toolLabels: Map<string, string>;
postToolBatchHook: ((batch: PostToolBatchCall[], meta?: {
injectedThisTurn: "final_verification" | "finalize";
}, ctx?: import("../hooks.js").PostToolBatchContext) => PostToolBatchResult | undefined | Promise<PostToolBatchResult | undefined>) | undefined;
batchArgs: Map<string, unknown> | undefined;
runnerHooks: {
onError: RunnerDeps["onError"];
onNotice: RunnerDeps["onNotice"];
};
/** The run's ONE isolation scope for the {@link RunInternals} observation sinks, minted by the
* identity-wiring lane (which calls the first of them) and shared with these handlers, so the
* "first failure per site" disclosure is one account for the whole run rather than one per lane. */
internalsNotifier: SafeNotifier;
}
/**
* The Runner as the modules it drives and the compositions over it see it — the delegation tools
* (Agent / SendMessage / AgentTranscript), the repair, verify, cascade, teacher and team loops, the
* workflow and goal orchestrations, the scenario registry, the HTTP server, `warmResume`, and the
* orchestrator's own trusted `runnerSelf` seat. Every one of them used to name the `Runner` CLASS
* through `import type`, which is a type edge from below the runner back up into it — a cycle the
* direction gate could only carry as an exemption, and one that reached the orchestrator through the
* delegation tools (prepare-task → send-message-tool → runtask).
*
* THE MEMBER SET IS MEASURED, NOT DESIGNED: it is exactly the union of what those consumers read off a
* Runner today (a TS-Program census over `src/`, pinned in test/runner-self-seat.test.ts so a member
* nobody reads reds as dead weight and a consumer reaching for a member outside it reds as a widening).
* `swapDeps`, `sideQuery` and the constructor are NOT here — no consumer reaches them; a host that
* needs them holds the class. The class implements this interface, so the two cannot drift apart.
*
* Signatures are spelled out (not `Pick<Runner, …>`), because naming the class from here would be the
* very edge this seat exists to remove.
*/
/**
* The Runner's deployment deps as a LIVE read for the lanes that read them after an await (the resume ladder's
* rungs). `Runner.deps` is not a constant: `swapDeps` replaces the object, so a lane handed the object at its
* call would read a stale catalog — and a stale anything — where the one-function ladder read `this.deps.x` at the
* instant of the read. The seat is the Runner's own view (a getter over the private field), so `runner.deps.x`
* in a lane is exactly `this.deps.x` in the method it came from.
*/
export interface RunnerDepsSeat {
/** The deps object the Runner holds RIGHT NOW — never cache it across an await. */
readonly deps: RunnerDeps;
}
export interface RunnerSelfSeat {
/** The session store this Runner was constructed over — the delegation tools acquire / release / fork through it. */
readonly sessions: SessionStore;
/** The deployment agent + model catalog, READ-ONLY (a fresh copy per read; the definitions are the shared objects); `roles` = `RunnerDeps.roles` (the delegation seat's `childModelSeat` reads `roles.subagent`). */
readonly agentCatalog: {
agents?: AgentDefinition[];
builtinAgents?: boolean;
models?: Record<string, Model>;
roles?: ModelRoles;
};
/** The context-window overlay a leg on `ref` reads RIGHT NOW (`RunnerDeps.contextSettings`, tier aliases followed
* against the catalog in force): prepare hands its leg a call-time reader over it, because this is the one seat a
* running leg re-reads (geometry, not route — see `context-settings.ts`). */
contextOverlayOf?(ref: import("../model-seat.js").ModelRef | undefined): import("../context-settings.js").ContextWindowOverlay | undefined;
/** The deployment-level gating baseline (deps toolPolicy / hooks) as delegate wrappers — for assemblers that compose with it. */
readonly gateBaseline: {
toolPolicy?: RunnerDeps["toolPolicy"];
hooks?: RunnerDeps["hooks"];
};
/** Fire `RunnerDeps.onTaskOutcome` through the swallow-guarded chokepoint (thin compositions emit at their terminal state). */
emitTaskOutcome(outcome: TaskOutcome): void;
/** Run a task to completion. `internals` is the TRUSTED run-scoped channel (never a TaskSpec field). */
runTask(spec: TaskSpec, internals?: RunInternals): Promise<TaskResult>;
/** Streaming form of {@link RunnerSelfSeat.runTask}; `resume` is the run loop's resume plan, `internals` the trusted channel. */
runTaskStream(spec: TaskSpec, resume?: ResumeRun, internals?: RunInternals): TaskStream;
/** Resume a suspended task from its checkpoint token with the caller's decision, to completion. */
resume(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig, internals?: RunInternals): Promise<TaskResult>;
/** Streaming form of {@link RunnerSelfSeat.resume}: the pre-CAS guards and the CAS run first, then the live stream is returned. */
resumeStream(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig, internals?: RunInternals): Promise<TaskStream>;
}
/**
* The orchestrator's entry (`prepareTask`, prepare-task.ts) as ONE contract: the notification lane's
* `prepareTask` seat names it, and the orchestrator pins its own declaration against it (`PrepareTaskIsTheContract`), so a
* signature change reds at the declaration — not at the driver's hand-in, and not as a seat spelled a second time. It lives
* on the floor because a lane may not name the orchestrator (docs/LAYERING.json: layer 4 sits above the lanes). Positional,
* exactly as the entry is declared — the seat carries the orchestrator's function itself, not a lane Input.
*/
export type PrepareTaskFn = (spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals, runnerSelf?: RunnerSelfSeat, runIdSink?: {
runId?: string;
}, holdSessionLock?: (sessionId: string) => Promise<void>) => Promise<Prepared>;
/**
* The Result of an INSTALLING phase: a lane whose every product is installed on a seat it borrowed
* (the harness's stop gate and recovery chain, the run state's counter groups, the stream's result setter, the claimed
* row's registries) hands nothing back, and says so by extending this marker. The marker is machine-read by gate:phase-api
* a Result with no members MUST extend it, a Result with members MAY NOT, and no Result
* inherits anything else — so an empty Result is a stated fact about the phase, never an interface someone forgot to
* fill, and a phase that starts handing a product back must drop the declaration in the same edit. Six lanes carry it:
* the attachment seats, the stop gate, the recovery lanes, the settle and teardown (run-), the preflight and the claim
* (resume-).
*/
export interface InstallingPhaseResult {
}
/** The live-task handle `runLocked` publishes once the harness exists: the harness + abort
* controller, the loop-liveness latch (`ended` flips when the single `harness.prompt` settles; `userInterrupted`
* / `userHalted` are the interrupt and halt verbs' attribution seats), the run's reminder mark, its session and
* engine-minted run id, and the hook bound + identity envelope the steer entrance screen runs under. */
export interface LiveHandle {
harness: AgentHarness;
abortController: AbortController;
loop: {
ended: boolean;
userInterrupted: boolean;
userHalted: boolean;
};
reminderMark: string;
sessionId: string;
/** The run body's engine-minted run id, so the stream-layer verbs can name the run their disclosures are about. */
runId: string;
hookTimeoutMs: number;
hookIdentity: HookInvocationIdentity;
/** The leg's live permission-mode seat (`TaskStream.setPermissionMode` steers it). */
permissionMode: PermissionModeController;
}
/** The run body's backstop CARRIER: the effective ids and the post-prepare
* observations the run body publishes as they are minted, so the stream layer's failure backstop names the
* same run, session and observations the frames the run body already emitted did. */
export interface TaskIdRef {
current?: string;
sessionId?: string;
runId?: string;
effectiveMemoryScopes?: TaskResult["effectiveMemoryScopes"];
effectiveReasoning?: TaskResult["effectiveReasoning"];
delegationTerminalOwed?: HookInvocationIdentity;
editedFiles?: () => TaskResult["editedFiles"];
}
/** The manual `/compact` request seat: the `requested` flag, the parked waiters (each with its
* caller's cancel signal and per-call instructions), the run body's mooted-frame channel and the registration
* gate the run-end backstop closes before its final drain. */
export interface ManualCompactRef {
requested: boolean;
waiters: Array<{
resolve: (outcome: CompactOutcome) => void;
signal?: AbortSignal;
instructions?: string;
}>;
emitMooted?: (reason: string) => void;
closed?: boolean;
}
/**
* The end-of-task compaction pass's outcome (`Runner.finish`'s return): what the terminal-adoption lane
* hands the settle lane, which reads it for the `compacted` frame, the phase-timings frame and the detector reset. Named
* once here so the two lanes and the method spell one type.
*/
export type EndOfTaskCompaction = {
compacted: boolean;
tokensBefore?: number;
freedTokens?: number;
triggerTokens?: number;
postTriggerTokens?: number;
durationMs?: number;
phaseDurations?: CompactionPhaseDurations;
firstKeptEntryId?: string;
attachedFiles?: Array<{
path: string;
chars: number;
truncated: boolean;
}>;
modelFallback?: true;
fallbackReason?: "window";
clampedRatio?: number;
clampReason?: "budget" | "tolerance";
} | undefined;
/**
* The notification lane's BINDINGS: the run-notification-lane's own four `let`s as ONE seat (getters
* and setters over the lane's variables, never a copy). The identity-wiring lane binds `harness` / `sessionId` /
* `ident` the moment `prepared` exists (the lane's closures read them by variable — the routing listener, the park
* destination, the injection entry); the leg lane flips `live` in its finally, from which point every notification
* parks per session for the next run. One seat, four slots; nothing copies a binding out of it.
*/
export interface NotificationLaneBindings {
harness: AgentHarness | undefined;
sessionId: string | undefined;
ident: () => {
eventId?: string;
parentToolCallId?: string;
sourceTaskId?: string;
};
live: boolean;
}
/** The `notify()` bridge: `runLocked` binds `inject` the moment the task-notification lane exists. */
export interface NotifyRef {
inject?: (n: TaskNotificationPayload, opts?: {
priority?: SystemInjectionPriority;
}) => void;
}
/** The capture opt-out flip verb's binding; bound only when the run mounted a memory session. */
export interface CaptureOptOutRef {
flip?: (reason?: string) => Promise<{
outcome: "created" | "existed";
}>;
}
/** The MCP re-dial verb's binding; bound only when the run declared MCP servers — a leg with
* none has nothing to re-dial, and the stream verb then refuses TYPED rather than answering about a
* materialization that was never made. Same shape and same law as {@link CaptureOptOutRef}. */
export interface McpRedialRef {
redial?: (server: string) => Promise<import("../mcp.js").McpReconnectResult>;
}
/**
* The stream's LIVE state as the verb lanes read it — a view over the driver's own locals (getters, and one
* setter), never a copy: `resultValue` is written by the run body's `setResult` callback and by the failure
* backstop, `handle` by the run body's `onReady`, `reapHandle` by its `onSuspend`; every lane reads each of them
* at the instant of its own read, exactly as the one-function façade read the closure variables.
*/
export interface TaskStreamLiveSeat {
/** The assembled result once the run settled (the run body's `setResult`, or the failure backstop's mint). */
resultValue: TaskResult | undefined;
/** The live-task handle once `runLocked` published it; `undefined` before, and forever when prepare threw. */
readonly handle: LiveHandle | undefined;
/** What `destroy()` reaps when the run SUSPENDED; unset for every other terminal state. */
readonly reapHandle: SuspendReap | undefined;
}