start-command 0.35.4 → 0.36.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +10 -0
  3. package/package.json +1 -1
  4. package/src/bin/cli.js +43 -7
  5. package/src/lib/args-parser.js +12 -0
  6. package/src/lib/atomic-write.js +34 -0
  7. package/src/lib/attached-diagnostics.js +90 -0
  8. package/src/lib/cgroup-memory.js +43 -11
  9. package/src/lib/command-builder.js +40 -0
  10. package/src/lib/cpu-penalty-monitor.js +226 -0
  11. package/src/lib/cpu-penalty.js +175 -0
  12. package/src/lib/detached-finalize.js +16 -0
  13. package/src/lib/docker-cleanup.js +23 -2
  14. package/src/lib/docker-resource-limits.js +28 -0
  15. package/src/lib/docker-resource-options.js +180 -0
  16. package/src/lib/docker-runtime-args.js +15 -0
  17. package/src/lib/execution-recovery.js +145 -29
  18. package/src/lib/execution-resume-all.js +11 -0
  19. package/src/lib/execution-resume.js +142 -29
  20. package/src/lib/execution-store.js +25 -121
  21. package/src/lib/exit-evidence.js +123 -0
  22. package/src/lib/exit-reason.js +24 -6
  23. package/src/lib/isolation.js +28 -3
  24. package/src/lib/launch-owner.js +25 -0
  25. package/src/lib/launch-persistence.js +54 -0
  26. package/src/lib/query-commands.js +14 -3
  27. package/src/lib/resume-resources.js +88 -0
  28. package/src/lib/status-formatter.js +28 -0
  29. package/src/lib/store-lock.js +133 -0
  30. package/src/lib/usage.js +11 -0
  31. package/test/docker-runtime-options.js +6 -0
  32. package/test/execution-resume.js +9 -3
  33. package/test/exit-reason.js +6 -1
  34. package/test/isolation.js +5 -4
  35. package/test/regression-174.js +3 -1
  36. package/test/regression-180.js +17 -10
  37. package/test/regression-182.js +7 -3
  38. package/test/regression-185.js +4 -9
  39. package/test/regression-187.js +2 -1
  40. package/test/regression-189.js +224 -0
  41. package/test/regression-190.js +111 -0
  42. package/test/regression-191.js +152 -0
  43. package/test/regression-192.js +66 -0
  44. package/test/regression-193.js +241 -0
  45. package/test/regression-194.js +182 -0
@@ -0,0 +1,182 @@
1
+ const { it, expect } = require('bun:test');
2
+ const {
3
+ resolveExitReason,
4
+ resolveMemoryExhaustion,
5
+ } = require('../src/lib/exit-reason');
6
+ it('historical child OOM kills cannot explain a subsequent SIGKILL', () => {
7
+ const facts = {
8
+ exitCode: 137,
9
+ oomKilled: true,
10
+ cgroupMemory: { oomKills: 3 },
11
+ };
12
+ expect(resolveExitReason(facts)).toBe('signal (SIGKILL; cause unknown)');
13
+ expect(resolveMemoryExhaustion(facts)).toBeNull();
14
+ });
15
+
16
+ const evidence = require('../src/lib/exit-evidence');
17
+ it('requires a recent sampled delta and rejects historical or malformed evidence', () => {
18
+ const finished = '1970-01-01T00:01:40Z';
19
+ expect(evidence.recentOomDelta('64 63 3 2 100 100', finished)).toBe(true);
20
+ for (const sample of [
21
+ '64 63 3 2',
22
+ '64 63 3 2 0 100',
23
+ '64 63 3 2 95 100',
24
+ '64 63 3 2 101 100',
25
+ '64 63 3 2 nope 100',
26
+ ]) {
27
+ expect(evidence.recentOomDelta(sample, finished)).toBe(false);
28
+ }
29
+ const line = evidence.collectExitEvidence(
30
+ {
31
+ containerName: 'task',
32
+ sample: '64 63 3 2 100 100',
33
+ finishedAt: finished,
34
+ exitCode: 137,
35
+ oomKilled: true,
36
+ },
37
+ () => ({ success: false, stdout: '' })
38
+ );
39
+ expect(line).toBe(evidence.MAIN_OOM);
40
+ expect(
41
+ resolveExitReason({ exitCode: 137, oomKilled: true, logTail: line })
42
+ ).toBe('memory-exhaustion (cgroup-oom-killer)');
43
+ });
44
+ it('requires Docker service and matching-container force-kill evidence for a daemon restart', () => {
45
+ const id = 'a'.repeat(64);
46
+ const journal = `docker.service: Main process exited\nContainer failed to exit within 10s of signal 15 - using the force container=${id}`;
47
+ expect(evidence.daemonRestartEvidence(journal, id)).toBe(true);
48
+ expect(
49
+ evidence.daemonRestartEvidence(
50
+ 'many containers exited at the same second',
51
+ id
52
+ )
53
+ ).toBe(false);
54
+ expect(evidence.daemonRestartEvidence(journal, 'b'.repeat(64))).toBe(false);
55
+ for (const oomKilled of [true, false]) {
56
+ const line = evidence.collectExitEvidence(
57
+ {
58
+ containerName: 'task',
59
+ sample: '64 63 3 2 100 100',
60
+ finishedAt: '1970-01-01T00:01:40Z',
61
+ exitCode: 137,
62
+ oomKilled,
63
+ },
64
+ (command) => ({
65
+ success: true,
66
+ stdout: command === 'journalctl' ? journal : id,
67
+ })
68
+ );
69
+ expect(line).toBe(evidence.DAEMON_RESTART);
70
+ expect(resolveExitReason({ exitCode: 137, oomKilled, logTail: line })).toBe(
71
+ 'killed (docker daemon restart)'
72
+ );
73
+ expect(
74
+ resolveMemoryExhaustion({ exitCode: 137, oomKilled, logTail: line })
75
+ ).toBeNull();
76
+ }
77
+ });
78
+ it('persists terminal evidence without requiring a surviving log file', () => {
79
+ const fs = require('fs'),
80
+ path = require('path'),
81
+ os = require('os');
82
+ const {
83
+ ExecutionStore,
84
+ ExecutionRecord,
85
+ } = require('../src/lib/execution-store');
86
+ const { finalizeDetachedExecution } = require('../src/lib/detached-finalize');
87
+ const { enrichDetachedStatus } = require('../src/lib/status-formatter');
88
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'evidence-194-'));
89
+ try {
90
+ const store = new ExecutionStore({ appFolder: dir, useLinks: false });
91
+ const record = new ExecutionRecord({
92
+ command: 'work',
93
+ options: { isolated: 'docker', isolationMode: 'detached' },
94
+ });
95
+ store.save(record);
96
+ finalizeDetachedExecution({
97
+ store,
98
+ executionId: record.uuid,
99
+ exitCode: 137,
100
+ oomKilled: true,
101
+ finishedAt: '1970-01-01T00:01:40Z',
102
+ running: false,
103
+ cgroupMemory: '64 63 3 2 100 100',
104
+ });
105
+ const current = store.get(record.uuid);
106
+ expect(current.options.exitEvidence.mainOom).toBe(true);
107
+ expect(enrichDetachedStatus(current).exitReason).toBe(
108
+ 'memory-exhaustion (cgroup-oom-killer)'
109
+ );
110
+ } finally {
111
+ fs.rmSync(dir, { recursive: true, force: true });
112
+ }
113
+ });
114
+
115
+ it('keeps logged main-process evidence when no cgroup sample reaches the finalizer', () => {
116
+ const fs = require('fs'),
117
+ path = require('path'),
118
+ os = require('os');
119
+ const {
120
+ ExecutionStore,
121
+ ExecutionRecord,
122
+ } = require('../src/lib/execution-store');
123
+ const { finalizeDetachedExecution } = require('../src/lib/detached-finalize');
124
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'logged-evidence-194-'));
125
+ try {
126
+ const store = new ExecutionStore({ appFolder: dir, useLinks: false });
127
+ const record = new ExecutionRecord({
128
+ command: 'work',
129
+ options: { isolated: 'docker', isolationMode: 'detached' },
130
+ });
131
+ record.logPath = path.join(dir, 'task.log');
132
+ fs.writeFileSync(record.logPath, `${evidence.MAIN_OOM}\n`);
133
+ store.save(record);
134
+ finalizeDetachedExecution({
135
+ store,
136
+ executionId: record.uuid,
137
+ exitCode: 137,
138
+ oomKilled: true,
139
+ finishedAt: '1970-01-01T00:01:40Z',
140
+ running: false,
141
+ });
142
+ expect(store.get(record.uuid).options.exitEvidence.mainOom).toBe(true);
143
+ fs.unlinkSync(record.logPath);
144
+ expect(
145
+ require('../src/lib/status-formatter').enrichDetachedStatus(
146
+ store.get(record.uuid)
147
+ ).exitReason
148
+ ).toBe('memory-exhaustion (cgroup-oom-killer)');
149
+ } finally {
150
+ fs.rmSync(dir, { recursive: true, force: true });
151
+ }
152
+ });
153
+
154
+ it('attached reporting uses attributed evidence and keeps ordinary exits final', () => {
155
+ const fs = require('fs'),
156
+ path = require('path'),
157
+ os = require('os');
158
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'attached-evidence-194-'));
159
+ try {
160
+ const logPath = path.join(dir, 'task.log');
161
+ fs.writeFileSync(logPath, `${evidence.MAIN_OOM}\n`);
162
+ const message =
163
+ require('../src/lib/docker-cleanup').buildAttachedDockerKeptMessage({
164
+ containerName: 'task',
165
+ exitCode: 137,
166
+ oomKilled: true,
167
+ logPath,
168
+ });
169
+ expect(message).toContain('reports it was OOM-killed');
170
+ for (const exitCode of [0, 1]) {
171
+ expect(
172
+ resolveMemoryExhaustion({
173
+ exitCode,
174
+ oomKilled: true,
175
+ logTail: evidence.MAIN_OOM,
176
+ })
177
+ ).toBeNull();
178
+ }
179
+ } finally {
180
+ fs.rmSync(dir, { recursive: true, force: true });
181
+ }
182
+ });