@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.
Files changed (37) hide show
  1. package/dist/cli/index.js +395 -390
  2. package/dist/cli/kobe.js +395 -390
  3. package/dist/cli/pty-host-node.mjs +327 -99
  4. package/dist/cli/rove.js +395 -390
  5. package/dist/web-ui/assets/{AppShell-BTjvuY-t.js → AppShell-ByiYfvCg.js} +1 -1
  6. package/dist/web-ui/assets/{Board-DPB4h6mJ.js → Board-B0oB8DMk.js} +1 -1
  7. package/dist/web-ui/assets/{BoardPeek-69VJptGV.js → BoardPeek-VXShygbB.js} +2 -2
  8. package/dist/web-ui/assets/{ChatTerminal-MCI1aBbT.js → ChatTerminal-DNTeFtk5.js} +1 -1
  9. package/dist/web-ui/assets/{ChatTranscript-DLthbnmB.js → ChatTranscript-Co0UhezW.js} +1 -1
  10. package/dist/web-ui/assets/{ConfirmDialog-CDPOtXww.js → ConfirmDialog-D-NILtP0.js} +1 -1
  11. package/dist/web-ui/assets/{IssueIntakePanel-XYsIcIhp.js → IssueIntakePanel-BN31TW37.js} +1 -1
  12. package/dist/web-ui/assets/{IssuePeek-CP_rAkqO.js → IssuePeek-BT8ZmKTT.js} +1 -1
  13. package/dist/web-ui/assets/{IssuesPage-BR5mpuo-.js → IssuesPage-BjLRf_cj.js} +1 -1
  14. package/dist/web-ui/assets/{ViewToggle-BdPCMBWa.js → ViewToggle-BHBzWlni.js} +1 -1
  15. package/dist/web-ui/assets/{WorkspaceTabs-CMKlvN_Y.js → WorkspaceTabs-kqzy_Dsf.js} +3 -3
  16. package/dist/web-ui/assets/active-task-CuoFMFoR.js +1 -0
  17. package/dist/web-ui/assets/board-GAhbgwzQ.js +1 -0
  18. package/dist/web-ui/assets/desktop-CFge7SbS.js +2 -0
  19. package/dist/web-ui/assets/{harness-q9gT7Jic.js → harness-B5VlW5ov.js} +1 -1
  20. package/dist/web-ui/assets/{index-D58CfI0X.js → index-BgcK6obF.js} +3 -3
  21. package/dist/web-ui/assets/{issues-1SCRkcB0.js → issues-BMqItbVN.js} +1 -1
  22. package/dist/web-ui/assets/issues-DmgD6qU_.js +1 -0
  23. package/dist/web-ui/assets/{lazy-issue-panels-0Rhl2uhh.js → lazy-issue-panels-BcU72R8r.js} +3 -3
  24. package/dist/web-ui/assets/routes-DMeRqoPI.js +1 -0
  25. package/dist/web-ui/assets/store-BXzqsTjs.js +2 -0
  26. package/dist/web-ui/assets/{task._taskId-BmObFoVZ.js → task._taskId-BuwKbKaG.js} +1 -1
  27. package/dist/web-ui/assets/{task._taskId-BlZ0vG_1.js → task._taskId-EQWWNO2k.js} +2 -2
  28. package/dist/web-ui/assets/{triage-7QLsCY8d.js → triage-DDohuPMh.js} +1 -1
  29. package/dist/web-ui/assets/{worktrees-C_XKA7jM.js → worktrees-3wHfn7Vh.js} +1 -1
  30. package/dist/web-ui/index.html +6 -6
  31. package/package.json +1 -1
  32. package/dist/web-ui/assets/active-task-GeQZqbJK.js +0 -1
  33. package/dist/web-ui/assets/board-CGiK0u0M.js +0 -1
  34. package/dist/web-ui/assets/desktop-BThq9eSR.js +0 -2
  35. package/dist/web-ui/assets/issues-BKkua1D9.js +0 -1
  36. package/dist/web-ui/assets/routes-SBb-uSy8.js +0 -1
  37. 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 StringDecoder2 } from "node:string_decoder";
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-host.ts
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
- spare = null;
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.adoptSpare(key, spec) ?? this.spawn(key, spec);
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
- const argv0 = shell ?? resolveLoginShell();
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
- let ringBytes = 0;
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
- const spare = this.spare;
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 StringDecoder("utf8"),
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: spec.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
- signalProcessGroup(proc.pid, "SIGTERM", () => proc.kill("SIGTERM"));
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 StringDecoder2("utf8");
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.killAll();
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: