akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
878 lines (877 loc) • 42.9 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* Native executor — executes one frozen step subgraph (`WorkflowPlanStep.root`)
* locally: fan-out through the scheduler, structured output through
* `runStructured`, per-unit journaling through the serialized writer queue,
* and `workflow_unit_*` events. It never writes step rows; advancing the spine
* is `run-workflow.ts`'s job.
*
* - Data flow: instructions are the step's prose byte-exact; data reaches a
* unit as attached context (docs/architecture/decisions/0001-…).
* - An empty successful free-text output is journaled as absent, so a live
* run and a resume promote the same artifact.
* - A declared step `output` schema is validated before the step completes;
* a mismatch fails the step with `artifactSchemaFailure`, which the gate
* loop may retry.
* - Unit ids are content-derived (`<node>:<sha(item)>` / `<node>:solo`), so a
* resume reuses every completed row whatever the item order; retries and
* gate loops journal under `~r<n>` / `~l<n>`.
* - `onError: "fail"` fails on any unit failure, `"continue"` lets the gate
* decide; `retry: { max, on }` re-dispatches listed failure reasons.
* - `isolation: worktree` runs each attempt of an agent/sdk unit in a fresh
* detached worktree (removed when clean, kept when dirty); llm units reject it.
* - Run `budget` ceilings are consumed per actual dispatch and abort the step
* when crossed, regardless of `on_error`.
*/
import { appendEvent } from "../../core/events.js";
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
import { runStructured } from "../../core/structured.js";
import { warn } from "../../core/warn.js";
import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
import { withWorkflowRunsConnection, withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
import { WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
// The ONE child-workflow drive — publishes and drives a
// `child-workflow`-targeted unit; this module's dispatch seam is its only
// production caller.
import { driveChildWorkflowUnit } from "./child-workflow.js";
// The ONE dispatch redaction contract, shared with the gate-judge path
// (exec/frozen-judge.ts). Consumers import the leaf directly — this module is
// not a second front door onto the seam.
import { collectWorkflowDispatchSensitiveValues, redactUnitOutcome } from "./dispatch-redaction.js";
import { materializeFrozenWorkflowEnvironment } from "./environment.js";
// The exec (shell) unit runner — a leaf that owns argv spawning, containment,
// and the process-outcome → failure-reason mapping.
import { runExecUnit } from "./exec-unit.js";
import { mergeLoweringNotices } from "./lowering-notices.js";
import { secretShapedParamValues } from "./param-secrets.js";
import { scheduleUnits } from "./scheduler.js";
// Shared step semantics — the ONE implementation consumed by the engine
// (this module + run-workflow.ts) on both the fresh-execution and the resume
// path. This module dispatches; step-work.ts owns the pure decisions.
import { clip, computeStepWorkList, reduceEmptyStep, reduceStepOutcomes, stepOutputsFromEvidence, unitOutcomeFromRow, } from "./step-work.js";
import { dispatchWorkflowExecution, } from "./unit-dispatch.js";
import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } from "../../tasks/frozen-script.js";
import { enqueueUnitWrite } from "./unit-writer.js";
import { assertGitWorkTree, cleanupUnitWorktree, createUnitWorktree } from "./worktree.js";
/**
* Per-step dispatch budget against the run's `budget` ceilings, seeded from
* the journal and consumed per actual dispatch (never by a reused row).
* Crossing a ceiling fires `onExceeded` once, aborting pending dispatches.
*/
class DispatchBudget {
used;
/** Run-total tokens: journal-seeded input + this step's dispatch usage. */
tokens;
/** Set (once) when a declared budget ceiling was hit; the step fails hard with it. */
budgetMessage;
maxUnits;
maxTokens;
onExceeded;
constructor(alreadyDispatched, opts) {
this.used = alreadyDispatched;
this.tokens = opts?.tokensUsed ?? 0;
this.maxUnits = opts?.budget?.maxUnits;
this.maxTokens = opts?.budget?.maxTokens;
this.onExceeded = opts?.onExceeded;
}
/** Consume one dispatch slot; false (and a sticky message) when a ceiling or the cap is hit. */
tryConsume() {
if (this.budgetMessage !== undefined)
return false;
if (this.maxUnits !== undefined && this.used >= this.maxUnits) {
this.exceed(`budget exceeded (max_units ceiling): ${this.used} unit(s) already dispatched for this run ` +
`against the workflow's declared budget.max_units of ${this.maxUnits} — refusing further dispatch.`);
return false;
}
if (this.maxTokens !== undefined && this.tokens >= this.maxTokens) {
this.exceed(`budget exceeded (max_tokens ceiling): ${this.tokens} token(s) already spent for this run ` +
`against the workflow's declared budget.max_tokens of ${this.maxTokens} — refusing further dispatch.`);
return false;
}
this.used++;
return true;
}
/** Record one dispatch's reported usage; crossing `maxTokens` trips the ceiling. */
addTokens(tokens) {
this.tokens += tokens;
if (this.budgetMessage === undefined && this.maxTokens !== undefined && this.tokens >= this.maxTokens) {
this.exceed(`budget exceeded (max_tokens ceiling): ${this.tokens} token(s) spent for this run, ` +
`reaching the workflow's declared budget.max_tokens of ${this.maxTokens} — aborting pending dispatches.`);
}
}
exceed(message) {
this.budgetMessage = message;
this.onExceeded?.();
}
}
function classifyUnitReuse(workUnit, completedRows) {
// Scan EVERY journaled attempt row of this unit (`<base>` / `<base>~r<N>`
// for ANY N), not just the attempts the CURRENT retry policy allows: a run
// re-invoked with a lowered retry.max must still find the `~rN` row a prior
// invocation completed beyond the new max, never re-dispatch finished work.
// The row's `input_hash` is informational — resume skips completed units.
const prior = completedRows?.get(workUnit.journalBaseId)?.[0];
return prior ? { kind: "reuse", row: prior } : { kind: "dispatch" };
}
function indexCompletedRows(rows) {
const index = new Map();
for (const row of rows) {
if (row.status !== "completed")
continue;
const base = row.unit_id.replace(/~r\d+$/, "");
const forBase = index.get(base);
if (forBase)
forBase.push(row);
else
index.set(base, [row]);
}
return index;
}
/**
* Execute one step plan natively. Never throws for unit-level failures. The
* whole step shares one state.db connection scope ({@link withWorkflowRunsConnection}).
*/
export function executeStepPlan(plan, ctx) {
return withWorkflowRunsConnection(() => executeStepPlanInConnection(plan, ctx));
}
/**
* Open the step's dispatch budget and the abort signal it trips: with a
* declared budget, dispatch runs under an AbortController chained onto
* `ctx.signal`. The caller must call `unchainSignal` when dispatch finishes.
*/
function openDispatchBudget(ctx, dispatched) {
const declaredBudget = ctx.budget && (ctx.budget.maxUnits !== undefined || ctx.budget.maxTokens !== undefined) ? ctx.budget : undefined;
let signal = ctx.signal;
let onExceeded;
let unchainSignal;
if (declaredBudget) {
const controller = new AbortController();
const upstream = ctx.signal;
if (upstream) {
if (upstream.aborted) {
controller.abort();
}
else {
const onUpstreamAbort = () => controller.abort();
upstream.addEventListener("abort", onUpstreamAbort, { once: true });
unchainSignal = () => upstream.removeEventListener("abort", onUpstreamAbort);
}
}
signal = controller.signal;
onExceeded = () => controller.abort();
}
const budget = new DispatchBudget(dispatched, {
tokensUsed: ctx.tokensUsed ?? 0,
...(declaredBudget ? { budget: declaredBudget } : {}),
...(onExceeded ? { onExceeded } : {}),
});
return { signal, budget, unchainSignal };
}
/** Resolve the live-at-dispatch prerequisites once, after durable-row reuse is known. */
async function prepareStepDispatchPrerequisites(input) {
const { plan, template, workUnits, ctx, willDispatch, dispatched } = input;
let env;
let sensitiveValues;
const frozenEnvironment = workUnits[0]?.environment;
if (willDispatch && frozenEnvironment && frozenEnvironment.length > 0) {
try {
const materialized = materializeFrozenWorkflowEnvironment(frozenEnvironment);
env = materialized.values;
sensitiveValues = materialized.sensitiveValues;
for (const audit of materialized.audits) {
appendEvent({
eventType: audit.eventType,
ref: audit.ref,
metadata: { keys: audit.keys, secretNames: audit.secretNames },
});
}
}
catch (err) {
return {
ok: false,
result: failedStep(dispatched, `Step "${plan.stepId}" frozen environment preflight failed: ${message(err)}`),
};
}
}
let worktreeBase;
if (willDispatch && template.isolation === "worktree") {
if (template.frozenTarget.kind === "command" && template.frozenTarget.runner.kind === "llm") {
return {
ok: false,
result: failedStep(dispatched, `Step "${plan.stepId}" declares isolation: worktree on an llm unit — the llm runner has no ` +
`working directory to isolate. Use the agent or sdk runner for worktree-isolated units.`),
};
}
const target = workUnits[0]?.frozenTarget;
const frozenCwd = target && "cwdIdentity" in target ? target.cwdIdentity : undefined;
if (frozenCwd)
assertFrozenDirectoryContained(frozenCwd);
const base = frozenCwd?.realRoot ?? ctx.workDir ?? process.cwd();
const preflightWorktree = ctx.preflightWorktree ?? assertGitWorkTree;
const gitError = preflightWorktree(base);
if (gitError !== undefined) {
return {
ok: false,
result: failedStep(dispatched, `Step "${plan.stepId}" cannot use isolation: worktree: ${gitError}`),
};
}
worktreeBase = base;
}
return {
ok: true,
...(env ? { env } : {}),
...(sensitiveValues ? { sensitiveValues } : {}),
...(worktreeBase !== undefined ? { worktreeBase } : {}),
};
}
async function executeStepPlanInConnection(plan, ctx) {
const dispatched = ctx.unitsDispatched ?? 0;
// The work list is the shared pure decision (step-work.ts), so a resume
// recomputes the identical list and matches journaled rows.
const workList = computeStepWorkList(plan, {
runId: ctx.runId,
params: ctx.params,
stepOutputs: stepOutputsFromEvidence(ctx.evidence),
...(ctx.gateLoop !== undefined ? { gateLoop: ctx.gateLoop } : {}),
...(ctx.gateFeedback ? { gateFeedback: ctx.gateFeedback } : {}),
});
if (!workList.ok) {
return failedStep(dispatched, workList.error);
}
const { template, reducer, isFanOut, items, units: workUnits } = workList.list;
if (items.length === 0) {
// Empty fan-out: the promoted artifact is the degenerate empty value, honored
// against the step's declared output schema. `reduceEmptyStep` (step-work.ts)
// owns that decision so a zero-unit step promotes the identical artifact +
// schema verdict every time the spine reaches it.
return { ...reduceEmptyStep(plan, reducer), unitsDispatched: dispatched };
}
const dispatcher = ctx.dispatcher ?? defaultUnitDispatcher;
// Load journaled rows first: a completed unit is reused, never re-dispatched.
const completedRows = indexCompletedRows(await withWorkflowRunsRepo((repo) => repo.getUnitsForStep(ctx.runId, plan.stepId)));
// Env resolution and worktree preflight run only when some unit will
// dispatch, so a fully-journaled step resumes even if an env asset or git is
// gone. Every unit is classified once; the preflight and dispatch share it.
const reuseDecisions = workUnits.map((unit) => classifyUnitReuse(unit, completedRows));
const willDispatch = reuseDecisions.some((decision) => decision.kind === "dispatch");
const prerequisites = await prepareStepDispatchPrerequisites({
plan,
template,
workUnits,
ctx,
willDispatch,
dispatched,
});
if (!prerequisites.ok)
return prerequisites.result;
const { env, sensitiveValues, worktreeBase } = prerequisites;
// Budget ceilings + lifetime-cap accounting, and the budget-chained abort
// signal they trip. Extracted verbatim (behavior-identical) — see
// {@link openDispatchBudget}.
const { signal, budget, unchainSignal } = openDispatchBudget(ctx, dispatched);
let outcomes;
// Worktree removals started by finished units and awaited below, before this
// step reports anything. Cleanup is best-effort, but "the step resolved" must
// still mean "its clean worktrees are gone" — only the WAIT moves off the
// unit's scheduler slot, not the guarantee.
const pendingWorktreeCleanups = [];
const frozenTargetConcurrency = template.frozenTarget.kind === "command" ? template.frozenTarget.concurrency : undefined;
try {
outcomes = await scheduleUnits(workUnits, (workUnit, index) => runUnit({
plan,
workUnit,
env,
sensitiveValues,
...(worktreeBase !== undefined ? { worktreeBase } : {}),
ctx,
signal,
dispatcher,
reuse: reuseDecisions[index],
pendingWorktreeCleanups,
budget,
}), {
concurrency: workList.list.concurrency,
signal,
maxConcurrency: ctx.maxConcurrency,
...(frozenTargetConcurrency !== undefined ? { llmConcurrency: frozenTargetConcurrency } : {}),
});
}
finally {
unchainSignal?.();
// The barrier. `allSettled` because each task already swallowed its own
// failure into a warn — nothing here can fail the step.
await Promise.allSettled(pendingWorktreeCleanups);
}
// Capture live-only diagnostics BEFORE any hard reduction replaces the unit
// list with a failed-step envelope. Budget/cap and journal-write failures
// must not erase notices already observed from real dispatches; durable row
// reuses naturally contribute none.
const notices = mergeLoweringNotices(...outcomes.map((outcome) => outcome?.notices));
// A declared budget ceiling is a hard backstop: a step that hit one FAILS
// regardless of on_error policy (a capped run must never quietly pass its
// gate). The budget message names WHICH ceiling tripped.
if (budget.budgetMessage) {
return { ...failedStep(budget.used, budget.budgetMessage, notices), tokensUsed: budget.tokens };
}
const units = outcomes.map((outcome, index) => outcome ?? {
unitId: workUnits[index].unitId,
ok: false,
failureReason: "aborted",
error: "unit was not dispatched (aborted or scheduler failure)",
});
// A journal-write failure fails the step regardless of on_error: the unit ran
// but its result could not be persisted, so no artifact can be promoted.
const unjournaled = units.filter((u) => u.failureReason === "journal_write_failed");
if (unjournaled.length > 0) {
return {
...failedStep(budget.used, unjournaled.map((u) => u.error ?? `unit "${u.unitId}" result could not be journaled`).join(" "), notices),
tokensUsed: budget.tokens,
};
}
// Failure policy, reducer, and artifact schema check: the shared `reduceStepOutcomes`.
const reduced = reduceStepOutcomes(plan, reducer, isFanOut, template.onError, units);
return {
...reduced,
...(notices ? { notices } : {}),
unitsDispatched: budget.used,
tokensUsed: budget.tokens,
};
}
async function runUnit(input) {
const { plan, workUnit, env, sensitiveValues, ctx, dispatcher } = input;
const unitId = workUnit.unitId;
// Target validity is a WHOLE-LIST invariant, never a per-unit condition:
// computeStepWorkList rejects a step before building any unit when its sole
// frozen target is invalid, so every unit below carries one executable
// command/agent/SDK/direct-LLM target.
// The prompt (and therefore the input hash) was built once with the BASE
// unit id by computeStepWorkList: a retry re-dispatches the SAME input, the
// `~r<n>` suffix is journal bookkeeping only.
const { prompt, inputHash } = workUnit;
const request = {
runId: ctx.runId,
stepId: plan.stepId,
unitId,
nodeId: workUnit.nodeId,
prompt,
frozenTarget: workUnit.frozenTarget,
...(workUnit.execContext ? { execContext: workUnit.execContext } : {}),
// A NON-isolated exec unit spawns in the engine invocation's working
// directory. `dispatchJournaledAttempt` overwrites this with the unit's
// fresh worktree when `isolation: worktree` is in play. Only exec units get
// it: handing an agent unit a cwd it never had would change harness
// behavior, and the agent path already takes its cwd from its profile.
...(workUnit.frozenTarget.kind !== "command" && ctx.workDir !== undefined ? { cwd: ctx.workDir } : {}),
timeoutMs: workUnit.timeoutMs,
...(workUnit.schema ? { schema: workUnit.schema } : {}),
...(env ? { env } : {}),
...(sensitiveValues ? { sensitiveValues } : {}),
...(input.signal ? { signal: input.signal } : {}),
// F-1: forwarded to exec-unit.ts's childEnv for a
// "script"/"shell" unit, and to dispatchWorkflowExecution's
// runExecution eventSource option (unit-dispatch.ts)
// for a "command" unit — both arms observe it.
...(ctx.eventSource !== undefined ? { eventSource: ctx.eventSource } : {}),
};
// One content-derived unit id is retained across every retry; the append-only
// attempt table supplies the 1-based attempt identity.
const retry = workUnit.retry;
const maxAttempts = 1 + Math.max(0, retry?.max ?? 0);
const journalBaseId = workUnit.journalBaseId;
const attemptIdFor = (_attempt) => journalBaseId;
// Durable-row reuse — literally the decision executeStepPlan's preflight gate
// counted, handed down rather than recomputed, so the gate cannot disagree
// with what happens here. A completed row IS the result: return it without
// touching rows, dispatching, or re-emitting events (a crash-resume must
// never double-issue work). Failed/running/missing rows dispatch live.
const reuse = input.reuse;
if (reuse.kind === "reuse") {
// Identity in the durable step evidence is the CONTENT-derived base id, not
// the `~r<n>` attempt row it was reused from — the work list only ever knows
// base ids, so evidence.units[].unitId stays stable across retries+resumes.
return reuseCompletedUnit(unitId, reuse.row, workUnit.schema !== undefined);
}
let outcome;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (input.signal?.aborted) {
return (outcome ?? {
unitId,
ok: false,
failureReason: "aborted",
error: "unit was not dispatched because the workflow invocation was interrupted",
});
}
const attemptId = attemptIdFor(attempt);
// Declared budget ceilings, consumed per ACTUAL dispatch (reuses above
// returned before reaching here). Refusal fails this unit without
// journaling a row — nothing was dispatched — and the sticky
// budgetMessage fails the step.
if (!input.budget.tryConsume()) {
return (outcome ?? {
unitId,
ok: false,
failureReason: "budget_exceeded",
error: input.budget.budgetMessage ?? "budget exceeded",
});
}
outcome = await dispatchJournaledAttempt({
plan,
workUnit,
ctx,
dispatcher,
request: { ...request, unitId: attemptId },
attemptId,
inputHash,
...(input.worktreeBase !== undefined ? { worktreeBase: input.worktreeBase } : {}),
pendingWorktreeCleanups: input.pendingWorktreeCleanups,
});
// Attempts use `~r<n>` journal suffixes while durable step evidence remains
// attached to the content-derived base identity.
outcome.unitId = unitId;
// Budget token accounting: every actual dispatch's reported
// usage counts against the run's max_tokens ceiling; crossing it aborts
// pending dispatches via the chained controller. Reuses never reach here
// (their tokens are already in the journal-seeded total).
if (outcome.tokens !== undefined)
input.budget.addTokens(outcome.tokens);
if (outcome.ok)
return outcome;
if (input.signal?.aborted)
return outcome;
const reason = outcome.failureReason;
if (!retry || reason === undefined || !retry.on.includes(reason))
return outcome;
}
// maxAttempts >= 1, so outcome is always set by the loop above.
return outcome;
}
/**
* What an attempt journals as the unit row's `result_json` (what `status
* --units` shows): a success's promoted value, or a failure's diagnostic plus
* any partial output. The caller passes an already-redacted outcome; clipped
* to {@link WORKFLOW_UNIT_DIAGNOSTIC_CLIP}.
*/
function journaledUnitResultJson(outcome) {
if (outcome.result !== undefined)
return JSON.stringify(outcome.result);
if (outcome.ok)
return outcome.text ? JSON.stringify(outcome.text) : null;
const parts = [outcome.error, outcome.text].filter((part) => Boolean(part?.trim()));
if (parts.length === 0)
return null;
return JSON.stringify(clip(parts.join("\n--- unit output ---\n"), WORKFLOW_UNIT_DIAGNOSTIC_CLIP));
}
async function prepareAttemptWorktree(input) {
if (input.worktreeBase === undefined)
return { ok: true, request: input.request };
const created = await createUnitWorktree(input.worktreeBase, input.ctx.runId, input.attemptId,
// A child-workflow target has no commit of its own (its prepared worktree goes unused).
input.workUnit.frozenTarget.kind === "child-workflow" ? undefined : input.workUnit.frozenTarget.gitCommitOid);
if (created.preservedLeftover !== undefined) {
warn(`Workflow unit ${input.attemptId}: a previous attempt left uncollected work in its isolation worktree; ` +
`preserved at ${created.preservedLeftover}`);
}
if (!created.ok) {
return {
ok: false,
outcome: {
unitId: input.request.unitId,
ok: false,
failureReason: "worktree_failed",
error: created.error,
},
};
}
return { ok: true, request: { ...input.request, cwd: created.path }, worktreePath: created.path };
}
async function reserveJournaledDispatch(input, worktreePath, startedAt) {
const { plan, workUnit, ctx, attemptId, inputHash } = input;
let durableAttempt;
await enqueueUnitWrite(async () => {
await withWorkflowRunsRepo((repo) => {
const target = workUnit.frozenTarget;
durableAttempt = repo.reserveUnitAttempt({
runId: ctx.runId,
unitId: attemptId,
stepId: plan.stepId,
nodeId: workUnit.nodeId,
parentUnitId: workUnit.isFanOut ? `${plan.stepId}.map` : null,
phase: "unit",
runner: workUnit.runner,
engine: target.kind === "command" ? target.request.engine.name : null,
model: target.kind === "command" ? (target.request.model?.resolved ?? null) : null,
inputHash,
worktreePath: worktreePath ?? null,
now: startedAt,
}).attempt;
});
});
if (!durableAttempt)
throw new Error(`unit "${attemptId}" did not reserve a durable attempt`);
return durableAttempt;
}
async function finishJournaledDispatch(input) {
const { attempt: source, durableAttempt, finishedAt, outcome } = input;
const { ctx, attemptId } = source;
await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => {
const finished = repo.finishUnitAttempt({
runId: ctx.runId,
unitId: attemptId,
attempt: durableAttempt.attempt,
dispatchId: durableAttempt.dispatch_id,
status: outcome.ok ? "completed" : "failed",
resultJson: journaledUnitResultJson(outcome),
tokens: outcome.tokens ?? null,
failureReason: outcome.failureReason ?? null,
sessionId: outcome.sessionId ?? null,
finishedAt,
});
if (!finished) {
if (repo.getUnitAttempts(ctx.runId, attemptId).length === 0) {
throw new Error(`finishUnitAttempt updated no row: no durable attempt "${attemptId}" exists for run "${ctx.runId}".`);
}
warn(`Workflow unit ${attemptId} (run ${ctx.runId}) ${outcome.ok ? "completed" : `failed (${outcome.failureReason ?? "error"})`}, ` +
"but its durable attempt was already finished — refusing a duplicate terminal write. " +
"This dispatch's result is not journaled.");
}
}));
}
function queueAttemptWorktreeCleanup(input, worktreePath) {
if (worktreePath === undefined || input.worktreeBase === undefined)
return;
const worktreeBase = input.worktreeBase;
input.pendingWorktreeCleanups.push((async () => {
try {
const cleanup = await cleanupUnitWorktree(worktreeBase, worktreePath);
if (cleanup.dirty) {
warn(`Workflow unit ${input.attemptId} left uncommitted changes in its isolation worktree; retained at ${worktreePath}`);
}
else if (!cleanup.removed) {
warn(`Workflow unit ${input.attemptId}: could not clean up isolation worktree ${worktreePath}: ${cleanup.error}`);
}
}
catch (err) {
warn(`Workflow unit ${input.attemptId}: could not clean up isolation worktree ${worktreePath}: ${message(err)}`);
}
})());
}
/** Journal one dispatch attempt: insert row, events, dispatch, finish row. */
async function dispatchJournaledAttempt(input) {
const { workUnit, ctx, dispatcher, attemptId } = input;
const prepared = await prepareAttemptWorktree(input);
if (!prepared.ok)
return prepared.outcome;
let { request } = prepared;
const { worktreePath } = prepared;
const startedAt = new Date().toISOString();
let durableAttempt;
try {
durableAttempt = await reserveJournaledDispatch(input, worktreePath, startedAt);
}
catch (err) {
// A failed dispatch-row insert means NOTHING dispatched (the row is the
// dispatch's precondition) — fail the unit with the real cause instead of
// letting the throw escape into the scheduler, where a swallowed worker
// error is indistinguishable from "never claimed" and used to be
// misreported as an aborted, never-dispatched unit.
if (worktreePath !== undefined && input.worktreeBase !== undefined)
await cleanupUnitWorktree(input.worktreeBase, worktreePath);
return {
unitId: request.unitId,
ok: false,
failureReason: "dispatch_error",
error: `unit "${attemptId}" could not journal its dispatch row (nothing was dispatched): ${message(err)}`,
};
}
request = {
...request,
attempt: durableAttempt.attempt,
dispatchId: durableAttempt.dispatch_id,
};
// A child-workflow unit goes to the child executor instead of the
// dispatcher, after its attempt row is reserved, so it journals like any unit.
const dispatched = request.frozenTarget.kind === "child-workflow"
? await driveChildWorkflowUnit({
request,
target: request.frozenTarget,
ctx,
childParams: workUnit.childParams ?? {},
inputHash: input.inputHash,
dispatcher,
})
: await dispatchUnit(request, dispatcher);
// Credential and passthrough values are intentionally sampled only AFTER
// the default dispatcher has authorized/lowered the frozen request and
// materialized credentials at its terminal dispatch boundary. Custom test
// dispatchers receive the same post-dispatch journal scrub. Secret-shaped
// run params join the set: they must reach the prompt in clear (the unit
// needs them), but nothing about them needs to reach the journal.
const sensitiveValues = collectWorkflowDispatchSensitiveValues({
...(request.frozenTarget.kind === "command" ? { runner: request.frozenTarget.runner } : {}),
sensitiveValues: [...(request.sensitiveValues ?? []), ...secretShapedParamValues(ctx.params)],
}, request.env);
const outcome = redactUnitOutcome(dispatched, sensitiveValues);
const finishedAt = new Date().toISOString();
// A dispatched unit's outcome is NEVER silently discarded: the attempt is
// finished with the real result even when the run went non-active
// mid-flight — dropping it would leave the row `running` and make a later
// resume re-dispatch side-effecting work that already ran and already spent
// tokens. Persisting a unit result never advances the run; that is
// completeWorkflowStep's job.
let journalError;
try {
await finishJournaledDispatch({
attempt: input,
durableAttempt,
finishedAt,
outcome,
});
}
catch (err) {
journalError = err;
}
// A clean worktree is removed, a dirty one kept and logged. Cleanup starts
// here and is awaited at the step barrier, never holding this scheduler slot.
queueAttemptWorktreeCleanup(input, worktreePath);
// A journal-write failure AFTER a successful dispatch is its own loud
// failure class: the unit's work ran (and may have succeeded), but its
// terminal state could not be recorded, so the row may be stuck `running`.
// It must never masquerade as "not dispatched" — the outcome names the unit
// and the real cause, and executeStepPlan fails the step hard on it
// (out-of-taxonomy reason, so retry.on can never re-dispatch the work).
if (journalError !== undefined) {
return {
unitId: request.unitId,
ok: false,
failureReason: "journal_write_failed",
error: `unit "${attemptId}" dispatched and ${outcome.ok ? "completed" : `failed (${outcome.failureReason ?? "error"})`}, ` +
`but its result could not be journaled: ${message(journalError)}`,
...(outcome.tokens !== undefined ? { tokens: outcome.tokens } : {}),
...(outcome.sessionId !== undefined ? { sessionId: outcome.sessionId } : {}),
...(outcome.notices ? { notices: outcome.notices } : {}),
};
}
return outcome;
}
/**
* Strict JSON parse for an `exec` unit's declared-schema output: stdout must be
* EXACTLY one JSON value (leading/trailing whitespace tolerated, nothing else).
*
* Deliberately NOT `parseEmbeddedJsonResponse` — that scan strips code fences
* and hunts for a JSON island inside prose, which is the right forgiving
* behavior for an LLM and the wrong one for a command, where it would silently
* promote a JSON fragment found in unrelated log noise as the typed artifact.
* Returning `undefined` makes `runStructured` report `parse_error` (a real
* member of the retry taxonomy), so `retry.on: [parse_error]` still works.
*/
function parseExecJson(raw) {
const trimmed = raw.trim();
if (!trimmed)
return undefined;
try {
return JSON.parse(trimmed);
}
catch {
return undefined;
}
}
/** Transport failures surface as this sentinel so runStructured doesn't retry them. */
class UnitTransportError extends Error {
result;
constructor(result) {
super(result.error ?? "unit dispatch failed");
this.result = result;
this.name = "UnitTransportError";
}
}
async function dispatchUnit(request, dispatcher) {
let tokens = 0;
let sawUsage = false;
let loweringNotices;
// Harness-native session id revealed by dispatch. Captured across
// structured-output retries (last one wins) so it survives into the
// UnitOutcome and gets journaled by finishUnitAttempt — the seam's
// contract ("stored opportunistically on the unit row for resume").
let sessionId;
const dispatchOnce = async (feedback) => {
const result = await dispatcher(request, feedback);
if (result.usage) {
sawUsage = true;
tokens +=
(result.usage.inputTokens ?? 0) + (result.usage.outputTokens ?? 0) + (result.usage.reasoningTokens ?? 0);
}
loweringNotices = mergeLoweringNotices(loweringNotices, result.notices);
// Capture before the ok-check: a failed attempt can still have configured
// a session (e.g. codex `session_configured` then a tool crash).
if (result.sessionId !== undefined)
sessionId = result.sessionId;
if (!result.ok)
throw new UnitTransportError(result);
return result.text;
};
const captured = () => ({
...(sawUsage ? { tokens } : {}),
...(sessionId !== undefined ? { sessionId } : {}),
...(loweringNotices ? { notices: loweringNotices } : {}),
});
try {
if (request.schema) {
const schema = request.schema;
const structured = await runStructured({
dispatch: dispatchOnce,
// A command is not re-promptable. Its stdout must be EXACTLY one JSON
// value: the default embedded-JSON scan (fences, think-blocks, "find the
// JSON inside the prose") is right for an LLM and wrong for a command,
// where it would pluck a JSON fragment out of unrelated log noise and
// promote it as the typed artifact. And it gets exactly ONE attempt —
// `runStructured`'s corrective retry re-dispatches with feedback, which
// for a command means running a SIDE-EFFECTING process a second time
// with byte-identical argv: it cannot produce different output, and it
// can produce a second deployment. Declared `retry:` still applies (the
// executor's own loop), because that is a policy the author opted into
// per failure reason.
...(request.frozenTarget.kind !== "command" ? { parse: parseExecJson, maxAttempts: 1 } : {}),
validate: (candidate) => {
const errors = validateJsonSchemaSubset(candidate, schema);
return errors.length === 0 ? { ok: true, value: candidate } : { ok: false, errors };
},
});
if (structured.ok) {
return { unitId: request.unitId, ok: true, result: structured.value, ...captured() };
}
return {
unitId: request.unitId,
ok: false,
// NOTE: `validation_error` is deliberately outside PROGRAM_RETRY_REASONS,
// so no `retry.on:` can name it and a schema-violating unit is not
// re-run — see "fails with `validation_error` and is NOT re-run" in
// tests/integration/workflows/exec-unit.test.ts. A sweep finding
// proposed mapping it onto `llm_invalid_json` (which the parser accepts
// but this path never emits) to make such failures retryable; that is a
// behaviour change against an intentional design, not a bug fix, so it
// is left alone. Reconciling the vocabulary is a 0.9.2 decision.
failureReason: structured.reason,
error: structured.errors.join("; "),
text: structured.raw,
...captured(),
};
}
const text = await dispatchOnce();
// Normalize an EMPTY successful output to "no text". `finishUnitAttempt` journals
// result_json = NULL for a falsy text, so durable-reuse rehydrates NO text
// from the row (unitOutcomeFromRow). Preserving `text: ""` only in this live
// outcome would make the LIVE step artifact ("") diverge from the artifact a
// resume rebuilds from the row (null) — the exact byte-identical-graph
// violation the cardinal rule forbids. Treating empty as absent keeps live
// dispatch and engine resume identical.
return { unitId: request.unitId, ok: true, ...(text ? { text } : {}), ...captured() };
}
catch (err) {
if (err instanceof UnitTransportError) {
return {
unitId: request.unitId,
ok: false,
failureReason: err.result.failureReason ?? "dispatch_error",
error: err.result.error ?? "unit dispatch failed",
text: err.result.text,
...captured(),
};
}
return {
unitId: request.unitId,
ok: false,
failureReason: "dispatch_error",
error: message(err),
...captured(),
};
}
}
// ── Default dispatcher (production substrate) ───────────────────────────────
/**
* Dispatch a frozen engine through the common prepare → lower → dispatch seam.
* No live profile/default/model map is consulted, and credentials remain
* symbolic until the final dispatch boundary.
*/
export const defaultUnitDispatcher = async (request, feedback) => {
const frozenTarget = request.frozenTarget;
if (frozenTarget.kind === "script") {
assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
const materialized = materializeFrozenScript({
sourceRef: frozenTarget.ref,
interpreter: frozenTarget.interpreter,
extension: frozenTarget.extension,
bytesBase64: frozenTarget.bytesBase64,
byteLength: frozenTarget.byteLength,
sha256: frozenTarget.contentHash,
});
try {
const command = frozenScriptCommand({
sourceRef: frozenTarget.ref,
interpreter: frozenTarget.interpreter,
extension: frozenTarget.extension,
bytesBase64: frozenTarget.bytesBase64,
byteLength: frozenTarget.byteLength,
sha256: frozenTarget.contentHash,
}, materialized.file);
return await runExecUnit({
unitId: request.unitId,
exec: {
...frozenTarget.exec,
command: command,
},
baseDir: request.cwd ?? frozenTarget.cwdIdentity.realCwd,
...(request.env ? { env: request.env } : {}),
...(request.execContext ? { context: request.execContext } : {}),
...(request.schema ? { hasOutputSchema: true } : {}),
timeoutMs: request.timeoutMs,
...(request.signal ? { signal: request.signal } : {}),
...(request.eventSource !== undefined ? { eventSource: request.eventSource } : {}),
});
}
finally {
cleanupFrozenScript(materialized);
}
}
if (frozenTarget.kind === "shell") {
if (frozenTarget.cwdIdentity)
assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
return runExecUnit({
unitId: request.unitId,
exec: frozenTarget.exec,
baseDir: request.cwd ?? frozenTarget.cwdIdentity?.realCwd ?? process.cwd(),
...(request.env ? { env: request.env } : {}),
...(request.execContext ? { context: request.execContext } : {}),
...(request.schema ? { hasOutputSchema: true } : {}),
timeoutMs: request.timeoutMs,
...(request.signal ? { signal: request.signal } : {}),
...(request.eventSource !== undefined ? { eventSource: request.eventSource } : {}),
});
}
return dispatchWorkflowExecution(request, feedback);
};
// ── Small helpers ────────────────────────────────────────────────────────────
/**
* Rehydrate a journaled completed unit row into a UnitOutcome (durable-row
* reuse). Delegates to the shared {@link unitOutcomeFromRow} — the reuse path
* only reaches here for completed rows (the caller guards `status ===
* "completed"`), so a reused unit contributes exactly what its original
* dispatch did.
*/
function reuseCompletedUnit(unitId, row, hasSchema) {
return unitOutcomeFromRow(unitId, row, hasSchema);
}
function failedStep(dispatched, reason, notices) {
return {
ok: false,
units: [],
...(notices ? { notices } : {}),
evidence: { error: reason },
summary: reason,
unitsDispatched: dispatched,
};
}
function message(err) {
return err instanceof Error ? err.message : String(err);
}