pty-manager 1.3.0 → 1.3.2
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 +3 -1
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +65 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +65 -41
- package/dist/index.mjs.map +1 -1
- package/dist/pty-worker.js +65 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -445,7 +445,9 @@ await session.selectMenuOption(2); // Sends Down, Down, Enter with 50ms delays
|
|
|
445
445
|
|
|
446
446
|
## Stall Detection & Task Completion
|
|
447
447
|
|
|
448
|
-
Content-based stall detection monitors sessions for output that stops changing.
|
|
448
|
+
Content-based stall detection monitors sessions for output that stops changing. The content hash normalizes ANSI escape codes, TUI spinner characters, and countdown/duration text (e.g. `8m 17s` → constant) so that live timers 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.
|
|
449
|
+
|
|
450
|
+
When a stall is detected, the session first tries the adapter's `detectTaskComplete()` fast-path. If the adapter recognizes the output as a completed task (e.g. duration summary + idle prompt), it transitions directly to `ready` and emits `task_complete` — skipping the expensive LLM stall classifier entirely.
|
|
449
451
|
|
|
450
452
|
If the adapter doesn't recognize the output, the session falls back to emitting `stall_detected` for external classification.
|
|
451
453
|
|
package/dist/index.d.mts
CHANGED
|
@@ -446,6 +446,8 @@ declare class PTYSession extends EventEmitter {
|
|
|
446
446
|
private _lastContentHash;
|
|
447
447
|
private _taskCompleteTimer;
|
|
448
448
|
private static readonly TASK_COMPLETE_DEBOUNCE_MS;
|
|
449
|
+
private _processScheduled;
|
|
450
|
+
private static readonly MAX_OUTPUT_BUFFER;
|
|
449
451
|
readonly id: string;
|
|
450
452
|
readonly config: SpawnConfig;
|
|
451
453
|
constructor(adapter: CLIAdapter, config: SpawnConfig, logger?: Logger, stallDetectionEnabled?: boolean, defaultStallTimeoutMs?: number);
|
|
@@ -533,6 +535,12 @@ declare class PTYSession extends EventEmitter {
|
|
|
533
535
|
* Set up event handlers for the PTY
|
|
534
536
|
*/
|
|
535
537
|
private setupEventHandlers;
|
|
538
|
+
/**
|
|
539
|
+
* Process the accumulated output buffer.
|
|
540
|
+
* Called via setImmediate() from the onData handler so that heavy regex
|
|
541
|
+
* work runs in its own event-loop tick, not inside node-pty's native callback.
|
|
542
|
+
*/
|
|
543
|
+
private processOutputBuffer;
|
|
536
544
|
/**
|
|
537
545
|
* Detect blocking prompts and handle them with auto-responses or user notification.
|
|
538
546
|
* Deduplicates emissions - won't re-emit the same blocking prompt repeatedly.
|
package/dist/index.d.ts
CHANGED
|
@@ -446,6 +446,8 @@ declare class PTYSession extends EventEmitter {
|
|
|
446
446
|
private _lastContentHash;
|
|
447
447
|
private _taskCompleteTimer;
|
|
448
448
|
private static readonly TASK_COMPLETE_DEBOUNCE_MS;
|
|
449
|
+
private _processScheduled;
|
|
450
|
+
private static readonly MAX_OUTPUT_BUFFER;
|
|
449
451
|
readonly id: string;
|
|
450
452
|
readonly config: SpawnConfig;
|
|
451
453
|
constructor(adapter: CLIAdapter, config: SpawnConfig, logger?: Logger, stallDetectionEnabled?: boolean, defaultStallTimeoutMs?: number);
|
|
@@ -533,6 +535,12 @@ declare class PTYSession extends EventEmitter {
|
|
|
533
535
|
* Set up event handlers for the PTY
|
|
534
536
|
*/
|
|
535
537
|
private setupEventHandlers;
|
|
538
|
+
/**
|
|
539
|
+
* Process the accumulated output buffer.
|
|
540
|
+
* Called via setImmediate() from the onData handler so that heavy regex
|
|
541
|
+
* work runs in its own event-loop tick, not inside node-pty's native callback.
|
|
542
|
+
*/
|
|
543
|
+
private processOutputBuffer;
|
|
536
544
|
/**
|
|
537
545
|
* Detect blocking prompts and handle them with auto-responses or user notification.
|
|
538
546
|
* Deduplicates emissions - won't re-emit the same blocking prompt repeatedly.
|
package/dist/index.js
CHANGED
|
@@ -347,6 +347,12 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
347
347
|
// Task completion detection (idle detection when busy)
|
|
348
348
|
_taskCompleteTimer = null;
|
|
349
349
|
static TASK_COMPLETE_DEBOUNCE_MS = 1500;
|
|
350
|
+
// Deferred output processing — prevents node-pty's synchronous data
|
|
351
|
+
// delivery from starving the event loop (timers, I/O callbacks, etc.)
|
|
352
|
+
_processScheduled = false;
|
|
353
|
+
// Output buffer cap — prevents unbounded growth during long tasks
|
|
354
|
+
static MAX_OUTPUT_BUFFER = 1e5;
|
|
355
|
+
// 100 KB
|
|
350
356
|
id;
|
|
351
357
|
config;
|
|
352
358
|
get status() {
|
|
@@ -541,6 +547,7 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
541
547
|
result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
542
548
|
result = result.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
543
549
|
result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓⬆⬇◆◇▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥]/g, " ");
|
|
550
|
+
result = result.replace(/\d+[hms](?:\s+\d+[hms])*/g, "0s");
|
|
544
551
|
result = result.replace(/ {2,}/g, " ");
|
|
545
552
|
return result;
|
|
546
553
|
}
|
|
@@ -688,49 +695,16 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
688
695
|
this.ptyProcess.onData((data) => {
|
|
689
696
|
this._lastActivityAt = /* @__PURE__ */ new Date();
|
|
690
697
|
this.outputBuffer += data;
|
|
691
|
-
if (this.
|
|
692
|
-
this.
|
|
698
|
+
if (this.outputBuffer.length > _PTYSession.MAX_OUTPUT_BUFFER) {
|
|
699
|
+
this.outputBuffer = this.outputBuffer.slice(-_PTYSession.MAX_OUTPUT_BUFFER);
|
|
693
700
|
}
|
|
694
701
|
this.emit("output", data);
|
|
695
|
-
if (
|
|
696
|
-
this.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
this.logger.info({ sessionId: this.id }, "Session ready");
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
|
|
705
|
-
this.scheduleTaskComplete();
|
|
706
|
-
} else {
|
|
707
|
-
this.cancelTaskComplete();
|
|
708
|
-
}
|
|
709
|
-
const blockingPrompt = this.detectAndHandleBlockingPrompt();
|
|
710
|
-
if (blockingPrompt) {
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
if (this._status !== "ready" && this._status !== "busy") {
|
|
714
|
-
const loginDetection = this.adapter.detectLogin(this.outputBuffer);
|
|
715
|
-
if (loginDetection.required && this._status !== "authenticating") {
|
|
716
|
-
this._status = "authenticating";
|
|
717
|
-
this.clearStallTimer();
|
|
718
|
-
this.emit("login_required", loginDetection.instructions, loginDetection.url);
|
|
719
|
-
this.logger.warn(
|
|
720
|
-
{ sessionId: this.id, loginType: loginDetection.type },
|
|
721
|
-
"Login required"
|
|
722
|
-
);
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
const exitDetection = this.adapter.detectExit(this.outputBuffer);
|
|
727
|
-
if (exitDetection.exited) {
|
|
728
|
-
this._status = "stopped";
|
|
729
|
-
this.clearStallTimer();
|
|
730
|
-
this.emit("exit", exitDetection.code || 0);
|
|
731
|
-
}
|
|
732
|
-
if (this._status !== "starting" && this._status !== "authenticating") {
|
|
733
|
-
this.tryParseOutput();
|
|
702
|
+
if (!this._processScheduled) {
|
|
703
|
+
this._processScheduled = true;
|
|
704
|
+
setImmediate(() => {
|
|
705
|
+
this._processScheduled = false;
|
|
706
|
+
this.processOutputBuffer();
|
|
707
|
+
});
|
|
734
708
|
}
|
|
735
709
|
});
|
|
736
710
|
this.ptyProcess.onExit(({ exitCode, signal }) => {
|
|
@@ -743,6 +717,56 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
743
717
|
this.emit("exit", exitCode);
|
|
744
718
|
});
|
|
745
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Process the accumulated output buffer.
|
|
722
|
+
* Called via setImmediate() from the onData handler so that heavy regex
|
|
723
|
+
* work runs in its own event-loop tick, not inside node-pty's native callback.
|
|
724
|
+
*/
|
|
725
|
+
processOutputBuffer() {
|
|
726
|
+
if (this._status === "busy" || this._status === "authenticating") {
|
|
727
|
+
this.resetStallTimer();
|
|
728
|
+
}
|
|
729
|
+
if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
|
|
730
|
+
this._status = "ready";
|
|
731
|
+
this._lastBlockingPromptHash = null;
|
|
732
|
+
this.outputBuffer = "";
|
|
733
|
+
this.clearStallTimer();
|
|
734
|
+
this.emit("ready");
|
|
735
|
+
this.logger.info({ sessionId: this.id }, "Session ready");
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
|
|
739
|
+
this.scheduleTaskComplete();
|
|
740
|
+
} else {
|
|
741
|
+
this.cancelTaskComplete();
|
|
742
|
+
}
|
|
743
|
+
const blockingPrompt = this.detectAndHandleBlockingPrompt();
|
|
744
|
+
if (blockingPrompt) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (this._status !== "ready" && this._status !== "busy") {
|
|
748
|
+
const loginDetection = this.adapter.detectLogin(this.outputBuffer);
|
|
749
|
+
if (loginDetection.required && this._status !== "authenticating") {
|
|
750
|
+
this._status = "authenticating";
|
|
751
|
+
this.clearStallTimer();
|
|
752
|
+
this.emit("login_required", loginDetection.instructions, loginDetection.url);
|
|
753
|
+
this.logger.warn(
|
|
754
|
+
{ sessionId: this.id, loginType: loginDetection.type },
|
|
755
|
+
"Login required"
|
|
756
|
+
);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const exitDetection = this.adapter.detectExit(this.outputBuffer);
|
|
761
|
+
if (exitDetection.exited) {
|
|
762
|
+
this._status = "stopped";
|
|
763
|
+
this.clearStallTimer();
|
|
764
|
+
this.emit("exit", exitDetection.code || 0);
|
|
765
|
+
}
|
|
766
|
+
if (this._status !== "starting" && this._status !== "authenticating") {
|
|
767
|
+
this.tryParseOutput();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
746
770
|
/**
|
|
747
771
|
* Detect blocking prompts and handle them with auto-responses or user notification.
|
|
748
772
|
* Deduplicates emissions - won't re-emit the same blocking prompt repeatedly.
|