nx 23.3.0-beta.7 → 23.3.0-beta.9
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/README.md +1 -1
- package/dist/nx-cloud-internals.d.ts +1 -1
- package/dist/nx-cloud-internals.js +4 -4
- package/dist/src/command-line/affected/affected.js +20 -1
- package/dist/src/command-line/affected/command-object.js +1 -1
- package/dist/src/command-line/migrate/agentic/master/run-master-session.d.ts +6 -2
- package/dist/src/command-line/migrate/agentic/master/run-master-session.js +88 -14
- package/dist/src/command-line/migrate/command-object.d.ts +1 -0
- package/dist/src/command-line/migrate/command-object.js +6 -1
- package/dist/src/command-line/migrate/migrate-analytics.d.ts +34 -9
- package/dist/src/command-line/migrate/migrate-analytics.js +71 -10
- package/dist/src/command-line/migrate/migrate-config.d.ts +5 -2
- package/dist/src/command-line/migrate/migrate-config.js +19 -11
- package/dist/src/command-line/migrate/migrate.d.ts +2 -0
- package/dist/src/command-line/migrate/migrate.js +152 -45
- package/dist/src/command-line/migrate/resolve-package-version.js +2 -19
- package/dist/src/command-line/migrate/run/broker.d.ts +14 -2
- package/dist/src/command-line/migrate/run/broker.js +95 -34
- package/dist/src/command-line/migrate/run/existing-run-report.d.ts +55 -0
- package/dist/src/command-line/migrate/run/existing-run-report.js +225 -0
- package/dist/src/command-line/migrate/run/give-up.d.ts +41 -0
- package/dist/src/command-line/migrate/run/give-up.js +148 -0
- package/dist/src/command-line/migrate/run/index.d.ts +6 -4
- package/dist/src/command-line/migrate/run/index.js +15 -2
- package/dist/src/command-line/migrate/run/issues.d.ts +14 -0
- package/dist/src/command-line/migrate/run/issues.js +59 -0
- package/dist/src/command-line/migrate/run/orchestrator.d.ts +40 -1
- package/dist/src/command-line/migrate/run/orchestrator.js +509 -160
- package/dist/src/command-line/migrate/run/run-id.d.ts +0 -8
- package/dist/src/command-line/migrate/run/run-id.js +0 -30
- package/dist/src/command-line/migrate/run/run-state.d.ts +11 -9
- package/dist/src/command-line/migrate/run/run-state.js +25 -12
- package/dist/src/command-line/migrate/run/runbook.js +9 -4
- package/dist/src/command-line/migrate/run/state-lock.d.ts +31 -0
- package/dist/src/command-line/migrate/run/state-lock.js +126 -0
- package/dist/src/command-line/migrate/run/state-machine.d.ts +15 -0
- package/dist/src/command-line/migrate/run/state-machine.js +137 -10
- package/dist/src/command-line/migrate/run/util.d.ts +1 -0
- package/dist/src/command-line/migrate/run/util.js +21 -0
- package/dist/src/command-line/migrate/run/worker.js +3 -0
- package/dist/src/command-line/migrate/step-actions.d.ts +1 -1
- package/dist/src/command-line/migrate/step-actions.js +7 -1
- package/dist/src/command-line/migrate/text.d.ts +6 -0
- package/dist/src/command-line/migrate/text.js +25 -0
- package/dist/src/command-line/release/utils/release-graph.js +1 -1
- package/dist/src/command-line/release/utils/remote-release-clients/github.js +2 -5
- package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.d.ts +2 -2
- package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.js +4 -5
- package/dist/src/command-line/release/version/version-actions.d.ts +3 -0
- package/dist/src/command-line/reset/reset.js +10 -4
- package/dist/src/command-line/show/command-object.js +1 -1
- package/dist/src/command-line/show/projects.js +22 -0
- package/dist/src/command-line/yargs-utils/shared-options.d.ts +4 -0
- package/dist/src/command-line/yargs-utils/shared-options.js +16 -0
- package/dist/src/config/task-graph.d.ts +1 -1
- package/dist/src/config/workspace-json-project-json.d.ts +4 -4
- package/dist/src/daemon/client/client.d.ts +3 -3
- package/dist/src/daemon/client/client.js +4 -4
- package/dist/src/daemon/message-types/select-affected-tasks.d.ts +3 -0
- package/dist/src/daemon/message-types/ultracache-configuration-version.d.ts +8 -0
- package/dist/src/daemon/server/handle-hash-tasks.d.ts +2 -2
- package/dist/src/daemon/server/handle-hash-tasks.js +3 -3
- package/dist/src/daemon/server/handle-select-affected-tasks.js +5 -2
- package/dist/src/daemon/server/ultracache-configuration-state.d.ts +7 -0
- package/dist/src/daemon/server/{io-snapshots-state.js → ultracache-configuration-state.js} +5 -4
- package/dist/src/devkit-internals.d.ts +3 -1
- package/dist/src/devkit-internals.js +8 -3
- package/dist/src/hasher/create-task-hasher.d.ts +3 -3
- package/dist/src/hasher/create-task-hasher.js +6 -6
- package/dist/src/hasher/hash-task.d.ts +2 -2
- package/dist/src/hasher/hash-task.js +6 -5
- package/dist/src/hasher/native-task-hasher-impl.d.ts +7 -7
- package/dist/src/hasher/native-task-hasher-impl.js +19 -19
- package/dist/src/hasher/task-hasher.d.ts +14 -14
- package/dist/src/hasher/task-hasher.js +13 -13
- package/dist/src/native/index.d.ts +151 -102
- package/dist/src/native/native-bindings.js +4 -5
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.js +15 -8
- package/dist/src/nx-cloud/update-manager.d.ts +3 -3
- package/dist/src/nx-cloud/update-manager.js +19 -13
- package/dist/src/nx-cloud/utilities/client.d.ts +1 -1
- package/dist/src/nx-cloud/utilities/client.js +1 -1
- package/dist/src/nx-cloud/utilities/is-workspace-claimed.js +2 -1
- package/dist/src/nx-cloud/utilities/nx-cloud-http-client.d.ts +5 -0
- package/dist/src/nx-cloud/utilities/nx-cloud-http-client.js +16 -0
- package/dist/src/nx-cloud/utilities/url-shorten.js +17 -10
- package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +22 -18
- package/dist/src/plugins/js/utils/register.js +11 -3
- package/dist/src/project-graph/affected/affected-reasons.d.ts +116 -0
- package/dist/src/project-graph/affected/affected-reasons.js +459 -0
- package/dist/src/project-graph/affected/affected-tasks.d.ts +23 -10
- package/dist/src/project-graph/affected/affected-tasks.js +203 -31
- package/dist/src/project-graph/affected/print-explanation.d.ts +11 -0
- package/dist/src/project-graph/affected/print-explanation.js +44 -0
- package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
- package/dist/src/tasks-runner/init-tasks-runner.d.ts +5 -5
- package/dist/src/tasks-runner/init-tasks-runner.js +8 -15
- package/dist/src/tasks-runner/run-command.d.ts +7 -7
- package/dist/src/tasks-runner/run-command.js +19 -23
- package/dist/src/tasks-runner/task-orchestrator.d.ts +7 -3
- package/dist/src/tasks-runner/task-orchestrator.js +9 -8
- package/dist/src/tasks-runner/tasks-runner.d.ts +2 -0
- package/dist/src/ultracache/config.d.ts +21 -0
- package/dist/src/ultracache/config.js +29 -0
- package/dist/src/ultracache/fetch.d.ts +21 -0
- package/dist/src/{io-snapshots → ultracache}/fetch.js +9 -9
- package/dist/src/ultracache/overrides.d.ts +17 -0
- package/dist/src/{io-snapshots → ultracache}/overrides.js +14 -13
- package/dist/src/ultracache/report.d.ts +13 -0
- package/dist/src/{io-snapshots → ultracache}/report.js +15 -21
- package/dist/src/ultracache/store.d.ts +40 -0
- package/dist/src/{io-snapshots → ultracache}/store.js +45 -45
- package/dist/src/utils/ab-testing.js +11 -12
- package/dist/src/utils/command-line-utils.d.ts +1 -0
- package/dist/src/utils/create-cli-options.d.ts +6 -0
- package/dist/src/utils/create-cli-options.js +29 -0
- package/dist/src/utils/db-connection.d.ts +1 -0
- package/dist/src/utils/db-connection.js +1 -0
- package/dist/src/utils/git-utils.d.ts +7 -0
- package/dist/src/utils/git-utils.js +21 -9
- package/dist/src/utils/http-client.d.ts +27 -0
- package/dist/src/utils/http-client.js +193 -0
- package/package.json +12 -38
- package/dist/src/daemon/message-types/io-snapshot-version.d.ts +0 -5
- package/dist/src/daemon/server/io-snapshots-state.d.ts +0 -7
- package/dist/src/io-snapshots/config.d.ts +0 -23
- package/dist/src/io-snapshots/config.js +0 -32
- package/dist/src/io-snapshots/fetch.d.ts +0 -21
- package/dist/src/io-snapshots/outputs.d.ts +0 -12
- package/dist/src/io-snapshots/outputs.js +0 -33
- package/dist/src/io-snapshots/overrides.d.ts +0 -17
- package/dist/src/io-snapshots/report.d.ts +0 -13
- package/dist/src/io-snapshots/store.d.ts +0 -37
- package/dist/src/nx-cloud/utilities/axios.d.ts +0 -4
- package/dist/src/nx-cloud/utilities/axios.js +0 -17
- /package/dist/src/daemon/message-types/{io-snapshot-version.js → ultracache-configuration-version.js} +0 -0
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runOrchestratorInit = runOrchestratorInit;
|
|
4
|
+
exports.runOrchestratorResume = runOrchestratorResume;
|
|
5
|
+
exports.holdRunToContinue = holdRunToContinue;
|
|
6
|
+
exports.activeRunToReplace = activeRunToReplace;
|
|
7
|
+
exports.releaseRunToHandOff = releaseRunToHandOff;
|
|
4
8
|
exports.runOrchestratorReconcile = runOrchestratorReconcile;
|
|
5
9
|
exports.completionWarnings = completionWarnings;
|
|
6
10
|
const child_process_1 = require("child_process");
|
|
7
11
|
const crypto_1 = require("crypto");
|
|
8
12
|
const fs_1 = require("fs");
|
|
9
13
|
const path_1 = require("path");
|
|
14
|
+
const native_1 = require("../../../native");
|
|
10
15
|
const fileutils_1 = require("../../../utils/fileutils");
|
|
11
16
|
const atomic_write_1 = require("./atomic-write");
|
|
12
17
|
const clean_retry_1 = require("./clean-retry");
|
|
@@ -25,6 +30,7 @@ const run_id_1 = require("./run-id");
|
|
|
25
30
|
const run_state_1 = require("./run-state");
|
|
26
31
|
const state_lock_1 = require("./state-lock");
|
|
27
32
|
const state_machine_1 = require("./state-machine");
|
|
33
|
+
const give_up_1 = require("./give-up");
|
|
28
34
|
const migration_shape_1 = require("../migration-shape");
|
|
29
35
|
const util_1 = require("./util");
|
|
30
36
|
const text_1 = require("../text");
|
|
@@ -32,6 +38,7 @@ const agent_output_1 = require("./agent-output");
|
|
|
32
38
|
const agent_work_payload_1 = require("./agent-work-payload");
|
|
33
39
|
const issues_1 = require("./issues");
|
|
34
40
|
const runbook_1 = require("./runbook");
|
|
41
|
+
const existing_run_report_1 = require("./existing-run-report");
|
|
35
42
|
const package_manager_1 = require("../../../utils/package-manager");
|
|
36
43
|
// The dark migrate orchestrator: drives a durable run one dispense at a time.
|
|
37
44
|
// An outer AI agent runs each dispensed command and re-invokes `nx migrate
|
|
@@ -44,11 +51,8 @@ const HANG_THRESHOLD_MS = 15 * 60 * 1000;
|
|
|
44
51
|
// 'no-progress' action. A still-running worker is exempt until the hang
|
|
45
52
|
// threshold: waiting on a live worker is not looping.
|
|
46
53
|
const NO_PROGRESS_THRESHOLD = 3;
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
// an agent that follows `next` blindly cannot retry forever. An explicit
|
|
50
|
-
// retry is still honored; the cap never refuses.
|
|
51
|
-
const REARM_ESCALATION_CAP = 3;
|
|
54
|
+
// Two rearms per step: one for the diagnosed fix and one for its correction.
|
|
55
|
+
const REARM_ESCALATION_CAP = 2;
|
|
52
56
|
const INIT_CONTINUE_HINT = 're-run the command, or unset NX_MIGRATE_ORCHESTRATOR to use the standard migrate flow.';
|
|
53
57
|
function continueRunHint(runId) {
|
|
54
58
|
return `re-run the command to continue run '${runId}'.`;
|
|
@@ -84,30 +88,50 @@ function refuseUnsafeScratchExposure(exposure, thenWhat) {
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
async function runOrchestratorInit(input) {
|
|
87
|
-
const { root, migrationsJson, createCommits, commitPrefix, skipInstall, installedNxVersion, validate, emitAgentInstructions = true, } = input;
|
|
88
|
-
const planHash = (0, run_id_1.computePlanHash)(migrationsJson);
|
|
89
|
-
// An active run means a prior init already happened (e.g. it crashed before
|
|
90
|
-
// the agent's first reconcile); starting a second run would compete with it.
|
|
91
|
-
// Same plan: resume it. Different plan: refuse rather than guess which plan
|
|
92
|
-
// the agent means. NewerRunStateFormatError propagates.
|
|
93
|
-
const active = findActiveRunForPlan(root, planHash);
|
|
94
|
-
// Dispensed commands interpolate migration ids verbatim, so every init
|
|
95
|
-
// (fresh or resumed) validates the incoming plan's ids. After the mismatch
|
|
96
|
-
// check: a plan that will be refused anyway should get the more actionable
|
|
97
|
-
// mismatch error, not this one.
|
|
91
|
+
const { root, migrationsJson, migrationsPath, createCommits, commitPrefix, skipInstall, installedNxVersion, validate, emitAgentInstructions = true, onExistingRun = 'report', replaceRunId, confirmStart, } = input;
|
|
98
92
|
const migrations = (migrationsJson.migrations ?? []);
|
|
99
93
|
const sorted = (0, sort_migrations_1.sortMigrations)(migrations.slice(), {
|
|
100
94
|
hoistHandoffGitignore: true,
|
|
101
95
|
});
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
const plannedIds = sorted.map((m) => `${m.package}:${m.name}`);
|
|
97
|
+
// An active run means a prior init already happened (e.g. it crashed before
|
|
98
|
+
// the agent's first reconcile); starting a second run would compete with it,
|
|
99
|
+
// and whether to continue it or replace it is the user's call, not a guess
|
|
100
|
+
// from the plans. Reported before the migration id check below: this plan
|
|
101
|
+
// is not being started. NewerRunStateFormatError propagates.
|
|
102
|
+
const active = onExistingRun === 'start-fresh'
|
|
103
|
+
? activeRunToReplace(root, replaceRunId)
|
|
104
|
+
: findActiveRunForInit(root);
|
|
105
|
+
if (active &&
|
|
106
|
+
(onExistingRun === 'report' ||
|
|
107
|
+
(replaceRunId !== undefined && active.runId !== replaceRunId))) {
|
|
108
|
+
return reportExistingRun(root, active.runId, active.state, plannedIds, migrationsPath, emitAgentInstructions, replaceRunId);
|
|
109
|
+
}
|
|
110
|
+
// Held through confirmStart so a concurrent start-fresh cannot delete the
|
|
111
|
+
// run while the user is asked. The refusals run here first, before the
|
|
112
|
+
// prompt, the .gitignore fallback and the checkpoint; the locked pass below
|
|
113
|
+
// is authoritative. Skipped without the scratch dir: the lock would create it.
|
|
114
|
+
if (active) {
|
|
115
|
+
(0, state_lock_1.holdRunActivity)(root, active.runId);
|
|
116
|
+
}
|
|
117
|
+
if ((0, fs_1.existsSync)((0, run_state_1.migrateRunsDir)(root))) {
|
|
118
|
+
(0, state_lock_1.withRunCreationLock)(root, () => {
|
|
119
|
+
refuseLiveReservation(root);
|
|
120
|
+
if (active) {
|
|
121
|
+
refuseUndeletableRun(root, active.runId, active.state);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Dispensed commands interpolate migration ids verbatim, so every init
|
|
126
|
+
// validates the incoming plan's ids. Before the delete below: a run must
|
|
127
|
+
// not be thrown away for a plan that cannot start.
|
|
128
|
+
for (const id of plannedIds) {
|
|
104
129
|
if (!run_state_1.SHELL_SAFE_VALUE.test(id)) {
|
|
105
130
|
throw new Error(`The migration id '${id}' contains characters that are not shell-safe. Orchestrated runs require shell-safe migration ids.`);
|
|
106
131
|
}
|
|
107
132
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return resumeRun(root, active.runId, active.state, policy, emitAgentInstructions);
|
|
133
|
+
if (confirmStart && !(await confirmStart())) {
|
|
134
|
+
return { kind: 'refused' };
|
|
111
135
|
}
|
|
112
136
|
const runId = (0, run_id_1.createRunId)();
|
|
113
137
|
const dir = (0, run_state_1.runDir)(root, runId);
|
|
@@ -144,12 +168,48 @@ async function runOrchestratorInit(input) {
|
|
|
144
168
|
// .gitignore, Lerna without nx.json).
|
|
145
169
|
refuseUnsafeScratchExposure((0, git_utils_1.getPathCommitExposure)(types_1.MIGRATE_RUNS_RELATIVE_DIR, root), INIT_CONTINUE_HINT);
|
|
146
170
|
}
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
// One creation-lock section: two inits cannot both create a run, and a
|
|
172
|
+
// start-fresh's delete and the new directory's reservation (no run.json
|
|
173
|
+
// yet) land together. Git stays outside the lock: a hook re-entering nx
|
|
174
|
+
// migrate would wait on this process. The .gitignore fallback is idempotent.
|
|
175
|
+
let deleted = false;
|
|
176
|
+
const reserved = (0, state_lock_1.withRunCreationLock)(root, () => {
|
|
177
|
+
refuseLiveReservation(root);
|
|
178
|
+
const nowActive = findActiveRunForInit(root, active !== null);
|
|
179
|
+
if (nowActive && nowActive.runId !== active?.runId) {
|
|
180
|
+
return nowActive;
|
|
181
|
+
}
|
|
182
|
+
if (active) {
|
|
183
|
+
// Completed meanwhile (a reconcile finished it): nothing to replace.
|
|
184
|
+
if (!nowActive) {
|
|
185
|
+
throw new Error(noActiveRunToReplace(active.runId));
|
|
186
|
+
}
|
|
187
|
+
deleted = deleteRunRecord(root, active.runId);
|
|
188
|
+
}
|
|
189
|
+
// The snapshot must exist before run.json makes the run discoverable: a
|
|
190
|
+
// crash in between must not leave an active run without its plan.
|
|
191
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
192
|
+
(0, fileutils_1.writeJsonFile)((0, path_1.join)(dir, PLAN_SNAPSHOT_0), migrationsJson);
|
|
193
|
+
// Held from here until run.json is written below and past it: a run must
|
|
194
|
+
// never be discoverable, or reserved, without a holder.
|
|
195
|
+
(0, state_lock_1.registerRunActivity)(dir);
|
|
196
|
+
return null;
|
|
197
|
+
});
|
|
198
|
+
if (reserved) {
|
|
199
|
+
// A run created concurrently since the check above. Reported, not
|
|
200
|
+
// deleted, even under 'start-fresh': that consent covered the run the
|
|
201
|
+
// user saw, not one that appeared while this init was running.
|
|
202
|
+
return reportExistingRun(root, reserved.runId, reserved.state, plannedIds, migrationsPath, emitAgentInstructions);
|
|
203
|
+
}
|
|
204
|
+
if (deleted) {
|
|
205
|
+
removeDeletedRunDir(root, active.runId);
|
|
206
|
+
}
|
|
207
|
+
// Checkpoint pre-existing working-tree state BEFORE run.json exists, so a
|
|
208
|
+
// clean tree stays uncommitted (writing run.json would otherwise dirty it
|
|
209
|
+
// and fire a spurious checkpoint). A crash between here and createRun
|
|
210
|
+
// leaves the committed changes orphaned but never lost, and the reserved
|
|
211
|
+
// directory unlocked, which discovery ignores; the next init re-checkpoints
|
|
212
|
+
// a now-clean tree as a no-op.
|
|
153
213
|
const checkpoint = createCommits ? checkpointEntry(root, commitPrefix) : null;
|
|
154
214
|
// The preflight checkpoint swallows its own failures, so the tree itself is
|
|
155
215
|
// the only reliable signal: anything still uncommitted here predates every
|
|
@@ -157,6 +217,7 @@ async function runOrchestratorInit(input) {
|
|
|
157
217
|
// failed probe counts as dirty: mistaking it for clean would let a later
|
|
158
218
|
// retry-clean reset destroy the very work this flag exists to protect.
|
|
159
219
|
const checkpointFailed = createCommits && (0, git_utils_1.getWorkingTreeStatus)(root) !== 'clean';
|
|
220
|
+
const branch = (0, git_utils_1.getGitCurrentBranch)(root);
|
|
160
221
|
const state = {
|
|
161
222
|
formatVersion: run_state_1.CURRENT_RUN_STATE_FORMAT_VERSION,
|
|
162
223
|
runId,
|
|
@@ -168,10 +229,10 @@ async function runOrchestratorInit(input) {
|
|
|
168
229
|
...(skipInstall ? { skipInstall: true } : {}),
|
|
169
230
|
validate: validate !== false,
|
|
170
231
|
runbookPath: runbook_1.RUNBOOK_FILE_NAME,
|
|
232
|
+
...(branch ? { branch } : {}),
|
|
171
233
|
rounds: [
|
|
172
234
|
{
|
|
173
235
|
index: 0,
|
|
174
|
-
planHash,
|
|
175
236
|
planSnapshot: PLAN_SNAPSHOT_0,
|
|
176
237
|
},
|
|
177
238
|
],
|
|
@@ -180,39 +241,77 @@ async function runOrchestratorInit(input) {
|
|
|
180
241
|
...(checkpointFailed ? { checkpointFailed: true } : {}),
|
|
181
242
|
analytics: { startEmitted: false, completeEmitted: false },
|
|
182
243
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// above, and the fallback's .gitignore edit is idempotent.
|
|
189
|
-
const winner = (0, state_lock_1.withRunCreationLock)(root, () => {
|
|
190
|
-
const nowActive = findActiveRunForPlan(root, planHash);
|
|
191
|
-
if (nowActive) {
|
|
192
|
-
return nowActive;
|
|
193
|
-
}
|
|
194
|
-
// The snapshot must exist before run.json makes the run discoverable: a
|
|
195
|
-
// crash in between must not leave an active run without its plan.
|
|
196
|
-
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
197
|
-
(0, fileutils_1.writeJsonFile)((0, path_1.join)(dir, PLAN_SNAPSHOT_0), migrationsJson);
|
|
198
|
-
// The runbook gets the same crash guarantee: a discoverable run always
|
|
199
|
-
// has the runbook a resume re-emits from disk. 'wx' creates without
|
|
200
|
-
// following links, so nothing pre-planted at the path can redirect it.
|
|
244
|
+
(0, state_lock_1.withRunCreationLock)(root, () => {
|
|
245
|
+
// The runbook gets the snapshot's crash guarantee: a discoverable run
|
|
246
|
+
// always has the runbook a resume re-emits from disk. 'wx' creates
|
|
247
|
+
// without following links, so nothing pre-planted at the path can
|
|
248
|
+
// redirect it.
|
|
201
249
|
(0, fs_1.writeFileSync)((0, path_1.join)(dir, runbook_1.RUNBOOK_FILE_NAME), (0, runbook_1.renderRunbook)(runbookContext(root, runId, state)), { flag: 'wx' });
|
|
202
250
|
(0, run_state_1.createRun)(root, state);
|
|
203
|
-
return null;
|
|
204
251
|
});
|
|
205
|
-
if (winner) {
|
|
206
|
-
return resumeRun(root, winner.runId, winner.state, policy, emitAgentInstructions);
|
|
207
|
-
}
|
|
208
252
|
return finishInit(root, dir, runId, state, 'created', emitAgentInstructions);
|
|
209
253
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Continues the active run `runId` names: the `--run-migrations --run-id`
|
|
256
|
+
* shape on both paths. Refuses as holdRunToContinue does.
|
|
257
|
+
*/
|
|
258
|
+
function runOrchestratorResume(input) {
|
|
259
|
+
const { root, runId, policy, emitAgentInstructions = true } = input;
|
|
260
|
+
const state = holdRunToContinue(root, runId);
|
|
261
|
+
return resumeRun(root, runId, state, policy, emitAgentInstructions);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Holds the active run `runId` names and returns its state. A continue calls
|
|
265
|
+
* this before the install and the agent selection: a concurrent start-fresh
|
|
266
|
+
* must not delete the run meanwhile. Throws when the id names no run or a
|
|
267
|
+
* finished one, and while another process holds the run: continuing would
|
|
268
|
+
* open a second session over a live one. Under WASM nothing is held and no
|
|
269
|
+
* holder refuses.
|
|
270
|
+
*/
|
|
271
|
+
function holdRunToContinue(root, runId) {
|
|
272
|
+
if (!run_id_1.RUN_ID_SAFE.test(runId)) {
|
|
273
|
+
throw new Error(`Invalid run id '${runId}'.`);
|
|
274
|
+
}
|
|
275
|
+
const dir = (0, run_state_1.runDir)(root, runId);
|
|
276
|
+
if (!(0, run_state_1.hasRunState)(dir)) {
|
|
277
|
+
throw new Error(`No migrate run '${runId}' was found under ${types_1.MIGRATE_RUNS_RELATIVE_DIR}.`);
|
|
278
|
+
}
|
|
279
|
+
const state = (0, run_state_1.readRunState)(dir);
|
|
280
|
+
if (state.status !== 'active') {
|
|
281
|
+
throw new Error(`Migrate run '${runId}' is already complete; there is nothing to continue.`);
|
|
282
|
+
}
|
|
283
|
+
(0, state_lock_1.holdRunActivity)(root, runId, true);
|
|
284
|
+
return state;
|
|
285
|
+
}
|
|
286
|
+
// The active run a start-fresh naming `runId` finds, read-only: init's first
|
|
287
|
+
// check, which the CLI also runs before the install. None refuses, since a
|
|
288
|
+
// completed run means the plan already ran. A different active run is returned
|
|
289
|
+
// too: init reports it instead of deleting it.
|
|
290
|
+
function activeRunToReplace(root, runId) {
|
|
291
|
+
if (runId !== undefined && !run_id_1.RUN_ID_SAFE.test(runId)) {
|
|
292
|
+
throw new Error(`Invalid run id '${runId}'.`);
|
|
293
|
+
}
|
|
294
|
+
const active = findActiveRunForInit(root, true);
|
|
295
|
+
if (!active) {
|
|
296
|
+
throw new Error(noActiveRunToReplace(runId));
|
|
297
|
+
}
|
|
298
|
+
return active;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Drops the hold holdRunToContinue took, for a wrapper that hands the
|
|
302
|
+
* continue to the workspace-local nx: the child takes its own hold, and an
|
|
303
|
+
* exclusive one refuses while this process still holds.
|
|
304
|
+
*/
|
|
305
|
+
function releaseRunToHandOff(root, runId) {
|
|
306
|
+
(0, state_lock_1.releaseRunActivity)((0, run_state_1.runDir)(root, runId));
|
|
307
|
+
}
|
|
308
|
+
// Reads the newest active run; null when no run is active. Uninterpretable
|
|
309
|
+
// run dirs refuse a fresh start (one of them could be an active run this
|
|
310
|
+
// init would compete with) but only warn when a healthy active run is found,
|
|
311
|
+
// unless `refuseUninterpretable`: a start-fresh would delete that run and
|
|
312
|
+
// then find only the unreadable ones. NewerRunStateFormatError propagates
|
|
313
|
+
// from the read.
|
|
314
|
+
function findActiveRunForInit(root, refuseUninterpretable = false) {
|
|
216
315
|
const { active, uninterpretable } = (0, run_state_1.findActiveRun)(root);
|
|
217
316
|
if (uninterpretable.length > 0) {
|
|
218
317
|
const noun = uninterpretable.length === 1 ? 'directory' : 'directories';
|
|
@@ -221,11 +320,11 @@ function findActiveRunForPlan(root, planHash) {
|
|
|
221
320
|
// Sanitized here rather than left to the gateway: these same lines are
|
|
222
321
|
// joined into the throw below, which leaves through handleErrors.
|
|
223
322
|
const details = uninterpretable.map((u) => `${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${(0, text_1.singleLine)(u.dirName)}: ${(0, text_1.singleLine)(u.reason)}`);
|
|
224
|
-
if (!active) {
|
|
323
|
+
if (!active || refuseUninterpretable) {
|
|
225
324
|
throw new Error([
|
|
226
325
|
`Whether a migrate run is still active could not be determined; starting a new run could re-apply migrations an unfinished run already applied.`,
|
|
227
326
|
...details,
|
|
228
|
-
`Fix or remove the listed ${noun}
|
|
327
|
+
`Fix or remove the listed ${noun}, then re-run the command.`,
|
|
229
328
|
].join('\n'));
|
|
230
329
|
}
|
|
231
330
|
(0, agent_output_1.warnToAgent)({
|
|
@@ -233,22 +332,123 @@ function findActiveRunForPlan(root, planHash) {
|
|
|
233
332
|
bodyLines: details,
|
|
234
333
|
});
|
|
235
334
|
}
|
|
236
|
-
if (active && (0, state_machine_1.latestRound)(active.state)?.planHash !== planHash) {
|
|
237
|
-
throw new Error(`A migrate run '${active.runId}' is already active with a different plan. ` +
|
|
238
|
-
`Finish it first by running \`${reconcileCommand(root, active.runId)}\`, ` +
|
|
239
|
-
`or remove ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${active.runId} to abandon it.`);
|
|
240
|
-
}
|
|
241
335
|
return active;
|
|
242
336
|
}
|
|
243
|
-
//
|
|
244
|
-
//
|
|
337
|
+
// A run directory without run.json but with a live activity lock is an init
|
|
338
|
+
// between reserving the directory and writing run.json: the checkpoint commit
|
|
339
|
+
// is in flight and no run may start alongside it. Refused at once, never
|
|
340
|
+
// waited for: a git hook re-entering nx migrate during that commit would
|
|
341
|
+
// otherwise wait on the process waiting for the hook. An unlocked one is the
|
|
342
|
+
// remains of an init that died, which discovery ignores like any other
|
|
343
|
+
// directory without run.json. Call under the creation lock.
|
|
344
|
+
function refuseLiveReservation(root) {
|
|
345
|
+
for (const entry of (0, fs_1.readdirSync)((0, run_state_1.migrateRunsDir)(root), {
|
|
346
|
+
withFileTypes: true,
|
|
347
|
+
})) {
|
|
348
|
+
if (!entry.isDirectory())
|
|
349
|
+
continue;
|
|
350
|
+
const dir = (0, path_1.join)((0, run_state_1.migrateRunsDir)(root), entry.name);
|
|
351
|
+
if ((0, run_state_1.hasRunState)(dir) || !(0, state_lock_1.hasAnyLiveRunActivity)(dir))
|
|
352
|
+
continue;
|
|
353
|
+
throw new Error(`Another nx migrate process is starting a run (${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${(0, text_1.singleLine)(entry.name)}). Wait for it to finish, then re-run the command.`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// The report for an init that found a run already active. On the agent path
|
|
357
|
+
// the report and both ways forward go to stdout, in a block of its own kind:
|
|
358
|
+
// an `error` block would read as a crash, and this is a decision for the
|
|
359
|
+
// user. Held before the facts are read: a concurrent start-fresh must not
|
|
360
|
+
// delete the run under the report or the prompt after it.
|
|
361
|
+
function reportExistingRun(root, runId, state, plannedIds, migrationsPath, emitAgentInstructions, replaceRunId) {
|
|
362
|
+
(0, state_lock_1.holdRunActivity)(root, runId);
|
|
363
|
+
const facts = (0, existing_run_report_1.collectExistingRunFacts)(root, runId, state, plannedIds);
|
|
364
|
+
if (replaceRunId !== undefined) {
|
|
365
|
+
facts.replacedRunId = replaceRunId;
|
|
366
|
+
}
|
|
367
|
+
(0, migrate_analytics_1.reportMigrateOrchestratorExistingRun)({
|
|
368
|
+
...(0, state_machine_1.runTallies)(state),
|
|
369
|
+
activity: facts.otherHolders === 'unknown'
|
|
370
|
+
? 'unknown'
|
|
371
|
+
: facts.otherHolders.length > 0
|
|
372
|
+
? 'held'
|
|
373
|
+
: 'idle',
|
|
374
|
+
});
|
|
375
|
+
if (emitAgentInstructions) {
|
|
376
|
+
const report = (0, existing_run_report_1.renderExistingRunReport)(facts, (0, existing_run_report_1.renderExistingRunCommands)(root, facts, migrationsPath));
|
|
377
|
+
(0, agent_output_1.logToAgent)(report);
|
|
378
|
+
const lines = (0, agent_output_1.safeLines)([
|
|
379
|
+
report.title,
|
|
380
|
+
...report.bodyLines,
|
|
381
|
+
``,
|
|
382
|
+
`No migration step ran in this response. Show this report to the user and let them choose; run neither command until they do.`,
|
|
383
|
+
]);
|
|
384
|
+
(0, agent_output_1.emitStepBlock)(runId, '-', 'existing-run', {
|
|
385
|
+
instructions: lines.join('\n'),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return { kind: 'existing-run', runId, facts };
|
|
389
|
+
}
|
|
390
|
+
// Throws when the run's record must not be deleted: a worker the run
|
|
391
|
+
// dispensed or another nx migrate process is still acting on it, or another
|
|
392
|
+
// run is active alongside it and would be reported in place of the
|
|
393
|
+
// replacement. `state` is the run state the worker check reads.
|
|
394
|
+
function refuseUndeletableRun(root, runId, state) {
|
|
395
|
+
if (native_1.IS_WASM) {
|
|
396
|
+
throw new Error(`Not deleting migrate run '${runId}': without a native file lock nx cannot tell whether another nx migrate process is acting on it. Make sure none is, remove ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId}, then re-run the command.`);
|
|
397
|
+
}
|
|
398
|
+
const holders = (0, state_lock_1.liveRunActivityPids)((0, run_state_1.runDir)(root, runId));
|
|
399
|
+
if (holders === 'unknown' || holders.length > 0) {
|
|
400
|
+
throw (0, state_lock_1.heldRunError)('deleting', runId, holders);
|
|
401
|
+
}
|
|
402
|
+
const workers = (0, existing_run_report_1.liveWorkers)(state);
|
|
403
|
+
if (workers.length > 0) {
|
|
404
|
+
throw new Error(`Not deleting migrate run '${runId}': ${(0, existing_run_report_1.workersLine)(workers)}. ` +
|
|
405
|
+
`Wait for it to finish, then re-run the command. If that pid is not an nx migrate worker, stop it or remove ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId}, then re-run the command.`);
|
|
406
|
+
}
|
|
407
|
+
const others = (0, run_state_1.findActiveRun)(root).activeRunIds.filter((id) => id !== runId);
|
|
408
|
+
if (others.length > 0) {
|
|
409
|
+
throw new Error(`Not deleting migrate run '${runId}': other migrate runs are active on disk (${others.join(', ')}), and a new run cannot start alongside them. Remove ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/<run id> for each one that should not be continued, then re-run the command.`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function noActiveRunToReplace(runId) {
|
|
413
|
+
return `Not starting fresh: ${runId === undefined ? 'no migrate run' : `no migrate run '${runId}'`} is active, so there is nothing to replace. To start a run, re-run the command without --start-fresh and --run-id.`;
|
|
414
|
+
}
|
|
415
|
+
// Removes run.json under the state lock, so no reconcile or worker can start
|
|
416
|
+
// against a directory about to disappear. The refusals run again on fresh
|
|
417
|
+
// state: the early pass predates the prompt. False when already gone.
|
|
418
|
+
function deleteRunRecord(root, runId) {
|
|
419
|
+
const dir = (0, run_state_1.runDir)(root, runId);
|
|
420
|
+
return (0, state_lock_1.withRunStateLock)(dir, () => {
|
|
421
|
+
if (!(0, run_state_1.hasRunState)(dir))
|
|
422
|
+
return false;
|
|
423
|
+
refuseUndeletableRun(root, runId, (0, run_state_1.readRunState)(dir));
|
|
424
|
+
// The hold's lock file is inside the directory removeDeletedRunDir removes.
|
|
425
|
+
(0, state_lock_1.releaseRunActivity)(dir);
|
|
426
|
+
(0, fs_1.rmSync)((0, path_1.join)(dir, run_state_1.RUN_STATE_FILE_NAME), { force: true });
|
|
427
|
+
return true;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
// The rest of a deleted run's directory, removed outside the locks and best
|
|
431
|
+
// effort: what is left holds no run.
|
|
432
|
+
function removeDeletedRunDir(root, runId) {
|
|
433
|
+
try {
|
|
434
|
+
(0, fs_1.rmSync)((0, run_state_1.runDir)(root, runId), { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
(0, agent_output_1.warnToAgent)({
|
|
438
|
+
title: `Could not remove the rest of ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId}: ${(0, util_1.summarizeError)(e)}. It no longer holds a run; remove it when convenient.`,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
(0, agent_output_1.logToAgent)({ title: `Deleted the record of migrate run ${runId}.` });
|
|
442
|
+
}
|
|
443
|
+
// The continue tail behind runOrchestratorResume: the runbook, checkpoint and
|
|
444
|
+
// analytics steps a paused run needs before it can be driven again.
|
|
245
445
|
function resumeRun(root, runId, state, policy, emitAgentInstructions) {
|
|
246
446
|
// Refused before the checkpoint retry below, which commits.
|
|
247
447
|
if (state.createCommits !== policy.createCommits ||
|
|
248
448
|
(state.skipInstall === true) !== policy.skipInstall) {
|
|
249
449
|
throw new Error(`Nx did not resume the active migrate run because its recorded install and commit policy differs from this invocation's. ` +
|
|
250
|
-
`Nx takes that policy from --create-commits and --skip-install
|
|
251
|
-
`Re-run with
|
|
450
|
+
`Nx takes that policy from --create-commits and --skip-install on the command line. ` +
|
|
451
|
+
`Re-run with the flags run '${runId}' recorded (its report shows them). To start fresh, re-run with --start-fresh --run-id=${runId}, keeping the same --run-migrations[=<path>] argument used to start the run.`);
|
|
252
452
|
}
|
|
253
453
|
const dir = (0, run_state_1.runDir)(root, runId);
|
|
254
454
|
// Ignore/index state can change while a durable run is paused (a checkout,
|
|
@@ -272,23 +472,16 @@ function resumeRun(root, runId, state, policy, emitAgentInstructions) {
|
|
|
272
472
|
return finishInit(root, dir, runId, resumed, 'resumed', emitAgentInstructions, runbook);
|
|
273
473
|
}
|
|
274
474
|
function announceResume(runId, state) {
|
|
275
|
-
const applied = state.steps.filter((s) => s.status === 'succeeded').length;
|
|
276
|
-
const skipped = state.steps.filter((s) => s.status === 'skipped').length;
|
|
277
|
-
const remaining = state.steps.length - applied - skipped;
|
|
278
|
-
// A subset of `remaining`, called out separately: a run is resumed most often
|
|
279
|
-
// because one of these is waiting on a decision, and the count alone would
|
|
280
|
-
// read as work that has not been reached yet.
|
|
281
|
-
const stalled = state.steps.filter((s) => s.status === 'failed' || s.status === 'died').length;
|
|
282
475
|
(0, agent_output_1.logToAgent)({
|
|
283
476
|
title: `nx migrate: resuming run ${runId}`,
|
|
284
477
|
bodyLines: [
|
|
285
478
|
` started: ${state.createdAt}`,
|
|
286
|
-
` progress: ${
|
|
479
|
+
` progress: ${(0, existing_run_report_1.progressLine)((0, state_machine_1.tallySteps)(state))}`,
|
|
287
480
|
],
|
|
288
481
|
});
|
|
289
482
|
}
|
|
290
483
|
// Resume-only checkpoint retry, gated on checkpointFailed: a fresh init always
|
|
291
|
-
// evaluates the checkpoint before
|
|
484
|
+
// evaluates the checkpoint before run.json exists, so an unflagged run
|
|
292
485
|
// without a checkpoint entry started from a clean tree and there is nothing to
|
|
293
486
|
// capture (retrying there would commit the run's own scratch instead). Skipped
|
|
294
487
|
// once any migration step has advanced (a late checkpoint would absorb an
|
|
@@ -382,6 +575,9 @@ runbook) {
|
|
|
382
575
|
});
|
|
383
576
|
}
|
|
384
577
|
}
|
|
578
|
+
if (origin === 'resumed') {
|
|
579
|
+
(0, migrate_analytics_1.reportMigrateOrchestratorResume)((0, state_machine_1.runTallies)(current));
|
|
580
|
+
}
|
|
385
581
|
const content = runbook ?? ensureRunbook(root, dir, runId, current);
|
|
386
582
|
if (content === null) {
|
|
387
583
|
return { kind: 'refused' };
|
|
@@ -438,7 +634,7 @@ function ensureRunbook(root, dir, runId, state) {
|
|
|
438
634
|
if (state.nxVersion !== versions_1.nxVersion) {
|
|
439
635
|
const reason = [
|
|
440
636
|
`The runbook for run '${runId}' is missing from ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId}, and this nx (${versions_1.nxVersion}) cannot re-render the one nx ${(0, text_1.singleLine)(state.nxVersion)} wrote.`,
|
|
441
|
-
`Restore the file (or, if the run was created before runbooks existed, re-run with the nx version that created it),
|
|
637
|
+
`Restore the file (or, if the run was created before runbooks existed, re-run with the nx version that created it). To start fresh, re-run with --start-fresh --run-id=${runId}, keeping the same --run-migrations[=<path>] argument used to start the run.`,
|
|
442
638
|
];
|
|
443
639
|
(0, agent_output_1.warnToAgent)({ title: reason[0], bodyLines: [reason[1]] });
|
|
444
640
|
(0, agent_output_1.emitStepBlock)(runId, '-', 'error', {
|
|
@@ -484,6 +680,24 @@ function runbookContext(root, runId, state) {
|
|
|
484
680
|
validate: state.validate !== false,
|
|
485
681
|
};
|
|
486
682
|
}
|
|
683
|
+
// Only the newest recorded commit is checked, so the warning stops on its own
|
|
684
|
+
// once the next step commits on the new history; checking every ledger entry
|
|
685
|
+
// would repeat it on every reconcile after a rebase.
|
|
686
|
+
function warnIfHistoryRewound(root, runId, state) {
|
|
687
|
+
const commits = (0, existing_run_report_1.recordedCommits)(state);
|
|
688
|
+
const newest = commits[commits.length - 1]?.sha;
|
|
689
|
+
if (!newest)
|
|
690
|
+
return;
|
|
691
|
+
const head = (0, git_utils_1.getLatestCommitSha)(root);
|
|
692
|
+
if (!head || (0, git_utils_1.getAncestorStatus)(newest, head, root) !== 'not-ancestor') {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const facts = (0, existing_run_report_1.collectExistingRunFacts)(root, runId, state);
|
|
696
|
+
(0, agent_output_1.warnToAgent)({
|
|
697
|
+
title: `The newest commit migrate run ${runId} recorded is not reachable from HEAD. Continuing the run as asked.`,
|
|
698
|
+
bodyLines: (0, existing_run_report_1.renderExistingRunReport)(facts).bodyLines,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
487
701
|
async function runOrchestratorReconcile(input) {
|
|
488
702
|
const { root, runId, stepAction } = input;
|
|
489
703
|
if (!run_id_1.RUN_ID_SAFE.test(runId)) {
|
|
@@ -496,8 +710,10 @@ async function runOrchestratorReconcile(input) {
|
|
|
496
710
|
// does something else entirely.
|
|
497
711
|
throw new Error(`No migrate run '${runId}' was found under ${types_1.MIGRATE_RUNS_RELATIVE_DIR}.`);
|
|
498
712
|
}
|
|
713
|
+
(0, state_lock_1.holdRunActivity)(root, runId);
|
|
499
714
|
// Version refusal (NewerRunStateFormatError) propagates.
|
|
500
715
|
let state = (0, run_state_1.readRunState)(dir);
|
|
716
|
+
warnIfHistoryRewound(root, runId, state);
|
|
501
717
|
// Ignore/index state can change while a durable run is paused (a checkout,
|
|
502
718
|
// a .gitignore edit, a forced add); re-verify before foldHandoffs, which
|
|
503
719
|
// can itself commit a settled prompt step.
|
|
@@ -521,9 +737,9 @@ async function runOrchestratorReconcile(input) {
|
|
|
521
737
|
state = detectDeaths(dir, state);
|
|
522
738
|
// (c) apply the decision relay to the single failed/died step.
|
|
523
739
|
if (stepAction) {
|
|
524
|
-
// Owns the tree reservation a clean retry's reset, an adopt's commit
|
|
525
|
-
// skip's install takes, released once the transition that
|
|
526
|
-
// written.
|
|
740
|
+
// Owns the tree reservation a clean retry's reset, an adopt's commit, a
|
|
741
|
+
// give-up or a skip's install takes, released once the transition that
|
|
742
|
+
// records it is written.
|
|
527
743
|
const scope = {};
|
|
528
744
|
try {
|
|
529
745
|
const result = await applyReconcileStepAction(root, dir, state, stepAction, scope);
|
|
@@ -532,21 +748,31 @@ async function runOrchestratorReconcile(input) {
|
|
|
532
748
|
return; // no transition was written
|
|
533
749
|
}
|
|
534
750
|
const target = result.targetStep;
|
|
751
|
+
// Unless a reset ran, give-up.ts commits the partial result under the
|
|
752
|
+
// migration's name and settles the step in one operation; a failed
|
|
753
|
+
// install or commit settles it with the debt recorded.
|
|
754
|
+
if (stepAction === 'unresolved' &&
|
|
755
|
+
state.createCommits &&
|
|
756
|
+
!result.resetTree) {
|
|
757
|
+
const outcome = await (0, broker_1.giveUpStepTree)(root, dir, target, state.skipInstall === true, reconcileCommand(root, runId), scope);
|
|
758
|
+
if (outcome.kind === 'refused') {
|
|
759
|
+
emitError(root, runId, outcome.reason);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
(0, give_up_1.warnAboutGiveUp)(target, outcome);
|
|
763
|
+
// Settled already; the dispense refuses a tree still held.
|
|
764
|
+
scope.lease?.release();
|
|
765
|
+
scope.lease = undefined;
|
|
766
|
+
advanceAndDispense(root, dir, runId);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
535
769
|
// The commit is a git side effect, so it runs before the lock (locked
|
|
536
770
|
// sections stay synchronous); the transition and any unrecorded entry then
|
|
537
771
|
// land in one write. Adopt and skip keep the tree, so the install they owe
|
|
538
772
|
// runs here: the next dispense would take the changed deps as its baseline.
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
? await commitForStep(root, dir, state, target, scope)
|
|
543
|
-
: {
|
|
544
|
-
entry: null,
|
|
545
|
-
installFailed: await installFailedForStep(root, dir, state, target, 'action-install', scope),
|
|
546
|
-
}
|
|
547
|
-
: stepAction === 'skip'
|
|
548
|
-
? await retainedTreeSideEffects(root, dir, state, target, 'action-install', scope)
|
|
549
|
-
: { entry: null, installFailed: false };
|
|
773
|
+
// Giving up keeps it the same way when the run does not commit. Retries
|
|
774
|
+
// owe nothing: the rearmed attempt reconciles itself.
|
|
775
|
+
const { entry, installFailed, recorded } = await stepActionSideEffects(root, dir, state, target, stepAction, result.resetTree, scope);
|
|
550
776
|
// A rearm starts a fresh attempt; drop the stale handoff before the rearm
|
|
551
777
|
// is persisted so a crash in between can't refold the old outcome into the
|
|
552
778
|
// new attempt. Losing the handoff without the rearm is safe: the step is
|
|
@@ -566,6 +792,7 @@ async function runOrchestratorReconcile(input) {
|
|
|
566
792
|
// rejection surfaces through emitError instead of being written over. The
|
|
567
793
|
// bound attempt keeps the snapshot checks above honest.
|
|
568
794
|
let freshRejection;
|
|
795
|
+
let unresolvedArchiveError;
|
|
569
796
|
const written = (0, state_lock_1.updateRunState)(dir, (fresh) => {
|
|
570
797
|
// A plain retry takes no reservation of its own, so this is where it
|
|
571
798
|
// learns that a live process still commits or installs for the step.
|
|
@@ -594,9 +821,16 @@ async function runOrchestratorReconcile(input) {
|
|
|
594
821
|
freshRejection = reapplied.reason;
|
|
595
822
|
return null;
|
|
596
823
|
}
|
|
824
|
+
let settled = reapplied.state;
|
|
825
|
+
// The unresolved status and its issue are persisted together.
|
|
826
|
+
if (stepAction === 'unresolved') {
|
|
827
|
+
const recorded = (0, give_up_1.recordUnresolvedIssue)(dir, settled, target.id);
|
|
828
|
+
unresolvedArchiveError = recorded.archiveError;
|
|
829
|
+
settled = recorded.state;
|
|
830
|
+
}
|
|
597
831
|
const next = installFailed
|
|
598
|
-
? (0, state_machine_1.markInstallFailed)(
|
|
599
|
-
:
|
|
832
|
+
? (0, state_machine_1.markInstallFailed)(settled, target.id)
|
|
833
|
+
: settled;
|
|
600
834
|
// An adopted commit absorbs uncovered failed steps the same way a fold
|
|
601
835
|
// commit does, so it carries their resolved issues too. A session's
|
|
602
836
|
// parent records its own commits as it answers.
|
|
@@ -610,6 +844,9 @@ async function runOrchestratorReconcile(input) {
|
|
|
610
844
|
: freshRejection);
|
|
611
845
|
return;
|
|
612
846
|
}
|
|
847
|
+
if (unresolvedArchiveError !== undefined) {
|
|
848
|
+
(0, give_up_1.warnUnresolvedNotArchived)(target.migrationId, unresolvedArchiveError);
|
|
849
|
+
}
|
|
613
850
|
state = written;
|
|
614
851
|
}
|
|
615
852
|
finally {
|
|
@@ -920,18 +1157,23 @@ async function applyReconcileStepAction(root, dir, state, action, scope) {
|
|
|
920
1157
|
if (held) {
|
|
921
1158
|
return { kind: 'error', reason: (0, broker_1.treeBusyMessage)(held) };
|
|
922
1159
|
}
|
|
1160
|
+
if ((action === 'retry' || action === 'retry-clean') &&
|
|
1161
|
+
rearmCapReached(step)) {
|
|
1162
|
+
return {
|
|
1163
|
+
kind: 'error',
|
|
1164
|
+
reason: `Cannot apply action '${action}' to step '${step.id}': ${rearmCapLine(step, (0, state_machine_1.commitMayBeInHistory)(state, step))}`,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
923
1167
|
// A retry-clean the dispense would not have offered must be refused here
|
|
924
1168
|
// too, or a hand-crafted reconcile could reset a tree with no restore point
|
|
925
1169
|
// and destroy prior steps' work.
|
|
926
1170
|
if (action === 'retry-clean') {
|
|
927
1171
|
const head = (0, git_utils_1.getLatestCommitSha)(root);
|
|
928
|
-
const fallback =
|
|
929
|
-
?
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
? `Use 'retry' instead.`
|
|
934
|
-
: `Use 'retry' or 'skip' instead.`;
|
|
1172
|
+
const fallback = `Use ${actionList([
|
|
1173
|
+
...(step.status === 'died' ? [] : ['retry']),
|
|
1174
|
+
'adopt',
|
|
1175
|
+
...((0, state_machine_1.commitMayBeInHistory)(state, step) ? [] : ['skip', 'unresolved']),
|
|
1176
|
+
])} instead.`;
|
|
935
1177
|
if (!(0, clean_retry_1.canOfferCleanRetry)(root, state, step, head)) {
|
|
936
1178
|
return {
|
|
937
1179
|
kind: 'error',
|
|
@@ -964,6 +1206,35 @@ async function applyReconcileStepAction(root, dir, state, action, scope) {
|
|
|
964
1206
|
};
|
|
965
1207
|
}
|
|
966
1208
|
}
|
|
1209
|
+
// Giving up discards what a generator that never completed left behind
|
|
1210
|
+
// when the same restore point a clean retry needs exists, through the same
|
|
1211
|
+
// reserved reset. When there is no such point the tree is kept: what an
|
|
1212
|
+
// agent authored is never discarded, and a reset here would have no
|
|
1213
|
+
// verified target.
|
|
1214
|
+
const resetTree = action === 'unresolved' &&
|
|
1215
|
+
unresolvedResetsTree(state, step, (0, clean_retry_1.canOfferCleanRetry)(root, state, step, (0, git_utils_1.getLatestCommitSha)(root)));
|
|
1216
|
+
if (resetTree) {
|
|
1217
|
+
try {
|
|
1218
|
+
await (0, broker_1.resetStepTree)(dir, step, () => (0, clean_retry_1.resetForCleanRetry)(root, dir, step.id), scope);
|
|
1219
|
+
}
|
|
1220
|
+
catch (e) {
|
|
1221
|
+
if (e instanceof broker_1.BrokerStaleRequestError ||
|
|
1222
|
+
e instanceof broker_1.BrokerUnavailableError ||
|
|
1223
|
+
e instanceof broker_1.TreeBusyError) {
|
|
1224
|
+
throw e;
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
kind: 'error',
|
|
1228
|
+
reason: `Cannot apply action 'unresolved' to step '${step.id}': the reset to ${step.gitRefBefore} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
if ((0, git_utils_1.getWorkingTreeStatus)(root) !== 'clean') {
|
|
1232
|
+
return {
|
|
1233
|
+
kind: 'error',
|
|
1234
|
+
reason: `Cannot apply action 'unresolved' to step '${step.id}': the working tree is not verifiably clean after the reset to ${step.gitRefBefore}. Inspect it with \`git status\`, then re-run it.`,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
967
1238
|
// A failed generator can have written to the tree before throwing, and a
|
|
968
1239
|
// plain retry reruns it, so a pre-marker retry is accepted only when git
|
|
969
1240
|
// can see nothing of the failed attempt in the tree. The state machine is
|
|
@@ -975,7 +1246,7 @@ async function applyReconcileStepAction(root, dir, state, action, scope) {
|
|
|
975
1246
|
if (safety.kind === 'unsafe') {
|
|
976
1247
|
return {
|
|
977
1248
|
kind: 'error',
|
|
978
|
-
reason: `Cannot apply action 'retry' to step '${step.id}': ${safety.reason} Use 'retry-clean' where offered${(0, state_machine_1.commitMayBeInHistory)(state, step) ? '' : `, or '
|
|
1249
|
+
reason: `Cannot apply action 'retry' to step '${step.id}': ${safety.reason} Use 'retry-clean' where offered${(0, state_machine_1.commitMayBeInHistory)(state, step) ? '' : `, 'skip' or 'unresolved'`}, or 'adopt'.`,
|
|
979
1250
|
};
|
|
980
1251
|
}
|
|
981
1252
|
if (safety.kind === 'warned') {
|
|
@@ -994,20 +1265,67 @@ async function applyReconcileStepAction(root, dir, state, action, scope) {
|
|
|
994
1265
|
if (applied.kind === 'error') {
|
|
995
1266
|
return applied;
|
|
996
1267
|
}
|
|
997
|
-
return { kind: 'ok', state: applied.state, targetStep: step };
|
|
1268
|
+
return { kind: 'ok', state: applied.state, targetStep: step, resetTree };
|
|
1269
|
+
}
|
|
1270
|
+
function actionList(actions) {
|
|
1271
|
+
const quoted = actions.map((a) => `'${a}'`);
|
|
1272
|
+
return quoted.length === 1
|
|
1273
|
+
? quoted[0]
|
|
1274
|
+
: `${quoted.slice(0, -1).join(', ')} or ${quoted[quoted.length - 1]}`;
|
|
1275
|
+
}
|
|
1276
|
+
// The reset runs before the transition, so it is withheld where the
|
|
1277
|
+
// transition refuses the give-up.
|
|
1278
|
+
function unresolvedResetsTree(state, step, cleanRetry) {
|
|
1279
|
+
return (generatorPending(step) && !(0, state_machine_1.commitMayBeInHistory)(state, step) && cleanRetry);
|
|
1280
|
+
}
|
|
1281
|
+
// Git and install side effects run here, before the transition and outside the
|
|
1282
|
+
// synchronous state lock.
|
|
1283
|
+
async function stepActionSideEffects(root, dir, state, step, action, resetTree, scope) {
|
|
1284
|
+
switch (action) {
|
|
1285
|
+
case 'adopt':
|
|
1286
|
+
// A died worker's unrecorded commit request is reused. A failed step, or
|
|
1287
|
+
// a recorded commit, needs an adopt request of its own, or the old answer
|
|
1288
|
+
// would be replayed.
|
|
1289
|
+
return state.createCommits
|
|
1290
|
+
? commitForStep(root, dir, state, step, scope, step.status === 'failed' ||
|
|
1291
|
+
(0, state_machine_1.coveringLandedEntries)(state, step.id).length > 0
|
|
1292
|
+
? 'adopt'
|
|
1293
|
+
: undefined)
|
|
1294
|
+
: {
|
|
1295
|
+
entry: null,
|
|
1296
|
+
installFailed: await installFailedForStep(root, dir, state, step, 'action-install', scope),
|
|
1297
|
+
};
|
|
1298
|
+
case 'unresolved':
|
|
1299
|
+
// A reset restored the tree the step was dispensed against, so nothing
|
|
1300
|
+
// is left to install or to record as debt.
|
|
1301
|
+
if (resetTree)
|
|
1302
|
+
return { entry: null, installFailed: false };
|
|
1303
|
+
// A run that commits settles the step in giveUpStepTree instead.
|
|
1304
|
+
return retainedTreeSideEffects(root, dir, state, step, 'action-install', scope);
|
|
1305
|
+
case 'skip':
|
|
1306
|
+
return retainedTreeSideEffects(root, dir, state, step, 'action-install', scope);
|
|
1307
|
+
case 'retry':
|
|
1308
|
+
case 'retry-clean':
|
|
1309
|
+
return { entry: null, installFailed: false };
|
|
1310
|
+
default: {
|
|
1311
|
+
const exhaustive = action;
|
|
1312
|
+
throw new Error(`Unhandled step action '${exhaustive}'.`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
998
1315
|
}
|
|
999
|
-
// Commits the working tree left by a folded prompt outcome or an adopted
|
|
1000
|
-
//
|
|
1001
|
-
// `recorded` says a session's parent already did; null when nothing to
|
|
1316
|
+
// Commits the working tree left by a folded prompt outcome or an adopted step
|
|
1317
|
+
// (failed or died). The caller persists `entry` with the step transition
|
|
1318
|
+
// unless `recorded` says a session's parent already did; null when nothing to
|
|
1002
1319
|
// commit. A crash between the git commit and the state write leaves that
|
|
1003
|
-
// commit in history and out of the ledger
|
|
1004
|
-
|
|
1320
|
+
// commit in history and out of the ledger, with the failures it absorbed
|
|
1321
|
+
// uncovered; completion rechecks the tree before warning about that debt.
|
|
1322
|
+
async function commitForStep(root, dir, state, step, scope, commitAs) {
|
|
1005
1323
|
const { name } = (0, state_machine_1.splitMigrationId)(step.migrationId);
|
|
1006
1324
|
const absorbedStepIds = (0, state_machine_1.uncoveredFailedStepIds)(state).filter((id) => id !== step.id);
|
|
1007
1325
|
const skipInstall = state.skipInstall === true;
|
|
1008
1326
|
let commit;
|
|
1009
1327
|
try {
|
|
1010
|
-
commit = await (0, broker_1.commitStepTree)(dir, step, absorbedStepIds, () => (0, migrate_commits_1.commitMigrationIfRequested)(root, { name }, true, state.commitPrefix, () => (0, util_1.installDepsChangedSinceDispense)(root, dir, step, skipInstall, reconcileCommand(root, state.runId)), (0, state_machine_1.stepsToPendingMigrations)(state, absorbedStepIds)), scope);
|
|
1328
|
+
commit = await (0, broker_1.commitStepTree)(dir, step, absorbedStepIds, () => (0, migrate_commits_1.commitMigrationIfRequested)(root, { name }, true, state.commitPrefix, () => (0, util_1.installDepsChangedSinceDispense)(root, dir, step, skipInstall, reconcileCommand(root, state.runId)), (0, state_machine_1.stepsToPendingMigrations)(state, absorbedStepIds)), scope, commitAs);
|
|
1011
1329
|
}
|
|
1012
1330
|
catch (e) {
|
|
1013
1331
|
// Nothing to record: the step moved on, another process holds the tree, or
|
|
@@ -1063,7 +1381,7 @@ function advanceAndDispense(root, dir, runId) {
|
|
|
1063
1381
|
// operation with a live worker keeps still-running, which offers none.
|
|
1064
1382
|
const held = (0, broker_1.liveTreeOperation)(state);
|
|
1065
1383
|
if (held && !isOwnOperation(step, held)) {
|
|
1066
|
-
emitHeld(root, runId, step, held, noProgress);
|
|
1384
|
+
emitHeld(root, runId, state, step, held, noProgress);
|
|
1067
1385
|
return;
|
|
1068
1386
|
}
|
|
1069
1387
|
switch (step.status) {
|
|
@@ -1081,13 +1399,14 @@ function advanceAndDispense(root, dir, runId) {
|
|
|
1081
1399
|
emitDied(root, runId, state, step, noProgress);
|
|
1082
1400
|
break;
|
|
1083
1401
|
case 'running':
|
|
1084
|
-
emitStillRunning(root, runId, step, held, noProgress);
|
|
1402
|
+
emitStillRunning(root, runId, state, step, held, noProgress);
|
|
1085
1403
|
break;
|
|
1086
1404
|
case 'awaiting-prompt-outcome':
|
|
1087
1405
|
emitAwaitPrompt(root, dir, runId, step, noProgress);
|
|
1088
1406
|
break;
|
|
1089
1407
|
case 'succeeded':
|
|
1090
1408
|
case 'skipped':
|
|
1409
|
+
case 'unresolved':
|
|
1091
1410
|
// firstActionableStep already excludes these via TERMINAL_STEP_STATUSES;
|
|
1092
1411
|
// landing here means an already-terminal step slipped through
|
|
1093
1412
|
// unclassified rather than being left to stall the run silently.
|
|
@@ -1183,14 +1502,19 @@ function dispenseNextStep(root, dir, runId, state, step, noProgress) {
|
|
|
1183
1502
|
return;
|
|
1184
1503
|
}
|
|
1185
1504
|
if (held) {
|
|
1186
|
-
emitHeld(root, runId, step, held, noProgress);
|
|
1505
|
+
emitHeld(root, runId, current, step, held, noProgress);
|
|
1187
1506
|
return;
|
|
1188
1507
|
}
|
|
1189
|
-
|
|
1508
|
+
const dispensed = current.steps.find((s) => s.id === step.id);
|
|
1509
|
+
(0, migrate_analytics_1.reportMigrateOrchestratorStepDispensed)({
|
|
1510
|
+
attempt: dispensed.attempt,
|
|
1511
|
+
ordinal: (0, state_machine_1.runTallies)(current).dispenseCount,
|
|
1512
|
+
});
|
|
1513
|
+
emitNextStep(root, runId, current, dispensed, noProgress);
|
|
1190
1514
|
}
|
|
1191
1515
|
function emitNextStep(root, runId, state, step, noProgress) {
|
|
1192
1516
|
const migrationId = step.migrationId;
|
|
1193
|
-
emit(root, runId, step, 'next-step', {
|
|
1517
|
+
emit(root, runId, state, step, 'next-step', {
|
|
1194
1518
|
command: workerCommand(root, migrationId, runId),
|
|
1195
1519
|
next: reconcileCommand(root, runId),
|
|
1196
1520
|
instructionLines: [
|
|
@@ -1207,6 +1531,8 @@ function emitRetryFailed(root, runId, state, step, noProgress) {
|
|
|
1207
1531
|
const head = (0, git_utils_1.getLatestCommitSha)(root);
|
|
1208
1532
|
const tree = dirtyTreeSummary(root);
|
|
1209
1533
|
const cleanRetry = (0, clean_retry_1.canOfferCleanRetry)(root, state, step, head);
|
|
1534
|
+
const landed = lastCoveringLandedEntry(state, step);
|
|
1535
|
+
const committed = (0, state_machine_1.commitMayBeInHistory)(state, step);
|
|
1210
1536
|
// A failure recorded before the generator marker can still have written to
|
|
1211
1537
|
// the tree (a direct fs or exec side effect, or a crash mid-flush); a
|
|
1212
1538
|
// marker means only the handed-back half (a prompt or a validation pass)
|
|
@@ -1223,27 +1549,31 @@ function emitRetryFailed(root, runId, state, step, noProgress) {
|
|
|
1223
1549
|
` current HEAD: ${head ?? '(unknown)'}`,
|
|
1224
1550
|
` working tree: ${tree === null ? '(unknown)' : tree ? `\n${tree}` : '(clean)'}`,
|
|
1225
1551
|
``,
|
|
1226
|
-
|
|
1552
|
+
retryBudgetLine(step, committed),
|
|
1553
|
+
``,
|
|
1227
1554
|
`Decide how to proceed and re-run reconcile with one of:`,
|
|
1228
|
-
|
|
1555
|
+
...(capReached
|
|
1556
|
+
? []
|
|
1557
|
+
: [retryOptionLine(retrySafety, reconcileCommand(root, runId, 'retry'))]),
|
|
1229
1558
|
];
|
|
1230
|
-
if (cleanRetry) {
|
|
1559
|
+
if (cleanRetry && !capReached) {
|
|
1231
1560
|
lines.push(` retry-clean: reset the tree to ${step.gitRefBefore ?? 'the pre-migration ref'} (discarding its uncommitted tracked changes and the untracked files git does not ignore, ${types_1.MIGRATE_RUNS_RELATIVE_DIR} kept) and retry from that clean state by running: ${reconcileCommand(root, runId, 'retry-clean')}`);
|
|
1232
1561
|
}
|
|
1562
|
+
lines.push(landed
|
|
1563
|
+
? ` adopt: keep the landed commit${landed.sha ? ` ${landed.sha}` : ''} and the current working-tree state as the migration's result, then run: ${reconcileCommand(root, runId, 'adopt')}`
|
|
1564
|
+
: ` adopt: ${committed
|
|
1565
|
+
? "a commit of this migration's changes was started and never recorded"
|
|
1566
|
+
: 'the migration was applied by hand'}; keep the current working-tree state as its result, then run: ${reconcileCommand(root, runId, 'adopt')}`);
|
|
1233
1567
|
// Refused by the state machine: the migration is, or may be, committed.
|
|
1234
|
-
if (!
|
|
1235
|
-
lines.push(` skip: ${reconcileCommand(root, runId, 'skip')}
|
|
1568
|
+
if (!committed) {
|
|
1569
|
+
lines.push(` skip: ${reconcileCommand(root, runId, 'skip')}`, unresolvedOptionLine(root, runId, state, step, unresolvedResetsTree(state, step, cleanRetry)));
|
|
1236
1570
|
}
|
|
1237
1571
|
if (pending) {
|
|
1238
1572
|
lines.push(UNVERIFIABLE_WRITES_LINE);
|
|
1239
1573
|
}
|
|
1240
|
-
//
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1243
|
-
// writes nothing here could see. Choosing a retry has to be explicit. The
|
|
1244
|
-
// rearm cap withholds it too: each rearm resets the response streak, so a
|
|
1245
|
-
// blindly-followed retry `next` would loop past every escalation.
|
|
1246
|
-
emit(root, runId, step, 'retry-failed', {
|
|
1574
|
+
// No preselected retry while the generator may still run: git cannot vouch
|
|
1575
|
+
// for every write. Past the cap no retry is offered or accepted.
|
|
1576
|
+
emit(root, runId, state, step, 'retry-failed', {
|
|
1247
1577
|
...(pending || capReached
|
|
1248
1578
|
? {}
|
|
1249
1579
|
: { next: reconcileCommand(root, runId, 'retry') }),
|
|
@@ -1255,11 +1585,21 @@ function emitRetryFailed(root, runId, state, step, noProgress) {
|
|
|
1255
1585
|
function rearmCapReached(step) {
|
|
1256
1586
|
return step.attempt - 1 >= REARM_ESCALATION_CAP;
|
|
1257
1587
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
return `
|
|
1588
|
+
function retryBudgetLine(step, committed) {
|
|
1589
|
+
if (rearmCapReached(step))
|
|
1590
|
+
return rearmCapLine(step, committed);
|
|
1591
|
+
const left = REARM_ESCALATION_CAP - (step.attempt - 1);
|
|
1592
|
+
return `Retries left for this migration: ${left}. Diagnose the failure first and retry only with a plausible fix in hand; ${left === 1
|
|
1593
|
+
? 'this is the last one, so ask the user before using it'
|
|
1594
|
+
: 'ask the user before using the last one'}. When no user can answer, ${committed
|
|
1595
|
+
? 'adopt only once you have inspected the tree and finished the migration; otherwise stop and report'
|
|
1596
|
+
: 'give the step up with unresolved and continue'}.`;
|
|
1597
|
+
}
|
|
1598
|
+
// Opens a capped dispense and is the reason a retry past the cap is refused.
|
|
1599
|
+
function rearmCapLine(step, committed) {
|
|
1600
|
+
return `This migration has already been retried ${step.attempt - 1} times without completing, and no further retry is accepted. ${committed
|
|
1601
|
+
? 'Adopt only once you have inspected the tree and finished the migration; otherwise ask the user how to proceed, or stop and report.'
|
|
1602
|
+
: 'Choose adopt, skip or unresolved, or ask the user how to proceed.'}`;
|
|
1263
1603
|
}
|
|
1264
1604
|
// Whether the step's generator half may still have to run: it exists and no
|
|
1265
1605
|
// attempt has recorded running it. Only then can a retry apply a generator
|
|
@@ -1320,12 +1660,31 @@ function assessPreMarkerRetry(root, step) {
|
|
|
1320
1660
|
}
|
|
1321
1661
|
return { kind: 'safe' };
|
|
1322
1662
|
}
|
|
1663
|
+
// Whichever attempt landed it, the commit is this migration's result.
|
|
1664
|
+
function lastCoveringLandedEntry(state, step) {
|
|
1665
|
+
const entries = (0, state_machine_1.coveringLandedEntries)(state, step.id);
|
|
1666
|
+
return entries.length > 0 ? entries[entries.length - 1] : null;
|
|
1667
|
+
}
|
|
1668
|
+
// The give-up option, worded for what happens to the tree: the reserved
|
|
1669
|
+
// reset, a partial commit, or the tree left as it stands.
|
|
1670
|
+
function unresolvedOptionLine(root, runId, state, step, resetTree) {
|
|
1671
|
+
const command = reconcileCommand(root, runId, 'unresolved');
|
|
1672
|
+
const recorded = `The failure is recorded as a run issue and listed in the completion report.`;
|
|
1673
|
+
if (resetTree) {
|
|
1674
|
+
return ` unresolved: give up on this migration, resetting the tree to ${step.gitRefBefore ?? 'the pre-migration ref'} (discarding what the failed attempt left, ${types_1.MIGRATE_RUNS_RELATIVE_DIR} kept), then run: ${command}. ${recorded}`;
|
|
1675
|
+
}
|
|
1676
|
+
if (state.createCommits) {
|
|
1677
|
+
return ` unresolved: give up on this migration; its partial changes are committed under its name, marked unresolved, and the run moves on. Then run: ${command}. ${recorded}`;
|
|
1678
|
+
}
|
|
1679
|
+
return ` unresolved: give up on this migration, leaving the tree as it stands, and move on. Then run: ${command}. ${recorded}`;
|
|
1680
|
+
}
|
|
1323
1681
|
function emitDied(root, runId, state, step, noProgress) {
|
|
1324
1682
|
const migrationId = step.migrationId;
|
|
1325
1683
|
const ref = step.gitRefBefore;
|
|
1326
1684
|
const head = (0, git_utils_1.getLatestCommitSha)(root);
|
|
1327
1685
|
const tree = dirtyTreeSummary(root);
|
|
1328
1686
|
const cleanRetry = (0, clean_retry_1.canOfferCleanRetry)(root, state, step, head);
|
|
1687
|
+
const committed = (0, state_machine_1.commitMayBeInHistory)(state, step);
|
|
1329
1688
|
const resume = !generatorPending(step);
|
|
1330
1689
|
const capReached = rearmCapReached(step);
|
|
1331
1690
|
const lines = [
|
|
@@ -1334,49 +1693,46 @@ function emitDied(root, runId, state, step, noProgress) {
|
|
|
1334
1693
|
` current HEAD: ${head ?? '(unknown)'}`,
|
|
1335
1694
|
` working tree: ${tree === null ? '(unknown)' : tree ? `\n${tree}` : '(clean)'}`,
|
|
1336
1695
|
``,
|
|
1337
|
-
|
|
1696
|
+
retryBudgetLine(step, committed),
|
|
1697
|
+
``,
|
|
1338
1698
|
];
|
|
1339
1699
|
const options = [];
|
|
1340
|
-
if (resume) {
|
|
1700
|
+
if (resume && !capReached) {
|
|
1341
1701
|
options.push(` retry: keep everything this migration already produced (its commit, if any, and the current tree) and run only the part that did not complete, then run: ${reconcileCommand(root, runId, 'retry')}`);
|
|
1342
1702
|
}
|
|
1343
|
-
if (cleanRetry) {
|
|
1703
|
+
if (cleanRetry && !capReached) {
|
|
1344
1704
|
options.push(` retry-clean: reset the tree to ${ref ?? 'the pre-migration ref'} (discarding its uncommitted tracked changes and the untracked files git does not ignore, ${types_1.MIGRATE_RUNS_RELATIVE_DIR} kept) and retry from that clean state by running: ${reconcileCommand(root, runId, 'retry-clean')}`);
|
|
1345
1705
|
}
|
|
1346
|
-
else {
|
|
1706
|
+
else if (!capReached) {
|
|
1347
1707
|
lines.push(`A clean retry is unavailable: ${(0, clean_retry_1.cleanRetryUnavailableReason)(root, state, step, head)}`);
|
|
1348
1708
|
}
|
|
1349
1709
|
options.push(` adopt: keep the current working-tree state as this migration's result, then run: ${reconcileCommand(root, runId, 'adopt')}`);
|
|
1350
1710
|
// Refused by the state machine: the migration is, or may be, committed.
|
|
1351
|
-
if (!
|
|
1352
|
-
options.push(` skip: leave the tree as it stands and move on without this migration, then run: ${reconcileCommand(root, runId, 'skip')}
|
|
1711
|
+
if (!committed) {
|
|
1712
|
+
options.push(` skip: leave the tree as it stands and move on without this migration, then run: ${reconcileCommand(root, runId, 'skip')}`, unresolvedOptionLine(root, runId, state, step, unresolvedResetsTree(state, step, cleanRetry)));
|
|
1353
1713
|
}
|
|
1354
1714
|
lines.push(`Choose exactly one:`);
|
|
1355
1715
|
lines.push(...options);
|
|
1356
1716
|
if (!resume) {
|
|
1357
1717
|
lines.push(UNVERIFIABLE_WRITES_LINE);
|
|
1358
1718
|
}
|
|
1359
|
-
// `retry` is preselected
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
|
|
1363
|
-
// nothing checked, so an agent that follows `next` blindly must land on
|
|
1364
|
-
// neither. The rearm cap withholds it for the same reason as in
|
|
1365
|
-
// emitRetryFailed.
|
|
1366
|
-
emit(root, runId, step, 'died', {
|
|
1719
|
+
// `retry` is preselected only when no generator remains to run and the cap
|
|
1720
|
+
// allows it: a reset cannot be verified against writes git does not see, and
|
|
1721
|
+
// adopting records a result nothing checked, so neither is automatic.
|
|
1722
|
+
emit(root, runId, state, step, 'died', {
|
|
1367
1723
|
...(resume && !capReached
|
|
1368
1724
|
? { next: reconcileCommand(root, runId, 'retry') }
|
|
1369
1725
|
: {}),
|
|
1370
1726
|
instructionLines: lines,
|
|
1371
1727
|
}, noProgress);
|
|
1372
1728
|
}
|
|
1373
|
-
function emitHeld(root, runId, step, held, noProgress) {
|
|
1374
|
-
emit(root, runId, step, 'held', {
|
|
1729
|
+
function emitHeld(root, runId, state, step, held, noProgress) {
|
|
1730
|
+
emit(root, runId, state, step, 'held', {
|
|
1375
1731
|
next: reconcileCommand(root, runId),
|
|
1376
1732
|
instructionLines: [(0, broker_1.treeBusyMessage)(held)],
|
|
1377
1733
|
}, noProgress);
|
|
1378
1734
|
}
|
|
1379
|
-
function emitStillRunning(root, runId, step, held, noProgress) {
|
|
1735
|
+
function emitStillRunning(root, runId, state, step, held, noProgress) {
|
|
1380
1736
|
const migrationId = step.migrationId;
|
|
1381
1737
|
const ageMs = step.startedAt ? Date.now() - Date.parse(step.startedAt) : 0;
|
|
1382
1738
|
const lines = [
|
|
@@ -1395,7 +1751,7 @@ function emitStillRunning(root, runId, step, held, noProgress) {
|
|
|
1395
1751
|
? `${hung} Report this to the user: they can quit this session, then press Ctrl+C in the terminal to end ${parentOperation}, and resume the run afterwards.`
|
|
1396
1752
|
: `${hung} Verify pid ${step.pid}; either keep waiting, or kill it so the next reconcile can classify it as died.`);
|
|
1397
1753
|
}
|
|
1398
|
-
emit(root, runId, step, 'still-running', {
|
|
1754
|
+
emit(root, runId, state, step, 'still-running', {
|
|
1399
1755
|
next: reconcileCommand(root, runId),
|
|
1400
1756
|
instructionLines: lines,
|
|
1401
1757
|
}, noProgress);
|
|
@@ -1493,7 +1849,7 @@ function emitAwaitPrompt(root, dir, runId, step, noProgress) {
|
|
|
1493
1849
|
if (rejection.length > 0) {
|
|
1494
1850
|
lines.push('', ...rejection);
|
|
1495
1851
|
}
|
|
1496
|
-
emit(root, runId, step, 'await-prompt', {
|
|
1852
|
+
emit(root, runId, claimed, step, 'await-prompt', {
|
|
1497
1853
|
next: reconcileCommand(root, runId),
|
|
1498
1854
|
instructionLines: lines,
|
|
1499
1855
|
}, noProgress);
|
|
@@ -1622,9 +1978,6 @@ function completionWarnings(root, runId, state) {
|
|
|
1622
1978
|
}
|
|
1623
1979
|
function completeRun(root, dir, runId, state) {
|
|
1624
1980
|
let current = state;
|
|
1625
|
-
const completed = current.steps.filter((s) => s.status === 'succeeded').length;
|
|
1626
|
-
const skipped = current.steps.filter((s) => s.status === 'skipped').length;
|
|
1627
|
-
const dispenseCount = current.steps.reduce((n, s) => n + s.dispenseCount, 0);
|
|
1628
1981
|
// Persist the terminal status and claim the watermark in one fresh-state
|
|
1629
1982
|
// write before emitting: a crash between the write and the output can't
|
|
1630
1983
|
// double-count the completion, and of two concurrent reconciles exactly one
|
|
@@ -1644,11 +1997,7 @@ function completeRun(root, dir, runId, state) {
|
|
|
1644
1997
|
});
|
|
1645
1998
|
}
|
|
1646
1999
|
if (shouldEmit) {
|
|
1647
|
-
(0, migrate_analytics_1.reportMigrateOrchestratorComplete)(
|
|
1648
|
-
completed,
|
|
1649
|
-
skipped,
|
|
1650
|
-
dispenseCount,
|
|
1651
|
-
});
|
|
2000
|
+
(0, migrate_analytics_1.reportMigrateOrchestratorComplete)((0, state_machine_1.runTallies)(current));
|
|
1652
2001
|
}
|
|
1653
2002
|
const warnings = completionWarnings(root, runId, current);
|
|
1654
2003
|
for (const lines of warnings) {
|
|
@@ -1656,8 +2005,7 @@ function completeRun(root, dir, runId, state) {
|
|
|
1656
2005
|
}
|
|
1657
2006
|
const instructionLines = [
|
|
1658
2007
|
`Migrate run ${runId} is complete.`,
|
|
1659
|
-
|
|
1660
|
-
` skipped: ${skipped}`,
|
|
2008
|
+
...(0, state_machine_1.completionSummaryLines)(current),
|
|
1661
2009
|
...warnings.flat(),
|
|
1662
2010
|
];
|
|
1663
2011
|
(0, agent_output_1.logToAgent)({ title: 'nx migrate: complete', bodyLines: instructionLines });
|
|
@@ -1665,7 +2013,7 @@ function completeRun(root, dir, runId, state) {
|
|
|
1665
2013
|
instructions: instructionLines.join('\n'),
|
|
1666
2014
|
});
|
|
1667
2015
|
}
|
|
1668
|
-
function emit(root, runId, step, action, payload, noProgress) {
|
|
2016
|
+
function emit(root, runId, state, step, action, payload, noProgress) {
|
|
1669
2017
|
// At the escalation threshold the whole response becomes the 'no-progress'
|
|
1670
2018
|
// action, with the step's own instructions kept below so acting on them stays
|
|
1671
2019
|
// possible. Never a step status: nothing durable changed, and a status would
|
|
@@ -1690,6 +2038,7 @@ function emit(root, runId, step, action, payload, noProgress) {
|
|
|
1690
2038
|
(0, migrate_analytics_1.reportMigrateOrchestratorDispense)({
|
|
1691
2039
|
action: effectiveAction,
|
|
1692
2040
|
attempt: step.attempt,
|
|
2041
|
+
ordinal: (0, state_machine_1.runTallies)(state).dispenseCount,
|
|
1693
2042
|
});
|
|
1694
2043
|
}
|
|
1695
2044
|
function noProgressLines(step, streak) {
|