pty-manager 1.2.21 → 1.3.0

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/index.mjs CHANGED
@@ -268,7 +268,7 @@ var SPECIAL_KEYS = {
268
268
  };
269
269
  var BRACKETED_PASTE_START = "\x1B[200~";
270
270
  var BRACKETED_PASTE_END = "\x1B[201~";
271
- var PTYSession = class extends EventEmitter {
271
+ var PTYSession = class _PTYSession extends EventEmitter {
272
272
  constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
273
273
  super();
274
274
  this.adapter = adapter;
@@ -306,6 +306,9 @@ var PTYSession = class extends EventEmitter {
306
306
  _lastStallHash = null;
307
307
  _stallStartedAt = null;
308
308
  _lastContentHash = null;
309
+ // Task completion detection (idle detection when busy)
310
+ _taskCompleteTimer = null;
311
+ static TASK_COMPLETE_DEBOUNCE_MS = 1500;
309
312
  id;
310
313
  config;
311
314
  get status() {
@@ -445,6 +448,19 @@ var PTYSession = class extends EventEmitter {
445
448
  return;
446
449
  }
447
450
  this._lastStallHash = hash;
451
+ if (this._status === "busy" && this.adapter.detectTaskComplete?.(this.outputBuffer)) {
452
+ this._status = "ready";
453
+ this._lastBlockingPromptHash = null;
454
+ this.outputBuffer = "";
455
+ this.clearStallTimer();
456
+ this.emit("status_changed", "ready");
457
+ this.emit("task_complete");
458
+ this.logger.info(
459
+ { sessionId: this.id },
460
+ "Task complete (adapter fast-path) \u2014 agent returned to idle prompt"
461
+ );
462
+ return;
463
+ }
448
464
  const recentRaw = this.outputBuffer.slice(-2e3);
449
465
  const recentOutput = this.stripAnsiForStall(recentRaw);
450
466
  const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
@@ -481,10 +497,12 @@ var PTYSession = class extends EventEmitter {
481
497
  * word boundaries — e.g. "Do\x1b[5Cyou" becomes "Do you", not "Doyou".
482
498
  */
483
499
  stripAnsiForStall(str) {
484
- let result = str.replace(/\x1b\[\d*[CDABG]/g, " ");
500
+ let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
485
501
  result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
502
+ result = result.replace(/\x1b\[\d*[JK]/g, " ");
486
503
  result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
487
- result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓]/g, " ");
504
+ result = result.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
505
+ result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓⬆⬇◆◇▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥]/g, " ");
488
506
  result = result.replace(/ {2,}/g, " ");
489
507
  return result;
490
508
  }
@@ -542,6 +560,39 @@ var PTYSession = class extends EventEmitter {
542
560
  }
543
561
  }
544
562
  // ─────────────────────────────────────────────────────────────────────────────
563
+ // Task Completion Detection
564
+ // ─────────────────────────────────────────────────────────────────────────────
565
+ /**
566
+ * Schedule a task_complete transition after a debounce period.
567
+ * If new non-whitespace output arrives before the timer fires,
568
+ * the timer is cancelled (by cancelTaskComplete in onData).
569
+ */
570
+ scheduleTaskComplete() {
571
+ if (this._taskCompleteTimer) return;
572
+ this._taskCompleteTimer = setTimeout(() => {
573
+ this._taskCompleteTimer = null;
574
+ if (this._status !== "busy") return;
575
+ if (!this.adapter.detectReady(this.outputBuffer)) return;
576
+ this._status = "ready";
577
+ this._lastBlockingPromptHash = null;
578
+ this.outputBuffer = "";
579
+ this.clearStallTimer();
580
+ this.emit("status_changed", "ready");
581
+ this.emit("task_complete");
582
+ this.logger.info({ sessionId: this.id }, "Task complete \u2014 agent returned to idle prompt");
583
+ }, _PTYSession.TASK_COMPLETE_DEBOUNCE_MS);
584
+ }
585
+ /**
586
+ * Cancel a pending task_complete timer (new output arrived that
587
+ * doesn't match the idle prompt, so the agent is still working).
588
+ */
589
+ cancelTaskComplete() {
590
+ if (this._taskCompleteTimer) {
591
+ clearTimeout(this._taskCompleteTimer);
592
+ this._taskCompleteTimer = null;
593
+ }
594
+ }
595
+ // ─────────────────────────────────────────────────────────────────────────────
545
596
  // Lifecycle
546
597
  // ─────────────────────────────────────────────────────────────────────────────
547
598
  /**
@@ -603,10 +654,6 @@ var PTYSession = class extends EventEmitter {
603
654
  this.resetStallTimer();
604
655
  }
605
656
  this.emit("output", data);
606
- const blockingPrompt = this.detectAndHandleBlockingPrompt();
607
- if (blockingPrompt) {
608
- return;
609
- }
610
657
  if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
611
658
  this._status = "ready";
612
659
  this._lastBlockingPromptHash = null;
@@ -616,6 +663,15 @@ var PTYSession = class extends EventEmitter {
616
663
  this.logger.info({ sessionId: this.id }, "Session ready");
617
664
  return;
618
665
  }
666
+ if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
667
+ this.scheduleTaskComplete();
668
+ } else {
669
+ this.cancelTaskComplete();
670
+ }
671
+ const blockingPrompt = this.detectAndHandleBlockingPrompt();
672
+ if (blockingPrompt) {
673
+ return;
674
+ }
619
675
  if (this._status !== "ready" && this._status !== "busy") {
620
676
  const loginDetection = this.adapter.detectLogin(this.outputBuffer);
621
677
  if (loginDetection.required && this._status !== "authenticating") {
@@ -838,6 +894,7 @@ var PTYSession = class extends EventEmitter {
838
894
  */
839
895
  send(message) {
840
896
  this._status = "busy";
897
+ this.emit("status_changed", "busy");
841
898
  this.resetStallTimer();
842
899
  const msg = {
843
900
  id: `${this.id}-msg-${++this.messageCounter}`,
@@ -948,6 +1005,7 @@ var PTYSession = class extends EventEmitter {
948
1005
  if (this.ptyProcess) {
949
1006
  this._status = "stopping";
950
1007
  this.clearStallTimer();
1008
+ this.cancelTaskComplete();
951
1009
  this.ptyProcess.kill(signal);
952
1010
  this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
953
1011
  }
@@ -1101,6 +1159,12 @@ var PTYManager = class extends EventEmitter2 {
1101
1159
  session.on("error", (error) => {
1102
1160
  this.emit("session_error", session.toHandle(), error.message);
1103
1161
  });
1162
+ session.on("status_changed", () => {
1163
+ this.emit("session_status_changed", session.toHandle());
1164
+ });
1165
+ session.on("task_complete", () => {
1166
+ this.emit("task_complete", session.toHandle());
1167
+ });
1104
1168
  session.on("stall_detected", (recentOutput, stallDurationMs) => {
1105
1169
  const handle = session.toHandle();
1106
1170
  this.emit("stall_detected", handle, recentOutput, stallDurationMs);
@@ -1505,6 +1569,14 @@ var BaseCLIAdapter = class {
1505
1569
  }
1506
1570
  return { detected: false };
1507
1571
  }
1572
+ /**
1573
+ * Default task completion detection — delegates to detectReady().
1574
+ * Subclasses should override to match high-confidence completion patterns
1575
+ * (e.g. duration summaries) that short-circuit the LLM stall classifier.
1576
+ */
1577
+ detectTaskComplete(output) {
1578
+ return this.detectReady(output);
1579
+ }
1508
1580
  /**
1509
1581
  * Default input formatting - just return as-is
1510
1582
  */
@@ -1954,6 +2026,24 @@ var BunCompatiblePTYManager = class extends EventEmitter3 {
1954
2026
  }
1955
2027
  break;
1956
2028
  }
2029
+ case "status_changed": {
2030
+ const session = this.sessions.get(id);
2031
+ if (session) {
2032
+ session.status = event.status;
2033
+ session.lastActivityAt = /* @__PURE__ */ new Date();
2034
+ this.emit("session_status_changed", session);
2035
+ }
2036
+ break;
2037
+ }
2038
+ case "task_complete": {
2039
+ const session = this.sessions.get(id);
2040
+ if (session) {
2041
+ session.status = "ready";
2042
+ session.lastActivityAt = /* @__PURE__ */ new Date();
2043
+ this.emit("task_complete", session);
2044
+ }
2045
+ break;
2046
+ }
1957
2047
  case "stall_detected": {
1958
2048
  const session = this.sessions.get(id);
1959
2049
  if (session) {