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.
package/dist/index.mjs CHANGED
@@ -310,6 +310,8 @@ var PTYSession = class _PTYSession extends EventEmitter {
310
310
  _stallBackoffMs = 0;
311
311
  // Initialized in constructor from _stallTimeoutMs
312
312
  static MAX_STALL_BACKOFF_MS = 3e4;
313
+ _stallEmissionCount = 0;
314
+ static MAX_STALL_EMISSIONS = 5;
313
315
  // Task completion detection (idle detection when busy)
314
316
  _taskCompleteTimer = null;
315
317
  _taskCompletePending = false;
@@ -427,6 +429,7 @@ var PTYSession = class _PTYSession extends EventEmitter {
427
429
  return;
428
430
  }
429
431
  this._lastContentHash = hash;
432
+ this._stallEmissionCount = 0;
430
433
  if (this._stallTimer) {
431
434
  clearTimeout(this._stallTimer);
432
435
  this._stallTimer = null;
@@ -449,6 +452,7 @@ var PTYSession = class _PTYSession extends EventEmitter {
449
452
  this._stallStartedAt = null;
450
453
  this._lastContentHash = null;
451
454
  this._stallBackoffMs = this._stallTimeoutMs;
455
+ this._stallEmissionCount = 0;
452
456
  }
453
457
  /**
454
458
  * Called when the stall timer fires (no output for stallTimeoutMs).
@@ -485,6 +489,15 @@ var PTYSession = class _PTYSession extends EventEmitter {
485
489
  );
486
490
  return;
487
491
  }
492
+ this._stallEmissionCount++;
493
+ if (this._stallEmissionCount > _PTYSession.MAX_STALL_EMISSIONS) {
494
+ this.logger.warn(
495
+ { sessionId: this.id, count: this._stallEmissionCount },
496
+ "Max stall emissions reached \u2014 suspending stall detection for this task"
497
+ );
498
+ this.clearStallTimer();
499
+ return;
500
+ }
488
501
  const recentRaw = this.outputBuffer.slice(-2e3);
489
502
  const recentOutput = this.stripAnsiForStall(recentRaw);
490
503
  const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
@@ -524,6 +537,8 @@ var PTYSession = class _PTYSession extends EventEmitter {
524
537
  let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
525
538
  result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
526
539
  result = result.replace(/\x1b\[\d*[JK]/g, " ");
540
+ result = result.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*(?:\x07|\x1b\\)/g, "");
541
+ result = result.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "");
527
542
  result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
528
543
  result = result.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
529
544
  result = result.replace(/\xa0/g, " ");
@@ -607,10 +622,15 @@ var PTYSession = class _PTYSession extends EventEmitter {
607
622
  * being a no-op when already scheduled. This allows TUI agents that
608
623
  * continue rendering decorative output (status bar, update notices) after
609
624
  * the prompt to eventually settle, rather than having the timer cancelled
610
- * by every new data chunk. The callback re-verifies detectReady() before
611
- * transitioning, so stale triggers are safe.
625
+ * by every new data chunk. The callback re-verifies the task-complete
626
+ * signal before transitioning, so stale triggers are safe.
612
627
  */
613
628
  scheduleTaskComplete() {
629
+ const wasPending = this._taskCompletePending;
630
+ this.traceTaskCompletion("debounce_schedule", {
631
+ wasPending,
632
+ debounceMs: _PTYSession.TASK_COMPLETE_DEBOUNCE_MS
633
+ });
614
634
  if (this._taskCompleteTimer) {
615
635
  clearTimeout(this._taskCompleteTimer);
616
636
  }
@@ -618,17 +638,72 @@ var PTYSession = class _PTYSession extends EventEmitter {
618
638
  this._taskCompleteTimer = setTimeout(() => {
619
639
  this._taskCompleteTimer = null;
620
640
  this._taskCompletePending = false;
621
- if (this._status !== "busy") return;
622
- if (!this.adapter.detectReady(this.outputBuffer)) return;
641
+ const signal = this.isTaskCompleteSignal(this.outputBuffer);
642
+ this.traceTaskCompletion("debounce_fire", { signal });
643
+ if (this._status !== "busy") {
644
+ this.traceTaskCompletion("debounce_reject_status", { signal });
645
+ return;
646
+ }
647
+ if (!signal) {
648
+ this.traceTaskCompletion("debounce_reject_signal", { signal });
649
+ return;
650
+ }
623
651
  this._status = "ready";
624
652
  this._lastBlockingPromptHash = null;
625
653
  this.outputBuffer = "";
626
654
  this.clearStallTimer();
627
655
  this.emit("status_changed", "ready");
628
656
  this.emit("task_complete");
657
+ this.traceTaskCompletion("transition_ready", { signal: true });
629
658
  this.logger.info({ sessionId: this.id }, "Task complete \u2014 agent returned to idle prompt");
630
659
  }, _PTYSession.TASK_COMPLETE_DEBOUNCE_MS);
631
660
  }
661
+ /**
662
+ * Adapter-level task completion check with compatibility fallback.
663
+ * Prefer detectTaskComplete() because detectReady() may be broad for TUIs.
664
+ */
665
+ isTaskCompleteSignal(output) {
666
+ if (this.adapter.detectTaskComplete) {
667
+ return this.adapter.detectTaskComplete(output);
668
+ }
669
+ return this.adapter.detectReady(output);
670
+ }
671
+ /**
672
+ * Claude-oriented task completion traces for PTY debugging.
673
+ * Enabled by config.traceTaskCompletion, otherwise defaults to enabled for Claude.
674
+ */
675
+ traceTaskCompletion(event, ctx = {}) {
676
+ if (!this.shouldTraceTaskCompletion()) return;
677
+ const output = this.outputBuffer;
678
+ const detectTaskComplete = this.adapter.detectTaskComplete ? this.adapter.detectTaskComplete(output) : void 0;
679
+ const detectReady = this.adapter.detectReady(output);
680
+ const detectLoading = this.adapter.detectLoading ? this.adapter.detectLoading(output) : void 0;
681
+ const normalizedTail = this.stripAnsiForStall(output.slice(-280));
682
+ this.logger.debug(
683
+ {
684
+ sessionId: this.id,
685
+ adapterType: this.adapter.adapterType,
686
+ event,
687
+ status: this._status,
688
+ taskCompletePending: this._taskCompletePending,
689
+ signal: ctx.signal,
690
+ wasPending: ctx.wasPending,
691
+ debounceMs: ctx.debounceMs,
692
+ detectTaskComplete,
693
+ detectReady,
694
+ detectLoading,
695
+ tailHash: this.simpleHash(normalizedTail),
696
+ tailSnippet: normalizedTail.slice(-140)
697
+ },
698
+ "Task completion trace"
699
+ );
700
+ }
701
+ shouldTraceTaskCompletion() {
702
+ if (typeof this.config.traceTaskCompletion === "boolean") {
703
+ return this.config.traceTaskCompletion;
704
+ }
705
+ return this.adapter.adapterType === "claude";
706
+ }
632
707
  /**
633
708
  * Cancel a pending task_complete timer (new output arrived that
634
709
  * doesn't match the idle prompt, so the agent is still working).
@@ -781,7 +856,9 @@ var PTYSession = class _PTYSession extends EventEmitter {
781
856
  return;
782
857
  }
783
858
  if (this._status === "busy") {
784
- if (this._taskCompletePending || this.adapter.detectReady(this.outputBuffer)) {
859
+ const signal = this.isTaskCompleteSignal(this.outputBuffer);
860
+ if (this._taskCompletePending || signal) {
861
+ this.traceTaskCompletion("busy_signal", { signal });
785
862
  this.scheduleTaskComplete();
786
863
  }
787
864
  }
@@ -1532,6 +1609,171 @@ var PTYManager = class extends EventEmitter2 {
1532
1609
  }
1533
1610
  };
1534
1611
 
1612
+ // src/task-completion-trace.ts
1613
+ function extractTaskCompletionTraceRecords(entries) {
1614
+ const out = [];
1615
+ for (const entry of entries) {
1616
+ let obj = null;
1617
+ if (typeof entry === "string") {
1618
+ const line = entry.trim();
1619
+ if (!line.startsWith("{") || !line.endsWith("}")) continue;
1620
+ try {
1621
+ obj = JSON.parse(line);
1622
+ } catch {
1623
+ continue;
1624
+ }
1625
+ } else if (entry && typeof entry === "object") {
1626
+ obj = entry;
1627
+ }
1628
+ if (!obj) continue;
1629
+ if (obj.msg !== "Task completion trace") continue;
1630
+ if (typeof obj.event !== "string") continue;
1631
+ out.push({
1632
+ sessionId: asString(obj.sessionId),
1633
+ adapterType: asString(obj.adapterType),
1634
+ event: obj.event,
1635
+ status: asString(obj.status),
1636
+ taskCompletePending: asBool(obj.taskCompletePending),
1637
+ signal: asBool(obj.signal),
1638
+ wasPending: asBool(obj.wasPending),
1639
+ debounceMs: asNumber(obj.debounceMs),
1640
+ detectTaskComplete: asBool(obj.detectTaskComplete),
1641
+ detectReady: asBool(obj.detectReady),
1642
+ detectLoading: asBool(obj.detectLoading),
1643
+ tailHash: asString(obj.tailHash),
1644
+ tailSnippet: asString(obj.tailSnippet),
1645
+ timestamp: asTimestamp(obj.time) ?? asTimestamp(obj.timestamp)
1646
+ });
1647
+ }
1648
+ return out;
1649
+ }
1650
+ function buildTaskCompletionTimeline(records, options = {}) {
1651
+ const filtered = records.filter((r) => {
1652
+ if (!options.adapterType) return true;
1653
+ return r.adapterType === options.adapterType;
1654
+ });
1655
+ const turns = [];
1656
+ let current = null;
1657
+ let ignored = 0;
1658
+ filtered.forEach((record, index) => {
1659
+ if (record.event === "busy_signal" && current && current.completed) {
1660
+ current = null;
1661
+ }
1662
+ if (!current) {
1663
+ current = {
1664
+ turn: turns.length + 1,
1665
+ startIndex: index,
1666
+ endIndex: index,
1667
+ completed: false,
1668
+ maxConfidence: 0,
1669
+ finalConfidence: 0,
1670
+ events: []
1671
+ };
1672
+ turns.push(current);
1673
+ }
1674
+ const step = toStep(record, index);
1675
+ if (!step) {
1676
+ ignored++;
1677
+ return;
1678
+ }
1679
+ current.events.push(step);
1680
+ current.endIndex = index;
1681
+ current.maxConfidence = Math.max(current.maxConfidence, step.confidence);
1682
+ current.finalConfidence = step.confidence;
1683
+ if (step.status === "completed") {
1684
+ current.completed = true;
1685
+ }
1686
+ });
1687
+ return {
1688
+ turns,
1689
+ totalRecords: filtered.length,
1690
+ ignoredRecords: ignored
1691
+ };
1692
+ }
1693
+ function toStep(record, atIndex) {
1694
+ const event = record.event;
1695
+ const confidence = scoreConfidence(record);
1696
+ if (event === "transition_ready") {
1697
+ return withCommon(record, {
1698
+ event,
1699
+ atIndex,
1700
+ status: "completed",
1701
+ confidence: 100
1702
+ });
1703
+ }
1704
+ if (event === "debounce_reject_signal" || event === "debounce_reject_status") {
1705
+ return withCommon(record, {
1706
+ event,
1707
+ atIndex,
1708
+ status: "rejected",
1709
+ confidence
1710
+ });
1711
+ }
1712
+ if (record.detectLoading) {
1713
+ return withCommon(record, {
1714
+ event,
1715
+ atIndex,
1716
+ status: "active_loading",
1717
+ confidence
1718
+ });
1719
+ }
1720
+ if (event === "debounce_fire" && record.signal) {
1721
+ return withCommon(record, {
1722
+ event,
1723
+ atIndex,
1724
+ status: "likely_complete",
1725
+ confidence
1726
+ });
1727
+ }
1728
+ if (event === "busy_signal" || event === "debounce_schedule" || event === "debounce_fire") {
1729
+ return withCommon(record, {
1730
+ event,
1731
+ atIndex,
1732
+ status: "active",
1733
+ confidence
1734
+ });
1735
+ }
1736
+ return null;
1737
+ }
1738
+ function scoreConfidence(record) {
1739
+ let score = 10;
1740
+ if (record.detectLoading) score -= 40;
1741
+ if (record.detectReady) score += 20;
1742
+ if (record.detectTaskComplete) score += 45;
1743
+ if (record.signal) score += 20;
1744
+ if (record.event === "debounce_reject_signal" || record.event === "debounce_reject_status") {
1745
+ score -= 30;
1746
+ }
1747
+ if (record.event === "transition_ready") score = 100;
1748
+ if (score < 0) return 0;
1749
+ if (score > 100) return 100;
1750
+ return score;
1751
+ }
1752
+ function withCommon(record, step) {
1753
+ return {
1754
+ ...step,
1755
+ signal: record.signal,
1756
+ detectTaskComplete: record.detectTaskComplete,
1757
+ detectReady: record.detectReady,
1758
+ detectLoading: record.detectLoading
1759
+ };
1760
+ }
1761
+ function asString(value) {
1762
+ return typeof value === "string" ? value : void 0;
1763
+ }
1764
+ function asBool(value) {
1765
+ return typeof value === "boolean" ? value : void 0;
1766
+ }
1767
+ function asNumber(value) {
1768
+ return typeof value === "number" ? value : void 0;
1769
+ }
1770
+ function asTimestamp(value) {
1771
+ if (typeof value === "string" || typeof value === "number" || value instanceof Date) {
1772
+ return value;
1773
+ }
1774
+ return void 0;
1775
+ }
1776
+
1535
1777
  // src/adapters/base-adapter.ts
1536
1778
  import { spawn } from "child_process";
1537
1779
  var BaseCLIAdapter = class {
@@ -1761,7 +2003,9 @@ var BaseCLIAdapter = class {
1761
2003
  */
1762
2004
  stripAnsi(str) {
1763
2005
  const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
1764
- return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
2006
+ const withoutOsc = withSpaces.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*(?:\x07|\x1b\\)/g, "");
2007
+ const withoutDcs = withoutOsc.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "");
2008
+ return withoutDcs.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1765
2009
  }
1766
2010
  };
1767
2011
 
@@ -2455,8 +2699,10 @@ export {
2455
2699
  PTYSession,
2456
2700
  SPECIAL_KEYS,
2457
2701
  ShellAdapter,
2702
+ buildTaskCompletionTimeline,
2458
2703
  createAdapter,
2459
2704
  createPTYManager,
2705
+ extractTaskCompletionTraceRecords,
2460
2706
  isBun
2461
2707
  };
2462
2708
  //# sourceMappingURL=index.mjs.map