pty-manager 1.2.13 → 1.2.14
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/dist/index.d.mts +81 -2
- package/dist/index.d.ts +81 -2
- package/dist/index.js +223 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +223 -4
- package/dist/index.mjs.map +1 -1
- package/dist/pty-worker.js +223 -3
- package/package.json +1 -1
package/dist/pty-worker.js
CHANGED
|
@@ -290,12 +290,14 @@ var SPECIAL_KEYS = {
|
|
|
290
290
|
var BRACKETED_PASTE_START = "\x1B[200~";
|
|
291
291
|
var BRACKETED_PASTE_END = "\x1B[201~";
|
|
292
292
|
var PTYSession = class extends import_events.EventEmitter {
|
|
293
|
-
constructor(adapter, config, logger) {
|
|
293
|
+
constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
|
|
294
294
|
super();
|
|
295
295
|
this.adapter = adapter;
|
|
296
296
|
this.id = config.id || generateId();
|
|
297
297
|
this.config = { ...config, id: this.id };
|
|
298
298
|
this.logger = logger || consoleLogger;
|
|
299
|
+
this._stallDetectionEnabled = stallDetectionEnabled ?? false;
|
|
300
|
+
this._stallTimeoutMs = config.stallTimeoutMs ?? defaultStallTimeoutMs ?? 8e3;
|
|
299
301
|
}
|
|
300
302
|
ptyProcess = null;
|
|
301
303
|
outputBuffer = "";
|
|
@@ -306,6 +308,12 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
306
308
|
logger;
|
|
307
309
|
sessionRules = [];
|
|
308
310
|
_lastBlockingPromptHash = null;
|
|
311
|
+
// Stall detection
|
|
312
|
+
_stallTimer = null;
|
|
313
|
+
_stallTimeoutMs;
|
|
314
|
+
_stallDetectionEnabled;
|
|
315
|
+
_lastStallHash = null;
|
|
316
|
+
_stallStartedAt = null;
|
|
309
317
|
id;
|
|
310
318
|
config;
|
|
311
319
|
get status() {
|
|
@@ -387,6 +395,123 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
387
395
|
this.logger.debug({ sessionId: this.id }, "Cleared auto-response rules");
|
|
388
396
|
}
|
|
389
397
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
398
|
+
// Stall Detection
|
|
399
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
400
|
+
/**
|
|
401
|
+
* Start or reset the stall detection timer.
|
|
402
|
+
* Only active when status is "busy" and stall detection is enabled.
|
|
403
|
+
*/
|
|
404
|
+
resetStallTimer() {
|
|
405
|
+
this.clearStallTimer();
|
|
406
|
+
if (!this._stallDetectionEnabled || this._status !== "busy") {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this._stallStartedAt = Date.now();
|
|
410
|
+
this._lastStallHash = null;
|
|
411
|
+
this._stallTimer = setTimeout(() => {
|
|
412
|
+
this.onStallTimerFired();
|
|
413
|
+
}, this._stallTimeoutMs);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Clear the stall detection timer.
|
|
417
|
+
*/
|
|
418
|
+
clearStallTimer() {
|
|
419
|
+
if (this._stallTimer) {
|
|
420
|
+
clearTimeout(this._stallTimer);
|
|
421
|
+
this._stallTimer = null;
|
|
422
|
+
}
|
|
423
|
+
this._stallStartedAt = null;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Called when the stall timer fires (no output for stallTimeoutMs).
|
|
427
|
+
*/
|
|
428
|
+
onStallTimerFired() {
|
|
429
|
+
if (this._status !== "busy") {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const tail = this.outputBuffer.slice(-500);
|
|
433
|
+
const hash = this.simpleHash(tail);
|
|
434
|
+
if (hash === this._lastStallHash) {
|
|
435
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this._lastStallHash = hash;
|
|
439
|
+
const recentRaw = this.outputBuffer.slice(-2e3);
|
|
440
|
+
const recentOutput = this.stripAnsiForStall(recentRaw);
|
|
441
|
+
const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
|
|
442
|
+
this.logger.debug(
|
|
443
|
+
{ sessionId: this.id, stallDurationMs, bufferTailLength: tail.length },
|
|
444
|
+
"Stall detected"
|
|
445
|
+
);
|
|
446
|
+
this.emit("stall_detected", recentOutput, stallDurationMs);
|
|
447
|
+
this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Simple string hash for deduplication.
|
|
451
|
+
*/
|
|
452
|
+
simpleHash(str) {
|
|
453
|
+
let hash = 0;
|
|
454
|
+
for (let i = 0; i < str.length; i++) {
|
|
455
|
+
const char = str.charCodeAt(i);
|
|
456
|
+
hash = (hash << 5) - hash + char;
|
|
457
|
+
hash |= 0;
|
|
458
|
+
}
|
|
459
|
+
return hash.toString(36);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Strip ANSI codes for stall detection output.
|
|
463
|
+
* Replaces cursor-forward sequences with spaces first.
|
|
464
|
+
*/
|
|
465
|
+
stripAnsiForStall(str) {
|
|
466
|
+
const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
|
|
467
|
+
return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Handle external stall classification result.
|
|
471
|
+
* Called by the manager after onStallClassify resolves.
|
|
472
|
+
*/
|
|
473
|
+
handleStallClassification(classification) {
|
|
474
|
+
if (this._status !== "busy") {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!classification || classification.state === "still_working") {
|
|
478
|
+
this.resetStallTimer();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
switch (classification.state) {
|
|
482
|
+
case "waiting_for_input": {
|
|
483
|
+
const promptInfo = {
|
|
484
|
+
type: "stall_classified",
|
|
485
|
+
prompt: classification.prompt,
|
|
486
|
+
canAutoRespond: !!classification.suggestedResponse
|
|
487
|
+
};
|
|
488
|
+
if (classification.suggestedResponse) {
|
|
489
|
+
this.logger.info(
|
|
490
|
+
{ sessionId: this.id, response: classification.suggestedResponse },
|
|
491
|
+
"Auto-responding to stall-classified prompt"
|
|
492
|
+
);
|
|
493
|
+
this.writeRaw(classification.suggestedResponse + "\r");
|
|
494
|
+
this.emit("blocking_prompt", promptInfo, true);
|
|
495
|
+
} else {
|
|
496
|
+
this.emit("blocking_prompt", promptInfo, false);
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "task_complete":
|
|
501
|
+
this._status = "ready";
|
|
502
|
+
this._lastBlockingPromptHash = null;
|
|
503
|
+
this.outputBuffer = "";
|
|
504
|
+
this.clearStallTimer();
|
|
505
|
+
this.emit("ready");
|
|
506
|
+
this.logger.info({ sessionId: this.id }, "Stall classified as task_complete, transitioning to ready");
|
|
507
|
+
break;
|
|
508
|
+
case "error":
|
|
509
|
+
this.clearStallTimer();
|
|
510
|
+
this.emit("error", new Error(classification.prompt || "Stall classified as error"));
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
390
515
|
// Lifecycle
|
|
391
516
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
392
517
|
/**
|
|
@@ -444,11 +569,15 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
444
569
|
this.ptyProcess.onData((data) => {
|
|
445
570
|
this._lastActivityAt = /* @__PURE__ */ new Date();
|
|
446
571
|
this.outputBuffer += data;
|
|
572
|
+
if (this._status === "busy") {
|
|
573
|
+
this.resetStallTimer();
|
|
574
|
+
}
|
|
447
575
|
this.emit("output", data);
|
|
448
576
|
if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
|
|
449
577
|
this._status = "ready";
|
|
450
578
|
this._lastBlockingPromptHash = null;
|
|
451
579
|
this.outputBuffer = "";
|
|
580
|
+
this.clearStallTimer();
|
|
452
581
|
this.emit("ready");
|
|
453
582
|
this.logger.info({ sessionId: this.id }, "Session ready");
|
|
454
583
|
return;
|
|
@@ -461,6 +590,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
461
590
|
const loginDetection = this.adapter.detectLogin(this.outputBuffer);
|
|
462
591
|
if (loginDetection.required && this._status !== "authenticating") {
|
|
463
592
|
this._status = "authenticating";
|
|
593
|
+
this.clearStallTimer();
|
|
464
594
|
this.emit("login_required", loginDetection.instructions, loginDetection.url);
|
|
465
595
|
this.logger.warn(
|
|
466
596
|
{ sessionId: this.id, loginType: loginDetection.type },
|
|
@@ -472,6 +602,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
472
602
|
const exitDetection = this.adapter.detectExit(this.outputBuffer);
|
|
473
603
|
if (exitDetection.exited) {
|
|
474
604
|
this._status = "stopped";
|
|
605
|
+
this.clearStallTimer();
|
|
475
606
|
this.emit("exit", exitDetection.code || 0);
|
|
476
607
|
}
|
|
477
608
|
if (this._status !== "starting" && this._status !== "authenticating") {
|
|
@@ -480,6 +611,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
480
611
|
});
|
|
481
612
|
this.ptyProcess.onExit(({ exitCode, signal }) => {
|
|
482
613
|
this._status = "stopped";
|
|
614
|
+
this.clearStallTimer();
|
|
483
615
|
this.logger.info(
|
|
484
616
|
{ sessionId: this.id, exitCode, signal },
|
|
485
617
|
"PTY session exited"
|
|
@@ -555,8 +687,11 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
555
687
|
if (allRules.length === 0) {
|
|
556
688
|
return false;
|
|
557
689
|
}
|
|
690
|
+
let stripped = this.stripAnsiForStall(this.outputBuffer);
|
|
691
|
+
stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
|
|
692
|
+
stripped = stripped.replace(/ {2,}/g, " ");
|
|
558
693
|
for (const rule of allRules) {
|
|
559
|
-
if (rule.pattern.test(
|
|
694
|
+
if (rule.pattern.test(stripped)) {
|
|
560
695
|
const safe = rule.safe !== false;
|
|
561
696
|
const isSessionRule = this.sessionRules.includes(rule);
|
|
562
697
|
if (safe) {
|
|
@@ -647,6 +782,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
647
782
|
*/
|
|
648
783
|
send(message) {
|
|
649
784
|
this._status = "busy";
|
|
785
|
+
this.resetStallTimer();
|
|
650
786
|
const msg = {
|
|
651
787
|
id: `${this.id}-msg-${++this.messageCounter}`,
|
|
652
788
|
sessionId: this.id,
|
|
@@ -735,6 +871,7 @@ var PTYSession = class extends import_events.EventEmitter {
|
|
|
735
871
|
kill(signal) {
|
|
736
872
|
if (this.ptyProcess) {
|
|
737
873
|
this._status = "stopping";
|
|
874
|
+
this.clearStallTimer();
|
|
738
875
|
this.ptyProcess.kill(signal);
|
|
739
876
|
this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
|
|
740
877
|
}
|
|
@@ -804,11 +941,18 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
804
941
|
maxLogLines;
|
|
805
942
|
logger;
|
|
806
943
|
adapters;
|
|
944
|
+
// Stall detection config
|
|
945
|
+
_stallDetectionEnabled;
|
|
946
|
+
_stallTimeoutMs;
|
|
947
|
+
_onStallClassify;
|
|
807
948
|
constructor(config = {}) {
|
|
808
949
|
super();
|
|
809
950
|
this.adapters = new AdapterRegistry();
|
|
810
951
|
this.logger = config.logger || consoleLogger2;
|
|
811
952
|
this.maxLogLines = config.maxLogLines || 1e3;
|
|
953
|
+
this._stallDetectionEnabled = config.stallDetectionEnabled ?? false;
|
|
954
|
+
this._stallTimeoutMs = config.stallTimeoutMs ?? 8e3;
|
|
955
|
+
this._onStallClassify = config.onStallClassify;
|
|
812
956
|
}
|
|
813
957
|
/**
|
|
814
958
|
* Register a CLI adapter
|
|
@@ -831,7 +975,13 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
831
975
|
{ type: config.type, name: config.name },
|
|
832
976
|
"Spawning session"
|
|
833
977
|
);
|
|
834
|
-
const session = new PTYSession(
|
|
978
|
+
const session = new PTYSession(
|
|
979
|
+
adapter,
|
|
980
|
+
config,
|
|
981
|
+
this.logger,
|
|
982
|
+
this._stallDetectionEnabled,
|
|
983
|
+
this._stallTimeoutMs
|
|
984
|
+
);
|
|
835
985
|
this.setupSessionEvents(session);
|
|
836
986
|
this.sessions.set(session.id, session);
|
|
837
987
|
this.outputLogs.set(session.id, []);
|
|
@@ -875,6 +1025,21 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
875
1025
|
session.on("error", (error) => {
|
|
876
1026
|
this.emit("session_error", session.toHandle(), error.message);
|
|
877
1027
|
});
|
|
1028
|
+
session.on("stall_detected", (recentOutput, stallDurationMs) => {
|
|
1029
|
+
const handle = session.toHandle();
|
|
1030
|
+
this.emit("stall_detected", handle, recentOutput, stallDurationMs);
|
|
1031
|
+
if (this._onStallClassify) {
|
|
1032
|
+
this._onStallClassify(session.id, recentOutput, stallDurationMs).then((classification) => {
|
|
1033
|
+
session.handleStallClassification(classification);
|
|
1034
|
+
}).catch((err) => {
|
|
1035
|
+
this.logger.error(
|
|
1036
|
+
{ sessionId: session.id, error: err },
|
|
1037
|
+
"Stall classification callback failed"
|
|
1038
|
+
);
|
|
1039
|
+
session.handleStallClassification(null);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
878
1043
|
}
|
|
879
1044
|
/**
|
|
880
1045
|
* Stop a session
|
|
@@ -1046,6 +1211,22 @@ var PTYManager = class extends import_events2.EventEmitter {
|
|
|
1046
1211
|
return this.sessions.get(sessionId);
|
|
1047
1212
|
}
|
|
1048
1213
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1214
|
+
// Stall Detection Configuration
|
|
1215
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1216
|
+
/**
|
|
1217
|
+
* Configure stall detection at runtime.
|
|
1218
|
+
* Affects newly spawned sessions only — existing sessions keep their config.
|
|
1219
|
+
*/
|
|
1220
|
+
configureStallDetection(enabled, timeoutMs, classify) {
|
|
1221
|
+
this._stallDetectionEnabled = enabled;
|
|
1222
|
+
if (timeoutMs !== void 0) {
|
|
1223
|
+
this._stallTimeoutMs = timeoutMs;
|
|
1224
|
+
}
|
|
1225
|
+
if (classify !== void 0) {
|
|
1226
|
+
this._onStallClassify = classify;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1049
1230
|
// Runtime Auto-Response Rules API
|
|
1050
1231
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1051
1232
|
/**
|
|
@@ -1234,6 +1415,14 @@ manager.on("question", (handle, question) => {
|
|
|
1234
1415
|
question
|
|
1235
1416
|
});
|
|
1236
1417
|
});
|
|
1418
|
+
manager.on("stall_detected", (handle, recentOutput, stallDurationMs) => {
|
|
1419
|
+
emit({
|
|
1420
|
+
event: "stall_detected",
|
|
1421
|
+
id: handle.id,
|
|
1422
|
+
recentOutput,
|
|
1423
|
+
stallDurationMs
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1237
1426
|
async function handleSpawn(id, config) {
|
|
1238
1427
|
try {
|
|
1239
1428
|
if (manager.has(id)) {
|
|
@@ -1421,6 +1610,27 @@ function handleClearRules(id) {
|
|
|
1421
1610
|
ack("clearRules", id, false, err instanceof Error ? err.message : String(err));
|
|
1422
1611
|
}
|
|
1423
1612
|
}
|
|
1613
|
+
function handleConfigureStallDetection(enabled, timeoutMs) {
|
|
1614
|
+
try {
|
|
1615
|
+
manager.configureStallDetection(enabled, timeoutMs);
|
|
1616
|
+
ack("configureStallDetection", void 0, true);
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
ack("configureStallDetection", void 0, false, err instanceof Error ? err.message : String(err));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function handleClassifyStallResult(id, classification) {
|
|
1622
|
+
try {
|
|
1623
|
+
const session = manager.getSession(id);
|
|
1624
|
+
if (!session) {
|
|
1625
|
+
ack("classifyStallResult", id, false, `Session ${id} not found`);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
session.handleStallClassification(classification);
|
|
1629
|
+
ack("classifyStallResult", id, true);
|
|
1630
|
+
} catch (err) {
|
|
1631
|
+
ack("classifyStallResult", id, false, err instanceof Error ? err.message : String(err));
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1424
1634
|
function processCommand(line) {
|
|
1425
1635
|
let command;
|
|
1426
1636
|
try {
|
|
@@ -1520,6 +1730,16 @@ function processCommand(line) {
|
|
|
1520
1730
|
}
|
|
1521
1731
|
handleClearRules(command.id);
|
|
1522
1732
|
break;
|
|
1733
|
+
case "configureStallDetection":
|
|
1734
|
+
handleConfigureStallDetection(command.enabled ?? false, command.timeoutMs);
|
|
1735
|
+
break;
|
|
1736
|
+
case "classifyStallResult":
|
|
1737
|
+
if (!command.id) {
|
|
1738
|
+
ack("classifyStallResult", command.id, false, "Missing id");
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
handleClassifyStallResult(command.id, command.classification ?? null);
|
|
1742
|
+
break;
|
|
1523
1743
|
default:
|
|
1524
1744
|
emit({ event: "error", message: `Unknown command: ${command.cmd}` });
|
|
1525
1745
|
}
|
package/package.json
CHANGED