start-command 0.35.4 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -0
- package/package.json +1 -1
- package/src/bin/cli.js +48 -7
- package/src/lib/args-parser-queries.js +14 -0
- package/src/lib/args-parser.js +32 -1
- package/src/lib/atomic-write.js +34 -0
- package/src/lib/attached-diagnostics.js +90 -0
- package/src/lib/cgroup-memory.js +43 -11
- package/src/lib/command-builder.js +43 -0
- package/src/lib/cpu-penalty-monitor.js +226 -0
- package/src/lib/cpu-penalty.js +175 -0
- package/src/lib/detached-finalize.js +16 -0
- package/src/lib/docker-cleanup.js +35 -7
- package/src/lib/docker-command-handoff.js +75 -0
- package/src/lib/docker-labels.js +33 -0
- package/src/lib/docker-resource-limits.js +28 -0
- package/src/lib/docker-resource-options.js +180 -0
- package/src/lib/docker-runtime-args.js +23 -0
- package/src/lib/docker-snapshot-safety.js +164 -0
- package/src/lib/execution-recovery.js +153 -30
- package/src/lib/execution-resume-all.js +11 -0
- package/src/lib/execution-resume.js +314 -54
- package/src/lib/execution-store.js +25 -121
- package/src/lib/exit-evidence.js +123 -0
- package/src/lib/exit-reason.js +24 -6
- package/src/lib/failure-handler.js +15 -5
- package/src/lib/isolation.js +48 -7
- package/src/lib/launch-owner.js +25 -0
- package/src/lib/launch-persistence.js +54 -0
- package/src/lib/log-sanitizer.js +267 -0
- package/src/lib/log-uploader.js +38 -19
- package/src/lib/query-commands.js +18 -6
- package/src/lib/resume-resources.js +88 -0
- package/src/lib/shell-utils.js +67 -17
- package/src/lib/status-formatter.js +28 -0
- package/src/lib/store-lock.js +133 -0
- package/src/lib/usage.js +16 -1
- package/test/ci-workflow-invariants.js +15 -14
- package/test/docker-command-handoff.js +107 -0
- package/test/docker-runtime-options.js +10 -0
- package/test/docker-snapshot-safety.js +106 -0
- package/test/execution-resume.js +19 -11
- package/test/exit-reason.js +6 -1
- package/test/failure-handler.js +11 -0
- package/test/isolation.js +5 -4
- package/test/javascript-first-ci.js +216 -0
- package/test/log-sanitizer.js +306 -0
- package/test/regression-174.js +3 -1
- package/test/regression-176.js +7 -0
- package/test/regression-180.js +17 -10
- package/test/regression-182.js +7 -3
- package/test/regression-185.js +4 -9
- package/test/regression-187.js +11 -2
- package/test/regression-189.js +224 -0
- package/test/regression-190.js +111 -0
- package/test/regression-191.js +160 -0
- package/test/regression-192.js +66 -0
- package/test/regression-193.js +251 -0
- package/test/regression-194.js +182 -0
- package/test/regression-198.js +281 -0
- package/test/regression-199.js +79 -0
- package/test/regression-201.js +25 -0
- package/test/regression-202.js +92 -0
- package/test/regression-91.js +23 -20
- package/test/shared-golden.js +54 -0
- package/test/status-query.js +97 -15
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* addressing one logical session across restarts.
|
|
8
8
|
*
|
|
9
9
|
* Three strategies, chosen from the probed session state:
|
|
10
|
-
* - DOCKER_START: the container still exists
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* - DOCKER_START: the container still exists. Newly managed containers
|
|
11
|
+
* accept replacement commands through a small handoff
|
|
12
|
+
* script, then restart in place without a snapshot.
|
|
13
|
+
* - DOCKER_SNAPSHOT: a legacy container needs a replacement command, or
|
|
14
|
+
* immutable labels need changing. After disk preflight,
|
|
15
|
+
* its filesystem is committed under a host-wide lock and
|
|
16
|
+
* a derived container runs the command.
|
|
17
17
|
* - RELAUNCH: nothing is left of the session, so the command is
|
|
18
18
|
* launched again through the stored isolation options.
|
|
19
19
|
*/
|
|
@@ -28,7 +28,6 @@ const { escapeForLinksNotation } = require('./output-blocks');
|
|
|
28
28
|
const {
|
|
29
29
|
buildResourceLimitsStatusLine,
|
|
30
30
|
normalizeResourceLimits,
|
|
31
|
-
readDockerResourceLimits,
|
|
32
31
|
} = require('./docker-resource-limits');
|
|
33
32
|
const { appendLogFile } = require('./isolation-log-utils');
|
|
34
33
|
const {
|
|
@@ -38,8 +37,23 @@ const {
|
|
|
38
37
|
patchAttempt,
|
|
39
38
|
} = require('./execution-attempt');
|
|
40
39
|
const { runCommand } = require('./execution-control');
|
|
40
|
+
const {
|
|
41
|
+
reserveLaunch,
|
|
42
|
+
rollbackLaunch,
|
|
43
|
+
failLaunchedPersistence,
|
|
44
|
+
} = require('./launch-persistence');
|
|
41
45
|
const { SessionState, probeSession } = require('./session-probe');
|
|
42
46
|
const { getDockerNetworks } = require('./docker-network-lifecycle');
|
|
47
|
+
const {
|
|
48
|
+
writeCommandHandoff,
|
|
49
|
+
buildCommandHandoffArgs,
|
|
50
|
+
commandHandoffPath,
|
|
51
|
+
} = require('./docker-command-handoff');
|
|
52
|
+
const {
|
|
53
|
+
acquireSnapshotLock,
|
|
54
|
+
preflightSnapshot,
|
|
55
|
+
SNAPSHOT_IMAGE_LABEL,
|
|
56
|
+
} = require('./docker-snapshot-safety');
|
|
43
57
|
const {
|
|
44
58
|
ResumeAllAction,
|
|
45
59
|
resumeAllExecutions,
|
|
@@ -89,6 +103,12 @@ function buildResumedSessionName(sessionName, attempt) {
|
|
|
89
103
|
function buildLaunchOptions(record) {
|
|
90
104
|
const opts = record.options || {};
|
|
91
105
|
return {
|
|
106
|
+
...opts,
|
|
107
|
+
labels: opts.labels || [],
|
|
108
|
+
sessionId: record.uuid,
|
|
109
|
+
rootSession:
|
|
110
|
+
opts.rootSession || opts.sessionNameHistory?.[0] || opts.sessionName,
|
|
111
|
+
resumeCount: (Number(opts.resumeCount) || 0) + 1,
|
|
92
112
|
image: opts.image || null,
|
|
93
113
|
session: opts.sessionName,
|
|
94
114
|
detached: true,
|
|
@@ -209,12 +229,13 @@ function buildResumePlan(
|
|
|
209
229
|
const attempt = (Number(opts.resumeCount) || 0) + 1;
|
|
210
230
|
|
|
211
231
|
if (backend === 'docker' && probe.state === SessionState.STOPPED) {
|
|
212
|
-
if (!newCommand) {
|
|
232
|
+
if (!opts.forceSnapshot && (!newCommand || opts.commandHandoff)) {
|
|
213
233
|
return {
|
|
214
234
|
mode: ResumeMode.DOCKER_START,
|
|
215
235
|
backend,
|
|
216
236
|
sessionName,
|
|
217
237
|
command,
|
|
238
|
+
...(newCommand ? { handoffCommand: command } : {}),
|
|
218
239
|
attempt,
|
|
219
240
|
steps: [
|
|
220
241
|
{
|
|
@@ -223,7 +244,9 @@ function buildResumePlan(
|
|
|
223
244
|
description: `Start stopped container ${sessionName}`,
|
|
224
245
|
},
|
|
225
246
|
],
|
|
226
|
-
message:
|
|
247
|
+
message: newCommand
|
|
248
|
+
? `Resumed new command in the same detached docker container: ${sessionName} (no filesystem snapshot)`
|
|
249
|
+
: `Resumed detached docker container: ${sessionName}`,
|
|
227
250
|
};
|
|
228
251
|
}
|
|
229
252
|
|
|
@@ -256,12 +279,24 @@ function buildResumePlan(
|
|
|
256
279
|
containerArgs: [
|
|
257
280
|
'--name',
|
|
258
281
|
newSessionName,
|
|
282
|
+
'--label',
|
|
283
|
+
`${SNAPSHOT_IMAGE_LABEL}=${snapshotImage}`,
|
|
259
284
|
...(opts.user ? ['--user', opts.user] : []),
|
|
260
|
-
...buildDockerRuntimeArgs({
|
|
285
|
+
...buildDockerRuntimeArgs({
|
|
286
|
+
...opts,
|
|
287
|
+
resourceLimits,
|
|
288
|
+
session: newSessionName,
|
|
289
|
+
containerName: newSessionName,
|
|
290
|
+
sessionId: record.uuid,
|
|
291
|
+
rootSession:
|
|
292
|
+
opts.rootSession || opts.sessionNameHistory?.[0] || sessionName,
|
|
293
|
+
resumeCount: attempt,
|
|
294
|
+
}),
|
|
261
295
|
snapshotImage,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
296
|
+
...buildCommandHandoffArgs(
|
|
297
|
+
['sh', '-c', command],
|
|
298
|
+
commandHandoffPath(newSessionName)
|
|
299
|
+
),
|
|
265
300
|
],
|
|
266
301
|
extraNetworks: getDockerNetworks(opts).slice(1),
|
|
267
302
|
newSessionName,
|
|
@@ -355,10 +390,15 @@ function applyResumeToRecord(
|
|
|
355
390
|
archiveAttempt(record);
|
|
356
391
|
record.attempt = attempt;
|
|
357
392
|
const options = { ...(record.options || {}) };
|
|
393
|
+
options.rootSession =
|
|
394
|
+
options.rootSession ||
|
|
395
|
+
options.sessionNameHistory?.[0] ||
|
|
396
|
+
options.sessionName;
|
|
358
397
|
options.resumeCount = plan.attempt;
|
|
359
398
|
options.resumedAt = attempt.startedAt;
|
|
360
399
|
// A resume is a new deliberate start: launch-time recovery applies again.
|
|
361
400
|
delete options.stopRequestedAt;
|
|
401
|
+
delete options.exitEvidence;
|
|
362
402
|
if (plan.resourceLimits && plan.resourceLimits.length > 0) {
|
|
363
403
|
options.resourceLimits = plan.resourceLimits;
|
|
364
404
|
}
|
|
@@ -372,6 +412,12 @@ function applyResumeToRecord(
|
|
|
372
412
|
}
|
|
373
413
|
if (plan.snapshotImage) {
|
|
374
414
|
options.image = plan.snapshotImage;
|
|
415
|
+
options.snapshotImage = plan.snapshotImage;
|
|
416
|
+
// Every new detached container, including a snapshot successor, supports handoff.
|
|
417
|
+
options.commandHandoff = true;
|
|
418
|
+
}
|
|
419
|
+
if (plan.mode === ResumeMode.RELAUNCH && plan.backend === 'docker') {
|
|
420
|
+
options.commandHandoff = true;
|
|
375
421
|
}
|
|
376
422
|
if (containerId) {
|
|
377
423
|
options.containerId = containerId;
|
|
@@ -411,27 +457,83 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
411
457
|
};
|
|
412
458
|
}
|
|
413
459
|
|
|
460
|
+
if (require('./launch-owner').hasActiveLaunch(record)) {
|
|
461
|
+
return {
|
|
462
|
+
success: false,
|
|
463
|
+
error: 'An execution launch is already reserved.',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const previousData = globalThis.structuredClone(
|
|
467
|
+
record.toObject ? record.toObject() : record
|
|
468
|
+
);
|
|
469
|
+
const previous = record.constructor.fromObject
|
|
470
|
+
? record.constructor.fromObject(previousData)
|
|
471
|
+
: previousData;
|
|
414
472
|
const runner = deps.runner || runCommand;
|
|
415
473
|
const probeFn = deps.probe || ((r) => probeSession(r, runner));
|
|
416
474
|
const probe = probeFn(record);
|
|
417
475
|
const opts = record.options || {};
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
476
|
+
let resources;
|
|
477
|
+
try {
|
|
478
|
+
resources = require('./resume-resources').prepareResumeResources(
|
|
479
|
+
record,
|
|
480
|
+
probe,
|
|
481
|
+
deps.resourceOptions || {},
|
|
482
|
+
runner,
|
|
483
|
+
deps.random
|
|
484
|
+
);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
return { success: false, error: error.message };
|
|
487
|
+
}
|
|
488
|
+
const requestedLabels = deps.resourceOptions?.labels || [];
|
|
489
|
+
const labels = [...(opts.labels || [])];
|
|
490
|
+
for (const label of requestedLabels) {
|
|
491
|
+
const key = label.split('=')[0];
|
|
492
|
+
const existing = labels.findIndex((value) => value.split('=')[0] === key);
|
|
493
|
+
if (existing >= 0) {
|
|
494
|
+
labels[existing] = label;
|
|
495
|
+
} else {
|
|
496
|
+
labels.push(label);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (requestedLabels.length && opts.isolated !== 'docker') {
|
|
500
|
+
return {
|
|
501
|
+
success: false,
|
|
502
|
+
error: 'Docker labels require a Docker execution when resuming.',
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const labelsChanged =
|
|
506
|
+
JSON.stringify(labels) !== JSON.stringify(opts.labels || []);
|
|
507
|
+
const planningRecord = {
|
|
508
|
+
...record,
|
|
509
|
+
options: {
|
|
510
|
+
...opts,
|
|
511
|
+
...(resources ? { resourceLimits: resources.resourceLimits } : {}),
|
|
512
|
+
...(requestedLabels.length ? { labels } : {}),
|
|
513
|
+
...(labelsChanged ? { forceSnapshot: true } : {}),
|
|
514
|
+
},
|
|
515
|
+
};
|
|
429
516
|
const plan = buildResumePlan(
|
|
430
|
-
|
|
517
|
+
planningRecord,
|
|
431
518
|
deps.command || null,
|
|
432
519
|
probe,
|
|
433
|
-
|
|
520
|
+
resources?.resourceLimits
|
|
434
521
|
);
|
|
522
|
+
if (resources && !plan.error) {
|
|
523
|
+
plan.resourceLimits = resources.resourceLimits;
|
|
524
|
+
if (plan.mode === ResumeMode.DOCKER_START && resources.update) {
|
|
525
|
+
plan.steps.unshift({
|
|
526
|
+
command: getDockerCommand(),
|
|
527
|
+
args: [
|
|
528
|
+
'update',
|
|
529
|
+
...resources.resourceLimits.filter((s) =>
|
|
530
|
+
/^--(?:memory|memory-swap|cpus|cpu-quota|cpu-period)=/.test(s)
|
|
531
|
+
),
|
|
532
|
+
plan.sessionName,
|
|
533
|
+
],
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
435
537
|
if (plan.error) {
|
|
436
538
|
return { success: false, error: plan.error };
|
|
437
539
|
}
|
|
@@ -444,15 +546,53 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
444
546
|
attempt,
|
|
445
547
|
};
|
|
446
548
|
appendLifecycle(lifecycleRecord, 'resume-started');
|
|
549
|
+
const updated = applyResumeToRecord(record, plan, null, attempt);
|
|
550
|
+
if (requestedLabels.length) {
|
|
551
|
+
updated.options.labels = labels;
|
|
552
|
+
}
|
|
553
|
+
if (resources) {
|
|
554
|
+
updated.options.resolvedLimits = resources.resolvedLimits;
|
|
555
|
+
updated.options.resourceLimitSpecs = resources.resourceLimitSpecs;
|
|
556
|
+
if (
|
|
557
|
+
opts.cpuPenaltyConfig &&
|
|
558
|
+
deps.resourceOptions?.cpus !== null &&
|
|
559
|
+
deps.resourceOptions?.cpus !== undefined
|
|
560
|
+
) {
|
|
561
|
+
updated.options.baseResourceLimits = resources.resourceLimits;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
reserveLaunch(store, updated, previous);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
appendLifecycle(lifecycleRecord, 'launch-failed', { error: error.message });
|
|
568
|
+
return {
|
|
569
|
+
success: false,
|
|
570
|
+
error: `Launch reservation failed: ${error.message}`,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (resources && deps.resourceOptions?.memory && record.logPath) {
|
|
575
|
+
const oldMemory = resources.oldMemory;
|
|
576
|
+
appendLogFile(
|
|
577
|
+
record.logPath,
|
|
578
|
+
`Resume memory limit: ${oldMemory} -> ${resources.resolvedLimits.memory} (${deps.resourceOptions.memory})\n`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
447
581
|
|
|
448
582
|
if (plan.mode === ResumeMode.RELAUNCH) {
|
|
449
583
|
const runIsolated = deps.runIsolated || require('./isolation').runIsolated;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
584
|
+
let launchResult;
|
|
585
|
+
try {
|
|
586
|
+
launchResult = await runIsolated(
|
|
587
|
+
plan.backend,
|
|
588
|
+
plan.command,
|
|
589
|
+
plan.launchOptions
|
|
590
|
+
);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
launchResult = { success: false, message: error.message };
|
|
593
|
+
}
|
|
455
594
|
if (!launchResult || !launchResult.success) {
|
|
595
|
+
rollbackLaunch(store, previous);
|
|
456
596
|
appendLifecycle(lifecycleRecord, 'launch-failed', {
|
|
457
597
|
error: launchResult?.message || 'unknown error',
|
|
458
598
|
});
|
|
@@ -465,20 +605,108 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
465
605
|
}
|
|
466
606
|
containerId = launchResult.containerId || null;
|
|
467
607
|
} else {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
608
|
+
let snapshotLock;
|
|
609
|
+
let snapshotCreated = false;
|
|
610
|
+
let derivedCreated = false;
|
|
611
|
+
try {
|
|
612
|
+
if (plan.mode === ResumeMode.DOCKER_SNAPSHOT) {
|
|
613
|
+
snapshotLock = acquireSnapshotLock(deps.snapshotOptions);
|
|
614
|
+
const preflight = preflightSnapshot(
|
|
615
|
+
plan.sessionName,
|
|
616
|
+
runner,
|
|
617
|
+
deps.snapshotOptions
|
|
618
|
+
);
|
|
619
|
+
plan.message += `\n${preflight.message}`;
|
|
620
|
+
if (record.logPath) {
|
|
621
|
+
appendLogFile(record.logPath, `${preflight.message}\n`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (plan.handoffCommand) {
|
|
625
|
+
const copied = writeCommandHandoff(
|
|
626
|
+
plan.sessionName,
|
|
627
|
+
plan.handoffCommand,
|
|
628
|
+
runner,
|
|
629
|
+
opts
|
|
630
|
+
);
|
|
631
|
+
if (!copied.success) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
(copied.stderr || '').trim() ||
|
|
634
|
+
copied.error ||
|
|
635
|
+
'Could not copy replacement command into the stopped container'
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (const step of plan.steps) {
|
|
640
|
+
let result;
|
|
641
|
+
try {
|
|
642
|
+
result = runner(step.command, step.args);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
result = { success: false, error: error.message };
|
|
645
|
+
}
|
|
646
|
+
if (!result.success) {
|
|
647
|
+
const detail =
|
|
648
|
+
(result.stderr || '').trim() ||
|
|
649
|
+
result.error ||
|
|
650
|
+
`exit code ${result.status}`;
|
|
651
|
+
throw new Error(detail);
|
|
652
|
+
}
|
|
653
|
+
if (step.args[0] === 'commit') {
|
|
654
|
+
snapshotCreated = true;
|
|
655
|
+
}
|
|
656
|
+
if (step.args[0] === 'create') {
|
|
657
|
+
derivedCreated = true;
|
|
658
|
+
}
|
|
659
|
+
// commit returns an image ID; cp/update/network commands are not IDs.
|
|
660
|
+
if (['run', 'create'].includes(step.args[0])) {
|
|
661
|
+
containerId = (result.stdout || '').trim() || containerId;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (plan.mode === ResumeMode.DOCKER_SNAPSHOT && deps.removeOriginal) {
|
|
665
|
+
const running = runner(getDockerCommand(), [
|
|
666
|
+
'inspect',
|
|
667
|
+
'-f',
|
|
668
|
+
'{{.State.Running}}',
|
|
669
|
+
plan.newSessionName,
|
|
670
|
+
]);
|
|
671
|
+
if (running.success && String(running.stdout).trim() === 'true') {
|
|
672
|
+
const removed = runner(getDockerCommand(), ['rm', plan.sessionName]);
|
|
673
|
+
const note = removed.success
|
|
674
|
+
? `Stopped original container removed: ${plan.sessionName}`
|
|
675
|
+
: `Original container retained: ${(removed.stderr || removed.error || 'docker rm failed').trim()}`;
|
|
676
|
+
plan.message += `\n${note}`;
|
|
677
|
+
if (record.logPath) {
|
|
678
|
+
appendLogFile(record.logPath, `${note}\n`);
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
plan.message +=
|
|
682
|
+
'\nOriginal container retained: the snapshot container is not running.';
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
} catch (error) {
|
|
686
|
+
if (derivedCreated) {
|
|
687
|
+
runner(getDockerCommand(), ['rm', plan.newSessionName]);
|
|
688
|
+
}
|
|
689
|
+
if (snapshotCreated) {
|
|
690
|
+
runner(getDockerCommand(), ['rmi', plan.snapshotImage]);
|
|
480
691
|
}
|
|
481
|
-
|
|
692
|
+
if (plan.handoffCommand) {
|
|
693
|
+
// A failed start must not leave a replacement hidden behind the old record.
|
|
694
|
+
try {
|
|
695
|
+
writeCommandHandoff(plan.sessionName, previous.command, runner, opts);
|
|
696
|
+
} catch {
|
|
697
|
+
/* Next explicit command can still repair the handoff. */
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
rollbackLaunch(store, previous);
|
|
701
|
+
appendLifecycle(lifecycleRecord, 'launch-failed', {
|
|
702
|
+
error: error.message,
|
|
703
|
+
});
|
|
704
|
+
return {
|
|
705
|
+
success: false,
|
|
706
|
+
error: `Failed to resume ${plan.backend} session "${plan.sessionName}": ${error.message}`,
|
|
707
|
+
};
|
|
708
|
+
} finally {
|
|
709
|
+
snapshotLock?.release();
|
|
482
710
|
}
|
|
483
711
|
|
|
484
712
|
const limitsLine = buildResourceLimitsStatusLine(plan.resourceLimits);
|
|
@@ -489,8 +717,16 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
489
717
|
|
|
490
718
|
const previousSessionName = plan.newSessionName ? plan.sessionName : null;
|
|
491
719
|
attempt.launchAcceptedAt = new Date().toISOString();
|
|
492
|
-
|
|
493
|
-
|
|
720
|
+
updated.options.launchPending = false;
|
|
721
|
+
if (containerId) {
|
|
722
|
+
updated.options.containerId = containerId;
|
|
723
|
+
}
|
|
724
|
+
let persistenceError = null;
|
|
725
|
+
try {
|
|
726
|
+
store.save(updated);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
persistenceError = error;
|
|
729
|
+
}
|
|
494
730
|
appendLifecycle(updated, 'launch-accepted');
|
|
495
731
|
|
|
496
732
|
if (plan.backend === 'docker') {
|
|
@@ -513,26 +749,50 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
513
749
|
if (watcher === false) {
|
|
514
750
|
throw new Error('Watcher process could not be started');
|
|
515
751
|
}
|
|
516
|
-
const current =
|
|
517
|
-
|
|
518
|
-
|
|
752
|
+
const current = persistenceError
|
|
753
|
+
? null
|
|
754
|
+
: patchAttempt(store, updated, {
|
|
755
|
+
watcherAttachedAt: new Date().toISOString(),
|
|
756
|
+
});
|
|
519
757
|
if (current) {
|
|
520
758
|
appendLifecycle(current, 'watcher-attached');
|
|
521
759
|
}
|
|
522
760
|
} catch (error) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
761
|
+
if (persistenceError) {
|
|
762
|
+
return {
|
|
763
|
+
success: false,
|
|
764
|
+
error: failLaunchedPersistence(
|
|
765
|
+
store,
|
|
766
|
+
updated,
|
|
767
|
+
runner,
|
|
768
|
+
persistenceError
|
|
769
|
+
),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const current = persistenceError
|
|
773
|
+
? null
|
|
774
|
+
: patchAttempt(store, updated, {
|
|
775
|
+
watcherError: error.message,
|
|
776
|
+
});
|
|
526
777
|
appendLifecycle(current || updated, 'watcher-attachment-failed', {
|
|
527
778
|
error: error.message,
|
|
528
779
|
});
|
|
529
780
|
return {
|
|
530
781
|
success: false,
|
|
531
|
-
error: `Launch accepted, but completion watcher attachment failed: ${error.message}`,
|
|
782
|
+
error: `Launch accepted for container ${activeSessionName(plan)}, but completion watcher attachment failed: ${error.message}`,
|
|
532
783
|
};
|
|
533
784
|
}
|
|
534
785
|
}
|
|
535
786
|
|
|
787
|
+
if (persistenceError) {
|
|
788
|
+
// The reservation remains durable and the watcher is attached. Stop the
|
|
789
|
+
// new container rather than allow an untracked command to keep running.
|
|
790
|
+
return {
|
|
791
|
+
success: false,
|
|
792
|
+
error: failLaunchedPersistence(store, updated, runner, persistenceError),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
536
796
|
return {
|
|
537
797
|
success: true,
|
|
538
798
|
output: formatResumeResult(
|