pty-manager 1.3.1 → 1.3.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 CHANGED
@@ -445,7 +445,9 @@ await session.selectMenuOption(2); // Sends Down, Down, Enter with 50ms delays
445
445
 
446
446
  ## Stall Detection & Task Completion
447
447
 
448
- Content-based stall detection monitors sessions for output that stops changing. The content hash normalizes ANSI escape codes, TUI spinner characters, and countdown/duration text (e.g. `8m 17s` → constant) so that live timers don't perpetually reset the stall timer. 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.
448
+ 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.
449
+
450
+ 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.
449
451
 
450
452
  If the adapter doesn't recognize the output, the session falls back to emitting `stall_detected` for external classification.
451
453
 
package/dist/index.d.mts CHANGED
@@ -446,6 +446,8 @@ declare class PTYSession extends EventEmitter {
446
446
  private _lastContentHash;
447
447
  private _taskCompleteTimer;
448
448
  private static readonly TASK_COMPLETE_DEBOUNCE_MS;
449
+ private _processScheduled;
450
+ private static readonly MAX_OUTPUT_BUFFER;
449
451
  readonly id: string;
450
452
  readonly config: SpawnConfig;
451
453
  constructor(adapter: CLIAdapter, config: SpawnConfig, logger?: Logger, stallDetectionEnabled?: boolean, defaultStallTimeoutMs?: number);
@@ -533,6 +535,12 @@ declare class PTYSession extends EventEmitter {
533
535
  * Set up event handlers for the PTY
534
536
  */
535
537
  private setupEventHandlers;
538
+ /**
539
+ * Process the accumulated output buffer.
540
+ * Called via setImmediate() from the onData handler so that heavy regex
541
+ * work runs in its own event-loop tick, not inside node-pty's native callback.
542
+ */
543
+ private processOutputBuffer;
536
544
  /**
537
545
  * Detect blocking prompts and handle them with auto-responses or user notification.
538
546
  * Deduplicates emissions - won't re-emit the same blocking prompt repeatedly.
package/dist/index.d.ts CHANGED
@@ -446,6 +446,8 @@ declare class PTYSession extends EventEmitter {
446
446
  private _lastContentHash;
447
447
  private _taskCompleteTimer;
448
448
  private static readonly TASK_COMPLETE_DEBOUNCE_MS;
449
+ private _processScheduled;
450
+ private static readonly MAX_OUTPUT_BUFFER;
449
451
  readonly id: string;
450
452
  readonly config: SpawnConfig;
451
453
  constructor(adapter: CLIAdapter, config: SpawnConfig, logger?: Logger, stallDetectionEnabled?: boolean, defaultStallTimeoutMs?: number);
@@ -533,6 +535,12 @@ declare class PTYSession extends EventEmitter {
533
535
  * Set up event handlers for the PTY
534
536
  */
535
537
  private setupEventHandlers;
538
+ /**
539
+ * Process the accumulated output buffer.
540
+ * Called via setImmediate() from the onData handler so that heavy regex
541
+ * work runs in its own event-loop tick, not inside node-pty's native callback.
542
+ */
543
+ private processOutputBuffer;
536
544
  /**
537
545
  * Detect blocking prompts and handle them with auto-responses or user notification.
538
546
  * Deduplicates emissions - won't re-emit the same blocking prompt repeatedly.
package/dist/index.js CHANGED
@@ -347,6 +347,12 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
347
347
  // Task completion detection (idle detection when busy)
348
348
  _taskCompleteTimer = null;
349
349
  static TASK_COMPLETE_DEBOUNCE_MS = 1500;
350
+ // Deferred output processing — prevents node-pty's synchronous data
351
+ // delivery from starving the event loop (timers, I/O callbacks, etc.)
352
+ _processScheduled = false;
353
+ // Output buffer cap — prevents unbounded growth during long tasks
354
+ static MAX_OUTPUT_BUFFER = 1e5;
355
+ // 100 KB
350
356
  id;
351
357
  config;
352
358
  get status() {
@@ -444,9 +450,9 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
444
450
  this.clearStallTimer();
445
451
  return;
446
452
  }
447
- const tail = this.outputBuffer.slice(-500);
448
- const stripped = this.stripAnsiForStall(tail).trim();
449
- const hash = this.simpleHash(stripped);
453
+ const stripped = this.stripAnsiForStall(this.outputBuffer).trim();
454
+ const tail = stripped.slice(-500);
455
+ const hash = this.simpleHash(tail);
450
456
  if (hash === this._lastContentHash) {
451
457
  return;
452
458
  }
@@ -689,49 +695,16 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
689
695
  this.ptyProcess.onData((data) => {
690
696
  this._lastActivityAt = /* @__PURE__ */ new Date();
691
697
  this.outputBuffer += data;
692
- if (this._status === "busy" || this._status === "authenticating") {
693
- this.resetStallTimer();
698
+ if (this.outputBuffer.length > _PTYSession.MAX_OUTPUT_BUFFER) {
699
+ this.outputBuffer = this.outputBuffer.slice(-_PTYSession.MAX_OUTPUT_BUFFER);
694
700
  }
695
701
  this.emit("output", data);
696
- if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
697
- this._status = "ready";
698
- this._lastBlockingPromptHash = null;
699
- this.outputBuffer = "";
700
- this.clearStallTimer();
701
- this.emit("ready");
702
- this.logger.info({ sessionId: this.id }, "Session ready");
703
- return;
704
- }
705
- if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
706
- this.scheduleTaskComplete();
707
- } else {
708
- this.cancelTaskComplete();
709
- }
710
- const blockingPrompt = this.detectAndHandleBlockingPrompt();
711
- if (blockingPrompt) {
712
- return;
713
- }
714
- if (this._status !== "ready" && this._status !== "busy") {
715
- const loginDetection = this.adapter.detectLogin(this.outputBuffer);
716
- if (loginDetection.required && this._status !== "authenticating") {
717
- this._status = "authenticating";
718
- this.clearStallTimer();
719
- this.emit("login_required", loginDetection.instructions, loginDetection.url);
720
- this.logger.warn(
721
- { sessionId: this.id, loginType: loginDetection.type },
722
- "Login required"
723
- );
724
- return;
725
- }
726
- }
727
- const exitDetection = this.adapter.detectExit(this.outputBuffer);
728
- if (exitDetection.exited) {
729
- this._status = "stopped";
730
- this.clearStallTimer();
731
- this.emit("exit", exitDetection.code || 0);
732
- }
733
- if (this._status !== "starting" && this._status !== "authenticating") {
734
- this.tryParseOutput();
702
+ if (!this._processScheduled) {
703
+ this._processScheduled = true;
704
+ setImmediate(() => {
705
+ this._processScheduled = false;
706
+ this.processOutputBuffer();
707
+ });
735
708
  }
736
709
  });
737
710
  this.ptyProcess.onExit(({ exitCode, signal }) => {
@@ -744,6 +717,56 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
744
717
  this.emit("exit", exitCode);
745
718
  });
746
719
  }
720
+ /**
721
+ * Process the accumulated output buffer.
722
+ * Called via setImmediate() from the onData handler so that heavy regex
723
+ * work runs in its own event-loop tick, not inside node-pty's native callback.
724
+ */
725
+ processOutputBuffer() {
726
+ if (this._status === "busy" || this._status === "authenticating") {
727
+ this.resetStallTimer();
728
+ }
729
+ if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
730
+ this._status = "ready";
731
+ this._lastBlockingPromptHash = null;
732
+ this.outputBuffer = "";
733
+ this.clearStallTimer();
734
+ this.emit("ready");
735
+ this.logger.info({ sessionId: this.id }, "Session ready");
736
+ return;
737
+ }
738
+ if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
739
+ this.scheduleTaskComplete();
740
+ } else {
741
+ this.cancelTaskComplete();
742
+ }
743
+ const blockingPrompt = this.detectAndHandleBlockingPrompt();
744
+ if (blockingPrompt) {
745
+ return;
746
+ }
747
+ if (this._status !== "ready" && this._status !== "busy") {
748
+ const loginDetection = this.adapter.detectLogin(this.outputBuffer);
749
+ if (loginDetection.required && this._status !== "authenticating") {
750
+ this._status = "authenticating";
751
+ this.clearStallTimer();
752
+ this.emit("login_required", loginDetection.instructions, loginDetection.url);
753
+ this.logger.warn(
754
+ { sessionId: this.id, loginType: loginDetection.type },
755
+ "Login required"
756
+ );
757
+ return;
758
+ }
759
+ }
760
+ const exitDetection = this.adapter.detectExit(this.outputBuffer);
761
+ if (exitDetection.exited) {
762
+ this._status = "stopped";
763
+ this.clearStallTimer();
764
+ this.emit("exit", exitDetection.code || 0);
765
+ }
766
+ if (this._status !== "starting" && this._status !== "authenticating") {
767
+ this.tryParseOutput();
768
+ }
769
+ }
747
770
  /**
748
771
  * Detect blocking prompts and handle them with auto-responses or user notification.
749
772
  * Deduplicates emissions - won't re-emit the same blocking prompt repeatedly.