pi-rlm 0.1.0 → 0.1.3
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 +56 -1
- package/bin/pi-rlm.mjs +794 -0
- package/index.ts +2 -1
- package/package.json +8 -1
- package/src/backends.ts +473 -19
- package/src/cli.ts +1027 -0
- package/src/engine.ts +87 -17
- package/src/runs.ts +5 -1
- package/src/schema.ts +6 -1
- package/src/types.ts +1 -0
package/index.ts
CHANGED
|
@@ -205,7 +205,8 @@ function resolveStartInput(params: RlmToolParams, cwd: string): StartRunInput {
|
|
|
205
205
|
maxNodes: params.maxNodes ?? 24,
|
|
206
206
|
maxBranching: params.maxBranching ?? 3,
|
|
207
207
|
concurrency: params.concurrency ?? 2,
|
|
208
|
-
timeoutMs: params.timeoutMs ?? defaultNodeTimeoutMs
|
|
208
|
+
timeoutMs: params.timeoutMs ?? defaultNodeTimeoutMs,
|
|
209
|
+
tmuxUseCurrentSession: params.tmuxUseCurrentSession ?? false
|
|
209
210
|
};
|
|
210
211
|
}
|
|
211
212
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-rlm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Recursive Language Model (RLM) extension for Pi coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"pi-rlm": "./bin/pi-rlm.mjs"
|
|
10
|
+
},
|
|
8
11
|
"keywords": [
|
|
9
12
|
"pi-package",
|
|
10
13
|
"pi-extension",
|
|
@@ -22,6 +25,7 @@
|
|
|
22
25
|
"files": [
|
|
23
26
|
"index.ts",
|
|
24
27
|
"src",
|
|
28
|
+
"bin",
|
|
25
29
|
"README.md"
|
|
26
30
|
],
|
|
27
31
|
"publishConfig": {
|
|
@@ -30,6 +34,9 @@
|
|
|
30
34
|
},
|
|
31
35
|
"scripts": {
|
|
32
36
|
"typecheck": "tsc --noEmit",
|
|
37
|
+
"build:cli": "tsc -p tsconfig.cli.json && node ./scripts/build-cli.mjs",
|
|
38
|
+
"prepack": "npm run build:cli",
|
|
39
|
+
"cli": "npm run build:cli --silent && node ./bin/pi-rlm.mjs",
|
|
33
40
|
"pack:check": "npm pack --dry-run",
|
|
34
41
|
"check": "npm run typecheck && npm run pack:check"
|
|
35
42
|
},
|
package/src/backends.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface CompletionRequest {
|
|
|
21
21
|
toolsProfile: RlmToolsProfile;
|
|
22
22
|
timeoutMs: number;
|
|
23
23
|
signal?: AbortSignal;
|
|
24
|
+
runId?: string;
|
|
25
|
+
nodeId?: string;
|
|
26
|
+
depth?: number;
|
|
27
|
+
stage?: string;
|
|
28
|
+
tmuxUseCurrentSession?: boolean;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
interface ProcessResult {
|
|
@@ -38,6 +43,8 @@ const defaultCliFlags = [
|
|
|
38
43
|
"--no-themes"
|
|
39
44
|
];
|
|
40
45
|
|
|
46
|
+
const tmuxWindowLocks = new Map<string, Promise<void>>();
|
|
47
|
+
|
|
41
48
|
export async function completeWithBackend(
|
|
42
49
|
request: CompletionRequest,
|
|
43
50
|
ctx: ExtensionContext
|
|
@@ -166,16 +173,124 @@ async function completeWithTmux(request: CompletionRequest): Promise<string> {
|
|
|
166
173
|
const stamp = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
167
174
|
const promptPath = join(tmpdir(), `pi-rlm-${stamp}.prompt.txt`);
|
|
168
175
|
const outputPath = join(tmpdir(), `pi-rlm-${stamp}.output.log`);
|
|
169
|
-
const sessionName = `pi-rlm-${stamp}`;
|
|
170
176
|
const tools = profileToTools(request.toolsProfile);
|
|
171
177
|
|
|
172
178
|
await fs.writeFile(promptPath, request.prompt, "utf8");
|
|
173
179
|
|
|
174
180
|
const modelPart = request.model ? ` --model ${shellQuote(request.model)}` : "";
|
|
175
|
-
const
|
|
181
|
+
const shellScript = [
|
|
176
182
|
`PROMPT_CONTENT=$(cat ${shellQuote(promptPath)})`,
|
|
177
|
-
|
|
183
|
+
"set -o pipefail",
|
|
184
|
+
`PI_OFFLINE=1 pi ${defaultCliFlags.join(" ")} --tools ${shellQuote(tools)}${modelPart} \"$PROMPT_CONTENT\" 2>&1 | tee ${shellQuote(outputPath)}`
|
|
178
185
|
].join("; ");
|
|
186
|
+
const command = `bash -lc ${shellQuote(shellScript)}`;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
if (request.runId && typeof request.depth === "number") {
|
|
190
|
+
const paneId = await startStructuredTmuxCall(request, command);
|
|
191
|
+
await waitForTmuxPane(paneId, request.timeoutMs, request.signal);
|
|
192
|
+
|
|
193
|
+
if (shouldCleanupCurrentSessionWindows(request)) {
|
|
194
|
+
const currentSessionName = await getCurrentTmuxSessionName();
|
|
195
|
+
if (currentSessionName) {
|
|
196
|
+
await cleanupCurrentSessionDepthWindows(currentSessionName);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
await runEphemeralTmuxCall(request, command, stamp);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const output = await fs.readFile(outputPath, "utf8").catch(() => "");
|
|
204
|
+
return output.trim() || "(no response)";
|
|
205
|
+
} finally {
|
|
206
|
+
await safeUnlink(promptPath);
|
|
207
|
+
await safeUnlink(outputPath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function startStructuredTmuxCall(
|
|
212
|
+
request: CompletionRequest,
|
|
213
|
+
command: string
|
|
214
|
+
): Promise<string> {
|
|
215
|
+
const currentSessionName = request.tmuxUseCurrentSession
|
|
216
|
+
? await getCurrentTmuxSessionName()
|
|
217
|
+
: undefined;
|
|
218
|
+
const useCurrentSession = Boolean(currentSessionName);
|
|
219
|
+
|
|
220
|
+
const sessionName = currentSessionName ?? toTmuxRunSessionName(request.runId!);
|
|
221
|
+
const windowName = toTmuxDepthWindowName(request.depth!, useCurrentSession);
|
|
222
|
+
|
|
223
|
+
if (useCurrentSession) {
|
|
224
|
+
const { paneId, windowTarget } = await startPaneInDepthWindow(
|
|
225
|
+
sessionName,
|
|
226
|
+
windowName,
|
|
227
|
+
request.cwd,
|
|
228
|
+
command,
|
|
229
|
+
request.signal
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
await setTmuxPaneTitle(paneId, toTmuxPaneTitle(request));
|
|
233
|
+
await setTmuxWindowTiled(windowTarget);
|
|
234
|
+
return paneId;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const createResult = await runProcess(
|
|
238
|
+
"tmux",
|
|
239
|
+
[
|
|
240
|
+
"new-session",
|
|
241
|
+
"-d",
|
|
242
|
+
"-s",
|
|
243
|
+
sessionName,
|
|
244
|
+
"-n",
|
|
245
|
+
windowName,
|
|
246
|
+
"-c",
|
|
247
|
+
request.cwd,
|
|
248
|
+
"-P",
|
|
249
|
+
"-F",
|
|
250
|
+
"#{pane_id}",
|
|
251
|
+
command
|
|
252
|
+
],
|
|
253
|
+
{
|
|
254
|
+
cwd: request.cwd,
|
|
255
|
+
timeoutMs: 10000,
|
|
256
|
+
signal: request.signal
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (createResult.code === 0) {
|
|
261
|
+
await configureStructuredTmuxSession(sessionName);
|
|
262
|
+
const paneId = createResult.stdout.trim();
|
|
263
|
+
const windowTarget = (await getTmuxWindowIdForPane(paneId)) ?? `${sessionName}:${windowName}`;
|
|
264
|
+
await setTmuxPaneTitle(paneId, toTmuxPaneTitle(request));
|
|
265
|
+
await setTmuxWindowTiled(windowTarget);
|
|
266
|
+
return paneId;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const createOutput = `${createResult.stderr}\n${createResult.stdout}`;
|
|
270
|
+
if (!isTmuxDuplicateSessionError(createOutput)) {
|
|
271
|
+
throw new Error(`tmux backend failed to start run session: ${createResult.stderr || createResult.stdout}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await configureStructuredTmuxSession(sessionName);
|
|
275
|
+
const { paneId, windowTarget } = await startPaneInDepthWindow(
|
|
276
|
+
sessionName,
|
|
277
|
+
windowName,
|
|
278
|
+
request.cwd,
|
|
279
|
+
command,
|
|
280
|
+
request.signal
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
await setTmuxPaneTitle(paneId, toTmuxPaneTitle(request));
|
|
284
|
+
await setTmuxWindowTiled(windowTarget);
|
|
285
|
+
return paneId;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function runEphemeralTmuxCall(
|
|
289
|
+
request: CompletionRequest,
|
|
290
|
+
command: string,
|
|
291
|
+
stamp: string
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
const sessionName = `pi-rlm-${stamp}`;
|
|
179
294
|
|
|
180
295
|
const startResult = await runProcess(
|
|
181
296
|
"tmux",
|
|
@@ -188,35 +303,374 @@ async function completeWithTmux(request: CompletionRequest): Promise<string> {
|
|
|
188
303
|
);
|
|
189
304
|
|
|
190
305
|
if (startResult.code !== 0) {
|
|
191
|
-
await safeUnlink(promptPath);
|
|
192
306
|
throw new Error(`tmux backend failed to start: ${startResult.stderr || startResult.stdout}`);
|
|
193
307
|
}
|
|
194
308
|
|
|
195
309
|
const deadline = Date.now() + request.timeoutMs;
|
|
196
310
|
|
|
311
|
+
while (Date.now() < deadline) {
|
|
312
|
+
if (request.signal?.aborted) {
|
|
313
|
+
await killTmuxSession(sessionName);
|
|
314
|
+
throw new Error("RLM request aborted");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const alive = await hasTmuxSession(sessionName);
|
|
318
|
+
if (!alive) return;
|
|
319
|
+
await sleep(250);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (await hasTmuxSession(sessionName)) {
|
|
323
|
+
await killTmuxSession(sessionName);
|
|
324
|
+
throw new Error(`tmux backend timed out after ${request.timeoutMs}ms`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function configureStructuredTmuxSession(sessionName: string): Promise<void> {
|
|
329
|
+
await runProcess(
|
|
330
|
+
"tmux",
|
|
331
|
+
["set-window-option", "-g", "-t", sessionName, "remain-on-exit", "on"],
|
|
332
|
+
{
|
|
333
|
+
timeoutMs: 5000
|
|
334
|
+
}
|
|
335
|
+
).catch(() => undefined);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function withTmuxWindowLock<T>(
|
|
339
|
+
sessionName: string,
|
|
340
|
+
windowName: string,
|
|
341
|
+
worker: () => Promise<T>
|
|
342
|
+
): Promise<T> {
|
|
343
|
+
const key = `${sessionName}:${windowName}`;
|
|
344
|
+
const previous = tmuxWindowLocks.get(key) ?? Promise.resolve();
|
|
345
|
+
|
|
346
|
+
let release: (() => void) | undefined;
|
|
347
|
+
const current = new Promise<void>((resolve) => {
|
|
348
|
+
release = resolve;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const queued = previous
|
|
352
|
+
.then(() => current)
|
|
353
|
+
.catch(() => current);
|
|
354
|
+
tmuxWindowLocks.set(key, queued);
|
|
355
|
+
|
|
356
|
+
await previous;
|
|
357
|
+
|
|
197
358
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
359
|
+
return await worker();
|
|
360
|
+
} finally {
|
|
361
|
+
release?.();
|
|
362
|
+
if (tmuxWindowLocks.get(key) === queued) {
|
|
363
|
+
tmuxWindowLocks.delete(key);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function startPaneInDepthWindow(
|
|
369
|
+
sessionName: string,
|
|
370
|
+
windowName: string,
|
|
371
|
+
cwd: string,
|
|
372
|
+
command: string,
|
|
373
|
+
signal?: AbortSignal
|
|
374
|
+
): Promise<{ paneId: string; windowTarget: string }> {
|
|
375
|
+
return withTmuxWindowLock(sessionName, windowName, async () => {
|
|
376
|
+
const existingWindowId = await getTmuxWindowIdByName(sessionName, windowName);
|
|
377
|
+
|
|
378
|
+
if (existingWindowId) {
|
|
379
|
+
const splitResult = await runProcess(
|
|
380
|
+
"tmux",
|
|
381
|
+
[
|
|
382
|
+
"split-window",
|
|
383
|
+
"-d",
|
|
384
|
+
"-t",
|
|
385
|
+
existingWindowId,
|
|
386
|
+
"-c",
|
|
387
|
+
cwd,
|
|
388
|
+
"-P",
|
|
389
|
+
"-F",
|
|
390
|
+
"#{pane_id}",
|
|
391
|
+
command
|
|
392
|
+
],
|
|
393
|
+
{
|
|
394
|
+
cwd,
|
|
395
|
+
timeoutMs: 10000,
|
|
396
|
+
signal
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (splitResult.code !== 0) {
|
|
401
|
+
throw new Error(`tmux backend failed to create pane: ${splitResult.stderr || splitResult.stdout}`);
|
|
202
402
|
}
|
|
203
403
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
404
|
+
return {
|
|
405
|
+
paneId: splitResult.stdout.trim(),
|
|
406
|
+
windowTarget: existingWindowId
|
|
407
|
+
};
|
|
207
408
|
}
|
|
208
409
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
410
|
+
const createResult = await runProcess(
|
|
411
|
+
"tmux",
|
|
412
|
+
[
|
|
413
|
+
"new-window",
|
|
414
|
+
"-d",
|
|
415
|
+
"-t",
|
|
416
|
+
sessionName,
|
|
417
|
+
"-n",
|
|
418
|
+
windowName,
|
|
419
|
+
"-c",
|
|
420
|
+
cwd,
|
|
421
|
+
"-P",
|
|
422
|
+
"-F",
|
|
423
|
+
"#{pane_id}",
|
|
424
|
+
command
|
|
425
|
+
],
|
|
426
|
+
{
|
|
427
|
+
cwd,
|
|
428
|
+
timeoutMs: 10000,
|
|
429
|
+
signal
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (createResult.code === 0) {
|
|
434
|
+
const paneId = createResult.stdout.trim();
|
|
435
|
+
const windowTarget = (await getTmuxWindowIdForPane(paneId)) ?? `${sessionName}:${windowName}`;
|
|
436
|
+
return { paneId, windowTarget };
|
|
212
437
|
}
|
|
213
438
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
439
|
+
const createOutput = `${createResult.stderr}\n${createResult.stdout}`;
|
|
440
|
+
if (isTmuxDuplicateWindowError(createOutput)) {
|
|
441
|
+
const recoveredWindowId = await getTmuxWindowIdByName(sessionName, windowName);
|
|
442
|
+
if (recoveredWindowId) {
|
|
443
|
+
const recoveredSplit = await runProcess(
|
|
444
|
+
"tmux",
|
|
445
|
+
[
|
|
446
|
+
"split-window",
|
|
447
|
+
"-d",
|
|
448
|
+
"-t",
|
|
449
|
+
recoveredWindowId,
|
|
450
|
+
"-c",
|
|
451
|
+
cwd,
|
|
452
|
+
"-P",
|
|
453
|
+
"-F",
|
|
454
|
+
"#{pane_id}",
|
|
455
|
+
command
|
|
456
|
+
],
|
|
457
|
+
{
|
|
458
|
+
cwd,
|
|
459
|
+
timeoutMs: 10000,
|
|
460
|
+
signal
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
if (recoveredSplit.code === 0) {
|
|
465
|
+
return {
|
|
466
|
+
paneId: recoveredSplit.stdout.trim(),
|
|
467
|
+
windowTarget: recoveredWindowId
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
throw new Error(`tmux backend failed to create depth window: ${createResult.stderr || createResult.stdout}`);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function getCurrentTmuxSessionName(): Promise<string | undefined> {
|
|
478
|
+
const result = await runProcess("tmux", ["display-message", "-p", "#S"], {
|
|
479
|
+
timeoutMs: 5000,
|
|
480
|
+
env: process.env
|
|
481
|
+
}).catch(() => ({ code: null, stdout: "", stderr: "" }));
|
|
482
|
+
|
|
483
|
+
if (!result || result.code !== 0) {
|
|
484
|
+
return undefined;
|
|
219
485
|
}
|
|
486
|
+
|
|
487
|
+
const sessionName = result.stdout.trim();
|
|
488
|
+
return sessionName || undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function shouldCleanupCurrentSessionWindows(request: CompletionRequest): boolean {
|
|
492
|
+
if (!request.tmuxUseCurrentSession) return false;
|
|
493
|
+
if (request.depth !== 0) return false;
|
|
494
|
+
return request.stage === "solver" || request.stage === "synthesizer";
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function cleanupCurrentSessionDepthWindows(sessionName: string): Promise<void> {
|
|
498
|
+
const result = await runProcess(
|
|
499
|
+
"tmux",
|
|
500
|
+
["list-windows", "-t", sessionName, "-F", "#{window_id}\t#{window_name}"],
|
|
501
|
+
{
|
|
502
|
+
timeoutMs: 5000
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (result.code !== 0) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const rlmDepthWindowIds: string[] = [];
|
|
511
|
+
|
|
512
|
+
for (const entry of result.stdout.split("\n")) {
|
|
513
|
+
const line = entry.trim();
|
|
514
|
+
if (!line) continue;
|
|
515
|
+
|
|
516
|
+
const [windowId, ...nameParts] = line.split("\t");
|
|
517
|
+
if (!windowId) continue;
|
|
518
|
+
|
|
519
|
+
const windowName = nameParts.join("\t").trim();
|
|
520
|
+
if (!windowName.startsWith("rlm-depth-")) continue;
|
|
521
|
+
|
|
522
|
+
rlmDepthWindowIds.push(windowId.trim());
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
for (const windowId of rlmDepthWindowIds) {
|
|
526
|
+
await killTmuxWindow(windowId);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function waitForTmuxPane(
|
|
531
|
+
paneId: string,
|
|
532
|
+
timeoutMs: number,
|
|
533
|
+
signal?: AbortSignal
|
|
534
|
+
): Promise<"missing" | "dead"> {
|
|
535
|
+
const deadline = Date.now() + timeoutMs;
|
|
536
|
+
|
|
537
|
+
while (Date.now() < deadline) {
|
|
538
|
+
if (signal?.aborted) {
|
|
539
|
+
await killTmuxPane(paneId);
|
|
540
|
+
throw new Error("RLM request aborted");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const paneState = await getTmuxPaneState(paneId);
|
|
544
|
+
if (paneState === "missing" || paneState === "dead") {
|
|
545
|
+
return paneState;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
await sleep(250);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
await killTmuxPane(paneId);
|
|
552
|
+
throw new Error(`tmux backend timed out after ${timeoutMs}ms`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function getTmuxPaneState(paneId: string): Promise<"alive" | "dead" | "missing"> {
|
|
556
|
+
const result = await runProcess("tmux", ["list-panes", "-a", "-F", "#{pane_id} #{pane_dead}"], {
|
|
557
|
+
timeoutMs: 5000
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (result.code !== 0) {
|
|
561
|
+
return "missing";
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const line = result.stdout
|
|
565
|
+
.split("\n")
|
|
566
|
+
.map((entry) => entry.trim())
|
|
567
|
+
.find((entry) => entry.startsWith(`${paneId} `));
|
|
568
|
+
|
|
569
|
+
if (!line) {
|
|
570
|
+
return "missing";
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const deadFlag = line.slice(paneId.length + 1).trim();
|
|
574
|
+
return deadFlag === "1" ? "dead" : "alive";
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function getTmuxWindowIdByName(
|
|
578
|
+
sessionName: string,
|
|
579
|
+
windowName: string
|
|
580
|
+
): Promise<string | undefined> {
|
|
581
|
+
const result = await runProcess(
|
|
582
|
+
"tmux",
|
|
583
|
+
["list-windows", "-t", sessionName, "-F", "#{window_id}\t#{window_name}"],
|
|
584
|
+
{
|
|
585
|
+
timeoutMs: 5000
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
if (result.code !== 0) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
for (const entry of result.stdout.split("\n")) {
|
|
594
|
+
const line = entry.trim();
|
|
595
|
+
if (!line) continue;
|
|
596
|
+
|
|
597
|
+
const [windowId, ...nameParts] = line.split("\t");
|
|
598
|
+
if (!windowId) continue;
|
|
599
|
+
|
|
600
|
+
const currentName = nameParts.join("\t").trim();
|
|
601
|
+
if (currentName === windowName) {
|
|
602
|
+
return windowId;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function getTmuxWindowIdForPane(paneId: string): Promise<string | undefined> {
|
|
610
|
+
if (!paneId) return undefined;
|
|
611
|
+
|
|
612
|
+
const result = await runProcess("tmux", ["display-message", "-p", "-t", paneId, "#{window_id}"], {
|
|
613
|
+
timeoutMs: 5000
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
if (result.code !== 0) {
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const windowId = result.stdout.trim();
|
|
621
|
+
return windowId || undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function setTmuxPaneTitle(paneId: string, paneTitle: string): Promise<void> {
|
|
625
|
+
if (!paneId) return;
|
|
626
|
+
|
|
627
|
+
await runProcess("tmux", ["select-pane", "-t", paneId, "-T", paneTitle], {
|
|
628
|
+
timeoutMs: 5000
|
|
629
|
+
}).catch(() => undefined);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function setTmuxWindowTiled(windowTarget: string): Promise<void> {
|
|
633
|
+
await runProcess("tmux", ["select-layout", "-t", windowTarget, "tiled"], {
|
|
634
|
+
timeoutMs: 5000
|
|
635
|
+
}).catch(() => undefined);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function killTmuxPane(paneId: string): Promise<void> {
|
|
639
|
+
await runProcess("tmux", ["kill-pane", "-t", paneId], {
|
|
640
|
+
timeoutMs: 5000
|
|
641
|
+
}).catch(() => undefined);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function killTmuxWindow(windowTarget: string): Promise<void> {
|
|
645
|
+
await runProcess("tmux", ["kill-window", "-t", windowTarget], {
|
|
646
|
+
timeoutMs: 5000
|
|
647
|
+
}).catch(() => undefined);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function toTmuxRunSessionName(runId: string): string {
|
|
651
|
+
const sanitized = runId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
652
|
+
return `pi-rlm-${sanitized}`.slice(0, 48);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function toTmuxDepthWindowName(depth: number, useCurrentSession: boolean): string {
|
|
656
|
+
return useCurrentSession ? `rlm-depth-${depth}` : `depth-${depth}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function toTmuxPaneTitle(request: CompletionRequest): string {
|
|
660
|
+
const node = request.nodeId ?? "root";
|
|
661
|
+
const stage = request.stage ?? "call";
|
|
662
|
+
const depth = typeof request.depth === "number" ? `d${request.depth}` : "dx";
|
|
663
|
+
return `${depth}:${node}:${stage}`.slice(0, 64);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function isTmuxDuplicateSessionError(output: string): boolean {
|
|
667
|
+
const normalized = output.toLowerCase();
|
|
668
|
+
return normalized.includes("duplicate session") || normalized.includes("session already exists");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function isTmuxDuplicateWindowError(output: string): boolean {
|
|
672
|
+
const normalized = output.toLowerCase();
|
|
673
|
+
return normalized.includes("duplicate window") || normalized.includes("window already exists");
|
|
220
674
|
}
|
|
221
675
|
|
|
222
676
|
function extractLastAssistantText(messages: unknown[]): string {
|