akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.4
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 +731 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +2 -7
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +0 -17
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +361 -751
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +87 -406
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -21
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/config/retired-experimental-keys-shim.js +0 -62
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -441
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -78
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -2,125 +2,37 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
* Native executor — executes one frozen step subgraph (`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Native executor — executes one frozen step subgraph (`WorkflowPlanStep.root`)
|
|
6
|
+
* locally: fan-out through the scheduler, structured output through
|
|
7
|
+
* `runStructured`, per-unit journaling through the serialized writer queue,
|
|
8
|
+
* and `workflow_unit_*` events. It never writes step rows; advancing the spine
|
|
9
|
+
* is `run-workflow.ts`'s job.
|
|
10
10
|
*
|
|
11
|
-
* Data flow:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* null); a `collect` fan-out promotes `null` in that item's slot.
|
|
28
|
-
* - A downstream `steps.x.output` reference to an empty solo step therefore
|
|
29
|
-
* resolves against `null` and fails LOUDLY at reference resolution
|
|
30
|
-
* (`… resolved to null`) — a deterministic whole-step failure, never a
|
|
31
|
-
* silent empty string.
|
|
32
|
-
* - A SCHEMA unit is unaffected by this normalization: an empty response is
|
|
33
|
-
* not parseable JSON, so `runStructured` fails it (`parse_error`) — an
|
|
34
|
-
* empty output can never satisfy a declared schema as a silent `null`.
|
|
35
|
-
*
|
|
36
|
-
* Typed artifacts (addendum, R2): when the step declares an `output` schema
|
|
37
|
-
* (`IrStepPlan.outputSchema`), the promoted artifact is validated with the
|
|
38
|
-
* JSON-schema-subset validator BEFORE the step can complete. A mismatch fails
|
|
39
|
-
* the step (fail-fast) with the validation errors in the summary — a
|
|
40
|
-
* downstream consumer must never receive an artifact the author's contract
|
|
41
|
-
* says cannot exist. The failure is flagged (`artifactSchemaFailure` on the
|
|
42
|
-
* result) so the engine's bounded gate loop can re-run the step with the
|
|
43
|
-
* validation errors as feedback ("gate loops can re-run it") — a step with
|
|
44
|
-
* loop budget left regenerates instead of killing the run.
|
|
45
|
-
*
|
|
46
|
-
* Unit identity (addendum, R2): CONTENT-DERIVED, never positional. A fan-out
|
|
47
|
-
* unit's id is `<node_id>:<sha256(canonicalJson(item))[:12]>`; a solo unit's
|
|
48
|
-
* is `<node_id>:solo`. Identity therefore survives item-list regeneration and
|
|
49
|
-
* reordering — resuming a run whose producer re-emitted the same items in a
|
|
50
|
-
* different order reuses every journaled result. Consequences:
|
|
51
|
-
* - DUPLICATE items in one fan-out list collide on identity. That is an
|
|
52
|
-
* authoring error (the same work dispatched twice under one id): the step
|
|
53
|
-
* fails deterministically after resolving the item list, naming the
|
|
54
|
-
* duplicate, before anything dispatches.
|
|
55
|
-
* - REPLAY DIVERGENCE: a journaled COMPLETED row whose unit_id matches but
|
|
56
|
-
* whose `input_hash` differs is a hard step failure ("replay divergence"),
|
|
57
|
-
* never a silent re-dispatch — under a frozen plan the same identity must
|
|
58
|
-
* reproduce the same inputs, so a mismatch means the journal (or params
|
|
59
|
-
* row) was tampered with. Failed/running/missing rows dispatch live.
|
|
60
|
-
* - Rows with unrelated ids never match a content-derived id and are ignored.
|
|
61
|
-
*
|
|
62
|
-
* Gate loops (addendum, R2 `gate.max_loops`): when the engine re-executes a
|
|
63
|
-
* step subgraph after a gate rejection, it threads the judge's feedback in as
|
|
64
|
-
* `ctx.gateFeedback` (appended to every unit prompt — the input hash changes,
|
|
65
|
-
* so re-dispatch is natural) and marks the attempt with `ctx.gateLoop` (>= 2).
|
|
66
|
-
* Loop attempts journal under `<unitId>~l<loop>` — like `~r<n>` retries, pure
|
|
67
|
-
* journal bookkeeping on top of the content-derived identity, so loop 1's
|
|
68
|
-
* rows are never clobbered. Because gate feedback is JUDGE-authored (a fresh
|
|
69
|
-
* LLM output per invocation, not a pure function of the frozen plan), a
|
|
70
|
-
* journaled loop row whose hash no longer matches re-dispatches live instead
|
|
71
|
-
* of raising replay divergence — the divergence guarantee applies to loop-1
|
|
72
|
-
* rows, whose inputs ARE pure functions of (plan, params, journaled results).
|
|
73
|
-
*
|
|
74
|
-
* Failure policy (addendum, "explicit surface, fail-fast default"):
|
|
75
|
-
* - `onError: "fail"` (default) fails the step on any unit failure;
|
|
76
|
-
* `"continue"` records failures in the evidence and lets the gate decide.
|
|
77
|
-
* - `retry: { max, on }` re-dispatches a failed unit up to `max` extra
|
|
78
|
-
* times when its `failureReason` is in `on`. Every retry journals its OWN
|
|
79
|
-
* row under `<unitId>~r<attempt>` so no attempt's record is clobbered.
|
|
80
|
-
*
|
|
81
|
-
* Worktree isolation (addendum, R2 `isolation: worktree`): each journaled
|
|
82
|
-
* attempt of an isolated agent/sdk unit runs in a FRESH detached git worktree
|
|
83
|
-
* of the engine's working directory (`ctx.workDir`, default `process.cwd()`),
|
|
84
|
-
* minted under a run-scoped tmp dir (`worktree.ts`) and passed to dispatch as
|
|
85
|
-
* the child's cwd. The path is journaled on the unit row (`worktree_path`);
|
|
86
|
-
* after the unit finishes, a clean worktree is removed and a dirty one is
|
|
87
|
-
* retained + logged (uncollected work is never destroyed). "Clean" is
|
|
88
|
-
* `git status --porcelain` WITHOUT `--ignored`, so a worktree whose only
|
|
89
|
-
* residue is `.gitignore`-matched files (build outputs, `node_modules`) counts
|
|
90
|
-
* as clean and IS removed — those files are disposable by the repo's own
|
|
91
|
-
* declaration, and retaining a worktree per build would blow up disk
|
|
92
|
-
* (`worktree.ts` contract). A non-git base directory fails the step cleanly
|
|
93
|
-
* before any dispatch, and llm units reject isolation loudly — there is no
|
|
94
|
-
* child process to isolate.
|
|
95
|
-
*
|
|
96
|
-
* Budget ceilings (addendum, R2): a frozen plan's `budget` block
|
|
97
|
-
* (`max_units` / `max_tokens`) is enforced per RUN. The engine seeds
|
|
98
|
-
* `ctx.unitsDispatched` (journal row count) and `ctx.tokensUsed` (journaled
|
|
99
|
-
* token sum) and threads the running totals across steps; this executor
|
|
100
|
-
* consumes both per ACTUAL dispatch. Hitting a ceiling aborts pending and
|
|
101
|
-
* in-flight dispatches through an AbortController chained onto `ctx.signal`
|
|
102
|
-
* and fails the step with a "budget exceeded (<which> ceiling)" summary —
|
|
103
|
-
* hard, regardless of `on_error`, exactly like the lifetime cap.
|
|
104
|
-
*
|
|
105
|
-
* Layering (see the plan's *Reconciliation* section):
|
|
106
|
-
* - Dispatch goes through ONE injected {@link UnitDispatcher} seam. The
|
|
107
|
-
* default dispatcher adapts the frozen snapshot into the common resolved
|
|
108
|
-
* request, lowers it through the registered harness/direct-LLM adapter,
|
|
109
|
-
* and reaches transport only through the central lowered-dispatch seam.
|
|
110
|
-
* - This module NEVER writes step rows: advancing the gated spine is the
|
|
111
|
-
* engine loop's job (`run-workflow.ts`) via `completeWorkflowStep`.
|
|
11
|
+
* - Data flow: instructions are the step's prose byte-exact; data reaches a
|
|
12
|
+
* unit as attached context (docs/architecture/decisions/0001-…).
|
|
13
|
+
* - An empty successful free-text output is journaled as absent, so a live
|
|
14
|
+
* run and a resume promote the same artifact.
|
|
15
|
+
* - A declared step `output` schema is validated before the step completes;
|
|
16
|
+
* a mismatch fails the step with `artifactSchemaFailure`, which the gate
|
|
17
|
+
* loop may retry.
|
|
18
|
+
* - Unit ids are content-derived (`<node>:<sha(item)>` / `<node>:solo`), so a
|
|
19
|
+
* resume reuses every completed row whatever the item order; retries and
|
|
20
|
+
* gate loops journal under `~r<n>` / `~l<n>`.
|
|
21
|
+
* - `onError: "fail"` fails on any unit failure, `"continue"` lets the gate
|
|
22
|
+
* decide; `retry: { max, on }` re-dispatches listed failure reasons.
|
|
23
|
+
* - `isolation: worktree` runs each attempt of an agent/sdk unit in a fresh
|
|
24
|
+
* detached worktree (removed when clean, kept when dirty); llm units reject it.
|
|
25
|
+
* - Run `budget` ceilings are consumed per actual dispatch and abort the step
|
|
26
|
+
* when crossed, regardless of `on_error`.
|
|
112
27
|
*/
|
|
113
|
-
import { randomUUID } from "node:crypto";
|
|
114
28
|
import { appendEvent } from "../../core/events.js";
|
|
115
29
|
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
116
30
|
import { runStructured } from "../../core/structured.js";
|
|
117
31
|
import { warn } from "../../core/warn.js";
|
|
118
32
|
import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
|
|
119
|
-
import { assertFrozenExecutableIdentity } from "../../execution/executable-identity.js";
|
|
120
33
|
import { withWorkflowRunsConnection, withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
121
|
-
import { materializeFrozenWorkflowEnvironment } from "../ir/environment-v4.js";
|
|
122
34
|
import { WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
|
|
123
|
-
// The ONE child-workflow drive
|
|
35
|
+
// The ONE child-workflow drive — publishes and drives a
|
|
124
36
|
// `child-workflow`-targeted unit; this module's dispatch seam is its only
|
|
125
37
|
// production caller.
|
|
126
38
|
import { driveChildWorkflowUnit } from "./child-workflow.js";
|
|
@@ -128,10 +40,12 @@ import { driveChildWorkflowUnit } from "./child-workflow.js";
|
|
|
128
40
|
// (exec/frozen-judge.ts). Consumers import the leaf directly — this module is
|
|
129
41
|
// not a second front door onto the seam.
|
|
130
42
|
import { collectWorkflowDispatchSensitiveValues, redactUnitOutcome } from "./dispatch-redaction.js";
|
|
43
|
+
import { materializeFrozenWorkflowEnvironment } from "./environment.js";
|
|
131
44
|
// The exec (shell) unit runner — a leaf that owns argv spawning, containment,
|
|
132
45
|
// and the process-outcome → failure-reason mapping.
|
|
133
46
|
import { runExecUnit } from "./exec-unit.js";
|
|
134
47
|
import { mergeLoweringNotices } from "./lowering-notices.js";
|
|
48
|
+
import { secretShapedParamValues } from "./param-secrets.js";
|
|
135
49
|
import { scheduleUnits } from "./scheduler.js";
|
|
136
50
|
// Shared step semantics — the ONE implementation consumed by the engine
|
|
137
51
|
// (this module + run-workflow.ts) on both the fresh-execution and the resume
|
|
@@ -142,16 +56,9 @@ import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } fro
|
|
|
142
56
|
import { enqueueUnitWrite } from "./unit-writer.js";
|
|
143
57
|
import { assertGitWorkTree, cleanupUnitWorktree, createUnitWorktree } from "./worktree.js";
|
|
144
58
|
/**
|
|
145
|
-
*
|
|
146
|
-
* (
|
|
147
|
-
*
|
|
148
|
-
* touch it — the peer-review fix that keeps large partially-completed
|
|
149
|
-
* fan-outs resumable instead of tripping a pre-batch check on
|
|
150
|
-
* `journaled + items`. Token usage accumulates per actual dispatch on top of
|
|
151
|
-
* the journal-seeded run total (reused rows' tokens are already in the
|
|
152
|
-
* seed). Check-and-increment is synchronous, so concurrent units cannot race
|
|
153
|
-
* it; crossing a declared ceiling fires `onExceeded` ONCE (the executor's
|
|
154
|
-
* chained AbortController), aborting pending and in-flight dispatches.
|
|
59
|
+
* Per-step dispatch budget against the run's `budget` ceilings, seeded from
|
|
60
|
+
* the journal and consumed per actual dispatch (never by a reused row).
|
|
61
|
+
* Crossing a ceiling fires `onExceeded` once, aborting pending dispatches.
|
|
155
62
|
*/
|
|
156
63
|
class DispatchBudget {
|
|
157
64
|
used;
|
|
@@ -199,30 +106,14 @@ class DispatchBudget {
|
|
|
199
106
|
this.onExceeded?.();
|
|
200
107
|
}
|
|
201
108
|
}
|
|
202
|
-
function classifyUnitReuse(workUnit, completedRows
|
|
203
|
-
const inputHash = workUnit.inputHash;
|
|
109
|
+
function classifyUnitReuse(workUnit, completedRows) {
|
|
204
110
|
// Scan EVERY journaled attempt row of this unit (`<base>` / `<base>~r<N>`
|
|
205
|
-
// for ANY N), not just the attempts the CURRENT retry policy allows:
|
|
206
|
-
// retry/onError are deliberately excluded from the input hash (step-work.ts)
|
|
207
|
-
// precisely so completed rows stay valid across policy changes — a run
|
|
111
|
+
// for ANY N), not just the attempts the CURRENT retry policy allows: a run
|
|
208
112
|
// re-invoked with a lowered retry.max must still find the `~rN` row a prior
|
|
209
113
|
// invocation completed beyond the new max, never re-dispatch finished work.
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const prior of completedRows?.get(workUnit.journalBaseId) ?? []) {
|
|
214
|
-
if (prior.input_hash === inputHash)
|
|
215
|
-
return { kind: "reuse", row: prior };
|
|
216
|
-
// Gate-loop rows are NOT replay-deterministic (the prompt embeds a fresh
|
|
217
|
-
// judge output): a stale loop-N row with a different hash re-dispatches
|
|
218
|
-
// live. Divergence only guards loop-1 rows, whose inputs ARE a pure
|
|
219
|
-
// function of (frozen plan, params, journaled results).
|
|
220
|
-
if (gateLoop <= 1)
|
|
221
|
-
divergedAttemptId ??= prior.unit_id;
|
|
222
|
-
}
|
|
223
|
-
if (divergedAttemptId !== undefined)
|
|
224
|
-
return { kind: "diverge", attemptId: divergedAttemptId };
|
|
225
|
-
return { kind: "dispatch" };
|
|
114
|
+
// The row's `input_hash` is informational — resume skips completed units.
|
|
115
|
+
const prior = completedRows?.get(workUnit.journalBaseId)?.[0];
|
|
116
|
+
return prior ? { kind: "reuse", row: prior } : { kind: "dispatch" };
|
|
226
117
|
}
|
|
227
118
|
function indexCompletedRows(rows) {
|
|
228
119
|
const index = new Map();
|
|
@@ -239,37 +130,16 @@ function indexCompletedRows(rows) {
|
|
|
239
130
|
return index;
|
|
240
131
|
}
|
|
241
132
|
/**
|
|
242
|
-
* Execute one step plan natively. Never throws for unit-level failures.
|
|
243
|
-
*
|
|
244
|
-
* The whole step runs inside ONE state.db connection scope
|
|
245
|
-
* ({@link withWorkflowRunsConnection}): the journal read, every unit's
|
|
246
|
-
* insert/finish transaction, and every `workflow_unit_*` event share a single
|
|
247
|
-
* handle for the step's lifetime instead of opening and closing state.db twice
|
|
248
|
-
* per unit plus twice per unit's events. The scope closes the handle when the
|
|
249
|
-
* step settles (success, failure, or throw), so there is no handle to leak and
|
|
250
|
-
* no lifetime that outlives the step. Everything inside keeps its existing
|
|
251
|
-
* transaction boundaries — see `core/state-db-scope.ts` for why sharing a
|
|
252
|
-
* handle across concurrently-scheduled units is safe here.
|
|
133
|
+
* Execute one step plan natively. Never throws for unit-level failures. The
|
|
134
|
+
* whole step shares one state.db connection scope ({@link withWorkflowRunsConnection}).
|
|
253
135
|
*/
|
|
254
136
|
export function executeStepPlan(plan, ctx) {
|
|
255
137
|
return withWorkflowRunsConnection(() => executeStepPlanInConnection(plan, ctx));
|
|
256
138
|
}
|
|
257
139
|
/**
|
|
258
|
-
* Open the step's dispatch budget and the abort signal it trips
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* dispatch runs under an AbortController CHAINED onto `ctx.signal` — hitting a
|
|
262
|
-
* ceiling aborts pending and in-flight dispatches, and the step fails hard.
|
|
263
|
-
* Without a budget the context signal passes through untouched (the no-budget
|
|
264
|
-
* path is byte-identical to pre-R2 behavior).
|
|
265
|
-
*
|
|
266
|
-
* The returned {@link DispatchBudget} is seeded with the run's journaled
|
|
267
|
-
* dispatch count and token total and consumed per ACTUAL dispatch inside
|
|
268
|
-
* `runUnit` — never for durable-row reuses, so resuming a large
|
|
269
|
-
* partially-completed fan-out works.
|
|
270
|
-
*
|
|
271
|
-
* `unchainSignal` MUST be called when dispatch finishes (the caller's
|
|
272
|
-
* `finally`) so the upstream abort listener is removed.
|
|
140
|
+
* Open the step's dispatch budget and the abort signal it trips: with a
|
|
141
|
+
* declared budget, dispatch runs under an AbortController chained onto
|
|
142
|
+
* `ctx.signal`. The caller must call `unchainSignal` when dispatch finishes.
|
|
273
143
|
*/
|
|
274
144
|
function openDispatchBudget(ctx, dispatched) {
|
|
275
145
|
const declaredBudget = ctx.budget && (ctx.budget.maxUnits !== undefined || ctx.budget.maxTokens !== undefined) ? ctx.budget : undefined;
|
|
@@ -358,13 +228,8 @@ async function prepareStepDispatchPrerequisites(input) {
|
|
|
358
228
|
}
|
|
359
229
|
async function executeStepPlanInConnection(plan, ctx) {
|
|
360
230
|
const dispatched = ctx.unitsDispatched ?? 0;
|
|
361
|
-
//
|
|
362
|
-
// the
|
|
363
|
-
// prompt, and hash its resolved input. A resume recomputes the identical list
|
|
364
|
-
// from the same frozen plan — that shared pure implementation is what lets
|
|
365
|
-
// journaled rows be matched instead of re-executed. This module owns only the
|
|
366
|
-
// impure remainder: env/worktree preflight, durable-row reuse, dispatch,
|
|
367
|
-
// journaling, budget.
|
|
231
|
+
// The work list is the shared pure decision (step-work.ts), so a resume
|
|
232
|
+
// recomputes the identical list and matches journaled rows.
|
|
368
233
|
const workList = computeStepWorkList(plan, {
|
|
369
234
|
runId: ctx.runId,
|
|
370
235
|
params: ctx.params,
|
|
@@ -384,26 +249,12 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
384
249
|
return { ...reduceEmptyStep(plan, reducer), unitsDispatched: dispatched };
|
|
385
250
|
}
|
|
386
251
|
const dispatcher = ctx.dispatcher ?? defaultUnitDispatcher;
|
|
387
|
-
//
|
|
388
|
-
// resolving env or preflighting worktrees. A unit whose previous attempt
|
|
389
|
-
// completed with the SAME input hash (the canonical envelope in step-work.ts)
|
|
390
|
-
// is reused, not re-dispatched — a crash-resume must never double-issue
|
|
391
|
-
// side-effecting work. Loading the rows up front is what lets us skip the
|
|
392
|
-
// dispatch prerequisites below when nothing will actually dispatch.
|
|
252
|
+
// Load journaled rows first: a completed unit is reused, never re-dispatched.
|
|
393
253
|
const completedRows = indexCompletedRows(await withWorkflowRunsRepo((repo) => repo.getUnitsForStep(ctx.runId, plan.stepId)));
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
// cwd is no longer a git worktree, or git is missing — none of that is needed
|
|
399
|
-
// to hand back a cached result. The predicate mirrors runUnit's reuse
|
|
400
|
-
// decision exactly (shared classifyUnitReuse).
|
|
401
|
-
const gateLoop = ctx.gateLoop ?? 1;
|
|
402
|
-
// Classify every unit ONCE. The gate below and each unit's own dispatch then
|
|
403
|
-
// read the SAME decision rather than recomputing it from inputs that must be
|
|
404
|
-
// identical — the agreement the gate depends on is structural, not a property
|
|
405
|
-
// two call sites have to keep re-establishing.
|
|
406
|
-
const reuseDecisions = workUnits.map((unit) => classifyUnitReuse(unit, completedRows, gateLoop));
|
|
254
|
+
// Env resolution and worktree preflight run only when some unit will
|
|
255
|
+
// dispatch, so a fully-journaled step resumes even if an env asset or git is
|
|
256
|
+
// gone. Every unit is classified once; the preflight and dispatch share it.
|
|
257
|
+
const reuseDecisions = workUnits.map((unit) => classifyUnitReuse(unit, completedRows));
|
|
407
258
|
const willDispatch = reuseDecisions.some((decision) => decision.kind === "dispatch");
|
|
408
259
|
const prerequisites = await prepareStepDispatchPrerequisites({
|
|
409
260
|
plan,
|
|
@@ -454,9 +305,9 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
454
305
|
await Promise.allSettled(pendingWorktreeCleanups);
|
|
455
306
|
}
|
|
456
307
|
// Capture live-only diagnostics BEFORE any hard reduction replaces the unit
|
|
457
|
-
// list with a failed-step envelope. Budget/cap
|
|
458
|
-
//
|
|
459
|
-
//
|
|
308
|
+
// list with a failed-step envelope. Budget/cap and journal-write failures
|
|
309
|
+
// must not erase notices already observed from real dispatches; durable row
|
|
310
|
+
// reuses naturally contribute none.
|
|
460
311
|
const notices = mergeLoweringNotices(...outcomes.map((outcome) => outcome?.notices));
|
|
461
312
|
// A declared budget ceiling is a hard backstop: a step that hit one FAILS
|
|
462
313
|
// regardless of on_error policy (a capped run must never quietly pass its
|
|
@@ -470,21 +321,8 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
470
321
|
failureReason: "aborted",
|
|
471
322
|
error: "unit was not dispatched (aborted or scheduler failure)",
|
|
472
323
|
});
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// run loudly (module doc), never be tolerated as "just a failed unit".
|
|
476
|
-
const diverged = units.filter((u) => u.failureReason === "replay_divergence");
|
|
477
|
-
if (diverged.length > 0) {
|
|
478
|
-
return failedStep(budget.used, diverged
|
|
479
|
-
.map((u) => u.error ?? `replay divergence: unit "${u.unitId}" was journaled with different inputs`)
|
|
480
|
-
.join(" "), notices);
|
|
481
|
-
}
|
|
482
|
-
// A journal-write failure is likewise HARD regardless of on_error: the
|
|
483
|
-
// unit dispatched (spent tokens, ran side effects) but its result could not
|
|
484
|
-
// be persisted, so completing the step would promote an artifact the
|
|
485
|
-
// journal cannot rebuild on resume — and the stuck-`running` row would
|
|
486
|
-
// wedge or double-dispatch a later invocation. The summary carries the
|
|
487
|
-
// per-unit cause verbatim.
|
|
324
|
+
// A journal-write failure fails the step regardless of on_error: the unit ran
|
|
325
|
+
// but its result could not be persisted, so no artifact can be promoted.
|
|
488
326
|
const unjournaled = units.filter((u) => u.failureReason === "journal_write_failed");
|
|
489
327
|
if (unjournaled.length > 0) {
|
|
490
328
|
return {
|
|
@@ -492,15 +330,7 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
492
330
|
tokensUsed: budget.tokens,
|
|
493
331
|
};
|
|
494
332
|
}
|
|
495
|
-
// Failure policy
|
|
496
|
-
// post-dispatch decision (`reduceStepOutcomes`): `onError: "fail"` (default)
|
|
497
|
-
// fails the step on any unit failure, `"continue"` records failures and lets
|
|
498
|
-
// the gate decide, a vote reducer with no majority fails under either policy,
|
|
499
|
-
// and the promoted artifact is validated against the step's declared output
|
|
500
|
-
// schema (fail-fast; the `artifactSchemaFailure` marker lets the bounded gate
|
|
501
|
-
// loop retry that ONE failure class with the errors as feedback). The report
|
|
502
|
-
// path (R3) reduces journal-replayed outcomes through the same function, so a
|
|
503
|
-
// step promotes the SAME artifact whichever surface drove it.
|
|
333
|
+
// Failure policy, reducer, and artifact schema check: the shared `reduceStepOutcomes`.
|
|
504
334
|
const reduced = reduceStepOutcomes(plan, reducer, isFanOut, template.onError, units);
|
|
505
335
|
return {
|
|
506
336
|
...reduced,
|
|
@@ -539,9 +369,9 @@ async function runUnit(input) {
|
|
|
539
369
|
...(env ? { env } : {}),
|
|
540
370
|
...(sensitiveValues ? { sensitiveValues } : {}),
|
|
541
371
|
...(input.signal ? { signal: input.signal } : {}),
|
|
542
|
-
// F-1
|
|
372
|
+
// F-1: forwarded to exec-unit.ts's childEnv for a
|
|
543
373
|
// "script"/"shell" unit, and to dispatchWorkflowExecution's
|
|
544
|
-
//
|
|
374
|
+
// runExecution eventSource option (unit-dispatch.ts)
|
|
545
375
|
// for a "command" unit — both arms observe it.
|
|
546
376
|
...(ctx.eventSource !== undefined ? { eventSource: ctx.eventSource } : {}),
|
|
547
377
|
};
|
|
@@ -553,14 +383,9 @@ async function runUnit(input) {
|
|
|
553
383
|
const attemptIdFor = (_attempt) => journalBaseId;
|
|
554
384
|
// Durable-row reuse — literally the decision executeStepPlan's preflight gate
|
|
555
385
|
// counted, handed down rather than recomputed, so the gate cannot disagree
|
|
556
|
-
// with what happens here. A completed row
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
// a DIFFERENT hash is replay divergence (under a frozen plan the same
|
|
560
|
-
// content-derived identity must reproduce the same inputs — the journal was
|
|
561
|
-
// tampered with; executeStepPlan promotes this to a hard step failure
|
|
562
|
-
// regardless of on_error). Stale gate-loop rows, failed/running/missing rows,
|
|
563
|
-
// and pre-release R1 positional ids all fall through and dispatch live.
|
|
386
|
+
// with what happens here. A completed row IS the result: return it without
|
|
387
|
+
// touching rows, dispatching, or re-emitting events (a crash-resume must
|
|
388
|
+
// never double-issue work). Failed/running/missing rows dispatch live.
|
|
564
389
|
const reuse = input.reuse;
|
|
565
390
|
if (reuse.kind === "reuse") {
|
|
566
391
|
// Identity in the durable step evidence is the CONTENT-derived base id, not
|
|
@@ -568,15 +393,6 @@ async function runUnit(input) {
|
|
|
568
393
|
// base ids, so evidence.units[].unitId stays stable across retries+resumes.
|
|
569
394
|
return reuseCompletedUnit(unitId, reuse.row, workUnit.schema !== undefined);
|
|
570
395
|
}
|
|
571
|
-
if (reuse.kind === "diverge") {
|
|
572
|
-
return {
|
|
573
|
-
unitId,
|
|
574
|
-
ok: false,
|
|
575
|
-
failureReason: "replay_divergence",
|
|
576
|
-
error: `replay divergence: unit "${reuse.attemptId}" was journaled with different inputs ` +
|
|
577
|
-
`(journaled input_hash does not match this invocation's) — refusing to re-dispatch.`,
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
396
|
let outcome;
|
|
581
397
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
582
398
|
if (input.signal?.aborted) {
|
|
@@ -614,7 +430,7 @@ async function runUnit(input) {
|
|
|
614
430
|
// Attempts use `~r<n>` journal suffixes while durable step evidence remains
|
|
615
431
|
// attached to the content-derived base identity.
|
|
616
432
|
outcome.unitId = unitId;
|
|
617
|
-
// Budget token accounting
|
|
433
|
+
// Budget token accounting: every actual dispatch's reported
|
|
618
434
|
// usage counts against the run's max_tokens ceiling; crossing it aborts
|
|
619
435
|
// pending dispatches via the chained controller. Reuses never reach here
|
|
620
436
|
// (their tokens are already in the journal-seeded total).
|
|
@@ -632,47 +448,17 @@ async function runUnit(input) {
|
|
|
632
448
|
return outcome;
|
|
633
449
|
}
|
|
634
450
|
/**
|
|
635
|
-
* What
|
|
636
|
-
* `
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
* A SUCCESS journals its promoted value, unchanged.
|
|
641
|
-
*
|
|
642
|
-
* A FAILURE journals its DIAGNOSTIC. Before this, only `result`/`text` were
|
|
643
|
-
* written: `outcome.error` — the one field that says WHY — reached nothing
|
|
644
|
-
* durable, because `buildEvidence` deliberately drops it from the deterministic
|
|
645
|
-
* evidence graph and nothing else persisted it. For an engine unit that mostly
|
|
646
|
-
* cost detail; for an `exec` unit it lost the diagnostic entirely, since a
|
|
647
|
-
* command that fails and explains itself on stderr with empty stdout left
|
|
648
|
-
* `status --units` showing a bare `non_zero_exit`.
|
|
649
|
-
*
|
|
650
|
-
* Three constraints hold:
|
|
651
|
-
*
|
|
652
|
-
* - REDACTION — the caller journals only `redactUnitOutcome(...)` output, so
|
|
653
|
-
* `error` has already been through the shared dispatch redaction contract
|
|
654
|
-
* (`exec/dispatch-redaction.ts`) with this dispatch's resolved `env:`
|
|
655
|
-
* values. It is scrubbed by construction, exactly like `text`.
|
|
656
|
-
* - BOUNDS — clipped to {@link WORKFLOW_UNIT_DIAGNOSTIC_CLIP}, the same bound
|
|
657
|
-
* `status --units` renders with, so a runaway command cannot use the journal
|
|
658
|
-
* as its log file.
|
|
659
|
-
* - HASHES — `result_json` is an OUTPUT. The unit input hash
|
|
660
|
-
* (`computeUnitInputHash`) is computed from plan-frozen INPUTS only
|
|
661
|
-
* (template bytes, item, declared inputs, params, dispatch/invocation/exec
|
|
662
|
-
* snapshots, env ref names, isolation), and reuse compares the stored
|
|
663
|
-
* `input_hash` against that. Nothing here is a hash preimage input, so no
|
|
664
|
-
* completed unit re-dispatches because of it.
|
|
665
|
-
*
|
|
666
|
-
* Partial output on a failed unit is kept ALONGSIDE the diagnostic rather than
|
|
667
|
-
* replacing it: a tool that fails after printing its real complaint on stdout
|
|
668
|
-
* is common, and the reason lives on whichever stream that tool chose.
|
|
451
|
+
* What an attempt journals as the unit row's `result_json` (what `status
|
|
452
|
+
* --units` shows): a success's promoted value, or a failure's diagnostic plus
|
|
453
|
+
* any partial output. The caller passes an already-redacted outcome; clipped
|
|
454
|
+
* to {@link WORKFLOW_UNIT_DIAGNOSTIC_CLIP}.
|
|
669
455
|
*/
|
|
670
456
|
function journaledUnitResultJson(outcome) {
|
|
671
457
|
if (outcome.result !== undefined)
|
|
672
458
|
return JSON.stringify(outcome.result);
|
|
673
459
|
if (outcome.ok)
|
|
674
460
|
return outcome.text ? JSON.stringify(outcome.text) : null;
|
|
675
|
-
const parts = [outcome.error, outcome.text].filter((part) => Boolean(part
|
|
461
|
+
const parts = [outcome.error, outcome.text].filter((part) => Boolean(part?.trim()));
|
|
676
462
|
if (parts.length === 0)
|
|
677
463
|
return null;
|
|
678
464
|
return JSON.stringify(clip(parts.join("\n--- unit output ---\n"), WORKFLOW_UNIT_DIAGNOSTIC_CLIP));
|
|
@@ -681,15 +467,7 @@ async function prepareAttemptWorktree(input) {
|
|
|
681
467
|
if (input.worktreeBase === undefined)
|
|
682
468
|
return { ok: true, request: input.request };
|
|
683
469
|
const created = await createUnitWorktree(input.worktreeBase, input.ctx.runId, input.attemptId,
|
|
684
|
-
// A child-workflow target (
|
|
685
|
-
// its own — it is a composition target, never a worktree-isolated exec
|
|
686
|
-
// one. This arm IS reachable — a step that composes a child workflow and
|
|
687
|
-
// also declares `isolation: worktree` gets a worktree prepared here
|
|
688
|
-
// (worktree prep runs ahead of dispatch), but the child executor
|
|
689
|
-
// (child-workflow.ts, P3b §3.2) never dispatches through it: driving a
|
|
690
|
-
// child publishes and drives a RUN, not a command/exec unit, so the
|
|
691
|
-
// prepared worktree is simply unused by the drive. This ternary keeps the
|
|
692
|
-
// field access total over the frozen-target union either way.
|
|
470
|
+
// A child-workflow target has no commit of its own (its prepared worktree goes unused).
|
|
693
471
|
input.workUnit.frozenTarget.kind === "child-workflow" ? undefined : input.workUnit.frozenTarget.gitCommitOid);
|
|
694
472
|
if (created.preservedLeftover !== undefined) {
|
|
695
473
|
warn(`Workflow unit ${input.attemptId}: a previous attempt left uncollected work in its isolation worktree; ` +
|
|
@@ -714,8 +492,7 @@ async function reserveJournaledDispatch(input, worktreePath, startedAt) {
|
|
|
714
492
|
await enqueueUnitWrite(async () => {
|
|
715
493
|
await withWorkflowRunsRepo((repo) => {
|
|
716
494
|
const target = workUnit.frozenTarget;
|
|
717
|
-
|
|
718
|
-
const reserved = repo.reserveUnitAttempt({
|
|
495
|
+
durableAttempt = repo.reserveUnitAttempt({
|
|
719
496
|
runId: ctx.runId,
|
|
720
497
|
unitId: attemptId,
|
|
721
498
|
stepId: plan.stepId,
|
|
@@ -727,15 +504,8 @@ async function reserveJournaledDispatch(input, worktreePath, startedAt) {
|
|
|
727
504
|
model: target.kind === "command" ? (target.request.model?.resolved ?? null) : null,
|
|
728
505
|
inputHash,
|
|
729
506
|
worktreePath: worktreePath ?? null,
|
|
730
|
-
claimHolder: holder,
|
|
731
|
-
claimExpiresAt: new Date(Date.parse(startedAt) + 90_000).toISOString(),
|
|
732
507
|
now: startedAt,
|
|
733
|
-
|
|
734
|
-
});
|
|
735
|
-
if (reserved.kind === "busy") {
|
|
736
|
-
throw new Error(`unit "${attemptId}" already has a live durable attempt held by ${reserved.attempt.claim_holder}`);
|
|
737
|
-
}
|
|
738
|
-
durableAttempt = reserved.attempt;
|
|
508
|
+
}).attempt;
|
|
739
509
|
});
|
|
740
510
|
});
|
|
741
511
|
if (!durableAttempt)
|
|
@@ -751,7 +521,6 @@ async function finishJournaledDispatch(input) {
|
|
|
751
521
|
unitId: attemptId,
|
|
752
522
|
attempt: durableAttempt.attempt,
|
|
753
523
|
dispatchId: durableAttempt.dispatch_id,
|
|
754
|
-
claimHolder: durableAttempt.claim_holder,
|
|
755
524
|
status: outcome.ok ? "completed" : "failed",
|
|
756
525
|
resultJson: journaledUnitResultJson(outcome),
|
|
757
526
|
tokens: outcome.tokens ?? null,
|
|
@@ -764,8 +533,8 @@ async function finishJournaledDispatch(input) {
|
|
|
764
533
|
throw new Error(`finishUnitAttempt updated no row: no durable attempt "${attemptId}" exists for run "${ctx.runId}".`);
|
|
765
534
|
}
|
|
766
535
|
warn(`Workflow unit ${attemptId} (run ${ctx.runId}) ${outcome.ok ? "completed" : `failed (${outcome.failureReason ?? "error"})`}, ` +
|
|
767
|
-
|
|
768
|
-
|
|
536
|
+
"but its durable attempt was already finished — refusing a duplicate terminal write. " +
|
|
537
|
+
"This dispatch's result is not journaled.");
|
|
769
538
|
}
|
|
770
539
|
}));
|
|
771
540
|
}
|
|
@@ -821,17 +590,8 @@ async function dispatchJournaledAttempt(input) {
|
|
|
821
590
|
attempt: durableAttempt.attempt,
|
|
822
591
|
dispatchId: durableAttempt.dispatch_id,
|
|
823
592
|
};
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
// instead (src/workflows/exec/child-workflow.ts), which publishes the
|
|
827
|
-
// child idempotently and drives it with the SAME engine
|
|
828
|
-
// (`runWorkflowSteps`) the top-level path uses. Placed HERE — after
|
|
829
|
-
// `reserveJournaledDispatch` claims this attempt row, before
|
|
830
|
-
// `finishJournaledDispatch`/the worktree epilogue below — so a
|
|
831
|
-
// child-workflow unit is journaled exactly like any other unit, and a
|
|
832
|
-
// crash between reservation and child publication leaves a `running`
|
|
833
|
-
// parent row with no child, recovered by resume (which re-dispatches the
|
|
834
|
-
// parent unit and republishes the child idempotently).
|
|
593
|
+
// A child-workflow unit goes to the child executor instead of the
|
|
594
|
+
// dispatcher, after its attempt row is reserved, so it journals like any unit.
|
|
835
595
|
const dispatched = request.frozenTarget.kind === "child-workflow"
|
|
836
596
|
? await driveChildWorkflowUnit({
|
|
837
597
|
request,
|
|
@@ -845,23 +605,21 @@ async function dispatchJournaledAttempt(input) {
|
|
|
845
605
|
// Credential and passthrough values are intentionally sampled only AFTER
|
|
846
606
|
// the default dispatcher has authorized/lowered the frozen request and
|
|
847
607
|
// materialized credentials at its terminal dispatch boundary. Custom test
|
|
848
|
-
// dispatchers receive the same post-dispatch journal scrub.
|
|
608
|
+
// dispatchers receive the same post-dispatch journal scrub. Secret-shaped
|
|
609
|
+
// run params join the set: they must reach the prompt in clear (the unit
|
|
610
|
+
// needs them), but nothing about them needs to reach the journal.
|
|
849
611
|
const sensitiveValues = collectWorkflowDispatchSensitiveValues({
|
|
850
612
|
...(request.frozenTarget.kind === "command" ? { runner: request.frozenTarget.runner } : {}),
|
|
851
|
-
...(request.sensitiveValues
|
|
613
|
+
sensitiveValues: [...(request.sensitiveValues ?? []), ...secretShapedParamValues(ctx.params)],
|
|
852
614
|
}, request.env);
|
|
853
615
|
const outcome = redactUnitOutcome(dispatched, sensitiveValues);
|
|
854
616
|
const finishedAt = new Date().toISOString();
|
|
855
|
-
// A dispatched unit's outcome is NEVER silently discarded
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
// dropping it would leave the row `running` and make a later resume
|
|
862
|
-
// re-dispatch side-effecting work that already ran and already spent tokens.
|
|
863
|
-
// Persisting a unit result never advances the run; spine advancement stays
|
|
864
|
-
// lease-guarded in completeWorkflowStep.
|
|
617
|
+
// A dispatched unit's outcome is NEVER silently discarded: the attempt is
|
|
618
|
+
// finished with the real result even when the run went non-active
|
|
619
|
+
// mid-flight — dropping it would leave the row `running` and make a later
|
|
620
|
+
// resume re-dispatch side-effecting work that already ran and already spent
|
|
621
|
+
// tokens. Persisting a unit result never advances the run; that is
|
|
622
|
+
// completeWorkflowStep's job.
|
|
865
623
|
let journalError;
|
|
866
624
|
try {
|
|
867
625
|
await finishJournaledDispatch({
|
|
@@ -874,14 +632,8 @@ async function dispatchJournaledAttempt(input) {
|
|
|
874
632
|
catch (err) {
|
|
875
633
|
journalError = err;
|
|
876
634
|
}
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
// worktree_path says where. Cleanup is best-effort observability, never a
|
|
880
|
-
// unit failure, so it is STARTED here and awaited at the step barrier: the
|
|
881
|
-
// removal serializes on the same per-repo chain as every sibling's
|
|
882
|
-
// `git worktree add`, and awaiting it in this unit's scheduler slot made a
|
|
883
|
-
// finished unit wait out other units' full checkouts before its worker could
|
|
884
|
-
// claim the next item.
|
|
635
|
+
// A clean worktree is removed, a dirty one kept and logged. Cleanup starts
|
|
636
|
+
// here and is awaited at the step barrier, never holding this scheduler slot.
|
|
885
637
|
queueAttemptWorktreeCleanup(input, worktreePath);
|
|
886
638
|
// A journal-write failure AFTER a successful dispatch is its own loud
|
|
887
639
|
// failure class: the unit's work ran (and may have succeeded), but its
|
|
@@ -938,7 +690,7 @@ async function dispatchUnit(request, dispatcher) {
|
|
|
938
690
|
let tokens = 0;
|
|
939
691
|
let sawUsage = false;
|
|
940
692
|
let loweringNotices;
|
|
941
|
-
// Harness-native session id revealed by dispatch
|
|
693
|
+
// Harness-native session id revealed by dispatch. Captured across
|
|
942
694
|
// structured-output retries (last one wins) so it survives into the
|
|
943
695
|
// UnitOutcome and gets journaled by finishUnitAttempt — the seam's
|
|
944
696
|
// contract ("stored opportunistically on the unit row for resume").
|
|
@@ -1046,9 +798,6 @@ export const defaultUnitDispatcher = async (request, feedback) => {
|
|
|
1046
798
|
const frozenTarget = request.frozenTarget;
|
|
1047
799
|
if (frozenTarget.kind === "script") {
|
|
1048
800
|
assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
|
|
1049
|
-
if (frozenTarget.executable) {
|
|
1050
|
-
assertFrozenExecutableIdentity(frozenTarget.executable, `unit ${request.unitId} executable`);
|
|
1051
|
-
}
|
|
1052
801
|
const materialized = materializeFrozenScript({
|
|
1053
802
|
sourceRef: frozenTarget.ref,
|
|
1054
803
|
interpreter: frozenTarget.interpreter,
|
|
@@ -1066,8 +815,6 @@ export const defaultUnitDispatcher = async (request, feedback) => {
|
|
|
1066
815
|
byteLength: frozenTarget.byteLength,
|
|
1067
816
|
sha256: frozenTarget.contentHash,
|
|
1068
817
|
}, materialized.file);
|
|
1069
|
-
if (frozenTarget.executable)
|
|
1070
|
-
command[0] = frozenTarget.executable.absolutePath;
|
|
1071
818
|
return await runExecUnit({
|
|
1072
819
|
unitId: request.unitId,
|
|
1073
820
|
exec: {
|
|
@@ -1090,15 +837,9 @@ export const defaultUnitDispatcher = async (request, feedback) => {
|
|
|
1090
837
|
if (frozenTarget.kind === "shell") {
|
|
1091
838
|
if (frozenTarget.cwdIdentity)
|
|
1092
839
|
assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
|
|
1093
|
-
if (frozenTarget.executable) {
|
|
1094
|
-
assertFrozenExecutableIdentity(frozenTarget.executable, `unit ${request.unitId} executable`);
|
|
1095
|
-
}
|
|
1096
|
-
const command = [...frozenTarget.exec.command];
|
|
1097
|
-
if (frozenTarget.executable)
|
|
1098
|
-
command[0] = frozenTarget.executable.absolutePath;
|
|
1099
840
|
return runExecUnit({
|
|
1100
841
|
unitId: request.unitId,
|
|
1101
|
-
exec:
|
|
842
|
+
exec: frozenTarget.exec,
|
|
1102
843
|
baseDir: request.cwd ?? frozenTarget.cwdIdentity?.realCwd ?? process.cwd(),
|
|
1103
844
|
...(request.env ? { env: request.env } : {}),
|
|
1104
845
|
...(request.execContext ? { context: request.execContext } : {}),
|