pty-manager 1.6.3 → 1.6.5
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 +51 -1
- package/dist/index.d.mts +82 -3
- package/dist/index.d.ts +82 -3
- package/dist/index.js +265 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +263 -9
- package/dist/index.mjs.map +1 -1
- package/dist/pty-worker.js +96 -9
- package/package.json +1 -1
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 `
|
|
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
|
|
@@ -532,6 +535,12 @@ declare class PTYSession extends EventEmitter {
|
|
|
532
535
|
* word boundaries — e.g. "Do\x1b[5Cyou" becomes "Do you", not "Doyou".
|
|
533
536
|
*/
|
|
534
537
|
private stripAnsiForStall;
|
|
538
|
+
/**
|
|
539
|
+
* Less-aggressive ANSI stripping for classifier context.
|
|
540
|
+
* Preserves visible TUI symbols (e.g. ❯, ✻) and durations while removing
|
|
541
|
+
* escape/control sequences so the classifier keeps useful evidence.
|
|
542
|
+
*/
|
|
543
|
+
private stripAnsiForClassifier;
|
|
535
544
|
/**
|
|
536
545
|
* Handle external stall classification result.
|
|
537
546
|
* Called by the manager after onStallClassify resolves.
|
|
@@ -543,10 +552,21 @@ declare class PTYSession extends EventEmitter {
|
|
|
543
552
|
* being a no-op when already scheduled. This allows TUI agents that
|
|
544
553
|
* continue rendering decorative output (status bar, update notices) after
|
|
545
554
|
* the prompt to eventually settle, rather than having the timer cancelled
|
|
546
|
-
* by every new data chunk. The callback re-verifies
|
|
547
|
-
* transitioning, so stale triggers are safe.
|
|
555
|
+
* by every new data chunk. The callback re-verifies the task-complete
|
|
556
|
+
* signal before transitioning, so stale triggers are safe.
|
|
548
557
|
*/
|
|
549
558
|
private scheduleTaskComplete;
|
|
559
|
+
/**
|
|
560
|
+
* Adapter-level task completion check with compatibility fallback.
|
|
561
|
+
* Prefer detectTaskComplete() because detectReady() may be broad for TUIs.
|
|
562
|
+
*/
|
|
563
|
+
private isTaskCompleteSignal;
|
|
564
|
+
/**
|
|
565
|
+
* Claude-oriented task completion traces for PTY debugging.
|
|
566
|
+
* Enabled by config.traceTaskCompletion, otherwise defaults to enabled for Claude.
|
|
567
|
+
*/
|
|
568
|
+
private traceTaskCompletion;
|
|
569
|
+
private shouldTraceTaskCompletion;
|
|
550
570
|
/**
|
|
551
571
|
* Cancel a pending task_complete timer (new output arrived that
|
|
552
572
|
* doesn't match the idle prompt, so the agent is still working).
|
|
@@ -786,6 +806,65 @@ declare class PTYManager extends EventEmitter {
|
|
|
786
806
|
clearAutoResponseRules(sessionId: string): void;
|
|
787
807
|
}
|
|
788
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Task completion trace analysis helpers.
|
|
811
|
+
*
|
|
812
|
+
* Parses structured "Task completion trace" logs and builds a compact
|
|
813
|
+
* per-turn confidence timeline useful for debugging idle/completion detection.
|
|
814
|
+
*/
|
|
815
|
+
interface TaskCompletionTraceRecord {
|
|
816
|
+
sessionId?: string;
|
|
817
|
+
adapterType?: string;
|
|
818
|
+
event: string;
|
|
819
|
+
status?: string;
|
|
820
|
+
taskCompletePending?: boolean;
|
|
821
|
+
signal?: boolean;
|
|
822
|
+
wasPending?: boolean;
|
|
823
|
+
debounceMs?: number;
|
|
824
|
+
detectTaskComplete?: boolean;
|
|
825
|
+
detectReady?: boolean;
|
|
826
|
+
detectLoading?: boolean;
|
|
827
|
+
tailHash?: string;
|
|
828
|
+
tailSnippet?: string;
|
|
829
|
+
timestamp?: string | number | Date;
|
|
830
|
+
}
|
|
831
|
+
interface TaskCompletionTimelineStep {
|
|
832
|
+
event: string;
|
|
833
|
+
atIndex: number;
|
|
834
|
+
status: 'active' | 'active_loading' | 'likely_complete' | 'completed' | 'rejected';
|
|
835
|
+
confidence: number;
|
|
836
|
+
signal?: boolean;
|
|
837
|
+
detectTaskComplete?: boolean;
|
|
838
|
+
detectReady?: boolean;
|
|
839
|
+
detectLoading?: boolean;
|
|
840
|
+
}
|
|
841
|
+
interface TaskCompletionTurnTimeline {
|
|
842
|
+
turn: number;
|
|
843
|
+
startIndex: number;
|
|
844
|
+
endIndex: number;
|
|
845
|
+
completed: boolean;
|
|
846
|
+
maxConfidence: number;
|
|
847
|
+
finalConfidence: number;
|
|
848
|
+
events: TaskCompletionTimelineStep[];
|
|
849
|
+
}
|
|
850
|
+
interface TaskCompletionTimelineResult {
|
|
851
|
+
turns: TaskCompletionTurnTimeline[];
|
|
852
|
+
totalRecords: number;
|
|
853
|
+
ignoredRecords: number;
|
|
854
|
+
}
|
|
855
|
+
interface BuildTimelineOptions {
|
|
856
|
+
adapterType?: string;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Extract trace records from mixed log inputs.
|
|
860
|
+
* Accepts structured objects and JSON lines.
|
|
861
|
+
*/
|
|
862
|
+
declare function extractTaskCompletionTraceRecords(entries: Array<string | Record<string, unknown>>): TaskCompletionTraceRecord[];
|
|
863
|
+
/**
|
|
864
|
+
* Build a per-turn confidence timeline from task-completion traces.
|
|
865
|
+
*/
|
|
866
|
+
declare function buildTaskCompletionTimeline(records: TaskCompletionTraceRecord[], options?: BuildTimelineOptions): TaskCompletionTimelineResult;
|
|
867
|
+
|
|
789
868
|
/**
|
|
790
869
|
* Base CLI Adapter
|
|
791
870
|
*
|
|
@@ -1077,4 +1156,4 @@ declare function isBun(): boolean;
|
|
|
1077
1156
|
*/
|
|
1078
1157
|
declare function createPTYManager(options?: BunPTYManagerOptions): BunCompatiblePTYManager;
|
|
1079
1158
|
|
|
1080
|
-
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 };
|
|
1159
|
+
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
|
|
@@ -532,6 +535,12 @@ declare class PTYSession extends EventEmitter {
|
|
|
532
535
|
* word boundaries — e.g. "Do\x1b[5Cyou" becomes "Do you", not "Doyou".
|
|
533
536
|
*/
|
|
534
537
|
private stripAnsiForStall;
|
|
538
|
+
/**
|
|
539
|
+
* Less-aggressive ANSI stripping for classifier context.
|
|
540
|
+
* Preserves visible TUI symbols (e.g. ❯, ✻) and durations while removing
|
|
541
|
+
* escape/control sequences so the classifier keeps useful evidence.
|
|
542
|
+
*/
|
|
543
|
+
private stripAnsiForClassifier;
|
|
535
544
|
/**
|
|
536
545
|
* Handle external stall classification result.
|
|
537
546
|
* Called by the manager after onStallClassify resolves.
|
|
@@ -543,10 +552,21 @@ declare class PTYSession extends EventEmitter {
|
|
|
543
552
|
* being a no-op when already scheduled. This allows TUI agents that
|
|
544
553
|
* continue rendering decorative output (status bar, update notices) after
|
|
545
554
|
* the prompt to eventually settle, rather than having the timer cancelled
|
|
546
|
-
* by every new data chunk. The callback re-verifies
|
|
547
|
-
* transitioning, so stale triggers are safe.
|
|
555
|
+
* by every new data chunk. The callback re-verifies the task-complete
|
|
556
|
+
* signal before transitioning, so stale triggers are safe.
|
|
548
557
|
*/
|
|
549
558
|
private scheduleTaskComplete;
|
|
559
|
+
/**
|
|
560
|
+
* Adapter-level task completion check with compatibility fallback.
|
|
561
|
+
* Prefer detectTaskComplete() because detectReady() may be broad for TUIs.
|
|
562
|
+
*/
|
|
563
|
+
private isTaskCompleteSignal;
|
|
564
|
+
/**
|
|
565
|
+
* Claude-oriented task completion traces for PTY debugging.
|
|
566
|
+
* Enabled by config.traceTaskCompletion, otherwise defaults to enabled for Claude.
|
|
567
|
+
*/
|
|
568
|
+
private traceTaskCompletion;
|
|
569
|
+
private shouldTraceTaskCompletion;
|
|
550
570
|
/**
|
|
551
571
|
* Cancel a pending task_complete timer (new output arrived that
|
|
552
572
|
* doesn't match the idle prompt, so the agent is still working).
|
|
@@ -786,6 +806,65 @@ declare class PTYManager extends EventEmitter {
|
|
|
786
806
|
clearAutoResponseRules(sessionId: string): void;
|
|
787
807
|
}
|
|
788
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Task completion trace analysis helpers.
|
|
811
|
+
*
|
|
812
|
+
* Parses structured "Task completion trace" logs and builds a compact
|
|
813
|
+
* per-turn confidence timeline useful for debugging idle/completion detection.
|
|
814
|
+
*/
|
|
815
|
+
interface TaskCompletionTraceRecord {
|
|
816
|
+
sessionId?: string;
|
|
817
|
+
adapterType?: string;
|
|
818
|
+
event: string;
|
|
819
|
+
status?: string;
|
|
820
|
+
taskCompletePending?: boolean;
|
|
821
|
+
signal?: boolean;
|
|
822
|
+
wasPending?: boolean;
|
|
823
|
+
debounceMs?: number;
|
|
824
|
+
detectTaskComplete?: boolean;
|
|
825
|
+
detectReady?: boolean;
|
|
826
|
+
detectLoading?: boolean;
|
|
827
|
+
tailHash?: string;
|
|
828
|
+
tailSnippet?: string;
|
|
829
|
+
timestamp?: string | number | Date;
|
|
830
|
+
}
|
|
831
|
+
interface TaskCompletionTimelineStep {
|
|
832
|
+
event: string;
|
|
833
|
+
atIndex: number;
|
|
834
|
+
status: 'active' | 'active_loading' | 'likely_complete' | 'completed' | 'rejected';
|
|
835
|
+
confidence: number;
|
|
836
|
+
signal?: boolean;
|
|
837
|
+
detectTaskComplete?: boolean;
|
|
838
|
+
detectReady?: boolean;
|
|
839
|
+
detectLoading?: boolean;
|
|
840
|
+
}
|
|
841
|
+
interface TaskCompletionTurnTimeline {
|
|
842
|
+
turn: number;
|
|
843
|
+
startIndex: number;
|
|
844
|
+
endIndex: number;
|
|
845
|
+
completed: boolean;
|
|
846
|
+
maxConfidence: number;
|
|
847
|
+
finalConfidence: number;
|
|
848
|
+
events: TaskCompletionTimelineStep[];
|
|
849
|
+
}
|
|
850
|
+
interface TaskCompletionTimelineResult {
|
|
851
|
+
turns: TaskCompletionTurnTimeline[];
|
|
852
|
+
totalRecords: number;
|
|
853
|
+
ignoredRecords: number;
|
|
854
|
+
}
|
|
855
|
+
interface BuildTimelineOptions {
|
|
856
|
+
adapterType?: string;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Extract trace records from mixed log inputs.
|
|
860
|
+
* Accepts structured objects and JSON lines.
|
|
861
|
+
*/
|
|
862
|
+
declare function extractTaskCompletionTraceRecords(entries: Array<string | Record<string, unknown>>): TaskCompletionTraceRecord[];
|
|
863
|
+
/**
|
|
864
|
+
* Build a per-turn confidence timeline from task-completion traces.
|
|
865
|
+
*/
|
|
866
|
+
declare function buildTaskCompletionTimeline(records: TaskCompletionTraceRecord[], options?: BuildTimelineOptions): TaskCompletionTimelineResult;
|
|
867
|
+
|
|
789
868
|
/**
|
|
790
869
|
* Base CLI Adapter
|
|
791
870
|
*
|
|
@@ -1077,4 +1156,4 @@ declare function isBun(): boolean;
|
|
|
1077
1156
|
*/
|
|
1078
1157
|
declare function createPTYManager(options?: BunPTYManagerOptions): BunCompatiblePTYManager;
|
|
1079
1158
|
|
|
1080
|
-
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 };
|
|
1159
|
+
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);
|
|
@@ -461,8 +463,10 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
461
463
|
return;
|
|
462
464
|
}
|
|
463
465
|
const stripped = this.stripAnsiForStall(this.outputBuffer).trim();
|
|
466
|
+
const visible = this.stripAnsiForClassifier(this.outputBuffer).trim();
|
|
464
467
|
const tail = stripped.slice(-500);
|
|
465
|
-
const
|
|
468
|
+
const fallbackTail = visible.slice(-500);
|
|
469
|
+
const hash = this.simpleHash(tail || fallbackTail);
|
|
466
470
|
if (hash === this._lastContentHash) {
|
|
467
471
|
return;
|
|
468
472
|
}
|
|
@@ -537,10 +541,16 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
537
541
|
return;
|
|
538
542
|
}
|
|
539
543
|
const recentRaw = this.outputBuffer.slice(-2e3);
|
|
540
|
-
const recentOutput = this.
|
|
544
|
+
const recentOutput = this.stripAnsiForClassifier(recentRaw).trim();
|
|
541
545
|
const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
|
|
542
546
|
this.logger.debug(
|
|
543
|
-
{
|
|
547
|
+
{
|
|
548
|
+
sessionId: this.id,
|
|
549
|
+
stallDurationMs,
|
|
550
|
+
bufferTailLength: tail.length,
|
|
551
|
+
recentOutputLength: recentOutput.length,
|
|
552
|
+
recentOutputHash: this.simpleHash(recentOutput.slice(-500))
|
|
553
|
+
},
|
|
544
554
|
"Stall detected"
|
|
545
555
|
);
|
|
546
556
|
this.emit("stall_detected", recentOutput, stallDurationMs);
|
|
@@ -585,6 +595,23 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
585
595
|
result = result.replace(/ {2,}/g, " ");
|
|
586
596
|
return result;
|
|
587
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Less-aggressive ANSI stripping for classifier context.
|
|
600
|
+
* Preserves visible TUI symbols (e.g. ❯, ✻) and durations while removing
|
|
601
|
+
* escape/control sequences so the classifier keeps useful evidence.
|
|
602
|
+
*/
|
|
603
|
+
stripAnsiForClassifier(str) {
|
|
604
|
+
let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
|
|
605
|
+
result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
|
|
606
|
+
result = result.replace(/\x1b\[\d*[JK]/g, " ");
|
|
607
|
+
result = result.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*(?:\x07|\x1b\\)/g, "");
|
|
608
|
+
result = result.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "");
|
|
609
|
+
result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
610
|
+
result = result.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
|
|
611
|
+
result = result.replace(/\xa0/g, " ");
|
|
612
|
+
result = result.replace(/ {2,}/g, " ");
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
588
615
|
/**
|
|
589
616
|
* Handle external stall classification result.
|
|
590
617
|
* Called by the manager after onStallClassify resolves.
|
|
@@ -660,10 +687,15 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
660
687
|
* being a no-op when already scheduled. This allows TUI agents that
|
|
661
688
|
* continue rendering decorative output (status bar, update notices) after
|
|
662
689
|
* the prompt to eventually settle, rather than having the timer cancelled
|
|
663
|
-
* by every new data chunk. The callback re-verifies
|
|
664
|
-
* transitioning, so stale triggers are safe.
|
|
690
|
+
* by every new data chunk. The callback re-verifies the task-complete
|
|
691
|
+
* signal before transitioning, so stale triggers are safe.
|
|
665
692
|
*/
|
|
666
693
|
scheduleTaskComplete() {
|
|
694
|
+
const wasPending = this._taskCompletePending;
|
|
695
|
+
this.traceTaskCompletion("debounce_schedule", {
|
|
696
|
+
wasPending,
|
|
697
|
+
debounceMs: _PTYSession.TASK_COMPLETE_DEBOUNCE_MS
|
|
698
|
+
});
|
|
667
699
|
if (this._taskCompleteTimer) {
|
|
668
700
|
clearTimeout(this._taskCompleteTimer);
|
|
669
701
|
}
|
|
@@ -671,17 +703,72 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
671
703
|
this._taskCompleteTimer = setTimeout(() => {
|
|
672
704
|
this._taskCompleteTimer = null;
|
|
673
705
|
this._taskCompletePending = false;
|
|
674
|
-
|
|
675
|
-
|
|
706
|
+
const signal = this.isTaskCompleteSignal(this.outputBuffer);
|
|
707
|
+
this.traceTaskCompletion("debounce_fire", { signal });
|
|
708
|
+
if (this._status !== "busy") {
|
|
709
|
+
this.traceTaskCompletion("debounce_reject_status", { signal });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (!signal) {
|
|
713
|
+
this.traceTaskCompletion("debounce_reject_signal", { signal });
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
676
716
|
this._status = "ready";
|
|
677
717
|
this._lastBlockingPromptHash = null;
|
|
678
718
|
this.outputBuffer = "";
|
|
679
719
|
this.clearStallTimer();
|
|
680
720
|
this.emit("status_changed", "ready");
|
|
681
721
|
this.emit("task_complete");
|
|
722
|
+
this.traceTaskCompletion("transition_ready", { signal: true });
|
|
682
723
|
this.logger.info({ sessionId: this.id }, "Task complete \u2014 agent returned to idle prompt");
|
|
683
724
|
}, _PTYSession.TASK_COMPLETE_DEBOUNCE_MS);
|
|
684
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* Adapter-level task completion check with compatibility fallback.
|
|
728
|
+
* Prefer detectTaskComplete() because detectReady() may be broad for TUIs.
|
|
729
|
+
*/
|
|
730
|
+
isTaskCompleteSignal(output) {
|
|
731
|
+
if (this.adapter.detectTaskComplete) {
|
|
732
|
+
return this.adapter.detectTaskComplete(output);
|
|
733
|
+
}
|
|
734
|
+
return this.adapter.detectReady(output);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Claude-oriented task completion traces for PTY debugging.
|
|
738
|
+
* Enabled by config.traceTaskCompletion, otherwise defaults to enabled for Claude.
|
|
739
|
+
*/
|
|
740
|
+
traceTaskCompletion(event, ctx = {}) {
|
|
741
|
+
if (!this.shouldTraceTaskCompletion()) return;
|
|
742
|
+
const output = this.outputBuffer;
|
|
743
|
+
const detectTaskComplete = this.adapter.detectTaskComplete ? this.adapter.detectTaskComplete(output) : void 0;
|
|
744
|
+
const detectReady = this.adapter.detectReady(output);
|
|
745
|
+
const detectLoading = this.adapter.detectLoading ? this.adapter.detectLoading(output) : void 0;
|
|
746
|
+
const normalizedTail = this.stripAnsiForStall(output.slice(-280));
|
|
747
|
+
this.logger.debug(
|
|
748
|
+
{
|
|
749
|
+
sessionId: this.id,
|
|
750
|
+
adapterType: this.adapter.adapterType,
|
|
751
|
+
event,
|
|
752
|
+
status: this._status,
|
|
753
|
+
taskCompletePending: this._taskCompletePending,
|
|
754
|
+
signal: ctx.signal,
|
|
755
|
+
wasPending: ctx.wasPending,
|
|
756
|
+
debounceMs: ctx.debounceMs,
|
|
757
|
+
detectTaskComplete,
|
|
758
|
+
detectReady,
|
|
759
|
+
detectLoading,
|
|
760
|
+
tailHash: this.simpleHash(normalizedTail),
|
|
761
|
+
tailSnippet: normalizedTail.slice(-140)
|
|
762
|
+
},
|
|
763
|
+
"Task completion trace"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
shouldTraceTaskCompletion() {
|
|
767
|
+
if (typeof this.config.traceTaskCompletion === "boolean") {
|
|
768
|
+
return this.config.traceTaskCompletion;
|
|
769
|
+
}
|
|
770
|
+
return this.adapter.adapterType === "claude";
|
|
771
|
+
}
|
|
685
772
|
/**
|
|
686
773
|
* Cancel a pending task_complete timer (new output arrived that
|
|
687
774
|
* doesn't match the idle prompt, so the agent is still working).
|
|
@@ -834,7 +921,9 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
834
921
|
return;
|
|
835
922
|
}
|
|
836
923
|
if (this._status === "busy") {
|
|
837
|
-
|
|
924
|
+
const signal = this.isTaskCompleteSignal(this.outputBuffer);
|
|
925
|
+
if (this._taskCompletePending || signal) {
|
|
926
|
+
this.traceTaskCompletion("busy_signal", { signal });
|
|
838
927
|
this.scheduleTaskComplete();
|
|
839
928
|
}
|
|
840
929
|
}
|
|
@@ -861,7 +950,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
861
950
|
this.clearStallTimer();
|
|
862
951
|
this.emit("exit", exitDetection.code || 0);
|
|
863
952
|
}
|
|
864
|
-
if (this._status
|
|
953
|
+
if (this._status === "ready") {
|
|
865
954
|
this.tryParseOutput();
|
|
866
955
|
}
|
|
867
956
|
}
|
|
@@ -1585,6 +1674,171 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
1585
1674
|
}
|
|
1586
1675
|
};
|
|
1587
1676
|
|
|
1677
|
+
// src/task-completion-trace.ts
|
|
1678
|
+
function extractTaskCompletionTraceRecords(entries) {
|
|
1679
|
+
const out = [];
|
|
1680
|
+
for (const entry of entries) {
|
|
1681
|
+
let obj = null;
|
|
1682
|
+
if (typeof entry === "string") {
|
|
1683
|
+
const line = entry.trim();
|
|
1684
|
+
if (!line.startsWith("{") || !line.endsWith("}")) continue;
|
|
1685
|
+
try {
|
|
1686
|
+
obj = JSON.parse(line);
|
|
1687
|
+
} catch {
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
} else if (entry && typeof entry === "object") {
|
|
1691
|
+
obj = entry;
|
|
1692
|
+
}
|
|
1693
|
+
if (!obj) continue;
|
|
1694
|
+
if (obj.msg !== "Task completion trace") continue;
|
|
1695
|
+
if (typeof obj.event !== "string") continue;
|
|
1696
|
+
out.push({
|
|
1697
|
+
sessionId: asString(obj.sessionId),
|
|
1698
|
+
adapterType: asString(obj.adapterType),
|
|
1699
|
+
event: obj.event,
|
|
1700
|
+
status: asString(obj.status),
|
|
1701
|
+
taskCompletePending: asBool(obj.taskCompletePending),
|
|
1702
|
+
signal: asBool(obj.signal),
|
|
1703
|
+
wasPending: asBool(obj.wasPending),
|
|
1704
|
+
debounceMs: asNumber(obj.debounceMs),
|
|
1705
|
+
detectTaskComplete: asBool(obj.detectTaskComplete),
|
|
1706
|
+
detectReady: asBool(obj.detectReady),
|
|
1707
|
+
detectLoading: asBool(obj.detectLoading),
|
|
1708
|
+
tailHash: asString(obj.tailHash),
|
|
1709
|
+
tailSnippet: asString(obj.tailSnippet),
|
|
1710
|
+
timestamp: asTimestamp(obj.time) ?? asTimestamp(obj.timestamp)
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
return out;
|
|
1714
|
+
}
|
|
1715
|
+
function buildTaskCompletionTimeline(records, options = {}) {
|
|
1716
|
+
const filtered = records.filter((r) => {
|
|
1717
|
+
if (!options.adapterType) return true;
|
|
1718
|
+
return r.adapterType === options.adapterType;
|
|
1719
|
+
});
|
|
1720
|
+
const turns = [];
|
|
1721
|
+
let current = null;
|
|
1722
|
+
let ignored = 0;
|
|
1723
|
+
filtered.forEach((record, index) => {
|
|
1724
|
+
if (record.event === "busy_signal" && current && current.completed) {
|
|
1725
|
+
current = null;
|
|
1726
|
+
}
|
|
1727
|
+
if (!current) {
|
|
1728
|
+
current = {
|
|
1729
|
+
turn: turns.length + 1,
|
|
1730
|
+
startIndex: index,
|
|
1731
|
+
endIndex: index,
|
|
1732
|
+
completed: false,
|
|
1733
|
+
maxConfidence: 0,
|
|
1734
|
+
finalConfidence: 0,
|
|
1735
|
+
events: []
|
|
1736
|
+
};
|
|
1737
|
+
turns.push(current);
|
|
1738
|
+
}
|
|
1739
|
+
const step = toStep(record, index);
|
|
1740
|
+
if (!step) {
|
|
1741
|
+
ignored++;
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
current.events.push(step);
|
|
1745
|
+
current.endIndex = index;
|
|
1746
|
+
current.maxConfidence = Math.max(current.maxConfidence, step.confidence);
|
|
1747
|
+
current.finalConfidence = step.confidence;
|
|
1748
|
+
if (step.status === "completed") {
|
|
1749
|
+
current.completed = true;
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
return {
|
|
1753
|
+
turns,
|
|
1754
|
+
totalRecords: filtered.length,
|
|
1755
|
+
ignoredRecords: ignored
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function toStep(record, atIndex) {
|
|
1759
|
+
const event = record.event;
|
|
1760
|
+
const confidence = scoreConfidence(record);
|
|
1761
|
+
if (event === "transition_ready") {
|
|
1762
|
+
return withCommon(record, {
|
|
1763
|
+
event,
|
|
1764
|
+
atIndex,
|
|
1765
|
+
status: "completed",
|
|
1766
|
+
confidence: 100
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
if (event === "debounce_reject_signal" || event === "debounce_reject_status") {
|
|
1770
|
+
return withCommon(record, {
|
|
1771
|
+
event,
|
|
1772
|
+
atIndex,
|
|
1773
|
+
status: "rejected",
|
|
1774
|
+
confidence
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
if (record.detectLoading) {
|
|
1778
|
+
return withCommon(record, {
|
|
1779
|
+
event,
|
|
1780
|
+
atIndex,
|
|
1781
|
+
status: "active_loading",
|
|
1782
|
+
confidence
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
if (event === "debounce_fire" && record.signal) {
|
|
1786
|
+
return withCommon(record, {
|
|
1787
|
+
event,
|
|
1788
|
+
atIndex,
|
|
1789
|
+
status: "likely_complete",
|
|
1790
|
+
confidence
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
if (event === "busy_signal" || event === "debounce_schedule" || event === "debounce_fire") {
|
|
1794
|
+
return withCommon(record, {
|
|
1795
|
+
event,
|
|
1796
|
+
atIndex,
|
|
1797
|
+
status: "active",
|
|
1798
|
+
confidence
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
function scoreConfidence(record) {
|
|
1804
|
+
let score = 10;
|
|
1805
|
+
if (record.detectLoading) score -= 40;
|
|
1806
|
+
if (record.detectReady) score += 20;
|
|
1807
|
+
if (record.detectTaskComplete) score += 45;
|
|
1808
|
+
if (record.signal) score += 20;
|
|
1809
|
+
if (record.event === "debounce_reject_signal" || record.event === "debounce_reject_status") {
|
|
1810
|
+
score -= 30;
|
|
1811
|
+
}
|
|
1812
|
+
if (record.event === "transition_ready") score = 100;
|
|
1813
|
+
if (score < 0) return 0;
|
|
1814
|
+
if (score > 100) return 100;
|
|
1815
|
+
return score;
|
|
1816
|
+
}
|
|
1817
|
+
function withCommon(record, step) {
|
|
1818
|
+
return {
|
|
1819
|
+
...step,
|
|
1820
|
+
signal: record.signal,
|
|
1821
|
+
detectTaskComplete: record.detectTaskComplete,
|
|
1822
|
+
detectReady: record.detectReady,
|
|
1823
|
+
detectLoading: record.detectLoading
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
function asString(value) {
|
|
1827
|
+
return typeof value === "string" ? value : void 0;
|
|
1828
|
+
}
|
|
1829
|
+
function asBool(value) {
|
|
1830
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1831
|
+
}
|
|
1832
|
+
function asNumber(value) {
|
|
1833
|
+
return typeof value === "number" ? value : void 0;
|
|
1834
|
+
}
|
|
1835
|
+
function asTimestamp(value) {
|
|
1836
|
+
if (typeof value === "string" || typeof value === "number" || value instanceof Date) {
|
|
1837
|
+
return value;
|
|
1838
|
+
}
|
|
1839
|
+
return void 0;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1588
1842
|
// src/adapters/base-adapter.ts
|
|
1589
1843
|
var import_child_process = require("child_process");
|
|
1590
1844
|
var BaseCLIAdapter = class {
|
|
@@ -2511,8 +2765,10 @@ function createPTYManager(options) {
|
|
|
2511
2765
|
PTYSession,
|
|
2512
2766
|
SPECIAL_KEYS,
|
|
2513
2767
|
ShellAdapter,
|
|
2768
|
+
buildTaskCompletionTimeline,
|
|
2514
2769
|
createAdapter,
|
|
2515
2770
|
createPTYManager,
|
|
2771
|
+
extractTaskCompletionTraceRecords,
|
|
2516
2772
|
isBun
|
|
2517
2773
|
});
|
|
2518
2774
|
//# sourceMappingURL=index.js.map
|