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.
@@ -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(this.outputBuffer)) {
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(adapter, config, this.logger);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pty-manager",
3
- "version": "1.2.13",
3
+ "version": "1.2.14",
4
4
  "description": "PTY session manager with lifecycle management, pluggable adapters, and blocking prompt detection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",