@empire-builder-kit/nx 1.0.0-rc.44 → 1.0.0-rc.46
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/package.json +3 -3
- package/src/blueprints/external-sandbox.d.ts +1 -1
- package/src/cli/main.d.ts +11 -5
- package/src/cli/main.js +88 -29
- package/src/container-images.d.ts +1 -1
- package/src/container-images.js +1 -1
- package/src/developer-agent/generator-planning.js +10 -3
- package/src/developer-agent/operation-registry.js +1 -1
- package/src/executors/deployment-plan/deployment-plan.js +5 -1
- package/src/executors/deployment-plan/schema.json +1 -1
- package/src/executors/fleet/fleet.js +4 -0
- package/src/executors/fleet/schema.json +1 -1
- package/src/executors/github-bootstrap/github-bootstrap.d.ts +3 -0
- package/src/executors/github-bootstrap/github-bootstrap.js +28 -0
- package/src/executors/github-bootstrap/schema.json +14 -0
- package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
- package/src/executors/local-deploy/federated-credential-transport.js +15 -5
- package/src/foundation/cost-estimate.d.ts +8 -3
- package/src/foundation/cost-estimate.js +42 -19
- package/src/foundation/index.d.ts +1 -0
- package/src/foundation/index.js +1 -0
- package/src/foundation/local-private-smoke-access.js +6 -0
- package/src/foundation/logging.d.ts +3 -1
- package/src/foundation/logging.js +8 -1
- package/src/foundation/observability.d.ts +42 -0
- package/src/foundation/observability.js +181 -0
- package/src/foundation/preflight.js +1 -1
- package/src/generators/function/function.js +5 -2
- package/src/generators/observability-infrastructure.d.ts +2 -0
- package/src/generators/observability-infrastructure.js +31 -0
- package/src/generators/observability-policy.d.ts +4 -0
- package/src/generators/observability-policy.js +23 -0
- package/src/generators/preset/dependency-overrides.d.ts +17 -6
- package/src/generators/preset/dependency-overrides.js +17 -6
- package/src/generators/preset/tool-versions.d.ts +2 -2
- package/src/generators/preset/tool-versions.js +2 -2
- package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
- package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
- package/src/generators/preset/workspace-files-agent-development.js +42 -0
- package/src/generators/preset/workspace-files-thin-deploy.js +17 -4
- package/src/generators/preset/workspace-files-thin-docs.js +10 -22
- package/src/generators/preset/workspace-files.d.ts +1 -0
- package/src/generators/preset/workspace-files.js +120 -35
- package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
- package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
- package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
- package/src/generators/slice/built-in-plan-data-files.js +10 -3
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
- package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
- package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
- package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
- package/src/generators/slice/built-in-plan-metadata-files.js +37 -2
- package/src/generators/slice/built-in-plan-operations-files.js +106 -6
- package/src/generators/slice/built-in-plan-project-files.js +1 -0
- package/src/generators/slice/built-in-plan-release-files.js +2 -44
- package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
- package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
- package/src/generators/slice/built-in-plan-shared.js +29 -2
- package/src/generators/slice/built-in-plan-verification-files.js +0 -1
- package/src/generators/slice/built-in-plan-web-files.js +22 -6
- package/src/generators/slice/built-in-plan.js +2 -2
- package/src/generators/slice/slice.js +2 -0
- package/src/generators/upgrade/upgrade.d.ts +4 -3
- package/src/generators/upgrade/upgrade.js +139 -29
- package/src/generators/workload/frozen-container.d.ts +1 -1
- package/src/generators/workload/reliability-registration.d.ts +1 -1
- package/src/generators/workload/reliability-registration.js +9 -9
- package/src/generators/workload/workload.d.ts +1 -1
- package/src/generators/workload/workload.js +32 -10
- package/src/git-policy/github-bootstrap.d.ts +18 -0
- package/src/git-policy/github-bootstrap.js +99 -6
- package/src/local-dev/dev-command-core.d.ts +24 -3
- package/src/local-dev/dev-command-core.js +124 -12
- package/src/local-dev/errors.d.ts +19 -1
- package/src/local-dev/errors.js +18 -1
- package/src/local-dev/fleet-client.d.ts +64 -6
- package/src/local-dev/fleet-client.js +189 -15
- package/src/local-dev/fleet-process.d.ts +47 -2
- package/src/local-dev/fleet-process.js +248 -31
- package/src/local-dev/fleet-runtime.d.ts +37 -0
- package/src/local-dev/fleet-runtime.js +127 -69
- package/src/local-dev/fleet-session.d.ts +1 -0
- package/src/local-dev/fleet-session.js +83 -7
- package/src/local-dev/fleet-state.d.ts +5 -0
- package/src/local-dev/fleet-state.js +11 -2
- package/src/local-dev/fleet-supervisor-daemon.js +1 -5
- package/src/local-dev/fleet-supervisor.d.ts +7 -0
- package/src/local-dev/fleet-supervisor.js +83 -12
- package/src/local-dev/slice-runner.d.ts +19 -1
- package/src/local-dev/slice-runner.js +290 -91
- package/src/repository-policy/deployment-plan.d.ts +1 -1
- package/src/repository-policy/deployment-plan.js +16 -1
- package/src/secrets/generated-aws-control-plane.d.ts +11 -1
- package/src/secrets/generated-aws-control-plane.js +92 -29
- package/src/secrets/operator.d.ts +3 -2
- package/src/secrets/operator.js +27 -5
- package/src/supply-chain/blueprint-verification-authority.js +24 -3
- package/src/supply-chain/generated-files.js +9 -0
- package/src/supply-chain/sbom.js +74 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveDevelopmentRouter = resolveDevelopmentRouter;
|
|
4
|
+
exports.createFleetSliceRunnerEffects = createFleetSliceRunnerEffects;
|
|
4
5
|
exports.startFleetRuntime = startFleetRuntime;
|
|
5
6
|
const node_fs_1 = require("node:fs");
|
|
6
7
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -71,6 +72,111 @@ async function resolveDevelopmentRouter(request) {
|
|
|
71
72
|
throw new errors_js_1.LocalDevContractError('foundation-unavailable', `Development Foundation discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
73
|
}
|
|
73
74
|
}
|
|
75
|
+
/** One Slice's Fleet process effects: a coordinating runner per launch,
|
|
76
|
+
* generation-bound restart and quiescence, and cleanup whose retry output
|
|
77
|
+
* joins the Slice log store. startFleetRuntime and the real-runner
|
|
78
|
+
* quiescence specs share this wiring, so a dropped effect fails those specs. */
|
|
79
|
+
function createFleetSliceRunnerEffects(input, daemon = {}) {
|
|
80
|
+
const { logStore, name } = input;
|
|
81
|
+
let activeRunner;
|
|
82
|
+
let activeAttempt = 0;
|
|
83
|
+
const runnersByChild = new WeakMap();
|
|
84
|
+
const project = input.projects[name];
|
|
85
|
+
const nativeProjectRoot = project?.targets?.[input.adapter.target]?.executor ===
|
|
86
|
+
'@empire-builder-kit/nx:dev'
|
|
87
|
+
? (0, node_path_1.join)(input.workspaceRealpath, project.root)
|
|
88
|
+
: undefined;
|
|
89
|
+
const effects = {
|
|
90
|
+
...(nativeProjectRoot
|
|
91
|
+
? {
|
|
92
|
+
nativeDeployment: async (signal) => activeRunner
|
|
93
|
+
? (0, native_completion_js_1.readNativeDevelopmentCompletion)({
|
|
94
|
+
projectRoot: nativeProjectRoot,
|
|
95
|
+
stage: input.stage,
|
|
96
|
+
startedAt: activeRunner.request.createdAt,
|
|
97
|
+
}, signal)
|
|
98
|
+
: 'pending',
|
|
99
|
+
applicationStarted: () => Boolean(activeRunner &&
|
|
100
|
+
(0, fleet_web_start_js_1.hasFleetWebStarted)(activeRunner.requestPath, activeRunner.request)),
|
|
101
|
+
}
|
|
102
|
+
: {}),
|
|
103
|
+
cleanup: async () => {
|
|
104
|
+
if (!activeRunner) {
|
|
105
|
+
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
(0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// A runner that already exited (or whose own cleanup failed)
|
|
112
|
+
// is retried by a separate runner; keep its output in the
|
|
113
|
+
// same Slice log as the in-runner cleanup.
|
|
114
|
+
const attempt = activeAttempt;
|
|
115
|
+
try {
|
|
116
|
+
await (0, slice_runner_js_1.retryFleetSliceRunnerCleanup)(activeRunner, {
|
|
117
|
+
...daemon,
|
|
118
|
+
output: (stream, chunk) => {
|
|
119
|
+
logStore.writeChunk(name, stream, attempt, chunk);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
logStore.flush(name, undefined, attempt);
|
|
125
|
+
}
|
|
126
|
+
(0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
|
|
127
|
+
}
|
|
128
|
+
await (0, slice_runner_js_1.recoverFleetSliceRunnerCleanupsForSlices)({
|
|
129
|
+
fleetId: activeRunner.request.fleetId,
|
|
130
|
+
sessionGeneration: activeRunner.request.sessionGeneration,
|
|
131
|
+
}, [name], input.runtimePaths, daemon);
|
|
132
|
+
},
|
|
133
|
+
launch: (attempt) => {
|
|
134
|
+
activeAttempt = attempt;
|
|
135
|
+
activeRunner = (0, slice_runner_js_1.launchFleetSliceRunner)({
|
|
136
|
+
origin: input.origin,
|
|
137
|
+
parent: input.parent,
|
|
138
|
+
port: input.port,
|
|
139
|
+
runnerGeneration: `${input.sessionGeneration}:${name}:${attempt}:${(0, node_crypto_1.randomUUID)()}`,
|
|
140
|
+
runtimePaths: input.runtimePaths,
|
|
141
|
+
sessionGeneration: input.sessionGeneration,
|
|
142
|
+
slice: name,
|
|
143
|
+
startupPath: input.startupPath,
|
|
144
|
+
workspaceRealpath: input.workspaceRealpath,
|
|
145
|
+
}, daemon);
|
|
146
|
+
runnersByChild.set(activeRunner.child, activeRunner);
|
|
147
|
+
return { attempt, child: activeRunner.child };
|
|
148
|
+
},
|
|
149
|
+
prepareRestart: async () => {
|
|
150
|
+
if (!activeRunner) {
|
|
151
|
+
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
|
|
152
|
+
}
|
|
153
|
+
(0, slice_runner_js_1.requestFleetSliceRunnerRestart)(activeRunner);
|
|
154
|
+
},
|
|
155
|
+
// The runner stops its writer and stays alive with pending
|
|
156
|
+
// cleanup authority; a later stop runs declared cleanup in that
|
|
157
|
+
// runner, so its output reaches this Slice's log store.
|
|
158
|
+
quiesce: async (child) => {
|
|
159
|
+
if (!activeRunner || activeRunner.child !== child) {
|
|
160
|
+
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before quiescence.');
|
|
161
|
+
}
|
|
162
|
+
await (0, fleet_process_js_1.quiesceCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
|
|
163
|
+
},
|
|
164
|
+
runnerGeneration: () => activeRunner?.request.runnerGeneration,
|
|
165
|
+
startTargetExit: (child) => {
|
|
166
|
+
const runner = runnersByChild.get(child);
|
|
167
|
+
return runner
|
|
168
|
+
? (0, slice_runner_js_1.readFleetSliceRunnerState)(runner.statePath).childExit
|
|
169
|
+
: undefined;
|
|
170
|
+
},
|
|
171
|
+
terminate: async (child) => {
|
|
172
|
+
if (!activeRunner || activeRunner.child !== child) {
|
|
173
|
+
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before shutdown.');
|
|
174
|
+
}
|
|
175
|
+
await (0, fleet_process_js_1.terminateCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
return { activeRunner: () => activeRunner, effects };
|
|
179
|
+
}
|
|
74
180
|
async function startFleetRuntime(startupPath, dependencies = {}) {
|
|
75
181
|
const request = (0, fleet_startup_js_1.readFleetStartupRequest)(startupPath);
|
|
76
182
|
if ((0, node_fs_1.realpathSync)(request.workspaceRealpath) !== request.workspaceRealpath) {
|
|
@@ -220,75 +326,27 @@ async function startFleetRuntime(startupPath, dependencies = {}) {
|
|
|
220
326
|
adapters: plan.adapters,
|
|
221
327
|
allocatePort: async (name) => ports.get(name),
|
|
222
328
|
fleetId,
|
|
223
|
-
processFactory: ({ adapter, name, onStateChange, upstream }) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
(0, fleet_web_start_js_1.hasFleetWebStarted)(activeRunner.requestPath, activeRunner.request)),
|
|
245
|
-
}
|
|
246
|
-
: {}),
|
|
247
|
-
cleanup: async () => {
|
|
248
|
-
if (!activeRunner) {
|
|
249
|
-
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
(0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
await (0, slice_runner_js_1.retryFleetSliceRunnerCleanup)(activeRunner);
|
|
256
|
-
(0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
|
|
257
|
-
}
|
|
258
|
-
await (0, slice_runner_js_1.recoverFleetSliceRunnerCleanupsForSlices)({
|
|
259
|
-
fleetId: activeRunner.request.fleetId,
|
|
260
|
-
sessionGeneration: activeRunner.request.sessionGeneration,
|
|
261
|
-
}, [name], runtimePaths);
|
|
262
|
-
},
|
|
263
|
-
launch: (attempt) => {
|
|
264
|
-
activeRunner = (0, slice_runner_js_1.launchFleetSliceRunner)({
|
|
265
|
-
origin,
|
|
266
|
-
parent: processOwner,
|
|
267
|
-
port: ports.get(name),
|
|
268
|
-
runnerGeneration: `${request.sessionGeneration}:${name}:${attempt}:${(0, node_crypto_1.randomUUID)()}`,
|
|
269
|
-
runtimePaths,
|
|
270
|
-
sessionGeneration: request.sessionGeneration,
|
|
271
|
-
slice: name,
|
|
272
|
-
startupPath,
|
|
273
|
-
workspaceRealpath: request.workspaceRealpath,
|
|
274
|
-
});
|
|
275
|
-
return { attempt, child: activeRunner.child };
|
|
276
|
-
},
|
|
277
|
-
prepareRestart: async () => {
|
|
278
|
-
if (!activeRunner) {
|
|
279
|
-
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
|
|
280
|
-
}
|
|
281
|
-
(0, slice_runner_js_1.requestFleetSliceRunnerRestart)(activeRunner);
|
|
282
|
-
},
|
|
283
|
-
runnerGeneration: () => activeRunner?.request.runnerGeneration,
|
|
284
|
-
terminate: async (child) => {
|
|
285
|
-
if (!activeRunner || activeRunner.child !== child) {
|
|
286
|
-
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before shutdown.');
|
|
287
|
-
}
|
|
288
|
-
await (0, fleet_process_js_1.terminateCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
},
|
|
329
|
+
processFactory: ({ adapter, name, onStateChange, upstream }) => new fleet_process_js_1.FleetProcessController({
|
|
330
|
+
adapter,
|
|
331
|
+
logStore,
|
|
332
|
+
maxRestartAttempts: request.context.maxRestartAttempts,
|
|
333
|
+
name,
|
|
334
|
+
onStateChange,
|
|
335
|
+
upstream,
|
|
336
|
+
}, createFleetSliceRunnerEffects({
|
|
337
|
+
adapter,
|
|
338
|
+
logStore,
|
|
339
|
+
name,
|
|
340
|
+
origin,
|
|
341
|
+
parent: processOwner,
|
|
342
|
+
port: ports.get(name),
|
|
343
|
+
projects: plan.projects,
|
|
344
|
+
runtimePaths,
|
|
345
|
+
sessionGeneration: request.sessionGeneration,
|
|
346
|
+
stage: request.identity.stage,
|
|
347
|
+
startupPath,
|
|
348
|
+
workspaceRealpath: request.workspaceRealpath,
|
|
349
|
+
}).effects),
|
|
292
350
|
proxy: proxyHost,
|
|
293
351
|
proxyPort,
|
|
294
352
|
routes,
|
|
@@ -8,6 +8,12 @@ const slice_claim_js_1 = require("./slice-claim.js");
|
|
|
8
8
|
function sessionError(classification, message) {
|
|
9
9
|
throw new errors_js_1.LocalDevContractError(classification, message);
|
|
10
10
|
}
|
|
11
|
+
function startupFailure(error) {
|
|
12
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13
|
+
return error instanceof errors_js_1.LocalDevContractError
|
|
14
|
+
? `${error.classification}: ${message}`
|
|
15
|
+
: message;
|
|
16
|
+
}
|
|
11
17
|
class FleetSession {
|
|
12
18
|
routes;
|
|
13
19
|
#allocatePort;
|
|
@@ -16,6 +22,7 @@ class FleetSession {
|
|
|
16
22
|
#cleanupStates = new Map();
|
|
17
23
|
#fleetId;
|
|
18
24
|
#operation = Promise.resolve();
|
|
25
|
+
#pendingStartup;
|
|
19
26
|
#processFactory;
|
|
20
27
|
#processes = new Map();
|
|
21
28
|
#proxy;
|
|
@@ -79,6 +86,29 @@ class FleetSession {
|
|
|
79
86
|
this.#operation = result.then(() => undefined, () => undefined);
|
|
80
87
|
return result;
|
|
81
88
|
}
|
|
89
|
+
async #quiesce(names) {
|
|
90
|
+
const failures = [];
|
|
91
|
+
// The complete selection was dependency-validated before this phase.
|
|
92
|
+
// Keep every claim selected until all writers stop, even if one fails.
|
|
93
|
+
for (const name of names) {
|
|
94
|
+
const process = this.#processes.get(name);
|
|
95
|
+
if (!process)
|
|
96
|
+
continue;
|
|
97
|
+
const route = this.#route(name);
|
|
98
|
+
try {
|
|
99
|
+
await process.quiesce();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
this.#cleanupStates.set(name, 'failed');
|
|
103
|
+
failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
route.selected = true;
|
|
107
|
+
route.state = 'unavailable';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return failures;
|
|
111
|
+
}
|
|
82
112
|
up(names) {
|
|
83
113
|
return this.#serialized(async () => {
|
|
84
114
|
const requested = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'up', names, this.selectedSlices());
|
|
@@ -115,15 +145,30 @@ class FleetSession {
|
|
|
115
145
|
upstream: route.upstream,
|
|
116
146
|
});
|
|
117
147
|
this.#processes.set(name, process);
|
|
118
|
-
|
|
148
|
+
const pending = {
|
|
149
|
+
name,
|
|
150
|
+
process,
|
|
151
|
+
interruptionRequested: false,
|
|
152
|
+
};
|
|
153
|
+
this.#pendingStartup = pending;
|
|
154
|
+
try {
|
|
155
|
+
await process.start();
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
if (this.#pendingStartup === pending)
|
|
159
|
+
this.#pendingStartup = undefined;
|
|
160
|
+
}
|
|
119
161
|
}
|
|
120
162
|
}
|
|
121
163
|
catch (error) {
|
|
122
|
-
const
|
|
164
|
+
const quiesceFailures = await this.#quiesce([...changed].reverse());
|
|
165
|
+
const cleanupFailures = [...quiesceFailures];
|
|
123
166
|
const releasable = [];
|
|
124
167
|
for (const name of [...changed].reverse()) {
|
|
125
168
|
const process = this.#processes.get(name);
|
|
126
169
|
if (process) {
|
|
170
|
+
if (quiesceFailures.length > 0)
|
|
171
|
+
continue;
|
|
127
172
|
try {
|
|
128
173
|
(0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', [name], this.selectedSlices());
|
|
129
174
|
await process.stop({ cleanup: true });
|
|
@@ -161,7 +206,9 @@ class FleetSession {
|
|
|
161
206
|
}
|
|
162
207
|
}
|
|
163
208
|
if (cleanupFailures.length > 0) {
|
|
164
|
-
|
|
209
|
+
// The start failure leads: the persisted failure message is
|
|
210
|
+
// bounded, and the cause must survive the rollback report.
|
|
211
|
+
return sessionError('cleanup-incomplete', `Fleet startup failed (${startupFailure(error)}) and generation-owned rollback was incomplete: ${cleanupFailures.join('; ')}`);
|
|
165
212
|
}
|
|
166
213
|
throw error;
|
|
167
214
|
}
|
|
@@ -194,7 +241,28 @@ class FleetSession {
|
|
|
194
241
|
return sessionError('not-running', `Product Slice ${name} is not selected.`);
|
|
195
242
|
}
|
|
196
243
|
const previousState = route.state;
|
|
197
|
-
|
|
244
|
+
const pending = {
|
|
245
|
+
name,
|
|
246
|
+
process,
|
|
247
|
+
interruptionRequested: false,
|
|
248
|
+
};
|
|
249
|
+
this.#pendingStartup = pending;
|
|
250
|
+
try {
|
|
251
|
+
await process.restart();
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (pending.interruptionRequested) {
|
|
255
|
+
const failures = await this.#quiesce([name]);
|
|
256
|
+
if (failures.length > 0) {
|
|
257
|
+
return sessionError('cleanup-incomplete', `Fleet restart failed (${startupFailure(error)}) and owned writer quiescence was incomplete: ${failures.join('; ')}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
if (this.#pendingStartup === pending)
|
|
264
|
+
this.#pendingStartup = undefined;
|
|
265
|
+
}
|
|
198
266
|
results.push({
|
|
199
267
|
changed: true,
|
|
200
268
|
name,
|
|
@@ -214,10 +282,15 @@ class FleetSession {
|
|
|
214
282
|
const results = [];
|
|
215
283
|
const cleanupFailures = [];
|
|
216
284
|
const ordered = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', names, this.selectedSlices());
|
|
285
|
+
const previousStates = new Map(ordered.map((name) => [name, this.#route(name).state]));
|
|
286
|
+
const quiesceFailures = await this.#quiesce(ordered);
|
|
287
|
+
if (quiesceFailures.length > 0) {
|
|
288
|
+
return sessionError('cleanup-incomplete', `Fleet writers did not quiesce; no cloud cleanup ran and all claims were retained: ${quiesceFailures.join('; ')}`);
|
|
289
|
+
}
|
|
217
290
|
for (const name of ordered) {
|
|
218
291
|
const route = this.#route(name);
|
|
219
292
|
const process = this.#processes.get(name);
|
|
220
|
-
const previousState =
|
|
293
|
+
const previousState = previousStates.get(name);
|
|
221
294
|
if (!route.selected || !process) {
|
|
222
295
|
results.push({
|
|
223
296
|
changed: false,
|
|
@@ -275,8 +348,11 @@ class FleetSession {
|
|
|
275
348
|
// running up operation owns rollback; down waits for that rollback rather
|
|
276
349
|
// than racing a second native cleanup against a live writer.
|
|
277
350
|
const ordered = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', names, this.selectedSlices());
|
|
278
|
-
|
|
279
|
-
|
|
351
|
+
const pending = this.#pendingStartup;
|
|
352
|
+
if (pending && ordered.includes(pending.name)) {
|
|
353
|
+
pending.interruptionRequested = true;
|
|
354
|
+
pending.process.interruptStartup?.();
|
|
355
|
+
}
|
|
280
356
|
}
|
|
281
357
|
allSlices() {
|
|
282
358
|
return this.routes.map(({ name }) => name).sort();
|
|
@@ -5,6 +5,9 @@ import { type PrivateStateSecurity } from './private-state.js';
|
|
|
5
5
|
import type { FleetRouterSource } from './fleet-configuration.js';
|
|
6
6
|
export declare const FLEET_STATE_SCHEMA_VERSION: 1;
|
|
7
7
|
export declare const FLEET_STATE_FILE: "control-state.json";
|
|
8
|
+
/** A persisted operation failure cause stays one bounded line, so the full
|
|
9
|
+
* state (up to 128 operations) remains inside the CLI result bound. */
|
|
10
|
+
export declare const FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = 1024;
|
|
8
11
|
export declare const FLEET_ROUTE_STATES: readonly ["remote", "starting", "ready", "unavailable"];
|
|
9
12
|
export declare const FLEET_CLEANUP_STATES: readonly ["not-declared", "pending", "succeeded", "failed"];
|
|
10
13
|
export declare const FLEET_OPERATION_STATES: readonly ["accepted", "running", "succeeded", "failed"];
|
|
@@ -28,6 +31,8 @@ export interface FleetOperationStateRecord {
|
|
|
28
31
|
command: FleetMutationCommand;
|
|
29
32
|
completedAt?: string;
|
|
30
33
|
failureClassification?: LocalDevFailureClassification;
|
|
34
|
+
/** Bounded, redacted, single-line cause of a failed operation. */
|
|
35
|
+
failureMessage?: string;
|
|
31
36
|
id: string;
|
|
32
37
|
idempotencyKey: string;
|
|
33
38
|
slices: string[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FLEET_OPERATION_STATES = exports.FLEET_CLEANUP_STATES = exports.FLEET_ROUTE_STATES = exports.FLEET_STATE_FILE = exports.FLEET_STATE_SCHEMA_VERSION = void 0;
|
|
3
|
+
exports.FLEET_OPERATION_STATES = exports.FLEET_CLEANUP_STATES = exports.FLEET_ROUTE_STATES = exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = exports.FLEET_STATE_FILE = exports.FLEET_STATE_SCHEMA_VERSION = void 0;
|
|
4
4
|
exports.fleetOwnedSliceNames = fleetOwnedSliceNames;
|
|
5
5
|
exports.fleetOwnsStartingSlice = fleetOwnsStartingSlice;
|
|
6
6
|
exports.validateFleetStateRecord = validateFleetStateRecord;
|
|
@@ -18,6 +18,9 @@ const private_state_js_1 = require("./private-state.js");
|
|
|
18
18
|
const personal_stage_js_1 = require("./personal-stage.js");
|
|
19
19
|
exports.FLEET_STATE_SCHEMA_VERSION = 1;
|
|
20
20
|
exports.FLEET_STATE_FILE = 'control-state.json';
|
|
21
|
+
/** A persisted operation failure cause stays one bounded line, so the full
|
|
22
|
+
* state (up to 128 operations) remains inside the CLI result bound. */
|
|
23
|
+
exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = 1_024;
|
|
21
24
|
exports.FLEET_ROUTE_STATES = [
|
|
22
25
|
'remote',
|
|
23
26
|
'starting',
|
|
@@ -120,7 +123,7 @@ function validateSliceState(value) {
|
|
|
120
123
|
}
|
|
121
124
|
function validateOperation(value) {
|
|
122
125
|
(0, contract_validation_js_1.assertRecord)(value, 'Fleet operation state');
|
|
123
|
-
(0, contract_validation_js_1.assertExactProperties)(value, ['acceptedAt', 'command', 'id', 'idempotencyKey', 'slices', 'state'], ['completedAt', 'failureClassification'], 'Fleet operation state');
|
|
126
|
+
(0, contract_validation_js_1.assertExactProperties)(value, ['acceptedAt', 'command', 'id', 'idempotencyKey', 'slices', 'state'], ['completedAt', 'failureClassification', 'failureMessage'], 'Fleet operation state');
|
|
124
127
|
if (value.command !== 'up' &&
|
|
125
128
|
value.command !== 'down' &&
|
|
126
129
|
value.command !== 'restart') {
|
|
@@ -157,6 +160,12 @@ function validateOperation(value) {
|
|
|
157
160
|
}
|
|
158
161
|
result.failureClassification = classification;
|
|
159
162
|
}
|
|
163
|
+
if (value.failureMessage !== undefined) {
|
|
164
|
+
if (result.state !== 'failed') {
|
|
165
|
+
return stateError('Fleet operation failureMessage is valid only for a failed operation.');
|
|
166
|
+
}
|
|
167
|
+
result.failureMessage = (0, contract_validation_js_1.requireBoundedString)(value.failureMessage, 'Fleet operation failureMessage', { maximumLength: exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH });
|
|
168
|
+
}
|
|
160
169
|
const terminal = result.state === 'succeeded' || result.state === 'failed';
|
|
161
170
|
if (terminal !== (result.completedAt !== undefined)) {
|
|
162
171
|
return stateError('Fleet operation terminal state and completedAt must agree.');
|
|
@@ -10,11 +10,7 @@ async function runFleetSupervisorDaemon(startupPath) {
|
|
|
10
10
|
return;
|
|
11
11
|
handlingSignal = true;
|
|
12
12
|
try {
|
|
13
|
-
|
|
14
|
-
if (selected.length > 0)
|
|
15
|
-
await runtime.session.down(selected);
|
|
16
|
-
await runtime.session.closeProxyWhenEmpty();
|
|
17
|
-
await runtime.supervisor.stopGracefully();
|
|
13
|
+
await runtime.supervisor.stopForSignal();
|
|
18
14
|
}
|
|
19
15
|
catch (error) {
|
|
20
16
|
handlingSignal = false;
|
|
@@ -42,6 +42,12 @@ export interface FleetSupervisorOptions {
|
|
|
42
42
|
security?: PrivateStateSecurity;
|
|
43
43
|
shutdownGraceMs?: number;
|
|
44
44
|
}
|
|
45
|
+
/** Failed operations beyond this many keep their classification only. */
|
|
46
|
+
export declare const FLEET_RETAINED_FAILURE_MESSAGES = 8;
|
|
47
|
+
/** The persisted cause is one redacted, bounded line. Whitespace collapses
|
|
48
|
+
* before redaction because the terminal-safety strip deletes line breaks and
|
|
49
|
+
* the state validator refuses every control character. */
|
|
50
|
+
export declare function fleetOperationFailureMessage(error: unknown): string | undefined;
|
|
45
51
|
export declare class FleetSupervisor {
|
|
46
52
|
#private;
|
|
47
53
|
constructor(options: FleetSupervisorOptions, effects?: FleetSupervisorEffects);
|
|
@@ -51,5 +57,6 @@ export declare class FleetSupervisor {
|
|
|
51
57
|
waitForIdle(): Promise<void>;
|
|
52
58
|
waitUntilStopped(): Promise<void>;
|
|
53
59
|
stopGracefully(): Promise<void>;
|
|
60
|
+
stopForSignal(): Promise<void>;
|
|
54
61
|
}
|
|
55
62
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FleetSupervisor = void 0;
|
|
3
|
+
exports.FleetSupervisor = exports.FLEET_RETAINED_FAILURE_MESSAGES = void 0;
|
|
4
|
+
exports.fleetOperationFailureMessage = fleetOperationFailureMessage;
|
|
4
5
|
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
const contracts_js_1 = require("./contracts.js");
|
|
6
7
|
const node_fs_1 = require("node:fs");
|
|
@@ -8,6 +9,7 @@ const control_contract_js_1 = require("./control-contract.js");
|
|
|
8
9
|
const errors_js_1 = require("./errors.js");
|
|
9
10
|
const fleet_ipc_js_1 = require("./fleet-ipc.js");
|
|
10
11
|
const fleet_lock_js_1 = require("./fleet-lock.js");
|
|
12
|
+
const fleet_logs_js_1 = require("./fleet-logs.js");
|
|
11
13
|
const fleet_state_js_1 = require("./fleet-state.js");
|
|
12
14
|
const private_state_js_1 = require("./private-state.js");
|
|
13
15
|
const process_identity_js_1 = require("./process-identity.js");
|
|
@@ -33,6 +35,49 @@ function failureClassification(error) {
|
|
|
33
35
|
? error.classification
|
|
34
36
|
: 'internal-error';
|
|
35
37
|
}
|
|
38
|
+
/** Failed operations beyond this many keep their classification only. */
|
|
39
|
+
exports.FLEET_RETAINED_FAILURE_MESSAGES = 8;
|
|
40
|
+
/** The persisted cause is one redacted, bounded line. Whitespace collapses
|
|
41
|
+
* before redaction because the terminal-safety strip deletes line breaks and
|
|
42
|
+
* the state validator refuses every control character. */
|
|
43
|
+
function fleetOperationFailureMessage(error) {
|
|
44
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
45
|
+
const line = (0, fleet_logs_js_1.redactLocalLogText)(raw.replace(/\s+/gu, ' '))
|
|
46
|
+
// The persisted line must carry no control character of any kind.
|
|
47
|
+
// eslint-disable-next-line no-control-regex
|
|
48
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/gu, ' ')
|
|
49
|
+
.replace(/ {2,}/gu, ' ')
|
|
50
|
+
.trim();
|
|
51
|
+
if (line.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (line.length <= fleet_state_js_1.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH)
|
|
54
|
+
return line;
|
|
55
|
+
let bounded = '';
|
|
56
|
+
for (const character of line) {
|
|
57
|
+
if (bounded.length + character.length >
|
|
58
|
+
fleet_state_js_1.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH - 1)
|
|
59
|
+
break;
|
|
60
|
+
bounded += character;
|
|
61
|
+
}
|
|
62
|
+
return `${bounded.trimEnd()}\u2026`;
|
|
63
|
+
}
|
|
64
|
+
function retainRecentFailureMessages(operations) {
|
|
65
|
+
let retained = 0;
|
|
66
|
+
return operations
|
|
67
|
+
.slice()
|
|
68
|
+
.reverse()
|
|
69
|
+
.map((operation) => {
|
|
70
|
+
if (operation.failureMessage === undefined)
|
|
71
|
+
return operation;
|
|
72
|
+
retained += 1;
|
|
73
|
+
if (retained <= exports.FLEET_RETAINED_FAILURE_MESSAGES)
|
|
74
|
+
return operation;
|
|
75
|
+
const classificationOnly = { ...operation };
|
|
76
|
+
delete classificationOnly.failureMessage;
|
|
77
|
+
return classificationOnly;
|
|
78
|
+
})
|
|
79
|
+
.reverse();
|
|
80
|
+
}
|
|
36
81
|
class FleetSupervisor {
|
|
37
82
|
#closed = false;
|
|
38
83
|
#adapters;
|
|
@@ -51,6 +96,7 @@ class FleetSupervisor {
|
|
|
51
96
|
#shutdownGraceMs;
|
|
52
97
|
#state;
|
|
53
98
|
#stopping = false;
|
|
99
|
+
#stopOperation;
|
|
54
100
|
constructor(options, effects = {}) {
|
|
55
101
|
this.#adapters = options.adapters;
|
|
56
102
|
this.#host = options.host;
|
|
@@ -316,12 +362,15 @@ class FleetSupervisor {
|
|
|
316
362
|
}
|
|
317
363
|
catch (error) {
|
|
318
364
|
this.#shutdownAfterOperations.delete(operation.id);
|
|
365
|
+
const failureMessage = fleetOperationFailureMessage(error);
|
|
319
366
|
this.#state.operations[index] = {
|
|
320
367
|
...this.#state.operations[index],
|
|
321
368
|
completedAt: this.#effects.now().toISOString(),
|
|
322
369
|
failureClassification: failureClassification(error),
|
|
370
|
+
...(failureMessage === undefined ? {} : { failureMessage }),
|
|
323
371
|
state: 'failed',
|
|
324
372
|
};
|
|
373
|
+
this.#state.operations = retainRecentFailureMessages(this.#state.operations);
|
|
325
374
|
this.#persistState();
|
|
326
375
|
}
|
|
327
376
|
}
|
|
@@ -374,22 +423,44 @@ class FleetSupervisor {
|
|
|
374
423
|
return;
|
|
375
424
|
await new Promise((resolvePromise) => this.#closedWaiters.push(resolvePromise));
|
|
376
425
|
}
|
|
377
|
-
|
|
426
|
+
stopGracefully() {
|
|
427
|
+
return this.#stop('empty');
|
|
428
|
+
}
|
|
429
|
+
stopForSignal() {
|
|
430
|
+
return this.#stop('down-selected');
|
|
431
|
+
}
|
|
432
|
+
#stop(mode) {
|
|
378
433
|
if (this.#closed)
|
|
379
|
-
return;
|
|
434
|
+
return Promise.resolve();
|
|
435
|
+
if (this.#stopOperation)
|
|
436
|
+
return this.#stopOperation;
|
|
380
437
|
this.#stopping = true;
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
this.#assertShutdownOwnership();
|
|
384
|
-
await this.#host.closeProxyWhenEmpty();
|
|
385
|
-
(0, fleet_runner_evidence_js_1.retireFleetRunnerEvidence)(this.#state, this.#paths, {
|
|
386
|
-
security: this.#security,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
438
|
+
this.#stopOperation = this.#finishStop(mode).catch((error) => {
|
|
390
439
|
this.#stopping = false;
|
|
440
|
+
this.#stopOperation = undefined;
|
|
391
441
|
throw error;
|
|
442
|
+
});
|
|
443
|
+
return this.#stopOperation;
|
|
444
|
+
}
|
|
445
|
+
async #finishStop(mode) {
|
|
446
|
+
await this.waitForIdle();
|
|
447
|
+
if (mode === 'down-selected') {
|
|
448
|
+
const selected = this.#host.selectedSlices();
|
|
449
|
+
if (selected.length > 0) {
|
|
450
|
+
try {
|
|
451
|
+
this.#syncSliceStates(await this.#host.down(selected));
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
this.#syncLiveSliceStates();
|
|
455
|
+
this.#persistState();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
392
458
|
}
|
|
459
|
+
this.#assertShutdownOwnership();
|
|
460
|
+
await this.#host.closeProxyWhenEmpty();
|
|
461
|
+
(0, fleet_runner_evidence_js_1.retireFleetRunnerEvidence)(this.#state, this.#paths, {
|
|
462
|
+
security: this.#security,
|
|
463
|
+
});
|
|
393
464
|
if (this.#heartbeat)
|
|
394
465
|
clearInterval(this.#heartbeat);
|
|
395
466
|
this.#heartbeat = undefined;
|
|
@@ -23,8 +23,16 @@ export interface FleetSliceRunnerRequest {
|
|
|
23
23
|
startupPath: string;
|
|
24
24
|
workspaceRealpath: string;
|
|
25
25
|
}
|
|
26
|
+
/** The Slice start target's own exit when it ended without owned
|
|
27
|
+
* termination: the cause behind a runner that then exits cleanly. */
|
|
28
|
+
export interface FleetSliceRunnerChildExit {
|
|
29
|
+
at: string;
|
|
30
|
+
code: number | null;
|
|
31
|
+
signal: string | null;
|
|
32
|
+
}
|
|
26
33
|
export interface FleetSliceRunnerState {
|
|
27
34
|
child?: FleetProcessIdentity;
|
|
35
|
+
childExit?: FleetSliceRunnerChildExit;
|
|
28
36
|
cleanup: 'failed' | 'pending' | 'running' | 'succeeded';
|
|
29
37
|
failure?: string;
|
|
30
38
|
fleetId: string;
|
|
@@ -91,6 +99,13 @@ export declare function runnerParentStatus(request: FleetSliceRunnerRequest, opt
|
|
|
91
99
|
processProbe?: ProcessIdentityProbe;
|
|
92
100
|
security?: PrivateStateSecurity;
|
|
93
101
|
}): 'ambiguous' | 'lost' | 'present';
|
|
102
|
+
interface FleetSliceRunnerControl {
|
|
103
|
+
acknowledgeQuiesce(): void;
|
|
104
|
+
/** Registers the quiesce handler and replays a request that arrived while
|
|
105
|
+
* the runner was still reading its request. */
|
|
106
|
+
onQuiesce(handler: () => void): void;
|
|
107
|
+
stopSignal: AbortSignal;
|
|
108
|
+
}
|
|
94
109
|
declare function runFleetSliceRunnerCore(requestPath: string, options: {
|
|
95
110
|
loadPlan?: typeof loadWorkspaceFleetPlan;
|
|
96
111
|
monitorIntervalMs?: number;
|
|
@@ -102,11 +117,14 @@ declare function runFleetSliceRunnerCore(requestPath: string, options: {
|
|
|
102
117
|
security?: PrivateStateSecurity;
|
|
103
118
|
spawnTarget?: typeof spawnDeclaredNxTarget;
|
|
104
119
|
terminate?: typeof terminateOwnedChild;
|
|
105
|
-
},
|
|
120
|
+
}, control: FleetSliceRunnerControl): Promise<0 | 1>;
|
|
106
121
|
export declare function runFleetSliceRunner(requestPath: string, options?: Parameters<typeof runFleetSliceRunnerCore>[1]): Promise<0 | 1>;
|
|
107
122
|
export declare function assertFleetSliceRunnerCleanup(statePath: string, expected: Pick<FleetSliceRunnerRequest, 'fleetId' | 'runnerGeneration' | 'sessionGeneration' | 'slice'>, security?: PrivateStateSecurity): void;
|
|
108
123
|
export declare function retryFleetSliceRunnerCleanup(launched: Pick<LaunchedFleetSliceRunner, 'request' | 'requestPath' | 'statePath'>, effects?: {
|
|
109
124
|
daemonPath?: string;
|
|
125
|
+
/** Receives the retry runner's output, including its declared cleanup
|
|
126
|
+
* target's, for the Slice log store. Without it the output is inherited. */
|
|
127
|
+
output?: (stream: 'stderr' | 'stdout', chunk: Buffer | string) => void;
|
|
110
128
|
processProbe?: (identity: FleetProcessIdentity) => ProcessIdentityProbe;
|
|
111
129
|
security?: PrivateStateSecurity;
|
|
112
130
|
spawn?: typeof spawn;
|