@sma1lboy/kobe 0.8.99 → 0.8.102
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/dist/cli/index.js +395 -390
- package/dist/cli/kobe.js +395 -390
- package/dist/cli/pty-host-node.mjs +327 -99
- package/dist/cli/rove.js +395 -390
- package/dist/web-ui/assets/{AppShell-BTjvuY-t.js → AppShell-ByiYfvCg.js} +1 -1
- package/dist/web-ui/assets/{Board-DPB4h6mJ.js → Board-B0oB8DMk.js} +1 -1
- package/dist/web-ui/assets/{BoardPeek-69VJptGV.js → BoardPeek-VXShygbB.js} +2 -2
- package/dist/web-ui/assets/{ChatTerminal-MCI1aBbT.js → ChatTerminal-DNTeFtk5.js} +1 -1
- package/dist/web-ui/assets/{ChatTranscript-DLthbnmB.js → ChatTranscript-Co0UhezW.js} +1 -1
- package/dist/web-ui/assets/{ConfirmDialog-CDPOtXww.js → ConfirmDialog-D-NILtP0.js} +1 -1
- package/dist/web-ui/assets/{IssueIntakePanel-XYsIcIhp.js → IssueIntakePanel-BN31TW37.js} +1 -1
- package/dist/web-ui/assets/{IssuePeek-CP_rAkqO.js → IssuePeek-BT8ZmKTT.js} +1 -1
- package/dist/web-ui/assets/{IssuesPage-BR5mpuo-.js → IssuesPage-BjLRf_cj.js} +1 -1
- package/dist/web-ui/assets/{ViewToggle-BdPCMBWa.js → ViewToggle-BHBzWlni.js} +1 -1
- package/dist/web-ui/assets/{WorkspaceTabs-CMKlvN_Y.js → WorkspaceTabs-kqzy_Dsf.js} +3 -3
- package/dist/web-ui/assets/active-task-CuoFMFoR.js +1 -0
- package/dist/web-ui/assets/board-GAhbgwzQ.js +1 -0
- package/dist/web-ui/assets/desktop-CFge7SbS.js +2 -0
- package/dist/web-ui/assets/{harness-q9gT7Jic.js → harness-B5VlW5ov.js} +1 -1
- package/dist/web-ui/assets/{index-D58CfI0X.js → index-BgcK6obF.js} +3 -3
- package/dist/web-ui/assets/{issues-1SCRkcB0.js → issues-BMqItbVN.js} +1 -1
- package/dist/web-ui/assets/issues-DmgD6qU_.js +1 -0
- package/dist/web-ui/assets/{lazy-issue-panels-0Rhl2uhh.js → lazy-issue-panels-BcU72R8r.js} +3 -3
- package/dist/web-ui/assets/routes-DMeRqoPI.js +1 -0
- package/dist/web-ui/assets/store-BXzqsTjs.js +2 -0
- package/dist/web-ui/assets/{task._taskId-BmObFoVZ.js → task._taskId-BuwKbKaG.js} +1 -1
- package/dist/web-ui/assets/{task._taskId-BlZ0vG_1.js → task._taskId-EQWWNO2k.js} +2 -2
- package/dist/web-ui/assets/{triage-7QLsCY8d.js → triage-DDohuPMh.js} +1 -1
- package/dist/web-ui/assets/{worktrees-C_XKA7jM.js → worktrees-3wHfn7Vh.js} +1 -1
- package/dist/web-ui/index.html +6 -6
- package/package.json +1 -1
- package/dist/web-ui/assets/active-task-GeQZqbJK.js +0 -1
- package/dist/web-ui/assets/board-CGiK0u0M.js +0 -1
- package/dist/web-ui/assets/desktop-BThq9eSR.js +0 -2
- package/dist/web-ui/assets/issues-BKkua1D9.js +0 -1
- package/dist/web-ui/assets/routes-SBb-uSy8.js +0 -1
- package/dist/web-ui/assets/store-uT65Mask.js +0 -2
|
@@ -58,7 +58,7 @@ async function nodePtyDriver(spawn) {
|
|
|
58
58
|
import { mkdir, unlink, writeFile } from "node:fs/promises";
|
|
59
59
|
import { createServer } from "node:net";
|
|
60
60
|
import { dirname as dirname2 } from "node:path";
|
|
61
|
-
import { StringDecoder as
|
|
61
|
+
import { StringDecoder as StringDecoder3 } from "node:string_decoder";
|
|
62
62
|
|
|
63
63
|
// ../kobe-daemon/src/daemon/client-writer.ts
|
|
64
64
|
var DEFAULT_WRITE_HIGH_WATER_MARK = 8 * 1024 * 1024;
|
|
@@ -249,6 +249,9 @@ function defaultPtyHostPidPath(homeDir = readRoveEnv("HOME_DIR") ?? homedir()) {
|
|
|
249
249
|
function defaultPtyExitsPath(homeDir = readRoveEnv("HOME_DIR") ?? homedir()) {
|
|
250
250
|
return join(homeDir, COMPAT_STATE_DIR_BASENAME, "pty-exits.json");
|
|
251
251
|
}
|
|
252
|
+
function defaultPtyFreezeDir(homeDir = readRoveEnv("HOME_DIR") ?? homedir()) {
|
|
253
|
+
return join(homeDir, COMPAT_STATE_DIR_BASENAME, "pty-sessions");
|
|
254
|
+
}
|
|
252
255
|
// ../kobe-plugin-sdk/dist/contract.js
|
|
253
256
|
var DAEMON_CHANNELS = [
|
|
254
257
|
"task.snapshot",
|
|
@@ -327,8 +330,128 @@ function recordPtyExit(info, path = defaultPtyExitsPath()) {
|
|
|
327
330
|
writeFileSync(path, JSON.stringify(Object.fromEntries(newest.map((r) => [r.key, r])), null, 2), "utf8");
|
|
328
331
|
}
|
|
329
332
|
|
|
330
|
-
// ../kobe-daemon/src/daemon/pty-
|
|
333
|
+
// ../kobe-daemon/src/daemon/pty-freeze-store.ts
|
|
334
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
335
|
+
import { join as join2 } from "node:path";
|
|
331
336
|
import { StringDecoder } from "node:string_decoder";
|
|
337
|
+
var FREEZE_VERSION = 1;
|
|
338
|
+
function freezeSession(session, now = new Date) {
|
|
339
|
+
return {
|
|
340
|
+
v: FREEZE_VERSION,
|
|
341
|
+
key: session.key,
|
|
342
|
+
cwd: session.cwd,
|
|
343
|
+
command: [...session.command],
|
|
344
|
+
cols: session.cols,
|
|
345
|
+
rows: session.rows,
|
|
346
|
+
title: session.title,
|
|
347
|
+
totalBytes: session.totalBytes,
|
|
348
|
+
exit: session.exit,
|
|
349
|
+
ringB64: Buffer.concat(session.chunks).toString("base64"),
|
|
350
|
+
updatedAt: now.toISOString()
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function thawRing(record, cap) {
|
|
354
|
+
let ring;
|
|
355
|
+
try {
|
|
356
|
+
ring = Buffer.from(record.ringB64, "base64");
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const trimmed = ring.byteLength > cap ? ring.subarray(ring.byteLength - cap) : ring;
|
|
361
|
+
return { chunks: trimmed.byteLength > 0 ? [trimmed] : [], bytes: trimmed.byteLength };
|
|
362
|
+
}
|
|
363
|
+
function thawSession(record, cap) {
|
|
364
|
+
const ring = thawRing(record, cap);
|
|
365
|
+
if (!ring)
|
|
366
|
+
return null;
|
|
367
|
+
return {
|
|
368
|
+
key: record.key,
|
|
369
|
+
cwd: record.cwd,
|
|
370
|
+
proc: null,
|
|
371
|
+
alive: false,
|
|
372
|
+
chunks: [...ring.chunks],
|
|
373
|
+
bytes: ring.bytes,
|
|
374
|
+
totalBytes: Math.max(record.totalBytes, ring.bytes),
|
|
375
|
+
cols: record.cols,
|
|
376
|
+
rows: record.rows,
|
|
377
|
+
command: record.command,
|
|
378
|
+
title: record.title,
|
|
379
|
+
titleCarry: "",
|
|
380
|
+
titleDecoder: new StringDecoder("utf8"),
|
|
381
|
+
sinks: new Map,
|
|
382
|
+
parked: false,
|
|
383
|
+
parkedScreenBytes: 0,
|
|
384
|
+
exit: record.exit,
|
|
385
|
+
restored: true,
|
|
386
|
+
lastFreezeAtMs: 0
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function recordFile(dir, key) {
|
|
390
|
+
return join2(dir, `${encodeURIComponent(key)}.json`);
|
|
391
|
+
}
|
|
392
|
+
function parseRecord(raw) {
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse(raw);
|
|
395
|
+
if (parsed?.v !== FREEZE_VERSION)
|
|
396
|
+
return null;
|
|
397
|
+
if (typeof parsed.key !== "string" || parsed.key.length === 0)
|
|
398
|
+
return null;
|
|
399
|
+
if (parsed.key.startsWith("::"))
|
|
400
|
+
return null;
|
|
401
|
+
if (typeof parsed.cwd !== "string" || !Array.isArray(parsed.command))
|
|
402
|
+
return null;
|
|
403
|
+
if (typeof parsed.ringB64 !== "string" || typeof parsed.totalBytes !== "number")
|
|
404
|
+
return null;
|
|
405
|
+
return parsed;
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function loadFrozenSessions(dir = defaultPtyFreezeDir()) {
|
|
411
|
+
let names;
|
|
412
|
+
try {
|
|
413
|
+
names = readdirSync(dir);
|
|
414
|
+
} catch {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
const out = [];
|
|
418
|
+
for (const name of names) {
|
|
419
|
+
if (!name.endsWith(".json"))
|
|
420
|
+
continue;
|
|
421
|
+
try {
|
|
422
|
+
const record = parseRecord(readFileSync2(join2(dir, name), "utf8"));
|
|
423
|
+
if (record)
|
|
424
|
+
out.push(record);
|
|
425
|
+
} catch {}
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
function fileFreezeSink(dir = defaultPtyFreezeDir()) {
|
|
430
|
+
return {
|
|
431
|
+
save(record) {
|
|
432
|
+
try {
|
|
433
|
+
mkdirSync2(dir, { recursive: true });
|
|
434
|
+
const target = recordFile(dir, record.key);
|
|
435
|
+
const staging = `${target}.${process.pid}.tmp`;
|
|
436
|
+
writeFileSync2(staging, JSON.stringify(record), "utf8");
|
|
437
|
+
renameSync(staging, target);
|
|
438
|
+
} catch {}
|
|
439
|
+
},
|
|
440
|
+
drop(key) {
|
|
441
|
+
try {
|
|
442
|
+
rmSync(recordFile(dir, key), { force: true });
|
|
443
|
+
} catch {}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function clearFrozenSessions(dir = defaultPtyFreezeDir()) {
|
|
448
|
+
try {
|
|
449
|
+
rmSync(dir, { recursive: true, force: true });
|
|
450
|
+
} catch {}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ../kobe-daemon/src/daemon/pty-host.ts
|
|
454
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
332
455
|
|
|
333
456
|
// ../kobe-daemon/src/daemon/platform-shell.js
|
|
334
457
|
import { existsSync } from "node:fs";
|
|
@@ -402,6 +525,20 @@ function embeddedTerminalEnv(base, overrides = {}) {
|
|
|
402
525
|
}
|
|
403
526
|
|
|
404
527
|
// ../kobe-daemon/src/daemon/pty-observability.ts
|
|
528
|
+
function sessionInfo(s) {
|
|
529
|
+
return {
|
|
530
|
+
key: s.key,
|
|
531
|
+
alive: s.alive,
|
|
532
|
+
pid: s.proc?.pid ?? null,
|
|
533
|
+
command: s.command,
|
|
534
|
+
title: s.title,
|
|
535
|
+
totalBytes: s.totalBytes,
|
|
536
|
+
parked: s.parked,
|
|
537
|
+
parkedScreenBytes: s.parkedScreenBytes,
|
|
538
|
+
exit: s.exit,
|
|
539
|
+
restored: s.restored || undefined
|
|
540
|
+
};
|
|
541
|
+
}
|
|
405
542
|
function describeExit(exit) {
|
|
406
543
|
if (exit?.signal)
|
|
407
544
|
return ` (signal ${exit.signal})`;
|
|
@@ -420,6 +557,28 @@ function ringTail(chunks, bytes, maxBytes) {
|
|
|
420
557
|
}
|
|
421
558
|
return Buffer.concat(parts).toString("utf8");
|
|
422
559
|
}
|
|
560
|
+
function hostStats(sessions, perSessionCap, parkRestoreDeltas, parkRestoreFallbacks) {
|
|
561
|
+
let ringBytes = 0;
|
|
562
|
+
let count = 0;
|
|
563
|
+
let parkedSessions = 0;
|
|
564
|
+
let parkedScreenBytes = 0;
|
|
565
|
+
for (const session of sessions) {
|
|
566
|
+
count++;
|
|
567
|
+
ringBytes += session.bytes;
|
|
568
|
+
if (session.parked) {
|
|
569
|
+
parkedSessions++;
|
|
570
|
+
parkedScreenBytes += session.parkedScreenBytes;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
ringBytes,
|
|
575
|
+
ringCapacityBytes: count * perSessionCap,
|
|
576
|
+
parkedSessions,
|
|
577
|
+
parkedScreenBytes,
|
|
578
|
+
parkRestoreDeltas,
|
|
579
|
+
parkRestoreFallbacks
|
|
580
|
+
};
|
|
581
|
+
}
|
|
423
582
|
var OSC_TITLE_RE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
|
|
424
583
|
var TITLE_CARRY_CAP = 1024;
|
|
425
584
|
function titleCarryFrom(rest) {
|
|
@@ -491,28 +650,99 @@ function signalProcessGroup(pid, signal, fallback, platform = process.platform)
|
|
|
491
650
|
fallback();
|
|
492
651
|
} catch {}
|
|
493
652
|
}
|
|
653
|
+
var TERMINATION_GRACE_MS = 500;
|
|
654
|
+
async function terminatePtyChild(proc, onSettled) {
|
|
655
|
+
signalProcessGroup(proc.pid, "SIGTERM", () => proc.kill("SIGTERM"));
|
|
656
|
+
if (!await settledWithin(proc.exited, TERMINATION_GRACE_MS)) {
|
|
657
|
+
signalProcessGroup(proc.pid, "SIGKILL", () => proc.kill("SIGKILL"));
|
|
658
|
+
await settledWithin(proc.exited, TERMINATION_GRACE_MS);
|
|
659
|
+
}
|
|
660
|
+
onSettled();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ../kobe-daemon/src/daemon/pty-warm.ts
|
|
664
|
+
class WarmSpare {
|
|
665
|
+
deps;
|
|
666
|
+
spare = null;
|
|
667
|
+
constructor(deps) {
|
|
668
|
+
this.deps = deps;
|
|
669
|
+
}
|
|
670
|
+
warm(cwd, shell, cols = 80, rows = 24) {
|
|
671
|
+
const argv0 = shell ?? resolveLoginShell();
|
|
672
|
+
if (this.spare?.alive && this.spare.cwd === cwd && this.spare.command[0] === argv0)
|
|
673
|
+
return;
|
|
674
|
+
const old = this.spare;
|
|
675
|
+
this.spare = null;
|
|
676
|
+
if (old)
|
|
677
|
+
this.deps.endChild(old);
|
|
678
|
+
const session = this.deps.spawn("::spare", { cwd, command: [argv0], cols, rows }, true);
|
|
679
|
+
this.spare = session.alive ? session : null;
|
|
680
|
+
}
|
|
681
|
+
adopt(key, spec) {
|
|
682
|
+
const spare = this.spare;
|
|
683
|
+
if (!spare?.alive || spare.cwd !== spec.cwd)
|
|
684
|
+
return null;
|
|
685
|
+
const want = spec.command && spec.command.length > 0 ? spec.command : [spec.shell ?? resolveLoginShell()];
|
|
686
|
+
if (want.length !== 1 || want[0] !== spare.command[0])
|
|
687
|
+
return null;
|
|
688
|
+
this.spare = null;
|
|
689
|
+
spare.key = key;
|
|
690
|
+
const cols = spec.cols ?? spare.cols;
|
|
691
|
+
const rows = spec.rows ?? spare.rows;
|
|
692
|
+
if (spare.cols !== cols || spare.rows !== rows) {
|
|
693
|
+
spare.cols = cols;
|
|
694
|
+
spare.rows = rows;
|
|
695
|
+
try {
|
|
696
|
+
spare.proc?.resize(cols, rows);
|
|
697
|
+
} catch {
|
|
698
|
+
this.deps.markExited(spare);
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
this.deps.log?.("pty", `adopted warm shell for ${key} (pid ${spare.proc?.pid})`);
|
|
703
|
+
this.deps.onSessionStart?.();
|
|
704
|
+
this.warm(spec.cwd, spare.command[0], cols, rows);
|
|
705
|
+
return spare;
|
|
706
|
+
}
|
|
707
|
+
async end() {
|
|
708
|
+
const spare = this.spare;
|
|
709
|
+
this.spare = null;
|
|
710
|
+
if (spare)
|
|
711
|
+
await this.deps.endChild(spare);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
494
714
|
|
|
495
715
|
// ../kobe-daemon/src/daemon/pty-host.ts
|
|
496
716
|
var DEFAULT_SCROLLBACK_CAP = 512 * 1024;
|
|
497
|
-
var TERMINATION_GRACE_MS = 500;
|
|
498
717
|
var EXIT_TAIL_BYTES = 16 * 1024;
|
|
718
|
+
var FREEZE_INTERVAL_MS = 5000;
|
|
499
719
|
|
|
500
720
|
class PtyHost {
|
|
501
721
|
sessions = new Map;
|
|
502
722
|
opts;
|
|
503
723
|
parkRestoreDeltas = 0;
|
|
504
724
|
parkRestoreFallbacks = 0;
|
|
505
|
-
|
|
725
|
+
warmSpare = new WarmSpare({
|
|
726
|
+
spawn: (key, spec, spare) => this.spawn(key, spec, spare),
|
|
727
|
+
endChild: (session) => this.endChild(session),
|
|
728
|
+
markExited: (session) => this.markExited(session),
|
|
729
|
+
log: (event, message) => this.opts.log?.(event, message),
|
|
730
|
+
onSessionStart: () => this.opts.onSessionStart?.()
|
|
731
|
+
});
|
|
506
732
|
constructor(opts = {}) {
|
|
507
733
|
this.opts = opts;
|
|
508
734
|
}
|
|
509
735
|
open(key, spec, token, sink, sinceOffset, sincePid) {
|
|
510
736
|
let session = this.sessions.get(key);
|
|
511
737
|
let created = false;
|
|
738
|
+
let respawned = false;
|
|
512
739
|
if (!session) {
|
|
513
740
|
created = true;
|
|
514
|
-
session = this.
|
|
741
|
+
session = this.warmSpare.adopt(key, spec) ?? this.spawn(key, spec);
|
|
515
742
|
this.sessions.set(key, session);
|
|
743
|
+
} else if (!session.alive && session.restored) {
|
|
744
|
+
this.respawn(session, spec);
|
|
745
|
+
respawned = session.alive;
|
|
516
746
|
} else if (session.alive && spec.cols !== undefined && spec.rows !== undefined && (session.cols !== spec.cols || session.rows !== spec.rows)) {
|
|
517
747
|
this.resize(key, spec.cols, spec.rows);
|
|
518
748
|
}
|
|
@@ -537,46 +767,13 @@ class PtyHost {
|
|
|
537
767
|
alive: session.alive,
|
|
538
768
|
pid: session.proc?.pid ?? null,
|
|
539
769
|
created,
|
|
770
|
+
respawned,
|
|
540
771
|
offset: session.totalBytes,
|
|
541
772
|
sinceValid
|
|
542
773
|
};
|
|
543
774
|
}
|
|
544
775
|
warm(cwd, shell, cols = 80, rows = 24) {
|
|
545
|
-
|
|
546
|
-
if (this.spare?.alive && this.spare.cwd === cwd && this.spare.command[0] === argv0)
|
|
547
|
-
return;
|
|
548
|
-
const old = this.spare;
|
|
549
|
-
this.spare = null;
|
|
550
|
-
if (old)
|
|
551
|
-
this.endChild(old);
|
|
552
|
-
const session = this.spawn("::spare", { cwd, command: [argv0], cols, rows }, true);
|
|
553
|
-
this.spare = session.alive ? session : null;
|
|
554
|
-
}
|
|
555
|
-
adoptSpare(key, spec) {
|
|
556
|
-
const spare = this.spare;
|
|
557
|
-
if (!spare?.alive || spare.cwd !== spec.cwd)
|
|
558
|
-
return null;
|
|
559
|
-
const want = spec.command && spec.command.length > 0 ? spec.command : [spec.shell ?? resolveLoginShell()];
|
|
560
|
-
if (want.length !== 1 || want[0] !== spare.command[0])
|
|
561
|
-
return null;
|
|
562
|
-
this.spare = null;
|
|
563
|
-
spare.key = key;
|
|
564
|
-
const cols = spec.cols ?? spare.cols;
|
|
565
|
-
const rows = spec.rows ?? spare.rows;
|
|
566
|
-
if (spare.cols !== cols || spare.rows !== rows) {
|
|
567
|
-
spare.cols = cols;
|
|
568
|
-
spare.rows = rows;
|
|
569
|
-
try {
|
|
570
|
-
spare.proc?.resize(cols, rows);
|
|
571
|
-
} catch {
|
|
572
|
-
this.markExited(spare);
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
this.opts.log?.("pty", `adopted warm shell for ${key} (pid ${spare.proc?.pid})`);
|
|
577
|
-
this.opts.onSessionStart?.();
|
|
578
|
-
this.warm(spec.cwd, spare.command[0], cols, rows);
|
|
579
|
-
return spare;
|
|
776
|
+
this.warmSpare.warm(cwd, shell, cols, rows);
|
|
580
777
|
}
|
|
581
778
|
write(key, data) {
|
|
582
779
|
const session = this.sessions.get(key);
|
|
@@ -601,6 +798,7 @@ class PtyHost {
|
|
|
601
798
|
if (!session)
|
|
602
799
|
return Promise.resolve();
|
|
603
800
|
this.sessions.delete(key);
|
|
801
|
+
this.opts.freeze?.drop(key);
|
|
604
802
|
return this.endChild(session);
|
|
605
803
|
}
|
|
606
804
|
detach(key, token, parked = false, parkedScreenBytes = 0) {
|
|
@@ -623,40 +821,13 @@ class PtyHost {
|
|
|
623
821
|
}
|
|
624
822
|
}
|
|
625
823
|
list() {
|
|
626
|
-
return Array.from(this.sessions.values(), (s) => (
|
|
627
|
-
key: s.key,
|
|
628
|
-
alive: s.alive,
|
|
629
|
-
pid: s.proc?.pid ?? null,
|
|
630
|
-
command: s.command,
|
|
631
|
-
title: s.title,
|
|
632
|
-
totalBytes: s.totalBytes,
|
|
633
|
-
parked: s.parked,
|
|
634
|
-
parkedScreenBytes: s.parkedScreenBytes,
|
|
635
|
-
exit: s.exit
|
|
636
|
-
}));
|
|
824
|
+
return Array.from(this.sessions.values(), (s) => sessionInfo(s));
|
|
637
825
|
}
|
|
638
826
|
peek(key, sinceOffset) {
|
|
639
827
|
return peekRing(this.sessions.get(key), sinceOffset);
|
|
640
828
|
}
|
|
641
829
|
stats() {
|
|
642
|
-
|
|
643
|
-
let parkedSessions = 0;
|
|
644
|
-
let parkedScreenBytes = 0;
|
|
645
|
-
for (const session of this.sessions.values()) {
|
|
646
|
-
ringBytes += session.bytes;
|
|
647
|
-
if (session.parked) {
|
|
648
|
-
parkedSessions++;
|
|
649
|
-
parkedScreenBytes += session.parkedScreenBytes;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
return {
|
|
653
|
-
ringBytes,
|
|
654
|
-
ringCapacityBytes: this.sessions.size * (this.opts.scrollbackCap ?? DEFAULT_SCROLLBACK_CAP),
|
|
655
|
-
parkedSessions,
|
|
656
|
-
parkedScreenBytes,
|
|
657
|
-
parkRestoreDeltas: this.parkRestoreDeltas,
|
|
658
|
-
parkRestoreFallbacks: this.parkRestoreFallbacks
|
|
659
|
-
};
|
|
830
|
+
return hostStats(this.sessions.values(), this.opts.scrollbackCap ?? DEFAULT_SCROLLBACK_CAP, this.parkRestoreDeltas, this.parkRestoreFallbacks);
|
|
660
831
|
}
|
|
661
832
|
sweepTasks(liveTaskIds) {
|
|
662
833
|
for (const key of Array.from(this.sessions.keys())) {
|
|
@@ -667,12 +838,57 @@ class PtyHost {
|
|
|
667
838
|
}
|
|
668
839
|
async killAll() {
|
|
669
840
|
const sessions = Array.from(this.sessions.keys(), (key) => this.kill(key));
|
|
670
|
-
|
|
671
|
-
this.spare = null;
|
|
672
|
-
if (spare)
|
|
673
|
-
sessions.push(this.endChild(spare));
|
|
841
|
+
sessions.push(this.warmSpare.end());
|
|
674
842
|
await Promise.all(sessions);
|
|
675
843
|
}
|
|
844
|
+
async shutdown() {
|
|
845
|
+
this.flushFrozen();
|
|
846
|
+
const endings = Array.from(this.sessions.values(), (session) => this.endChild(session));
|
|
847
|
+
endings.push(this.warmSpare.end());
|
|
848
|
+
await Promise.all(endings);
|
|
849
|
+
}
|
|
850
|
+
flushFrozen() {
|
|
851
|
+
for (const session of this.sessions.values())
|
|
852
|
+
this.maybeFreeze(session, true);
|
|
853
|
+
}
|
|
854
|
+
restoreFrozen(records) {
|
|
855
|
+
let restoredCount = 0;
|
|
856
|
+
for (const record of records) {
|
|
857
|
+
if (this.sessions.has(record.key))
|
|
858
|
+
continue;
|
|
859
|
+
const session = thawSession(record, this.opts.scrollbackCap ?? DEFAULT_SCROLLBACK_CAP);
|
|
860
|
+
if (!session)
|
|
861
|
+
continue;
|
|
862
|
+
this.sessions.set(record.key, session);
|
|
863
|
+
restoredCount++;
|
|
864
|
+
}
|
|
865
|
+
if (restoredCount > 0)
|
|
866
|
+
this.opts.log?.("pty", `restored ${restoredCount} frozen session(s) from disk`);
|
|
867
|
+
return restoredCount;
|
|
868
|
+
}
|
|
869
|
+
respawn(session, spec) {
|
|
870
|
+
session.restored = false;
|
|
871
|
+
session.exit = null;
|
|
872
|
+
if (spec.command && spec.command.length > 0)
|
|
873
|
+
session.command = [...spec.command];
|
|
874
|
+
session.cols = spec.cols ?? session.cols;
|
|
875
|
+
session.rows = spec.rows ?? session.rows;
|
|
876
|
+
this.startChild(session);
|
|
877
|
+
if (!session.alive)
|
|
878
|
+
return;
|
|
879
|
+
this.opts.log?.("pty", `respawned restored session ${session.key} (pid ${session.proc?.pid})`);
|
|
880
|
+
this.opts.onSessionStart?.();
|
|
881
|
+
}
|
|
882
|
+
maybeFreeze(session, force = false) {
|
|
883
|
+
const freeze = this.opts.freeze;
|
|
884
|
+
if (!freeze || session.key.startsWith("::"))
|
|
885
|
+
return;
|
|
886
|
+
const now = Date.now();
|
|
887
|
+
if (!force && now - session.lastFreezeAtMs < FREEZE_INTERVAL_MS)
|
|
888
|
+
return;
|
|
889
|
+
session.lastFreezeAtMs = now;
|
|
890
|
+
freeze.save(freezeSession(session));
|
|
891
|
+
}
|
|
676
892
|
liveCount() {
|
|
677
893
|
let n = 0;
|
|
678
894
|
for (const session of this.sessions.values())
|
|
@@ -682,8 +898,6 @@ class PtyHost {
|
|
|
682
898
|
}
|
|
683
899
|
spawn(key, spec, spare = false) {
|
|
684
900
|
const argv = spec.command && spec.command.length > 0 ? [...spec.command] : [spec.shell ?? resolveLoginShell()];
|
|
685
|
-
const cols = spec.cols ?? 80;
|
|
686
|
-
const rows = spec.rows ?? 24;
|
|
687
901
|
const session = {
|
|
688
902
|
key,
|
|
689
903
|
cwd: spec.cwd,
|
|
@@ -692,41 +906,49 @@ class PtyHost {
|
|
|
692
906
|
chunks: [],
|
|
693
907
|
bytes: 0,
|
|
694
908
|
totalBytes: 0,
|
|
695
|
-
cols,
|
|
696
|
-
rows,
|
|
909
|
+
cols: spec.cols ?? 80,
|
|
910
|
+
rows: spec.rows ?? 24,
|
|
697
911
|
command: argv,
|
|
698
912
|
title: "",
|
|
699
913
|
titleCarry: "",
|
|
700
|
-
titleDecoder: new
|
|
914
|
+
titleDecoder: new StringDecoder2("utf8"),
|
|
701
915
|
sinks: new Map,
|
|
702
916
|
parked: false,
|
|
703
917
|
parkedScreenBytes: 0,
|
|
704
|
-
exit: null
|
|
918
|
+
exit: null,
|
|
919
|
+
restored: false,
|
|
920
|
+
lastFreezeAtMs: 0
|
|
705
921
|
};
|
|
922
|
+
this.startChild(session);
|
|
923
|
+
if (session.alive) {
|
|
924
|
+
this.opts.log?.("pty", `spawned ${argv[0]} for ${key} (pid ${session.proc?.pid})`);
|
|
925
|
+
if (!spare)
|
|
926
|
+
this.opts.onSessionStart?.();
|
|
927
|
+
}
|
|
928
|
+
return session;
|
|
929
|
+
}
|
|
930
|
+
startChild(session) {
|
|
706
931
|
try {
|
|
707
932
|
session.proc = (this.opts.driver ?? bunTerminalDriver())({
|
|
708
|
-
argv,
|
|
709
|
-
cwd:
|
|
933
|
+
argv: [...session.command],
|
|
934
|
+
cwd: session.cwd,
|
|
710
935
|
env: embeddedTerminalEnv(process.env, {
|
|
711
936
|
TERM: "xterm-256color",
|
|
712
|
-
COLUMNS: String(cols),
|
|
713
|
-
LINES: String(rows),
|
|
937
|
+
COLUMNS: String(session.cols),
|
|
938
|
+
LINES: String(session.rows),
|
|
714
939
|
BASH_SILENCE_DEPRECATION_WARNING: "1",
|
|
715
940
|
KOBE_TERMINAL_PTY: "1"
|
|
716
941
|
}),
|
|
717
|
-
cols,
|
|
718
|
-
rows,
|
|
942
|
+
cols: session.cols,
|
|
943
|
+
rows: session.rows,
|
|
719
944
|
onData: (data) => this.onData(session, data)
|
|
720
945
|
});
|
|
946
|
+
session.alive = true;
|
|
721
947
|
session.proc.exited.then((exit) => this.markExited(session, exit), () => this.markExited(session));
|
|
722
|
-
this.opts.log?.("pty", `spawned ${argv[0]} for ${key} (pid ${session.proc.pid})`);
|
|
723
|
-
if (!spare)
|
|
724
|
-
this.opts.onSessionStart?.();
|
|
725
948
|
} catch (err) {
|
|
726
949
|
session.alive = false;
|
|
727
|
-
this.opts.log?.("pty", `spawn failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
950
|
+
this.opts.log?.("pty", `spawn failed for ${session.key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
728
951
|
}
|
|
729
|
-
return session;
|
|
730
952
|
}
|
|
731
953
|
onData(session, data) {
|
|
732
954
|
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
|
|
@@ -739,8 +961,10 @@ class PtyHost {
|
|
|
739
961
|
if (dropped)
|
|
740
962
|
session.bytes -= dropped.byteLength;
|
|
741
963
|
}
|
|
742
|
-
if (session.sinks.size === 0)
|
|
964
|
+
if (session.sinks.size === 0) {
|
|
965
|
+
this.maybeFreeze(session);
|
|
743
966
|
return;
|
|
967
|
+
}
|
|
744
968
|
const frame = {
|
|
745
969
|
type: "event",
|
|
746
970
|
name: "pty.data",
|
|
@@ -748,6 +972,7 @@ class PtyHost {
|
|
|
748
972
|
};
|
|
749
973
|
for (const sink of session.sinks.values())
|
|
750
974
|
sink(frame);
|
|
975
|
+
this.maybeFreeze(session);
|
|
751
976
|
}
|
|
752
977
|
markExited(session, exit) {
|
|
753
978
|
if (!session.alive)
|
|
@@ -765,6 +990,7 @@ class PtyHost {
|
|
|
765
990
|
for (const sink of session.sinks.values())
|
|
766
991
|
sink(frame);
|
|
767
992
|
this.opts.log?.("pty", `session ${session.key} exited${describeExit(session.exit)}`);
|
|
993
|
+
this.maybeFreeze(session, true);
|
|
768
994
|
try {
|
|
769
995
|
this.opts.onSessionExit?.({
|
|
770
996
|
key: session.key,
|
|
@@ -783,12 +1009,7 @@ class PtyHost {
|
|
|
783
1009
|
this.markExited(session);
|
|
784
1010
|
return;
|
|
785
1011
|
}
|
|
786
|
-
|
|
787
|
-
if (!await settledWithin(proc.exited, TERMINATION_GRACE_MS)) {
|
|
788
|
-
signalProcessGroup(proc.pid, "SIGKILL", () => proc.kill("SIGKILL"));
|
|
789
|
-
await settledWithin(proc.exited, TERMINATION_GRACE_MS);
|
|
790
|
-
}
|
|
791
|
-
this.markExited(session);
|
|
1012
|
+
await terminatePtyChild(proc, () => this.markExited(session));
|
|
792
1013
|
}
|
|
793
1014
|
}
|
|
794
1015
|
|
|
@@ -804,10 +1025,12 @@ function resolveIdleExitMs() {
|
|
|
804
1025
|
async function startPtyHostServer(options = {}) {
|
|
805
1026
|
const socketPath = options.socketPath ?? defaultPtyHostSocketPath();
|
|
806
1027
|
const pidPath = options.pidPath ?? defaultPtyHostPidPath();
|
|
1028
|
+
const freezeDir = options.freezeDir ?? defaultPtyFreezeDir();
|
|
807
1029
|
const idleExitMs = options.idleExitMs || resolveIdleExitMs();
|
|
808
1030
|
const log = options.log ?? (() => {});
|
|
809
1031
|
const clients = new Set;
|
|
810
1032
|
let stopping = false;
|
|
1033
|
+
let wipeFreezeOnStop = false;
|
|
811
1034
|
let idleTimer = null;
|
|
812
1035
|
const cancelIdle = () => {
|
|
813
1036
|
if (idleTimer)
|
|
@@ -838,9 +1061,11 @@ async function startPtyHostServer(options = {}) {
|
|
|
838
1061
|
log("pty", `exit record write failed for ${info.key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
839
1062
|
}
|
|
840
1063
|
},
|
|
1064
|
+
freeze: fileFreezeSink(freezeDir),
|
|
841
1065
|
driver: options.driver,
|
|
842
1066
|
log
|
|
843
1067
|
});
|
|
1068
|
+
ptys.restoreFrozen(loadFrozenSessions(freezeDir));
|
|
844
1069
|
const pipeSocket = isWindowsPipePath(socketPath);
|
|
845
1070
|
if (!pipeSocket)
|
|
846
1071
|
await mkdir(dirname2(socketPath), { recursive: true });
|
|
@@ -857,7 +1082,7 @@ async function startPtyHostServer(options = {}) {
|
|
|
857
1082
|
buffer: ""
|
|
858
1083
|
};
|
|
859
1084
|
clients.add(client);
|
|
860
|
-
const decoder = new
|
|
1085
|
+
const decoder = new StringDecoder3("utf8");
|
|
861
1086
|
socket.on("data", (chunk) => {
|
|
862
1087
|
client.buffer += decoder.write(chunk);
|
|
863
1088
|
drain(client);
|
|
@@ -876,7 +1101,9 @@ async function startPtyHostServer(options = {}) {
|
|
|
876
1101
|
return;
|
|
877
1102
|
stopping = true;
|
|
878
1103
|
cancelIdle();
|
|
879
|
-
await ptys.
|
|
1104
|
+
await ptys.shutdown();
|
|
1105
|
+
if (wipeFreezeOnStop)
|
|
1106
|
+
clearFrozenSessions(freezeDir);
|
|
880
1107
|
for (const client of Array.from(clients))
|
|
881
1108
|
client.socket.destroy();
|
|
882
1109
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
@@ -940,6 +1167,7 @@ async function startPtyHostServer(options = {}) {
|
|
|
940
1167
|
return {};
|
|
941
1168
|
}
|
|
942
1169
|
case "daemon.stop":
|
|
1170
|
+
wipeFreezeOnStop = true;
|
|
943
1171
|
setTimeout(() => void stop(), 0).unref();
|
|
944
1172
|
return {};
|
|
945
1173
|
default:
|