pty-manager 1.6.2 → 1.6.4

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.
@@ -331,6 +331,8 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
331
331
  _stallBackoffMs = 0;
332
332
  // Initialized in constructor from _stallTimeoutMs
333
333
  static MAX_STALL_BACKOFF_MS = 3e4;
334
+ _stallEmissionCount = 0;
335
+ static MAX_STALL_EMISSIONS = 5;
334
336
  // Task completion detection (idle detection when busy)
335
337
  _taskCompleteTimer = null;
336
338
  _taskCompletePending = false;
@@ -448,6 +450,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
448
450
  return;
449
451
  }
450
452
  this._lastContentHash = hash;
453
+ this._stallEmissionCount = 0;
451
454
  if (this._stallTimer) {
452
455
  clearTimeout(this._stallTimer);
453
456
  this._stallTimer = null;
@@ -470,6 +473,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
470
473
  this._stallStartedAt = null;
471
474
  this._lastContentHash = null;
472
475
  this._stallBackoffMs = this._stallTimeoutMs;
476
+ this._stallEmissionCount = 0;
473
477
  }
474
478
  /**
475
479
  * Called when the stall timer fires (no output for stallTimeoutMs).
@@ -506,6 +510,15 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
506
510
  );
507
511
  return;
508
512
  }
513
+ this._stallEmissionCount++;
514
+ if (this._stallEmissionCount > _PTYSession.MAX_STALL_EMISSIONS) {
515
+ this.logger.warn(
516
+ { sessionId: this.id, count: this._stallEmissionCount },
517
+ "Max stall emissions reached \u2014 suspending stall detection for this task"
518
+ );
519
+ this.clearStallTimer();
520
+ return;
521
+ }
509
522
  const recentRaw = this.outputBuffer.slice(-2e3);
510
523
  const recentOutput = this.stripAnsiForStall(recentRaw);
511
524
  const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
@@ -545,6 +558,8 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
545
558
  let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
546
559
  result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
547
560
  result = result.replace(/\x1b\[\d*[JK]/g, " ");
561
+ result = result.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*(?:\x07|\x1b\\)/g, "");
562
+ result = result.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "");
548
563
  result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
549
564
  result = result.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
550
565
  result = result.replace(/\xa0/g, " ");
@@ -628,10 +643,15 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
628
643
  * being a no-op when already scheduled. This allows TUI agents that
629
644
  * continue rendering decorative output (status bar, update notices) after
630
645
  * the prompt to eventually settle, rather than having the timer cancelled
631
- * by every new data chunk. The callback re-verifies detectReady() before
632
- * transitioning, so stale triggers are safe.
646
+ * by every new data chunk. The callback re-verifies the task-complete
647
+ * signal before transitioning, so stale triggers are safe.
633
648
  */
634
649
  scheduleTaskComplete() {
650
+ const wasPending = this._taskCompletePending;
651
+ this.traceTaskCompletion("debounce_schedule", {
652
+ wasPending,
653
+ debounceMs: _PTYSession.TASK_COMPLETE_DEBOUNCE_MS
654
+ });
635
655
  if (this._taskCompleteTimer) {
636
656
  clearTimeout(this._taskCompleteTimer);
637
657
  }
@@ -639,17 +659,72 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
639
659
  this._taskCompleteTimer = setTimeout(() => {
640
660
  this._taskCompleteTimer = null;
641
661
  this._taskCompletePending = false;
642
- if (this._status !== "busy") return;
643
- if (!this.adapter.detectReady(this.outputBuffer)) return;
662
+ const signal = this.isTaskCompleteSignal(this.outputBuffer);
663
+ this.traceTaskCompletion("debounce_fire", { signal });
664
+ if (this._status !== "busy") {
665
+ this.traceTaskCompletion("debounce_reject_status", { signal });
666
+ return;
667
+ }
668
+ if (!signal) {
669
+ this.traceTaskCompletion("debounce_reject_signal", { signal });
670
+ return;
671
+ }
644
672
  this._status = "ready";
645
673
  this._lastBlockingPromptHash = null;
646
674
  this.outputBuffer = "";
647
675
  this.clearStallTimer();
648
676
  this.emit("status_changed", "ready");
649
677
  this.emit("task_complete");
678
+ this.traceTaskCompletion("transition_ready", { signal: true });
650
679
  this.logger.info({ sessionId: this.id }, "Task complete \u2014 agent returned to idle prompt");
651
680
  }, _PTYSession.TASK_COMPLETE_DEBOUNCE_MS);
652
681
  }
682
+ /**
683
+ * Adapter-level task completion check with compatibility fallback.
684
+ * Prefer detectTaskComplete() because detectReady() may be broad for TUIs.
685
+ */
686
+ isTaskCompleteSignal(output) {
687
+ if (this.adapter.detectTaskComplete) {
688
+ return this.adapter.detectTaskComplete(output);
689
+ }
690
+ return this.adapter.detectReady(output);
691
+ }
692
+ /**
693
+ * Claude-oriented task completion traces for PTY debugging.
694
+ * Enabled by config.traceTaskCompletion, otherwise defaults to enabled for Claude.
695
+ */
696
+ traceTaskCompletion(event, ctx = {}) {
697
+ if (!this.shouldTraceTaskCompletion()) return;
698
+ const output = this.outputBuffer;
699
+ const detectTaskComplete = this.adapter.detectTaskComplete ? this.adapter.detectTaskComplete(output) : void 0;
700
+ const detectReady = this.adapter.detectReady(output);
701
+ const detectLoading = this.adapter.detectLoading ? this.adapter.detectLoading(output) : void 0;
702
+ const normalizedTail = this.stripAnsiForStall(output.slice(-280));
703
+ this.logger.debug(
704
+ {
705
+ sessionId: this.id,
706
+ adapterType: this.adapter.adapterType,
707
+ event,
708
+ status: this._status,
709
+ taskCompletePending: this._taskCompletePending,
710
+ signal: ctx.signal,
711
+ wasPending: ctx.wasPending,
712
+ debounceMs: ctx.debounceMs,
713
+ detectTaskComplete,
714
+ detectReady,
715
+ detectLoading,
716
+ tailHash: this.simpleHash(normalizedTail),
717
+ tailSnippet: normalizedTail.slice(-140)
718
+ },
719
+ "Task completion trace"
720
+ );
721
+ }
722
+ shouldTraceTaskCompletion() {
723
+ if (typeof this.config.traceTaskCompletion === "boolean") {
724
+ return this.config.traceTaskCompletion;
725
+ }
726
+ return this.adapter.adapterType === "claude";
727
+ }
653
728
  /**
654
729
  * Cancel a pending task_complete timer (new output arrived that
655
730
  * doesn't match the idle prompt, so the agent is still working).
@@ -802,7 +877,9 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
802
877
  return;
803
878
  }
804
879
  if (this._status === "busy") {
805
- if (this._taskCompletePending || this.adapter.detectReady(this.outputBuffer)) {
880
+ const signal = this.isTaskCompleteSignal(this.outputBuffer);
881
+ if (this._taskCompletePending || signal) {
882
+ this.traceTaskCompletion("busy_signal", { signal });
806
883
  this.scheduleTaskComplete();
807
884
  }
808
885
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pty-manager",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "PTY session manager with lifecycle management, pluggable adapters, and blocking prompt detection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",