akm-cli 0.9.17-alpha.2 → 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 +756 -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 +363 -679
- 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 -427
- 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
|
@@ -3,17 +3,15 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { ConfigError } from "../../core/errors.js";
|
|
5
5
|
import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { canonicalResolvedExecutionRequest } from "../../execution/resolved-request.js";
|
|
7
|
+
import { buildExecutionFromWire } from "../../integrations/agent/execution.js";
|
|
8
|
+
import { runExecution } from "../../integrations/agent/runner-dispatch.js";
|
|
9
9
|
import { getHarness } from "../../integrations/harnesses/index.js";
|
|
10
|
-
/** Lower a
|
|
10
|
+
/** Lower a frozen common request through its frozen runner only. */
|
|
11
11
|
export function prepareWorkflowExecution(request, prompt = request.prompt) {
|
|
12
12
|
const target = request.frozenTarget;
|
|
13
13
|
if (target.cwdIdentity)
|
|
14
14
|
assertFrozenDirectoryContained(target.cwdIdentity);
|
|
15
|
-
if (target.executable)
|
|
16
|
-
assertFrozenExecutableIdentity(target.executable, `unit ${request.unitId} executable`);
|
|
17
15
|
const wire = JSON.parse(canonicalResolvedExecutionRequest(target.request));
|
|
18
16
|
const command = { ...wire.command, content: prompt };
|
|
19
17
|
const runtime = {
|
|
@@ -30,42 +28,15 @@ export function prepareWorkflowExecution(request, prompt = request.prompt) {
|
|
|
30
28
|
if (request.systemPrompt !== undefined) {
|
|
31
29
|
wire.conversation = [{ role: "system", content: request.systemPrompt }];
|
|
32
30
|
}
|
|
33
|
-
return
|
|
31
|
+
return buildExecutionFromWire({ request: wire, runner: target.runner });
|
|
34
32
|
}
|
|
35
33
|
function message(err) {
|
|
36
34
|
return err instanceof Error ? err.message : String(err);
|
|
37
35
|
}
|
|
38
36
|
/**
|
|
39
|
-
* The `eventSource`
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* Precedence fix (P1b Lane C code review, round 2). The gap-fix originally
|
|
44
|
-
* forwarded `request.eventSource` unconditionally.
|
|
45
|
-
* `dispatchLoweredExecutionRequest` applies a forwarded value as `env: {
|
|
46
|
-
* ...lowered.options.env, AKM_EVENT_SOURCE: eventSource }`
|
|
47
|
-
* (execution-lowering.ts:998-1001) — an unconditional override of that one
|
|
48
|
-
* key — and `lowered.options.env` IS the unit's own authored/resolved `env:`
|
|
49
|
-
* binding (`request.env`, folded in by `prepareWorkflowExecution` above via
|
|
50
|
-
* `request.runtime.environment`), so the unconditional forward let the
|
|
51
|
-
* provenance stamp win over an authored `env: { AKM_EVENT_SOURCE: ... }`
|
|
52
|
-
* binding. That inverts the precedence pre-P1b had (the child env was built
|
|
53
|
-
* from ambient passthrough with `options.env` — the authored binding —
|
|
54
|
-
* applied AFTER it, at highest precedence, in `buildChildEnv`/`spawn.ts`) and
|
|
55
|
-
* disagrees with the sibling "script"/"shell" arm: `exec-unit.ts`'s own
|
|
56
|
-
* `childEnv` stamps its allowlisted base only when the name is absent there,
|
|
57
|
-
* strictly BEFORE the bindings overlay runs, so an authored binding always
|
|
58
|
-
* wins there. Gating the forward on `request.env` not already binding the
|
|
59
|
-
* name restores agreement: an authored binding leaves `eventSource`
|
|
60
|
-
* unforwarded (so the merge above never touches the key, and the authored
|
|
61
|
-
* value in `lowered.options.env` stands), while an absent binding still
|
|
62
|
-
* forwards the resolved value exactly as before.
|
|
63
|
-
*
|
|
64
|
-
* Exported so this precedence rule is pinned directly:
|
|
65
|
-
* `dispatchWorkflowExecution` itself has no injectable
|
|
66
|
-
* `runAgent`/`executeRunner`/`chat` seam to exercise the decision end-to-end
|
|
67
|
-
* without a live agent/LLM dispatch (see the P1b spec's Review log, which
|
|
68
|
-
* records the same constraint for the gap-fix this corrects).
|
|
37
|
+
* The `eventSource` to forward into `runExecution`, or undefined when the
|
|
38
|
+
* unit's own `env:` already binds `AKM_EVENT_SOURCE` — an authored binding
|
|
39
|
+
* wins, as it does for exec units.
|
|
69
40
|
*/
|
|
70
41
|
export function forwardedDispatchEventSource(request) {
|
|
71
42
|
if (request.eventSource === undefined)
|
|
@@ -80,20 +51,8 @@ export function forwardedDispatchEventSource(request) {
|
|
|
80
51
|
*/
|
|
81
52
|
export async function dispatchWorkflowExecution(request, feedback) {
|
|
82
53
|
const prompt = feedback ? `${request.prompt}\n\n${feedback}` : request.prompt;
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// (native-executor.ts, P3b §3.2) routes a `child-workflow` unit to the child
|
|
86
|
-
// executor (child-workflow.ts) BEFORE dispatch is ever reached, so arriving
|
|
87
|
-
// HERE with one means that seam was BYPASSED — an engine routing bug, never
|
|
88
|
-
// a not-yet-implemented feature (that premise, P3a Review log R8's, is gone
|
|
89
|
-
// now that P3b ships a production caller). A plain `Error` naming the seam
|
|
90
|
-
// that should have been reached instead, not a `UsageError`: nothing a user
|
|
91
|
-
// can author reaches this line once the seam exists, so there is no
|
|
92
|
-
// user-facing code to carry. Kept here, rather than deleted outright, so a
|
|
93
|
-
// bypassed seam still fails closed instead of falling into the generic
|
|
94
|
-
// `kind !== "command"` guard below, which would blame a legitimate target
|
|
95
|
-
// kind as "not a command target" — the exact false, unhelpful message R8
|
|
96
|
-
// was opened to remove.
|
|
54
|
+
// Internal invariant: native-executor routes child-workflow units to the
|
|
55
|
+
// child executor before dispatch, so reaching here is an engine bug.
|
|
97
56
|
if (request.frozenTarget.kind === "child-workflow") {
|
|
98
57
|
throw new Error(`unit ${JSON.stringify(request.unitId)} targets child workflow ${JSON.stringify(request.frozenTarget.ref)}, ` +
|
|
99
58
|
"but reached dispatchWorkflowExecution directly. The child-workflow dispatch seam " +
|
|
@@ -128,23 +87,12 @@ export async function dispatchWorkflowExecution(request, feedback) {
|
|
|
128
87
|
let result;
|
|
129
88
|
try {
|
|
130
89
|
const eventSource = forwardedDispatchEventSource(request);
|
|
131
|
-
result = await
|
|
90
|
+
result = await runExecution(lowered, {
|
|
132
91
|
runOptions: {
|
|
133
92
|
stdio: "captured",
|
|
134
93
|
parseOutput: "text",
|
|
135
94
|
...(request.signal ? { signal: request.signal } : {}),
|
|
136
95
|
},
|
|
137
|
-
// Gap fix (P1b Lane C code review, spec §5.2(2)); precedence-gated
|
|
138
|
-
// (round 2, see forwardedDispatchEventSource above): forward the
|
|
139
|
-
// resolved provenance event source so an "agent"/"sdk" unit's
|
|
140
|
-
// dispatched child env carries AKM_EVENT_SOURCE too, not only a
|
|
141
|
-
// "script"/"shell" unit's — but only when the unit's own authored
|
|
142
|
-
// `env:` binding does not already set the name, so an authored binding
|
|
143
|
-
// still wins, mirroring exec-unit.ts's childEnv guard.
|
|
144
|
-
// dispatchLoweredExecutionRequest applies a forwarded value as exactly
|
|
145
|
-
// one child-env key (execution-lowering.ts:998-1001) — the same
|
|
146
|
-
// mechanism the R-07 command-arm fix (command-execution.ts) already
|
|
147
|
-
// uses.
|
|
148
96
|
...(eventSource !== undefined ? { eventSource } : {}),
|
|
149
97
|
});
|
|
150
98
|
}
|
|
@@ -2,62 +2,18 @@
|
|
|
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
|
-
* Serialized writer queue for `workflow_run_units
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `withWorkflowRunsConnection` scope, `withWorkflowRunsRepo` opens a fresh
|
|
12
|
-
* connection per call, so N units completing at once would be N separate
|
|
13
|
-
* connections racing for the same file's write lock, burning the 30 s
|
|
14
|
-
* `busy_timeout` on contention we created ourselves. Bun runs a single-threaded
|
|
15
|
-
* event loop, so an in-process promise chain is a sufficient (and free)
|
|
16
|
-
* admission control: every unit write executes strictly in enqueue order.
|
|
17
|
-
*
|
|
18
|
-
* That is the ONLY invariant the chain owns. In particular it does NOT own
|
|
19
|
-
* per-unit reserve→finish ordering: `dispatchJournaledAttempt` awaits its reservation
|
|
20
|
-
* before it dispatches and only finishes after the dispatch resolves, so a
|
|
21
|
-
* unit's own writes are ordered by program order, not by queue position. The
|
|
22
|
-
* queue could be reordered arbitrarily between units without breaking the
|
|
23
|
-
* attempt claim/dispatch CAS.
|
|
24
|
-
*
|
|
25
|
-
* ## Scope of the serialization (issue B)
|
|
26
|
-
*
|
|
27
|
-
* The chain is keyed by DATABASE PATH, not global, because the resource being
|
|
28
|
-
* protected is one SQLite file's write lock. Two runs against two different
|
|
29
|
-
* data dirs (`AKM_DATA_DIR` isolation, a test sandbox, a second stash) share no
|
|
30
|
-
* write lock and must not queue behind each other. Narrowing further — per run
|
|
31
|
-
* — would be unsound: two runs in the SAME state.db still contend for that one
|
|
32
|
-
* file's writer, which is precisely what the chain exists to avoid.
|
|
33
|
-
*
|
|
34
|
-
* Inside a `withWorkflowRunsConnection` scope the chain also becomes nearly
|
|
35
|
-
* free: every write goes through the same handle, so each queued task is a
|
|
36
|
-
* synchronous statement (or a synchronous `BEGIN IMMEDIATE … COMMIT`) that
|
|
37
|
-
* settles in microseconds rather than a per-call open/migrate-preflight/close.
|
|
38
|
-
* The queue depth stops gating unit completion in practice, without weakening
|
|
39
|
-
* anything.
|
|
40
|
-
*
|
|
41
|
-
* Reads and gate evaluation stay OFF this queue — only writes serialize.
|
|
42
|
-
*
|
|
43
|
-
* A failed write rejects its own caller but never wedges its chain.
|
|
5
|
+
* Serialized writer queue for `workflow_run_units`. SQLite allows one writer
|
|
6
|
+
* per database file, so unit writes are chained per state.db path (an
|
|
7
|
+
* in-process promise chain suffices on Bun's single thread) instead of N
|
|
8
|
+
* connections racing for the lock. A unit's own reserve→finish order comes
|
|
9
|
+
* from program order, not the queue. Reads stay off it; a failed write rejects
|
|
10
|
+
* its caller without wedging the chain.
|
|
44
11
|
*/
|
|
45
12
|
import { serializeByKey } from "../../core/concurrent.js";
|
|
46
13
|
import { getStateDbPath } from "../../core/state-db.js";
|
|
47
|
-
/**
|
|
48
|
-
* One promise chain per database path ({@link serializeByKey}). Entries are
|
|
49
|
-
* pruned when their chain drains, so a long-lived process that touches many
|
|
50
|
-
* data dirs (the test harness swaps `AKM_DATA_DIR` per test) does not
|
|
51
|
-
* accumulate one resolved promise per path forever.
|
|
52
|
-
*/
|
|
14
|
+
/** One promise chain per database path, pruned when it drains ({@link serializeByKey}). */
|
|
53
15
|
const chains = new Map();
|
|
54
|
-
/**
|
|
55
|
-
* Enqueue a `workflow_run_units` write behind every write already queued for
|
|
56
|
-
* the same state.db. The key is the current state.db path — the file whose one
|
|
57
|
-
* write lock this exists to protect — and is not a caller's choice: a caller
|
|
58
|
-
* writing somewhere else is not on this queue at all, and wants
|
|
59
|
-
* {@link serializeByKey} with its own chain map.
|
|
60
|
-
*/
|
|
16
|
+
/** Enqueue a `workflow_run_units` write behind every write already queued for the current state.db. */
|
|
61
17
|
export function enqueueUnitWrite(fn) {
|
|
62
18
|
return serializeByKey(chains, getStateDbPath(), fn);
|
|
63
19
|
}
|
|
@@ -2,102 +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
|
-
* Git worktree lifecycle for `isolation: worktree` units
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* tmp directory, so concurrent units can never trample each other's working
|
|
9
|
-
* tree. Lifecycle (driven by the native executor per journaled attempt):
|
|
5
|
+
* Git worktree lifecycle for `isolation: worktree` units: each attempt of a
|
|
6
|
+
* file-mutating agent/sdk unit gets a fresh detached worktree of the run's base
|
|
7
|
+
* repository under `<tmp>/akm-worktrees/<runId>/<attemptId>`.
|
|
10
8
|
*
|
|
11
|
-
* 1. {@link assertGitWorkTree} —
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* DIRTY → it is RETAINED (the caller logs the path) so uncollected work
|
|
20
|
-
* is never destroyed.
|
|
21
|
-
* 4. {@link sweepStaleWorktrees} — opportunistic, at most once per process:
|
|
22
|
-
* an age-based GC of run roots and retained trees that outlived their run.
|
|
23
|
-
* Age alone cannot see a unit that is still running in ANOTHER process, so
|
|
24
|
-
* every live worktree carries a liveness lease (pid + host + path) in
|
|
25
|
-
* git's own administrative directory for it, and the sweep skips a tree
|
|
26
|
-
* whose lease holder is still running.
|
|
9
|
+
* 1. {@link assertGitWorkTree} — once per step, before dispatch.
|
|
10
|
+
* 2. {@link createUnitWorktree} — `git worktree add --detach`; the path is
|
|
11
|
+
* journaled on the unit row and becomes the unit's cwd.
|
|
12
|
+
* 3. {@link cleanupUnitWorktree} — a clean tree is removed, a dirty one kept.
|
|
13
|
+
* "Clean" ignores `.gitignore`d files (build outputs, `node_modules`),
|
|
14
|
+
* which the repo already declares disposable.
|
|
15
|
+
* 4. {@link sweepStaleWorktrees} — at most once per process, removes trees
|
|
16
|
+
* and run roots older than a week.
|
|
27
17
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* as `node_modules`/`dist`). Those ignored files are DISPOSABLE BY DEFINITION
|
|
33
|
-
* — the repository already declares them regenerable — so a worktree whose only
|
|
34
|
-
* residue is ignored files probes clean and IS removed. This is deliberate:
|
|
35
|
-
* adding `--ignored` would retain a worktree after essentially every unit that
|
|
36
|
-
* ran a package install or a build (the ignored `node_modules`/`dist` tree),
|
|
37
|
-
* blowing up disk under the run-scoped tmp root. Work a unit needs preserved
|
|
38
|
-
* must therefore be tracked or untracked-unignored; anything the workflow
|
|
39
|
-
* repo has chosen to `.gitignore` is treated as throwaway.
|
|
40
|
-
*
|
|
41
|
-
* Concurrency (bug 6). `git worktree add|prune|remove` mutate the base repo's
|
|
42
|
-
* administrative state (`.git/worktrees/*`) under repo-level locks, so a map
|
|
43
|
-
* step running N isolated units at once used to have N of them racing on the
|
|
44
|
-
* same repository. Two invariants close that:
|
|
45
|
-
*
|
|
46
|
-
* • every repo-mutating operation runs inside {@link withRepoWorktreeLock},
|
|
47
|
-
* a promise chain keyed by the resolved base repo path (`serializeByKey`
|
|
48
|
-
* in `core/concurrent.ts`, shared with `unit-writer.ts` — Bun is
|
|
49
|
-
* single-threaded, so an in-process chain is sufficient), so at most one
|
|
50
|
-
* add/prune/remove per repository is ever in flight;
|
|
51
|
-
* • those git calls are ASYNC ({@link runManagedSubprocess}) rather than
|
|
52
|
-
* `spawnSync`, so a unit waiting on a git lock parks a promise instead of
|
|
53
|
-
* wedging the whole event loop (and with it every other in-flight unit,
|
|
54
|
-
* the lease heartbeat, and abort handling).
|
|
55
|
-
*
|
|
56
|
-
* The two sync git shell-outs that remain — {@link isGitAvailable} and
|
|
57
|
-
* {@link assertGitWorkTree} — are read-only, take no repo lock, and run
|
|
58
|
-
* BEFORE any unit dispatches (preflight / test gate), so they can never block
|
|
59
|
-
* work that is already in flight.
|
|
60
|
-
*
|
|
61
|
-
* This module never throws — every operation returns a result object so the
|
|
62
|
-
* executor maps failures onto its own step/unit failure vocabulary. The GC
|
|
63
|
-
* sweep is the sole exception to "no logging here": it is fire-and-forget and
|
|
64
|
-
* has no caller to report to, so it reports through `warn`.
|
|
18
|
+
* Every repo-mutating git call is async and serialized per base repository
|
|
19
|
+
* ({@link withRepoWorktreeLock}), so parallel units never race on
|
|
20
|
+
* `.git/worktrees`. Nothing here throws: results are objects the executor maps
|
|
21
|
+
* onto its failure vocabulary (the fire-and-forget GC sweep reports via `warn`).
|
|
65
22
|
*/
|
|
66
23
|
import { spawnSync } from "node:child_process";
|
|
67
24
|
import fsp from "node:fs/promises";
|
|
68
25
|
import os from "node:os";
|
|
69
26
|
import path from "node:path";
|
|
70
|
-
import {
|
|
27
|
+
import { isWithinAsync, safeRealpathAsync } from "../../core/common.js";
|
|
71
28
|
import { serializeByKey } from "../../core/concurrent.js";
|
|
72
29
|
import { runManagedSubprocess } from "../../core/subprocess.js";
|
|
73
30
|
import { warn } from "../../core/warn.js";
|
|
74
|
-
/**
|
|
75
|
-
* Timeout for every `git worktree add|prune|remove|status` call this module
|
|
76
|
-
* makes. Was 30s; raised to 120s after #891 confirmed a real product gap:
|
|
77
|
-
* these calls run under {@link withRepoWorktreeLock}, a per-process,
|
|
78
|
-
* per-repository queue, so a machine also busy with OTHER git-heavy work
|
|
79
|
-
* (other akm runs, other agents' worktrees, a loaded CI runner) can genuinely
|
|
80
|
-
* push one `git worktree remove` past 30s without anything being stuck —
|
|
81
|
-
* plain CPU/fork/IO contention. At 30s that showed up as `{ removed: false,
|
|
82
|
-
* error: "... timed out after 30000ms" }` on a perfectly healthy op: a clean
|
|
83
|
-
* worktree left retained, or a create failing a unit outright, purely because
|
|
84
|
-
* the box was busy. 120s matches the same trade already made for
|
|
85
|
-
* {@link GIT_PUSH_TIMEOUT_MS} (`core/write-source.ts`,
|
|
86
|
-
* `sources/providers/git-stash.ts`) for the same class of administrative git
|
|
87
|
-
* call under load; a call that is truly hung (not just slow) is still caught.
|
|
88
|
-
*/
|
|
31
|
+
/** Timeout for each worktree git call (#891: a busy machine can push a healthy one past 30s). */
|
|
89
32
|
const GIT_TIMEOUT_MS = 120_000;
|
|
90
33
|
/** Directory under `os.tmpdir()` that owns every run's worktree roots. */
|
|
91
34
|
export const WORKTREES_DIR_NAME = "akm-worktrees";
|
|
92
|
-
/**
|
|
93
|
-
* Age after which an orphaned entry under the worktrees root is swept.
|
|
94
|
-
*
|
|
95
|
-
* Retained dirty worktrees are forensic state — deleting them is only
|
|
96
|
-
* acceptable once they are far past any plausible investigation window. Seven
|
|
97
|
-
* days is one full on-call rotation: long enough that a retained tree from a
|
|
98
|
-
* failed run has been triaged (or abandoned), short enough that a tmpdir does
|
|
99
|
-
* not accumulate whole repository checkouts indefinitely.
|
|
100
|
-
*/
|
|
35
|
+
/** Age after which an orphaned or retained worktree is swept: long enough to triage a failed run. */
|
|
101
36
|
export const STALE_WORKTREE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
102
37
|
function gitExitError(args, code, stderr, stdout) {
|
|
103
38
|
const detail = (stderr || stdout || "").trim();
|
|
@@ -128,15 +63,7 @@ async function realGitExecutor(cwd, args) {
|
|
|
128
63
|
return { ok: true, stdout: result.stdout };
|
|
129
64
|
}
|
|
130
65
|
let gitExecutor = realGitExecutor;
|
|
131
|
-
/**
|
|
132
|
-
* TEST-ONLY seam (#891): swap the executor every repo-mutating git call in
|
|
133
|
-
* this module goes through. Lets a test prove those calls genuinely
|
|
134
|
-
* interleave with other event-loop work — the property the old "count
|
|
135
|
-
* setInterval ticks during real git calls" test asserted before it was
|
|
136
|
-
* deleted for timing on the real scheduler instead of on behavior — using a
|
|
137
|
-
* fake, deterministically-timed async git in place of the real subprocess.
|
|
138
|
-
* Call with `undefined` to restore the real spawn-based executor.
|
|
139
|
-
*/
|
|
66
|
+
/** Test seam: swap the git executor for repo-mutating calls (`undefined` restores the real one). */
|
|
140
67
|
export function setGitExecutorForTesting(executor) {
|
|
141
68
|
gitExecutor = executor ?? realGitExecutor;
|
|
142
69
|
}
|
|
@@ -191,13 +118,7 @@ export function assertGitWorkTree(dir) {
|
|
|
191
118
|
// ── Per-repository serialization ────────────────────────────────────────────
|
|
192
119
|
/** In-flight tail of each base repository's serialized git-worktree chain. */
|
|
193
120
|
const repoOperationTails = new Map();
|
|
194
|
-
/**
|
|
195
|
-
* Base repos already pruned in this process, per run id. Granularity is
|
|
196
|
-
* per-(repo, run), not per-repo: a run resuming against a repo another run
|
|
197
|
-
* already pruned must still reap ITS own orphaned registrations. A run's whole
|
|
198
|
-
* entry is dropped when its drained worktree root is removed
|
|
199
|
-
* ({@link removeRunRootIfEmpty}), so the map never outgrows the live runs.
|
|
200
|
-
*/
|
|
121
|
+
/** Base repos already pruned in this process, per run id (dropped with the run's drained root). */
|
|
201
122
|
const prunedRuns = new Map();
|
|
202
123
|
/**
|
|
203
124
|
* Serialize `fn` against every other repo-mutating worktree operation on the
|
|
@@ -237,27 +158,10 @@ async function pathExists(p) {
|
|
|
237
158
|
return fsp.access(p).then(() => true, () => false);
|
|
238
159
|
}
|
|
239
160
|
/**
|
|
240
|
-
* Create a fresh
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* A leftover directory at the attempt path (a RETAINED dirty worktree from a
|
|
245
|
-
* prior invocation, or a crashed attempt's partial state) is handled with the
|
|
246
|
-
* same never-destroy-unverified-work rule as {@link cleanupUnitWorktree}:
|
|
247
|
-
* `git status --porcelain` CLEAN → removed; DIRTY or unverifiable (the probe
|
|
248
|
-
* fails — e.g. a half-created directory that is no longer a valid worktree)
|
|
249
|
-
* → moved aside to `<dest>.retained-<ts>` and reported via
|
|
250
|
-
* `preservedLeftover` so the caller can log where the work went. Either way
|
|
251
|
-
* `git worktree prune` clears the stale registration before re-creating.
|
|
252
|
-
*
|
|
253
|
-
* The whole body runs under {@link withRepoWorktreeLock}: the leftover probe,
|
|
254
|
-
* the prune and the add form ONE critical section against the base repo's
|
|
255
|
-
* administrative state, so a concurrent unit's prune can never land between
|
|
256
|
-
* another unit's prune and its add.
|
|
257
|
-
*
|
|
258
|
-
* A successful add takes a liveness lease ({@link acquireWorktreeLease}) so the
|
|
259
|
-
* GC sweep — in this process or another one — never collects the tree while the
|
|
260
|
-
* unit is still running in it.
|
|
161
|
+
* Create a fresh detached worktree at `<tmp>/akm-worktrees/<runId>/<attemptId>`.
|
|
162
|
+
* A leftover at that path is removed when clean, otherwise moved aside to
|
|
163
|
+
* `<dest>.retained-<ts>` (`preservedLeftover`). Probe, prune, and add form one
|
|
164
|
+
* critical section under {@link withRepoWorktreeLock}.
|
|
261
165
|
*/
|
|
262
166
|
export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
|
|
263
167
|
// Opportunistic, at most once per process, never awaited — GC must never sit
|
|
@@ -274,7 +178,7 @@ export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
|
|
|
274
178
|
if (status.ok && status.stdout.trim() === "") {
|
|
275
179
|
// Async on purpose: a recursive delete of a whole leftover checkout
|
|
276
180
|
// inside this critical section would otherwise block the event loop
|
|
277
|
-
// (every other in-flight unit,
|
|
181
|
+
// (every other in-flight unit, abort handling).
|
|
278
182
|
await fsp.rm(dest, { recursive: true, force: true });
|
|
279
183
|
}
|
|
280
184
|
else {
|
|
@@ -291,14 +195,8 @@ export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
|
|
|
291
195
|
...(preservedLeftover !== undefined ? { preservedLeftover } : {}),
|
|
292
196
|
};
|
|
293
197
|
}
|
|
294
|
-
// Prune
|
|
295
|
-
//
|
|
296
|
-
// necessary, and never per-unit-attempt (which multiplied lock contention
|
|
297
|
-
// without buying safety):
|
|
298
|
-
// • a leftover was just removed/moved — its stale registration MUST go
|
|
299
|
-
// before re-adding at the same path;
|
|
300
|
-
// • first worktree of this (repo, run) — reaps registrations orphaned by
|
|
301
|
-
// earlier runs whose roots were GC'd or deleted out from under git.
|
|
198
|
+
// Prune (never touches a live worktree) after a leftover was removed, and
|
|
199
|
+
// on the first worktree of this (repo, run) to reap orphaned registrations.
|
|
302
200
|
const prunedRepos = prunedRuns.get(runId);
|
|
303
201
|
if (leftoverHandled || !prunedRepos?.has(repoKey)) {
|
|
304
202
|
if (prunedRepos)
|
|
@@ -315,47 +213,24 @@ export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
|
|
|
315
213
|
...(preservedLeftover !== undefined ? { preservedLeftover } : {}),
|
|
316
214
|
};
|
|
317
215
|
}
|
|
318
|
-
await acquireWorktreeLease(dest);
|
|
319
216
|
return { ok: true, path: dest, ...(preservedLeftover !== undefined ? { preservedLeftover } : {}) };
|
|
320
217
|
});
|
|
321
218
|
}
|
|
322
219
|
/**
|
|
323
|
-
* Post-unit cleanup: remove
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* too (never destroy a tree whose state could not be verified).
|
|
327
|
-
*
|
|
328
|
-
* The probe deliberately omits `--ignored`: a worktree whose only residue is
|
|
329
|
-
* files matched by the base repo's `.gitignore` (build artifacts, caches,
|
|
330
|
-
* logs, `node_modules`) probes clean and IS removed. Those files are disposable
|
|
331
|
-
* by the repo's own declaration; retaining a worktree per build/install would
|
|
332
|
-
* blow up disk. "Uncollected work" the caller preserves is therefore
|
|
333
|
-
* tracked-or-untracked-unignored changes only (module doc).
|
|
334
|
-
*
|
|
335
|
-
* Only `git worktree remove` takes the base repo's lock; the status probe stays
|
|
336
|
-
* OFF {@link withRepoWorktreeLock}. Since the probe now runs only when a
|
|
337
|
-
* removal was refused, a dirty worktree costs one failed removal inside the
|
|
338
|
-
* lock that it used to avoid — the trade that makes every CLEAN cleanup a
|
|
339
|
-
* single git process.
|
|
220
|
+
* Post-unit cleanup: remove a clean worktree; retain (dirty: true) one with
|
|
221
|
+
* uncommitted work or whose state could not be verified. `.gitignore`d files do
|
|
222
|
+
* not count as work.
|
|
340
223
|
*/
|
|
341
224
|
export async function cleanupUnitWorktree(baseDir, worktreePath) {
|
|
342
|
-
//
|
|
343
|
-
// remove` without `--force` already refuses a worktree carrying changes, on
|
|
344
|
-
// the same terms as the probe (ignored files excluded either way). The clean
|
|
345
|
-
// case — the overwhelmingly common one — is then ONE git process per unit
|
|
346
|
-
// instead of two, which a wide fan-out pays per unit.
|
|
225
|
+
// The removal (without --force) is itself the cleanliness check: one git process per clean unit.
|
|
347
226
|
const removed = await withRepoWorktreeLock(await safeRealpathAsync(baseDir), () => git(baseDir, ["worktree", "remove", worktreePath]));
|
|
348
227
|
if (removed.ok) {
|
|
349
228
|
await removeRunRootIfEmpty(worktreePath);
|
|
350
229
|
return { removed: true, dirty: false };
|
|
351
230
|
}
|
|
352
|
-
// It refused, so the tree stays on disk
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
await releaseWorktreeLease(worktreePath);
|
|
356
|
-
// Ask the probe WHY it refused rather than parsing git's message, whose
|
|
357
|
-
// wording varies with version and locale — and which the caller's warn text
|
|
358
|
-
// has never been written against.
|
|
231
|
+
// It refused, so the tree stays on disk. Ask the probe WHY it refused rather
|
|
232
|
+
// than parsing git's message, whose wording varies with version and locale —
|
|
233
|
+
// and which the caller's warn text has never been written against.
|
|
359
234
|
const status = await git(worktreePath, ["status", "--porcelain"]);
|
|
360
235
|
if (!status.ok) {
|
|
361
236
|
return { removed: false, dirty: false, error: status.error };
|
|
@@ -365,100 +240,8 @@ export async function cleanupUnitWorktree(baseDir, worktreePath) {
|
|
|
365
240
|
}
|
|
366
241
|
return { removed: false, dirty: false, error: removed.error };
|
|
367
242
|
}
|
|
368
|
-
// ── Liveness leases ─────────────────────────────────────────────────────────
|
|
369
|
-
/** Marker file, inside a worktree's git admin dir, naming the process using it. */
|
|
370
|
-
const LEASE_FILE_NAME = "akm-lease";
|
|
371
|
-
/**
|
|
372
|
-
* Path of `p`'s git administrative directory (`<repo>/.git/worktrees/<name>`),
|
|
373
|
-
* read from the `.git` FILE every linked worktree carries. Undefined when `p`
|
|
374
|
-
* is not a readable linked worktree.
|
|
375
|
-
*/
|
|
376
|
-
async function worktreeAdminDir(p) {
|
|
377
|
-
let contents;
|
|
378
|
-
try {
|
|
379
|
-
contents = await fsp.readFile(path.join(p, ".git"), "utf8");
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
return undefined;
|
|
383
|
-
}
|
|
384
|
-
const gitdir = /^gitdir:[ \t]*(\S.*)$/m.exec(contents)?.[1];
|
|
385
|
-
return gitdir?.trim();
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* Record this process as the user of `worktreePath`, so {@link
|
|
389
|
-
* sweepStaleWorktrees} can tell a live worktree from an abandoned one.
|
|
390
|
-
*
|
|
391
|
-
* The marker lives in git's administrative directory for the worktree, never in
|
|
392
|
-
* the checkout: an untracked file inside the tree would make it probe DIRTY (and
|
|
393
|
-
* be retained forever), while git's own `worktree remove`/`prune` delete the
|
|
394
|
-
* admin dir — lease included — with no extra bookkeeping here. Best effort: a
|
|
395
|
-
* lease that cannot be written only leaves the tree collectible once stale,
|
|
396
|
-
* which is the pre-lease behaviour.
|
|
397
|
-
*/
|
|
398
|
-
async function acquireWorktreeLease(worktreePath) {
|
|
399
|
-
const adminDir = await worktreeAdminDir(worktreePath);
|
|
400
|
-
if (adminDir === undefined)
|
|
401
|
-
return;
|
|
402
|
-
const lease = {
|
|
403
|
-
pid: process.pid,
|
|
404
|
-
host: os.hostname(),
|
|
405
|
-
path: await safeRealpathAsync(worktreePath),
|
|
406
|
-
};
|
|
407
|
-
try {
|
|
408
|
-
await fsp.writeFile(path.join(adminDir, LEASE_FILE_NAME), JSON.stringify(lease));
|
|
409
|
-
}
|
|
410
|
-
catch {
|
|
411
|
-
/* best effort — see above */
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
/** Drop the lease of a worktree this process is done with but is not removing. */
|
|
415
|
-
async function releaseWorktreeLease(worktreePath) {
|
|
416
|
-
const adminDir = await worktreeAdminDir(worktreePath);
|
|
417
|
-
if (adminDir === undefined)
|
|
418
|
-
return;
|
|
419
|
-
try {
|
|
420
|
-
await fsp.rm(path.join(adminDir, LEASE_FILE_NAME), { force: true });
|
|
421
|
-
}
|
|
422
|
-
catch {
|
|
423
|
-
/* best effort — a stale lease only delays the sweep by one run of it */
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* True when a still-running process holds `candidate`'s lease — the guard age
|
|
428
|
-
* cannot provide. A unit that runs longer than the sweep threshold while
|
|
429
|
-
* writing only inside subdirectories leaves the worktree ROOT's mtime at
|
|
430
|
-
* creation time, so another akm process minting a worktree would otherwise
|
|
431
|
-
* delete a tree that is still in use.
|
|
432
|
-
*
|
|
433
|
-
* A lease from a dead pid, from another host (where the pid means nothing), or
|
|
434
|
-
* for a different path is NOT liveness: crashed runs and retained dirty trees
|
|
435
|
-
* stay collectible, which is the whole point of the sweep.
|
|
436
|
-
*/
|
|
437
|
-
async function isWorktreeLeaseLive(candidate) {
|
|
438
|
-
const adminDir = await worktreeAdminDir(candidate);
|
|
439
|
-
if (adminDir === undefined)
|
|
440
|
-
return false;
|
|
441
|
-
let lease;
|
|
442
|
-
try {
|
|
443
|
-
lease = JSON.parse(await fsp.readFile(path.join(adminDir, LEASE_FILE_NAME), "utf8"));
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
return false;
|
|
447
|
-
}
|
|
448
|
-
if (lease.host !== os.hostname())
|
|
449
|
-
return false;
|
|
450
|
-
if (lease.path !== (await safeRealpathAsync(candidate)))
|
|
451
|
-
return false;
|
|
452
|
-
return isProcessAlive(lease.pid);
|
|
453
|
-
}
|
|
454
243
|
// ── Garbage collection ──────────────────────────────────────────────────────
|
|
455
|
-
/**
|
|
456
|
-
* Drop the run-scoped root once its last unit worktree is gone. `rmdir`
|
|
457
|
-
* refuses a non-empty directory, so a run that retained a dirty worktree (or
|
|
458
|
-
* a `.retained-<ts>` copy) keeps its root and its forensic contents; only a
|
|
459
|
-
* fully drained root disappears. Never touches anything that is not a DIRECT
|
|
460
|
-
* child of the worktrees root.
|
|
461
|
-
*/
|
|
244
|
+
/** Drop the run-scoped root once empty (`rmdir` keeps any retained tree). */
|
|
462
245
|
async function removeRunRootIfEmpty(worktreePath) {
|
|
463
246
|
const root = worktreesRoot();
|
|
464
247
|
const runRoot = path.dirname(path.resolve(worktreePath));
|
|
@@ -480,25 +263,10 @@ async function removeRunRootIfEmpty(worktreePath) {
|
|
|
480
263
|
prunedRuns.delete(path.basename(runRoot));
|
|
481
264
|
}
|
|
482
265
|
/**
|
|
483
|
-
* Age-based GC of the worktrees root
|
|
484
|
-
* directories
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* acceptable. A run root is dropped once it is empty and itself stale (or
|
|
488
|
-
* this sweep just emptied it), so a live run whose first worktree is mid-`add`
|
|
489
|
-
* is never pulled out from under git.
|
|
490
|
-
*
|
|
491
|
-
* Safety invariants: it only ever descends two levels from `root`; entries
|
|
492
|
-
* that are not real directories (symlinks included — `Dirent.isDirectory()`
|
|
493
|
-
* reflects `lstat`) are skipped, never followed; a stale-looking candidate
|
|
494
|
-
* whose {@link isWorktreeLeaseLive} lease holder is still running is skipped
|
|
495
|
-
* (age alone cannot see a unit in flight in another process); and every
|
|
496
|
-
* candidate is re-verified with {@link isWithin} against the resolved root
|
|
497
|
-
* before removal.
|
|
498
|
-
* Deleting a directory leaves its registration in whatever base repo minted
|
|
499
|
-
* it; the next run's `git worktree prune` on that repo reaps it.
|
|
500
|
-
*
|
|
501
|
-
* Returns the paths removed. Never throws.
|
|
266
|
+
* Age-based GC of the worktrees root: removes `<root>/<runId>/<entry>`
|
|
267
|
+
* directories older than `maxAgeMs`, and empty stale run roots. Descends two
|
|
268
|
+
* levels only, never follows a symlink, and re-checks containment before each
|
|
269
|
+
* removal. Returns the paths removed; never throws.
|
|
502
270
|
*/
|
|
503
271
|
export async function sweepStaleWorktrees(opts = {}) {
|
|
504
272
|
const root = path.resolve(opts.root ?? worktreesRoot());
|
|
@@ -536,8 +304,6 @@ export async function sweepStaleWorktrees(opts = {}) {
|
|
|
536
304
|
continue;
|
|
537
305
|
if (now - (await lastActivityMs(candidate, entry.name)) < maxAgeMs)
|
|
538
306
|
continue;
|
|
539
|
-
if (await isWorktreeLeaseLive(candidate))
|
|
540
|
-
continue;
|
|
541
307
|
try {
|
|
542
308
|
await fsp.rm(candidate, { recursive: true, force: true });
|
|
543
309
|
removed.push(candidate);
|
|
@@ -2,21 +2,10 @@
|
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `<child>`'s frozen target is `kind: "child-workflow"`. Only the FIRST path
|
|
10
|
-
* segment is a freeze-time concern: it must name one of the child's declared
|
|
11
|
-
* `outputs:` names, or — when the child declares none — `runId` or `status`
|
|
12
|
-
* (the default `workflowRunExportedResult` shape, `runtime/run-outputs.ts`).
|
|
13
|
-
* A reference AT `steps.<child>.output` with no further segment always
|
|
14
|
-
* accepts — it names the whole exported object. Anything deeper resolves (and,
|
|
15
|
-
* if wrong, fails) at pre-attempt through the existing, unchanged resolver —
|
|
16
|
-
* the value's shape past the first segment is unconstrained unless the
|
|
17
|
-
* output declares a `schema:`.
|
|
18
|
-
*
|
|
19
|
-
* Pure over the frozen step list — no IO, no config.
|
|
5
|
+
* Freeze-time check of references into a child-workflow step's output: the
|
|
6
|
+
* first path segment after `steps.<child>.output` must name one of the child's
|
|
7
|
+
* declared `outputs:` (or `runId`/`status` when it declares none). Deeper
|
|
8
|
+
* segments resolve at run time.
|
|
20
9
|
*/
|
|
21
10
|
import { UsageError } from "../../core/errors.js";
|
|
22
11
|
import { formatReference, parseReference } from "../program/expressions.js";
|