@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.
Files changed (85) hide show
  1. package/package.json +3 -3
  2. package/src/blueprints/external-sandbox.d.ts +1 -1
  3. package/src/cli/main.d.ts +11 -5
  4. package/src/cli/main.js +88 -29
  5. package/src/container-images.d.ts +1 -1
  6. package/src/container-images.js +1 -1
  7. package/src/executors/deployment-plan/deployment-plan.js +5 -1
  8. package/src/executors/deployment-plan/schema.json +1 -1
  9. package/src/executors/fleet/fleet.js +4 -0
  10. package/src/executors/fleet/schema.json +1 -1
  11. package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
  12. package/src/executors/local-deploy/federated-credential-transport.js +15 -5
  13. package/src/foundation/cost-estimate.d.ts +8 -3
  14. package/src/foundation/cost-estimate.js +42 -19
  15. package/src/foundation/index.d.ts +1 -0
  16. package/src/foundation/index.js +1 -0
  17. package/src/foundation/local-private-smoke-access.js +6 -0
  18. package/src/foundation/logging.d.ts +3 -1
  19. package/src/foundation/logging.js +8 -1
  20. package/src/foundation/observability.d.ts +42 -0
  21. package/src/foundation/observability.js +181 -0
  22. package/src/foundation/preflight.js +1 -1
  23. package/src/generators/function/function.js +5 -2
  24. package/src/generators/observability-infrastructure.d.ts +2 -0
  25. package/src/generators/observability-infrastructure.js +31 -0
  26. package/src/generators/observability-policy.d.ts +4 -0
  27. package/src/generators/observability-policy.js +23 -0
  28. package/src/generators/preset/dependency-overrides.d.ts +17 -6
  29. package/src/generators/preset/dependency-overrides.js +17 -6
  30. package/src/generators/preset/tool-versions.d.ts +2 -2
  31. package/src/generators/preset/tool-versions.js +2 -2
  32. package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
  33. package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
  34. package/src/generators/preset/workspace-files-agent-development.js +42 -0
  35. package/src/generators/preset/workspace-files-thin-deploy.js +12 -3
  36. package/src/generators/preset/workspace-files-thin-docs.js +10 -22
  37. package/src/generators/preset/workspace-files.d.ts +1 -0
  38. package/src/generators/preset/workspace-files.js +89 -7
  39. package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
  40. package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
  41. package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
  42. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
  43. package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
  44. package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
  45. package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
  46. package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
  47. package/src/generators/slice/built-in-plan-metadata-files.js +33 -1
  48. package/src/generators/slice/built-in-plan-operations-files.js +106 -6
  49. package/src/generators/slice/built-in-plan-project-files.js +1 -0
  50. package/src/generators/slice/built-in-plan-release-files.js +2 -43
  51. package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
  52. package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
  53. package/src/generators/slice/built-in-plan-shared.js +29 -2
  54. package/src/generators/slice/built-in-plan-web-files.js +18 -1
  55. package/src/generators/slice/built-in-plan.js +1 -0
  56. package/src/generators/slice/slice.js +2 -0
  57. package/src/generators/upgrade/upgrade.d.ts +4 -3
  58. package/src/generators/upgrade/upgrade.js +50 -26
  59. package/src/generators/workload/frozen-container.d.ts +1 -1
  60. package/src/generators/workload/workload.js +5 -1
  61. package/src/local-dev/dev-command-core.d.ts +24 -3
  62. package/src/local-dev/dev-command-core.js +124 -12
  63. package/src/local-dev/errors.d.ts +19 -1
  64. package/src/local-dev/errors.js +18 -1
  65. package/src/local-dev/fleet-client.d.ts +64 -6
  66. package/src/local-dev/fleet-client.js +189 -15
  67. package/src/local-dev/fleet-process.d.ts +47 -2
  68. package/src/local-dev/fleet-process.js +248 -31
  69. package/src/local-dev/fleet-runtime.d.ts +37 -0
  70. package/src/local-dev/fleet-runtime.js +127 -69
  71. package/src/local-dev/fleet-session.d.ts +1 -0
  72. package/src/local-dev/fleet-session.js +42 -3
  73. package/src/local-dev/fleet-state.d.ts +5 -0
  74. package/src/local-dev/fleet-state.js +11 -2
  75. package/src/local-dev/fleet-supervisor.d.ts +6 -0
  76. package/src/local-dev/fleet-supervisor.js +49 -1
  77. package/src/local-dev/slice-runner.d.ts +19 -1
  78. package/src/local-dev/slice-runner.js +290 -91
  79. package/src/repository-policy/deployment-plan.d.ts +1 -1
  80. package/src/repository-policy/deployment-plan.js +16 -1
  81. package/src/secrets/generated-aws-control-plane.d.ts +11 -1
  82. package/src/secrets/generated-aws-control-plane.js +92 -29
  83. package/src/secrets/operator.d.ts +3 -2
  84. package/src/secrets/operator.js +27 -5
  85. 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
- let activeRunner;
225
- return new fleet_process_js_1.FleetProcessController({
226
- adapter,
227
- logStore,
228
- maxRestartAttempts: request.context.maxRestartAttempts,
229
- name,
230
- onStateChange,
231
- upstream,
232
- }, {
233
- ...(plan.projects[name]?.targets?.[adapter.target]?.executor ===
234
- '@empire-builder-kit/nx:dev'
235
- ? {
236
- nativeDeployment: async (signal) => activeRunner
237
- ? (0, native_completion_js_1.readNativeDevelopmentCompletion)({
238
- projectRoot: (0, node_path_1.join)(request.workspaceRealpath, plan.projects[name].root),
239
- stage: request.identity.stage,
240
- startedAt: activeRunner.request.createdAt,
241
- }, signal)
242
- : 'pending',
243
- applicationStarted: () => Boolean(activeRunner &&
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,7 @@ export interface FleetSliceProcess {
8
8
  readonly runnerGeneration?: string;
9
9
  readonly state: FleetProcessRouteState;
10
10
  interruptStartup?(): void;
11
+ quiesce(): Promise<void>;
11
12
  restart(): Promise<void>;
12
13
  start(): Promise<void>;
13
14
  stop(options: {
@@ -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 cleanupFailures = [];
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
- return sessionError('cleanup-incomplete', `Fleet startup failed and generation-owned rollback was incomplete: ${cleanupFailures.join('; ')}`);
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 = route.state;
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
- }, stopSignal: AbortSignal): Promise<0 | 1>;
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;