devharness 0.9.7 → 0.9.9
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/README.md +15 -0
- package/build/block-events.d.ts +3 -5
- package/build/block-events.d.ts.map +1 -1
- package/build/block-events.js +6 -24
- package/build/block-events.js.map +1 -1
- package/build/block-events.test.js +10 -7
- package/build/block-events.test.js.map +1 -1
- package/build/cdp-manager.d.ts +7 -0
- package/build/cdp-manager.d.ts.map +1 -1
- package/build/cdp-manager.js +9 -0
- package/build/cdp-manager.js.map +1 -1
- package/build/cli/index.d.ts +26 -0
- package/build/cli/index.d.ts.map +1 -0
- package/build/cli/index.js +290 -0
- package/build/cli/index.js.map +1 -0
- package/build/cli/process-tree.d.ts +21 -0
- package/build/cli/process-tree.d.ts.map +1 -0
- package/build/cli/process-tree.js +51 -0
- package/build/cli/process-tree.js.map +1 -0
- package/build/cli/session-match.d.ts +37 -0
- package/build/cli/session-match.d.ts.map +1 -0
- package/build/cli/session-match.js +55 -0
- package/build/cli/session-match.js.map +1 -0
- package/build/cli/session-match.test.d.ts +17 -0
- package/build/cli/session-match.test.d.ts.map +1 -0
- package/build/cli/session-match.test.js +125 -0
- package/build/cli/session-match.test.js.map +1 -0
- package/build/config.d.ts +1 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +1 -0
- package/build/config.js.map +1 -1
- package/build/connection-manager.d.ts.map +1 -1
- package/build/connection-manager.js +6 -1
- package/build/connection-manager.js.map +1 -1
- package/build/index.js +57 -4
- package/build/index.js.map +1 -1
- package/build/mcp-supervisor.js +43 -0
- package/build/mcp-supervisor.js.map +1 -1
- package/build/network-monitor.d.ts +13 -2
- package/build/network-monitor.d.ts.map +1 -1
- package/build/network-monitor.js +54 -23
- package/build/network-monitor.js.map +1 -1
- package/build/network-monitor.test.d.ts +11 -0
- package/build/network-monitor.test.d.ts.map +1 -0
- package/build/network-monitor.test.js +151 -0
- package/build/network-monitor.test.js.map +1 -0
- package/build/plugin-session-start-hook.test.d.ts +10 -0
- package/build/plugin-session-start-hook.test.d.ts.map +1 -0
- package/build/plugin-session-start-hook.test.js +187 -0
- package/build/plugin-session-start-hook.test.js.map +1 -0
- package/build/session-endpoint.d.ts +72 -0
- package/build/session-endpoint.d.ts.map +1 -0
- package/build/session-endpoint.js +220 -0
- package/build/session-endpoint.js.map +1 -0
- package/build/session-endpoint.test.d.ts +10 -0
- package/build/session-endpoint.test.d.ts.map +1 -0
- package/build/session-endpoint.test.js +202 -0
- package/build/session-endpoint.test.js.map +1 -0
- package/build/session-events.d.ts +28 -0
- package/build/session-events.d.ts.map +1 -0
- package/build/session-events.js +38 -0
- package/build/session-events.js.map +1 -0
- package/build/session-events.test.d.ts +8 -0
- package/build/session-events.test.d.ts.map +1 -0
- package/build/session-events.test.js +56 -0
- package/build/session-events.test.js.map +1 -0
- package/build/session-identity.d.ts +56 -0
- package/build/session-identity.d.ts.map +1 -0
- package/build/session-identity.js +96 -0
- package/build/session-identity.js.map +1 -0
- package/build/session-identity.test.d.ts +9 -0
- package/build/session-identity.test.d.ts.map +1 -0
- package/build/session-identity.test.js +126 -0
- package/build/session-identity.test.js.map +1 -0
- package/build/session-messages.d.ts +70 -0
- package/build/session-messages.d.ts.map +1 -0
- package/build/session-messages.js +139 -0
- package/build/session-messages.js.map +1 -0
- package/build/tool-response.d.ts +40 -1
- package/build/tool-response.d.ts.map +1 -1
- package/build/tool-response.js.map +1 -1
- package/build/tools/console-tools.d.ts +4 -0
- package/build/tools/console-tools.d.ts.map +1 -1
- package/build/tools/console-tools.js +55 -1
- package/build/tools/console-tools.js.map +1 -1
- package/build/tools/inspect-worker-meta.test.d.ts +9 -0
- package/build/tools/inspect-worker-meta.test.d.ts.map +1 -0
- package/build/tools/inspect-worker-meta.test.js +122 -0
- package/build/tools/inspect-worker-meta.test.js.map +1 -0
- package/build/tools/inspection-tools.d.ts +12 -5
- package/build/tools/inspection-tools.d.ts.map +1 -1
- package/build/tools/inspection-tools.js +86 -5
- package/build/tools/inspection-tools.js.map +1 -1
- package/build/tools/issues-tools.d.ts +3 -3
- package/build/tools/message-tools.d.ts +51 -0
- package/build/tools/message-tools.d.ts.map +1 -0
- package/build/tools/message-tools.js +250 -0
- package/build/tools/message-tools.js.map +1 -0
- package/build/tools/message-tools.test.d.ts +9 -0
- package/build/tools/message-tools.test.d.ts.map +1 -0
- package/build/tools/message-tools.test.js +332 -0
- package/build/tools/message-tools.test.js.map +1 -0
- package/build/tools/network-tools.js +1 -1
- package/build/tools/network-tools.js.map +1 -1
- package/build/tools/replay-tools.d.ts +3 -3
- package/build/tools/server-tools.d.ts +3 -3
- package/build/worker-targets.d.ts +75 -0
- package/build/worker-targets.d.ts.map +1 -0
- package/build/worker-targets.js +171 -0
- package/build/worker-targets.js.map +1 -0
- package/build/worker-targets.test.d.ts +8 -0
- package/build/worker-targets.test.d.ts.map +1 -0
- package/build/worker-targets.test.js +118 -0
- package/build/worker-targets.test.js.map +1 -0
- package/docs/instructions.md +23 -12
- package/docs/messages.md +150 -0
- package/package.json +3 -1
- package/plugin/skills/devharness/SKILL.md +36 -8
- package/plugin/skills/devharness/references/tool-categories.md +8 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `devharness <command>` - run a tool inside the session the shell belongs to.
|
|
3
|
+
*
|
|
4
|
+
* The MCP process's stdin belongs to its client, so a request arrives instead
|
|
5
|
+
* over that session's socket (src/session-endpoint.ts). Which session is
|
|
6
|
+
* decided by process ancestry, not by a flag, so `! devharness screenshot`
|
|
7
|
+
* inside a session reaches that session's browser.
|
|
8
|
+
*
|
|
9
|
+
* Guards that the MCP request handler applies - a dead dev server port, a
|
|
10
|
+
* paused breakpoint, a pending bug - are not applied here, the same as
|
|
11
|
+
* `devharness run`.
|
|
12
|
+
*/
|
|
13
|
+
import { connect } from 'net';
|
|
14
|
+
import { readFileSync } from 'fs';
|
|
15
|
+
import { dirname, join } from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import { initializePaths } from '../helpers/paths.js';
|
|
18
|
+
import { listSessionRecords } from '../session-endpoint.js';
|
|
19
|
+
import { readParentMap } from './process-tree.js';
|
|
20
|
+
import { matchByAncestry, findSessionByName, filterToListedProcesses } from './session-match.js';
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
22
|
+
/** Commands that are shorthand for one action of the `message` tool. */
|
|
23
|
+
const MESSAGE_VERBS = new Set(['sessions', 'send', 'read', 'reply']);
|
|
24
|
+
export const CLI_COMMANDS = ['call', 'which', ...MESSAGE_VERBS];
|
|
25
|
+
export function isCliCommand(word) {
|
|
26
|
+
return word !== undefined && CLI_COMMANDS.includes(word);
|
|
27
|
+
}
|
|
28
|
+
export function isVersionFlag(word) {
|
|
29
|
+
return word === '--version' || word === '-v';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The installed version, printed by `devharness --version`.
|
|
33
|
+
*
|
|
34
|
+
* The plugin's SessionStart hook compares this against the version its
|
|
35
|
+
* `.mcp.json` pins, so a global install that has drifted from the plugin is
|
|
36
|
+
* reported at the start of a session rather than discovered through behaviour
|
|
37
|
+
* that does not match the docs.
|
|
38
|
+
*/
|
|
39
|
+
export function readPackageVersion() {
|
|
40
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf-8')).version ?? 'unknown';
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Slack over the server's own wait, so the socket outlives the work it asked
|
|
49
|
+
* for. A socket that gives up first aborts a reply the server has already
|
|
50
|
+
* received and marked read. */
|
|
51
|
+
const WAIT_SOCKET_MARGIN_MS = 15000;
|
|
52
|
+
function parseArgs(argv) {
|
|
53
|
+
const positional = [];
|
|
54
|
+
let session;
|
|
55
|
+
let json = false;
|
|
56
|
+
let explicitTimeoutMs;
|
|
57
|
+
let waitMs;
|
|
58
|
+
// Everything after `--` is text, not flags: message bodies contain `--json`
|
|
59
|
+
// and the like, and stripping them silently rewrote what was sent.
|
|
60
|
+
let flagsEnded = false;
|
|
61
|
+
for (const arg of argv.slice(1)) {
|
|
62
|
+
if (!flagsEnded) {
|
|
63
|
+
if (arg === '--') {
|
|
64
|
+
flagsEnded = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg === '--json') {
|
|
68
|
+
json = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (arg.startsWith('--session=')) {
|
|
72
|
+
session = arg.slice('--session='.length);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg.startsWith('--timeout=')) {
|
|
76
|
+
explicitTimeoutMs = Number(arg.slice('--timeout='.length)) || undefined;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (arg.startsWith('--wait=')) {
|
|
80
|
+
waitMs = Number(arg.slice('--wait='.length)) || undefined;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
positional.push(arg);
|
|
85
|
+
}
|
|
86
|
+
const timeoutMs = explicitTimeoutMs
|
|
87
|
+
?? (waitMs ? waitMs + WAIT_SOCKET_MARGIN_MS : DEFAULT_TIMEOUT_MS);
|
|
88
|
+
return { command: argv[0], positional, session, json, timeoutMs, waitMs };
|
|
89
|
+
}
|
|
90
|
+
/** The tool call a command stands for. */
|
|
91
|
+
function buildCall(parsed) {
|
|
92
|
+
const [first, ...rest] = parsed.positional;
|
|
93
|
+
switch (parsed.command) {
|
|
94
|
+
case 'call': {
|
|
95
|
+
if (!first)
|
|
96
|
+
return 'Usage: devharness call <tool> [jsonArgs]';
|
|
97
|
+
let args = {};
|
|
98
|
+
if (rest.length > 0) {
|
|
99
|
+
try {
|
|
100
|
+
args = JSON.parse(rest.join(' '));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return `Arguments must be one JSON object: ${error?.message || error}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { tool: first, args };
|
|
107
|
+
}
|
|
108
|
+
case 'sessions':
|
|
109
|
+
return { tool: 'message', args: { action: 'sessions' } };
|
|
110
|
+
case 'read':
|
|
111
|
+
return { tool: 'message', args: { action: 'read' } };
|
|
112
|
+
case 'send': {
|
|
113
|
+
if (!first || rest.length === 0)
|
|
114
|
+
return 'Usage: devharness send <sessionId> <text> [--wait=<ms>]';
|
|
115
|
+
return {
|
|
116
|
+
tool: 'message',
|
|
117
|
+
args: {
|
|
118
|
+
action: 'send',
|
|
119
|
+
to: first,
|
|
120
|
+
text: rest.join(' '),
|
|
121
|
+
...(parsed.waitMs ? { waitForReplyMs: parsed.waitMs } : {}),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
case 'reply': {
|
|
126
|
+
if (!first || rest.length === 0)
|
|
127
|
+
return 'Usage: devharness reply <messageId> <text> [--wait=<ms>]';
|
|
128
|
+
return {
|
|
129
|
+
tool: 'message',
|
|
130
|
+
args: {
|
|
131
|
+
action: 'reply',
|
|
132
|
+
replyTo: first,
|
|
133
|
+
text: rest.join(' '),
|
|
134
|
+
...(parsed.waitMs ? { waitForReplyMs: parsed.waitMs } : {}),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
return `Unknown command "${parsed.command}". Commands: ${CLI_COMMANDS.join(', ')}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Send one request over a session's socket and return its reply. */
|
|
143
|
+
function callSession(record, tool, args, timeoutMs) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let buffer = '';
|
|
146
|
+
let settled = false;
|
|
147
|
+
const socket = connect(record.address);
|
|
148
|
+
socket.setEncoding('utf-8');
|
|
149
|
+
const timer = setTimeout(() => {
|
|
150
|
+
if (settled)
|
|
151
|
+
return;
|
|
152
|
+
settled = true;
|
|
153
|
+
socket.destroy();
|
|
154
|
+
reject(new Error(`Session ${record.shortId ?? record.pid} did not answer within ${timeoutMs}ms`));
|
|
155
|
+
}, timeoutMs);
|
|
156
|
+
const finish = (fn) => {
|
|
157
|
+
if (settled)
|
|
158
|
+
return;
|
|
159
|
+
settled = true;
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
socket.destroy();
|
|
162
|
+
fn();
|
|
163
|
+
};
|
|
164
|
+
socket.on('connect', () => {
|
|
165
|
+
socket.write(JSON.stringify({ tool, args }) + '\n');
|
|
166
|
+
});
|
|
167
|
+
socket.on('data', (chunk) => {
|
|
168
|
+
buffer += chunk;
|
|
169
|
+
const newline = buffer.indexOf('\n');
|
|
170
|
+
if (newline === -1)
|
|
171
|
+
return;
|
|
172
|
+
const line = buffer.slice(0, newline);
|
|
173
|
+
finish(() => {
|
|
174
|
+
try {
|
|
175
|
+
resolve(JSON.parse(line));
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
reject(new Error(`Session replied with something that is not JSON: ${error?.message || error}`));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
socket.on('error', (error) => {
|
|
183
|
+
finish(() => reject(new Error(error.code === 'ECONNREFUSED' || error.code === 'ENOENT'
|
|
184
|
+
? `Session ${record.shortId ?? record.pid} is not listening on ${record.address}. It is suspended or shutting down.`
|
|
185
|
+
: `Could not reach session ${record.shortId ?? record.pid}: ${error.message}`)));
|
|
186
|
+
});
|
|
187
|
+
socket.on('close', () => {
|
|
188
|
+
finish(() => reject(new Error(`Session ${record.shortId ?? record.pid} closed the connection without replying`)));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function describeRecord(record) {
|
|
193
|
+
return `${record.shortId ?? `pid-${record.pid}`} (pid ${record.pid}) - ${record.cwd}`;
|
|
194
|
+
}
|
|
195
|
+
/** Rendered text of a tool response, the way an MCP client would show it. */
|
|
196
|
+
function renderResponse(response) {
|
|
197
|
+
const items = Array.isArray(response?.content) ? response.content : [];
|
|
198
|
+
return items
|
|
199
|
+
.map((item) => (typeof item?.text === 'string' ? item.text : ''))
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
.join('\n');
|
|
202
|
+
}
|
|
203
|
+
export async function runCli(argv) {
|
|
204
|
+
initializePaths();
|
|
205
|
+
const parsed = parseArgs(argv);
|
|
206
|
+
const records = listSessionRecords();
|
|
207
|
+
if (records.length === 0) {
|
|
208
|
+
console.error('No devharness session is listening on this machine.');
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
let target;
|
|
212
|
+
if (parsed.session) {
|
|
213
|
+
target = findSessionByName(records, parsed.session);
|
|
214
|
+
if (!target) {
|
|
215
|
+
console.error(`No session matches "${parsed.session}". Listening sessions:`);
|
|
216
|
+
for (const record of records)
|
|
217
|
+
console.error(` ${describeRecord(record)}`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const parents = readParentMap();
|
|
223
|
+
const listed = filterToListedProcesses(records, parents);
|
|
224
|
+
if (listed.length === 0) {
|
|
225
|
+
console.error('Every session on record has already exited - the server is restarting. Try again in a moment.');
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
const result = matchByAncestry(listed, process.pid, parents);
|
|
229
|
+
if (result.ambiguous.length > 1) {
|
|
230
|
+
// Several servers can serve one session - a dev build beside the
|
|
231
|
+
// plugin's. The mailbox and its cursor are per session, not per server,
|
|
232
|
+
// so any of them answers a message verb identically. `call` reaches a
|
|
233
|
+
// particular server's browser and dev servers, so there the tie is real.
|
|
234
|
+
const ids = new Set(result.ambiguous.map(m => m.record.shortId ?? `pid-${m.record.pid}`));
|
|
235
|
+
if (ids.size === 1 && MESSAGE_VERBS.has(parsed.command)) {
|
|
236
|
+
target = result.ambiguous[0].record;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// A short id resolves to the first record, so with two servers under
|
|
240
|
+
// one id `--session=<shortId>` picks one silently. Only the pid names
|
|
241
|
+
// a single server, and the listing has to say which to use.
|
|
242
|
+
const distinct = new Set(result.ambiguous.map(m => m.record.shortId ?? `pid-${m.record.pid}`)).size > 1;
|
|
243
|
+
console.error(distinct
|
|
244
|
+
? 'Two sessions are the same distance up the process tree. Name one with --session=<id>:'
|
|
245
|
+
: 'One session is served by two devharness servers, and this command reaches a particular one. Name it by pid with --session=<pid>:');
|
|
246
|
+
for (const match of result.ambiguous)
|
|
247
|
+
console.error(` ${describeRecord(match.record)}`);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (!result.matched) {
|
|
252
|
+
console.error('This shell does not descend from any devharness session. Name one with --session=<id>:');
|
|
253
|
+
for (const record of records)
|
|
254
|
+
console.error(` ${describeRecord(record)}`);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
target = result.matched;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (parsed.command === 'which') {
|
|
262
|
+
console.log(describeRecord(target));
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
const call = buildCall(parsed);
|
|
266
|
+
if (typeof call === 'string') {
|
|
267
|
+
console.error(call);
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
let reply;
|
|
271
|
+
try {
|
|
272
|
+
reply = await callSession(target, call.tool, call.args, parsed.timeoutMs);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error(error?.message || String(error));
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
if (!reply.ok) {
|
|
279
|
+
console.error(reply.error || 'The session refused the call');
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
if (parsed.json) {
|
|
283
|
+
console.log(JSON.stringify(reply.response, null, 2));
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.log(renderResponse(reply.response));
|
|
287
|
+
}
|
|
288
|
+
return reply.response?.isError ? 1 : 0;
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent lookup over the process tree.
|
|
3
|
+
*
|
|
4
|
+
* A CLI invoked from a session's shell and that session's MCP process share an
|
|
5
|
+
* ancestor: both descend from the Claude Code process that owns the session.
|
|
6
|
+
* That shared ancestor is what identifies which session the command was typed
|
|
7
|
+
* in, so the whole pid -> ppid map is read once and walked in memory rather
|
|
8
|
+
* than spawning `ps` per hop.
|
|
9
|
+
*/
|
|
10
|
+
/** Every pid on the machine mapped to its parent. An empty map means the
|
|
11
|
+
* process listing failed, which leaves matching with nothing to go on. */
|
|
12
|
+
export declare function readParentMap(): Map<number, number>;
|
|
13
|
+
/**
|
|
14
|
+
* Ancestors of `pid`, nearest parent first, excluding `pid` itself and the
|
|
15
|
+
* init process. The walk stops on a repeat so a cycle in a malformed listing
|
|
16
|
+
* cannot spin.
|
|
17
|
+
*/
|
|
18
|
+
export declare function collectAncestors(pid: number, parents: Map<number, number>): number[];
|
|
19
|
+
/** `pid` and everything above it, for testing membership of a session's chain. */
|
|
20
|
+
export declare function collectChain(pid: number, parents: Map<number, number>): Set<number>;
|
|
21
|
+
//# sourceMappingURL=process-tree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-tree.d.ts","sourceRoot":"","sources":["../../src/cli/process-tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;2EAC2E;AAC3E,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBnD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAYpF;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAEnF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent lookup over the process tree.
|
|
3
|
+
*
|
|
4
|
+
* A CLI invoked from a session's shell and that session's MCP process share an
|
|
5
|
+
* ancestor: both descend from the Claude Code process that owns the session.
|
|
6
|
+
* That shared ancestor is what identifies which session the command was typed
|
|
7
|
+
* in, so the whole pid -> ppid map is read once and walked in memory rather
|
|
8
|
+
* than spawning `ps` per hop.
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
/** Every pid on the machine mapped to its parent. An empty map means the
|
|
12
|
+
* process listing failed, which leaves matching with nothing to go on. */
|
|
13
|
+
export function readParentMap() {
|
|
14
|
+
const parents = new Map();
|
|
15
|
+
try {
|
|
16
|
+
const output = process.platform === 'win32'
|
|
17
|
+
? execFileSync('powershell', ['-NoProfile', '-Command', 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'], { encoding: 'utf-8', timeout: 5000 })
|
|
18
|
+
: execFileSync('ps', ['-eo', 'pid=,ppid='], { encoding: 'utf-8', timeout: 5000 });
|
|
19
|
+
for (const line of output.split('\n')) {
|
|
20
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
21
|
+
if (!match)
|
|
22
|
+
continue;
|
|
23
|
+
parents.set(Number(match[1]), Number(match[2]));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return parents;
|
|
28
|
+
}
|
|
29
|
+
return parents;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Ancestors of `pid`, nearest parent first, excluding `pid` itself and the
|
|
33
|
+
* init process. The walk stops on a repeat so a cycle in a malformed listing
|
|
34
|
+
* cannot spin.
|
|
35
|
+
*/
|
|
36
|
+
export function collectAncestors(pid, parents) {
|
|
37
|
+
const ancestors = [];
|
|
38
|
+
const seen = new Set([pid]);
|
|
39
|
+
let current = parents.get(pid);
|
|
40
|
+
while (current !== undefined && current > 1 && !seen.has(current)) {
|
|
41
|
+
ancestors.push(current);
|
|
42
|
+
seen.add(current);
|
|
43
|
+
current = parents.get(current);
|
|
44
|
+
}
|
|
45
|
+
return ancestors;
|
|
46
|
+
}
|
|
47
|
+
/** `pid` and everything above it, for testing membership of a session's chain. */
|
|
48
|
+
export function collectChain(pid, parents) {
|
|
49
|
+
return new Set([pid, ...collectAncestors(pid, parents)]);
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=process-tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-tree.js","sourceRoot":"","sources":["../../src/cli/process-tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C;2EAC2E;AAC3E,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO;YACzC,CAAC,CAAC,YAAY,CACV,YAAY,EACZ,CAAC,YAAY,EAAE,UAAU,EAAE,4FAA4F,CAAC,EACxH,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CACrC;YACH,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAEpF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;YACnD,IAAI,CAAC,KAAK;gBAAE,SAAS;YACrB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,OAA4B;IACxE,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpC,IAAI,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,OAAO,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAClE,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAClB,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,OAA4B;IACpE,OAAO,IAAI,GAAG,CAAS,CAAC,GAAG,EAAE,GAAG,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which session a CLI command was typed in.
|
|
3
|
+
*
|
|
4
|
+
* The caller and the session's MCP process meet somewhere up the process tree.
|
|
5
|
+
* Every session under the same terminal or editor shares the outer ancestors,
|
|
6
|
+
* so the meeting point alone is not enough: the session whose shared ancestor
|
|
7
|
+
* is NEAREST the caller is the one whose shell ran the command. Distance is
|
|
8
|
+
* measured in hops from the caller, and the nearest wins.
|
|
9
|
+
*/
|
|
10
|
+
import type { SessionRecord } from '../session-endpoint.js';
|
|
11
|
+
export interface SessionMatch {
|
|
12
|
+
record: SessionRecord;
|
|
13
|
+
/** Hops from the caller to the ancestor it shares with this session. */
|
|
14
|
+
distance: number;
|
|
15
|
+
}
|
|
16
|
+
export interface MatchResult {
|
|
17
|
+
/** Every session sharing an ancestor, nearest first. */
|
|
18
|
+
candidates: SessionMatch[];
|
|
19
|
+
/** The single nearest session, or null when none matched or two tie. */
|
|
20
|
+
matched: SessionRecord | null;
|
|
21
|
+
/** Set when two sessions are the same distance away. */
|
|
22
|
+
ambiguous: SessionMatch[];
|
|
23
|
+
}
|
|
24
|
+
export declare function matchByAncestry(records: SessionRecord[], callerPid: number, parents: Map<number, number>): MatchResult;
|
|
25
|
+
/**
|
|
26
|
+
* Records whose process is in the listing.
|
|
27
|
+
*
|
|
28
|
+
* A record survives its process by the moment it takes the supervisor to
|
|
29
|
+
* replace a child, and one absent from the process listing has no chain to
|
|
30
|
+
* walk - it would report as "no shared ancestor", which reads as the caller
|
|
31
|
+
* being in the wrong shell rather than the session being mid-restart. An
|
|
32
|
+
* empty listing means the walk failed entirely, so nothing is filtered.
|
|
33
|
+
*/
|
|
34
|
+
export declare function filterToListedProcesses(records: SessionRecord[], parents: Map<number, number>): SessionRecord[];
|
|
35
|
+
/** The session a caller named explicitly: short id, session id, or pid. */
|
|
36
|
+
export declare function findSessionByName(records: SessionRecord[], name: string): SessionRecord | null;
|
|
37
|
+
//# sourceMappingURL=session-match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-match.d.ts","sourceRoot":"","sources":["../../src/cli/session-match.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAG5D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,wDAAwD;IACxD,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,wEAAwE;IACxE,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,wDAAwD;IACxD,SAAS,EAAE,YAAY,EAAE,CAAC;CAC3B;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,aAAa,EAAE,EACxB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,WAAW,CAwBb;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,aAAa,EAAE,EACxB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,aAAa,EAAE,CAGjB;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAS9F"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which session a CLI command was typed in.
|
|
3
|
+
*
|
|
4
|
+
* The caller and the session's MCP process meet somewhere up the process tree.
|
|
5
|
+
* Every session under the same terminal or editor shares the outer ancestors,
|
|
6
|
+
* so the meeting point alone is not enough: the session whose shared ancestor
|
|
7
|
+
* is NEAREST the caller is the one whose shell ran the command. Distance is
|
|
8
|
+
* measured in hops from the caller, and the nearest wins.
|
|
9
|
+
*/
|
|
10
|
+
import { collectAncestors, collectChain } from './process-tree.js';
|
|
11
|
+
export function matchByAncestry(records, callerPid, parents) {
|
|
12
|
+
const callerAncestors = collectAncestors(callerPid, parents);
|
|
13
|
+
const candidates = [];
|
|
14
|
+
for (const record of records) {
|
|
15
|
+
const chain = collectChain(record.pid, parents);
|
|
16
|
+
const distance = callerAncestors.findIndex(pid => chain.has(pid));
|
|
17
|
+
if (distance === -1)
|
|
18
|
+
continue;
|
|
19
|
+
candidates.push({ record, distance });
|
|
20
|
+
}
|
|
21
|
+
candidates.sort((a, b) => a.distance - b.distance);
|
|
22
|
+
if (candidates.length === 0) {
|
|
23
|
+
return { candidates, matched: null, ambiguous: [] };
|
|
24
|
+
}
|
|
25
|
+
const nearest = candidates[0].distance;
|
|
26
|
+
const tied = candidates.filter(c => c.distance === nearest);
|
|
27
|
+
if (tied.length > 1) {
|
|
28
|
+
return { candidates, matched: null, ambiguous: tied };
|
|
29
|
+
}
|
|
30
|
+
return { candidates, matched: candidates[0].record, ambiguous: [] };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Records whose process is in the listing.
|
|
34
|
+
*
|
|
35
|
+
* A record survives its process by the moment it takes the supervisor to
|
|
36
|
+
* replace a child, and one absent from the process listing has no chain to
|
|
37
|
+
* walk - it would report as "no shared ancestor", which reads as the caller
|
|
38
|
+
* being in the wrong shell rather than the session being mid-restart. An
|
|
39
|
+
* empty listing means the walk failed entirely, so nothing is filtered.
|
|
40
|
+
*/
|
|
41
|
+
export function filterToListedProcesses(records, parents) {
|
|
42
|
+
if (parents.size === 0)
|
|
43
|
+
return records;
|
|
44
|
+
return records.filter(record => parents.has(record.pid));
|
|
45
|
+
}
|
|
46
|
+
/** The session a caller named explicitly: short id, session id, or pid. */
|
|
47
|
+
export function findSessionByName(records, name) {
|
|
48
|
+
const asPid = Number(name);
|
|
49
|
+
return (records.find(r => r.shortId === name) ??
|
|
50
|
+
records.find(r => r.sessionId === name) ??
|
|
51
|
+
(Number.isInteger(asPid) ? records.find(r => r.pid === asPid) : undefined) ??
|
|
52
|
+
records.find(r => r.sessionId?.startsWith(name)) ??
|
|
53
|
+
null);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=session-match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-match.js","sourceRoot":"","sources":["../../src/cli/session-match.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAiBnE,MAAM,UAAU,eAAe,CAC7B,OAAwB,EACxB,SAAiB,EACjB,OAA4B;IAE5B,MAAM,eAAe,GAAG,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAE7D,MAAM,UAAU,GAAmB,EAAE,CAAC;IACtC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAClE,IAAI,QAAQ,KAAK,CAAC,CAAC;YAAE,SAAS;QAC9B,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IACxC,CAAC;IAED,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IAEnD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACtD,CAAC;IAED,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACvC,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAC5D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACxD,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;AACtE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAwB,EACxB,OAA4B;IAE5B,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACvC,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,OAAwB,EAAE,IAAY;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CACL,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC;QACvC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;QAChD,IAAI,CACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for identifying the session a CLI command was typed in.
|
|
3
|
+
*
|
|
4
|
+
* The tree below is the shape this has to get right: one editor, two Claude
|
|
5
|
+
* sessions under it, each with a supervisor and an MCP child, and a shell
|
|
6
|
+
* under one of them. Every session shares the editor, so a rule that stops at
|
|
7
|
+
* "shares an ancestor" picks both.
|
|
8
|
+
*
|
|
9
|
+
* 900 editor
|
|
10
|
+
* +-- 800 claude A +-- 700 claude B
|
|
11
|
+
* +-- 810 supervisor A +-- 710 supervisor B
|
|
12
|
+
* | +-- 811 mcp A | +-- 711 mcp B
|
|
13
|
+
* +-- 820 shell A +-- 720 shell B
|
|
14
|
+
* +-- 821 devharness +-- 721 devharness
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=session-match.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-match.test.d.ts","sourceRoot":"","sources":["../../src/cli/session-match.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for identifying the session a CLI command was typed in.
|
|
3
|
+
*
|
|
4
|
+
* The tree below is the shape this has to get right: one editor, two Claude
|
|
5
|
+
* sessions under it, each with a supervisor and an MCP child, and a shell
|
|
6
|
+
* under one of them. Every session shares the editor, so a rule that stops at
|
|
7
|
+
* "shares an ancestor" picks both.
|
|
8
|
+
*
|
|
9
|
+
* 900 editor
|
|
10
|
+
* +-- 800 claude A +-- 700 claude B
|
|
11
|
+
* +-- 810 supervisor A +-- 710 supervisor B
|
|
12
|
+
* | +-- 811 mcp A | +-- 711 mcp B
|
|
13
|
+
* +-- 820 shell A +-- 720 shell B
|
|
14
|
+
* +-- 821 devharness +-- 721 devharness
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { collectAncestors, collectChain } from './process-tree.js';
|
|
18
|
+
import { matchByAncestry, findSessionByName, filterToListedProcesses } from './session-match.js';
|
|
19
|
+
const PARENTS = new Map([
|
|
20
|
+
[900, 1],
|
|
21
|
+
[800, 900], [810, 800], [811, 810], [820, 800], [821, 820],
|
|
22
|
+
[700, 900], [710, 700], [711, 710], [720, 700], [721, 720],
|
|
23
|
+
]);
|
|
24
|
+
function record(pid, shortId) {
|
|
25
|
+
return {
|
|
26
|
+
pid,
|
|
27
|
+
ppid: PARENTS.get(pid) ?? 1,
|
|
28
|
+
cwd: `/repo/${shortId}`,
|
|
29
|
+
shortId,
|
|
30
|
+
sessionId: `${shortId}-0000-0000`,
|
|
31
|
+
address: `/tmp/${pid}.sock`,
|
|
32
|
+
startedAt: 1000 + pid,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const SESSION_A = record(811, 'aaaaaaaa');
|
|
36
|
+
const SESSION_B = record(711, 'bbbbbbbb');
|
|
37
|
+
describe('collectAncestors', () => {
|
|
38
|
+
it('walks to the top, nearest first, excluding the process itself and init', () => {
|
|
39
|
+
expect(collectAncestors(821, PARENTS)).toEqual([820, 800, 900]);
|
|
40
|
+
});
|
|
41
|
+
it('returns nothing for a pid with no recorded parent', () => {
|
|
42
|
+
expect(collectAncestors(4242, PARENTS)).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
it('stops on a cycle instead of spinning', () => {
|
|
45
|
+
const cyclic = new Map([[10, 20], [20, 10]]);
|
|
46
|
+
expect(collectAncestors(10, cyclic)).toEqual([20]);
|
|
47
|
+
});
|
|
48
|
+
it('includes the process itself in a chain', () => {
|
|
49
|
+
expect(collectChain(811, PARENTS)).toEqual(new Set([811, 810, 800, 900]));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('matchByAncestry', () => {
|
|
53
|
+
it('picks the session whose shared ancestor is nearest the caller', () => {
|
|
54
|
+
const result = matchByAncestry([SESSION_A, SESSION_B], 821, PARENTS);
|
|
55
|
+
expect(result.matched?.shortId).toBe('aaaaaaaa');
|
|
56
|
+
expect(result.ambiguous).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
it('picks the other session from the other shell', () => {
|
|
59
|
+
const result = matchByAncestry([SESSION_A, SESSION_B], 721, PARENTS);
|
|
60
|
+
expect(result.matched?.shortId).toBe('bbbbbbbb');
|
|
61
|
+
});
|
|
62
|
+
it('still lists the far session as a candidate, just further away', () => {
|
|
63
|
+
const result = matchByAncestry([SESSION_A, SESSION_B], 821, PARENTS);
|
|
64
|
+
expect(result.candidates.map(c => [c.record.shortId, c.distance])).toEqual([
|
|
65
|
+
['aaaaaaaa', 1], // claude A, one hop up from the shell
|
|
66
|
+
['bbbbbbbb', 2], // the editor, two hops up
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
it('reports ambiguity rather than guessing when two are equally near', () => {
|
|
70
|
+
const twin = { ...record(812, 'cccccccc'), ppid: 810 };
|
|
71
|
+
const parents = new Map(PARENTS).set(812, 810);
|
|
72
|
+
const result = matchByAncestry([SESSION_A, twin], 821, parents);
|
|
73
|
+
expect(result.matched).toBeNull();
|
|
74
|
+
expect(result.ambiguous.map(m => m.record.shortId).sort()).toEqual(['aaaaaaaa', 'cccccccc']);
|
|
75
|
+
});
|
|
76
|
+
it('matches nothing for a caller outside every session tree', () => {
|
|
77
|
+
const parents = new Map(PARENTS).set(50, 1);
|
|
78
|
+
const result = matchByAncestry([SESSION_A, SESSION_B], 50, parents);
|
|
79
|
+
expect(result.matched).toBeNull();
|
|
80
|
+
expect(result.candidates).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
it('matches nothing when the process listing is empty', () => {
|
|
83
|
+
const result = matchByAncestry([SESSION_A, SESSION_B], 821, new Map());
|
|
84
|
+
expect(result.matched).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('filterToListedProcesses', () => {
|
|
88
|
+
it('drops a record whose process is no longer in the listing', () => {
|
|
89
|
+
const gone = record(811, 'aaaaaaaa');
|
|
90
|
+
const parents = new Map(PARENTS);
|
|
91
|
+
parents.delete(811);
|
|
92
|
+
expect(filterToListedProcesses([gone, SESSION_B], parents)).toEqual([SESSION_B]);
|
|
93
|
+
});
|
|
94
|
+
it('keeps every record when the process listing failed', () => {
|
|
95
|
+
expect(filterToListedProcesses([SESSION_A, SESSION_B], new Map())).toEqual([SESSION_A, SESSION_B]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('findSessionByName', () => {
|
|
99
|
+
const records = [SESSION_A, SESSION_B];
|
|
100
|
+
it('finds by short id', () => {
|
|
101
|
+
expect(findSessionByName(records, 'bbbbbbbb')?.pid).toBe(711);
|
|
102
|
+
});
|
|
103
|
+
it('finds by full session id and by prefix', () => {
|
|
104
|
+
expect(findSessionByName(records, 'aaaaaaaa-0000-0000')?.pid).toBe(811);
|
|
105
|
+
expect(findSessionByName(records, 'aaaaaaaa-0000')?.pid).toBe(811);
|
|
106
|
+
});
|
|
107
|
+
it('finds by pid', () => {
|
|
108
|
+
expect(findSessionByName(records, '711')?.shortId).toBe('bbbbbbbb');
|
|
109
|
+
});
|
|
110
|
+
it('returns null for a name that matches nothing', () => {
|
|
111
|
+
expect(findSessionByName(records, 'zzzz')).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('ordering when two servers start together', () => {
|
|
115
|
+
it('is decided by pid, not by whatever order the directory listed', () => {
|
|
116
|
+
// A rebuild restarts every child at once, so equal startedAt is the norm.
|
|
117
|
+
const a = { ...record(811, 'aaaaaaaa'), startedAt: 500 };
|
|
118
|
+
const b = { ...record(711, 'bbbbbbbb'), startedAt: 500 };
|
|
119
|
+
const sorted = [a, b].sort((x, y) => y.startedAt - x.startedAt || x.pid - y.pid);
|
|
120
|
+
const reversed = [b, a].sort((x, y) => y.startedAt - x.startedAt || x.pid - y.pid);
|
|
121
|
+
expect(sorted.map(r => r.pid)).toEqual([711, 811]);
|
|
122
|
+
expect(reversed.map(r => r.pid)).toEqual([711, 811]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
//# sourceMappingURL=session-match.test.js.map
|