@link-assistant/hive-mind 1.74.0 → 1.74.2
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 +30 -0
- package/README.hi.md +30 -2
- package/README.md +33 -2
- package/README.ru.md +32 -1
- package/README.zh.md +30 -2
- package/package.json +1 -1
- package/src/claude.lib.mjs +2 -0
- package/src/cleanup.mjs +75 -1
- package/src/cleanup.os.lib.mjs +341 -0
- package/src/codex.lib.mjs +2 -0
- package/src/interactive-image-render.lib.mjs +140 -0
- package/src/interactive-image-upload.lib.mjs +415 -0
- package/src/interactive-mode.lib.mjs +27 -8
- package/src/interactive-mode.shared.lib.mjs +97 -0
- package/src/isolation-runner.lib.mjs +27 -4
- package/src/process-debug.lib.mjs +361 -0
- package/src/solve.config.lib.mjs +9 -0
|
@@ -25,6 +25,16 @@ const VALID_ISOLATION_BACKENDS = ['screen', 'tmux', 'docker'];
|
|
|
25
25
|
const RUNNING_SESSION_STATUSES = new Set(['executing', 'running']);
|
|
26
26
|
const TERMINAL_SESSION_STATUSES = new Set(['executed', 'completed', 'failed', 'cancelled', 'canceled', 'error']);
|
|
27
27
|
|
|
28
|
+
function normalizeProcessIds(value) {
|
|
29
|
+
if (!value || typeof value !== 'object') return {};
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
32
|
+
const number = Number(raw);
|
|
33
|
+
if (Number.isInteger(number) && number > 0) out[key] = number;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
/**
|
|
29
39
|
* Generate a UUID v4 for unique session identification
|
|
30
40
|
* @returns {string} UUID v4 string
|
|
@@ -41,12 +51,12 @@ export function generateSessionId() {
|
|
|
41
51
|
* Keep the parser tolerant so completion monitoring survives either format.
|
|
42
52
|
*
|
|
43
53
|
* @param {string} output - Raw stdout from `$ --status`
|
|
44
|
-
* @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, raw: string}}
|
|
54
|
+
* @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}}
|
|
45
55
|
*/
|
|
46
56
|
export function parseSessionStatusOutput(output) {
|
|
47
57
|
const raw = (output || '').trim();
|
|
48
58
|
if (!raw) {
|
|
49
|
-
return { exists: false, uuid: null, status: null, exitCode: null, startTime: null, endTime: null, currentTime: null, logPath: null, command: null, isolation: null, workingDirectory: null, raw: '' };
|
|
59
|
+
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: {}, raw: '' };
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
try {
|
|
@@ -58,6 +68,9 @@ export function parseSessionStatusOutput(output) {
|
|
|
58
68
|
// field — keep accepting all three so we are tolerant of future renames.
|
|
59
69
|
// See https://github.com/link-assistant/hive-mind/issues/1700.
|
|
60
70
|
const isolationCandidate = (typeof data?.isolation === 'string' && data.isolation) || (typeof data?.options?.isolated === 'string' && data.options.isolated) || (typeof data?.options?.isolation === 'string' && data.options.isolation) || null;
|
|
71
|
+
const topPid = Number(data?.pid);
|
|
72
|
+
const processIds = normalizeProcessIds(data?.processIds);
|
|
73
|
+
if (Number.isInteger(topPid) && topPid > 0 && processIds.pid == null) processIds.pid = topPid;
|
|
61
74
|
return {
|
|
62
75
|
exists: true,
|
|
63
76
|
uuid: data?.uuid || null,
|
|
@@ -70,6 +83,8 @@ export function parseSessionStatusOutput(output) {
|
|
|
70
83
|
command: data?.command || null,
|
|
71
84
|
isolation: isolationCandidate ? isolationCandidate.toLowerCase() : null,
|
|
72
85
|
workingDirectory: data?.workingDirectory || null,
|
|
86
|
+
sessionName: data?.sessionName || data?.options?.sessionName || null,
|
|
87
|
+
processIds,
|
|
73
88
|
raw,
|
|
74
89
|
};
|
|
75
90
|
} catch {
|
|
@@ -95,6 +110,12 @@ export function parseSessionStatusOutput(output) {
|
|
|
95
110
|
// returned null for every real session and made /log + /terminal_watch
|
|
96
111
|
// reject screen/tmux/docker sessions. See issue #1700.
|
|
97
112
|
const isolationText = readField('isolated') || readField('isolation');
|
|
113
|
+
const processIds = {};
|
|
114
|
+
for (const name of ['pid', 'wrapperPid', 'childPid', 'processPid', 'commandPid']) {
|
|
115
|
+
const value = readField(name);
|
|
116
|
+
const number = Number(value);
|
|
117
|
+
if (Number.isInteger(number) && number > 0) processIds[name] = number;
|
|
118
|
+
}
|
|
98
119
|
|
|
99
120
|
return {
|
|
100
121
|
exists: Boolean(status || firstLine),
|
|
@@ -108,6 +129,8 @@ export function parseSessionStatusOutput(output) {
|
|
|
108
129
|
command: readField('command'),
|
|
109
130
|
isolation: isolationText?.toLowerCase() || null,
|
|
110
131
|
workingDirectory: readField('workingDirectory'),
|
|
132
|
+
sessionName: readField('sessionName'),
|
|
133
|
+
processIds,
|
|
111
134
|
raw,
|
|
112
135
|
};
|
|
113
136
|
}
|
|
@@ -234,7 +257,7 @@ export async function querySessionStatus(sessionId, verbose = false) {
|
|
|
234
257
|
if (verbose) {
|
|
235
258
|
console.log('[VERBOSE] isolation-runner: Cannot query status - $ binary not found');
|
|
236
259
|
}
|
|
237
|
-
return { exists: false, uuid: null, status: null, exitCode: null, startTime: null, endTime: null, currentTime: null, logPath: null, command: null, isolation: null, workingDirectory: null, raw: '' };
|
|
260
|
+
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: {}, raw: '' };
|
|
238
261
|
}
|
|
239
262
|
|
|
240
263
|
try {
|
|
@@ -251,7 +274,7 @@ export async function querySessionStatus(sessionId, verbose = false) {
|
|
|
251
274
|
if (verbose) {
|
|
252
275
|
console.log(`[VERBOSE] isolation-runner: Status query error: ${error.message}`);
|
|
253
276
|
}
|
|
254
|
-
return { exists: false, uuid: null, status: null, exitCode: null, startTime: null, endTime: null, currentTime: null, logPath: null, command: null, isolation: null, workingDirectory: null, raw: '' };
|
|
277
|
+
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: {}, raw: '' };
|
|
255
278
|
}
|
|
256
279
|
}
|
|
257
280
|
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure process/session correlation helpers for cleanup process diagnostics
|
|
3
|
+
* (issue #1851).
|
|
4
|
+
*
|
|
5
|
+
* This module intentionally avoids /proc, screen, filesystem, and network
|
|
6
|
+
* access. The OS layer supplies process records and start-command session
|
|
7
|
+
* metadata; this file only parses, matches, redacts, and formats.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { extractTaskRefsFromCommand } from './cleanup.lib.mjs';
|
|
13
|
+
|
|
14
|
+
const AGENT_KINDS = ['claude', 'codex', 'gemini', 'qwen', 'opencode'];
|
|
15
|
+
const RUNNING_STATUSES = new Set(['executing', 'running']);
|
|
16
|
+
const TERMINAL_STATUSES = new Set(['executed', 'completed', 'failed', 'cancelled', 'canceled', 'error']);
|
|
17
|
+
|
|
18
|
+
function taskUrlFromRef(ref) {
|
|
19
|
+
if (!ref) return null;
|
|
20
|
+
const kind = ref.type === 'pull' ? 'pull' : 'issues';
|
|
21
|
+
return `https://github.com/${ref.owner}/${ref.repo}/${kind}/${ref.number}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function firstTaskUrl(text) {
|
|
25
|
+
const refs = extractTaskRefsFromCommand(text || '');
|
|
26
|
+
return taskUrlFromRef(refs[0]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeWhitespace(value) {
|
|
30
|
+
return String(value || '')
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toPositiveInteger(value) {
|
|
36
|
+
const number = Number(value);
|
|
37
|
+
return Number.isInteger(number) && number > 0 ? number : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeProcessIds(value) {
|
|
41
|
+
const out = {};
|
|
42
|
+
if (!value || typeof value !== 'object') return out;
|
|
43
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
44
|
+
const number = toPositiveInteger(raw);
|
|
45
|
+
if (number) out[key] = number;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizePath(value) {
|
|
51
|
+
if (!value || typeof value !== 'string') return null;
|
|
52
|
+
return value.trim().replace(/\/+$/, '') || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isPathInside(candidate, parent) {
|
|
56
|
+
const child = normalizePath(candidate);
|
|
57
|
+
const root = normalizePath(parent);
|
|
58
|
+
if (!child || !root) return false;
|
|
59
|
+
return child === root || child.startsWith(root + path.sep);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function containsToken(text, token) {
|
|
63
|
+
return Boolean(text && token && String(text).includes(String(token)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readFlagValue(command, flag) {
|
|
67
|
+
if (!command) return null;
|
|
68
|
+
const re = new RegExp(`(?:^|\\s)${flag}(?:=|\\s+)("([^"]+)"|'([^']+)'|\\S+)`, 'i');
|
|
69
|
+
const match = command.match(re);
|
|
70
|
+
return match ? (match[2] || match[3] || match[1] || '').replace(/^["']|["']$/g, '') : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractWorkspace(text) {
|
|
74
|
+
if (!text) return null;
|
|
75
|
+
const clean = value => {
|
|
76
|
+
const raw = String(value || '')
|
|
77
|
+
.trim()
|
|
78
|
+
.replace(/^["']|["']$/g, '');
|
|
79
|
+
const tmpMatch = raw.match(/\/tmp\/gh-issue-solver-[A-Za-z0-9._-]+/);
|
|
80
|
+
if (tmpMatch) return normalizePath(tmpMatch[0]);
|
|
81
|
+
return normalizePath(raw.split(/\\n|\s/)[0]);
|
|
82
|
+
};
|
|
83
|
+
const patterns = [/Your prepared working directory:\s*([^\r\n]+)/i, /Creating temporary directory:\s*([^\r\n]+)/i, /Cloning into ['"]([^'"]+)['"]/i, /\bworking directory:\s*([^\r\n]+)/i, /\bworkspace(?: directory)?:\s*([^\r\n]+)/i, /\((?:cd|pushd)\s+["']?([^"')\s]+)["']?\s+&&/i, /(\/tmp\/gh-issue-solver-[A-Za-z0-9._-]+)/];
|
|
84
|
+
|
|
85
|
+
for (const pattern of patterns) {
|
|
86
|
+
const match = text.match(pattern);
|
|
87
|
+
const value = match?.[1] ? clean(match[1]) : null;
|
|
88
|
+
if (value) return normalizePath(value);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function detectTool(command, text) {
|
|
94
|
+
const fromFlag = readFlagValue(command, '--tool');
|
|
95
|
+
if (fromFlag) return String(fromFlag).toLowerCase();
|
|
96
|
+
const haystack = `${command || ''}\n${text || ''}`.toLowerCase();
|
|
97
|
+
for (const kind of AGENT_KINDS) {
|
|
98
|
+
if (new RegExp(`\\b${kind}\\b`, 'i').test(haystack)) return kind;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function detectAgentKind(processRecord) {
|
|
104
|
+
const commandName = String(processRecord?.commandName || '').toLowerCase();
|
|
105
|
+
const exeBase = path.basename(String(processRecord?.exe || '')).toLowerCase();
|
|
106
|
+
const cmdline = String(processRecord?.cmdline || '').toLowerCase();
|
|
107
|
+
const combined = `${commandName} ${exeBase} ${cmdline}`;
|
|
108
|
+
|
|
109
|
+
for (const kind of AGENT_KINDS) {
|
|
110
|
+
if (commandName === kind || exeBase === kind) return kind;
|
|
111
|
+
if (new RegExp(`(?:^|[\\s/.-])${kind}(?:$|[\\s/.-])`).test(combined)) return kind;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Synchronous redaction for process command lines and log snippets. Process
|
|
118
|
+
* debugging runs inside cleanup, so it cannot rely on async full-log
|
|
119
|
+
* sanitizers. Keep this conservative and token-shape based.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} text
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
export function redactProcessText(text) {
|
|
125
|
+
let out = String(text ?? '');
|
|
126
|
+
out = out.replace(/\b(\d{6,12}):([A-Za-z0-9_-]{20,})\b/g, '$1:[REDACTED]');
|
|
127
|
+
out = out.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, 'github_pat_[REDACTED]');
|
|
128
|
+
out = out.replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, match => `${match.slice(0, 4)}[REDACTED]`);
|
|
129
|
+
out = out.replace(/\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, 'sk-ant-[REDACTED]');
|
|
130
|
+
out = out.replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, 'sk-[REDACTED]');
|
|
131
|
+
out = out.replace(/\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g, match => `${match.slice(0, 5)}[REDACTED]`);
|
|
132
|
+
out = out.replace(/\bnpm_[A-Za-z0-9]{20,}\b/g, 'npm_[REDACTED]');
|
|
133
|
+
out = out.replace(/\bhf_[A-Za-z0-9]{20,}\b/g, 'hf_[REDACTED]');
|
|
134
|
+
out = out.replace(/(Authorization:\s*Bearer\s+)([A-Za-z0-9._~+/=-]{10,})/gi, '$1[REDACTED]');
|
|
135
|
+
out = out.replace(/((?:api[_-]?key|token|secret|password|bot[_-]?token)\s*[:=]\s*['"]?)([A-Za-z0-9._~+/:=-]{12,})(['"]?)/gi, '$1[REDACTED]$3');
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract useful metadata from a start-command isolation log. The logs commonly
|
|
141
|
+
* contain the original command, the temporary worktree path, and the command
|
|
142
|
+
* that launches the selected agent.
|
|
143
|
+
*
|
|
144
|
+
* @param {{logPath?: string|null, text?: string|null, sessionId?: string|null}} input
|
|
145
|
+
* @returns {{sessionId: string|null, command: string|null, taskUrl: string|null, workspace: string|null, tool: string|null, logPath: string|null}}
|
|
146
|
+
*/
|
|
147
|
+
export function parseStartCommandLogMetadata(input = {}) {
|
|
148
|
+
const logPath = input.logPath || null;
|
|
149
|
+
const text = String(input.text || '');
|
|
150
|
+
const command = text.match(/^Command:\s*(.+)$/im)?.[1]?.trim() || null;
|
|
151
|
+
const sessionId = input.sessionId || (logPath ? path.basename(logPath).replace(/\.[^.]+$/, '') : null) || null;
|
|
152
|
+
|
|
153
|
+
const taskUrl = firstTaskUrl(command) || firstTaskUrl(text);
|
|
154
|
+
const workspace = extractWorkspace(text);
|
|
155
|
+
const tool = detectTool(command, text);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
sessionId,
|
|
159
|
+
command: command ? redactProcessText(command) : null,
|
|
160
|
+
taskUrl,
|
|
161
|
+
workspace,
|
|
162
|
+
tool,
|
|
163
|
+
logPath,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeSession(input) {
|
|
168
|
+
const command = input?.command ? String(input.command) : null;
|
|
169
|
+
const status = input?.status ? String(input.status).toLowerCase() : null;
|
|
170
|
+
const processIds = normalizeProcessIds(input?.processIds);
|
|
171
|
+
const sessionId = input?.sessionId || input?.uuid || input?.id || null;
|
|
172
|
+
const sessionName = input?.sessionName || input?.screenSessionName || sessionId || null;
|
|
173
|
+
const taskUrl = input?.taskUrl || firstTaskUrl(command);
|
|
174
|
+
const live = input?.live === true || RUNNING_STATUSES.has(status);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...input,
|
|
178
|
+
sessionId,
|
|
179
|
+
uuid: input?.uuid || sessionId,
|
|
180
|
+
sessionName,
|
|
181
|
+
screenSessionName: input?.screenSessionName || sessionName,
|
|
182
|
+
status,
|
|
183
|
+
command,
|
|
184
|
+
taskUrl,
|
|
185
|
+
workspace: normalizePath(input?.workspace || input?.workingDirectory || null),
|
|
186
|
+
tool: input?.tool ? String(input.tool).toLowerCase() : detectTool(command, command),
|
|
187
|
+
logPath: input?.logPath || null,
|
|
188
|
+
processIds,
|
|
189
|
+
live,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeProcess(input) {
|
|
194
|
+
return {
|
|
195
|
+
...input,
|
|
196
|
+
pid: toPositiveInteger(input?.pid),
|
|
197
|
+
ppid: toPositiveInteger(input?.ppid),
|
|
198
|
+
pgid: toPositiveInteger(input?.pgid) || null,
|
|
199
|
+
sid: toPositiveInteger(input?.sid) || null,
|
|
200
|
+
state: input?.state || null,
|
|
201
|
+
commandName: input?.commandName || null,
|
|
202
|
+
cmdline: normalizeWhitespace(input?.cmdline || input?.command || ''),
|
|
203
|
+
cwd: normalizePath(input?.cwd || null),
|
|
204
|
+
exe: input?.exe || null,
|
|
205
|
+
screenSessionName: input?.screenSessionName || null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function scoreSessionMatch(processRecord, session) {
|
|
210
|
+
const reasons = [];
|
|
211
|
+
const processIdValues = new Set(Object.values(session.processIds || {}).filter(Boolean));
|
|
212
|
+
|
|
213
|
+
if (processIdValues.has(processRecord.pid)) reasons.push('pid-session-process');
|
|
214
|
+
if (processIdValues.has(processRecord.ppid)) reasons.push('parent-session-process');
|
|
215
|
+
if (processIdValues.has(processRecord.pgid)) reasons.push('process-group-session-process');
|
|
216
|
+
if (processIdValues.has(processRecord.sid)) reasons.push('session-id-session-process');
|
|
217
|
+
|
|
218
|
+
if (processRecord.screenSessionName) {
|
|
219
|
+
const names = new Set([session.screenSessionName, session.sessionName, session.sessionId].filter(Boolean));
|
|
220
|
+
if (names.has(processRecord.screenSessionName)) reasons.push('screen-session');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (session.workspace && processRecord.cwd && isPathInside(processRecord.cwd, session.workspace)) {
|
|
224
|
+
reasons.push('cwd-workspace');
|
|
225
|
+
}
|
|
226
|
+
if (session.workspace && containsToken(processRecord.cmdline, session.workspace)) {
|
|
227
|
+
reasons.push('cmd-workspace');
|
|
228
|
+
}
|
|
229
|
+
if (session.taskUrl && containsToken(processRecord.cmdline, session.taskUrl)) {
|
|
230
|
+
reasons.push('cmd-task-url');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const agentKind = detectAgentKind(processRecord);
|
|
234
|
+
if (agentKind && session.tool && agentKind === session.tool) reasons.push('agent-tool');
|
|
235
|
+
if (reasons.length === 1 && reasons[0] === 'agent-tool') {
|
|
236
|
+
return { score: 0, reasons: [] };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const weights = {
|
|
240
|
+
'pid-session-process': 100,
|
|
241
|
+
'parent-session-process': 90,
|
|
242
|
+
'process-group-session-process': 80,
|
|
243
|
+
'session-id-session-process': 80,
|
|
244
|
+
'screen-session': 75,
|
|
245
|
+
'cwd-workspace': 60,
|
|
246
|
+
'cmd-workspace': 50,
|
|
247
|
+
'cmd-task-url': 45,
|
|
248
|
+
'agent-tool': 10,
|
|
249
|
+
};
|
|
250
|
+
const score = reasons.reduce((sum, reason) => sum + (weights[reason] || 1), 0);
|
|
251
|
+
return { score, reasons };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function chooseSession(processRecord, sessions) {
|
|
255
|
+
let best = null;
|
|
256
|
+
for (const session of sessions) {
|
|
257
|
+
const match = scoreSessionMatch(processRecord, session);
|
|
258
|
+
if (match.score <= 0) continue;
|
|
259
|
+
if (!best || match.score > best.score) {
|
|
260
|
+
best = { session, score: match.score, reasons: match.reasons };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return best;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isOrphanedAgent(processRecord, session, agentKind, currentPid) {
|
|
267
|
+
if (!agentKind || !session || processRecord.pid === currentPid) return false;
|
|
268
|
+
const status = String(session.status || '').toLowerCase();
|
|
269
|
+
const terminal = TERMINAL_STATUSES.has(status);
|
|
270
|
+
if (!terminal) return false;
|
|
271
|
+
if (session.live) return false;
|
|
272
|
+
return processRecord.ppid === 1 || processRecord.ppid == null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Correlate process records with start-command session/task records.
|
|
277
|
+
*
|
|
278
|
+
* @param {{processes: Array, sessions: Array, currentPid?: number|null}} input
|
|
279
|
+
* @returns {{items: Array, orphans: Array, sessions: Array}}
|
|
280
|
+
*/
|
|
281
|
+
export function correlateProcesses(input = {}) {
|
|
282
|
+
const sessions = (input.sessions || []).map(normalizeSession).filter(session => session.sessionId || session.taskUrl || session.workspace);
|
|
283
|
+
const currentPid = toPositiveInteger(input.currentPid) || null;
|
|
284
|
+
const targetPids = new Set((input.targetPids || []).map(toPositiveInteger).filter(Boolean));
|
|
285
|
+
const items = [];
|
|
286
|
+
|
|
287
|
+
for (const rawProcess of input.processes || []) {
|
|
288
|
+
const processRecord = normalizeProcess(rawProcess);
|
|
289
|
+
if (!processRecord.pid) continue;
|
|
290
|
+
|
|
291
|
+
const agentKind = detectAgentKind(processRecord);
|
|
292
|
+
const match = chooseSession(processRecord, sessions);
|
|
293
|
+
const targeted = targetPids.has(processRecord.pid);
|
|
294
|
+
const strongMatch = match?.reasons?.some(reason => !['cwd-workspace', 'cmd-workspace', 'agent-tool'].includes(reason));
|
|
295
|
+
if (!agentKind && !targeted && !strongMatch) continue;
|
|
296
|
+
|
|
297
|
+
const session = match?.session || null;
|
|
298
|
+
const orphaned = isOrphanedAgent(processRecord, session, agentKind, currentPid);
|
|
299
|
+
items.push({
|
|
300
|
+
...processRecord,
|
|
301
|
+
agentKind,
|
|
302
|
+
sessionId: session?.sessionId || null,
|
|
303
|
+
sessionName: session?.sessionName || null,
|
|
304
|
+
screenSessionName: processRecord.screenSessionName || session?.screenSessionName || null,
|
|
305
|
+
sessionStatus: session?.status || null,
|
|
306
|
+
sessionLive: session?.live === true,
|
|
307
|
+
taskUrl: session?.taskUrl || firstTaskUrl(processRecord.cmdline),
|
|
308
|
+
workspace: session?.workspace || processRecord.cwd || null,
|
|
309
|
+
logPath: session?.logPath || null,
|
|
310
|
+
matchReasons: match?.reasons || [],
|
|
311
|
+
orphaned,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
items.sort((a, b) => {
|
|
316
|
+
if (a.orphaned !== b.orphaned) return a.orphaned ? -1 : 1;
|
|
317
|
+
return a.pid - b.pid;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
items,
|
|
322
|
+
orphans: items.filter(item => item.orphaned),
|
|
323
|
+
sessions,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function yesNo(value) {
|
|
328
|
+
return value ? 'yes' : 'no';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Render a console-safe diagnostic report. All command lines are redacted.
|
|
333
|
+
*
|
|
334
|
+
* @param {{items?: Array, orphans?: Array}} report
|
|
335
|
+
* @returns {string}
|
|
336
|
+
*/
|
|
337
|
+
export function formatProcessDebugReport(report = {}) {
|
|
338
|
+
const items = report.items || [];
|
|
339
|
+
const orphans = report.orphans || [];
|
|
340
|
+
const lines = ['Process debug report', '====================', `Matched processes: ${items.length}`, `Orphaned terminal-session agents: ${orphans.length}`];
|
|
341
|
+
|
|
342
|
+
if (items.length === 0) {
|
|
343
|
+
lines.push('', 'No claude/codex/gemini/qwen/opencode processes were linked to hive-mind tasks.');
|
|
344
|
+
return lines.join('\n');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const item of items) {
|
|
348
|
+
lines.push('');
|
|
349
|
+
lines.push(`PID ${item.pid} ppid=${item.ppid ?? '?'} state=${item.state || '?'} agent=${item.agentKind || 'unknown'} orphan=${yesNo(item.orphaned)}`);
|
|
350
|
+
if (item.sessionId || item.sessionStatus) {
|
|
351
|
+
lines.push(` session: ${item.sessionId || '(unknown)'} status=${item.sessionStatus || '?'} live=${yesNo(item.sessionLive)}`);
|
|
352
|
+
}
|
|
353
|
+
if (item.taskUrl) lines.push(` task: ${item.taskUrl}`);
|
|
354
|
+
if (item.workspace) lines.push(` workspace: ${item.workspace}`);
|
|
355
|
+
if (item.logPath) lines.push(` log: ${item.logPath}`);
|
|
356
|
+
if (item.matchReasons?.length) lines.push(` match: ${item.matchReasons.join(', ')}`);
|
|
357
|
+
if (item.cmdline) lines.push(` cmd: ${redactProcessText(item.cmdline)}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return lines.join('\n');
|
|
361
|
+
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -405,6 +405,15 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
405
405
|
description: '[EXPERIMENTAL] Post tool output as PR comments in real-time. Supported for --tool claude and --tool codex.',
|
|
406
406
|
default: false,
|
|
407
407
|
},
|
|
408
|
+
// Issue #1843: render images that Claude/Codex read/write inline in the PR
|
|
409
|
+
// comments interactive mode posts. Images are committed to hidden custom Git
|
|
410
|
+
// refs and embedded via commit-SHA ?raw=true blob URLs (GitHub strips data: URIs).
|
|
411
|
+
// Disable with --no-interactive-image-upload to fall back to a metadata note.
|
|
412
|
+
'interactive-image-upload': {
|
|
413
|
+
type: 'boolean',
|
|
414
|
+
description: '[EXPERIMENTAL] When --interactive-mode is on, upload images read/written by the AI to hidden custom Git refs (refs/hive-mind-media/...) and embed them inline in PR comments. Enabled by default; use --no-interactive-image-upload to disable.',
|
|
415
|
+
default: true,
|
|
416
|
+
},
|
|
408
417
|
// Issue #817: Bidirectional interactive options
|
|
409
418
|
'accept-incomming-comments-as-input': {
|
|
410
419
|
type: 'boolean',
|