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/README.md CHANGED
@@ -216,6 +216,8 @@ interface SpawnConfig {
216
216
  rows?: number; // Terminal rows (default: 40)
217
217
  timeout?: number; // Session timeout in ms
218
218
  readySettleMs?: number; // Override adapter's ready settle delay
219
+ stallTimeoutMs?: number; // Override manager stall timeout for this session
220
+ traceTaskCompletion?: boolean; // Verbose completion trace logs (auto-enabled for Claude)
219
221
  }
220
222
  ```
221
223
 
@@ -479,12 +481,60 @@ Content-based stall detection monitors sessions for output that stops changing.
479
481
 
480
482
  ### Task Completion Fast-Path (Settle Pattern)
481
483
 
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.
484
+ When a busy session's output matches the adapter's task-complete signal (`detectTaskComplete()` when implemented, otherwise `detectReady()`), 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 the same task-complete signal before transitioning, so stale triggers are safe.
483
485
 
484
486
  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
487
 
486
488
  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.
487
489
 
490
+ ### Task Completion Trace Logs (Claude-focused)
491
+
492
+ `PTYSession` now emits structured debug logs with message `"Task completion trace"` at each completion transition point:
493
+
494
+ - `busy_signal`
495
+ - `debounce_schedule`
496
+ - `debounce_fire`
497
+ - `debounce_reject_status`
498
+ - `debounce_reject_signal`
499
+ - `transition_ready`
500
+
501
+ By default, tracing is enabled for adapter type `claude`. You can override per session:
502
+
503
+ ```typescript
504
+ const handle = await manager.spawn({
505
+ name: 'agent',
506
+ type: 'claude',
507
+ traceTaskCompletion: true, // force on
508
+ // traceTaskCompletion: false, // force off
509
+ });
510
+ ```
511
+
512
+ Each trace includes detection booleans (`detectTaskComplete`, `detectReady`, `detectLoading`) and a normalized tail hash/snippet to correlate against PTY output captures.
513
+
514
+ ### Completion Confidence Timeline Utility
515
+
516
+ Use the exported helpers to turn raw trace logs into a per-turn confidence timeline:
517
+
518
+ ```typescript
519
+ import {
520
+ extractTaskCompletionTraceRecords,
521
+ buildTaskCompletionTimeline,
522
+ } from 'pty-manager';
523
+
524
+ const records = extractTaskCompletionTraceRecords(mixedLogEntries);
525
+ const timeline = buildTaskCompletionTimeline(records, { adapterType: 'claude' });
526
+
527
+ console.log(timeline.turns);
528
+ ```
529
+
530
+ The timeline classifies each trace step as:
531
+
532
+ - `active`
533
+ - `active_loading`
534
+ - `likely_complete`
535
+ - `rejected`
536
+ - `completed`
537
+
488
538
  ```typescript
489
539
  // Enable stall detection with a pluggable classifier
490
540
  const session = await manager.spawn({
package/dist/index.d.mts CHANGED
@@ -40,6 +40,9 @@ interface SpawnConfig {
40
40
  /** Override adapter's readySettleMs for this session.
41
41
  * Ms of output silence after detectReady match before emitting session_ready. */
42
42
  readySettleMs?: number;
43
+ /** Enable verbose task-completion trace logs.
44
+ * If unset, PTYSession enables this automatically for adapter type "claude". */
45
+ traceTaskCompletion?: boolean;
43
46
  /** Override or disable specific adapter auto-response rules for this session.
44
47
  * Keys are regex source strings (from rule.pattern.source).
45
48
  * - null value disables that rule entirely
@@ -460,6 +463,8 @@ declare class PTYSession extends EventEmitter {
460
463
  private _lastContentHash;
461
464
  private _stallBackoffMs;
462
465
  private static readonly MAX_STALL_BACKOFF_MS;
466
+ private _stallEmissionCount;
467
+ private static readonly MAX_STALL_EMISSIONS;
463
468
  private _taskCompleteTimer;
464
469
  private _taskCompletePending;
465
470
  private static readonly TASK_COMPLETE_DEBOUNCE_MS;
@@ -541,10 +546,21 @@ declare class PTYSession extends EventEmitter {
541
546
  * being a no-op when already scheduled. This allows TUI agents that
542
547
  * continue rendering decorative output (status bar, update notices) after
543
548
  * 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.
549
+ * by every new data chunk. The callback re-verifies the task-complete
550
+ * signal before transitioning, so stale triggers are safe.
546
551
  */
547
552
  private scheduleTaskComplete;
553
+ /**
554
+ * Adapter-level task completion check with compatibility fallback.
555
+ * Prefer detectTaskComplete() because detectReady() may be broad for TUIs.
556
+ */
557
+ private isTaskCompleteSignal;
558
+ /**
559
+ * Claude-oriented task completion traces for PTY debugging.
560
+ * Enabled by config.traceTaskCompletion, otherwise defaults to enabled for Claude.
561
+ */
562
+ private traceTaskCompletion;
563
+ private shouldTraceTaskCompletion;
548
564
  /**
549
565
  * Cancel a pending task_complete timer (new output arrived that
550
566
  * doesn't match the idle prompt, so the agent is still working).
@@ -784,6 +800,65 @@ declare class PTYManager extends EventEmitter {
784
800
  clearAutoResponseRules(sessionId: string): void;
785
801
  }
786
802
 
803
+ /**
804
+ * Task completion trace analysis helpers.
805
+ *
806
+ * Parses structured "Task completion trace" logs and builds a compact
807
+ * per-turn confidence timeline useful for debugging idle/completion detection.
808
+ */
809
+ interface TaskCompletionTraceRecord {
810
+ sessionId?: string;
811
+ adapterType?: string;
812
+ event: string;
813
+ status?: string;
814
+ taskCompletePending?: boolean;
815
+ signal?: boolean;
816
+ wasPending?: boolean;
817
+ debounceMs?: number;
818
+ detectTaskComplete?: boolean;
819
+ detectReady?: boolean;
820
+ detectLoading?: boolean;
821
+ tailHash?: string;
822
+ tailSnippet?: string;
823
+ timestamp?: string | number | Date;
824
+ }
825
+ interface TaskCompletionTimelineStep {
826
+ event: string;
827
+ atIndex: number;
828
+ status: 'active' | 'active_loading' | 'likely_complete' | 'completed' | 'rejected';
829
+ confidence: number;
830
+ signal?: boolean;
831
+ detectTaskComplete?: boolean;
832
+ detectReady?: boolean;
833
+ detectLoading?: boolean;
834
+ }
835
+ interface TaskCompletionTurnTimeline {
836
+ turn: number;
837
+ startIndex: number;
838
+ endIndex: number;
839
+ completed: boolean;
840
+ maxConfidence: number;
841
+ finalConfidence: number;
842
+ events: TaskCompletionTimelineStep[];
843
+ }
844
+ interface TaskCompletionTimelineResult {
845
+ turns: TaskCompletionTurnTimeline[];
846
+ totalRecords: number;
847
+ ignoredRecords: number;
848
+ }
849
+ interface BuildTimelineOptions {
850
+ adapterType?: string;
851
+ }
852
+ /**
853
+ * Extract trace records from mixed log inputs.
854
+ * Accepts structured objects and JSON lines.
855
+ */
856
+ declare function extractTaskCompletionTraceRecords(entries: Array<string | Record<string, unknown>>): TaskCompletionTraceRecord[];
857
+ /**
858
+ * Build a per-turn confidence timeline from task-completion traces.
859
+ */
860
+ declare function buildTaskCompletionTimeline(records: TaskCompletionTraceRecord[], options?: BuildTimelineOptions): TaskCompletionTimelineResult;
861
+
787
862
  /**
788
863
  * Base CLI Adapter
789
864
  *
@@ -1075,4 +1150,4 @@ declare function isBun(): boolean;
1075
1150
  */
1076
1151
  declare function createPTYManager(options?: BunPTYManagerOptions): BunCompatiblePTYManager;
1077
1152
 
1078
- export { type AdapterFactoryConfig, AdapterRegistry, type AutoResponseRule, BaseCLIAdapter, type BlockingPromptDetection, type BlockingPromptInfo, type BlockingPromptType, BunCompatiblePTYManager, type BunPTYManagerOptions, type CLIAdapter, type LogOptions, type Logger, type LoginDetection, type MessageType, PTYManager, type PTYManagerConfig, type PTYManagerEvents, PTYSession, type PTYSessionEvents, type ParsedOutput, SPECIAL_KEYS, type SessionFilter, type SessionHandle, type SessionMessage, type SessionStatus, ShellAdapter, type ShellAdapterOptions, type SpawnConfig, type StallClassification, type StopOptions, type TerminalAttachment, type WorkerSessionHandle, createAdapter, createPTYManager, isBun };
1153
+ export { type AdapterFactoryConfig, AdapterRegistry, type AutoResponseRule, BaseCLIAdapter, type BlockingPromptDetection, type BlockingPromptInfo, type BlockingPromptType, type BuildTimelineOptions, BunCompatiblePTYManager, type BunPTYManagerOptions, type CLIAdapter, type LogOptions, type Logger, type LoginDetection, type MessageType, PTYManager, type PTYManagerConfig, type PTYManagerEvents, PTYSession, type PTYSessionEvents, type ParsedOutput, SPECIAL_KEYS, type SessionFilter, type SessionHandle, type SessionMessage, type SessionStatus, ShellAdapter, type ShellAdapterOptions, type SpawnConfig, type StallClassification, type StopOptions, type TaskCompletionTimelineResult, type TaskCompletionTimelineStep, type TaskCompletionTraceRecord, type TaskCompletionTurnTimeline, type TerminalAttachment, type WorkerSessionHandle, buildTaskCompletionTimeline, createAdapter, createPTYManager, extractTaskCompletionTraceRecords, isBun };
package/dist/index.d.ts CHANGED
@@ -40,6 +40,9 @@ interface SpawnConfig {
40
40
  /** Override adapter's readySettleMs for this session.
41
41
  * Ms of output silence after detectReady match before emitting session_ready. */
42
42
  readySettleMs?: number;
43
+ /** Enable verbose task-completion trace logs.
44
+ * If unset, PTYSession enables this automatically for adapter type "claude". */
45
+ traceTaskCompletion?: boolean;
43
46
  /** Override or disable specific adapter auto-response rules for this session.
44
47
  * Keys are regex source strings (from rule.pattern.source).
45
48
  * - null value disables that rule entirely
@@ -460,6 +463,8 @@ declare class PTYSession extends EventEmitter {
460
463
  private _lastContentHash;
461
464
  private _stallBackoffMs;
462
465
  private static readonly MAX_STALL_BACKOFF_MS;
466
+ private _stallEmissionCount;
467
+ private static readonly MAX_STALL_EMISSIONS;
463
468
  private _taskCompleteTimer;
464
469
  private _taskCompletePending;
465
470
  private static readonly TASK_COMPLETE_DEBOUNCE_MS;
@@ -541,10 +546,21 @@ declare class PTYSession extends EventEmitter {
541
546
  * being a no-op when already scheduled. This allows TUI agents that
542
547
  * continue rendering decorative output (status bar, update notices) after
543
548
  * 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.
549
+ * by every new data chunk. The callback re-verifies the task-complete
550
+ * signal before transitioning, so stale triggers are safe.
546
551
  */
547
552
  private scheduleTaskComplete;
553
+ /**
554
+ * Adapter-level task completion check with compatibility fallback.
555
+ * Prefer detectTaskComplete() because detectReady() may be broad for TUIs.
556
+ */
557
+ private isTaskCompleteSignal;
558
+ /**
559
+ * Claude-oriented task completion traces for PTY debugging.
560
+ * Enabled by config.traceTaskCompletion, otherwise defaults to enabled for Claude.
561
+ */
562
+ private traceTaskCompletion;
563
+ private shouldTraceTaskCompletion;
548
564
  /**
549
565
  * Cancel a pending task_complete timer (new output arrived that
550
566
  * doesn't match the idle prompt, so the agent is still working).
@@ -784,6 +800,65 @@ declare class PTYManager extends EventEmitter {
784
800
  clearAutoResponseRules(sessionId: string): void;
785
801
  }
786
802
 
803
+ /**
804
+ * Task completion trace analysis helpers.
805
+ *
806
+ * Parses structured "Task completion trace" logs and builds a compact
807
+ * per-turn confidence timeline useful for debugging idle/completion detection.
808
+ */
809
+ interface TaskCompletionTraceRecord {
810
+ sessionId?: string;
811
+ adapterType?: string;
812
+ event: string;
813
+ status?: string;
814
+ taskCompletePending?: boolean;
815
+ signal?: boolean;
816
+ wasPending?: boolean;
817
+ debounceMs?: number;
818
+ detectTaskComplete?: boolean;
819
+ detectReady?: boolean;
820
+ detectLoading?: boolean;
821
+ tailHash?: string;
822
+ tailSnippet?: string;
823
+ timestamp?: string | number | Date;
824
+ }
825
+ interface TaskCompletionTimelineStep {
826
+ event: string;
827
+ atIndex: number;
828
+ status: 'active' | 'active_loading' | 'likely_complete' | 'completed' | 'rejected';
829
+ confidence: number;
830
+ signal?: boolean;
831
+ detectTaskComplete?: boolean;
832
+ detectReady?: boolean;
833
+ detectLoading?: boolean;
834
+ }
835
+ interface TaskCompletionTurnTimeline {
836
+ turn: number;
837
+ startIndex: number;
838
+ endIndex: number;
839
+ completed: boolean;
840
+ maxConfidence: number;
841
+ finalConfidence: number;
842
+ events: TaskCompletionTimelineStep[];
843
+ }
844
+ interface TaskCompletionTimelineResult {
845
+ turns: TaskCompletionTurnTimeline[];
846
+ totalRecords: number;
847
+ ignoredRecords: number;
848
+ }
849
+ interface BuildTimelineOptions {
850
+ adapterType?: string;
851
+ }
852
+ /**
853
+ * Extract trace records from mixed log inputs.
854
+ * Accepts structured objects and JSON lines.
855
+ */
856
+ declare function extractTaskCompletionTraceRecords(entries: Array<string | Record<string, unknown>>): TaskCompletionTraceRecord[];
857
+ /**
858
+ * Build a per-turn confidence timeline from task-completion traces.
859
+ */
860
+ declare function buildTaskCompletionTimeline(records: TaskCompletionTraceRecord[], options?: BuildTimelineOptions): TaskCompletionTimelineResult;
861
+
787
862
  /**
788
863
  * Base CLI Adapter
789
864
  *
@@ -1075,4 +1150,4 @@ declare function isBun(): boolean;
1075
1150
  */
1076
1151
  declare function createPTYManager(options?: BunPTYManagerOptions): BunCompatiblePTYManager;
1077
1152
 
1078
- export { type AdapterFactoryConfig, AdapterRegistry, type AutoResponseRule, BaseCLIAdapter, type BlockingPromptDetection, type BlockingPromptInfo, type BlockingPromptType, BunCompatiblePTYManager, type BunPTYManagerOptions, type CLIAdapter, type LogOptions, type Logger, type LoginDetection, type MessageType, PTYManager, type PTYManagerConfig, type PTYManagerEvents, PTYSession, type PTYSessionEvents, type ParsedOutput, SPECIAL_KEYS, type SessionFilter, type SessionHandle, type SessionMessage, type SessionStatus, ShellAdapter, type ShellAdapterOptions, type SpawnConfig, type StallClassification, type StopOptions, type TerminalAttachment, type WorkerSessionHandle, createAdapter, createPTYManager, isBun };
1153
+ export { type AdapterFactoryConfig, AdapterRegistry, type AutoResponseRule, BaseCLIAdapter, type BlockingPromptDetection, type BlockingPromptInfo, type BlockingPromptType, type BuildTimelineOptions, BunCompatiblePTYManager, type BunPTYManagerOptions, type CLIAdapter, type LogOptions, type Logger, type LoginDetection, type MessageType, PTYManager, type PTYManagerConfig, type PTYManagerEvents, PTYSession, type PTYSessionEvents, type ParsedOutput, SPECIAL_KEYS, type SessionFilter, type SessionHandle, type SessionMessage, type SessionStatus, ShellAdapter, type ShellAdapterOptions, type SpawnConfig, type StallClassification, type StopOptions, type TaskCompletionTimelineResult, type TaskCompletionTimelineStep, type TaskCompletionTraceRecord, type TaskCompletionTurnTimeline, type TerminalAttachment, type WorkerSessionHandle, buildTaskCompletionTimeline, createAdapter, createPTYManager, extractTaskCompletionTraceRecords, isBun };
package/dist/index.js CHANGED
@@ -37,8 +37,10 @@ __export(index_exports, {
37
37
  PTYSession: () => PTYSession,
38
38
  SPECIAL_KEYS: () => SPECIAL_KEYS,
39
39
  ShellAdapter: () => ShellAdapter,
40
+ buildTaskCompletionTimeline: () => buildTaskCompletionTimeline,
40
41
  createAdapter: () => createAdapter,
41
42
  createPTYManager: () => createPTYManager,
43
+ extractTaskCompletionTraceRecords: () => extractTaskCompletionTraceRecords,
42
44
  isBun: () => isBun
43
45
  });
44
46
  module.exports = __toCommonJS(index_exports);
@@ -348,6 +350,8 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
348
350
  _stallBackoffMs = 0;
349
351
  // Initialized in constructor from _stallTimeoutMs
350
352
  static MAX_STALL_BACKOFF_MS = 3e4;
353
+ _stallEmissionCount = 0;
354
+ static MAX_STALL_EMISSIONS = 5;
351
355
  // Task completion detection (idle detection when busy)
352
356
  _taskCompleteTimer = null;
353
357
  _taskCompletePending = false;
@@ -465,6 +469,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
465
469
  return;
466
470
  }
467
471
  this._lastContentHash = hash;
472
+ this._stallEmissionCount = 0;
468
473
  if (this._stallTimer) {
469
474
  clearTimeout(this._stallTimer);
470
475
  this._stallTimer = null;
@@ -487,6 +492,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
487
492
  this._stallStartedAt = null;
488
493
  this._lastContentHash = null;
489
494
  this._stallBackoffMs = this._stallTimeoutMs;
495
+ this._stallEmissionCount = 0;
490
496
  }
491
497
  /**
492
498
  * Called when the stall timer fires (no output for stallTimeoutMs).
@@ -523,6 +529,15 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
523
529
  );
524
530
  return;
525
531
  }
532
+ this._stallEmissionCount++;
533
+ if (this._stallEmissionCount > _PTYSession.MAX_STALL_EMISSIONS) {
534
+ this.logger.warn(
535
+ { sessionId: this.id, count: this._stallEmissionCount },
536
+ "Max stall emissions reached \u2014 suspending stall detection for this task"
537
+ );
538
+ this.clearStallTimer();
539
+ return;
540
+ }
526
541
  const recentRaw = this.outputBuffer.slice(-2e3);
527
542
  const recentOutput = this.stripAnsiForStall(recentRaw);
528
543
  const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
@@ -562,6 +577,8 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
562
577
  let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
563
578
  result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
564
579
  result = result.replace(/\x1b\[\d*[JK]/g, " ");
580
+ result = result.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*(?:\x07|\x1b\\)/g, "");
581
+ result = result.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "");
565
582
  result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
566
583
  result = result.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
567
584
  result = result.replace(/\xa0/g, " ");
@@ -645,10 +662,15 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
645
662
  * being a no-op when already scheduled. This allows TUI agents that
646
663
  * continue rendering decorative output (status bar, update notices) after
647
664
  * 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.
665
+ * by every new data chunk. The callback re-verifies the task-complete
666
+ * signal before transitioning, so stale triggers are safe.
650
667
  */
651
668
  scheduleTaskComplete() {
669
+ const wasPending = this._taskCompletePending;
670
+ this.traceTaskCompletion("debounce_schedule", {
671
+ wasPending,
672
+ debounceMs: _PTYSession.TASK_COMPLETE_DEBOUNCE_MS
673
+ });
652
674
  if (this._taskCompleteTimer) {
653
675
  clearTimeout(this._taskCompleteTimer);
654
676
  }
@@ -656,17 +678,72 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
656
678
  this._taskCompleteTimer = setTimeout(() => {
657
679
  this._taskCompleteTimer = null;
658
680
  this._taskCompletePending = false;
659
- if (this._status !== "busy") return;
660
- if (!this.adapter.detectReady(this.outputBuffer)) return;
681
+ const signal = this.isTaskCompleteSignal(this.outputBuffer);
682
+ this.traceTaskCompletion("debounce_fire", { signal });
683
+ if (this._status !== "busy") {
684
+ this.traceTaskCompletion("debounce_reject_status", { signal });
685
+ return;
686
+ }
687
+ if (!signal) {
688
+ this.traceTaskCompletion("debounce_reject_signal", { signal });
689
+ return;
690
+ }
661
691
  this._status = "ready";
662
692
  this._lastBlockingPromptHash = null;
663
693
  this.outputBuffer = "";
664
694
  this.clearStallTimer();
665
695
  this.emit("status_changed", "ready");
666
696
  this.emit("task_complete");
697
+ this.traceTaskCompletion("transition_ready", { signal: true });
667
698
  this.logger.info({ sessionId: this.id }, "Task complete \u2014 agent returned to idle prompt");
668
699
  }, _PTYSession.TASK_COMPLETE_DEBOUNCE_MS);
669
700
  }
701
+ /**
702
+ * Adapter-level task completion check with compatibility fallback.
703
+ * Prefer detectTaskComplete() because detectReady() may be broad for TUIs.
704
+ */
705
+ isTaskCompleteSignal(output) {
706
+ if (this.adapter.detectTaskComplete) {
707
+ return this.adapter.detectTaskComplete(output);
708
+ }
709
+ return this.adapter.detectReady(output);
710
+ }
711
+ /**
712
+ * Claude-oriented task completion traces for PTY debugging.
713
+ * Enabled by config.traceTaskCompletion, otherwise defaults to enabled for Claude.
714
+ */
715
+ traceTaskCompletion(event, ctx = {}) {
716
+ if (!this.shouldTraceTaskCompletion()) return;
717
+ const output = this.outputBuffer;
718
+ const detectTaskComplete = this.adapter.detectTaskComplete ? this.adapter.detectTaskComplete(output) : void 0;
719
+ const detectReady = this.adapter.detectReady(output);
720
+ const detectLoading = this.adapter.detectLoading ? this.adapter.detectLoading(output) : void 0;
721
+ const normalizedTail = this.stripAnsiForStall(output.slice(-280));
722
+ this.logger.debug(
723
+ {
724
+ sessionId: this.id,
725
+ adapterType: this.adapter.adapterType,
726
+ event,
727
+ status: this._status,
728
+ taskCompletePending: this._taskCompletePending,
729
+ signal: ctx.signal,
730
+ wasPending: ctx.wasPending,
731
+ debounceMs: ctx.debounceMs,
732
+ detectTaskComplete,
733
+ detectReady,
734
+ detectLoading,
735
+ tailHash: this.simpleHash(normalizedTail),
736
+ tailSnippet: normalizedTail.slice(-140)
737
+ },
738
+ "Task completion trace"
739
+ );
740
+ }
741
+ shouldTraceTaskCompletion() {
742
+ if (typeof this.config.traceTaskCompletion === "boolean") {
743
+ return this.config.traceTaskCompletion;
744
+ }
745
+ return this.adapter.adapterType === "claude";
746
+ }
670
747
  /**
671
748
  * Cancel a pending task_complete timer (new output arrived that
672
749
  * doesn't match the idle prompt, so the agent is still working).
@@ -819,7 +896,9 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
819
896
  return;
820
897
  }
821
898
  if (this._status === "busy") {
822
- if (this._taskCompletePending || this.adapter.detectReady(this.outputBuffer)) {
899
+ const signal = this.isTaskCompleteSignal(this.outputBuffer);
900
+ if (this._taskCompletePending || signal) {
901
+ this.traceTaskCompletion("busy_signal", { signal });
823
902
  this.scheduleTaskComplete();
824
903
  }
825
904
  }
@@ -1570,6 +1649,171 @@ var PTYManager = class extends import_events2.EventEmitter {
1570
1649
  }
1571
1650
  };
1572
1651
 
1652
+ // src/task-completion-trace.ts
1653
+ function extractTaskCompletionTraceRecords(entries) {
1654
+ const out = [];
1655
+ for (const entry of entries) {
1656
+ let obj = null;
1657
+ if (typeof entry === "string") {
1658
+ const line = entry.trim();
1659
+ if (!line.startsWith("{") || !line.endsWith("}")) continue;
1660
+ try {
1661
+ obj = JSON.parse(line);
1662
+ } catch {
1663
+ continue;
1664
+ }
1665
+ } else if (entry && typeof entry === "object") {
1666
+ obj = entry;
1667
+ }
1668
+ if (!obj) continue;
1669
+ if (obj.msg !== "Task completion trace") continue;
1670
+ if (typeof obj.event !== "string") continue;
1671
+ out.push({
1672
+ sessionId: asString(obj.sessionId),
1673
+ adapterType: asString(obj.adapterType),
1674
+ event: obj.event,
1675
+ status: asString(obj.status),
1676
+ taskCompletePending: asBool(obj.taskCompletePending),
1677
+ signal: asBool(obj.signal),
1678
+ wasPending: asBool(obj.wasPending),
1679
+ debounceMs: asNumber(obj.debounceMs),
1680
+ detectTaskComplete: asBool(obj.detectTaskComplete),
1681
+ detectReady: asBool(obj.detectReady),
1682
+ detectLoading: asBool(obj.detectLoading),
1683
+ tailHash: asString(obj.tailHash),
1684
+ tailSnippet: asString(obj.tailSnippet),
1685
+ timestamp: asTimestamp(obj.time) ?? asTimestamp(obj.timestamp)
1686
+ });
1687
+ }
1688
+ return out;
1689
+ }
1690
+ function buildTaskCompletionTimeline(records, options = {}) {
1691
+ const filtered = records.filter((r) => {
1692
+ if (!options.adapterType) return true;
1693
+ return r.adapterType === options.adapterType;
1694
+ });
1695
+ const turns = [];
1696
+ let current = null;
1697
+ let ignored = 0;
1698
+ filtered.forEach((record, index) => {
1699
+ if (record.event === "busy_signal" && current && current.completed) {
1700
+ current = null;
1701
+ }
1702
+ if (!current) {
1703
+ current = {
1704
+ turn: turns.length + 1,
1705
+ startIndex: index,
1706
+ endIndex: index,
1707
+ completed: false,
1708
+ maxConfidence: 0,
1709
+ finalConfidence: 0,
1710
+ events: []
1711
+ };
1712
+ turns.push(current);
1713
+ }
1714
+ const step = toStep(record, index);
1715
+ if (!step) {
1716
+ ignored++;
1717
+ return;
1718
+ }
1719
+ current.events.push(step);
1720
+ current.endIndex = index;
1721
+ current.maxConfidence = Math.max(current.maxConfidence, step.confidence);
1722
+ current.finalConfidence = step.confidence;
1723
+ if (step.status === "completed") {
1724
+ current.completed = true;
1725
+ }
1726
+ });
1727
+ return {
1728
+ turns,
1729
+ totalRecords: filtered.length,
1730
+ ignoredRecords: ignored
1731
+ };
1732
+ }
1733
+ function toStep(record, atIndex) {
1734
+ const event = record.event;
1735
+ const confidence = scoreConfidence(record);
1736
+ if (event === "transition_ready") {
1737
+ return withCommon(record, {
1738
+ event,
1739
+ atIndex,
1740
+ status: "completed",
1741
+ confidence: 100
1742
+ });
1743
+ }
1744
+ if (event === "debounce_reject_signal" || event === "debounce_reject_status") {
1745
+ return withCommon(record, {
1746
+ event,
1747
+ atIndex,
1748
+ status: "rejected",
1749
+ confidence
1750
+ });
1751
+ }
1752
+ if (record.detectLoading) {
1753
+ return withCommon(record, {
1754
+ event,
1755
+ atIndex,
1756
+ status: "active_loading",
1757
+ confidence
1758
+ });
1759
+ }
1760
+ if (event === "debounce_fire" && record.signal) {
1761
+ return withCommon(record, {
1762
+ event,
1763
+ atIndex,
1764
+ status: "likely_complete",
1765
+ confidence
1766
+ });
1767
+ }
1768
+ if (event === "busy_signal" || event === "debounce_schedule" || event === "debounce_fire") {
1769
+ return withCommon(record, {
1770
+ event,
1771
+ atIndex,
1772
+ status: "active",
1773
+ confidence
1774
+ });
1775
+ }
1776
+ return null;
1777
+ }
1778
+ function scoreConfidence(record) {
1779
+ let score = 10;
1780
+ if (record.detectLoading) score -= 40;
1781
+ if (record.detectReady) score += 20;
1782
+ if (record.detectTaskComplete) score += 45;
1783
+ if (record.signal) score += 20;
1784
+ if (record.event === "debounce_reject_signal" || record.event === "debounce_reject_status") {
1785
+ score -= 30;
1786
+ }
1787
+ if (record.event === "transition_ready") score = 100;
1788
+ if (score < 0) return 0;
1789
+ if (score > 100) return 100;
1790
+ return score;
1791
+ }
1792
+ function withCommon(record, step) {
1793
+ return {
1794
+ ...step,
1795
+ signal: record.signal,
1796
+ detectTaskComplete: record.detectTaskComplete,
1797
+ detectReady: record.detectReady,
1798
+ detectLoading: record.detectLoading
1799
+ };
1800
+ }
1801
+ function asString(value) {
1802
+ return typeof value === "string" ? value : void 0;
1803
+ }
1804
+ function asBool(value) {
1805
+ return typeof value === "boolean" ? value : void 0;
1806
+ }
1807
+ function asNumber(value) {
1808
+ return typeof value === "number" ? value : void 0;
1809
+ }
1810
+ function asTimestamp(value) {
1811
+ if (typeof value === "string" || typeof value === "number" || value instanceof Date) {
1812
+ return value;
1813
+ }
1814
+ return void 0;
1815
+ }
1816
+
1573
1817
  // src/adapters/base-adapter.ts
1574
1818
  var import_child_process = require("child_process");
1575
1819
  var BaseCLIAdapter = class {
@@ -1799,7 +2043,9 @@ var BaseCLIAdapter = class {
1799
2043
  */
1800
2044
  stripAnsi(str) {
1801
2045
  const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
1802
- return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
2046
+ const withoutOsc = withSpaces.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*(?:\x07|\x1b\\)/g, "");
2047
+ const withoutDcs = withoutOsc.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "");
2048
+ return withoutDcs.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1803
2049
  }
1804
2050
  };
1805
2051
 
@@ -2494,8 +2740,10 @@ function createPTYManager(options) {
2494
2740
  PTYSession,
2495
2741
  SPECIAL_KEYS,
2496
2742
  ShellAdapter,
2743
+ buildTaskCompletionTimeline,
2497
2744
  createAdapter,
2498
2745
  createPTYManager,
2746
+ extractTaskCompletionTraceRecords,
2499
2747
  isBun
2500
2748
  });
2501
2749
  //# sourceMappingURL=index.js.map