pty-manager 1.6.0 → 1.6.2

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 CHANGED
@@ -10,7 +10,7 @@ PTY session manager with lifecycle management, pluggable adapters, and blocking
10
10
  - **Auto-response rules** - Automatically respond to known prompts with text or key sequences
11
11
  - **TUI menu navigation** - Navigate arrow-key menus via `selectMenuOption()` and key-sequence rules
12
12
  - **Stall detection** - Content-based stall detection with pluggable external classifiers, loading suppression, and exponential backoff
13
- - **Task completion detection** - Adapter-level fast-path that short-circuits the LLM stall classifier when the CLI returns to its idle prompt
13
+ - **Task completion detection** - Settle-based fast-path that short-circuits the LLM stall classifier when the CLI returns to its idle prompt, resilient to decorative TUI rendering after the prompt
14
14
  - **Terminal attachment** - Attach to sessions for raw I/O streaming
15
15
  - **Special key support** - Send Ctrl, Alt, Shift, and function key combinations via `sendKeys()`
16
16
  - **Bracketed paste** - Proper paste handling with bracketed paste mode support
@@ -475,11 +475,15 @@ const handle = await manager.spawn({
475
475
 
476
476
  ## Stall Detection & Task Completion
477
477
 
478
- Content-based stall detection monitors sessions for output that stops changing. The content hash strips the full buffer first, then slices the last 500 characters of the normalized text — this ensures identical visual content always produces the same hash regardless of how many raw escape sequences surround it. The normalization strips ANSI escape codes, TUI spinner characters, and countdown/duration text (e.g. `8m 17s` → constant) so that live timers and TUI redraws don't perpetually reset the stall timer. All detection work (ready, blocking prompt, login, exit, stall) runs in a deferred `setImmediate()` tick so that node-pty's synchronous data delivery cannot starve the event loop — timers and I/O callbacks always get a chance to run between data bursts. The output buffer is capped at 100 KB to prevent unbounded growth during long tasks.
478
+ Content-based stall detection monitors sessions for output that stops changing. The content hash strips the full buffer first, then slices the last 500 characters of the normalized text — this ensures identical visual content always produces the same hash regardless of how many raw escape sequences surround it. The normalization strips ANSI escape codes, carriage returns (`\r`), non-breaking spaces (`\xa0`), TUI spinner characters, and countdown/duration text (e.g. `8m 17s` → constant) so that live timers, TUI line-overwrites, and redraws don't perpetually reset the stall timer. All detection work (ready, blocking prompt, login, exit, stall) runs in a deferred `setImmediate()` tick so that node-pty's synchronous data delivery cannot starve the event loop — timers and I/O callbacks always get a chance to run between data bursts. The output buffer is capped at 100 KB to prevent unbounded growth during long tasks.
479
479
 
480
- When a stall is detected, the session first tries the adapter's `detectTaskComplete()` fast-path. If the adapter recognizes the output as a completed task (e.g. duration summary + idle prompt), it transitions directly to `ready` and emits `task_complete` — skipping the expensive LLM stall classifier entirely.
480
+ ### Task Completion Fast-Path (Settle Pattern)
481
481
 
482
- If the adapter doesn't recognize the output, the session falls back to emitting `stall_detected` for external classification.
482
+ When a busy session's output matches the adapter's `detectReady()` pattern, a `task_complete` debounce timer starts. TUI agents like Claude Code continue rendering decorative output after the prompt — update notices, shortcut hints, status bar updates. Instead of cancelling the timer on each new data chunk (which would prevent the event from ever firing), the session uses a **settle pattern**: the debounce timer resets on each new chunk but is never cancelled. The timer callback re-verifies `detectReady()` before transitioning, so stale triggers are safe.
483
+
484
+ This mirrors the `readySettlePending` pattern used for startup ready detection, and ensures the `task_complete` event fires reliably even when TUI chrome continues rendering after the agent has finished its task.
485
+
486
+ If the fast-path timer doesn't fire (e.g. the prompt indicator disappears from the buffer), the session falls back to stall detection which emits `stall_detected` for external classification.
483
487
 
484
488
  ```typescript
485
489
  // Enable stall detection with a pluggable classifier
package/dist/index.d.mts CHANGED
@@ -461,6 +461,7 @@ declare class PTYSession extends EventEmitter {
461
461
  private _stallBackoffMs;
462
462
  private static readonly MAX_STALL_BACKOFF_MS;
463
463
  private _taskCompleteTimer;
464
+ private _taskCompletePending;
464
465
  private static readonly TASK_COMPLETE_DEBOUNCE_MS;
465
466
  private _readySettleTimer;
466
467
  private _readySettlePending;
@@ -536,8 +537,12 @@ declare class PTYSession extends EventEmitter {
536
537
  handleStallClassification(classification: StallClassification | null): void;
537
538
  /**
538
539
  * Schedule a task_complete transition after a debounce period.
539
- * If new non-whitespace output arrives before the timer fires,
540
- * the timer is cancelled (by cancelTaskComplete in onData).
540
+ * Uses a settle pattern: each call resets the debounce timer instead of
541
+ * being a no-op when already scheduled. This allows TUI agents that
542
+ * continue rendering decorative output (status bar, update notices) after
543
+ * the prompt to eventually settle, rather than having the timer cancelled
544
+ * by every new data chunk. The callback re-verifies detectReady() before
545
+ * transitioning, so stale triggers are safe.
541
546
  */
542
547
  private scheduleTaskComplete;
543
548
  /**
package/dist/index.d.ts CHANGED
@@ -461,6 +461,7 @@ declare class PTYSession extends EventEmitter {
461
461
  private _stallBackoffMs;
462
462
  private static readonly MAX_STALL_BACKOFF_MS;
463
463
  private _taskCompleteTimer;
464
+ private _taskCompletePending;
464
465
  private static readonly TASK_COMPLETE_DEBOUNCE_MS;
465
466
  private _readySettleTimer;
466
467
  private _readySettlePending;
@@ -536,8 +537,12 @@ declare class PTYSession extends EventEmitter {
536
537
  handleStallClassification(classification: StallClassification | null): void;
537
538
  /**
538
539
  * Schedule a task_complete transition after a debounce period.
539
- * If new non-whitespace output arrives before the timer fires,
540
- * the timer is cancelled (by cancelTaskComplete in onData).
540
+ * Uses a settle pattern: each call resets the debounce timer instead of
541
+ * being a no-op when already scheduled. This allows TUI agents that
542
+ * continue rendering decorative output (status bar, update notices) after
543
+ * the prompt to eventually settle, rather than having the timer cancelled
544
+ * by every new data chunk. The callback re-verifies detectReady() before
545
+ * transitioning, so stale triggers are safe.
541
546
  */
542
547
  private scheduleTaskComplete;
543
548
  /**
package/dist/index.js CHANGED
@@ -350,6 +350,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
350
350
  static MAX_STALL_BACKOFF_MS = 3e4;
351
351
  // Task completion detection (idle detection when busy)
352
352
  _taskCompleteTimer = null;
353
+ _taskCompletePending = false;
353
354
  static TASK_COMPLETE_DEBOUNCE_MS = 1500;
354
355
  // Ready detection settle delay — defers session_ready until output goes quiet
355
356
  _readySettleTimer = null;
@@ -562,7 +563,8 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
562
563
  result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
563
564
  result = result.replace(/\x1b\[\d*[JK]/g, " ");
564
565
  result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
565
- result = result.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
566
+ result = result.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
567
+ result = result.replace(/\xa0/g, " ");
566
568
  result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓⬆⬇◆◇▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥]/g, " ");
567
569
  result = result.replace(/\d+[hms](?:\s+\d+[hms])*/g, "0s");
568
570
  result = result.replace(/ {2,}/g, " ");
@@ -639,13 +641,21 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
639
641
  // ─────────────────────────────────────────────────────────────────────────────
640
642
  /**
641
643
  * Schedule a task_complete transition after a debounce period.
642
- * If new non-whitespace output arrives before the timer fires,
643
- * the timer is cancelled (by cancelTaskComplete in onData).
644
+ * Uses a settle pattern: each call resets the debounce timer instead of
645
+ * being a no-op when already scheduled. This allows TUI agents that
646
+ * continue rendering decorative output (status bar, update notices) after
647
+ * the prompt to eventually settle, rather than having the timer cancelled
648
+ * by every new data chunk. The callback re-verifies detectReady() before
649
+ * transitioning, so stale triggers are safe.
644
650
  */
645
651
  scheduleTaskComplete() {
646
- if (this._taskCompleteTimer) return;
652
+ if (this._taskCompleteTimer) {
653
+ clearTimeout(this._taskCompleteTimer);
654
+ }
655
+ this._taskCompletePending = true;
647
656
  this._taskCompleteTimer = setTimeout(() => {
648
657
  this._taskCompleteTimer = null;
658
+ this._taskCompletePending = false;
649
659
  if (this._status !== "busy") return;
650
660
  if (!this.adapter.detectReady(this.outputBuffer)) return;
651
661
  this._status = "ready";
@@ -666,6 +676,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
666
676
  clearTimeout(this._taskCompleteTimer);
667
677
  this._taskCompleteTimer = null;
668
678
  }
679
+ this._taskCompletePending = false;
669
680
  }
670
681
  // ─────────────────────────────────────────────────────────────────────────────
671
682
  // Ready Detection Settle Delay
@@ -807,10 +818,10 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
807
818
  this.scheduleReadySettle();
808
819
  return;
809
820
  }
810
- if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
811
- this.scheduleTaskComplete();
812
- } else {
813
- this.cancelTaskComplete();
821
+ if (this._status === "busy") {
822
+ if (this._taskCompletePending || this.adapter.detectReady(this.outputBuffer)) {
823
+ this.scheduleTaskComplete();
824
+ }
814
825
  }
815
826
  const blockingPrompt = this.detectAndHandleBlockingPrompt();
816
827
  if (blockingPrompt) {
@@ -1028,6 +1039,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
1028
1039
  */
1029
1040
  send(message) {
1030
1041
  this._status = "busy";
1042
+ this.outputBuffer = "";
1031
1043
  this.emit("status_changed", "busy");
1032
1044
  this.resetStallTimer();
1033
1045
  const msg = {