UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

984 lines • 70.7 kB
import type { ExecutionEnv } from "../internal/harness.js";
import type { BackgroundShellCapability, BackgroundShellId } from "./background-shell.js";
import { type WorkflowRunStore } from "./workflow-run-store.js";
import type { BackgroundAgentRecord, BackgroundAgentStore } from "./background-agent-store.js";
import type { WorkflowHandle, WorkflowRun } from "../orchestration/workflow.js";
import type { TaskNotificationPayload } from "./task-notification.js";
import type { ToolResultStore } from "./tool-result-store.js";
import type { AssertAllKeysHandled } from "./ask-origin.js";
export type SemaTaskType = "background_bash" | "workflow" | "background_agent" | "monitor";
/** design/134 §3.3 (R3): WHO initiated a kill — recorded at the initiation site (first-marker-wins),
 *  because the settle-side then/catch context has already lost it. OPEN ENUM on the wire, now in the
 *  TYPE too (`(string & {})` keeps literal completion while admitting future values like "timeout"
 *  without a breaking change): consumers must tolerate unknown values and fall back to default copy.
 *  SINGLE SOURCE of the shape — two wire mirrors are INLINED to avoid import cycles (this module sits
 *  above both) and must stay in sync: types.ts `BackgroundChildEvent.stoppedBy` and
 *  task-notification.ts `TaskNotificationPayload.stoppedBy`. */
export type StopSource = "user" | "parent" | "system" | (string & {});
/** design/153 §7.1: `"parked"` (background agents only today) = durably suspended on a pending
 *  approval checkpoint — neither live nor terminal. Every `=== "running"` liveness check treats it
 *  as not-running (correct: no process exists); it exits poll waits, refuses settles, and is
 *  reap-exempt. Downstream switches over this union gain an arm (announced). */
export type SemaTaskStatus = "pending" | "running" | "parked" | "completed" | "failed" | "killed" | "cancelled";
export type TaskRetrievalStatus = "success" | "not_ready" | "timeout";
export interface UnifiedTaskOutput {
    task_id: string;
    /** `"unknown"` is a DECLARED member here, not a stray literal. A
     *  not-found lookup has no task kind to report, and the TaskStop envelope's `task_type` is required by
     *  the client schema — so the wrapper was already emitting `"unknown"` while the type said it could
     *  only be a `SemaTaskType`. Declaring it keeps the two honest with each other, and keeps `SemaTaskType`
     *  itself clean for every consumer that switches over real task kinds. */
    type?: SemaTaskType | "unknown";
    status?: SemaTaskStatus | string;
    retrieval_status: TaskRetrievalStatus;
    content?: string;
    error?: string;
    /** Machine twin of {@link error}, same token, present exactly where `error` names a coded refusal
     *  (`not_local`/`parked_pending_approval`/`park_resume_won`/`park_arbiter_unreachable`): the wire
     *  errorCode lift reads `details.code`, and an error-only refusal is unclassifiable on tool_end
     *  frames. Distinct from {@link errorCode}, which carries a FAILED child's own result taxonomy —
     *  this key classifies THIS poll/stop answer itself. Additive. */
    code?: string;
    /** RB-386② — machine-readable failure code on a FAILED background_agent row's poll
     *  details (the child's TaskResult.errorCode taxonomy — brain codes / `limit.*` / `budget.*` …).
     *  Additive; absent on non-failed rows, on rows whose failure carried no code, and on other lanes.
     *  The same key also carries a failed WORKFLOW run's `WorkflowRun.errorCode` (
     *  `workflow.*` for the orchestration's own refusals / a failed child's terminal code), populated
     *  by `formatWorkflowRun`: one field name for "why this poll answer is a failure", whichever lane
     *  minted the failure. */
    errorCode?: string;
    /** RB-386② — retryability verdict for a FAILED background_agent row, derived at settle time by the
     *  same classifier the sync sub-agent report's `error_kind (retryable: …)` line uses. Additive. */
    retryable?: boolean;
    /** design/134 §3.3: only present when `status === "killed"` (open enum — see {@link StopSource}). */
    stoppedBy?: StopSource;
    /** server[1526] ask — the durable row's committed stop-cycle number (1.371 settle mint), served on
     *  durable-fallback polls so a tail consumer can key revive cycles without the notify lane.
     *  ALSO served by the in-process live poll/stop arms (the handle's current cycle
     *  projection — spawn = 1, each 1.332/tier-3 revival opens the next), same key, same meaning,
     *  so tail meta is complete whichever arm answers. */
    seq?: number;
    /** fidelity-sweep A1: the served result is the child's PARTIAL work — it landed after a kill had
     *  already won the terminal race and was backfilled (produced before/while the task was stopped). */
    partial_result?: boolean;
    /** A cross-channel
     *  correlation key for ONE completion event. The same task's completion fans out across several
     *  independent channels today (a live `TaskNotificationPayload` frame, a durable inbox entry, and
     *  this poll projection) — a consumer today has to reconstruct "are these the same completion?" from
     *  weaker signals (task_id + status + timing). Minted ONCE per completion, fill-once (first writer
     *  wins) on the handle/record the completion is settled onto, so every read face — this poll
     *  projection, the notification payload, and (for background_agent) the cross-restart durable
     *  fallback row — serves the exact same value. Open-shape `string` (a uuidv7 in this implementation,
     *  not contractually required to stay one); absent when the underlying task hasn't reached a terminal
     *  settle yet, or on a pre-P1-3 core build that never minted one. */
    completionId?: string;
    /** A background agent's current state (status word, last activity, the approval card a parked row
     *  waits on) — {@link import("./agent-state.js").readAgentState}, the one reading AgentTranscript and
     *  ListAgents render too. Present on every background_agent POLL answer that resolved a row (live,
     *  durable terminal, durable parked, durable still-running); absent on other lanes, on not-found, and on
     *  the stop face. */
    agentState?: import("./agent-state.js").AgentState;
    details?: unknown;
}
/**
 * The shared internal poll/stop result shape every helper below returns
 * (`noTask`/`noTaskForStop`/`formatWorkflowRun`/`serveDurableAgentRow`/`pollTask`/`stopTask` and every
 * per-task-kind poll/stop arm). `UnifiedTaskOutput` (the `details` face) has no isError channel of its
 * own — it is a REQUIRED-field wire shape consumed by callers beyond these two tools too, so isError is
 * carried as a sibling on the WRAPPER instead (trap avoidance per the E1 sweep: never carry across isError
 * through a narrower helper contract). Optional, absent/false = success — `createTaskOutputTool`/
 * `createTaskStopTool` forward it onto the real `ToolReturn` at the tool boundary. Every internal helper
 * keeps constructing `content`/`details` exactly as before; this only adds a place to say "and this one
 * is a genuine failure" where the failure is actually detected.
 */
export type UnifiedTaskResult = {
    content: string;
    details: UnifiedTaskOutput;
    isError?: boolean;
};
export interface SemaTaskHandle {
    id: string;
    type: SemaTaskType;
    status: SemaTaskStatus;
    description?: string;
    owner?: string;
    scope?: string;
    toolUseId?: string;
    createdAt: number;
    updatedAt: number;
    outputFile?: string;
    /** P1-3 — see {@link UnifiedTaskOutput.completionId}. Minted fill-once at this task's own settle
     *  point ({@link mintCompletionId}); absent until the task reaches a terminal status. */
    completionId?: string;
}
/** Mint a cross-channel completion correlation id ONCE per
 *  handle/record, fill-once (first writer wins): every settle/direct-terminal-flip site calls this
 *  instead of writing `completionId` directly, so no matter how many terminal-transition sites a lane
 *  has (settle / TaskStop circumvention / backfill), the value is minted exactly once and every later caller
 *  (including an idempotent re-call from a circumvention path that already ran) gets back the SAME id. `string`
 *  is intentionally loose (any object with an optional `completionId` field) so the same helper serves
 *  both a `RegisteredTaskHandle` and a `WorkflowRun` (orchestration/workflow.ts — a different module,
 *  no shared base type, same fill-once shape). */
export declare function mintCompletionId(target: {
    completionId?: string;
}): string;
export interface TaskAccess {
    owner?: string;
    scope?: string;
    /** design/129: the caller's SESSION id — matches session-scoped handles across turns (a fresh per-turn
     *  spec.taskId changes `owner`, but the session is the stable key a background child outlives turns under). */
    sessionId?: string;
}
export interface BackgroundBashTaskHandle extends SemaTaskHandle {
    type: "background_bash";
    env: ExecutionEnv & BackgroundShellCapability;
    shellId: BackgroundShellId;
    /** (design/129 lifetime discipline, CC parity): a background bash spawned inside a SESSION
     *  outlives the run — CC's bg shells survive the agent turn (TB live evidence in settleKilledForOwner's
     *  T1-1 note) and die at the SESSION terminal. When set, owner MUST be the sessionId: skipped by the
     *  run-teardown settle/evict, kept alive through the dispose except-list, reaped by
     *  {@link TaskRegistry.reapSessionBackground}. Explicit flag only — never inferred from owner shape. */
    sessionScoped?: true;
    /** Deliver-a-living-service ledger mark — the row's lane of THE retain predicate
     *  (`retainsBackgroundProcesses`). Set at REGISTRATION when the launching task
     *  (`TaskSpec.retainBackgroundProcesses`) or the env capability declares retain, and by a run teardown
     *  whose own spec declares it. In-memory only (shell-lane rows are never persisted); a row without the
     *  field reads as not marked. The row is the delivered daemon's ONLY management surface — while the process runs it survives the
     *  per-run evict (TaskOutput/TaskStop resolve through this map; the watcher must stay live to mint
     *  the natural-exit receipt) and rides every later run's dispose except-list on the same env
     *  ({@link TaskRegistry.runningShellIds}). A terminal row evicts normally. Terminals: explicit
     *  TaskStop, the process's own exit (watcher settle), or env destruction (watcher env-death settle)
     *  — deliberately NO timed reaper (the deliverable is the living process). A retained SESSION-resident
     *  row is likewise exempt from the session-release reap ({@link TaskRegistry.reapSessionBackground}):
     *  the delivered daemon outlives its session, with the same three terminals. */
    retained?: true;
    /** The launcher STATED the task lane at registration (`retainBackgroundProcesses` was passed, true or
     *  false): the mark above already carries it, so a run teardown's own spec lane does not re-judge this row
     *  (a later run's declaration must not re-label a process its launch receipt described). Absent ⇒ the
     *  launcher did not know the lane (a host tool registering directly): the owning run's teardown supplies it. */
    retainStatedAtLaunch?: true;
    /** design/116 §7 G2b: when a watcher is attached, the registry accumulates the FULL output here (rolling
     *  bound) — TaskOutput then serves a RE-READABLE snapshot (CC semantics) instead of a consume-once cursor.
     *  `timedOut`/`timeoutSec` record a SELF-INFLICTED background-timeout kill (the env's per-shell budget
     *  fired), so later re-readable polls keep rendering it distinguishably from an external stop.
     *  `mirrorFailed` records that at least one append to the advertised output FILE failed — the terminal
     *  frame then discloses the file as incomplete instead of letting the launch receipt's "full output is
     *  appended there" promise stand over a silently partial file. */
    /** `droppedBytes`/`dropUnknown` account ENV-SIDE tail-buffer eviction disclosed by
     *  the poll (`bytesDroppedBeforeCursor`/`truncated`) — a loss upstream of (and distinct from) the
     *  registry's own `rolledChars` memory bound. Bytes counted here never reached the spool at all. */
    spool?: {
        stdout: string;
        stderr: string;
        rolledChars: number;
        exitCode?: number;
        timedOut?: boolean;
        timeoutSec?: number;
        mirrorFailed?: true;
        droppedBytes?: number;
        dropUnknown?: true;
    };
    watcher?: ReturnType<typeof setInterval>;
    /** design/134 §3.3 (R3 MED, 修类不修点): same attribution pair as the agent handle — see there. */
    stopSource?: StopSource;
    stoppedBy?: StopSource;
    /** The terminal-notification sink, HELD ON THE HANDLE (not only inside the watcher closure)
     *  so the run-teardown killed settle ({@link TaskRegistry.settleKilledForOwner}) can mint the receipt the
     *  watcher never will — a disposed shell polls as vanished ("nothing to notify about") and an evicted
     *  handle stops the watcher outright. */
    onTerminal?: (notification: TaskNotificationPayload) => void;
    /** Terminal-notification once-latch (fidelity R1-3): set atomically right before the FIRST terminal
     *  notice is sent. A teardown settle and an in-flight watcher tick can both reach their emit point
     *  (the kill/poll awaits are suspension windows between the status gate and the send) — whichever
     *  fires first wins; every later producer stays silent. */
    terminalNotified?: true;
}
export interface WorkflowTaskHandle extends SemaTaskHandle {
    type: "workflow";
    runId: string;
    handle: WorkflowHandle<unknown>;
    store?: WorkflowRunStore;
    /** B-1 (live leg) — see {@link RegisterWorkflowInput.originatingSessionId}. */
    originatingSessionId?: string;
    /** Design/129 session-lifetime discipline, extended to workflow — the
     *  ONE background kind that never had it (`background_bash`/`monitor`/`background_agent` all do).
     *  Unlike those three (task-scoped BY DEFAULT, `sessionScoped` an explicit opt-in via
     *  `TaskSpec.backgroundScope`), a workflow is unconditionally session-anchored — `originatingSessionId`
     *  is set at every standard mount (`prepare-task.ts`, never gated), and Workflow's whole raison d'être
     *  (its own guidance text) is "runs asynchronously, notifies you or is polled LATER" — there is no
     *  sensible "dies with this turn" mode for it to opt out into, unlike a background_agent which
     *  genuinely has both use cases. Set `true` whenever `originatingSessionId` is present (`registerWorkflow`).
     *  Read only by {@link TaskRegistry.reapSessionBackground} — the explicit, deployment-invoked session
     *  terminal anchor (core has no session TTL reaper of its own); NEVER by `Runner.runTask()`'s own
     *  per-leg teardown, whose finally block runs on every NORMAL leg completion, not just a genuine
     *  session end — a would-be regression caught by cross-review before this shipped (see docs/
     *  REVIEW-BACKLOG.md's "RB-215 复核" entry for the full incident). */
    sessionScoped?: true;
    /** See {@link RegisterWorkflowInput.onServedTerminal}. Cleared after the single fire. */
    onServedTerminal?: () => void;
    /** The bounded return value, captured on the done-hook so the STORE-LESS in-memory lane's
     *  terminal poll can hand it over too (the launch note promises it; without a store there was no result). */
    result?: string;
    /** T2A-8: the failure reason (bounded + redacted), captured on the done-hook's rejection so a store-less
     *  terminal poll can say WHY it failed (the store lane's formatWorkflowRun already carries run.error). */
    error?: string;
}
/** design/151 S1a — the per-row durable write lane. The registry is the row's SINGLE writer; writes
 *  are serialized on a promise chain (put → bind → settle order preserved), CAS'd with the locally
 *  held `record.rev`, and BEST-EFFORT (a store outage degrades durability with a visible warning,
 *  never execution). A lost CAS or a put `already_exists` means a FOREIGN row lives under this
 *  handle (leftover from a dead process / another writer) — the lane poisons itself loudly instead
 *  of clobbering (design/151 MED-8: the writer's own rev is the anti-clobber line). */
export interface DurableAgentLane {
    store: BackgroundAgentStore;
    /** The writer's authoritative local copy (rev included — bumped only on a confirmed CAS win). */
    record: BackgroundAgentRecord;
    chain: Promise<void>;
    /** True once the initial `put` landed (an outage before that retries put on the next write). */
    written: boolean;
    poisoned: boolean;
    /** true while the LATEST flush exhausted its retries (cleared by the next success).
     *  The lifecycle-flip probe requires it false: a row whose bind/settle never persisted must not
     *  have its transcript preserved-but-unreachable (the local record accumulates patches, so the
     *  next successful write self-heals the missing fields). */
    flushFailed: boolean;
    /** 增量 R-4 — consecutive heartbeat re-drives of a settled flush-failed lane (reset on any
     *  flush success); past the ceiling the lane is abandoned loudly instead of retrying forever. */
    redrives?: number;
}
export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
    type: "background_agent";
    /** design/115 P3: aborting this cancels the child run (TaskStop / parent teardown). */
    abort: AbortController;
    /** Lineage axes, declared (registration always spread them into the handle; readers used
     *  to cast). `rootSessionId` feeds {@link canAccess}'s root-session arm; the other two stay
     *  read-only lineage metadata on the live face (no parentSessionId access arm — see the arm's
     *  comment for why). The register side clamps `parentTaskId`. */
    parentTaskId?: string;
    parentSessionId?: string;
    rootSessionId?: string;
    /** design/151 S1a — the child's store session id (transcript/resume anchor), bound once the child
     *  session is minted ({@link TaskRegistry.bindBackgroundAgentSession}). */
    sessionId?: string;
    /** design/151 S1a — the durable write lane (present iff the registration wired a store). */
    durable?: DurableAgentLane;
    /** design/147 S2a — the RUNNING child run's notification injector (attached once its lane is
     *  live, dropped at terminal settle). A parent/sibling SendMessage delivers through this at the
     *  child's next turn boundary (CC in-memory pendingMessages pedestal); late calls after the run
     *  ends park per session inside the injector itself (the lane-teardown branch). */
    notify?: (notification: import("./task-notification.js").TaskNotificationPayload, opts?: {
        priority?: import("./task-notification.js").SystemInjectionPriority;
    }) => import("./task-notification.js").NotificationInjectionOutcome | Promise<import("./task-notification.js").NotificationInjectionOutcome>;
    /** design/147 S2a — messages that arrived in the STARTUP window (row already
     *  running, lane not yet bound). Bounded; flushed in order by attachAgentNotify. Discarded at
     *  terminal settle: a run that failed during prepare never had a lane to park through, and the
     *  sender's completion notification (failed) prompts the resend. */
    /** S2b — park redeemability (see RegisterBackgroundAgentInput.retainedContinuation). */
    retainedContinuation?: boolean;
    /** S2b RB-27② — revive-cycle stamp (see reviveBackgroundAgent): attach/settle from a resume leg
     *  carry it so a stale cycle's late calls no-op instead of clobbering the newer cycle. */
    reviveCycle?: number;
    /** The CURRENT stop-cycle number in settle-generation terms (spawn run = 1; each
     *  revival — 1.332 retained or tier-3 seeded — opens the next). Served by the LIVE poll/stop
     *  projection under the SAME `seq` key as the durable-fallback arm, so a tail consumer keys
     *  revive cycles identically whichever arm answered. Re-aligned to the settle's authoritative
     *  `outcome.seq` at settle time (the ledger's cycleSeq is the mint; this is its projection). */
    cycleSeq?: number;
    /** design/153 §7.1 — the approval checkpoint this handle is parked on (`status:"parked"` only). */
    parkedCheckpointToken?: string;
    /** design/153 §7.1 — in-process TaskStop arbitration closure, minted by the parking watcher
     *  (captures the checkpoint store + token; the registry itself holds no durable stores — r4 F-06).
     *  Resolves `true` iff the STOP won the checkpoint CAS (expire landed; resume can no longer
     *  consume it) — only then may the row flip killed. `false` = a resume consumed the token first.
     *  Absent (cross-process row, no live park context) ⇒ TaskStop refuses honestly: the durable
     *  approval inbox (server /decide) owns that arbitration. */
    resolveParkedStop?: () => Promise<boolean>;
    /** S2b — resolvers of DIRECT (post-attach) deliveries currently awaiting the
     *  injector; terminal settle/stop sweep these as not_running so a never-settling adapter can
     *  never pin the sender's tool call. */
    inFlightDirect?: Set<(r: {
        ok: true;
        disposition: "queued" | "parked";
    } | {
        ok: false;
        reason: "not_found" | "not_running" | "no_channel";
    }) => void>;
    /** S2a F7 — channel lifecycle: undefined = unsupported (spawner never declared), "attaching" =
     *  declared, lane not yet bound (buffer window), "attached" = live injector present. */
    channelState?: "attaching" | "attached";
    preAttachQueue?: Array<[
        import("./task-notification.js").TaskNotificationPayload,
        {
            priority?: import("./task-notification.js").SystemInjectionPriority;
        } | undefined,
        (((r: {
            ok: true;
            disposition: "queued" | "parked";
        } | {
            ok: false;
            reason: "not_found" | "not_running" | "no_channel";
        }) => void) | undefined)?
    ]>;
    /** [c209-D] the EXPLICIT addressing name (CC Agent.name parity, validated at spawn: charset regex +
     *  "main" reserved). Distinct from `description` (the display/legacy-matching label): the name layer
     *  resolves latest-wins ("a newer agent took the name"), the description layers keep their
     *  running-preferred + ambiguous posture. Absent on pre-[c209] rows and unnamed spawns. */
    name?: string;
    /** design/129: this child OUTLIVES its parent turn — owner is the SESSION id; the parent-teardown reap
     *  skips it; `reapSessionBackground` is its terminal anchor. Explicit flag, never inferred from key values
     *  (hostTaskId===sessionId deployments would otherwise be ambiguous). */
    sessionScoped?: true;
    /** Terminal snapshot (bounded, {@link BG_AGENT_RESULT_MAX} in subagent.ts) — served by TaskOutput
     *  once the child finishes. */
    result?: string;
    /** The FULL result (bounded only by the defensive {@link BG_AGENT_RESULT_FULL_MAX} ceiling
     *  in subagent.ts), set ONLY when it differs from `result` — same shape as {@link WorkflowRun.resultFull}
     *  (workflow.ts NH-1). Before this field existed, the settle call sites pre-sliced `child.result` to
     *  ~2-4K chars with no disclosure — clipTaskOutput's own file-pointer/offload-friendly design at the
     *  poll layer was structurally unreachable because `result` itself could never be long enough to need it. */
    resultFull?: string;
    /** design/158 S1 (RB-205-B 半) — the tool-result-store ref `resultFull`'s full text was spilled to,
     *  the FIRST time a poll's `clipTaskOutput` actually cut it (lazy write, not at settle time). Cached
     *  here so every LATER poll within the SAME cycle reuses the same ref instead of re-writing (the
     *  store's own `put` is write-once too, but re-deriving/re-calling it every poll would still cost a
     *  redundant round-trip on a durable backend, and a changing ref across polls would be a needless
     *  disclosure churn). {@link reviveBackgroundAgentLane} clears this on revive — the cycle's full text
     *  is gone, so a stale ref must not be reused across cycles. Ref format ties in `reviveCycle` (see
     *  {@link import("./task-registry-agent.js").spillClippedAgentResult}) precisely so a revived cycle's
     *  eventual spill lands under a DIFFERENT ref rather than colliding with (and being silently refused
     *  by the write-once store under) cycle 1's. */
    spillRef?: string;
    /** Backlog #169 — the spill `put` for the CURRENT revive cycle threw or rejected. One-time latch,
     *  scoped to the cycle ({@link import("./task-registry-agent.js").reviveBackgroundAgentLane} clears
     *  it in lockstep with `spillRef`): later polls serve the clipped text with an honest ref-free loss
     *  note instead of retrying the write — the ref is minted deterministically, so the known permanent
     *  failure class (the store's write-once/provenance conflict) would fail identically on every poll,
     *  each retry a guaranteed-failure round trip to a durable backend. The monitor lane's
     *  `spillFailed` twin (same degrade discipline, different write shape — that one is fire-and-forget,
     *  this one is awaited). */
    spillFailed?: true;
    error?: string;
    /** RB-386② — the machine-readable failure code beside `error`, threaded from the settle
     *  mint point (subagent.ts computes it ONCE from the child's TaskResult.errorCode taxonomy /
     *  extractErrorCode — single source with the sync report's classification). Served on the failed
     *  row's poll `details` and the failed terminal notification. Absent when no code was known. */
    errorCode?: string;
    /** RB-386② — retryability verdict for a FAILED row, computed at the settle mint point by the SAME
     *  classifier the sync report face uses (`classifySubagentError` — error_kind ∈ rate_limit/
     *  overloaded/timeout/network ⇒ true, logic ⇒ false). Stored rather than re-derived so the core
     *  poll lane never has to import the agents-domain classifier (no new domain cycle). */
    errorRetryable?: boolean;
    /** The coarse classification KIND beside `errorRetryable`
     *  (classifySubagentError's error_kind ∈ rate_limit/overloaded/timeout/network/logic), threaded
     *  from the same settle mint point and stored for the same no-domain-cycle reason as its sibling.
     *  Rendered on the FAILED row's poll TEXT face as the `(error_kind: …, retryable: …)` clause —
     *  the same field names the sync report's error_kind line already speaks. */
    errorKind?: string;
    /** Ruled 2026-08-05 (governance kind follow-up): the wait hint paired with a deferred-retryable
     *  failure (`usage.*` codes — errorKind `"governance"`), threaded from the same settle mint point
     *  as its three siblings. Without it the background faces exposed `retryable:false` with no way to
     *  know WHEN the refusal lifts — the sync report card carried `retryAfterMs` while the bg poll/
     *  notification faces structurally lost it. */
    errorRetryAfterMs?: number;
    /** fidelity-sweep A1: `result` arrived AFTER a kill won the status race — it is the child's partial
     *  work (produced before/while being stopped), backfilled by the refused settle. Poll frames label it. */
    resultIsPartial?: boolean;
    /** design/134 §3.3 (R3): the PENDING attribution marker, written by {@link TaskRegistry.markStopSource}
     *  at the kill INITIATION site, strictly BEFORE the abort/status flip (once the handle leaves "running"
     *  the marker is refused and attribution falls back to "system"). First-marker-wins. */
    stopSource?: StopSource;
    /** The LANDED attribution — set exactly when the handle goes `killed`, never rewritten afterwards
     *  (first-writer-wins, same discipline as the terminal status itself). Absent on natural completion/failure. */
    stoppedBy?: StopSource;
    /** RB-375 A1 — the reap-time terminal notifier, hung by the spawn lanes (subagent.ts) right after
     *  registration: composes and sends the SAME terminal faces (observer sink + parent notification)
     *  the child's own unwind would, so {@link import("./task-registry.js").TaskRegistry.reapSessionBackground}
     *  can emit the terminal news AT REAP TIME instead of waiting on the child's unwind (which blocks on
     *  the in-flight tool actually honoring the abort — unbounded). The bash handle's `onTerminal` twin.
     *  Cleared at the accepted settle (with `notify`); invoked at most once via {@link terminalNotified}. */
    onReapTerminal?: () => void;
    /** RB-375 A1 — terminal-notification once-latch (the bash/monitor `terminalNotified` twin): claimed
     *  by whichever side reaches its emit point first — the session-release reap sweep (synchronous send)
     *  or the child's own unwind (`TaskRegistry.claimAgentTerminalNotify`). The loser stays silent, so a
     *  reaped child never double-announces. Cleared on revive (a new stop cycle owns its own terminal). */
    terminalNotified?: true;
}
/** design/135 G2 (Monitor): injectable timer/clock seam for the monitor watcher — the 200ms batch window
 *  and the timeout/storm checks must be drivable by a test without real sleeps (design/87 discipline).
 *  `setInterval` receives an ASYNC tick (the default adapter fires it void; a fake can await it). */
export interface MonitorTimers {
    setInterval: (fn: () => unknown, ms: number) => unknown;
    clearInterval: (handle: unknown) => void;
    now: () => number;
}
export declare const defaultMonitorTimers: MonitorTimers;
/** design/135 G2: Monitor watch — background_bash's near kin, but the watcher streams stdout LINES as
 *  batched event notifications instead of firing one terminal notify. Spool stays re-readable (TaskOutput). */
export interface MonitorTaskHandle extends SemaTaskHandle {
    type: "monitor";
    env: ExecutionEnv & BackgroundShellCapability;
    shellId: BackgroundShellId;
    /** RB-238/RB-239/RB-240 spool honesty fields: `droppedBytes`/`dropUnknown` = env-side
     *  tail-buffer eviction (see the bash handle's twin); `dropNotified` = the once-latch for the event-stream
     *  disclosure (terminal frames disclose unconditionally); `drainFailed` = the stop-time final drain poll
     *  failed, so the spool may be missing the last window; `timedOut`/`timeoutSec` = the env's own
     *  background time budget killed the process (bash-lane parity — renders distinguishably from a stop). */
    spool: {
        stdout: string;
        stderr: string;
        rolledChars: number;
        exitCode?: number;
        droppedBytes?: number;
        dropUnknown?: true;
        dropNotified?: true;
        drainFailed?: true;
        timedOut?: boolean;
        timeoutSec?: number;
    };
    /** Partial (newline-less) stdout tail awaiting completion — a line becomes an event only once terminated. */
    lineBuf: string;
    /** Per-batch monotonic counter → unique notification dedup keys (see task-notification.ts). */
    seq: number;
    /** The TWO-LEVEL event-storm control's state.
     *  Level 1 = a token bucket (`stormTokens` out of `stormBurst`, refilled one per
     *  `stormRefillIntervalMs`): a batch that cannot consume a token is SUPPRESSED (its lines stay in the
     *  re-readable spool) and counted in `suppressedBatches`; the next batch that DOES consume one first
     *  emits the disclosure frame naming that count. Level 2 = kill, and only for a monitor that stayed
     *  continuously over budget since `overloadSince` for MONITOR_STORM_KILL_AFTER_MS.
     *  `lastSuppressedAt` clears `overloadSince` once the pressure is old enough (CC's `OCo * 3` rule) —
     *  a short burst every few minutes must never accumulate into a kill. */
    stormTokens: number;
    stormBurst: number;
    stormRefillIntervalMs: number;
    stormRefilledAt: number;
    suppressedBatches?: number;
    overloadSince?: number;
    lastSuppressedAt?: number;
    batchWindowMs: number;
    maxBatchesPerMinute: number;
    /** Session-level residency (design/129 sessionScoped semantics): reaped by `reapSessionBackground`,
     *  skipped by the per-run `clearBackgroundForOwner`, and NEVER timeout-killed by the watcher. */
    persistent?: true;
    sessionScoped?: true;
    /** Deliver-a-living-service ledger mark — same semantics as the bash handle's `retained` (see there). */
    retained?: true;
    /** Same as the bash handle's `retainStatedAtLaunch` (see there). */
    retainStatedAtLaunch?: true;
    /** Non-persistent only: watcher-enforced hard deadline (epoch ms on the injected clock).
     *  (CC parity): this deadline — not the parent run — is the watch's lifetime anchor.
     *  A monitor's life = min(timeout, process exit, TaskStop); a persistent one is SESSION-anchored.
     *  NEITHER is run-anchored: the run teardown leaves an in-window watch alive (settle/evict/dispose
     *  all skip it) so between-turns events keep flowing through the pending lane. */
    deadlineAt?: number;
    timeoutMs?: number;
    watcher?: unknown;
    /** A watcher tick is suspended on its pollBackground await RIGHT NOW. The stop-time
     *  final drain must skip its own poll then (that in-flight tick already consumed the env cursor — and a
     *  never-resolving adapter must not pin TaskStop). Set/cleared in lockstep with the tick's re-entrancy
     *  guard. */
    tickInFlight?: true;
    timers: MonitorTimers;
    onEvent?: (n: TaskNotificationPayload) => void;
    stopSource?: StopSource;
    stoppedBy?: StopSource;
    /** Terminal-notification once-latch — same discipline as the bash handle (fidelity R1-3). Only the
     *  TERMINAL frame is latched; "event" frames are unaffected. */
    terminalNotified?: true;
    /** design/158 S2 (RB-364 半) — the offload store a ROLLED-OFF spool middle spills to, captured at
     *  REGISTRATION time (RegisterMonitorInput's additive field), not per-poll like the background_agent
     *  lane's `TaskPollOptions.toolResultStore` (S1): the roll happens inside the watcher tick — an async
     *  timer callback with no per-call "access" to thread a store through — so registration is the only
     *  point that has one to hang on the handle. Absent ⇒ legal degrade: `rollSpoolText`'s `onDrop` hook
     *  is simply never wired, byte-identical to pre-S2 rolling (design/158 §2.2). */
    toolResultStore?: ToolResultStore;
    /** design/158 S2 — the session id spilled refs are scoped under (same `TaskAccess.sessionId` role as
     *  every other `buildToolResultRef` call site); captured at registration for the same reason as
     *  `toolResultStore` above. */
    spillSessionId?: string;
    /** design/158 S2 — next segment index to mint for the STDOUT dropped-middle ref chain (0-based;
     *  design/158 §3 "方案 a": a ref PER dropped segment, chained by index, rather than one ref with an
     *  append primitive — ToolResultStore's contract is write-once with no append). stdout and stderr get
     *  INDEPENDENT counters (this field vs `spillErrSegCount`) so each stream's own segments concatenate
     *  back in original byte order without interleaving the other stream's drops between them. */
    spillSegCount?: number;
    /** design/158 S2 — the STDERR twin of {@link spillSegCount}. */
    spillErrSegCount?: number;
    /** design/158 S2 — cumulative chars (UTF-16 code units, `string.length`) handed to `store.put`
     *  across every segment this handle has spilled so far (both streams combined) — the per-handle
     *  disk-cap accounting ({@link import("./task-registry-monitor.js")} reads `MONITOR_SPILL_CAP_CHARS`
     *  against this). The field used to be named `spillBytesUsed` while the ledger counted
     *  chars — renamed to the axis it really measures (see the constant's own doc for why chars). */
    spillCharsUsed?: number;
    /** design/158 S2 — the per-handle spill cap (`MONITOR_SPILL_CAP_CHARS`) was reached; further
     *  rolled-off middles are no longer spilled (already-written segments stay readable — "earlier
     *  segments retained"). One-time latch; the poll disclosure reads it to stop claiming full ref
     *  coverage. */
    spillCapped?: true;
    /** design/158 S2 — at least one `store.put` for this handle rejected or threw. One-time latch; the
     *  poll disclosure downgrades its wording once this is set (never claims the ref chain is complete —
     *  the fire-and-forget write may have silently failed; see task-registry-monitor.ts's spill helper). */
    spillFailed?: true;
}
export type RegisteredTaskHandle = BackgroundBashTaskHandle | WorkflowTaskHandle | BackgroundAgentTaskHandle | MonitorTaskHandle;
export interface RegisterMonitorInput extends TaskAccess {
    shellId: BackgroundShellId;
    env: ExecutionEnv & BackgroundShellCapability;
    description?: string;
    toolUseId?: string;
    /** Session-level residency: no watcher timeout; reaped by session release (design/129 semantics —
     *  when set, `owner` MUST be the session id and `sessionScoped` should be set too). */
    persistent?: boolean;
    /** design/129 explicit-flag discipline: session-scoped access matching + session-terminal reap. */
    sessionScoped?: true;
    /** Non-persistent watch deadline (ms). The tool clamps to [1, 3_600_000], default 300_000. */
    timeoutMs?: number;
    /** The event sink — each stdout-line batch AND the single terminal state fires exactly one payload. */
    onEvent?: (n: TaskNotificationPayload) => void;
    /** design/87: injectable timers/clock (tests drive ticks + time manually; default = real timers). */
    timers?: MonitorTimers;
    /** Batch window (ms, default 200): lines arriving within one tick coalesce into one notification. */
    batchWindowMs?: number;
    /** The LAUNCHING task's retain declaration (`TaskSpec.retainBackgroundProcesses`) — the task lane of
     *  THE retain predicate; stamped onto the row's `retained` mark at registration, and passing it states the
     *  lane, same as the bash lane (see `RegisterBackgroundBashInput.retainBackgroundProcesses`). */
    retainBackgroundProcesses?: boolean;
    /** The SUSTAINED batch rate the storm bucket refills at (default 50/min). Exceeding it is
     *  suppression, not death — see `stormBurst` and MONITOR_STORM_KILL_AFTER_MS. */
    maxBatchesPerMinute?: number;
    /** Burst capacity of the storm bucket (default MONITOR_STORM_BURST = CC's 10). */
    stormBurst?: number;
    now?: number;
    /** design/158 S2 (RB-364 半) — the offload store a rolled-off spool middle spills to (see
     *  {@link MonitorTaskHandle.toolResultStore}). Absent ⇒ legal degrade, byte-identical rolling. */
    toolResultStore?: ToolResultStore;
}
export interface RegisterWorkflowInput extends TaskAccess {
    runId: string;
    handle: WorkflowHandle<unknown>;
    store?: WorkflowRunStore;
    description?: string;
    toolUseId?: string;
    now?: number;
    /** B-1 (live leg): the ORIGINATING session id — anchors the in-process handle to its session
     *  so a later turn of the SAME session (fresh per-turn owner) reaches it through `canAccess`'s
     *  workflow session arm, mirroring the durable predicate. A workflow is a SESSION-level asset
     *  (its completion notification routes to the session) — deliberately unlike a background agent's
     *  live handle, whose sibling-turn invisibility is a channel-discipline ruling (§3.1c). */
    originatingSessionId?: string;
    /**
     * Fired AT MOST ONCE when a `pollTask` through THIS registry entry (= the originating
     * session's own lane; cross-session store-fallback polls don't fire it) serves a TERMINAL snapshot.
     * The RunWorkflow tool wires it to {@link import("../orchestration/run-workflow-tool.js").WorkflowCompletionNotifier.ackServed}
     * so the deployment can drop the now-redundant completion-inbox entry. Errors are swallowed.
     */
    onServedTerminal?: () => void;
}
/** T2A-7 (2026-07-05 soak): one blocked-poll step, CLAMPED to the caller's deadline — a small timeoutMs
 *  (e.g. 10ms) must not sleep the full 400ms step (~40× overshoot, and the overshoot window even served a
 *  late terminal as success). `undefined` deadline keeps the plain step (callers without a deadline). */
export declare const sleepPollStep: (deadline: number | undefined, signal?: AbortSignal) => Promise<void>;
export declare const sleepMs: (ms: number, signal?: AbortSignal) => Promise<void>;
/** design/135 G2 Monitor defaults (CC schema: 200ms batch window; timeout default 5min / max 60min).
 *  `MONITOR_MAX_BATCHES_PER_MINUTE` = the SUSTAINED batch rate the storm bucket refills at
 *  (RB-334: CC `TIs(bIs=10, OCo=2000)` 220:403284/403421-403423 refills 1 token per 2000ms = 30/min;
 *  sema keeps its own 50/min figure — the sema batch window is the same 200ms, so the two are the same
 *  order of magnitude — and derives the refill interval from it, see `stormRefillIntervalMs`). */
export declare const MONITOR_BATCH_WINDOW_MS = 200;
export declare const MONITOR_DEFAULT_TIMEOUT_MS = 300000;
export declare const MONITOR_MAX_TIMEOUT_MS = 3600000;
export declare const MONITOR_MAX_BATCHES_PER_MINUTE = 50;
/** Burst capacity of the storm token bucket — how many batches may fire
 *  back-to-back before the SUPPRESSION level engages. CC value, adopted verbatim. */
export declare const MONITOR_STORM_BURST = 10;
/** How long a monitor must stay CONTINUOUSLY over budget before the
 *  second level (kill) fires. CC value, adopted verbatim. */
export declare const MONITOR_STORM_KILL_AFTER_MS = 30000;
/** The partial-line accumulator bound. sema's `lineBuf` had NO bound —
 *  a monitored process emitting megabytes without a single newline grew it without limit (the spool's
 *  own roll never covered it). Keep the TAIL (the newest bytes are the ones a watch is about). */
export declare const MONITOR_LINE_BUF_CAP = 1048576;
/** The monitor event-result clip, shared by the watcher's batch/terminal emits and
 *  the stop-time drain emit (one clip, one truncation wording). */
export declare function clipMonitorEvent(s: string): string;
/** Per-line clip applied to the `lines[]` a batch carries. Spool untouched. */
export declare function clipMonitorLine(s: string): string;
/**
 * The ONE terminal-notification summary
 * HEAD for a background command / monitor watch. Every terminal frame in the bash and monitor lanes
 * composes its summary as `terminalTaskSummary(...)` plus this lane's own honest detail clause — the
 * pre-RB-335 code had six hand-written phrasings whose comment claimed "no CC sample for those faces"
 * while 220 carries a verbatim sample for every one of them (the `failed` branch was the concrete
 * mismatch: sema said `failed (exit code N)`, CC says `failed with exit code N`).
 */
export declare function terminalTaskSummary(kind: "bash" | "monitor", label: string, status: "completed" | "failed" | "killed", exitCode?: number): string;
export declare const TASK_OUTPUT_MAX_CHARS = 160000;
/**
 * CC `FHl` shape (pretty.js:458012-458016) when the task has an on-disk output file: TAIL-keep (a
 * background command's latest output is the valuable end) behind a `[Truncated. Full output: <path>]`
 * pointer so the model can Read the full spool. Without a file (workflow/agent results), keep the
 * legacy head+tail middle-omission — there is no path to point at, and the head often carries status.
 * RB-198 F4: the clip arithmetic itself moved to the shared {@link clipWithFilePointer} (a second caller,
 * the foreground Bash tool, needed the SAME already-hardened RB-85/RB-112/RB-122 logic) — this wrapper
 * only supplies the task-output budget.
 */
export declare function clipTaskOutput(s: string, fullOutputPath?: string): string;
export declare function statusFromBackground(status: string, exitCode?: number): SemaTaskStatus;
/** Rolling middle-drop bound for the bash spool, shared by the watcher's per-tick accumulate and the
 *  teardown settle's terminal flush: keep head+tail halves within `cap`, account the dropped middle in
 *  `rolledChars` (the honesty counter the poll face reports as permanently-dropped bytes).
 *
 *  design/158 S2 (RB-364 半): `onDrop`, when given, is called SYNCHRONOUSLY (before this function
 *  returns) with exactly the middle substring being dropped — the ONE place that substring is ever
 *  materialized, so a caller wanting to preserve it (the monitor lane's spill-to-offload-store) does not
 *  need to re-derive the same slice arithmetic a second time. Every EXISTING caller (the background_bash
 *  lane, task-registry.ts) omits the 4th argument, so their behavior — return value AND `rolledChars`
 *  accounting — is untouched byte-for-byte; the callback is a pure side-channel hook. */
export declare function rollSpoolText(spool: {
    rolledChars: number;
}, s: string, cap: number, onDrop?: (dropped: string) => void): string;
/** design/158 S2 (RB-364 半) — per-monitor-handle hard cap on cumulative CHARS (UTF-16 code units)
 *  spilled to the offload store (design/158 §3's S3 half, pulled forward alongside S2). Once a
 *  handle's running total reaches this, further rolled-off middles are simply not spilled —
 *  already-written segments stay readable (the poll disclosure says "earlier segments retained"),
 *  matching the pre-S2 loss shape for whatever is dropped PAST the cap. Exported so the boundary is a
 *  testable contract, not a buried literal.
 *
 *  The constant used to be named
 *  `..._BYTES` while the ledger added `dropped.length` — chars, up to ~3× fewer than UTF-8 bytes on
 *  multibyte content. CHARS is the axis kept (name changed, arithmetic untouched) because it is the
 *  offload store family's own coordinate (`InMemoryToolResultStore.maxTotalChars` evicts by
 *  `content.length`; `ToolResultSlice.totalChars`) and the spool's `rolledChars` axis — one unit
 *  end-to-end; a `Buffer.byteLength` ledger here would sit crosswise to the store's own cap. */
export declare const MONITOR_SPILL_CAP_CHARS: number;
/** Fold one poll's env-side-loss disclosure into the spool's account. The env's
 *  tail buffer evicts its HEAD under pressure (node-execution-env 8MB bound) — those bytes are gone
 *  before any watcher ever saw them, which is a different loss than the registry-side `rolledChars`
 *  middle-drop. `bytesDroppedBeforeCursor` only counts bytes dropped before an unconsumed cursor, so
 *  summing successive polls never double-counts; an env that says only `truncated` (no byte figure)
 *  degrades to the unquantified flag. Shared by ALL THREE consumers of a background poll — the monitor
 *  watcher, the bash watcher, and the blocking pollBackgroundBash leg (one class, one arithmetic).
 *  The blocked-wait leg used to keep its own hand-rolled `dropped += bytesDroppedBeforeCursor ?? 0`
 *  accumulator, which ignored `truncated` outright: an env that discloses the loss WITHOUT a byte
 *  figure produced ZERO disclosure on that leg. It routes through here now — the header's former
 *  "already accounted this on its own" claim was describing exactly the half-accounting that made the
 *  degraded shape invisible. */
export declare function accountDroppedBytes(spool: {
    droppedBytes?: number;
    dropUnknown?: true;
}, poll: {
    truncated?: boolean;
    bytesDroppedBeforeCursor?: number;
}): void;
/** The POLL-FACE wording for env-side loss (the terminal/event-frame wording is
 *  {@link droppedGapNote}'s; the two faces differ by design and always have). One function so the two
 *  quantified/unquantified branches cannot drift between the three legs that render it: the monitor
 *  re-readable spool, the bash re-readable spool, and the bash blocked wait. Trailing newline included
 *  (it is a banner LINE above the stdout heading); "" when nothing was dropped. */
export declare function spoolDropNote(acct: {
    droppedBytes?: number;
    dropUnknown?: true;
}): string;
/**
 * HOW a poll's output body may be read AGAIN — the machine word beside `details`, previously legible
 * only from the stdout heading's prose.
 *
 * `full` = the whole output of the task, re-rendered from the registry's own spool on every poll: a
 * later poll answers the same bytes plus whatever arrived, so a consumer REPLACES what it holds.
 * `cursor` = only what is NEW since the previous read: the env cursor was consumed and those bytes
 * cannot be served twice, so a consumer APPENDS.
 *
 * The distinction was machine-invisible and the one key nearby reads the two faces OPPOSITELY:
 * `bytesDroppedBeforeCursor` rides the spool faces only when something was actually dropped, and the
 * increment face unconditionally (value possibly 0). A consumer discriminating on that key's PRESENCE
 * therefore reads a spool that happened to drop bytes as an increment — and appends a body that was a
 * full re-render, printing the same text twice.
 */
export declare const TASK_OUTPUT_REPLAY_KINDS: readonly ["full", "cursor"];
export type TaskOutputReplay = (typeof TASK_OUTPUT_REPLAY_KINDS)[number];
/**
 * The ONE table the word and the prose both come out of. Each face's stdout/stderr headings live here
 * rather than at the three legs that render them, so the banner a person reads and the key a program
 * reads cannot drift — the failure mode this whole field exists to end (the two spool renderers were
 * hand-copied twins before {@link renderSpoolBody}, and their drift was three disclosure bugs).
 *
 * `stdoutWhenGapped` is the heading a face falls back to once a gap has been disclosed. `undefined` is
 * a ROW, not an omission: an increment window claims nothing about completeness to begin with ("new
 * stdout" is true whether or not bytes were lost before it), so it has no claim to downgrade — while a
 * spool advertising itself as "full" with a disclosed gap would be saying something false.
 */
export declare const REPLAY_FACE: {
    readonly full: {
        readonly stdout: "stdout (full, re-readable)";
        readonly stdoutWhenGapped: "stdout (re-readable spool — INCOMPLETE, see note above)";
        readonly stderr: "stderr";
    };
    readonly cursor: {
        readonly stdout: "new stdout";
        readonly stdoutWhenGapped: undefined;
        readonly stderr: "new stderr";
    };
};
export type ReplayHeadingTableCoversEveryReplay = AssertAllKeysHandled<Exclude<TaskOutputReplay, keyof typeof REPLAY_FACE>>;
/** The re-readable spool face: the monitor lane and the bash spool lane, both rendered by
 *  {@link renderSpoolBody}. Named once so the three legs that report it cannot each spell it. */
export declare const SPOOL_REPLAY = "full";
/** The consume-once increment face: the bash blocked wait, which accumulates env-cursor increments. */
export declare const INCREMENT_REPLAY = "cursor";
/** The stdout heading of a face, downgraded when a gap has been disclosed (see {@link REPLAY_FACE}). */
export declare function stdoutHeadingOf(replay: TaskOutputReplay, gapDisclosed: boolean): string;
/** The re-readable spool fields {@link renderSpoolBody} reads — the intersection of the bash handle's
 *  optional `spool` and the monitor handle's mandatory one (both carry supersets: `mirrorFailed` is
 *  bash-only, `drainFailed`/`dropNotified` monitor-only). Declared as a named shape, not inlined, so
 *  the renderer's contract is one thing both lanes are checked against. */
export interface RenderableSpool {
    stdout: string;
    stderr: string;
    rolledChars: number;
    exitCode?: number;
    timedOut?: boolean;
    timeoutSec?: number;
    droppedBytes?: number;
    dropUnknown?: true;
}
/** The per-lane inputs {@link renderSpoolBody} cannot derive from the spool itself.
 *  `rolledExtraClause` / `drainNote` are PRE-RENDERED STRINGS, deliberately not booleans or a lane
 *  discriminator: the renderer must stay ignorant of which lane called it, otherwise the two monitor-only
 *  disclosures (design/158 S2 spill refs; RB-239 stop-time drain failure) come back as in-renderer
 *  branches on a kind switch — the exact shape that let the two hand-copied renderers drift in the first
 *  place. Both are "" on a lane that has no such disclosure. */
export interface SpoolBodyOptions {
    /** The handle is still running (drives the "running" display status). */
    running: boolean;
    /** Fallback display status for a non-running handle with no recorded exit code. */
    handleStatus: SemaTaskStatus;
    /** The lane's filter+clip projection, applied to stdout and stderr alike. */
    shape: (text: string) => string;
    /** Appended INSIDE the roll note, before its newline ("" when the lane has no extra clause). */
    rolledExtraClause: string;
    /** A full banner line (own trailing newline) rendered after the drop note, or "". */
    drainNote: string;
}
/** The ONE renderer for a re-readable spool's TaskOutput body,
 *  shared by the monitor lane (pollMonitorLane) and the bash spool lane (pollBackgroundBash).
 *
 *  Both faces were hand-copied twins carrying three real disclosure bugs' worth of wording (env-side
 *  eviction vs registry roll are SEPARATE notes, never merged; a self-inflicted background-timeout kill
 *  renders distinguishably from an external stop; a spool with a disclosed gap must stop advertising
 *  itself as "full") — kept in sync by a comment on each side telling the reader to make the same edit
 *  twice. The heading rule is the load-bearing one: any disclosed gap (env-side drop OR a failed
 *  stop-time drain) downgrades the heading, so a caller cannot add a gap note and forget the heading.
 *
 *  NOT used by the blocked-wait leg: that face is a consume-once "new stdout" increment with its own
 *  wording ("accumulated char(s) dropped from the middle during the blocked wait"), an intentional
 *  divergence — it shares {@link accountDroppedBytes} + {@link spoolDropNote}, not this. */
export declare function renderSpoolBody(spool: RenderableSpool, opts: SpoolBodyOptions): string;
/** The honesty suffix for frames over a spool with env-side loss. Empty when nothing dropped. */
export declare function droppedGapNote(spool: {
    droppedBytes?: number;
    dropUnknown?: true;
}): string;
/** The event-stream disclosure rides the FIRST batch after a loss is observed (once-latch —
 *  a chatty watch must not repeat the note on every batch; terminal frames disclose unconditionally). */
export declare function firstDropNote(spool: {
    droppedBytes?: number;
    dropUnknown?: true;
    dropNotified?: true;
}): string;
/** ONE wording for "you stopped nothing" across every already-terminal stop face —
 *  the in-process bash/monitor/agent arms and the durable-row fallback previously split: the durable
 *  arm answered honestly while the in-process arms claimed `Terminated <id>.` over work that finished
 *  (or was killed) on its own. CC 220 refuses the call outright (`Task X is not running (status: …)`,
 *  220:468231-468236); sema keeps the idempotent no-op posture (stopWorkflow / durable-arm precedent,
 *  no isError) but the receipt must tell the truth. */
export declare function alreadyTerminalStopNote(id: string, status: string): string;
/** design/115 review B1 → A-2: register-side fail-fast, now on BOTH axes — `canAccess` is
 *  default-deny, so a handle missing either axis would be unreachable by every caller (a dead row
 *  is worse than a loud registration error). The production chain always sets both (hostTaskId +
 *  principal-or-"default"); a direct registry caller declares its access domain explicitly
 *  (`scope: "default"` is the single-tenant spelling — explicit, never implied). */
export declare function assertOwnership(input: TaskAccess, site: string): void;
/** POSITIVE-MATCH form, one polarity with `canAccess`/
 *  `canAccessAgentRecord` (A-2): scope must match, then the caller must HIT an arm — owner
 *  (the spawning task) or SESSION (the run's `originatingSessionId`). The session arm is the B-1
 *  HIGH fix: the completion notification routes back to the originating SESSION, but per-turn
 *  taskIds die with their turn — before this arm, a later turn of the same session could not poll
 *  or stop its own workflow (default-deny functional breakage). The old form's
 *  `access.owner === undefined ⇒ pass` scope-only residue is gone with the same β polarity ruling
 *  (design/136 §2.3 S-1 定谳 posture, now positive-match on every arm). Exported: the run-workflow
 *  tool's resume-ownership recheck and a deployment's serving layer apply the SAME predicate. */
export declare function canAccessWorkflowRun(run: Pick<WorkflowRun, "scope" | "sourceTaskId" | "originatingSessionId">, access: TaskAccess): boolean;
export declare function formatWorkflowRun(run: WorkflowRun): UnifiedTaskResult;
/** design/157 B13 一期定稿(二期细察补充 §「私有成员解法」)——the kernel-面 seam the MON/WF lane free
 *  functions call through instead of `this`: exactly the 5 members `startMonitorWatcher`/`stopMonitor`/
 *  `registerMonitor`/`registerWorkflow` actually reach into (`this.*` 耦合 grep 逐方法统计,见 B13 档).
 *  Does NOT include `fireServedTerminal` (WF-private helper, only called from within `pollWorkflow`
 *  itself — travels with the lane, never crosses the core seam) nor any AGENT-domain member (二期 extends
 *  this as `DurableAgentCore` per B13 §「私有成员解法」). The TaskRegistry class satisfies this via a
 *  `#coreView`-style cached object (`private readonly core: RegistryCore`), declared AFTER `handles` so
 *  the eager `handles` field reference below sees an already-initialized Map. */
export interface RegistryCore {
    handles: Map<string, RegisteredTaskHandle>;
    mintTaskId(type: SemaTaskType): string;
    markStopSource(id: string, source: StopSource): void;
    notifyTerminalOnce(handle: BackgroundBashTaskHandle | MonitorTaskHandle, send: () => void): void;
    pokeBgQuiescence(owner: string | undefined): void;
}
/** writer-lease renewal period for armed durable rows (see ensureDurableHeartbeat).
 *  Exported so deployments size `staleRunningMaxAgeMs` against it (recommend ≥10× this). */
export declare const DURABLE_AGENT_HEARTBEAT_MS = 60000;
/** design/151 S1a: the durable fallback consults the store ONLY for handle-shaped ids —
 *  a NAME (the fuzzy stop leg) or a foreign-domain id must not take a store round-trip, and a store
 *  outage must never block the local name-resolution ladder. Mint form: `a` + 16 hex; the revive
 *  leg re-registers the same form.
 *  EXPORTED: a serving layer's fallback needs the SAME shape gate before its
 *  durable arm (a bare exact-key miss lets padded/legacy/name-shaped ids leak into live branches —
 *  the server 1.250 HIGH) — one shared regex kills the drift, roster-normalize precedent. */
export declare const DURABLE_AGENT_HANDLE_RE: RegExp;
/** Synthetic settle error the subagent reap path passes after a stop/parent teardown. It is NOT a
 *  real child error — settleBackgroundAgent never stores it (status "killed" + stoppedBy already say
 *  it); exported so the subagent call sites and the registry agree on the exact sentinel. */
export declare const BG_AGENT_REAP_STOP_ERROR = "stopped (parent task ended or TaskStop)";
/** CC206-B 欠账 — TaskStop name addressing, CC `XN` verbatim (206:574955-574962): NFKC-normalize, strip
 *  control/format chars (whitespace-class ones survive), trim, lowercase, whitespace runs → hyphens. Both
 *  the query and the candidate label go through this, so "Fix Tests" addresses "fix tests".
 *  [c209-D]: EXPORTED — SendMessage's "main" reserved lane must use the SAME canonicalization
 *  as this resolver, or an NFKC-equivalent spelling (fullwidth "main") skips the reserved lane and
 *  reaches the description layers. One normalizer, every name face. */
export declare function normalizeAgentName(s: string): string;
/** CC `gtr` (fable 复审 F1 — 亲核 @16169850: NOT plain Levenshtein but OSA / restricted
 *  Damerau-Levenshtein, WITH the adjacent-transposition term `o[i][s]=min(o[i][s], o[i-2][s-2]+1)`
 *  when `e[i-1]===t[s-2] && e[i-2]===t[s-1]`). The transposition matters: a typo like "tset"→"test"
 *  is distance 1 (CC has a Did-you-mean), plain Levenshtein would call it 2. */
export declare function editDistance(a: string, b: string): number;
/** CC `htr` shape (206:519814-519827): candidates within Levenshtein ≤2 (length-diff ≤2 pre-filter),
 *  best-first; the single best is the "Did you mean" suggestion (`htr(e, keys, 1)[0]`, 206:575196). */
export declare function closestName(query: string, candidates: Iterable<string>): string | undefined;
/** DEFAULT-DENY on BOTH axes — the old predicate skipped
 *  a missing axis (owner-less handle = readable by everyone in scope; scope-less handle = readable
 *  across scopes), splitting polarity with `canAccessAgentRecord` and re-opening the session-axis
 *  class cli/server just swept. Registration now REQUIRES both axes ({@link assertOwnership}), so a
 *  missing axis here is a broken invariant, answered with denial, never a pass. */
export declare function canAccess(handle: SemaTaskHandle, access: TaskAccess): boolean;
/** design/157 B13 二期(B13 档预调研 §4.2/§4.3 定稿)— the durable-agent lane's widened core seam.
 *  Extends {@link RegistryCore} with the extra members the AGENT lane's `this.*` census reached
 *  (73 处/9 成员,扣车道内互调后落在 5 个上,见 B13 档「二期细察补充」;`reapedHandles` 是后加的
 *  第 6 个——reap 窗口的后半段,见其自身注释)。
 *  `durableAgentWrite`/`ensureDurableHeartbeat`/`settleBackgroundAgent` are NOT here — they are lane
 *  members themselves (车道内直接函数调用;类上只留转发或不留)。The class satisfies this via the
 *  same cached-view field, upgraded with lazy getter/setter proxies (an IIFE capturing `this`) so
 *  field declaration ORDER no longer constrains it — `writerId`/`reapingHandles` 등 are read at
 *  call time, after construction completes. */
export interface DurableAgentCore extends RegistryCore {
    readonly writerId: string;
    readonly reapingHandles: Set<string>;
    /** The SECOND half of the reap window: ids whose durable row the sweep has already DELETED under its
     *  rev guard, still inside the lifecycle fence while the sweep drops the mailbox and releases the
     *  session. The fence must hold across that cleanup, but the row is gone from the moment the delete
     *  wins, and the two halves owe a caller opposite answers — "this clears on its own, send again"
     *  versus "it is gone, relaunch". A subset of `reapingHandles`, cleared with it. */
    readonly reapedHandles: Set<string>;
    readonly claimingHandles: Set<string>;
    /** get/set proxy — `ensureDurableHeartbeatLane` both reads AND writes the timer slot. */
    durableHeartbeatTimer: ReturnType<typeof setInterval> | undefined;
    readonly deliverConfirmTimeoutMs: number;
}
/** design/153 §7.2c (件3b) — the compensable reservation ticket {@link TaskRegistry.claimParkedAgent}
 *  returns: everything {@link TaskRegistry.rollbackParkedClaim} and the 件3c consume flip need to
 *  proceed or compensate. `row` is the PRE-RESERVATION snapshot (restored verbatim on rollback);
 *  `reservedRev` is the row's rev AFTER the reservation CAS (the next guarded write binds to it). */
export interface ParkedClaimTicket {
    handle: string;
    scope: string;
    token: string;
    claimId: string;
    reservedRev: number;
    row: import("./background-agent-store.js").BackgroundAgentRecord;
}
/** Sibling of {@link mintCompletionId} for a caller that must compute the
 *  value BEFORE it is safe to commit (a CAS not yet known to win): fill-once with an EXTERNALLY-chosen
 *  value instead of generating a fresh one. Exists because `rollbackParkedClaim`'s original fix minted
 *  directly onto the live handle before its durable CAS was confirmed — a losing CAS then left a
 *  non-terminal ("parked") handle carrying a completionId nothing ever cleared, which a later legitimate
 *  parked→running flip (`consumeParkedFlip`) silently carried into an unrelated real completion via
 *  `mintCompletionId`'s own fill-once guard (independently reproduced end-to-end). The fix: compute the
 *  candidate once, use it in the durable payload immediately (that write is fine — it only takes effect
 *  if its own CAS wins), and commit it to the live handle ONLY from the call that already knows the CAS
 *  won. */
export declare function commitCompletionIdIfEmpty(target: {
    completionId?: string;
}, value: string): string;
export interface RegisterBackgroundAgentInput extends TaskAccess {
    /** design/153 §7.2d (件3c) — parked-resume registration shape: the handle is born PARKED (the
     *  pre-consume window keeps the live face's checkpoint arbitration; the consume flip
     *  {@link TaskRegistry.adoptParkedResume} is the only path to running). Pass the ticket's token
     *  and a stop-arbitration closure alongside. Also licenses replacing an in-process PARKED prior
     *  handle of the same id (the planned park→resume handoff; lane contaminated like the terminal replace). */
    initialStatus?: "parked";
    /** design/153 §7.2d — the parked checkpoint token this parked-born handle waits on. */
    parkedCheckpointToken?: string;
    /** design/153 §7.2d — the TaskStop arbitration closure for the parked-born handle (expire CAS). */
    resolveParkedStop?: () => Promise<boolean>;
    /** design/147 S2a — declare that the spawner WILL attach a mid-run delivery
     *  channel (attachAgentNotify) once the child's lane binds. Only declared-"attaching" rows buffer
     *  startup-window messages; an undeclared row refuses `no_channel` IMMEDIATELY — a spawner that
     *  never attaches must not collect acknowledged-then-discarded messages (silent loss). */
    deliveryChannel?: "attaching";
    /** S2b RB-27① — the spawner DECLARES whether a parked message is redeemable: true when
     *  the child's session outlives the run (retain ledger or session-scoped registration), so a
     *  continuation can drain the park. A non-retained row must NOT hand out parked acknowledgements
     *  (the session is released right after settle — the park would be irrecoverable): its
     *  lane-dead race window reads as `not_running` instead, steering the sender to the settled path. */
    retainedContinuation?: boolean;
    description?: string;
    /** [c209-D] explicit addressing name (CC Agent.name parity) — the CALLER validates it (charset +
     *  reserved "main") before registration; the registry stores it verbatim. Duplicate names are legal
     *  (latest-wins at resolution; older rows stay task_id-addressable). */
    name?: string;
    /** the RESOLVED subagent type ("general-purpose"/"fork"/a roster name), persisted
     *  so a REVIVE cycle's observer frames can reproduce the row's TYPE column (never the description). */
    agentType?: string;
    /** the spawn frame's parent linkage, persisted so a REVIVE cycle's spawn frame nests the
     *  revived row exactly like the first spawn did. The caller passes the SAME values it emitted on the
     *  original frame (parentTaskId already-clamped there; parentSessionId always-on). */
    parentTaskId?: string;
    parentSessionId?: string;
    /** the ROOT host session of the delegation tree (fixed point down the trusted
     *  internals chain; equals parentSessionId at depth 1). Persisted on the handle, the durable row
     *  and the roster so recovery faces enumerate the whole tree under the root without alias walks. */
    rootSessionId?: string;
    /** design/151 S3b — the revival lookup key (CC meta-sidecar shape): the model RECORD KEY,
     *  persisted on the durable row so a tier-3 revival rebuilds the spec the way a fresh
     *  named-teammate spawn would. A lookup key only, never a serialized spec. `model` is NOT the
     *  resolved model id: a spawn that named a model in WORDS records the caller's spelling verbatim
     *  (catalog key / tier word / CC alias) so the revival can re-resolve it against the catalog in
     *  force at WAKE time; only a spawn that carried a Model OBJECT records that object's id. Same
     *  value and same rule as the roster row's `model` column. */
    model?: string;
    toolUseId?: string;
    abort: AbortController;
    now?: number;
    /** design/129: register as session-scoped (owner MUST be the sessionId; see the handle flag). */
    sessionScoped?: true;
    /** design/151 S1a (LOW-12): caller-supplied handle — the tier-3 revival leg (S3) re-registers the
     *  SAME durable handle so the row stays continuous across restarts. Collision with a live in-memory
     *  row throws (registerBackgroundBash parity). Absent ⇒ mint. */
    id?: string;
    /** design/151 S1a: the durable execution-row ledger. Present (with BOTH owner and scope — the
     *  durable predicate is fail-closed on both axes) ⇒ this registration writes a durable row
     *  (put → bind → settle, a serialized best-effort single-writer lane); absent ⇒ the pre-151
     *  in-memory-only lifecycle, byte for byte. */
    store?: BackgroundAgentStore;
    /** design/151 §7.2 (X-2/F-7) — tier-3 revival lane seeding: the CLAIMED durable row (identity
     *  inherited verbatim, status running, writerId = this instance) and its post-claim rev. Present
     *  ⇒ the durable lane starts from this snapshot with `written: true` and SKIPS the initial put
     *  (the claim-CAS already committed the row); every later write CASes against `rev`, so the first
     *  settle moves the claim forward instead of losing to it. Also authorizes REPLACING a leftover
     *  TERMINAL in-memory handle of the same id (the pre-revival cycle's row survives ~1h of terminal
     *  GC): its stale lane is silenced first — a queued/re-driven write from it carries the pre-claim
     *  rev under this process's own writerId, and the adopt path would clobber the claim. */
    durableSeed?: {
        record: BackgroundAgentRecord;
        rev: number;
    };
}