akm-cli 0.9.1 → 0.9.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*
|
|
37
37
|
* Empty free-text outputs (peer review): a SUCCESSFUL schemaless unit that
|
|
38
38
|
* returns the empty string is normalized to "no output" — {@link dispatchUnit}
|
|
39
|
-
* drops the falsy `text`, `
|
|
39
|
+
* drops the falsy `text`, `finishUnitAttempt` journals `result_json = NULL`, and
|
|
40
40
|
* durable-row reuse rehydrates the same absence (`unitOutcomeFromRow`). This is
|
|
41
41
|
* the ONLY empty-output resolution: `''` never survives into the journal, so the
|
|
42
42
|
* live artifact cannot diverge from the artifact a resume rebuilds from the same
|
|
@@ -123,18 +123,21 @@
|
|
|
123
123
|
*
|
|
124
124
|
* Layering (see the plan's *Reconciliation* section):
|
|
125
125
|
* - Dispatch goes through ONE injected {@link UnitDispatcher} seam. The
|
|
126
|
-
* default dispatcher
|
|
127
|
-
*
|
|
128
|
-
*
|
|
126
|
+
* default dispatcher adapts the frozen snapshot into the common resolved
|
|
127
|
+
* request, lowers it through the registered harness/direct-LLM adapter,
|
|
128
|
+
* and reaches transport only through the central lowered-dispatch seam.
|
|
129
129
|
* - This module NEVER writes step rows: advancing the gated spine is the
|
|
130
130
|
* engine loop's job (`run-workflow.ts`) via `completeWorkflowStep`.
|
|
131
131
|
*/
|
|
132
|
+
import { randomUUID } from "node:crypto";
|
|
132
133
|
import { appendEvent } from "../../core/events.js";
|
|
133
134
|
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
134
135
|
import { runStructured } from "../../core/structured.js";
|
|
135
136
|
import { warn } from "../../core/warn.js";
|
|
136
|
-
import {
|
|
137
|
+
import { assertFrozenDirectoryIdentity } from "../../execution/directory-identity.js";
|
|
138
|
+
import { assertFrozenExecutableIdentity } from "../../execution/executable-identity.js";
|
|
137
139
|
import { withWorkflowRunsConnection, withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
140
|
+
import { materializeFrozenWorkflowEnvironment } from "../ir/environment-v4.js";
|
|
138
141
|
import { WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
|
|
139
142
|
// The ONE dispatch redaction contract, shared with the gate-judge path
|
|
140
143
|
// (exec/frozen-judge.ts). Consumers import the leaf directly — this module is
|
|
@@ -143,12 +146,14 @@ import { collectWorkflowDispatchSensitiveValues, redactUnitOutcome } from "./dis
|
|
|
143
146
|
// The exec (shell) unit runner — a leaf that owns argv spawning, containment,
|
|
144
147
|
// and the process-outcome → failure-reason mapping.
|
|
145
148
|
import { runExecUnit } from "./exec-unit.js";
|
|
149
|
+
import { mergeLoweringNotices } from "./lowering-notices.js";
|
|
146
150
|
import { LIFETIME_UNIT_CAP, scheduleUnits, UnitCapExceededError } from "./scheduler.js";
|
|
147
151
|
// Shared step semantics — the ONE implementation consumed by the engine
|
|
148
152
|
// (this module + run-workflow.ts) on both the fresh-execution and the resume
|
|
149
153
|
// path. This module dispatches; step-work.ts owns the pure decisions.
|
|
150
154
|
import { clip, computeStepWorkList, reduceEmptyStep, reduceStepOutcomes, stepOutputsFromEvidence, unitOutcomeFromRow, } from "./step-work.js";
|
|
151
|
-
import {
|
|
155
|
+
import { dispatchWorkflowExecution, } from "./unit-dispatch.js";
|
|
156
|
+
import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } from "../../tasks/frozen-script.js";
|
|
152
157
|
import { enqueueUnitWrite } from "./unit-writer.js";
|
|
153
158
|
import { assertGitWorkTree, cleanupUnitWorktree, createUnitWorktree } from "./worktree.js";
|
|
154
159
|
/**
|
|
@@ -315,6 +320,63 @@ function openDispatchBudget(ctx, dispatched) {
|
|
|
315
320
|
});
|
|
316
321
|
return { signal, budget, unchainSignal };
|
|
317
322
|
}
|
|
323
|
+
/** Resolve the live-at-dispatch prerequisites once, after durable-row reuse is known. */
|
|
324
|
+
async function prepareStepDispatchPrerequisites(input) {
|
|
325
|
+
const { plan, template, workUnits, ctx, willDispatch, dispatched } = input;
|
|
326
|
+
let env;
|
|
327
|
+
let sensitiveValues;
|
|
328
|
+
const frozenEnvironment = workUnits[0]?.environment;
|
|
329
|
+
if (willDispatch && frozenEnvironment && frozenEnvironment.length > 0) {
|
|
330
|
+
try {
|
|
331
|
+
const materialized = materializeFrozenWorkflowEnvironment(frozenEnvironment);
|
|
332
|
+
env = materialized.values;
|
|
333
|
+
sensitiveValues = materialized.sensitiveValues;
|
|
334
|
+
for (const audit of materialized.audits) {
|
|
335
|
+
appendEvent({
|
|
336
|
+
eventType: audit.eventType,
|
|
337
|
+
ref: audit.ref,
|
|
338
|
+
metadata: { keys: audit.keys, secretNames: audit.secretNames },
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
result: failedStep(dispatched, `Step "${plan.stepId}" frozen environment preflight failed: ${message(err)}`),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
let worktreeBase;
|
|
350
|
+
if (willDispatch && template.isolation === "worktree") {
|
|
351
|
+
if (template.frozenTarget.kind === "command" && template.frozenTarget.runner.kind === "llm") {
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
result: failedStep(dispatched, `Step "${plan.stepId}" declares isolation: worktree on an llm unit — the llm runner has no ` +
|
|
355
|
+
`working directory to isolate. Use the agent or sdk runner for worktree-isolated units.`),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const target = workUnits[0]?.frozenTarget;
|
|
359
|
+
const frozenCwd = target && "cwdIdentity" in target ? target.cwdIdentity : undefined;
|
|
360
|
+
if (frozenCwd)
|
|
361
|
+
assertFrozenDirectoryIdentity(frozenCwd);
|
|
362
|
+
const base = frozenCwd?.realRoot ?? ctx.workDir ?? process.cwd();
|
|
363
|
+
const preflightWorktree = ctx.preflightWorktree ?? assertGitWorkTree;
|
|
364
|
+
const gitError = preflightWorktree(base);
|
|
365
|
+
if (gitError !== undefined) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
result: failedStep(dispatched, `Step "${plan.stepId}" cannot use isolation: worktree: ${gitError}`),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
worktreeBase = base;
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
...(env ? { env } : {}),
|
|
376
|
+
...(sensitiveValues ? { sensitiveValues } : {}),
|
|
377
|
+
...(worktreeBase !== undefined ? { worktreeBase } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
318
380
|
async function executeStepPlanInConnection(plan, ctx) {
|
|
319
381
|
const dispatched = ctx.unitsDispatched ?? 0;
|
|
320
382
|
// Work-list computation is the SHARED, PURE decision (step-work.ts): resolve
|
|
@@ -328,7 +390,6 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
328
390
|
runId: ctx.runId,
|
|
329
391
|
params: ctx.params,
|
|
330
392
|
stepOutputs: stepOutputsFromEvidence(ctx.evidence),
|
|
331
|
-
engines: ctx.engines ?? {},
|
|
332
393
|
...(ctx.gateLoop !== undefined ? { gateLoop: ctx.gateLoop } : {}),
|
|
333
394
|
...(ctx.gateFeedback ? { gateFeedback: ctx.gateFeedback } : {}),
|
|
334
395
|
});
|
|
@@ -365,47 +426,17 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
365
426
|
// two call sites have to keep re-establishing.
|
|
366
427
|
const reuseDecisions = workUnits.map((unit) => classifyUnitReuse(unit, completedRows, gateLoop));
|
|
367
428
|
const willDispatch = reuseDecisions.some((decision) => decision.kind === "dispatch");
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// Worktree isolation preflight (addendum R2), once per step, before any
|
|
382
|
-
// dispatch — and ONLY when a unit will dispatch: llm units have no working
|
|
383
|
-
// directory to isolate (fail loudly), and a non-git base directory (or a
|
|
384
|
-
// missing git binary) fails the step cleanly instead of N units racing into
|
|
385
|
-
// identical git errors. The actual worktrees are minted per journaled
|
|
386
|
-
// attempt in dispatchJournaledAttempt.
|
|
387
|
-
let worktreeBase;
|
|
388
|
-
if (willDispatch && template.isolation === "worktree") {
|
|
389
|
-
const engine = template.invocation ? ctx.engines?.[template.invocation.engine] : undefined;
|
|
390
|
-
if (engine?.kind === "llm") {
|
|
391
|
-
return failedStep(dispatched, `Step "${plan.stepId}" declares isolation: worktree on an llm unit — the llm runner has no ` +
|
|
392
|
-
`working directory to isolate. Use the agent or sdk runner for worktree-isolated units.`);
|
|
393
|
-
}
|
|
394
|
-
const base = ctx.workDir ?? process.cwd();
|
|
395
|
-
const preflightWorktree = ctx.preflightWorktree ?? assertGitWorkTree;
|
|
396
|
-
const gitError = preflightWorktree(base);
|
|
397
|
-
if (gitError !== undefined) {
|
|
398
|
-
return failedStep(dispatched, `Step "${plan.stepId}" cannot use isolation: worktree: ${gitError}`);
|
|
399
|
-
}
|
|
400
|
-
worktreeBase = base;
|
|
401
|
-
}
|
|
402
|
-
// The values that must never survive into the journal. Collected once per
|
|
403
|
-
// step, not once per unit: the frozen engine and its SDK fallback are
|
|
404
|
-
// resolved from the step template and handed unchanged to every unit, and
|
|
405
|
-
// `env` is step-wide, so a fan-out was re-scanning process.env — and
|
|
406
|
-
// re-inspecting every passthrough value — once per unit, including for units
|
|
407
|
-
// that go on to reuse a journaled row and dispatch nothing at all.
|
|
408
|
-
const sensitiveValues = willDispatch && workUnits[0] ? collectWorkflowDispatchSensitiveValues(workUnits[0], env) : [];
|
|
429
|
+
const prerequisites = await prepareStepDispatchPrerequisites({
|
|
430
|
+
plan,
|
|
431
|
+
template,
|
|
432
|
+
workUnits,
|
|
433
|
+
ctx,
|
|
434
|
+
willDispatch,
|
|
435
|
+
dispatched,
|
|
436
|
+
});
|
|
437
|
+
if (!prerequisites.ok)
|
|
438
|
+
return prerequisites.result;
|
|
439
|
+
const { env, sensitiveValues, worktreeBase } = prerequisites;
|
|
409
440
|
// Budget ceilings + lifetime-cap accounting, and the budget-chained abort
|
|
410
441
|
// signal they trip. Extracted verbatim (behavior-identical) — see
|
|
411
442
|
// {@link openDispatchBudget}.
|
|
@@ -416,30 +447,25 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
416
447
|
// still mean "its clean worktrees are gone" — only the WAIT moves off the
|
|
417
448
|
// unit's scheduler slot, not the guarantee.
|
|
418
449
|
const pendingWorktreeCleanups = [];
|
|
419
|
-
const
|
|
420
|
-
const selectedLlmEngine = selectedEngine?.kind === "llm"
|
|
421
|
-
? selectedEngine
|
|
422
|
-
: selectedEngine?.kind === "agent" && selectedEngine.fallbackLlmEngine
|
|
423
|
-
? ctx.engines?.[selectedEngine.fallbackLlmEngine]
|
|
424
|
-
: undefined;
|
|
450
|
+
const frozenTargetConcurrency = template.frozenTarget.kind === "command" ? template.frozenTarget.concurrency : undefined;
|
|
425
451
|
try {
|
|
426
452
|
outcomes = await scheduleUnits(workUnits, (workUnit, index) => runUnit({
|
|
427
453
|
plan,
|
|
428
454
|
workUnit,
|
|
429
455
|
env,
|
|
456
|
+
sensitiveValues,
|
|
430
457
|
...(worktreeBase !== undefined ? { worktreeBase } : {}),
|
|
431
458
|
ctx,
|
|
432
459
|
signal,
|
|
433
460
|
dispatcher,
|
|
434
461
|
reuse: reuseDecisions[index],
|
|
435
|
-
sensitiveValues,
|
|
436
462
|
pendingWorktreeCleanups,
|
|
437
463
|
budget,
|
|
438
464
|
}), {
|
|
439
465
|
concurrency: workList.list.concurrency,
|
|
440
466
|
signal,
|
|
441
467
|
maxConcurrency: ctx.maxConcurrency,
|
|
442
|
-
...(
|
|
468
|
+
...(frozenTargetConcurrency !== undefined ? { llmConcurrency: frozenTargetConcurrency } : {}),
|
|
443
469
|
});
|
|
444
470
|
}
|
|
445
471
|
finally {
|
|
@@ -448,14 +474,19 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
448
474
|
// failure into a warn — nothing here can fail the step.
|
|
449
475
|
await Promise.allSettled(pendingWorktreeCleanups);
|
|
450
476
|
}
|
|
477
|
+
// Capture live-only diagnostics BEFORE any hard reduction replaces the unit
|
|
478
|
+
// list with a failed-step envelope. Budget/cap, replay divergence, and
|
|
479
|
+
// journal-write failures must not erase notices already observed from real
|
|
480
|
+
// dispatches; durable row reuses naturally contribute none.
|
|
481
|
+
const notices = mergeLoweringNotices(...outcomes.map((outcome) => outcome?.notices));
|
|
451
482
|
// Declared budget ceilings and the lifetime cap are hard backstops: a step
|
|
452
483
|
// that hit one FAILS regardless of on_error policy (a capped run must never
|
|
453
484
|
// quietly pass its gate). The budget message names WHICH ceiling tripped.
|
|
454
485
|
if (budget.budgetMessage) {
|
|
455
|
-
return { ...failedStep(budget.used, budget.budgetMessage), tokensUsed: budget.tokens };
|
|
486
|
+
return { ...failedStep(budget.used, budget.budgetMessage, notices), tokensUsed: budget.tokens };
|
|
456
487
|
}
|
|
457
488
|
if (budget.capMessage) {
|
|
458
|
-
return { ...failedStep(budget.used, budget.capMessage), tokensUsed: budget.tokens };
|
|
489
|
+
return { ...failedStep(budget.used, budget.capMessage, notices), tokensUsed: budget.tokens };
|
|
459
490
|
}
|
|
460
491
|
const units = outcomes.map((outcome, index) => outcome ?? {
|
|
461
492
|
unitId: workUnits[index].unitId,
|
|
@@ -470,7 +501,7 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
470
501
|
if (diverged.length > 0) {
|
|
471
502
|
return failedStep(budget.used, diverged
|
|
472
503
|
.map((u) => u.error ?? `replay divergence: unit "${u.unitId}" was journaled with different inputs`)
|
|
473
|
-
.join(" "));
|
|
504
|
+
.join(" "), notices);
|
|
474
505
|
}
|
|
475
506
|
// A journal-write failure is likewise HARD regardless of on_error: the
|
|
476
507
|
// unit dispatched (spent tokens, ran side effects) but its result could not
|
|
@@ -481,7 +512,7 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
481
512
|
const unjournaled = units.filter((u) => u.failureReason === "journal_write_failed");
|
|
482
513
|
if (unjournaled.length > 0) {
|
|
483
514
|
return {
|
|
484
|
-
...failedStep(budget.used, unjournaled.map((u) => u.error ?? `unit "${u.unitId}" result could not be journaled`).join(" ")),
|
|
515
|
+
...failedStep(budget.used, unjournaled.map((u) => u.error ?? `unit "${u.unitId}" result could not be journaled`).join(" "), notices),
|
|
485
516
|
tokensUsed: budget.tokens,
|
|
486
517
|
};
|
|
487
518
|
}
|
|
@@ -497,57 +528,48 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
497
528
|
const reduced = reduceStepOutcomes(plan, reducer, isFanOut, template.onError, units);
|
|
498
529
|
return {
|
|
499
530
|
...reduced,
|
|
531
|
+
...(notices ? { notices } : {}),
|
|
500
532
|
unitsDispatched: budget.used,
|
|
501
533
|
tokensUsed: budget.tokens,
|
|
502
534
|
};
|
|
503
535
|
}
|
|
504
536
|
async function runUnit(input) {
|
|
505
|
-
const { plan, workUnit, env, ctx, dispatcher } = input;
|
|
537
|
+
const { plan, workUnit, env, sensitiveValues, ctx, dispatcher } = input;
|
|
506
538
|
const unitId = workUnit.unitId;
|
|
507
|
-
//
|
|
508
|
-
// computeStepWorkList
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
// executeStepPlanInConnection returns on `!workList.ok` without reaching here.
|
|
512
|
-
// So every unit below carries either `exec` (a child process, naming no
|
|
513
|
-
// engine) or `engine` + `invocation`.
|
|
539
|
+
// Target validity is a WHOLE-LIST invariant, never a per-unit condition:
|
|
540
|
+
// computeStepWorkList rejects a step before building any unit when its sole
|
|
541
|
+
// frozen target is invalid, so every unit below carries one executable
|
|
542
|
+
// command/agent/SDK/direct-LLM target.
|
|
514
543
|
// The prompt (and therefore the input hash) was built once with the BASE
|
|
515
544
|
// unit id by computeStepWorkList: a retry re-dispatches the SAME input, the
|
|
516
545
|
// `~r<n>` suffix is journal bookkeeping only.
|
|
517
546
|
const { prompt, inputHash } = workUnit;
|
|
518
|
-
const sensitiveValues = input.sensitiveValues;
|
|
519
547
|
const request = {
|
|
520
548
|
runId: ctx.runId,
|
|
521
549
|
stepId: plan.stepId,
|
|
522
550
|
unitId,
|
|
523
551
|
nodeId: workUnit.nodeId,
|
|
524
552
|
prompt,
|
|
525
|
-
|
|
526
|
-
...(workUnit.fallbackEngine ? { fallbackEngine: workUnit.fallbackEngine } : {}),
|
|
527
|
-
...(workUnit.invocation ? { invocation: workUnit.invocation } : {}),
|
|
528
|
-
...(workUnit.exec ? { exec: workUnit.exec } : {}),
|
|
553
|
+
frozenTarget: workUnit.frozenTarget,
|
|
529
554
|
...(workUnit.execContext ? { execContext: workUnit.execContext } : {}),
|
|
530
555
|
// A NON-isolated exec unit spawns in the engine invocation's working
|
|
531
556
|
// directory. `dispatchJournaledAttempt` overwrites this with the unit's
|
|
532
557
|
// fresh worktree when `isolation: worktree` is in play. Only exec units get
|
|
533
558
|
// it: handing an agent unit a cwd it never had would change harness
|
|
534
559
|
// behavior, and the agent path already takes its cwd from its profile.
|
|
535
|
-
...(workUnit.
|
|
560
|
+
...(workUnit.frozenTarget.kind !== "command" && ctx.workDir !== undefined ? { cwd: ctx.workDir } : {}),
|
|
536
561
|
timeoutMs: workUnit.timeoutMs,
|
|
537
562
|
...(workUnit.schema ? { schema: workUnit.schema } : {}),
|
|
538
563
|
...(env ? { env } : {}),
|
|
539
|
-
...(sensitiveValues
|
|
564
|
+
...(sensitiveValues ? { sensitiveValues } : {}),
|
|
540
565
|
...(input.signal ? { signal: input.signal } : {}),
|
|
541
566
|
};
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// the shared work-list), retry attempt N under `<baseId>~r<N>`. Every attempt
|
|
545
|
-
// keeps its own row. Retries only fire when the failure reason is in
|
|
546
|
-
// `retry.on`.
|
|
567
|
+
// One content-derived unit id is retained across every retry; the append-only
|
|
568
|
+
// attempt table supplies the 1-based attempt identity.
|
|
547
569
|
const retry = workUnit.retry;
|
|
548
570
|
const maxAttempts = 1 + Math.max(0, retry?.max ?? 0);
|
|
549
571
|
const journalBaseId = workUnit.journalBaseId;
|
|
550
|
-
const attemptIdFor = (
|
|
572
|
+
const attemptIdFor = (_attempt) => journalBaseId;
|
|
551
573
|
// Durable-row reuse — literally the decision executeStepPlan's preflight gate
|
|
552
574
|
// counted, handed down rather than recomputed, so the gate cannot disagree
|
|
553
575
|
// with what happens here. A completed row with the matching input hash IS the
|
|
@@ -609,11 +631,8 @@ async function runUnit(input) {
|
|
|
609
631
|
...(input.worktreeBase !== undefined ? { worktreeBase: input.worktreeBase } : {}),
|
|
610
632
|
pendingWorktreeCleanups: input.pendingWorktreeCleanups,
|
|
611
633
|
});
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
// the content-derived BASE id — the suffix is journal bookkeeping the report
|
|
615
|
-
// surface never sees, so leaking it into evidence.units would diverge the two
|
|
616
|
-
// surfaces (R4 parity, exposed once the conformance graph compares evidence.units).
|
|
634
|
+
// Attempts use `~r<n>` journal suffixes while durable step evidence remains
|
|
635
|
+
// attached to the content-derived base identity.
|
|
617
636
|
outcome.unitId = unitId;
|
|
618
637
|
// Budget token accounting (addendum R2): every actual dispatch's reported
|
|
619
638
|
// usage counts against the run's max_tokens ceiling; crossing it aborts
|
|
@@ -678,50 +697,119 @@ function journaledUnitResultJson(outcome) {
|
|
|
678
697
|
return null;
|
|
679
698
|
return JSON.stringify(clip(parts.join("\n--- unit output ---\n"), WORKFLOW_UNIT_DIAGNOSTIC_CLIP));
|
|
680
699
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
// row — nothing was dispatched (same contract as an expression failure).
|
|
689
|
-
let worktreePath;
|
|
690
|
-
if (input.worktreeBase !== undefined) {
|
|
691
|
-
const created = await createUnitWorktree(input.worktreeBase, ctx.runId, attemptId);
|
|
692
|
-
// Reported BEFORE the failure check: when the worktree could not be minted,
|
|
693
|
-
// the leftover moved aside is the only copy of the prior attempt's
|
|
694
|
-
// uncollected work, so that is exactly when its path must not be swallowed.
|
|
695
|
-
if (created.preservedLeftover !== undefined) {
|
|
696
|
-
// Never destroy a dirty (or unverifiable) leftover from a prior
|
|
697
|
-
// invocation of the same attempt — it was moved aside instead.
|
|
698
|
-
warn(`Workflow unit ${attemptId}: a previous attempt left uncollected work in its isolation worktree; ` +
|
|
699
|
-
`preserved at ${created.preservedLeftover}`);
|
|
700
|
-
}
|
|
701
|
-
if (!created.ok) {
|
|
702
|
-
return { unitId: request.unitId, ok: false, failureReason: "worktree_failed", error: created.error };
|
|
703
|
-
}
|
|
704
|
-
worktreePath = created.path;
|
|
705
|
-
request = { ...request, cwd: worktreePath };
|
|
700
|
+
async function prepareAttemptWorktree(input) {
|
|
701
|
+
if (input.worktreeBase === undefined)
|
|
702
|
+
return { ok: true, request: input.request };
|
|
703
|
+
const created = await createUnitWorktree(input.worktreeBase, input.ctx.runId, input.attemptId, input.workUnit.frozenTarget.gitCommitOid);
|
|
704
|
+
if (created.preservedLeftover !== undefined) {
|
|
705
|
+
warn(`Workflow unit ${input.attemptId}: a previous attempt left uncollected work in its isolation worktree; ` +
|
|
706
|
+
`preserved at ${created.preservedLeftover}`);
|
|
706
707
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
708
|
+
if (!created.ok) {
|
|
709
|
+
return {
|
|
710
|
+
ok: false,
|
|
711
|
+
outcome: {
|
|
712
|
+
unitId: input.request.unitId,
|
|
713
|
+
ok: false,
|
|
714
|
+
failureReason: "worktree_failed",
|
|
715
|
+
error: created.error,
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
return { ok: true, request: { ...input.request, cwd: created.path }, worktreePath: created.path };
|
|
720
|
+
}
|
|
721
|
+
async function reserveJournaledDispatch(input, request, worktreePath, startedAt) {
|
|
722
|
+
const { plan, workUnit, ctx, attemptId, inputHash } = input;
|
|
723
|
+
let durableAttempt;
|
|
724
|
+
await enqueueUnitWrite(async () => {
|
|
725
|
+
await withWorkflowRunsRepo((repo) => {
|
|
726
|
+
const target = workUnit.frozenTarget;
|
|
727
|
+
const holder = ctx.leaseHolder ?? `direct:${randomUUID()}`;
|
|
728
|
+
const reserved = repo.reserveUnitAttempt({
|
|
711
729
|
runId: ctx.runId,
|
|
712
730
|
unitId: attemptId,
|
|
713
731
|
stepId: plan.stepId,
|
|
714
732
|
nodeId: workUnit.nodeId,
|
|
715
733
|
parentUnitId: workUnit.isFanOut ? `${plan.stepId}.map` : null,
|
|
716
|
-
phase:
|
|
734
|
+
phase: "unit",
|
|
717
735
|
runner: workUnit.runner,
|
|
718
|
-
engine: request.engine
|
|
719
|
-
model: request.
|
|
736
|
+
engine: target.kind === "command" ? target.request.engine.name : null,
|
|
737
|
+
model: target.kind === "command" ? (target.request.model?.resolved ?? null) : null,
|
|
720
738
|
inputHash,
|
|
721
739
|
worktreePath: worktreePath ?? null,
|
|
722
|
-
|
|
723
|
-
|
|
740
|
+
claimHolder: holder,
|
|
741
|
+
claimExpiresAt: new Date(Date.parse(startedAt) + 90_000).toISOString(),
|
|
742
|
+
now: startedAt,
|
|
743
|
+
leaseMode: ctx.leaseHolder === undefined ? "direct" : "engine",
|
|
744
|
+
});
|
|
745
|
+
if (reserved.kind === "busy") {
|
|
746
|
+
throw new Error(`unit "${attemptId}" already has a live durable attempt held by ${reserved.attempt.claim_holder}`);
|
|
747
|
+
}
|
|
748
|
+
durableAttempt = reserved.attempt;
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
if (!durableAttempt)
|
|
752
|
+
throw new Error(`unit "${attemptId}" did not reserve a durable attempt`);
|
|
753
|
+
return durableAttempt;
|
|
754
|
+
}
|
|
755
|
+
async function finishJournaledDispatch(input) {
|
|
756
|
+
const { attempt: source, durableAttempt, finishedAt, outcome } = input;
|
|
757
|
+
const { ctx, attemptId } = source;
|
|
758
|
+
await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => {
|
|
759
|
+
const finished = repo.finishUnitAttempt({
|
|
760
|
+
runId: ctx.runId,
|
|
761
|
+
unitId: attemptId,
|
|
762
|
+
attempt: durableAttempt.attempt,
|
|
763
|
+
dispatchId: durableAttempt.dispatch_id,
|
|
764
|
+
claimHolder: durableAttempt.claim_holder,
|
|
765
|
+
status: outcome.ok ? "completed" : "failed",
|
|
766
|
+
resultJson: journaledUnitResultJson(outcome),
|
|
767
|
+
tokens: outcome.tokens ?? null,
|
|
768
|
+
failureReason: outcome.failureReason ?? null,
|
|
769
|
+
sessionId: outcome.sessionId ?? null,
|
|
770
|
+
finishedAt,
|
|
724
771
|
});
|
|
772
|
+
if (!finished) {
|
|
773
|
+
if (repo.getUnitAttempts(ctx.runId, attemptId).length === 0) {
|
|
774
|
+
throw new Error(`finishUnitAttempt updated no row: no durable attempt "${attemptId}" exists for run "${ctx.runId}".`);
|
|
775
|
+
}
|
|
776
|
+
warn(`Workflow unit ${attemptId} (run ${ctx.runId}) ${outcome.ok ? "completed" : `failed (${outcome.failureReason ?? "error"})`}, ` +
|
|
777
|
+
`but its durable attempt was reclaimed or finished by another engine invocation — refusing to overwrite ` +
|
|
778
|
+
`the CAS winner. This dispatch's result is not journaled.`);
|
|
779
|
+
}
|
|
780
|
+
}));
|
|
781
|
+
}
|
|
782
|
+
function queueAttemptWorktreeCleanup(input, worktreePath) {
|
|
783
|
+
if (worktreePath === undefined || input.worktreeBase === undefined)
|
|
784
|
+
return;
|
|
785
|
+
const worktreeBase = input.worktreeBase;
|
|
786
|
+
input.pendingWorktreeCleanups.push((async () => {
|
|
787
|
+
try {
|
|
788
|
+
const cleanup = await cleanupUnitWorktree(worktreeBase, worktreePath);
|
|
789
|
+
if (cleanup.dirty) {
|
|
790
|
+
warn(`Workflow unit ${input.attemptId} left uncommitted changes in its isolation worktree; retained at ${worktreePath}`);
|
|
791
|
+
}
|
|
792
|
+
else if (!cleanup.removed) {
|
|
793
|
+
warn(`Workflow unit ${input.attemptId}: could not clean up isolation worktree ${worktreePath}: ${cleanup.error}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
warn(`Workflow unit ${input.attemptId}: could not clean up isolation worktree ${worktreePath}: ${message(err)}`);
|
|
798
|
+
}
|
|
799
|
+
})());
|
|
800
|
+
}
|
|
801
|
+
/** Journal one dispatch attempt: insert row, events, dispatch, finish row. */
|
|
802
|
+
async function dispatchJournaledAttempt(input) {
|
|
803
|
+
const { workUnit, ctx, dispatcher, attemptId } = input;
|
|
804
|
+
const prepared = await prepareAttemptWorktree(input);
|
|
805
|
+
if (!prepared.ok)
|
|
806
|
+
return prepared.outcome;
|
|
807
|
+
let { request } = prepared;
|
|
808
|
+
const { worktreePath } = prepared;
|
|
809
|
+
const startedAt = new Date().toISOString();
|
|
810
|
+
let durableAttempt;
|
|
811
|
+
try {
|
|
812
|
+
durableAttempt = await reserveJournaledDispatch(input, request, worktreePath, startedAt);
|
|
725
813
|
}
|
|
726
814
|
catch (err) {
|
|
727
815
|
// A failed dispatch-row insert means NOTHING dispatched (the row is the
|
|
@@ -729,9 +817,8 @@ async function dispatchJournaledAttempt(input) {
|
|
|
729
817
|
// letting the throw escape into the scheduler, where a swallowed worker
|
|
730
818
|
// error is indistinguishable from "never claimed" and used to be
|
|
731
819
|
// misreported as an aborted, never-dispatched unit.
|
|
732
|
-
if (worktreePath !== undefined && input.worktreeBase !== undefined)
|
|
820
|
+
if (worktreePath !== undefined && input.worktreeBase !== undefined)
|
|
733
821
|
await cleanupUnitWorktree(input.worktreeBase, worktreePath);
|
|
734
|
-
}
|
|
735
822
|
return {
|
|
736
823
|
unitId: request.unitId,
|
|
737
824
|
ok: false,
|
|
@@ -739,21 +826,27 @@ async function dispatchJournaledAttempt(input) {
|
|
|
739
826
|
error: `unit "${attemptId}" could not journal its dispatch row (nothing was dispatched): ${message(err)}`,
|
|
740
827
|
};
|
|
741
828
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
829
|
+
request = {
|
|
830
|
+
...request,
|
|
831
|
+
attempt: durableAttempt.attempt,
|
|
832
|
+
dispatchId: durableAttempt.dispatch_id,
|
|
833
|
+
};
|
|
834
|
+
const dispatched = await dispatchUnit(request, dispatcher);
|
|
835
|
+
// Credential and passthrough values are intentionally sampled only AFTER
|
|
836
|
+
// the default dispatcher has authorized/lowered the frozen request and
|
|
837
|
+
// materialized credentials at its terminal dispatch boundary. Custom test
|
|
838
|
+
// dispatchers receive the same post-dispatch journal scrub.
|
|
839
|
+
const sensitiveValues = collectWorkflowDispatchSensitiveValues({
|
|
840
|
+
...(request.frozenTarget.kind === "command" ? { runner: request.frozenTarget.runner } : {}),
|
|
841
|
+
...(request.sensitiveValues ? { sensitiveValues: request.sensitiveValues } : {}),
|
|
842
|
+
}, request.env);
|
|
843
|
+
const outcome = redactUnitOutcome(dispatched, sensitiveValues);
|
|
750
844
|
const finishedAt = new Date().toISOString();
|
|
751
845
|
// A dispatched unit's outcome is NEVER silently discarded. The single-driver
|
|
752
|
-
// guard lives
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
// driver's live dispatch. A row that IS still ours is finished with the real
|
|
846
|
+
// guard lives on the append-only attempt row: attempt number, dispatch id,
|
|
847
|
+
// claim holder, and running status must all match. A stale driver's finish
|
|
848
|
+
// therefore cannot clobber a reclaimed or retried dispatch. An attempt that
|
|
849
|
+
// IS still ours is finished with the real
|
|
757
850
|
// result even when the run went non-active or the lease moved mid-flight —
|
|
758
851
|
// dropping it would leave the row `running` and make a later resume
|
|
759
852
|
// re-dispatch side-effecting work that already ran and already spent tokens.
|
|
@@ -761,59 +854,12 @@ async function dispatchJournaledAttempt(input) {
|
|
|
761
854
|
// lease-guarded in completeWorkflowStep.
|
|
762
855
|
let journalError;
|
|
763
856
|
try {
|
|
764
|
-
await
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
tokens: outcome.tokens ?? null,
|
|
771
|
-
failureReason: outcome.failureReason ?? null,
|
|
772
|
-
// Harness-native session id (P2): journaled so resume can replay the
|
|
773
|
-
// harness's own context cache (e.g. `codex exec resume <id>`).
|
|
774
|
-
sessionId: outcome.sessionId ?? null,
|
|
775
|
-
finishedAt,
|
|
776
|
-
dispatchStartedAt: startedAt,
|
|
777
|
-
});
|
|
778
|
-
if (!finished) {
|
|
779
|
-
if (!repo.getUnit(ctx.runId, attemptId)) {
|
|
780
|
-
// The dispatch row vanished (run deleted mid-flight, journal
|
|
781
|
-
// tampered): the same journaling-bug contract finishUnit throws
|
|
782
|
-
// for — surfaced below as a journal-write failure, never a no-op.
|
|
783
|
-
throw new Error(`finishUnit updated no row: no unit "${attemptId}" exists for run "${ctx.runId}". ` +
|
|
784
|
-
`The dispatch row this invocation inserted is gone, so the unit's terminal state cannot be journaled.`);
|
|
785
|
-
}
|
|
786
|
-
// The row exists but is no longer this dispatch's `running` row:
|
|
787
|
-
// another engine invocation re-dispatched (or finished) the unit
|
|
788
|
-
// after taking the run. Its journal owns the unit now — writing
|
|
789
|
-
// would clobber a live dispatch — so the outcome is surfaced
|
|
790
|
-
// loudly instead of silently dropped.
|
|
791
|
-
warn(`Workflow unit ${attemptId} (run ${ctx.runId}) ${outcome.ok ? "completed" : `failed (${outcome.failureReason ?? "error"})`}, ` +
|
|
792
|
-
`but its journal row was re-dispatched by another engine invocation mid-flight — refusing to overwrite ` +
|
|
793
|
-
`the new driver's row. This dispatch's result is not journaled.`);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
const run = repo.getRunById(ctx.runId);
|
|
797
|
-
if (run?.status !== "active" ||
|
|
798
|
-
(ctx.leaseHolder !== undefined && run.engine_lease_holder !== ctx.leaseHolder)) {
|
|
799
|
-
warn(`Workflow unit ${attemptId} (run ${ctx.runId}): the run ` +
|
|
800
|
-
`${run?.status !== "active" ? `is now ${run?.status ?? "gone"}` : `lease moved to ${run?.engine_lease_holder ?? "(nobody)"}`} ` +
|
|
801
|
-
`while the unit was in flight; its result was journaled so a resume reuses it instead of re-dispatching.`);
|
|
802
|
-
}
|
|
803
|
-
insertEventStrict(db, {
|
|
804
|
-
eventType: "workflow_unit_finished",
|
|
805
|
-
ts: finishedAt,
|
|
806
|
-
ref: ctx.workflowRef,
|
|
807
|
-
metadata: {
|
|
808
|
-
runId: ctx.runId,
|
|
809
|
-
stepId: plan.stepId,
|
|
810
|
-
unitId: attemptId,
|
|
811
|
-
status: outcome.ok ? "completed" : "failed",
|
|
812
|
-
...(outcome.failureReason ? { failureReason: outcome.failureReason } : {}),
|
|
813
|
-
...(outcome.tokens !== undefined ? { tokens: outcome.tokens } : {}),
|
|
814
|
-
},
|
|
815
|
-
});
|
|
816
|
-
})));
|
|
857
|
+
await finishJournaledDispatch({
|
|
858
|
+
attempt: input,
|
|
859
|
+
durableAttempt,
|
|
860
|
+
finishedAt,
|
|
861
|
+
outcome,
|
|
862
|
+
});
|
|
817
863
|
}
|
|
818
864
|
catch (err) {
|
|
819
865
|
journalError = err;
|
|
@@ -826,23 +872,7 @@ async function dispatchJournaledAttempt(input) {
|
|
|
826
872
|
// `git worktree add`, and awaiting it in this unit's scheduler slot made a
|
|
827
873
|
// finished unit wait out other units' full checkouts before its worker could
|
|
828
874
|
// claim the next item.
|
|
829
|
-
|
|
830
|
-
const worktreeBase = input.worktreeBase;
|
|
831
|
-
input.pendingWorktreeCleanups.push((async () => {
|
|
832
|
-
try {
|
|
833
|
-
const cleanup = await cleanupUnitWorktree(worktreeBase, worktreePath);
|
|
834
|
-
if (cleanup.dirty) {
|
|
835
|
-
warn(`Workflow unit ${attemptId} left uncommitted changes in its isolation worktree; retained at ${worktreePath}`);
|
|
836
|
-
}
|
|
837
|
-
else if (!cleanup.removed) {
|
|
838
|
-
warn(`Workflow unit ${attemptId}: could not clean up isolation worktree ${worktreePath}: ${cleanup.error}`);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
catch (err) {
|
|
842
|
-
warn(`Workflow unit ${attemptId}: could not clean up isolation worktree ${worktreePath}: ${message(err)}`);
|
|
843
|
-
}
|
|
844
|
-
})());
|
|
845
|
-
}
|
|
875
|
+
queueAttemptWorktreeCleanup(input, worktreePath);
|
|
846
876
|
// A journal-write failure AFTER a successful dispatch is its own loud
|
|
847
877
|
// failure class: the unit's work ran (and may have succeeded), but its
|
|
848
878
|
// terminal state could not be recorded, so the row may be stuck `running`.
|
|
@@ -858,6 +888,7 @@ async function dispatchJournaledAttempt(input) {
|
|
|
858
888
|
`but its result could not be journaled: ${message(journalError)}`,
|
|
859
889
|
...(outcome.tokens !== undefined ? { tokens: outcome.tokens } : {}),
|
|
860
890
|
...(outcome.sessionId !== undefined ? { sessionId: outcome.sessionId } : {}),
|
|
891
|
+
...(outcome.notices ? { notices: outcome.notices } : {}),
|
|
861
892
|
};
|
|
862
893
|
}
|
|
863
894
|
return outcome;
|
|
@@ -896,9 +927,10 @@ class UnitTransportError extends Error {
|
|
|
896
927
|
async function dispatchUnit(request, dispatcher) {
|
|
897
928
|
let tokens = 0;
|
|
898
929
|
let sawUsage = false;
|
|
930
|
+
let loweringNotices;
|
|
899
931
|
// Harness-native session id revealed by dispatch (P2). Captured across
|
|
900
932
|
// structured-output retries (last one wins) so it survives into the
|
|
901
|
-
// UnitOutcome and gets journaled
|
|
933
|
+
// UnitOutcome and gets journaled by finishUnitAttempt — the seam's
|
|
902
934
|
// contract ("stored opportunistically on the unit row for resume").
|
|
903
935
|
let sessionId;
|
|
904
936
|
const dispatchOnce = async (feedback) => {
|
|
@@ -908,6 +940,7 @@ async function dispatchUnit(request, dispatcher) {
|
|
|
908
940
|
tokens +=
|
|
909
941
|
(result.usage.inputTokens ?? 0) + (result.usage.outputTokens ?? 0) + (result.usage.reasoningTokens ?? 0);
|
|
910
942
|
}
|
|
943
|
+
loweringNotices = mergeLoweringNotices(loweringNotices, result.notices);
|
|
911
944
|
// Capture before the ok-check: a failed attempt can still have configured
|
|
912
945
|
// a session (e.g. codex `session_configured` then a tool crash).
|
|
913
946
|
if (result.sessionId !== undefined)
|
|
@@ -919,6 +952,7 @@ async function dispatchUnit(request, dispatcher) {
|
|
|
919
952
|
const captured = () => ({
|
|
920
953
|
...(sawUsage ? { tokens } : {}),
|
|
921
954
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
955
|
+
...(loweringNotices ? { notices: loweringNotices } : {}),
|
|
922
956
|
});
|
|
923
957
|
try {
|
|
924
958
|
if (request.schema) {
|
|
@@ -936,7 +970,7 @@ async function dispatchUnit(request, dispatcher) {
|
|
|
936
970
|
// can produce a second deployment. Declared `retry:` still applies (the
|
|
937
971
|
// executor's own loop), because that is a policy the author opted into
|
|
938
972
|
// per failure reason.
|
|
939
|
-
...(request.
|
|
973
|
+
...(request.frozenTarget.kind !== "command" ? { parse: parseExecJson, maxAttempts: 1 } : {}),
|
|
940
974
|
validate: (candidate) => {
|
|
941
975
|
const errors = validateJsonSchemaSubset(candidate, schema);
|
|
942
976
|
return errors.length === 0 ? { ok: true, value: candidate } : { ok: false, errors };
|
|
@@ -963,7 +997,7 @@ async function dispatchUnit(request, dispatcher) {
|
|
|
963
997
|
};
|
|
964
998
|
}
|
|
965
999
|
const text = await dispatchOnce();
|
|
966
|
-
// Normalize an EMPTY successful output to "no text". `
|
|
1000
|
+
// Normalize an EMPTY successful output to "no text". `finishUnitAttempt` journals
|
|
967
1001
|
// result_json = NULL for a falsy text, so durable-reuse rehydrates NO text
|
|
968
1002
|
// from the row (unitOutcomeFromRow). Preserving `text: ""` only in this live
|
|
969
1003
|
// outcome would make the LIVE step artifact ("") diverge from the artifact a
|
|
@@ -992,270 +1026,78 @@ async function dispatchUnit(request, dispatcher) {
|
|
|
992
1026
|
};
|
|
993
1027
|
}
|
|
994
1028
|
}
|
|
995
|
-
// ── Env bindings ─────────────────────────────────────────────────────────────
|
|
996
|
-
/**
|
|
997
|
-
* Resolve every unit `env` ref through the extracted `akm env run` core
|
|
998
|
-
* (loadEnv + secret tokens + dangerous-key policy + keys-only audit event).
|
|
999
|
-
* Lazily imported so the engine has no env/secret dependency until a
|
|
1000
|
-
* workflow actually declares bindings.
|
|
1001
|
-
*/
|
|
1002
|
-
async function resolveEnvBindings(refs) {
|
|
1003
|
-
const { resolveEnvBinding } = await import("../../commands/env/env-binding.js");
|
|
1004
|
-
const merged = {};
|
|
1005
|
-
for (const ref of refs) {
|
|
1006
|
-
Object.assign(merged, resolveEnvBinding(ref).values);
|
|
1007
|
-
}
|
|
1008
|
-
return merged;
|
|
1009
|
-
}
|
|
1010
1029
|
// ── Default dispatcher (production substrate) ───────────────────────────────
|
|
1011
1030
|
/**
|
|
1012
|
-
* Dispatch through
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
-
* sdk → `executeRunner` → `runOpencodeSdk`
|
|
1016
|
-
*
|
|
1017
|
-
* Every v3 invocation names a frozen engine; no live profile/default fallback
|
|
1018
|
-
* is consulted during dispatch.
|
|
1031
|
+
* Dispatch a frozen engine through the common prepare → lower → dispatch seam.
|
|
1032
|
+
* No live profile/default/model map is consulted, and credentials remain
|
|
1033
|
+
* symbolic until the final dispatch boundary.
|
|
1019
1034
|
*/
|
|
1020
|
-
/**
|
|
1021
|
-
* Build the platform-agnostic {@link import("../../integrations/agent/builder-shared.js").AgentDispatchRequest}
|
|
1022
|
-
* for an agent (CLI) unit from the resolved dispatch request and its final
|
|
1023
|
-
* (feedback-augmented) prompt.
|
|
1024
|
-
*
|
|
1025
|
-
* Threading the unit's output `schema` here is what activates each harness's
|
|
1026
|
-
* native structured-output path (plan §"Structured-output normalization"):
|
|
1027
|
-
* - Codex (native-schema tier) writes it to a temp file and passes
|
|
1028
|
-
* `--output-schema <file>`.
|
|
1029
|
-
* - Copilot / Gemini switch stdout to their documented JSON envelope
|
|
1030
|
-
* (`--output-format json`) and append their schema-aware prompt directive.
|
|
1031
|
-
* - Pi switches to its JSONL event stream (`--mode json`) and appends its
|
|
1032
|
-
* directive.
|
|
1033
|
-
* Without the schema the argv is byte-identical to the pre-fix plain-prompt
|
|
1034
|
-
* shape. The engine's post-hoc `runStructured` validation runs regardless — the
|
|
1035
|
-
* harness path constrains/hints, the engine still verifies (constrained output
|
|
1036
|
-
* is trusted but verified). The `model` is passed raw so the builder resolves
|
|
1037
|
-
* aliases per-harness. Only `prompt` (with any gate feedback already folded in),
|
|
1038
|
-
* `model`, and `schema` are engine-derived; `systemPrompt`/`tools`/`cwd` come
|
|
1039
|
-
* from the profile/asset, not the workflow unit.
|
|
1040
|
-
*/
|
|
1041
|
-
export function buildAgentDispatchRequest(request, prompt) {
|
|
1042
|
-
return {
|
|
1043
|
-
prompt,
|
|
1044
|
-
...(request.systemPrompt ? { systemPrompt: request.systemPrompt } : {}),
|
|
1045
|
-
...(request.invocation?.model ? { model: request.invocation.model } : {}),
|
|
1046
|
-
...(request.invocation?.model ? { modelIsExact: true } : {}),
|
|
1047
|
-
...(request.schema ? { schema: request.schema } : {}),
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
1035
|
export const defaultUnitDispatcher = async (request, feedback) => {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
// A declared `output:` schema is what makes an output-cap overflow fatal:
|
|
1065
|
-
// stdout must then parse as exactly one JSON value, which a truncated
|
|
1066
|
-
// prefix cannot. Without one, overflow is marked in the artifact and the
|
|
1067
|
-
// command's own exit code decides the unit. See `exec-unit.ts`.
|
|
1068
|
-
...(request.schema ? { hasOutputSchema: true } : {}),
|
|
1069
|
-
timeoutMs: request.timeoutMs,
|
|
1070
|
-
...(request.signal ? { signal: request.signal } : {}),
|
|
1036
|
+
const frozenTarget = request.frozenTarget;
|
|
1037
|
+
if (frozenTarget.kind === "script") {
|
|
1038
|
+
assertFrozenDirectoryIdentity(frozenTarget.cwdIdentity);
|
|
1039
|
+
if (frozenTarget.executable) {
|
|
1040
|
+
assertFrozenExecutableIdentity(frozenTarget.executable, `unit ${request.unitId} executable`);
|
|
1041
|
+
}
|
|
1042
|
+
const materialized = materializeFrozenScript({
|
|
1043
|
+
sourceRef: frozenTarget.ref,
|
|
1044
|
+
interpreter: frozenTarget.interpreter,
|
|
1045
|
+
extension: frozenTarget.extension,
|
|
1046
|
+
bytesBase64: frozenTarget.bytesBase64,
|
|
1047
|
+
byteLength: frozenTarget.byteLength,
|
|
1048
|
+
sha256: frozenTarget.contentHash,
|
|
1071
1049
|
});
|
|
1072
|
-
}
|
|
1073
|
-
if (!request.engine || !request.invocation) {
|
|
1074
|
-
return {
|
|
1075
|
-
ok: false,
|
|
1076
|
-
text: "",
|
|
1077
|
-
failureReason: "dispatch_error",
|
|
1078
|
-
error: `unit "${request.unitId}" has neither a frozen engine snapshot nor an exec command to dispatch.`,
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
const engineRequest = request;
|
|
1082
|
-
const prompt = feedback ? `${request.prompt}\n\n${feedback}` : request.prompt;
|
|
1083
|
-
const resolved = frozenUnitRunner(engineRequest);
|
|
1084
|
-
// `env` bindings can only reach a child process. The agent (CLI) runner
|
|
1085
|
-
// spawns one per call, and the sdk runner now injects them for real via the
|
|
1086
|
-
// env-keyed opencode server registry (sdk-runner.ts module doc, open seam
|
|
1087
|
-
// decision 1 resolved in R2) — but the llm runner has no child at all, so
|
|
1088
|
-
// it still fails loudly: an audit event claiming an injection that never
|
|
1089
|
-
// reached the unit would be a lie.
|
|
1090
|
-
if (request.env && Object.keys(request.env).length > 0 && resolved.kind === "llm") {
|
|
1091
|
-
return {
|
|
1092
|
-
ok: false,
|
|
1093
|
-
text: "",
|
|
1094
|
-
failureReason: "env_unsupported",
|
|
1095
|
-
error: `unit "${request.unitId}" declares env bindings, which require a child process (agent or sdk runner) — ` +
|
|
1096
|
-
`the "llm" runner cannot inject a per-unit child environment.`,
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
// Same shape for worktree isolation resolved onto llm through `inherit`:
|
|
1100
|
-
// the executor already rejects an EXPLICIT llm+isolation pairing before
|
|
1101
|
-
// dispatch, but an inherit unit only reveals its runner here.
|
|
1102
|
-
if (request.cwd && resolved.kind === "llm") {
|
|
1103
|
-
return {
|
|
1104
|
-
ok: false,
|
|
1105
|
-
text: "",
|
|
1106
|
-
failureReason: "isolation_unsupported",
|
|
1107
|
-
error: `unit "${request.unitId}" declares isolation: worktree but resolved to the "llm" runner, ` +
|
|
1108
|
-
`which has no working directory to isolate. Use the agent or sdk runner for isolated units.`,
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
if (resolved.kind === "llm") {
|
|
1112
|
-
const { chatCompletion, LlmCallError } = await import("../../llm/client.js");
|
|
1113
|
-
const connection = resolved.connection;
|
|
1114
1050
|
try {
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
:
|
|
1121
|
-
|
|
1051
|
+
const command = frozenScriptCommand({
|
|
1052
|
+
sourceRef: frozenTarget.ref,
|
|
1053
|
+
interpreter: frozenTarget.interpreter,
|
|
1054
|
+
extension: frozenTarget.extension,
|
|
1055
|
+
bytesBase64: frozenTarget.bytesBase64,
|
|
1056
|
+
byteLength: frozenTarget.byteLength,
|
|
1057
|
+
sha256: frozenTarget.contentHash,
|
|
1058
|
+
}, materialized.file);
|
|
1059
|
+
if (frozenTarget.executable)
|
|
1060
|
+
command[0] = frozenTarget.executable.absolutePath;
|
|
1061
|
+
return await runExecUnit({
|
|
1062
|
+
unitId: request.unitId,
|
|
1063
|
+
exec: {
|
|
1064
|
+
...frozenTarget.exec,
|
|
1065
|
+
command: command,
|
|
1066
|
+
},
|
|
1067
|
+
baseDir: request.cwd ?? frozenTarget.cwdIdentity.realCwd,
|
|
1068
|
+
...(request.env ? { env: request.env } : {}),
|
|
1069
|
+
...(request.execContext ? { context: request.execContext } : {}),
|
|
1070
|
+
...(request.schema ? { hasOutputSchema: true } : {}),
|
|
1122
1071
|
timeoutMs: request.timeoutMs,
|
|
1123
1072
|
...(request.signal ? { signal: request.signal } : {}),
|
|
1124
|
-
// Native structured output where the connection supports it; the
|
|
1125
|
-
// executor's subset validator still runs downstream either way.
|
|
1126
|
-
...(request.schema ? { responseSchema: request.schema } : {}),
|
|
1127
1073
|
});
|
|
1128
|
-
return { ok: true, text };
|
|
1129
1074
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
// taxonomy — the vocabulary `retry.on` is validated against (program
|
|
1133
|
-
// schema PROGRAM_RETRY_REASONS) and the journal's failure_reason column
|
|
1134
|
-
// speaks. A collapsed out-of-taxonomy value ("llm_error") made the
|
|
1135
|
-
// declared failure policy dead for the entire llm runner.
|
|
1136
|
-
const failureReason = err instanceof LlmCallError ? llmFailureReasonFor(err.code) : "dispatch_error";
|
|
1137
|
-
return { ok: false, text: "", failureReason, error: message(err) };
|
|
1075
|
+
finally {
|
|
1076
|
+
cleanupFrozenScript(materialized);
|
|
1138
1077
|
}
|
|
1139
1078
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
? {
|
|
1146
|
-
kind: "sdk",
|
|
1147
|
-
profile,
|
|
1148
|
-
...(resolved.fallbackConnection ? { fallbackConnection: resolved.fallbackConnection } : {}),
|
|
1149
|
-
}
|
|
1150
|
-
: { kind: "agent", profile }, prompt, {
|
|
1151
|
-
stdio: "captured",
|
|
1152
|
-
parseOutput: "text",
|
|
1153
|
-
timeoutMs: request.timeoutMs,
|
|
1154
|
-
...(request.env ? { env: request.env } : {}),
|
|
1155
|
-
// Worktree isolation: the unit's fresh checkout is the child's cwd —
|
|
1156
|
-
// runAgent spawns there; the sdk runner scopes the session to it.
|
|
1157
|
-
...(request.cwd ? { cwd: request.cwd } : {}),
|
|
1158
|
-
...(request.signal ? { signal: request.signal } : {}),
|
|
1159
|
-
// Route CLI dispatch through the platform AgentCommandBuilder so model
|
|
1160
|
-
// aliases resolve per-harness (P0.5 model routing) AND the unit's output
|
|
1161
|
-
// schema reaches the harness's structured-output path (see
|
|
1162
|
-
// buildAgentDispatchRequest).
|
|
1163
|
-
...(resolved.kind === "agent" ? { dispatch: buildAgentDispatchRequest(request, prompt) } : {}),
|
|
1164
|
-
});
|
|
1165
|
-
// Harness result extraction (P2, plan §"The adapter contract" step 3):
|
|
1166
|
-
// when the profile's harness declares a `resultExtractor`, normalize the
|
|
1167
|
-
// raw stdout into the final answer (+ opportunistic session id) BEFORE the
|
|
1168
|
-
// engine's schema validation / retry loop sees it. Only successful agent
|
|
1169
|
-
// (CLI) runs are normalized — failures keep the raw stdout for diagnostics,
|
|
1170
|
-
// and the default path is byte-identical when no extractor is registered.
|
|
1171
|
-
let text = result.stdout;
|
|
1172
|
-
let sessionId = result.sessionId;
|
|
1173
|
-
if (resolved.kind === "agent" && result.ok) {
|
|
1174
|
-
const extractor = await resolveHarnessExtractor(resolved.profile);
|
|
1175
|
-
if (extractor) {
|
|
1176
|
-
const extraction = extractor(result);
|
|
1177
|
-
text = extraction.text;
|
|
1178
|
-
if (extraction.sessionId !== undefined)
|
|
1179
|
-
sessionId = extraction.sessionId;
|
|
1079
|
+
if (frozenTarget.kind === "shell") {
|
|
1080
|
+
if (frozenTarget.cwdIdentity)
|
|
1081
|
+
assertFrozenDirectoryIdentity(frozenTarget.cwdIdentity);
|
|
1082
|
+
if (frozenTarget.executable) {
|
|
1083
|
+
assertFrozenExecutableIdentity(frozenTarget.executable, `unit ${request.unitId} executable`);
|
|
1180
1084
|
}
|
|
1085
|
+
const command = [...frozenTarget.exec.command];
|
|
1086
|
+
if (frozenTarget.executable)
|
|
1087
|
+
command[0] = frozenTarget.executable.absolutePath;
|
|
1088
|
+
return runExecUnit({
|
|
1089
|
+
unitId: request.unitId,
|
|
1090
|
+
exec: { ...frozenTarget.exec, command: command },
|
|
1091
|
+
baseDir: request.cwd ?? frozenTarget.cwdIdentity?.realCwd ?? process.cwd(),
|
|
1092
|
+
...(request.env ? { env: request.env } : {}),
|
|
1093
|
+
...(request.execContext ? { context: request.execContext } : {}),
|
|
1094
|
+
...(request.schema ? { hasOutputSchema: true } : {}),
|
|
1095
|
+
timeoutMs: request.timeoutMs,
|
|
1096
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
1097
|
+
});
|
|
1181
1098
|
}
|
|
1182
|
-
return
|
|
1183
|
-
ok: result.ok,
|
|
1184
|
-
text,
|
|
1185
|
-
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1186
|
-
...(result.reason ? { failureReason: result.reason } : {}),
|
|
1187
|
-
...(result.error ? { error: result.error } : {}),
|
|
1188
|
-
...(result.usage ? { usage: result.usage } : {}),
|
|
1189
|
-
};
|
|
1099
|
+
return dispatchWorkflowExecution(request, feedback);
|
|
1190
1100
|
};
|
|
1191
|
-
/**
|
|
1192
|
-
* Map a typed {@link import("../../llm/client.js").LlmCallErrorCode} into the
|
|
1193
|
-
* persisted `AgentFailureReason` taxonomy (agent/spawn.ts) — the ONLY
|
|
1194
|
-
* vocabulary `retry.on` accepts and the journal's `failure_reason` column
|
|
1195
|
-
* carries. Exhaustive over the code union (typecheck fails on drift):
|
|
1196
|
-
*
|
|
1197
|
-
* - `timeout` → `timeout` (wall-clock expiry)
|
|
1198
|
-
* - `aborted` → `aborted` (caller/budget cancellation)
|
|
1199
|
-
* - `rate_limited` → `llm_rate_limit` (HTTP 429 — the canonical transient)
|
|
1200
|
-
* - `parse_error` / `provider_html_error`
|
|
1201
|
-
* → `parse_error` (a response arrived but was not the
|
|
1202
|
-
* promised JSON)
|
|
1203
|
-
* - `network_error` / `provider_error`
|
|
1204
|
-
* → `spawn_failed` (the backend could not be reached or
|
|
1205
|
-
* could not do the work — the LLM
|
|
1206
|
-
* analog of failing to start the
|
|
1207
|
-
* child; retryable as a transient)
|
|
1208
|
-
*/
|
|
1209
|
-
export function llmFailureReasonFor(code) {
|
|
1210
|
-
switch (code) {
|
|
1211
|
-
case "aborted":
|
|
1212
|
-
return "aborted";
|
|
1213
|
-
case "timeout":
|
|
1214
|
-
return "timeout";
|
|
1215
|
-
case "rate_limited":
|
|
1216
|
-
return "llm_rate_limit";
|
|
1217
|
-
case "parse_error":
|
|
1218
|
-
case "provider_html_error":
|
|
1219
|
-
return "parse_error";
|
|
1220
|
-
case "network_error":
|
|
1221
|
-
case "provider_error":
|
|
1222
|
-
return "spawn_failed";
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
/**
|
|
1226
|
-
* Resolve the harness `resultExtractor` from the canonical platform frozen
|
|
1227
|
-
* from the named engine. Unknown platforms pass raw stdout through unchanged.
|
|
1228
|
-
*/
|
|
1229
|
-
async function resolveHarnessExtractor(profile) {
|
|
1230
|
-
const { getHarness } = await import("../../integrations/harnesses/index.js");
|
|
1231
|
-
const harness = getHarness(profile.platform ?? profile.name);
|
|
1232
|
-
return harness?.resultExtractor;
|
|
1233
|
-
}
|
|
1234
|
-
/** Reconstruct the existing RunnerSpec substrate from the frozen allowlist only. */
|
|
1235
|
-
function frozenUnitRunner(request) {
|
|
1236
|
-
const snapshot = request.engine;
|
|
1237
|
-
if (snapshot.kind === "llm") {
|
|
1238
|
-
return { kind: "llm", connection: materializeFrozenLlm(snapshot, request.invocation) };
|
|
1239
|
-
}
|
|
1240
|
-
const profile = {
|
|
1241
|
-
name: snapshot.name,
|
|
1242
|
-
platform: snapshot.platform,
|
|
1243
|
-
bin: snapshot.bin,
|
|
1244
|
-
args: snapshot.args,
|
|
1245
|
-
stdio: "captured",
|
|
1246
|
-
envPassthrough: snapshot.envPassthrough,
|
|
1247
|
-
parseOutput: "text",
|
|
1248
|
-
...(snapshot.workspace ? { workspace: snapshot.workspace } : {}),
|
|
1249
|
-
...(request.invocation?.model ? { model: request.invocation.model } : {}),
|
|
1250
|
-
...(request.invocation?.model ? { modelIsExact: true } : {}),
|
|
1251
|
-
};
|
|
1252
|
-
if (snapshot.runnerKind === "agent")
|
|
1253
|
-
return { kind: "agent", profile };
|
|
1254
|
-
// The catalog is supplied transitively by the work-list only for hashing; the
|
|
1255
|
-
// SDK runner receives a frozen fallback copied into the request by its caller.
|
|
1256
|
-
const fallback = request.fallbackEngine ? materializeFrozenLlm(request.fallbackEngine, undefined) : undefined;
|
|
1257
|
-
return { kind: "sdk", profile, ...(fallback ? { fallbackConnection: fallback } : {}) };
|
|
1258
|
-
}
|
|
1259
1101
|
// ── Small helpers ────────────────────────────────────────────────────────────
|
|
1260
1102
|
/**
|
|
1261
1103
|
* Rehydrate a journaled completed unit row into a UnitOutcome (durable-row
|
|
@@ -1267,10 +1109,11 @@ function frozenUnitRunner(request) {
|
|
|
1267
1109
|
function reuseCompletedUnit(unitId, row, hasSchema) {
|
|
1268
1110
|
return unitOutcomeFromRow(unitId, row, hasSchema);
|
|
1269
1111
|
}
|
|
1270
|
-
function failedStep(dispatched, reason) {
|
|
1112
|
+
function failedStep(dispatched, reason, notices) {
|
|
1271
1113
|
return {
|
|
1272
1114
|
ok: false,
|
|
1273
1115
|
units: [],
|
|
1116
|
+
...(notices ? { notices } : {}),
|
|
1274
1117
|
evidence: { error: reason },
|
|
1275
1118
|
summary: reason,
|
|
1276
1119
|
unitsDispatched: dispatched,
|