pty-manager 1.3.1 → 1.3.3
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 +67 -44
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +67 -44
- package/dist/index.mjs.map +1 -1
- package/dist/pty-worker.js +67 -44
- 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. The content hash
|
|
448
|
+
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.
|
|
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() {
|
|
@@ -444,9 +450,9 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
444
450
|
this.clearStallTimer();
|
|
445
451
|
return;
|
|
446
452
|
}
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
const hash = this.simpleHash(
|
|
453
|
+
const stripped = this.stripAnsiForStall(this.outputBuffer).trim();
|
|
454
|
+
const tail = stripped.slice(-500);
|
|
455
|
+
const hash = this.simpleHash(tail);
|
|
450
456
|
if (hash === this._lastContentHash) {
|
|
451
457
|
return;
|
|
452
458
|
}
|
|
@@ -689,49 +695,16 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
689
695
|
this.ptyProcess.onData((data) => {
|
|
690
696
|
this._lastActivityAt = /* @__PURE__ */ new Date();
|
|
691
697
|
this.outputBuffer += data;
|
|
692
|
-
if (this.
|
|
693
|
-
this.
|
|
698
|
+
if (this.outputBuffer.length > _PTYSession.MAX_OUTPUT_BUFFER) {
|
|
699
|
+
this.outputBuffer = this.outputBuffer.slice(-_PTYSession.MAX_OUTPUT_BUFFER);
|
|
694
700
|
}
|
|
695
701
|
this.emit("output", data);
|
|
696
|
-
if (
|
|
697
|
-
this.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
this.logger.info({ sessionId: this.id }, "Session ready");
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
|
|
706
|
-
this.scheduleTaskComplete();
|
|
707
|
-
} else {
|
|
708
|
-
this.cancelTaskComplete();
|
|
709
|
-
}
|
|
710
|
-
const blockingPrompt = this.detectAndHandleBlockingPrompt();
|
|
711
|
-
if (blockingPrompt) {
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
if (this._status !== "ready" && this._status !== "busy") {
|
|
715
|
-
const loginDetection = this.adapter.detectLogin(this.outputBuffer);
|
|
716
|
-
if (loginDetection.required && this._status !== "authenticating") {
|
|
717
|
-
this._status = "authenticating";
|
|
718
|
-
this.clearStallTimer();
|
|
719
|
-
this.emit("login_required", loginDetection.instructions, loginDetection.url);
|
|
720
|
-
this.logger.warn(
|
|
721
|
-
{ sessionId: this.id, loginType: loginDetection.type },
|
|
722
|
-
"Login required"
|
|
723
|
-
);
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
const exitDetection = this.adapter.detectExit(this.outputBuffer);
|
|
728
|
-
if (exitDetection.exited) {
|
|
729
|
-
this._status = "stopped";
|
|
730
|
-
this.clearStallTimer();
|
|
731
|
-
this.emit("exit", exitDetection.code || 0);
|
|
732
|
-
}
|
|
733
|
-
if (this._status !== "starting" && this._status !== "authenticating") {
|
|
734
|
-
this.tryParseOutput();
|
|
702
|
+
if (!this._processScheduled) {
|
|
703
|
+
this._processScheduled = true;
|
|
704
|
+
setImmediate(() => {
|
|
705
|
+
this._processScheduled = false;
|
|
706
|
+
this.processOutputBuffer();
|
|
707
|
+
});
|
|
735
708
|
}
|
|
736
709
|
});
|
|
737
710
|
this.ptyProcess.onExit(({ exitCode, signal }) => {
|
|
@@ -744,6 +717,56 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
|
|
|
744
717
|
this.emit("exit", exitCode);
|
|
745
718
|
});
|
|
746
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
|
+
}
|
|
747
770
|
/**
|
|
748
771
|
* Detect blocking prompts and handle them with auto-responses or user notification.
|
|
749
772
|
* Deduplicates emissions - won't re-emit the same blocking prompt repeatedly.
|