@empire-builder-kit/nx 1.0.0-rc.44 → 1.0.0-rc.45
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/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/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 +12 -3
- 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 +89 -7
- 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-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 +33 -1
- 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 -43
- 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-web-files.js +18 -1
- package/src/generators/slice/built-in-plan.js +1 -0
- package/src/generators/slice/slice.js +2 -0
- package/src/generators/upgrade/upgrade.d.ts +4 -3
- package/src/generators/upgrade/upgrade.js +50 -26
- package/src/generators/workload/frozen-container.d.ts +1 -1
- package/src/generators/workload/workload.js +5 -1
- 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 +42 -3
- 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.d.ts +6 -0
- package/src/local-dev/fleet-supervisor.js +49 -1
- 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/generated-files.js +9 -0
|
@@ -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;
|
|
@@ -79,6 +85,29 @@ class FleetSession {
|
|
|
79
85
|
this.#operation = result.then(() => undefined, () => undefined);
|
|
80
86
|
return result;
|
|
81
87
|
}
|
|
88
|
+
async #quiesce(names) {
|
|
89
|
+
const failures = [];
|
|
90
|
+
// The complete selection was dependency-validated before this phase.
|
|
91
|
+
// Keep every claim selected until all writers stop, even if one fails.
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
const process = this.#processes.get(name);
|
|
94
|
+
if (!process)
|
|
95
|
+
continue;
|
|
96
|
+
const route = this.#route(name);
|
|
97
|
+
try {
|
|
98
|
+
await process.quiesce();
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
this.#cleanupStates.set(name, 'failed');
|
|
102
|
+
failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
route.selected = true;
|
|
106
|
+
route.state = 'unavailable';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return failures;
|
|
110
|
+
}
|
|
82
111
|
up(names) {
|
|
83
112
|
return this.#serialized(async () => {
|
|
84
113
|
const requested = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'up', names, this.selectedSlices());
|
|
@@ -119,11 +148,14 @@ class FleetSession {
|
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
150
|
catch (error) {
|
|
122
|
-
const
|
|
151
|
+
const quiesceFailures = await this.#quiesce([...changed].reverse());
|
|
152
|
+
const cleanupFailures = [...quiesceFailures];
|
|
123
153
|
const releasable = [];
|
|
124
154
|
for (const name of [...changed].reverse()) {
|
|
125
155
|
const process = this.#processes.get(name);
|
|
126
156
|
if (process) {
|
|
157
|
+
if (quiesceFailures.length > 0)
|
|
158
|
+
continue;
|
|
127
159
|
try {
|
|
128
160
|
(0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', [name], this.selectedSlices());
|
|
129
161
|
await process.stop({ cleanup: true });
|
|
@@ -161,7 +193,9 @@ class FleetSession {
|
|
|
161
193
|
}
|
|
162
194
|
}
|
|
163
195
|
if (cleanupFailures.length > 0) {
|
|
164
|
-
|
|
196
|
+
// The start failure leads: the persisted failure message is
|
|
197
|
+
// bounded, and the cause must survive the rollback report.
|
|
198
|
+
return sessionError('cleanup-incomplete', `Fleet startup failed (${startupFailure(error)}) and generation-owned rollback was incomplete: ${cleanupFailures.join('; ')}`);
|
|
165
199
|
}
|
|
166
200
|
throw error;
|
|
167
201
|
}
|
|
@@ -214,10 +248,15 @@ class FleetSession {
|
|
|
214
248
|
const results = [];
|
|
215
249
|
const cleanupFailures = [];
|
|
216
250
|
const ordered = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', names, this.selectedSlices());
|
|
251
|
+
const previousStates = new Map(ordered.map((name) => [name, this.#route(name).state]));
|
|
252
|
+
const quiesceFailures = await this.#quiesce(ordered);
|
|
253
|
+
if (quiesceFailures.length > 0) {
|
|
254
|
+
return sessionError('cleanup-incomplete', `Fleet writers did not quiesce; no cloud cleanup ran and all claims were retained: ${quiesceFailures.join('; ')}`);
|
|
255
|
+
}
|
|
217
256
|
for (const name of ordered) {
|
|
218
257
|
const route = this.#route(name);
|
|
219
258
|
const process = this.#processes.get(name);
|
|
220
|
-
const previousState =
|
|
259
|
+
const previousState = previousStates.get(name);
|
|
221
260
|
if (!route.selected || !process) {
|
|
222
261
|
results.push({
|
|
223
262
|
changed: false,
|
|
@@ -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.');
|
|
@@ -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);
|
|
@@ -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;
|
|
@@ -316,12 +361,15 @@ class FleetSupervisor {
|
|
|
316
361
|
}
|
|
317
362
|
catch (error) {
|
|
318
363
|
this.#shutdownAfterOperations.delete(operation.id);
|
|
364
|
+
const failureMessage = fleetOperationFailureMessage(error);
|
|
319
365
|
this.#state.operations[index] = {
|
|
320
366
|
...this.#state.operations[index],
|
|
321
367
|
completedAt: this.#effects.now().toISOString(),
|
|
322
368
|
failureClassification: failureClassification(error),
|
|
369
|
+
...(failureMessage === undefined ? {} : { failureMessage }),
|
|
323
370
|
state: 'failed',
|
|
324
371
|
};
|
|
372
|
+
this.#state.operations = retainRecentFailureMessages(this.#state.operations);
|
|
325
373
|
this.#persistState();
|
|
326
374
|
}
|
|
327
375
|
}
|
|
@@ -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;
|