pty-manager 1.2.12 → 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.js CHANGED
@@ -307,12 +307,14 @@ var SPECIAL_KEYS = {
307
307
  var BRACKETED_PASTE_START = "\x1B[200~";
308
308
  var BRACKETED_PASTE_END = "\x1B[201~";
309
309
  var PTYSession = class extends import_events.EventEmitter {
310
- constructor(adapter, config, logger) {
310
+ constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
311
311
  super();
312
312
  this.adapter = adapter;
313
313
  this.id = config.id || generateId();
314
314
  this.config = { ...config, id: this.id };
315
315
  this.logger = logger || consoleLogger;
316
+ this._stallDetectionEnabled = stallDetectionEnabled ?? false;
317
+ this._stallTimeoutMs = config.stallTimeoutMs ?? defaultStallTimeoutMs ?? 8e3;
316
318
  }
317
319
  ptyProcess = null;
318
320
  outputBuffer = "";
@@ -323,6 +325,12 @@ var PTYSession = class extends import_events.EventEmitter {
323
325
  logger;
324
326
  sessionRules = [];
325
327
  _lastBlockingPromptHash = null;
328
+ // Stall detection
329
+ _stallTimer = null;
330
+ _stallTimeoutMs;
331
+ _stallDetectionEnabled;
332
+ _lastStallHash = null;
333
+ _stallStartedAt = null;
326
334
  id;
327
335
  config;
328
336
  get status() {
@@ -404,6 +412,123 @@ var PTYSession = class extends import_events.EventEmitter {
404
412
  this.logger.debug({ sessionId: this.id }, "Cleared auto-response rules");
405
413
  }
406
414
  // ─────────────────────────────────────────────────────────────────────────────
415
+ // Stall Detection
416
+ // ─────────────────────────────────────────────────────────────────────────────
417
+ /**
418
+ * Start or reset the stall detection timer.
419
+ * Only active when status is "busy" and stall detection is enabled.
420
+ */
421
+ resetStallTimer() {
422
+ this.clearStallTimer();
423
+ if (!this._stallDetectionEnabled || this._status !== "busy") {
424
+ return;
425
+ }
426
+ this._stallStartedAt = Date.now();
427
+ this._lastStallHash = null;
428
+ this._stallTimer = setTimeout(() => {
429
+ this.onStallTimerFired();
430
+ }, this._stallTimeoutMs);
431
+ }
432
+ /**
433
+ * Clear the stall detection timer.
434
+ */
435
+ clearStallTimer() {
436
+ if (this._stallTimer) {
437
+ clearTimeout(this._stallTimer);
438
+ this._stallTimer = null;
439
+ }
440
+ this._stallStartedAt = null;
441
+ }
442
+ /**
443
+ * Called when the stall timer fires (no output for stallTimeoutMs).
444
+ */
445
+ onStallTimerFired() {
446
+ if (this._status !== "busy") {
447
+ return;
448
+ }
449
+ const tail = this.outputBuffer.slice(-500);
450
+ const hash = this.simpleHash(tail);
451
+ if (hash === this._lastStallHash) {
452
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
453
+ return;
454
+ }
455
+ this._lastStallHash = hash;
456
+ const recentRaw = this.outputBuffer.slice(-2e3);
457
+ const recentOutput = this.stripAnsiForStall(recentRaw);
458
+ const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
459
+ this.logger.debug(
460
+ { sessionId: this.id, stallDurationMs, bufferTailLength: tail.length },
461
+ "Stall detected"
462
+ );
463
+ this.emit("stall_detected", recentOutput, stallDurationMs);
464
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
465
+ }
466
+ /**
467
+ * Simple string hash for deduplication.
468
+ */
469
+ simpleHash(str) {
470
+ let hash = 0;
471
+ for (let i = 0; i < str.length; i++) {
472
+ const char = str.charCodeAt(i);
473
+ hash = (hash << 5) - hash + char;
474
+ hash |= 0;
475
+ }
476
+ return hash.toString(36);
477
+ }
478
+ /**
479
+ * Strip ANSI codes for stall detection output.
480
+ * Replaces cursor-forward sequences with spaces first.
481
+ */
482
+ stripAnsiForStall(str) {
483
+ const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
484
+ return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
485
+ }
486
+ /**
487
+ * Handle external stall classification result.
488
+ * Called by the manager after onStallClassify resolves.
489
+ */
490
+ handleStallClassification(classification) {
491
+ if (this._status !== "busy") {
492
+ return;
493
+ }
494
+ if (!classification || classification.state === "still_working") {
495
+ this.resetStallTimer();
496
+ return;
497
+ }
498
+ switch (classification.state) {
499
+ case "waiting_for_input": {
500
+ const promptInfo = {
501
+ type: "stall_classified",
502
+ prompt: classification.prompt,
503
+ canAutoRespond: !!classification.suggestedResponse
504
+ };
505
+ if (classification.suggestedResponse) {
506
+ this.logger.info(
507
+ { sessionId: this.id, response: classification.suggestedResponse },
508
+ "Auto-responding to stall-classified prompt"
509
+ );
510
+ this.writeRaw(classification.suggestedResponse + "\r");
511
+ this.emit("blocking_prompt", promptInfo, true);
512
+ } else {
513
+ this.emit("blocking_prompt", promptInfo, false);
514
+ }
515
+ break;
516
+ }
517
+ case "task_complete":
518
+ this._status = "ready";
519
+ this._lastBlockingPromptHash = null;
520
+ this.outputBuffer = "";
521
+ this.clearStallTimer();
522
+ this.emit("ready");
523
+ this.logger.info({ sessionId: this.id }, "Stall classified as task_complete, transitioning to ready");
524
+ break;
525
+ case "error":
526
+ this.clearStallTimer();
527
+ this.emit("error", new Error(classification.prompt || "Stall classified as error"));
528
+ break;
529
+ }
530
+ }
531
+ // ─────────────────────────────────────────────────────────────────────────────
407
532
  // Lifecycle
408
533
  // ─────────────────────────────────────────────────────────────────────────────
409
534
  /**
@@ -461,11 +586,15 @@ var PTYSession = class extends import_events.EventEmitter {
461
586
  this.ptyProcess.onData((data) => {
462
587
  this._lastActivityAt = /* @__PURE__ */ new Date();
463
588
  this.outputBuffer += data;
589
+ if (this._status === "busy") {
590
+ this.resetStallTimer();
591
+ }
464
592
  this.emit("output", data);
465
593
  if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
466
594
  this._status = "ready";
467
595
  this._lastBlockingPromptHash = null;
468
596
  this.outputBuffer = "";
597
+ this.clearStallTimer();
469
598
  this.emit("ready");
470
599
  this.logger.info({ sessionId: this.id }, "Session ready");
471
600
  return;
@@ -478,6 +607,7 @@ var PTYSession = class extends import_events.EventEmitter {
478
607
  const loginDetection = this.adapter.detectLogin(this.outputBuffer);
479
608
  if (loginDetection.required && this._status !== "authenticating") {
480
609
  this._status = "authenticating";
610
+ this.clearStallTimer();
481
611
  this.emit("login_required", loginDetection.instructions, loginDetection.url);
482
612
  this.logger.warn(
483
613
  { sessionId: this.id, loginType: loginDetection.type },
@@ -489,6 +619,7 @@ var PTYSession = class extends import_events.EventEmitter {
489
619
  const exitDetection = this.adapter.detectExit(this.outputBuffer);
490
620
  if (exitDetection.exited) {
491
621
  this._status = "stopped";
622
+ this.clearStallTimer();
492
623
  this.emit("exit", exitDetection.code || 0);
493
624
  }
494
625
  if (this._status !== "starting" && this._status !== "authenticating") {
@@ -497,6 +628,7 @@ var PTYSession = class extends import_events.EventEmitter {
497
628
  });
498
629
  this.ptyProcess.onExit(({ exitCode, signal }) => {
499
630
  this._status = "stopped";
631
+ this.clearStallTimer();
500
632
  this.logger.info(
501
633
  { sessionId: this.id, exitCode, signal },
502
634
  "PTY session exited"
@@ -572,8 +704,11 @@ var PTYSession = class extends import_events.EventEmitter {
572
704
  if (allRules.length === 0) {
573
705
  return false;
574
706
  }
707
+ let stripped = this.stripAnsiForStall(this.outputBuffer);
708
+ stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
709
+ stripped = stripped.replace(/ {2,}/g, " ");
575
710
  for (const rule of allRules) {
576
- if (rule.pattern.test(this.outputBuffer)) {
711
+ if (rule.pattern.test(stripped)) {
577
712
  const safe = rule.safe !== false;
578
713
  const isSessionRule = this.sessionRules.includes(rule);
579
714
  if (safe) {
@@ -664,6 +799,7 @@ var PTYSession = class extends import_events.EventEmitter {
664
799
  */
665
800
  send(message) {
666
801
  this._status = "busy";
802
+ this.resetStallTimer();
667
803
  const msg = {
668
804
  id: `${this.id}-msg-${++this.messageCounter}`,
669
805
  sessionId: this.id,
@@ -752,6 +888,7 @@ var PTYSession = class extends import_events.EventEmitter {
752
888
  kill(signal) {
753
889
  if (this.ptyProcess) {
754
890
  this._status = "stopping";
891
+ this.clearStallTimer();
755
892
  this.ptyProcess.kill(signal);
756
893
  this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
757
894
  }
@@ -821,11 +958,18 @@ var PTYManager = class extends import_events2.EventEmitter {
821
958
  maxLogLines;
822
959
  logger;
823
960
  adapters;
961
+ // Stall detection config
962
+ _stallDetectionEnabled;
963
+ _stallTimeoutMs;
964
+ _onStallClassify;
824
965
  constructor(config = {}) {
825
966
  super();
826
967
  this.adapters = new AdapterRegistry();
827
968
  this.logger = config.logger || consoleLogger2;
828
969
  this.maxLogLines = config.maxLogLines || 1e3;
970
+ this._stallDetectionEnabled = config.stallDetectionEnabled ?? false;
971
+ this._stallTimeoutMs = config.stallTimeoutMs ?? 8e3;
972
+ this._onStallClassify = config.onStallClassify;
829
973
  }
830
974
  /**
831
975
  * Register a CLI adapter
@@ -848,7 +992,13 @@ var PTYManager = class extends import_events2.EventEmitter {
848
992
  { type: config.type, name: config.name },
849
993
  "Spawning session"
850
994
  );
851
- const session = new PTYSession(adapter, config, this.logger);
995
+ const session = new PTYSession(
996
+ adapter,
997
+ config,
998
+ this.logger,
999
+ this._stallDetectionEnabled,
1000
+ this._stallTimeoutMs
1001
+ );
852
1002
  this.setupSessionEvents(session);
853
1003
  this.sessions.set(session.id, session);
854
1004
  this.outputLogs.set(session.id, []);
@@ -892,6 +1042,21 @@ var PTYManager = class extends import_events2.EventEmitter {
892
1042
  session.on("error", (error) => {
893
1043
  this.emit("session_error", session.toHandle(), error.message);
894
1044
  });
1045
+ session.on("stall_detected", (recentOutput, stallDurationMs) => {
1046
+ const handle = session.toHandle();
1047
+ this.emit("stall_detected", handle, recentOutput, stallDurationMs);
1048
+ if (this._onStallClassify) {
1049
+ this._onStallClassify(session.id, recentOutput, stallDurationMs).then((classification) => {
1050
+ session.handleStallClassification(classification);
1051
+ }).catch((err) => {
1052
+ this.logger.error(
1053
+ { sessionId: session.id, error: err },
1054
+ "Stall classification callback failed"
1055
+ );
1056
+ session.handleStallClassification(null);
1057
+ });
1058
+ }
1059
+ });
895
1060
  }
896
1061
  /**
897
1062
  * Stop a session
@@ -1063,6 +1228,22 @@ var PTYManager = class extends import_events2.EventEmitter {
1063
1228
  return this.sessions.get(sessionId);
1064
1229
  }
1065
1230
  // ─────────────────────────────────────────────────────────────────────────────
1231
+ // Stall Detection Configuration
1232
+ // ─────────────────────────────────────────────────────────────────────────────
1233
+ /**
1234
+ * Configure stall detection at runtime.
1235
+ * Affects newly spawned sessions only — existing sessions keep their config.
1236
+ */
1237
+ configureStallDetection(enabled, timeoutMs, classify) {
1238
+ this._stallDetectionEnabled = enabled;
1239
+ if (timeoutMs !== void 0) {
1240
+ this._stallTimeoutMs = timeoutMs;
1241
+ }
1242
+ if (classify !== void 0) {
1243
+ this._onStallClassify = classify;
1244
+ }
1245
+ }
1246
+ // ─────────────────────────────────────────────────────────────────────────────
1066
1247
  // Runtime Auto-Response Rules API
1067
1248
  // ─────────────────────────────────────────────────────────────────────────────
1068
1249
  /**
@@ -1152,7 +1333,9 @@ var BaseCLIAdapter = class {
1152
1333
  * Subclasses should override for CLI-specific detection.
1153
1334
  */
1154
1335
  detectBlockingPrompt(output) {
1155
- const stripped = this.stripAnsi(output);
1336
+ let stripped = this.stripAnsi(output);
1337
+ stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
1338
+ stripped = stripped.replace(/ {2,}/g, " ");
1156
1339
  const loginDetection = this.detectLogin(output);
1157
1340
  if (loginDetection.required) {
1158
1341
  return {
@@ -1332,7 +1515,8 @@ var BaseCLIAdapter = class {
1332
1515
  * Helper to strip ANSI escape codes from output
1333
1516
  */
1334
1517
  stripAnsi(str) {
1335
- return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1518
+ const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
1519
+ return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1336
1520
  }
1337
1521
  };
1338
1522
 
@@ -1535,12 +1719,18 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
1535
1719
  workerPath;
1536
1720
  env;
1537
1721
  adapterModules;
1722
+ _stallDetectionEnabled;
1723
+ _stallTimeoutMs;
1724
+ _onStallClassify;
1538
1725
  constructor(options = {}) {
1539
1726
  super();
1540
1727
  this.nodePath = options.nodePath || "node";
1541
1728
  this.workerPath = options.workerPath || this.findWorkerPath();
1542
1729
  this.env = options.env || {};
1543
1730
  this.adapterModules = options.adapterModules || [];
1731
+ this._stallDetectionEnabled = options.stallDetectionEnabled ?? false;
1732
+ this._stallTimeoutMs = options.stallTimeoutMs ?? 8e3;
1733
+ this._onStallClassify = options.onStallClassify;
1544
1734
  this.readyPromise = new Promise((resolve) => {
1545
1735
  this.readyResolve = resolve;
1546
1736
  });
@@ -1602,6 +1792,13 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
1602
1792
  if (this.adapterModules.length > 0) {
1603
1793
  this.sendCommand({ cmd: "registerAdapters", modules: this.adapterModules });
1604
1794
  }
1795
+ if (this._stallDetectionEnabled) {
1796
+ this.sendCommand({
1797
+ cmd: "configureStallDetection",
1798
+ enabled: true,
1799
+ timeoutMs: this._stallTimeoutMs
1800
+ });
1801
+ }
1605
1802
  this.ready = true;
1606
1803
  this.readyResolve();
1607
1804
  this.emit("ready");
@@ -1692,6 +1889,30 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
1692
1889
  }
1693
1890
  break;
1694
1891
  }
1892
+ case "stall_detected": {
1893
+ const session = this.sessions.get(id);
1894
+ if (session) {
1895
+ const recentOutput = event.recentOutput;
1896
+ const stallDurationMs = event.stallDurationMs;
1897
+ this.emit("stall_detected", session, recentOutput, stallDurationMs);
1898
+ if (this._onStallClassify) {
1899
+ this._onStallClassify(id, recentOutput, stallDurationMs).then((classification) => {
1900
+ this.sendCommand({
1901
+ cmd: "classifyStallResult",
1902
+ id,
1903
+ classification
1904
+ });
1905
+ }).catch(() => {
1906
+ this.sendCommand({
1907
+ cmd: "classifyStallResult",
1908
+ id,
1909
+ classification: null
1910
+ });
1911
+ });
1912
+ }
1913
+ }
1914
+ break;
1915
+ }
1695
1916
  case "list": {
1696
1917
  const sessions = event.sessions.map((s) => ({
1697
1918
  ...s,