@link-assistant/hive-mind 2.15.2 → 2.17.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 +125 -0
- package/README.hi.md +12 -0
- package/README.md +15 -0
- package/README.ru.md +15 -0
- package/README.zh.md +24 -12
- package/package.json +24 -17
- package/src/agent-snapshot-store.lib.mjs +252 -0
- package/src/agent.lib.mjs +25 -25
- package/src/agent.version-gates.lib.mjs +73 -0
- package/src/bot-lifecycle.lib.mjs +63 -5
- package/src/child-exit.lib.mjs +53 -1
- package/src/claude.session-tokens.lib.mjs +10 -8
- package/src/claude.session-transcript-repair.lib.mjs +65 -34
- package/src/cleanup.mjs +57 -3
- package/src/codex.lib.mjs +11 -1
- package/src/development-log.lib.mjs +22 -7
- package/src/disk-guard.lib.mjs +21 -1
- package/src/formal-ai-version.lib.mjs +10 -6
- package/src/github-error-reporter.lib.mjs +84 -2
- package/src/github.lib.mjs +212 -152
- package/src/instrument.mjs +12 -14
- package/src/instrument.sanitize.lib.mjs +52 -0
- package/src/isolation-runner.lib.mjs +56 -33
- package/src/isolation-runner.parsers.lib.mjs +29 -3
- package/src/isolation-runner.resume.lib.mjs +263 -0
- package/src/log-bounded-read.lib.mjs +411 -0
- package/src/log-sanitize-stream.lib.mjs +267 -0
- package/src/log-sanitize-worker-entry.mjs +31 -0
- package/src/log-sanitize-worker.lib.mjs +186 -0
- package/src/log-upload.lib.mjs +16 -4
- package/src/pull-request-changes.lib.mjs +1 -1
- package/src/session-completion-state.lib.mjs +124 -0
- package/src/session-kill-diagnostics.lib.mjs +117 -12
- package/src/session-kill-policy.lib.mjs +18 -7
- package/src/session-kill-resume.in-place.lib.mjs +136 -0
- package/src/session-kill-resume.lib.mjs +48 -18
- package/src/session-monitor.kill-sections.lib.mjs +8 -0
- package/src/session-monitor.lib.mjs +132 -9
- package/src/session-store.lib.mjs +15 -1
- package/src/solve.clone-errors.lib.mjs +86 -0
- package/src/solve.config.lib.mjs +5 -2
- package/src/solve.repository.lib.mjs +36 -63
- package/src/solve.resource-diagnostics.lib.mjs +105 -5
- package/src/start-command-cli.lib.mjs +60 -0
- package/src/telegram-bot.mjs +31 -91
- package/src/telegram-log-command.lib.mjs +7 -3
- package/src/telegram-overrides-validation.lib.mjs +73 -0
- package/src/telegram-terminal-watch-command.lib.mjs +9 -1
- package/src/working-session-summary.lib.mjs +1 -1
|
@@ -73,13 +73,22 @@ export function parseStartCommandExecutionUuid(output) {
|
|
|
73
73
|
* `--output-format json` is supported, or human-readable key/value text.
|
|
74
74
|
* Keep the parser tolerant so completion monitoring survives either format.
|
|
75
75
|
*
|
|
76
|
+
* start-command 0.33.0 (link-foundation/start#164, #165) added three additive
|
|
77
|
+
* hint fields to a finished record: `exitReason` (e.g.
|
|
78
|
+
* `memory-exhaustion (v8-heap-limit)` or `signal (SIGSEGV)`),
|
|
79
|
+
* `memoryExhausted` and `memoryExhaustedReason` (the log line carrying the
|
|
80
|
+
* evidence). They are hints, never verdicts — upstream never lets them change
|
|
81
|
+
* `status`, `exitCode` or `oomKilled` — and they are absent on older `$`
|
|
82
|
+
* binaries, so they are parsed as nullable and every consumer keeps its own
|
|
83
|
+
* log-marker classification as defense in depth (issue #2189).
|
|
84
|
+
*
|
|
76
85
|
* @param {string} output - Raw stdout from `$ --status`
|
|
77
|
-
* @returns {{exists: boolean, uuid: string|null, status: string|null, exitCode: number|null, startTime: string|null, endTime: string|null, currentTime: string|null, logPath: string|null, command: string|null, isolation: string|null, workingDirectory: string|null, sessionName: string|null, processIds: Object, raw: string}}
|
|
86
|
+
* @returns {{exists: boolean, uuid: string|null, status: string|null, exitCode: number|null, startTime: string|null, endTime: string|null, currentTime: string|null, logPath: string|null, command: string|null, isolation: string|null, workingDirectory: string|null, sessionName: string|null, processIds: Object, oomKilled: boolean|null, exitReason: string|null, memoryExhausted: boolean|null, memoryExhaustedReason: string|null, raw: string}}
|
|
78
87
|
*/
|
|
79
88
|
export function parseSessionStatusOutput(output) {
|
|
80
89
|
const raw = (output || '').trim();
|
|
81
90
|
if (!raw) {
|
|
82
|
-
return { exists: false, uuid: null, status: null, exitCode: null, startTime: null, endTime: null, currentTime: null, logPath: null, command: null, isolation: null, workingDirectory: null, sessionName: null, processIds: {}, oomKilled: null, raw: '' };
|
|
91
|
+
return { exists: false, uuid: null, status: null, exitCode: null, startTime: null, endTime: null, currentTime: null, logPath: null, command: null, isolation: null, workingDirectory: null, sessionName: null, processIds: {}, oomKilled: null, exitReason: null, memoryExhausted: null, memoryExhaustedReason: null, raw: '' };
|
|
83
92
|
}
|
|
84
93
|
const normalizeBooleanField = value => {
|
|
85
94
|
if (typeof value === 'boolean') return value;
|
|
@@ -112,6 +121,9 @@ export function parseSessionStatusOutput(output) {
|
|
|
112
121
|
sessionName: data?.sessionName || data?.options?.sessionName || null,
|
|
113
122
|
processIds,
|
|
114
123
|
oomKilled: normalizeBooleanField(data?.oomKilled ?? data?.OOMKilled ?? data?.options?.oomKilled ?? data?.state?.oomKilled ?? data?.State?.OOMKilled),
|
|
124
|
+
exitReason: typeof data?.exitReason === 'string' && data.exitReason.trim() ? data.exitReason.trim() : null,
|
|
125
|
+
memoryExhausted: normalizeBooleanField(data?.memoryExhausted),
|
|
126
|
+
memoryExhaustedReason: typeof data?.memoryExhaustedReason === 'string' && data.memoryExhaustedReason.trim() ? data.memoryExhaustedReason.trim() : null,
|
|
115
127
|
raw,
|
|
116
128
|
};
|
|
117
129
|
} catch {
|
|
@@ -123,7 +135,11 @@ export function parseSessionStatusOutput(output) {
|
|
|
123
135
|
.find(line => line.trim() && !line.includes(' '))
|
|
124
136
|
?.trim() || null;
|
|
125
137
|
const readField = name => {
|
|
126
|
-
|
|
138
|
+
// Links notation separates key and value with whitespace (` exitReason x`);
|
|
139
|
+
// `--output-format text` uses a padded colon (`Exit Reason: x`). Accept
|
|
140
|
+
// both — the colon is optional, so every existing camelCase lookup is
|
|
141
|
+
// unchanged and the text labels (which contain a space) become readable too.
|
|
142
|
+
const match = raw.match(new RegExp(`^\\s*${name}\\s*:?\\s+"?([^"\\n]+)"?\\s*$`, 'mi'));
|
|
127
143
|
return match ? match[1].trim() : null;
|
|
128
144
|
};
|
|
129
145
|
const readBooleanField = name => normalizeBooleanField(readField(name));
|
|
@@ -154,6 +170,12 @@ export function parseSessionStatusOutput(output) {
|
|
|
154
170
|
sessionName: readField('sessionName'),
|
|
155
171
|
processIds,
|
|
156
172
|
oomKilled: readBooleanField('oomKilled'),
|
|
173
|
+
// `--output-format text` labels the same three fields `Exit Reason:`,
|
|
174
|
+
// `Memory Exhausted:` and `Memory Evidence:`; links notation uses the camelCase
|
|
175
|
+
// keys. Accept both so the parser does not depend on the output format.
|
|
176
|
+
exitReason: readField('exitReason') || readField('Exit Reason'),
|
|
177
|
+
memoryExhausted: readBooleanField('memoryExhausted') ?? readBooleanField('Memory Exhausted'),
|
|
178
|
+
memoryExhaustedReason: readField('memoryExhaustedReason') || readField('Memory Evidence'),
|
|
157
179
|
raw,
|
|
158
180
|
};
|
|
159
181
|
}
|
|
@@ -286,6 +308,10 @@ export function parseSessionListOutput(output) {
|
|
|
286
308
|
isolation: isolationCandidate ? isolationCandidate.toLowerCase() : null,
|
|
287
309
|
workingDirectory: data.workingDirectory || null,
|
|
288
310
|
sessionName: data.sessionName || data.options?.sessionName || null,
|
|
311
|
+
// Additive 0.33.0 hints (link-foundation/start#164, #165); null on older `$`.
|
|
312
|
+
exitReason: typeof data.exitReason === 'string' && data.exitReason.trim() ? data.exitReason.trim() : null,
|
|
313
|
+
memoryExhausted: typeof data.memoryExhausted === 'boolean' ? data.memoryExhausted : null,
|
|
314
|
+
memoryExhaustedReason: typeof data.memoryExhaustedReason === 'string' && data.memoryExhaustedReason.trim() ? data.memoryExhaustedReason.trim() : null,
|
|
289
315
|
};
|
|
290
316
|
})
|
|
291
317
|
.filter(Boolean);
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `$ --resume` / `$ --resume-all` wrappers (start-command >= 0.33.0).
|
|
3
|
+
*
|
|
4
|
+
* Issue #2189 reported that a killed working session was only ever *offered*
|
|
5
|
+
* for resume, and that when Hive Mind did resume one it had to start a fresh
|
|
6
|
+
* isolated run — the container the work had happened in, with its clone, its
|
|
7
|
+
* build cache and its half-finished branch, was thrown away. The missing
|
|
8
|
+
* capability was filed upstream as link-foundation/start#162 and delivered in
|
|
9
|
+
* `start-command@0.33.0`:
|
|
10
|
+
*
|
|
11
|
+
* - `$ --resume <id> -- <command>` re-enters an existing execution. For a
|
|
12
|
+
* stopped docker session it commits the container filesystem and runs the
|
|
13
|
+
* new command in a container derived from that snapshot, so the workspace
|
|
14
|
+
* survives. The execution UUID is preserved, so `--status`, `--list` and
|
|
15
|
+
* `--upload-log` keep addressing one logical session across restarts.
|
|
16
|
+
* - `$ --resume-all` re-attaches a completion watcher to every execution
|
|
17
|
+
* still marked running and reconciles the ones that ended unsupervised. It
|
|
18
|
+
* never restarts work silently.
|
|
19
|
+
*
|
|
20
|
+
* Both are additive: a Hive Mind talking to an older `$` gets a clean
|
|
21
|
+
* `unsupported` result and the caller falls back to its previous behaviour.
|
|
22
|
+
* Neither wrapper throws.
|
|
23
|
+
*
|
|
24
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2189
|
|
25
|
+
* @see https://github.com/link-foundation/start/issues/162
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { describeChildExit } from './child-exit.lib.mjs';
|
|
29
|
+
import { findStartCommandBinary, getCommandStreamDollar, START_COMMAND_MISSING_ERROR } from './start-command-cli.lib.mjs';
|
|
30
|
+
|
|
31
|
+
/** Strategies `$ --resume` can pick, mirroring upstream `ResumeMode`. */
|
|
32
|
+
export const RESUME_MODES = Object.freeze({
|
|
33
|
+
DOCKER_START: 'docker-start',
|
|
34
|
+
DOCKER_SNAPSHOT: 'docker-snapshot',
|
|
35
|
+
RELAUNCH: 'relaunch',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Outcomes `$ --resume-all` reports per execution, mirroring `ResumeAllAction`. */
|
|
39
|
+
export const RESUME_ALL_ACTIONS = Object.freeze({
|
|
40
|
+
REATTACHED: 'reattached',
|
|
41
|
+
RUNNING: 'running',
|
|
42
|
+
RECONCILED: 'reconciled',
|
|
43
|
+
UNKNOWN: 'unknown',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Does this `$` failure mean the verb does not exist yet?
|
|
48
|
+
*
|
|
49
|
+
* An older binary rejects the flag while parsing, long before it looks at the
|
|
50
|
+
* store. Distinguishing that from a real refusal ("session is still running")
|
|
51
|
+
* is what lets the caller degrade gracefully instead of reporting a bug.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} message - stderr/message from the failed invocation
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
export function isUnsupportedStartCommandVerb(message) {
|
|
57
|
+
const text = String(message || '').toLowerCase();
|
|
58
|
+
// 0.32.1 answers `$ --resume-all` with `Error: Unknown wrapper option:
|
|
59
|
+
// --resume-all` (verified against the pinned pre-0.33.0 binary), and other
|
|
60
|
+
// argument parsers word it differently; match the family, not one string.
|
|
61
|
+
return /unknown (\w+ )?(option|argument|flag)|unrecognized option|invalid option|no such option/.test(text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse the `executionResume` block `$ --resume --output-format json` prints.
|
|
66
|
+
*
|
|
67
|
+
* Tolerates links notation too (`executionResume` followed by indented
|
|
68
|
+
* `key value` pairs), because an operator's `$` may default to it.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} output - Raw stdout
|
|
71
|
+
* @returns {{uuid: string|null, mode: string|null, backend: string|null, sessionName: string|null, previousSessionName: string|null, snapshotImage: string|null, command: string|null, message: string|null}}
|
|
72
|
+
*/
|
|
73
|
+
export function parseExecutionResumeOutput(output) {
|
|
74
|
+
const empty = { uuid: null, mode: null, backend: null, sessionName: null, previousSessionName: null, snapshotImage: null, command: null, message: null };
|
|
75
|
+
const raw = (output || '').trim();
|
|
76
|
+
if (!raw) return empty;
|
|
77
|
+
const str = value => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
const data = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
81
|
+
return {
|
|
82
|
+
uuid: str(data?.uuid),
|
|
83
|
+
mode: str(data?.mode),
|
|
84
|
+
backend: str(data?.backend),
|
|
85
|
+
sessionName: str(data?.sessionName),
|
|
86
|
+
previousSessionName: str(data?.previousSessionName),
|
|
87
|
+
snapshotImage: str(data?.snapshotImage),
|
|
88
|
+
command: str(data?.command),
|
|
89
|
+
message: str(data?.message),
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
// Links notation — fall through.
|
|
93
|
+
}
|
|
94
|
+
// Links notation indents `key value`; `--output-format text` prints
|
|
95
|
+
// `Label: value`. The optional colon covers both with one expression.
|
|
96
|
+
const readField = name => {
|
|
97
|
+
const match = raw.match(new RegExp(`^\\s*${name}\\s*:?\\s+"?([^"\\n]+)"?\\s*$`, 'mi'));
|
|
98
|
+
return str(match?.[1]);
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
uuid: readField('uuid'),
|
|
102
|
+
mode: readField('mode') || readField('Resume Mode'),
|
|
103
|
+
backend: readField('backend'),
|
|
104
|
+
sessionName: readField('sessionName') || readField('Session Name'),
|
|
105
|
+
previousSessionName: readField('previousSessionName'),
|
|
106
|
+
snapshotImage: readField('snapshotImage'),
|
|
107
|
+
command: readField('command'),
|
|
108
|
+
message: readField('message'),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse the `executionResumeAll` block `$ --resume-all --output-format json`
|
|
114
|
+
* prints. Anything unparseable yields an empty list rather than a throw — a
|
|
115
|
+
* startup reconciliation must never be able to stop the bot from starting.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} output - Raw stdout
|
|
118
|
+
* @returns {Array<{uuid: string|null, backend: string|null, sessionName: string|null, state: string|null, action: string|null, exitCode: number|null, message: string|null}>}
|
|
119
|
+
*/
|
|
120
|
+
export function parseExecutionResumeAllOutput(output) {
|
|
121
|
+
const raw = (output || '').trim();
|
|
122
|
+
if (!raw) return [];
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(raw);
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const records = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.executions) ? parsed.executions : [];
|
|
130
|
+
const str = value => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
131
|
+
return records
|
|
132
|
+
.map(entry => {
|
|
133
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
134
|
+
const exitCode = entry.exitCode === null || entry.exitCode === undefined ? null : Number(entry.exitCode);
|
|
135
|
+
return {
|
|
136
|
+
uuid: str(entry.uuid),
|
|
137
|
+
backend: str(entry.backend),
|
|
138
|
+
sessionName: str(entry.sessionName),
|
|
139
|
+
state: str(entry.state),
|
|
140
|
+
action: str(entry.action),
|
|
141
|
+
exitCode: Number.isFinite(exitCode) ? exitCode : null,
|
|
142
|
+
message: str(entry.message),
|
|
143
|
+
};
|
|
144
|
+
})
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Normalize what `command-stream`'s `$` hands back for one invocation.
|
|
150
|
+
*
|
|
151
|
+
* `$` does *not* throw on a non-zero exit — it resolves with `code` set (checked
|
|
152
|
+
* against command-stream in experiments/issue-2189-start-command-resume.mjs).
|
|
153
|
+
* Reading only the resolved value would therefore report every refusal
|
|
154
|
+
* ("session is still running", "no execution found", "unknown wrapper option")
|
|
155
|
+
* as a successful resume. Both shapes are folded into one verdict here.
|
|
156
|
+
*
|
|
157
|
+
* @param {object|null} result - Resolved value from `$`
|
|
158
|
+
* @param {*} [error] - Rejection from `$`, when it threw instead
|
|
159
|
+
* @returns {{ok: boolean, stdout: string, message: string|null, unsupported: boolean}}
|
|
160
|
+
*/
|
|
161
|
+
function interpretStartCommandResult(result, error = null) {
|
|
162
|
+
const source = error || result || {};
|
|
163
|
+
const stdout = source.stdout?.toString?.().trim() || '';
|
|
164
|
+
const stderr = source.stderr?.toString?.().trim() || '';
|
|
165
|
+
const code = error ? (Number.isFinite(source.code) ? source.code : 1) : Number.isFinite(source.code) ? source.code : 0;
|
|
166
|
+
if (!error && code === 0) return { ok: true, stdout, message: null, unsupported: false };
|
|
167
|
+
// describeChildExit is the repository's single vocabulary for "how a child
|
|
168
|
+
// ended" (issue #2135); command-stream has already normalized a signalled
|
|
169
|
+
// exit to 128+signum by this point, so the code is all there is to say.
|
|
170
|
+
const message = stderr || source.message || describeChildExit({ command: 'start-command', code });
|
|
171
|
+
return { ok: false, stdout, message, unsupported: isUnsupportedStartCommandVerb(`${message}\n${stdout}`) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Re-enter an existing execution via `$ --resume <identifier> [-- <command>]`.
|
|
176
|
+
*
|
|
177
|
+
* With a `command`, a stopped docker session is snapshotted and the command runs
|
|
178
|
+
* against that snapshot, so the work already on disk is preserved — this is the
|
|
179
|
+
* "re-enter the same container" half of issue #2189. Without one, the stored
|
|
180
|
+
* command is re-run in place.
|
|
181
|
+
*
|
|
182
|
+
* The command is passed as a single argument after `--`, exactly like the launch
|
|
183
|
+
* path does: start-command 0.33.0 preserves argv boundaries and runs a lone
|
|
184
|
+
* argument verbatim as a shell script, so no quoting is lost.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} identifier - Execution UUID or session name
|
|
187
|
+
* @param {Object} [options]
|
|
188
|
+
* @param {string|null} [options.command] - Command to run against the resumed session
|
|
189
|
+
* @param {boolean} [options.verbose]
|
|
190
|
+
* @returns {Promise<{success: boolean, unsupported: boolean, uuid: string|null, mode: string|null, backend: string|null, sessionName: string|null, previousSessionName: string|null, snapshotImage: string|null, message: string|null, output: string, error: string|null}>}
|
|
191
|
+
*/
|
|
192
|
+
export async function resumeIsolatedSession(identifier, { command = null, verbose = false } = {}) {
|
|
193
|
+
const base = { success: false, unsupported: false, uuid: null, mode: null, backend: null, sessionName: null, previousSessionName: null, snapshotImage: null, message: null, output: '', error: null };
|
|
194
|
+
if (!identifier) return { ...base, error: 'No execution identifier was given to resume.' };
|
|
195
|
+
|
|
196
|
+
const binPath = await findStartCommandBinary();
|
|
197
|
+
if (!binPath) {
|
|
198
|
+
if (verbose) console.log('[VERBOSE] isolation-runner: cannot resume - $ binary not found');
|
|
199
|
+
return { ...base, unsupported: true, error: START_COMMAND_MISSING_ERROR };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const $ = await getCommandStreamDollar();
|
|
204
|
+
const raw = command ? await $({ mirror: false })`${binPath} --resume ${identifier} --output-format json -- ${command}` : await $({ mirror: false })`${binPath} --resume ${identifier} --output-format json`;
|
|
205
|
+
const { ok, stdout, message, unsupported } = interpretStartCommandResult(raw);
|
|
206
|
+
if (!ok) {
|
|
207
|
+
if (verbose) console.log(`[VERBOSE] isolation-runner: $ --resume ${identifier} refused${unsupported ? ' (verb not supported by this $ build)' : ''}: ${message}`);
|
|
208
|
+
return { ...base, unsupported, output: stdout, error: message };
|
|
209
|
+
}
|
|
210
|
+
const parsed = parseExecutionResumeOutput(stdout);
|
|
211
|
+
if (verbose) {
|
|
212
|
+
console.log(`[VERBOSE] isolation-runner: $ --resume ${identifier} → mode=${parsed.mode || '(unknown)'} session=${parsed.sessionName || '(unknown)'} uuid=${parsed.uuid || '(unknown)'}`);
|
|
213
|
+
}
|
|
214
|
+
return { ...base, ...parsed, success: true, output: stdout };
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const { stdout, message, unsupported } = interpretStartCommandResult(null, error);
|
|
217
|
+
if (verbose) console.log(`[VERBOSE] isolation-runner: $ --resume ${identifier} failed${unsupported ? ' (verb not supported by this $ build)' : ''}: ${message}`);
|
|
218
|
+
return { ...base, unsupported, output: stdout, error: message };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Reconcile every execution still marked running via `$ --resume-all`.
|
|
224
|
+
*
|
|
225
|
+
* Run at bot startup: the detached-docker completion watchers are children of
|
|
226
|
+
* the process that launched them, so a bot restart leaves every running
|
|
227
|
+
* container unsupervised — its exit would never be written to the log footer,
|
|
228
|
+
* which is one of the ways issue #2189's session stayed in limbo. `--resume-all`
|
|
229
|
+
* re-attaches a watcher to what is alive and finalizes what died meanwhile. It
|
|
230
|
+
* starts no work on its own.
|
|
231
|
+
*
|
|
232
|
+
* @param {Object} [options]
|
|
233
|
+
* @param {boolean} [options.verbose]
|
|
234
|
+
* @returns {Promise<{success: boolean, unsupported: boolean, executions: Array<Object>, output: string, error: string|null}>}
|
|
235
|
+
*/
|
|
236
|
+
export async function resumeAllIsolationSessions({ verbose = false } = {}) {
|
|
237
|
+
const base = { success: false, unsupported: false, executions: [], output: '', error: null };
|
|
238
|
+
const binPath = await findStartCommandBinary();
|
|
239
|
+
if (!binPath) {
|
|
240
|
+
if (verbose) console.log('[VERBOSE] isolation-runner: cannot run $ --resume-all - $ binary not found');
|
|
241
|
+
return { ...base, unsupported: true, error: START_COMMAND_MISSING_ERROR };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const $ = await getCommandStreamDollar();
|
|
246
|
+
const raw = await $({ mirror: false })`${binPath} --resume-all --output-format json`;
|
|
247
|
+
const { ok, stdout, message, unsupported } = interpretStartCommandResult(raw);
|
|
248
|
+
if (!ok) {
|
|
249
|
+
if (verbose) console.log(`[VERBOSE] isolation-runner: $ --resume-all refused${unsupported ? ' (verb not supported by this $ build)' : ''}: ${message}`);
|
|
250
|
+
return { ...base, unsupported, output: stdout, error: message };
|
|
251
|
+
}
|
|
252
|
+
const executions = parseExecutionResumeAllOutput(stdout);
|
|
253
|
+
if (verbose) {
|
|
254
|
+
const summary = executions.map(entry => `${entry.action}:${entry.sessionName || entry.uuid}`).join(', ') || '(none)';
|
|
255
|
+
console.log(`[VERBOSE] isolation-runner: $ --resume-all reconciled ${executions.length} execution(s): ${summary}`);
|
|
256
|
+
}
|
|
257
|
+
return { ...base, success: true, executions, output: stdout };
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const { stdout, message, unsupported } = interpretStartCommandResult(null, error);
|
|
260
|
+
if (verbose) console.log(`[VERBOSE] isolation-runner: $ --resume-all failed${unsupported ? ' (verb not supported by this $ build)' : ''}: ${message}`);
|
|
261
|
+
return { ...base, unsupported, output: stdout, error: message };
|
|
262
|
+
}
|
|
263
|
+
}
|