@link-assistant/hive-mind 2.16.0 → 2.18.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 +55 -4
- package/src/cleanup.mjs +57 -3
- package/src/disk-guard.lib.mjs +21 -1
- package/src/formal-ai-version.lib.mjs +10 -6
- package/src/github-url-parser.lib.mjs +80 -23
- package/src/github-url-recovery.lib.mjs +514 -0
- package/src/hive.mjs +10 -0
- 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/locales/en.lino +7 -0
- package/src/locales/hi.lino +7 -0
- package/src/locales/ru.lino +7 -0
- package/src/locales/zh.lino +7 -0
- package/src/pull-request-changes.lib.mjs +1 -1
- package/src/session-kill-diagnostics.lib.mjs +47 -5
- package/src/session-kill-resume.in-place.lib.mjs +136 -0
- package/src/session-kill-resume.lib.mjs +43 -16
- package/src/session-monitor.kill-sections.lib.mjs +8 -0
- package/src/session-store.lib.mjs +1 -1
- package/src/solve.clone-errors.lib.mjs +86 -0
- package/src/solve.repository.lib.mjs +36 -63
- package/src/solve.resource-diagnostics.lib.mjs +34 -1
- package/src/solve.validation.lib.mjs +16 -0
- package/src/start-command-cli.lib.mjs +60 -0
- package/src/telegram-bot.mjs +51 -95
- package/src/telegram-overrides-validation.lib.mjs +73 -0
- package/src/working-session-summary.lib.mjs +1 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
2
1
|
/**
|
|
3
2
|
* Isolation Runner for Telegram bot
|
|
4
3
|
*
|
|
@@ -30,30 +29,22 @@ import { buildRouterGitConfigEntries, buildRouterTaskEnv, getRouterSuppressedCre
|
|
|
30
29
|
import { acquireRouterForTask, attachRouterTaskContainer, registerFormalAiWithRouter, releaseRouterForTask } from './router-task-isolation.lib.mjs';
|
|
31
30
|
import { buildGitConfigEnv, GIT_PUSH_GUARD_CONTAINER_DIR, GIT_PUSH_GUARD_ESCAPE_ENV, hasForcePushOptIn, installGitPushGuard } from './git-push-guard.lib.mjs';
|
|
32
31
|
export { getDockerIsolationImage, resolveDockerIsolationImageTag } from './hive-mind-image.lib.mjs';
|
|
33
|
-
let commandStreamDollarPromise = null;
|
|
34
|
-
async function getCommandStreamDollar() {
|
|
35
|
-
if (!commandStreamDollarPromise) {
|
|
36
|
-
commandStreamDollarPromise = (async () => {
|
|
37
|
-
if (typeof globalThis.use === 'undefined') {
|
|
38
|
-
await ensureUseM();
|
|
39
|
-
}
|
|
40
|
-
const { $ } = await globalThis.use('command-stream');
|
|
41
|
-
return $;
|
|
42
|
-
})();
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
return await commandStreamDollarPromise;
|
|
46
|
-
} catch (error) {
|
|
47
|
-
commandStreamDollarPromise = null;
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
32
|
// Re-export the shared status predicates so existing callers that reach them via the isolation-runner module (e.g. session-monitor's `runner.isExecutingSessionStatus`) keep working. The canonical definitions live in session-status.lib.mjs so the killed/terminated/oom vocabulary stays consistent everywhere (issue #1927).
|
|
52
33
|
export { isExecutingSessionStatus, isTerminalSessionStatus, isKilledSessionStatus } from './session-status.lib.mjs';
|
|
53
34
|
// Issue #2175: the `$` output parsers live in their own module to keep this file
|
|
54
35
|
// under the 1350-line warning threshold. Re-exported so importers are unaffected.
|
|
55
36
|
import { isUnknownDockerExitCode, parseSessionExitFooter, parseSessionListOutput, parseSessionStatusOutput, parseStartCommandExecutionUuid, readSessionExitFromLog, shouldFallbackToScreenStatus } from './isolation-runner.parsers.lib.mjs';
|
|
56
37
|
export { isUnknownDockerExitCode, parseSessionExitFooter, parseSessionListOutput, parseSessionStatusOutput, parseStartCommandExecutionUuid, readSessionExitFromLog, shouldFallbackToScreenStatus };
|
|
38
|
+
// Issue #2189: the `$` loader and PATH lookup live in their own module so the
|
|
39
|
+
// resume/attach wrappers can use them without importing this runner (a cycle).
|
|
40
|
+
import { findStartCommandBinary, getCommandStreamDollar } from './start-command-cli.lib.mjs';
|
|
41
|
+
export { findStartCommandBinary };
|
|
42
|
+
// Issue #2189: `$ --resume` / `$ --resume-all`, added in start-command 0.33.0
|
|
43
|
+
// (link-foundation/start#162). Re-exported so callers keep reaching every
|
|
44
|
+
// isolation verb through this module.
|
|
45
|
+
import { resumeAllIsolationSessions, resumeIsolatedSession } from './isolation-runner.resume.lib.mjs';
|
|
46
|
+
export { resumeAllIsolationSessions, resumeIsolatedSession };
|
|
47
|
+
export { parseExecutionResumeAllOutput, parseExecutionResumeOutput, RESUME_ALL_ACTIONS, RESUME_MODES } from './isolation-runner.resume.lib.mjs';
|
|
57
48
|
// Valid isolation backends
|
|
58
49
|
const VALID_ISOLATION_BACKENDS = ['screen', 'tmux', 'docker'];
|
|
59
50
|
const DOCKER_CONTAINER_HOME = '/home/box';
|
|
@@ -311,20 +302,6 @@ async function runStartCommand(binPath, startCommandArgs) {
|
|
|
311
302
|
export function generateSessionId() {
|
|
312
303
|
return crypto.randomUUID();
|
|
313
304
|
}
|
|
314
|
-
/**
|
|
315
|
-
* Find the `$` CLI binary path
|
|
316
|
-
* @returns {Promise<string|null>} Path to `$` binary or null
|
|
317
|
-
*/
|
|
318
|
-
async function findStartCommandBinary() {
|
|
319
|
-
try {
|
|
320
|
-
const $ = await getCommandStreamDollar();
|
|
321
|
-
const result = await $({ mirror: false })`which $`;
|
|
322
|
-
const path = result.stdout?.toString().trim() || '';
|
|
323
|
-
return path || null;
|
|
324
|
-
} catch {
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
305
|
/**
|
|
329
306
|
* Verbose post-launch diagnostics for a native docker-isolated session.
|
|
330
307
|
*
|
|
@@ -611,6 +588,21 @@ export async function stopIsolatedSession(sessionId, verbose = false) {
|
|
|
611
588
|
console.log(`[VERBOSE] isolation-runner: $ --stop ${sessionId} stderr: ${stderr.substring(0, 300)}`);
|
|
612
589
|
}
|
|
613
590
|
}
|
|
591
|
+
// Issue #2189: `command-stream`'s `$` resolves — it does not throw — when the
|
|
592
|
+
// child exits non-zero, so the catch below never sees a refusal. `$ --stop`
|
|
593
|
+
// answers `Error: No execution found with UUID or session name: …` on stderr
|
|
594
|
+
// with exit code 1; without this check every such refusal was reported to the
|
|
595
|
+
// operator as a successful stop, and a session nobody stopped looked handled.
|
|
596
|
+
const code = Number.isFinite(result.code) ? result.code : 0;
|
|
597
|
+
if (code !== 0) {
|
|
598
|
+
// describeChildExit rather than an interpolated code: issue #2135 made it
|
|
599
|
+
// the single vocabulary for "how a child ended", so a `$` that was
|
|
600
|
+
// signalled reads the same way here as everywhere else. command-stream
|
|
601
|
+
// normalizes a signalled exit to 128+signum before we see it
|
|
602
|
+
// (node_modules/command-stream/src/$.process-runner-stream-kill.mjs:194),
|
|
603
|
+
// so there is no separate signal to pass.
|
|
604
|
+
return { success: false, output: stdout, error: stderr.trim() || describeChildExit({ command: '`$ --stop`', code }) };
|
|
605
|
+
}
|
|
614
606
|
return { success: true, output: stdout || stderr, error: null };
|
|
615
607
|
} catch (error) {
|
|
616
608
|
const stderr = error?.stderr?.toString?.() || '';
|
|
@@ -679,6 +671,37 @@ export async function checkDockerContainerRunning(containerName, verbose = false
|
|
|
679
671
|
return false;
|
|
680
672
|
}
|
|
681
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Check whether the Docker container backing a session still exists at all —
|
|
676
|
+
* running or stopped.
|
|
677
|
+
*
|
|
678
|
+
* Issue #2189 requirement R2: a killed session should be re-entered rather than
|
|
679
|
+
* restarted from scratch, and `$ --resume` can only do that while the container
|
|
680
|
+
* is still there. `checkDockerContainerRunning` answers a different question (a
|
|
681
|
+
* stopped container is "not running" but is exactly the one worth resuming), so
|
|
682
|
+
* the state is read instead of the running flag.
|
|
683
|
+
*
|
|
684
|
+
* @param {string} containerName - Container name (the session UUID)
|
|
685
|
+
* @param {boolean} [verbose] - Enable verbose logging
|
|
686
|
+
* @returns {Promise<boolean>} True when `docker inspect` finds the container
|
|
687
|
+
*/
|
|
688
|
+
export async function checkDockerContainerExists(containerName, verbose = false) {
|
|
689
|
+
if (!containerName) return false;
|
|
690
|
+
try {
|
|
691
|
+
const $ = await getCommandStreamDollar();
|
|
692
|
+
const result = await $({ mirror: false })`docker inspect -f ${'{{.State.Status}}'} ${containerName}`;
|
|
693
|
+
const code = Number.isFinite(result.code) ? result.code : 0;
|
|
694
|
+
const state = (result.stdout?.toString() || '').trim();
|
|
695
|
+
const exists = code === 0 && state !== '';
|
|
696
|
+
if (verbose) {
|
|
697
|
+
console.log(`[VERBOSE] isolation-runner: docker inspect state for '${containerName}': ${exists ? state : 'no such container'}`);
|
|
698
|
+
}
|
|
699
|
+
return exists;
|
|
700
|
+
} catch {
|
|
701
|
+
// `docker inspect` exits non-zero when no such container exists.
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
682
705
|
export function parseDockerContainerWritableLayerSizeOutput(output) {
|
|
683
706
|
const text = String(output || '').trim();
|
|
684
707
|
if (!text) return null;
|
|
@@ -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
|
+
}
|
package/src/locales/en.lino
CHANGED
|
@@ -508,6 +508,13 @@ en
|
|
|
508
508
|
must
|
|
509
509
|
be
|
|
510
510
|
type "URL must be a GitHub {{allowedTypes}} (not {{type}})"
|
|
511
|
+
recovered """
|
|
512
|
+
ℹ️ I repaired the link before starting.
|
|
513
|
+
|
|
514
|
+
You sent: {{original}}
|
|
515
|
+
Using: {{used}}
|
|
516
|
+
Repaired: {{repairs}}
|
|
517
|
+
"""
|
|
511
518
|
language
|
|
512
519
|
invalid """
|
|
513
520
|
❌ Invalid language. Supported: {{supported}}.
|
package/src/locales/hi.lino
CHANGED
|
@@ -508,6 +508,13 @@ hi
|
|
|
508
508
|
must
|
|
509
509
|
be
|
|
510
510
|
type "URL GitHub {{allowedTypes}} होना चाहिए ({{type}} नहीं)"
|
|
511
|
+
recovered """
|
|
512
|
+
ℹ️ शुरू करने से पहले लिंक ठीक की गई।
|
|
513
|
+
|
|
514
|
+
आपने भेजा: {{original}}
|
|
515
|
+
उपयोग किया जा रहा है: {{used}}
|
|
516
|
+
ठीक किया गया: {{repairs}}
|
|
517
|
+
"""
|
|
511
518
|
language
|
|
512
519
|
invalid """
|
|
513
520
|
❌ अमान्य भाषा। समर्थित: {{supported}}।
|
package/src/locales/ru.lino
CHANGED
|
@@ -508,6 +508,13 @@ ru
|
|
|
508
508
|
must
|
|
509
509
|
be
|
|
510
510
|
type "URL должен быть GitHub {{allowedTypes}} (не {{type}})"
|
|
511
|
+
recovered """
|
|
512
|
+
ℹ️ Ссылка была исправлена перед запуском.
|
|
513
|
+
|
|
514
|
+
Вы отправили: {{original}}
|
|
515
|
+
Используется: {{used}}
|
|
516
|
+
Исправлено: {{repairs}}
|
|
517
|
+
"""
|
|
511
518
|
language
|
|
512
519
|
invalid """
|
|
513
520
|
❌ Неверный язык. Поддерживаются: {{supported}}.
|
package/src/locales/zh.lino
CHANGED
|
@@ -508,6 +508,13 @@ zh
|
|
|
508
508
|
must
|
|
509
509
|
be
|
|
510
510
|
type "URL 必须是 GitHub {{allowedTypes}}(不是 {{type}})"
|
|
511
|
+
recovered """
|
|
512
|
+
ℹ️ 开始前已修复链接。
|
|
513
|
+
|
|
514
|
+
您发送的:{{original}}
|
|
515
|
+
实际使用:{{used}}
|
|
516
|
+
修复内容:{{repairs}}
|
|
517
|
+
"""
|
|
511
518
|
language
|
|
512
519
|
invalid """
|
|
513
520
|
❌ 语言无效。支持的语言:{{supported}}。
|
|
@@ -106,7 +106,7 @@ const measureDiff = diff => {
|
|
|
106
106
|
section = null;
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
for (let start = 0; start < diff.length;
|
|
109
|
+
for (let start = 0; start < diff.length;) {
|
|
110
110
|
let end = diff.indexOf('\n', start);
|
|
111
111
|
if (end === -1) end = diff.length;
|
|
112
112
|
const line = diff.slice(start, end);
|
|
@@ -54,6 +54,16 @@ export const KILL_DIAGNOSTICS_LOG_BYTES = 1024 * 1024;
|
|
|
54
54
|
*/
|
|
55
55
|
export const HEAP_EXHAUSTED_PERCENT = 90;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Prefix start-command 0.33.0 puts on every `exitReason` that reports memory
|
|
59
|
+
* exhaustion, whatever the mechanism — `memory-exhaustion (v8-heap-limit)`,
|
|
60
|
+
* `memory-exhaustion (kernel-oom-killer)`, `memory-exhaustion (go-runtime)`,
|
|
61
|
+
* `memory-exhaustion (allocation-failure)`. Matching the prefix rather than the
|
|
62
|
+
* exact strings means a new upstream mechanism is classified correctly without
|
|
63
|
+
* a Hive Mind release. See link-foundation/start#164 and #165.
|
|
64
|
+
*/
|
|
65
|
+
export const UPSTREAM_MEMORY_EXHAUSTION_PREFIX = 'memory-exhaustion';
|
|
66
|
+
|
|
57
67
|
/** Disk is considered full at or above this used percentage… */
|
|
58
68
|
export const DISK_FULL_USED_PERCENT = 95;
|
|
59
69
|
/** …or below this much free space, whichever triggers first. */
|
|
@@ -268,9 +278,12 @@ function describeDisk(disk, timestamp) {
|
|
|
268
278
|
* @param {number|null} [params.exitCode]
|
|
269
279
|
* @param {Object|null} [params.system] - collectSystemKillDiagnostics() result
|
|
270
280
|
* @param {boolean} [params.stopRequestedByUser] - The operator asked for the stop
|
|
281
|
+
* @param {boolean|null} [params.reportedMemoryExhausted] - `$ --status` `memoryExhausted` (start-command >= 0.33.0)
|
|
282
|
+
* @param {string|null} [params.reportedMemoryExhaustedReason] - `$ --status` `memoryExhaustedReason` (the evidence line)
|
|
283
|
+
* @param {string|null} [params.reportedExitReason] - `$ --status` `exitReason` hint, e.g. `memory-exhaustion (v8-heap-limit)`
|
|
271
284
|
* @returns {{cause: string, summary: string, evidence: string[], memory: Object|null, disk: Object|null, victims: Array}}
|
|
272
285
|
*/
|
|
273
|
-
export function describeKillCause({ logText = null, resourceMarkers = null, oomKilled = false, exitCode = null, system = null, stopRequestedByUser = false } = {}) {
|
|
286
|
+
export function describeKillCause({ logText = null, resourceMarkers = null, oomKilled = false, exitCode = null, system = null, stopRequestedByUser = false, reportedMemoryExhausted = null, reportedMemoryExhaustedReason = null, reportedExitReason = null } = {}) {
|
|
274
287
|
const parsed = resourceMarkers || (logText ? parseResourceMarkers(logText) : { markers: [], byPhase: {} });
|
|
275
288
|
const memoryMarker = selectLastMemoryResourceMarker(parsed);
|
|
276
289
|
const heapMarker = selectLastHeapResourceMarker(parsed);
|
|
@@ -310,6 +323,30 @@ export function describeKillCause({ logText = null, resourceMarkers = null, oomK
|
|
|
310
323
|
evidence.push(`the ${fatalMemoryMarker.runtime} runtime aborted on its own heap limit: \`${fatalMemoryMarker.line}\` (a self-abort is invisible to \`docker inspect\` and to cgroup OOM counters)`);
|
|
311
324
|
}
|
|
312
325
|
|
|
326
|
+
// Issue #2189 also went upstream: start-command 0.33.0 (link-foundation/start
|
|
327
|
+
// #164, #165) performs the same tail scan inside `$` and reports it as
|
|
328
|
+
// `memoryExhausted` / `memoryExhaustedReason` / `exitReason`. Consuming it
|
|
329
|
+
// catches the case our own scan cannot: the fatal line scrolled out of the
|
|
330
|
+
// bounded window we read, but `$` saw it when the command exited. The local
|
|
331
|
+
// scan stays as defense in depth — these fields are absent on an older `$`.
|
|
332
|
+
const reportedExitReasonText = typeof reportedExitReason === 'string' && reportedExitReason.trim() ? reportedExitReason.trim() : null;
|
|
333
|
+
const reportedMemoryExhaustion = abnormalExit && (reportedMemoryExhausted === true || (reportedExitReasonText !== null && reportedExitReasonText.startsWith(UPSTREAM_MEMORY_EXHAUSTION_PREFIX)));
|
|
334
|
+
// `memory-exhaustion (v8-heap-limit)` → `v8-heap-limit`: the prefix is already
|
|
335
|
+
// said in words by the surrounding sentence, so only the mechanism is new.
|
|
336
|
+
const reportedMechanism =
|
|
337
|
+
reportedMemoryExhaustion && reportedExitReasonText
|
|
338
|
+
? reportedExitReasonText
|
|
339
|
+
.slice(UPSTREAM_MEMORY_EXHAUSTION_PREFIX.length)
|
|
340
|
+
.trim()
|
|
341
|
+
.replace(/^\((.*)\)$/, '$1') || null
|
|
342
|
+
: null;
|
|
343
|
+
if (reportedMemoryExhaustion) {
|
|
344
|
+
const detail = reportedMemoryExhaustedReason ? `: \`${reportedMemoryExhaustedReason}\`` : '';
|
|
345
|
+
evidence.push(`\`$ --status\` reports memory exhaustion${reportedMechanism ? ` (\`${reportedMechanism}\`)` : ''}${detail}`);
|
|
346
|
+
} else if (reportedExitReasonText && abnormalExit) {
|
|
347
|
+
evidence.push(`\`$ --status\` reports \`exitReason = ${reportedExitReasonText}\``);
|
|
348
|
+
}
|
|
349
|
+
|
|
313
350
|
const ratio = memoryRatio(memory);
|
|
314
351
|
const memoryExhausted = ratio !== null && ratio <= MEMORY_EXHAUSTED_AVAILABLE_RATIO;
|
|
315
352
|
// A heap already at the limit is only evidence of a kill when the session
|
|
@@ -322,7 +359,7 @@ export function describeKillCause({ logText = null, resourceMarkers = null, oomK
|
|
|
322
359
|
let cause = KILL_CAUSE_UNKNOWN;
|
|
323
360
|
if (stopRequestedByUser) {
|
|
324
361
|
cause = KILL_CAUSE_FORCED_KILL;
|
|
325
|
-
} else if (victims.length > 0 || (cgroupOomKills !== null && cgroupOomKills > 0) || oomKilled || memoryExhausted || fatalMemoryMarker || heapExhausted) {
|
|
362
|
+
} else if (victims.length > 0 || (cgroupOomKills !== null && cgroupOomKills > 0) || oomKilled || memoryExhausted || fatalMemoryMarker || heapExhausted || reportedMemoryExhaustion) {
|
|
326
363
|
cause = KILL_CAUSE_OUT_OF_MEMORY;
|
|
327
364
|
} else if (diskFull) {
|
|
328
365
|
cause = KILL_CAUSE_DISK_FULL;
|
|
@@ -340,6 +377,11 @@ export function describeKillCause({ logText = null, resourceMarkers = null, oomK
|
|
|
340
377
|
// Same shape as the fatal-marker case, but reconstructed from telemetry when
|
|
341
378
|
// the fatal line itself was lost (truncated tail, killed before flushing).
|
|
342
379
|
summary = `out of memory — the runtime's own heap was exhausted: ${heapLine}${memoryLine ? ` (host memory was fine: ${memoryLine})` : ''}`;
|
|
380
|
+
} else if (cause === KILL_CAUSE_OUT_OF_MEMORY && reportedMemoryExhaustion && victims.length === 0 && !oomKilled && !(cgroupOomKills > 0) && !memoryExhausted) {
|
|
381
|
+
// Only `$` saw the evidence (our bounded window missed the fatal line).
|
|
382
|
+
// Quote what it saw rather than falling back to the host-memory phrasing,
|
|
383
|
+
// which would again read as a contradiction on a healthy machine.
|
|
384
|
+
summary = `out of memory — start-command reported memory exhaustion${reportedMechanism ? ` (${reportedMechanism})` : ''}${reportedMemoryExhaustedReason ? `: \`${reportedMemoryExhaustedReason}\`` : ''}${memoryLine ? ` (host memory was fine: ${memoryLine})` : ''}`;
|
|
343
385
|
} else if (cause === KILL_CAUSE_OUT_OF_MEMORY) {
|
|
344
386
|
const victim = victims.length > 0 ? `, kernel OOM killer terminated \`${victims[victims.length - 1].comm || 'unknown'}\` (pid ${victims[victims.length - 1].pid ?? '?'})` : '';
|
|
345
387
|
summary = `out of memory${memoryLine ? ` — ${memoryLine}` : ''}${victim}`;
|
|
@@ -352,7 +394,7 @@ export function describeKillCause({ logText = null, resourceMarkers = null, oomK
|
|
|
352
394
|
summary = 'unknown — no resource marker, cgroup counter or kernel OOM report was available';
|
|
353
395
|
}
|
|
354
396
|
|
|
355
|
-
return { cause, summary, evidence, memory, heap: heapMemory, heapUsedPercent, disk, victims, fatalMemoryMarker };
|
|
397
|
+
return { cause, summary, evidence, memory, heap: heapMemory, heapUsedPercent, disk, victims, fatalMemoryMarker, reportedMemoryExhaustion, reportedExitReason: reportedExitReasonText };
|
|
356
398
|
}
|
|
357
399
|
|
|
358
400
|
/**
|
|
@@ -429,7 +471,7 @@ export function formatKillResumeSection({ sessionId = null, attempt = null, maxA
|
|
|
429
471
|
* @param {Object} [options]
|
|
430
472
|
* @returns {Promise<{section: string, diagnosis: Object|null}>}
|
|
431
473
|
*/
|
|
432
|
-
export async function buildKillDiagnosticsSection(logPath, { verbose = false, readFile = fsPromises.readFile, maxLogBytes = KILL_DIAGNOSTICS_LOG_BYTES, oomKilled = false, exitCode = null, stopRequestedByUser = false, locale = null, collectSystem = collectSystemKillDiagnostics } = {}) {
|
|
474
|
+
export async function buildKillDiagnosticsSection(logPath, { verbose = false, readFile = fsPromises.readFile, maxLogBytes = KILL_DIAGNOSTICS_LOG_BYTES, oomKilled = false, exitCode = null, stopRequestedByUser = false, locale = null, collectSystem = collectSystemKillDiagnostics, reportedMemoryExhausted = null, reportedMemoryExhaustedReason = null, reportedExitReason = null } = {}) {
|
|
433
475
|
try {
|
|
434
476
|
let logText = '';
|
|
435
477
|
if (logPath) {
|
|
@@ -441,7 +483,7 @@ export async function buildKillDiagnosticsSection(logPath, { verbose = false, re
|
|
|
441
483
|
logText = await readLogTextBounded(logPath, { readFile, maxBytes: maxLogBytes, verbose });
|
|
442
484
|
}
|
|
443
485
|
const system = await collectSystem({ verbose });
|
|
444
|
-
const diagnosis = describeKillCause({ logText, oomKilled, exitCode, system, stopRequestedByUser });
|
|
486
|
+
const diagnosis = describeKillCause({ logText, oomKilled, exitCode, system, stopRequestedByUser, reportedMemoryExhausted, reportedMemoryExhaustedReason, reportedExitReason });
|
|
445
487
|
if (verbose) console.log(`[VERBOSE] kill-diagnostics: cause=${diagnosis.cause} — ${diagnosis.summary}`);
|
|
446
488
|
return { section: formatKillDiagnosticsSection(diagnosis, { locale }), diagnosis };
|
|
447
489
|
} catch (error) {
|