import { type Context } from '@deepseek-ai/cordis'; import type { DshSelectedGeneration } from '@mstar-harness/engine'; import type { AgentFlowContinuationEventView, AgentFlowView } from '../types.ts'; /** The projected continuation view lives in the shared types module (it is a member of the exported {@link AgentFlowView} union) — re-exported here for the module's existing import surface. */ export type { AgentFlowContinuationEventView }; import type { Config } from './_shared.ts'; /** Default native-ledger read window. */ export declare const AGENT_FLOW_DEFAULT_LIMIT = 50; /** Bounded in-memory call-window candidate slots per parent Session. */ export declare const AGENT_FLOW_CATALOG_SLOT_CAPACITY = 500; /** * Bounded in-memory child pairing slots (apply-scoped, like the call/job * pairing maps) — keyed by the child `SessionId` and shared by the TEAM * spawn pairing and the subagent activation pairing (one settlement * channel). A `spawn_teammate` dispatch is paired to its member's child * `SessionId` and consumed by the member's `subagent/end` settlement; a * member whose settlement never arrives (service disposal, HMR, * an upstream shape change) must not grow the map forever. At capacity the * OLDEST entry is evicted with an explicit unresolved-continuation row for * that dispatch, so the bound never silently drops a pairing. */ export declare const AGENT_FLOW_TEAM_CHILD_CAPACITY = 1024; /** * Bounded in-memory TEAM member-child END buffer (apply-scoped, like the * pairing maps). A team member's initial turn can settle BEFORE its spawn's * `tools/post-execute` pairing write (the early-end reconcile race): the paired path used to drop * that `subagent/end` record silently, so the dispatch never settled. The * record is buffered instead and reconciled when the pairing write lands. * Styled after {@link AGENT_FLOW_TEAM_CHILD_CAPACITY} — which bounds the * pairing MAP, not this buffer; the two bounds count independently (and by * construction this one can never need more slots: an unmatched end exists * only while its spawn's pairing write is still in flight). At capacity the * OLDEST record is evicted with an explicit unresolved marker that names the * child id, states the eviction, and states that no settle was fabricated. */ export declare const AGENT_FLOW_TEAM_END_BUFFER_CAPACITY = 1024; /** Stable identity namespace for verified native workflow events. */ export declare const WORKFLOW_EVENT_ID_PREFIX = "wfe1"; /** Stable identity namespace for live tool-call rows. */ export declare const CALL_EVENT_ID_PREFIX = "wfc1"; /** * Workflow field length caps are enforced at the producer boundary and again * when native rows are projected. Oversized IDs skip the row rather than * being truncated into a collision; oversized display fields are truncated * deterministically with a visible marker. Invalid optional settle identities * are omitted while the real completion remains recorded. Sequence numbers * stay within the exact range supported by the native scan-bound API. */ export declare const WORKFLOW_LEDGER_MAX_ID_LENGTH = 512; /** Cap for label-sized display fields (`label`, `phase`). */ export declare const WORKFLOW_LEDGER_MAX_LABEL_LENGTH = 512; /** Cap for the run display `name`. */ export declare const WORKFLOW_LEDGER_MAX_NAME_LENGTH = 1024; /** The deterministic suffix marker appended to capped display fields. */ export declare const WORKFLOW_LEDGER_TRUNCATION_MARKER = "\u2026"; /** Upper bound (exclusive) for envelope + member sequence numbers. */ export declare const WORKFLOW_LEDGER_MAX_SEQ: number; /** * Deterministically cap one display field: values at or under the cap pass * through unchanged; longer values are truncated to `cap − marker` chars * plus the {@link WORKFLOW_LEDGER_TRUNCATION_MARKER} suffix (the marker * guarantees the truncation is visible in the panel — never a silent cut). * CODE-POINT gated AND sliced : `String.prototype.slice` operates on UTF-16 code units and can split * a surrogate pair at the cap boundary (a lone surrogate renders as U+FFFD * in the log line), and a UTF-16 `.length` gate would false-positive * truncate astral-dense values (≤ cap code points but > cap UTF-16 units) * then re-inflate the result to cap code points — up to ~2× the cap in the * unit the gate measured. ONE `Array.from` drives both the gate and the * slice, so the bound is measured and enforced in the same unit; a * multi-byte character at the boundary is kept whole, at the cost of at * most one code point of slack. Pure — NEVER throws. */ export declare function truncateLedgerField(value: string, cap: number): string; /** * Canonicalize one Assignment `Execute as` value for the ledger's `role` * field: `trim()`, then strip ONE leading `@`. The `role` column is what * every consumer groups by, so `@explore` and `explore` must be the SAME * role — normalization happens ONCE, at the write boundary (the dispatch row * and its pairing ref, plus the direct `recordSettle` / * `recordSubagentLink` entry points, whose callers may hand in a raw * Assignment value). NO case folding: role ids are host-defined strings, and * folding could merge ids a host intends to keep distinct. No interior * rewrite either — only the edges are trimmed. A missing role stays `''`, * and rows already on disk are never rewritten or renormalized. Pure — * NEVER throws. */ export declare function normalizeRoleId(value: string): string; /** Logger label for the agent-flow ledger (dsh logger naming: `/`). */ export declare const AGENT_FLOW_LOGGER = "mstar/agent-flow"; /** * The settle seam name — the dsh-tools registry's `tools/post-execute` * waterfall. VERIFIED to be dispatched by the real registry for every tool * call (`runPostExecute` → `postExecute`, upstream source; pinned by the * real-call probe in `tests/agent-flow.spec.ts`), so settles are no longer * host-emission-dependent (the * old "not part of the verified surface" assumption is obsolete). */ export declare const SETTLE_SEAM = "tools/post-execute"; /** * The once-per-apply settle-pairing trace. Foreground dispatch settles through * the tool seam; background jobs settle from the jobs lifecycle event stream. */ export declare const SETTLE_SEAM_PAIRING_NOTE = "settle seam \"tools/post-execute\" IS part of the verified dsh-tools registry surface (runPostExecute dispatches it for every tool call) \u2014 foreground dispatch calls settle here, background subagents settle from ctx.inject(['jobs']) \u2192 jobs.events settled notifications; only UNPAIRED payloads (non-dispatch tools, calls outside the apply-scoped pairing window) stay dispatch-only \u2014 never a fabricated settle"; /** Dispatch verdict vocabulary (spec §2.1.3). */ export type DispatchVerdict = 'ok' | 'advisory' | 'denied'; /** Settle outcome vocabulary (spec §2.1.3). */ export type SettleOutcome = 'ok' | 'error' | 'denied'; /** Workflow run terminal reason (`tool-workflow` vocabulary — `workflow/src/types.ts:63`). */ export type WorkflowStopReason = 'completed' | 'cancelled' | 'error'; /** * Workflow/ralph gate verdict vocabulary : the RESOLVED outcomes reuse the dispatch verdict vocabulary * (`ok`/`advisory`/`denied` — the plan interface "the workflow verdict * vocabulary"); `ask` is the PENDING-decision member — the first-seen ask * itself is a gated call, so its row carries `ask` until the approval * waterfall resolves it ("one ledger row per gated call"). */ export type WorkflowVerdict = DispatchVerdict | 'ask'; /** * The workflow/ralph gate mode (Config `workflowGate` — plan * verdict rows), recorded on every verdict row. * `off` rows never exist: the gate short-circuits `off` BEFORE the policy, * so no verdict is produced. */ export type WorkflowGateMode = 'off' | 'warn' | 'ask' | 'hard'; /** * One v1 workflow ledger event (the native SQLite row). Produced from the * durable `tool-workflow/*` session events by the session-event consumer: * `run-start` → `workflow-run`, `agent-start` → `workflow-agent`, and * `run-end` → `workflow-run-end`; upstream `agent-end` has no ledger kind. * Optional fields (`agent` / `phase`) are omitted when absent. */ export type AgentFlowWorkflowEvent = { v: 1; ts: number; kind: 'workflow-run'; /** The workflow run's stable id (upstream `runId`). */ runId: string; /** The run's display name (upstream `name`). */ name: string; /** The calling session's stable id, when carried. */ agent?: string; } | { v: 1; ts: number; kind: 'workflow-agent'; /** The workflow run's stable id (upstream `runId`). */ runId: string; /** 1-based `agent()` call sequence within the run (upstream `seq`). */ seq: number; /** The published member's display label (upstream `label`). */ label: string; /** The member's phase, when carried (upstream `phase?`). */ phase?: string; /** The published child session's stable id (upstream `childId`). */ childId: string; } | { v: 1; ts: number; kind: 'workflow-run-end'; /** The workflow run's stable id (upstream `runId`). */ runId: string; /** Terminal run reason (upstream `stopReason`). */ stopReason: WorkflowStopReason; }; /** * One v1 event (spec §2.1.3 + native row). Optional fields are omitted when * absent. The three `workflow-*` variants are the W-B2 addition; see * {@link AgentFlowWorkflowEvent}. */ export type AgentFlowEvent = { v: 1; ts: number; kind: 'dispatch'; /** The dispatching session's stable id (exec.agent.id; host-hook path has no exec → absent). */ agent?: string; /** Assignment `Execute as` ('' when missing). */ role: string; /** planIdOf(header): `Plan Path` / `SDD dir` / `plan_id` basename. */ planId?: string; /** Body `Task N` best-effort extraction (taskIdOf). */ taskId?: string; /** Assignment `Task category`. */ taskCategory?: string; /** Gate verdict derivation: no violations → ok; hard + violations → denied; else advisory. */ verdict: DispatchVerdict; /** resolveDispatchHard result (recorded unconditionally, incl. hard denies). */ hard: boolean; /** * The live tool-call id the dispatch seam observed (the SAME id the * settle / continuation row carries), when the exec supplied one — the * call-scoped pairing identity that keeps two concurrent same-identity * dispatches distinct. Omitted when the seam supplied none — never * fabricated. */ callId?: string; } | { v: 1; ts: number; kind: 'settle'; /** The settled session's stable id (the paired dispatch's agent). */ agent?: string; outcome: SettleOutcome; durationMs?: number; /** * The PAIRED dispatch's identity (plan same field names + semantics as the dispatch event: * `role` is the Assignment `Execute as` ('' when missing), `planId` / * `taskId` the plan + `Task N` tags. Written for every paired settle; * ABSENT on unpaired (legacy) settles — the client pairs on identity * presence (`paired` in the view never depends on `childId`). */ role?: string; planId?: string; taskId?: string; /** * The settled child session's stable id, when a seam actually supplied * one: the returned foreground `runId`, or a background job's child id * once the catalog join copied it onto the dispatch ref. OMITTED * otherwise — never fabricated, never a registry job id. */ childId?: string; /** * The registry background-job id a background settle pairs on * (`jobs.events` settled notification → `recordJobSettle`). A jobs-registry key * (`-N`), NEVER a child session id and never the Assignment * `Task N` tag (`taskId`). */ taskRef?: string; /** * The paired dispatch's live tool-call id — the settle row's call-scoped * pairing identity (the SAME id the dispatch row carries). Omitted when * the pairing ref carried none — never fabricated. */ callId?: string; } | { v: 1; ts: number; kind: 'subagent-link'; /** The dispatching session's stable id (the paired dispatch ref's agent; absent when it carried none). */ agent?: string; /** * The catalog `childId` — the child session the dispatch actually * started. REQUIRED: a row whose identity is missing, empty, or * oversized is skipped (never truncated, never fabricated). */ childId: string; /** * The delegation `description` the join correlated on, DISPLAY- * normalized and capped at this write boundary (the matcher always * compares the RAW label, never this value). */ label: string; /** The paired dispatch's Assignment `Execute as` — written even when `''`. */ role: string; /** The paired dispatch's `planIdOf(header)`. */ planId?: string; /** The paired dispatch's Assignment `Task N` tag. */ taskId?: string; /** * The registry background-job id, on a link whose dispatch started a * background job (`-N`). A continuable link omits it — that path * starts no registry job. Never a child session id. */ taskRef?: string; } | { v: 1; ts: number; kind: 'workflow-verdict'; /** The calling session's stable id, when carried. */ agent?: string; /** The matched tool name (`'workflow'` | `'ralph'`). */ tool: 'workflow' | 'ralph'; /** `meta.name` for workflow calls — the P-a/P-c allowlist identity. */ workflow?: string; /** `objective` for ralph calls — the ralph identity. */ objective?: string; /** The effective `workflowGate` mode at decision time (never `off` — off records nothing). */ mode: WorkflowGateMode; /** The gate verdict (ok/advisory/denied/ask). */ verdict: WorkflowVerdict; /** The policy violation code (advisory/ask/denied rows only). */ code?: string; } | AgentFlowWorkflowEvent; /** * The explicit UNRESOLVED-CONTINUATION marker of one team dispatch. It is * written through the same native ledger append and carries the paired * dispatch identity, but it is deliberately NOT a settle: it is written ONLY * instead of a settle when a team dispatch's continuation cannot be paired — * never an `ok` settle, never a fabricated outcome. Projected — the * unresolved-continuation projection: {@link eventFromUnknown} accepts the kind and the catalog view * lists the row (its state + reason) instead of dropping it, so the panel * derives a DISTINCT non-running entity state from it. Additive only — every * existing event kind keeps its projected rendering. */ export interface AgentFlowContinuationEvent { v: 1; ts: number; kind: 'continuation'; /** The dispatching session's stable id (the paired dispatch ref's agent; absent when it carried none). */ agent?: string; /** The paired dispatch's Assignment `Execute as` ('' allowed — the dispatch row's grammar). */ role: string; /** The paired dispatch's `planIdOf(header)`. */ planId?: string; /** The paired dispatch's Assignment `Task N` tag. */ taskId?: string; /** * The continuation state. `unresolved` is the ONLY value: the dispatch was * observed but its continuation cannot be paired (member/child identity * unresolvable, no attributable member turn, pairing window ended). */ state: 'unresolved'; /** Bounded display reason (normalized; why the continuation could not be paired). */ reason: string; /** * The paired dispatch's live tool-call id, when the dispatch seam supplied * one — the call-scoped pairing identity a reader pairs this row with * (two concurrent same-identity dispatches stay distinct). Omitted when the * ref carried none — never fabricated. */ callId?: string; } /** One durable ledger row this module appends and the reader projects (the continuation marker is projected, not dropped). */ type AgentFlowLedgerEvent = AgentFlowEvent | AgentFlowContinuationEvent; /** * The identity of one recorded dispatch, carried by the pairing store so a * later completion (post-execute settle / settled jobs event) can record a * settle carrying the SAME identity fields as its dispatch event. */ export interface AgentFlowDispatchRef { /** The resolved `{HARNESS_DIR}` the dispatch was attributed to. */ harnessDir: string; workflowId: string; /** The ACTIVE workflow directory; process target only, not machine identity. */ workflowDir: string; selectedGeneration: DshSelectedGeneration; /** The dispatching session's stable id ('' when the exec carried none). */ agent?: string; /** * The tool-call id observed at the dispatch seam (the SAME id the settle * seam looks the pairing up by). It is the record identity namespace of * the live call rows (`wfc1:::`), so a settle / * link row can carry the identity of the call that produced it. Omitted * when the seam supplied none — never fabricated. */ callId?: string; /** Assignment `Execute as`, canonicalized by `normalizeRoleId` ('' when missing — the dispatch event's grammar). */ role: string; /** `planIdOf(header)` — the dispatch event's grammar. */ planId?: string; /** `taskIdOf(prompt)` — the Assignment `Task N` tag, NOT a registry task id. */ taskId?: string; /** * The child session's stable id, once an upstream seam actually supplied * one: the catalog join copies the observed catalog `childId` onto this * ref (the same object survives into `dispatchByJobId`, so a background * settle then carries it). Omitted until known — never a synthetic id and * never a registry job id. */ childId?: string; /** * The registry job id of the background call this dispatch started * (observed at `tools/post-execute`, paired by `recordJobSettle`). A * jobs-registry key (`-N`) — NEVER a child session id and never the * `Task N` tag. */ taskRef?: string; /** * The admitted call-window catalog CANDIDATE of this dispatch: the live * parent Session it runs on plus the RAW delegation label that names its * slot. Present only while a reservation is live — `recordDispatch` * installs it, and the post-execute branch either advances it to eligible * or retires it (both release this field). Its presence is what lets the * post-execute path find the join without re-deriving the session. */ catalog?: { session: AgentFlowSessionView; label: string; }; } /** * The structural view of one live dsh `Session` the call-window join needs: * the VERIFIED upstream surface (`id` / `seq` / `eventAt`). A real Session * exposes no `events` member — that read is why the join uses `eventAt(seq)` * over a bounded window instead of copying a log. Structural by design (no * session package dependency); `seq` is read LIVE at every use, so the * captured view is also the catch-up bound source while `fromSeq` (stored on * the join) keeps the capture moment. */ export interface AgentFlowSessionView { readonly id: string; readonly seq: number; eventAt(seq: number): unknown; } /** * One pending call-window catalog candidate: the recorded dispatch ref and * the `fromSeq` window start. `result` marks ELIGIBILITY — an inert * candidate (no `result` yet, awaiting its post-execute branch) can never be * matched by a catalog, and the slot's `null` tombstone is a * consumed/rejected candidate, never a second one. */ export interface AgentFlowCatalogJoin { ref: AgentFlowDispatchRef; fromSeq: number; result?: { kind: 'background'; jobId: string; } | { kind: 'continuable'; subagentId: string; }; } /** * The apply-scoped pairing store ( * decision D1 — created in the entry `apply`, same lifetime as the catalog * cache; an HMR restart resets it, and completions outside the window stay * unpaired → no settle, the documented honest degrade). Maps are keyed by * the TWO verified pairing keys: the tool-call `callId` (pre → post-execute) * and the registry background-job id (post-execute background shape → * `jobs.events` settled lifecycle notification). */ export interface AgentFlowPairing { /** * The agent-namespaced call key → the dispatch it recorded (populated by * `recordDispatch` when an exec is present). Key = `${sessionId}\u0000${toolName}\u0000${callId}` * : a raw `ToolExecution.callId` is NOT globally unique * in one process — dsh runs many sessions concurrently and upstream mints * per-message ids (`call-${index}`), so the dispatching session id must * namespace the key or session B's same-id call could overwrite session A's * pairing and mis-pair A's settle into B's dispatchRef. Consumed (deleted) * by the post-execute branch — the map holds only in-flight calls. */ dispatchByCallId: Map; /** Registry background-job id (`JobSnapshot.id`) → the dispatch that started it (populated by the post-execute background branch; consumed by `recordJobSettle`). */ dispatchByJobId: Map; /** * Child `SessionId` → the dispatch that started it. Populated by the team * post-execute branch once the roster read resolves the model-facing member * name (`spawn_teammate`), and by the subagent activation branch for the * `{ kind: 'activation', subagentId }` return (started, not settled); * consumed by the child's `subagent/end` settlement, or evicted at * {@link AGENT_FLOW_TEAM_CHILD_CAPACITY}. Optional so existing pairing * constructors stay valid; both branches create it lazily. */ teamDispatchByChildId?: Map; /** * Team member child `SessionId` → the `stopReason` of the member's * `subagent/end` settlement that arrived BEFORE its `spawn_teammate` * pairing write (the early-end reconcile race). Created lazily by the end-buffer path * (existing pairing constructors stay valid); the pairing write reconciles * against it first — a hit settles from the buffered record and consumes * the entry, so no dispatch can settle twice. Bounded by * {@link AGENT_FLOW_TEAM_END_BUFFER_CAPACITY} (independent of the pairing * map's own bound). */ teamEndBuffer?: Map; /** * The call-window catalog join's slots: the EXACT parent Session object → * (RAW delegation label → candidate). A `null` slot is a consumed or * rejected tombstone, never a second candidate — reuse is what would let a * delayed duplicate label or a pre-existing catalog acquire a new owner. * Keying on the session OBJECT (not its id) is what makes the namespace * session-incarnation safe and delimiter-free; per Session the map holds at * most `AGENT_FLOW_CATALOG_SLOT_CAPACITY` slots (new keys are refused at capacity — * existing pending entries and tombstones are retained). */ catalogBySession: WeakMap>; } /** Module-scoped log sink (bound to `mstar/agent-flow` by the entry at apply). */ type AgentFlowLogSink = (level: 'info' | 'warn' | 'error', message: string) => void; /** * Module-scoped catalog-invalidation hook : called with * the affected `{HARNESS_DIR}` after every SUCCESSFUL ledger record * (`recordDispatch` / `recordSettle`). The entry binds the real invalidation * closure at apply (the apply-scoped harnessDir → cache-key * reverse-map closure in `index.ts` — see `createCatalogInvalidation`); * unbound → no-op. Never throws into the record path. */ type AgentFlowInvalidator = (harnessDir: string) => void; /** * Bind the module's catalog-invalidation hook (plan * the settle-pairing upgrade — same pattern as * {@link setAgentFlowLogger}; the entry binds at apply; the apply-scoped * real binding — the apply-scoped harnessDir → cache-key reverse-map * closure in `index.ts`). * @param invalidate - the hook (`undefined` clears the binding). * @returns the PREVIOUS hook (tests restore it in a `finally`). */ export declare function setAgentFlowInvalidator(invalidate: AgentFlowInvalidator | undefined): AgentFlowInvalidator | undefined; /** * Bind the module's log sink (called once at apply; tests may rebind to * capture ledger logs). Rebinding RESETS the once-per-apply trace latches * — each binding is a fresh "apply" (production binds once; tests bind * per case for deterministic capture). * @param sink - the sink (entry binds `ctx.logger('mstar/agent-flow')`); * `undefined` clears the binding (restores the pre-bind no-op state). * @returns the PREVIOUS sink (tests restore it in a `finally`). */ export declare function setAgentFlowLogger(sink: AgentFlowLogSink | undefined): AgentFlowLogSink | undefined; /** * Best-effort extraction of the targeted `Task N` from the Assignment BODY * (spec §2.1.1 — `taskIdOf`). The engine `assignmentHeaderRegion` boundary is * reused: only text AFTER the header region is scanned, so a `## Task N` * example quoted in the header never resolves a task id. Only a LEVEL-2 * heading (`^## Task N`) matches (an example or sub-heading at * another depth before the real task must not resolve — lower false-hit * surface); normalized to `T` (matches the panel render `planId#taskId`, * e.g. #T2`). * @param prompt - the full Assignment text. */ export declare function taskIdOf(prompt: string): string | undefined; /** * The durable source position of one workflow event: the carrying native * session, the native log INCARNATION that produced the envelope, and the * envelope's position in that log. `streamId` deliberately is NOT the store * epoch — an epoch change is not a new stream and must not duplicate * history; a rebuilt/reused log, by contrast, is a different incarnation the * caller names explicitly. */ export interface AgentFlowEventSource { /** The carrying native session's stable id. */ sessionId: string; /** The native log incarnation (a session id when the surface carries no explicit incarnation). */ streamId: string; /** The envelope's position in that log (integer in `[0, 2^31)`). */ seq: number; } /** * The identity transport of one NEW ledger line. `eventId` is the stable * record id; `source` is present on durable workflow events (the identity and * transactional scan bound are keyed to its stream position) and absent on * live tool-call rows (their identity is the call, not a log position). Older * rows carry neither — their identity is their native row contents. */ export interface AgentFlowRecordIdentity { /** The stable record id ({@link workflowEventId} / {@link callIdentity}). */ eventId: string; /** The durable source position (durable workflow events only). */ source?: AgentFlowEventSource; } /** * The deterministic event id of one durable workflow event: * `wfe1::::` — the COMPLETE verified identity * tuple. BOTH the carrying native session id and the verified log incarnation * are part of the id, so two records can never collide across sessions or * across log incarnations of one session. Deterministic by construction: a * replay after a crash rebuilds the SAME id, so the ledger recognizes its own * already-appended row and never appends it twice. */ export declare function workflowEventId(event: AgentFlowWorkflowEvent, source: AgentFlowEventSource): string; /** * The identity transport of one live tool-call row, formed ONLY from a * verified carrying native session id AND the call id. Either one missing * (an exec without an agent, a call without a callId) yields NO identity — * the row is still recorded with its documented advisory semantics, but no id * is fabricated from an empty string or an unrelated handle, and the row can * never collide with another session's call. The carrying session namespaces * the call id because a raw `callId` is not globally unique in one process. */ export declare function callIdentity(kind: AgentFlowLedgerEvent['kind'], sessionId: string | undefined, callId: string | undefined): AgentFlowRecordIdentity | undefined; /** SHA-256 digest of one serialized row, used for durable event identity conflict checks. */ export declare function digestOfLedgerLine(line: string): string; /** * Record one dispatch event (spec §2.1.3). Fully try/catch-contained — NEVER * throws into the gate; a failing record logs only (`mstar/agent-flow`). * Verdict derivation (ok/advisory/denied, incl. hard denies) and the header * identity derivation (role / planId / taskId / taskCategory) reuse the gate's * own parsers — one grammar. * * Pairing: when the input * carries an `exec` AND the apply-scoped `pairing` store, the successful * record registers the session- and tool-namespaced key * `${sessionId}\u0000${toolName}\u0000${callId}` → dispatchRef (the full * dispatch identity), so a later `tools/post-execute` for the same call can * settle with the SAME identity (the session id namespaces the key — a raw * callId is not globally unique across sessions in one process — and the tool * name keeps two channels' per-message ids from colliding). The pairing registers * only after the ledger append SUCCEEDED — a failed record never pairs to a * phantom dispatch. An exec-less record (host-hook path) has no callId → no * pairing. The pairing sub-path has its OWN catch scope: a `Map.set` throw must not log "record * failed" after the dispatch was already appended. On that same successful * pairing the dispatch's call-window catalog candidate is RESERVED (the * first RAW-label slot under the live parent Session, when the exec carries * one) — the child-identity join's step 1; admission is all-or-nothing and * never affects the dispatch, the pairing, or a later settle. TEAM carriers * are the ONE exception to both the unconditional record and the catalog * reservation: with the experimental team service absent they record nothing * at all, and when present they never reserve a call-window slot (the * `spawn_teammate` member identity comes from the roster read, not the raw * label), so a team call cannot permanently consume one of the 500 slots. * @param input - the harness dir; the dispatching exec's agent id; the * Assignment text; the gate's violations; the hard-enforcement resolution; * the apply-scoped pairing store (the adapter passes its own; direct callers * may omit it); the carrying session's `hint` (the adapter derives it — this * module never reads the picker store); whether the optional team service is * mounted; the tool names the settle seam can pair (the adapter passes its * Config-resolved set; omitted → the documented defaults, so an unknown * exec-bound name still records its dispatch but leaves NO pairing entry). */ export declare function recordDispatch(input: { harnessDir: string; exec?: unknown; prompt: string; violations: readonly unknown[]; hard: boolean; pairing?: AgentFlowPairing; workflowId: string; /** ACTIVE execution graph directory target resolved by the asynchronous host gate. */ resolvedWorkflowDir: string; selectedGeneration: DshSelectedGeneration; /** * Whether the experimental Agent Teams service is mounted. Team carriers * record NOTHING while this is not `true` (the plugin keeps working on a * composition that mounts no team bundle); non-team carriers ignore it. */ teamPresent?: boolean; /** * The tool names the settle listener can pair (Config `dispatchTools` plus * the fixed team carriers). An exec-bound record whose name is not in this * set still writes its dispatch row but registers NO pairing entry and NO * catalog candidate — a name the settle seam declines can never leak * in-memory state. */ pairingTools?: readonly string[]; }): void; /** * Record one settle event. A paired settle writes through the same native * store generation retained by its dispatch, even if active workflow selection * changes. A direct unpaired call without a selected authority does not write. * Optional child/task identities are omitted when missing or invalid; the real * completion still records. Pairing identity fields are canonicalized and * retained so clients can match the settle to its dispatch. */ export declare function recordSettle(input: { harnessDir: string; workflowId: string; workflowDir: string; agent?: string; outcome: SettleOutcome; durationMs?: number; role?: string; planId?: string; taskId?: string; childId?: string; taskRef?: string; /** The paired dispatch's tool-call id — the settle row's live-call identity (omitted when unknown). */ callId?: string; selectedGeneration: DshSelectedGeneration; }): void; /** * Atomically accept one verified native event, append its display row, and * advance the matching per-session scan bound. */ export declare function recordWorkflowEvent(input: { harnessDir: string; workflowId: string; workflowDir: string; event: AgentFlowWorkflowEvent; source: AgentFlowEventSource; selectedGeneration: DshSelectedGeneration; }): boolean; /** * Input for {@link recordWorkflowVerdict} — one gated workflow/ralph call's * decision identity (verdict + * metaName/objective + mode). */ export interface WorkflowVerdictInput { /** The resolved `{HARNESS_DIR}` the row records into. */ harnessDir: string; /** The in-flight tool call (structural read for the carrying session id). */ exec?: unknown; /** The matched tool name (`'workflow'` | `'ralph'`). */ tool: 'workflow' | 'ralph'; /** `meta.name` for workflow calls (the P-a/P-c allowlist identity). */ workflow?: string; /** `objective` for ralph calls (the ralph identity). */ objective?: string; /** The effective `workflowGate` mode at decision time. */ mode: WorkflowGateMode; /** The gate verdict. */ verdict: WorkflowVerdict; /** The policy violation code (advisory/ask/denied rows only). */ code?: string; /** The authorized directory and original generation selected by the gate. */ workflowId: string; resolvedWorkflowDir: string; selectedGeneration: DshSelectedGeneration; } /** * Record one workflow/ralph gate verdict row . Fully * try/catch-contained — NEVER throws into the gate (`gateWorkflow` calls * this on EVERY policy decision); a failing record logs only * (`mstar/agent-flow`), so a ledger write never crashes or alters a * workflow call. The event is serialized lossless — optional fields * (`agent` / `workflow` / `objective` / `code`) omit when absent; the * display identity fields (`workflow` / `objective`) are capped at the * ledger boundary (same discipline as the W-B2 name cap) and the ralph * `objective` is routed through the SHARED `normalizeWorkflowName` strip at * this write boundary (— control-char discipline parity with the * name/label/phase fields; the gate already normalizes `workflow`). The * Persisted rows are JSON-serialized, so control characters cannot corrupt * the native ledger boundary; the panel view still normalizes hostile display * text. Malformed input is a caller bug — strict narrowing applies on READ, * never here. * @param input - the gate's decision identity (see {@link WorkflowVerdictInput}). */ export declare function recordWorkflowVerdict(input: WorkflowVerdictInput): void; /** Read the latest native event window; workflowId is the selected machine identity. */ export declare function readAgentFlow(workflowDir: string, workflowId: string, limit?: number, harnessDir?: string): AgentFlowView | null; /** * The `tools/post-execute` settle pairing : the VERIFIED * dsh-tools registry dispatches this seam for every tool call * (`runPostExecute` → `postExecute`, upstream source), so the listener only * decides whether a completion signal exists for the PAIRED dispatch: * * - non-dispatch tool (`exec.name` ∉ Config `dispatchTools` and ∉ the fixed * {@link TEAM_DISPATCH_TOOLS} carriers) → nothing; * - dispatch tool whose agent-namespaced call key * (the exact call identity) is not in the pairing * store (HMR reset, host-hook dispatch, non-gate path) → nothing (warned * once per registration — honest degrade, never fabricated settlement); * - `result.isError === true` OR an `error` payload present → settle `error` * immediately (fabrication guard — the dispatch * call failed; a result carrying `error` without `isError` never settles ok); * - successful `result.value` shape `{ kind: 'background', jobId }` with a * valid jobId (the registry id, `-N` — never a child session id) → * store `jobId → dispatchRef` and the bounded job id as the ref's `taskRef` * (so the eventual settled jobs event carries it); `{ kind: 'background' }` * WITHOUT a valid jobId → nothing mappable (no settle); * - `{ kind: 'continuable', subagentId }` → no terminal signal this round → * no settle (documented limit — the child owns its turns), and NOTHING is * copied from its value onto any row; * - `{ kind: 'activation', subagentId }` (the installed dsh `subagent` * return — STARTED, not settled) → no settle at tool return; the dispatch * stays open and is paired to the returned child id, settled later by the * child's `subagent/end` through the SAME keyed-by-child pairing and * outcome mapping the team spawn uses; a result without a usable * `subagentId` → nothing mappable (no settle, no pairing); * - any other successful value (foreground `{ kind: 'foreground', … }` * included) → settle `ok` (the call completed synchronously). * Child identity: a returned foreground `runId` becomes the settle's * `childId`, extracted from the value INDEPENDENTLY of the outcome branch — * a failed call whose value still carries a foreground `runId` keeps that * identity on its `error` settle (identity never turns an error into `ok`). * The consumed `dispatchByCallId` entry is DELETED after the branch resolves * the call (map pruning — each callId * pairs exactly once; the map holds only in-flight calls). * * Catalog candidate (steps 2–3 of the child-identity join): the SAME branch * decides the dispatch's reserved call-window catalog candidate — a valid * background result makes it eligible expecting the job's `one-shot` catalog * (after the job pairing + `taskRef` are in place), a valid continuable OR * activation result makes it eligible for exactly the returned `subagentId`, * and every * other outcome (other success / foreground, tool error, missing result * payload, malformed result, invalid background job id) retires it to its * tombstone. Eligibility then runs the catch-up walk over * `[fromSeq, session.seq)` through the same matcher the live observer uses, * so a catalog appended before this seam ran is still recovered. Both * candidate helpers are self-contained: a throwing join degrades to a log * line and NEVER costs the settle/awarded identity its row. * * The waterfall MUST be delegated via `next()` on every path — returning * without calling `next` bails the chain and breaks every tool call. A * throwing record never propagates. * * - a TEAM carrier ({@link TEAM_DISPATCH_TOOLS}) → the team branch * ({@link handleTeamPostExecute}): never settled at tool return; the * `spawn_teammate` member is paired to its child session and settled by the * member's `subagent/end`, every other shape records an explicit unresolved * continuation, and the call key is always consumed. * * The SAME registration also observes `subagent/end` (the verified * continuable-child settlement edge) for the member-child pairing: a * `spawn_teammate` dispatch completes when its member's initial turn settles, * not when the creation call returns. An untagged registration at the plugin * apply observes every scoped child edge upstream. * * Cordis typing note: `ctx.on` only accepts declared event keys and * `tools/post-execute` / `subagent/end` are not declared by this package's * imports — the registrations cast through the runtime-accepted event-name * string (the event bus dispatches any name). * @param ctx - registrant context (fiber disposal unwinds the listeners). * @param config - the plugin Config (dispatch-tool matching). * @param pairing - the apply-scoped pairing store (dispatchByCallId read, * dispatchByJobId written by the background branch, catalogBySession * advanced/retired here, teamDispatchByChildId written/consumed by the team * branch). */ export declare function registerSettleListener(ctx: Context, config: Config, pairing: AgentFlowPairing): void; /** * Structural read of the settled dsh-jobs projection used by pairing. The * pinned peer exposes it as `event.job` on `jobs.events.subscribe` lifecycle * events. `status` is one of `completed | killed | failed`; `startedAt` and * `finishedAt` are epoch ms. `id` is the registry JOB id (`-N`), never a * child session id. The optional service remains structurally typed. */ export interface JobDoneSnapshot { /** The registry-issued job id (`-N`, e.g. `subagent-1`) — never a child session id. */ id: string; /** Terminal lifecycle status: `completed | killed | failed`. */ status: string; /** Epoch ms when the task was registered. */ startedAt?: number; /** Epoch ms when the task settled. */ finishedAt?: number; } /** * Record a background-job settle from a terminal jobs lifecycle event. The * projection's registry job id must hit `dispatchByJobId`; a miss records * NOTHING (honest degrade, never fabricated). Outcome mapping: * `completed → ok` / `killed → denied` / `failed → error`; * `durationMs = finishedAt − startedAt` when both are present. After a * successful settle the consumed entry is deleted; a non-terminal projection * records nothing and keeps the entry so a later terminal event can settle. * Fully contained — never throws into the jobs event notification. * @param snapshot - the terminal job projection. * @param pairing - the apply-scoped pairing store. */ export declare function recordJobSettle(snapshot: JobDoneSnapshot, pairing: AgentFlowPairing): void; /** * Record one NONTERMINAL `subagent-link` row — the call-window join's output: * the child session a dispatch actually started, correlated back to the * dispatch identity mstar recorded. This is an IDENTITY record, not a * completion: the row carries NO `outcome`, NO `verdict` and NO `paired` * marker (the child is still running this round), and `ts` is the link * OBSERVATION time — never a fabricated child-creation time * (`childCreatedAt` is validated upstream, it is not another ledger clock). * The row is written ONLY from a candidate that actually matched, and only * from the ref of a dispatch that was really recorded: a link is never a * guess or a synthesis. * * Attribution follows the dispatch, not the clock: the row lands in * the native directory recorded when its dispatch was appended, never in today's * active workflow dir, and the ref's `harnessDir` is the invalidation key. * Fully try/catch-contained — a failing link write logs only and never * escapes into the session append (or the job notification) that triggered * it. * @param input - the retained dispatch ref, the observed catalog `childId`, * and the RAW delegation label the join matched on. */ export declare function recordSubagentLink(input: { ref: AgentFlowDispatchRef; childId: string; label: string; }): void; /** * The LIVE half of the call-window join (step 4): ONE root-context * `session/event` observer, registered by the entry at apply — before * execution can start — that reads only an ELIGIBLE slot of the EXACT * carrying Session. The root (untagged) registration is admitted globally by * the session store's scope carrier, so a catalog appended by any session * reaches it; slots for every other session are simply absent from the * WeakMap, and an inert (pending) or tombstoned slot is skipped. Its window * bound is the live `session.seq`, not the catch-up endpoint, so a catalog * appended after eligibility is still joined. * * No cold scan and no `session/created` backfill: a catalog written before * apply has no surviving dispatch ref, so it can never label a future * same-label dispatch. Contained — a throwing observation never escapes the * session append. * @param ctx - registrant context (fiber disposal unwinds the observer). * @param pairing - the apply-scoped pairing store (the slots). */ export declare function registerSubagentCatalogListener(ctx: Context, pairing: AgentFlowPairing): void;