pty-manager 1.5.0 → 1.6.0
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 +48 -1
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +31 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31 -4
- package/dist/index.mjs.map +1 -1
- package/dist/pty-worker.js +31 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ PTY session manager with lifecycle management, pluggable adapters, and blocking
|
|
|
9
9
|
- **Blocking prompt detection** - Detect login prompts, confirmations, and interactive prompts
|
|
10
10
|
- **Auto-response rules** - Automatically respond to known prompts with text or key sequences
|
|
11
11
|
- **TUI menu navigation** - Navigate arrow-key menus via `selectMenuOption()` and key-sequence rules
|
|
12
|
-
- **Stall detection** - Content-based stall detection with pluggable external classifiers
|
|
12
|
+
- **Stall detection** - Content-based stall detection with pluggable external classifiers, loading suppression, and exponential backoff
|
|
13
13
|
- **Task completion detection** - Adapter-level fast-path that short-circuits the LLM stall classifier when the CLI returns to its idle prompt
|
|
14
14
|
- **Terminal attachment** - Attach to sessions for raw I/O streaming
|
|
15
15
|
- **Special key support** - Send Ctrl, Alt, Shift, and function key combinations via `sendKeys()`
|
|
@@ -136,6 +136,11 @@ class MyCLIAdapter extends BaseCLIAdapter {
|
|
|
136
136
|
return /done in \d+s/.test(output) && /ready>/.test(output);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Optional: detect active loading state (suppresses stall detection)
|
|
140
|
+
detectLoading(output) {
|
|
141
|
+
return /processing|loading/i.test(output);
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
parseOutput(output) {
|
|
140
145
|
return {
|
|
141
146
|
type: 'response',
|
|
@@ -210,6 +215,7 @@ interface SpawnConfig {
|
|
|
210
215
|
cols?: number; // Terminal columns (default: 120)
|
|
211
216
|
rows?: number; // Terminal rows (default: 40)
|
|
212
217
|
timeout?: number; // Session timeout in ms
|
|
218
|
+
readySettleMs?: number; // Override adapter's ready settle delay
|
|
213
219
|
}
|
|
214
220
|
```
|
|
215
221
|
|
|
@@ -457,6 +463,16 @@ class MyCLIAdapter extends BaseCLIAdapter {
|
|
|
457
463
|
}
|
|
458
464
|
```
|
|
459
465
|
|
|
466
|
+
The settle delay can also be overridden per-spawn via `SpawnConfig.readySettleMs`, which takes precedence over the adapter default. This lets orchestrators tune the delay for varying environments (CI, remote containers, local dev) without forking adapters:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
const handle = await manager.spawn({
|
|
470
|
+
name: 'agent',
|
|
471
|
+
type: 'claude',
|
|
472
|
+
readySettleMs: 1000, // Slow CI environment — wait longer
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
460
476
|
## Stall Detection & Task Completion
|
|
461
477
|
|
|
462
478
|
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.
|
|
@@ -510,6 +526,37 @@ The default `BaseCLIAdapter` implementation delegates to `detectReady()`. Coding
|
|
|
510
526
|
| Codex | `Worked for 1m 05s` separator + `›` prompt |
|
|
511
527
|
| Aider | `Aider is waiting for your input`, mode prompts with edit markers |
|
|
512
528
|
|
|
529
|
+
### Loading Pattern Suppression
|
|
530
|
+
|
|
531
|
+
Adapters can implement `detectLoading(output)` to detect when the CLI is actively working — thinking spinners, file reading progress, model streaming indicators. When `detectLoading()` returns `true`, stall detection is suppressed entirely because the agent is provably working, just not producing new visible text.
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
class MyCLIAdapter extends BaseCLIAdapter {
|
|
535
|
+
detectLoading(output: string): boolean {
|
|
536
|
+
// Match loading indicators specific to this CLI
|
|
537
|
+
return /esc to interrupt/i.test(output) || /Reading \d+ files/i.test(output);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
This avoids unnecessary LLM classifier calls during normal operation and prevents false stall alerts when agents are thinking or reading files.
|
|
543
|
+
|
|
544
|
+
### Stall Backoff
|
|
545
|
+
|
|
546
|
+
When the external stall classifier returns `still_working` (or `null`), the next stall check interval doubles exponentially instead of repeating at the base rate. This prevents hammering the classifier every few seconds during long-running tasks.
|
|
547
|
+
|
|
548
|
+
- Base interval: `stallTimeoutMs` (default: 8000ms)
|
|
549
|
+
- After each `still_working`: interval doubles (8s → 16s → 30s cap)
|
|
550
|
+
- Maximum interval: 30 seconds
|
|
551
|
+
- Reset: backoff resets to the base interval whenever new real content arrives (content hash changes)
|
|
552
|
+
|
|
553
|
+
```
|
|
554
|
+
First stall check: 8s → classifier says "still_working"
|
|
555
|
+
Second check: 16s → classifier says "still_working"
|
|
556
|
+
Third check: 30s → (capped at 30s)
|
|
557
|
+
New output arrives: → backoff resets to 8s
|
|
558
|
+
```
|
|
559
|
+
|
|
513
560
|
## Blocking Prompt Types
|
|
514
561
|
|
|
515
562
|
The library recognizes these blocking prompt types:
|
package/dist/index.d.mts
CHANGED
|
@@ -37,6 +37,9 @@ interface SpawnConfig {
|
|
|
37
37
|
adapterConfig?: Record<string, unknown>;
|
|
38
38
|
/** Per-session stall timeout in ms. Overrides PTYManagerConfig.stallTimeoutMs. */
|
|
39
39
|
stallTimeoutMs?: number;
|
|
40
|
+
/** Override adapter's readySettleMs for this session.
|
|
41
|
+
* Ms of output silence after detectReady match before emitting session_ready. */
|
|
42
|
+
readySettleMs?: number;
|
|
40
43
|
/** Override or disable specific adapter auto-response rules for this session.
|
|
41
44
|
* Keys are regex source strings (from rule.pattern.source).
|
|
42
45
|
* - null value disables that rule entirely
|
|
@@ -353,6 +356,15 @@ interface CLIAdapter {
|
|
|
353
356
|
}>;
|
|
354
357
|
/** Ms of output silence after detectReady match before emitting session_ready (default: 100) */
|
|
355
358
|
readonly readySettleMs?: number;
|
|
359
|
+
/**
|
|
360
|
+
* Optional: Detect if the CLI is actively loading/processing (thinking spinner,
|
|
361
|
+
* file reading, model streaming, etc.). When true, stall detection is suppressed
|
|
362
|
+
* because the agent is provably working — just not producing new visible text.
|
|
363
|
+
*
|
|
364
|
+
* Patterns should match active loading indicators like "esc to interrupt",
|
|
365
|
+
* "Reading N files", "Waiting for LLM", etc.
|
|
366
|
+
*/
|
|
367
|
+
detectLoading?(output: string): boolean;
|
|
356
368
|
/**
|
|
357
369
|
* Optional: Get health check command
|
|
358
370
|
*/
|
|
@@ -446,6 +458,8 @@ declare class PTYSession extends EventEmitter {
|
|
|
446
458
|
private _lastStallHash;
|
|
447
459
|
private _stallStartedAt;
|
|
448
460
|
private _lastContentHash;
|
|
461
|
+
private _stallBackoffMs;
|
|
462
|
+
private static readonly MAX_STALL_BACKOFF_MS;
|
|
449
463
|
private _taskCompleteTimer;
|
|
450
464
|
private static readonly TASK_COMPLETE_DEBOUNCE_MS;
|
|
451
465
|
private _readySettleTimer;
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,9 @@ interface SpawnConfig {
|
|
|
37
37
|
adapterConfig?: Record<string, unknown>;
|
|
38
38
|
/** Per-session stall timeout in ms. Overrides PTYManagerConfig.stallTimeoutMs. */
|
|
39
39
|
stallTimeoutMs?: number;
|
|
40
|
+
/** Override adapter's readySettleMs for this session.
|
|
41
|
+
* Ms of output silence after detectReady match before emitting session_ready. */
|
|
42
|
+
readySettleMs?: number;
|
|
40
43
|
/** Override or disable specific adapter auto-response rules for this session.
|
|
41
44
|
* Keys are regex source strings (from rule.pattern.source).
|
|
42
45
|
* - null value disables that rule entirely
|
|
@@ -353,6 +356,15 @@ interface CLIAdapter {
|
|
|
353
356
|
}>;
|
|
354
357
|
/** Ms of output silence after detectReady match before emitting session_ready (default: 100) */
|
|
355
358
|
readonly readySettleMs?: number;
|
|
359
|
+
/**
|
|
360
|
+
* Optional: Detect if the CLI is actively loading/processing (thinking spinner,
|
|
361
|
+
* file reading, model streaming, etc.). When true, stall detection is suppressed
|
|
362
|
+
* because the agent is provably working — just not producing new visible text.
|
|
363
|
+
*
|
|
364
|
+
* Patterns should match active loading indicators like "esc to interrupt",
|
|
365
|
+
* "Reading N files", "Waiting for LLM", etc.
|
|
366
|
+
*/
|
|
367
|
+
detectLoading?(output: string): boolean;
|
|
356
368
|
/**
|
|
357
369
|
* Optional: Get health check command
|
|
358
370
|
*/
|
|
@@ -446,6 +458,8 @@ declare class PTYSession extends EventEmitter {
|
|
|
446
458
|
private _lastStallHash;
|
|
447
459
|
private _stallStartedAt;
|
|
448
460
|
private _lastContentHash;
|
|
461
|
+
private _stallBackoffMs;
|
|
462
|
+
private static readonly MAX_STALL_BACKOFF_MS;
|
|
449
463
|
private _taskCompleteTimer;
|
|
450
464
|
private static readonly TASK_COMPLETE_DEBOUNCE_MS;
|
|
451
465
|
private _readySettleTimer;
|
package/dist/index.js
CHANGED
|
@@ -315,6 +315,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
315
315
|
this.logger = logger || consoleLogger;
|
|
316
316
|
this._stallDetectionEnabled = stallDetectionEnabled ?? false;
|
|
317
317
|
this._stallTimeoutMs = config.stallTimeoutMs ?? defaultStallTimeoutMs ?? 8e3;
|
|
318
|
+
this._stallBackoffMs = this._stallTimeoutMs;
|
|
318
319
|
if (config.ruleOverrides) {
|
|
319
320
|
for (const [key, value] of Object.entries(config.ruleOverrides)) {
|
|
320
321
|
if (value === null) {
|
|
@@ -344,6 +345,9 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
344
345
|
_lastStallHash = null;
|
|
345
346
|
_stallStartedAt = null;
|
|
346
347
|
_lastContentHash = null;
|
|
348
|
+
_stallBackoffMs = 0;
|
|
349
|
+
// Initialized in constructor from _stallTimeoutMs
|
|
350
|
+
static MAX_STALL_BACKOFF_MS = 3e4;
|
|
347
351
|
// Task completion detection (idle detection when busy)
|
|
348
352
|
_taskCompleteTimer = null;
|
|
349
353
|
static TASK_COMPLETE_DEBOUNCE_MS = 1500;
|
|
@@ -466,6 +470,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
466
470
|
}
|
|
467
471
|
this._stallStartedAt = Date.now();
|
|
468
472
|
this._lastStallHash = null;
|
|
473
|
+
this._stallBackoffMs = this._stallTimeoutMs;
|
|
469
474
|
this._stallTimer = setTimeout(() => {
|
|
470
475
|
this.onStallTimerFired();
|
|
471
476
|
}, this._stallTimeoutMs);
|
|
@@ -480,6 +485,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
480
485
|
}
|
|
481
486
|
this._stallStartedAt = null;
|
|
482
487
|
this._lastContentHash = null;
|
|
488
|
+
this._stallBackoffMs = this._stallTimeoutMs;
|
|
483
489
|
}
|
|
484
490
|
/**
|
|
485
491
|
* Called when the stall timer fires (no output for stallTimeoutMs).
|
|
@@ -488,10 +494,18 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
488
494
|
if (this._status !== "busy" && this._status !== "authenticating") {
|
|
489
495
|
return;
|
|
490
496
|
}
|
|
497
|
+
if (this.adapter.detectLoading?.(this.outputBuffer)) {
|
|
498
|
+
this.logger.debug(
|
|
499
|
+
{ sessionId: this.id },
|
|
500
|
+
"Loading pattern detected \u2014 suppressing stall emission"
|
|
501
|
+
);
|
|
502
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
491
505
|
const tail = this.outputBuffer.slice(-500);
|
|
492
506
|
const hash = this.simpleHash(tail);
|
|
493
507
|
if (hash === this._lastStallHash) {
|
|
494
|
-
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this.
|
|
508
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
|
|
495
509
|
return;
|
|
496
510
|
}
|
|
497
511
|
this._lastStallHash = hash;
|
|
@@ -516,7 +530,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
516
530
|
"Stall detected"
|
|
517
531
|
);
|
|
518
532
|
this.emit("stall_detected", recentOutput, stallDurationMs);
|
|
519
|
-
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this.
|
|
533
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
|
|
520
534
|
}
|
|
521
535
|
/**
|
|
522
536
|
* Promise-based delay helper.
|
|
@@ -563,8 +577,21 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
563
577
|
return;
|
|
564
578
|
}
|
|
565
579
|
if (!classification || classification.state === "still_working") {
|
|
580
|
+
this._stallBackoffMs = Math.min(
|
|
581
|
+
this._stallBackoffMs * 2,
|
|
582
|
+
_PTYSession.MAX_STALL_BACKOFF_MS
|
|
583
|
+
);
|
|
584
|
+
this.logger.debug(
|
|
585
|
+
{ sessionId: this.id, nextCheckMs: this._stallBackoffMs },
|
|
586
|
+
"Still working \u2014 backing off stall check interval"
|
|
587
|
+
);
|
|
566
588
|
this._lastContentHash = null;
|
|
567
|
-
this.
|
|
589
|
+
this._lastStallHash = null;
|
|
590
|
+
if (this._stallTimer) {
|
|
591
|
+
clearTimeout(this._stallTimer);
|
|
592
|
+
this._stallTimer = null;
|
|
593
|
+
}
|
|
594
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
|
|
568
595
|
return;
|
|
569
596
|
}
|
|
570
597
|
switch (classification.state) {
|
|
@@ -654,7 +681,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
654
681
|
if (this._readySettleTimer) {
|
|
655
682
|
clearTimeout(this._readySettleTimer);
|
|
656
683
|
}
|
|
657
|
-
const settleMs = this.adapter.readySettleMs ?? 100;
|
|
684
|
+
const settleMs = this.config.readySettleMs ?? this.adapter.readySettleMs ?? 100;
|
|
658
685
|
this._readySettleTimer = setTimeout(() => {
|
|
659
686
|
this._readySettleTimer = null;
|
|
660
687
|
this._readySettlePending = false;
|