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.mjs CHANGED
@@ -269,12 +269,14 @@ var SPECIAL_KEYS = {
269
269
  var BRACKETED_PASTE_START = "\x1B[200~";
270
270
  var BRACKETED_PASTE_END = "\x1B[201~";
271
271
  var PTYSession = class extends EventEmitter {
272
- constructor(adapter, config, logger) {
272
+ constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
273
273
  super();
274
274
  this.adapter = adapter;
275
275
  this.id = config.id || generateId();
276
276
  this.config = { ...config, id: this.id };
277
277
  this.logger = logger || consoleLogger;
278
+ this._stallDetectionEnabled = stallDetectionEnabled ?? false;
279
+ this._stallTimeoutMs = config.stallTimeoutMs ?? defaultStallTimeoutMs ?? 8e3;
278
280
  }
279
281
  ptyProcess = null;
280
282
  outputBuffer = "";
@@ -285,6 +287,12 @@ var PTYSession = class extends EventEmitter {
285
287
  logger;
286
288
  sessionRules = [];
287
289
  _lastBlockingPromptHash = null;
290
+ // Stall detection
291
+ _stallTimer = null;
292
+ _stallTimeoutMs;
293
+ _stallDetectionEnabled;
294
+ _lastStallHash = null;
295
+ _stallStartedAt = null;
288
296
  id;
289
297
  config;
290
298
  get status() {
@@ -366,6 +374,123 @@ var PTYSession = class extends EventEmitter {
366
374
  this.logger.debug({ sessionId: this.id }, "Cleared auto-response rules");
367
375
  }
368
376
  // ─────────────────────────────────────────────────────────────────────────────
377
+ // Stall Detection
378
+ // ─────────────────────────────────────────────────────────────────────────────
379
+ /**
380
+ * Start or reset the stall detection timer.
381
+ * Only active when status is "busy" and stall detection is enabled.
382
+ */
383
+ resetStallTimer() {
384
+ this.clearStallTimer();
385
+ if (!this._stallDetectionEnabled || this._status !== "busy") {
386
+ return;
387
+ }
388
+ this._stallStartedAt = Date.now();
389
+ this._lastStallHash = null;
390
+ this._stallTimer = setTimeout(() => {
391
+ this.onStallTimerFired();
392
+ }, this._stallTimeoutMs);
393
+ }
394
+ /**
395
+ * Clear the stall detection timer.
396
+ */
397
+ clearStallTimer() {
398
+ if (this._stallTimer) {
399
+ clearTimeout(this._stallTimer);
400
+ this._stallTimer = null;
401
+ }
402
+ this._stallStartedAt = null;
403
+ }
404
+ /**
405
+ * Called when the stall timer fires (no output for stallTimeoutMs).
406
+ */
407
+ onStallTimerFired() {
408
+ if (this._status !== "busy") {
409
+ return;
410
+ }
411
+ const tail = this.outputBuffer.slice(-500);
412
+ const hash = this.simpleHash(tail);
413
+ if (hash === this._lastStallHash) {
414
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
415
+ return;
416
+ }
417
+ this._lastStallHash = hash;
418
+ const recentRaw = this.outputBuffer.slice(-2e3);
419
+ const recentOutput = this.stripAnsiForStall(recentRaw);
420
+ const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
421
+ this.logger.debug(
422
+ { sessionId: this.id, stallDurationMs, bufferTailLength: tail.length },
423
+ "Stall detected"
424
+ );
425
+ this.emit("stall_detected", recentOutput, stallDurationMs);
426
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallTimeoutMs);
427
+ }
428
+ /**
429
+ * Simple string hash for deduplication.
430
+ */
431
+ simpleHash(str) {
432
+ let hash = 0;
433
+ for (let i = 0; i < str.length; i++) {
434
+ const char = str.charCodeAt(i);
435
+ hash = (hash << 5) - hash + char;
436
+ hash |= 0;
437
+ }
438
+ return hash.toString(36);
439
+ }
440
+ /**
441
+ * Strip ANSI codes for stall detection output.
442
+ * Replaces cursor-forward sequences with spaces first.
443
+ */
444
+ stripAnsiForStall(str) {
445
+ const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
446
+ return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
447
+ }
448
+ /**
449
+ * Handle external stall classification result.
450
+ * Called by the manager after onStallClassify resolves.
451
+ */
452
+ handleStallClassification(classification) {
453
+ if (this._status !== "busy") {
454
+ return;
455
+ }
456
+ if (!classification || classification.state === "still_working") {
457
+ this.resetStallTimer();
458
+ return;
459
+ }
460
+ switch (classification.state) {
461
+ case "waiting_for_input": {
462
+ const promptInfo = {
463
+ type: "stall_classified",
464
+ prompt: classification.prompt,
465
+ canAutoRespond: !!classification.suggestedResponse
466
+ };
467
+ if (classification.suggestedResponse) {
468
+ this.logger.info(
469
+ { sessionId: this.id, response: classification.suggestedResponse },
470
+ "Auto-responding to stall-classified prompt"
471
+ );
472
+ this.writeRaw(classification.suggestedResponse + "\r");
473
+ this.emit("blocking_prompt", promptInfo, true);
474
+ } else {
475
+ this.emit("blocking_prompt", promptInfo, false);
476
+ }
477
+ break;
478
+ }
479
+ case "task_complete":
480
+ this._status = "ready";
481
+ this._lastBlockingPromptHash = null;
482
+ this.outputBuffer = "";
483
+ this.clearStallTimer();
484
+ this.emit("ready");
485
+ this.logger.info({ sessionId: this.id }, "Stall classified as task_complete, transitioning to ready");
486
+ break;
487
+ case "error":
488
+ this.clearStallTimer();
489
+ this.emit("error", new Error(classification.prompt || "Stall classified as error"));
490
+ break;
491
+ }
492
+ }
493
+ // ─────────────────────────────────────────────────────────────────────────────
369
494
  // Lifecycle
370
495
  // ─────────────────────────────────────────────────────────────────────────────
371
496
  /**
@@ -423,11 +548,15 @@ var PTYSession = class extends EventEmitter {
423
548
  this.ptyProcess.onData((data) => {
424
549
  this._lastActivityAt = /* @__PURE__ */ new Date();
425
550
  this.outputBuffer += data;
551
+ if (this._status === "busy") {
552
+ this.resetStallTimer();
553
+ }
426
554
  this.emit("output", data);
427
555
  if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
428
556
  this._status = "ready";
429
557
  this._lastBlockingPromptHash = null;
430
558
  this.outputBuffer = "";
559
+ this.clearStallTimer();
431
560
  this.emit("ready");
432
561
  this.logger.info({ sessionId: this.id }, "Session ready");
433
562
  return;
@@ -440,6 +569,7 @@ var PTYSession = class extends EventEmitter {
440
569
  const loginDetection = this.adapter.detectLogin(this.outputBuffer);
441
570
  if (loginDetection.required && this._status !== "authenticating") {
442
571
  this._status = "authenticating";
572
+ this.clearStallTimer();
443
573
  this.emit("login_required", loginDetection.instructions, loginDetection.url);
444
574
  this.logger.warn(
445
575
  { sessionId: this.id, loginType: loginDetection.type },
@@ -451,6 +581,7 @@ var PTYSession = class extends EventEmitter {
451
581
  const exitDetection = this.adapter.detectExit(this.outputBuffer);
452
582
  if (exitDetection.exited) {
453
583
  this._status = "stopped";
584
+ this.clearStallTimer();
454
585
  this.emit("exit", exitDetection.code || 0);
455
586
  }
456
587
  if (this._status !== "starting" && this._status !== "authenticating") {
@@ -459,6 +590,7 @@ var PTYSession = class extends EventEmitter {
459
590
  });
460
591
  this.ptyProcess.onExit(({ exitCode, signal }) => {
461
592
  this._status = "stopped";
593
+ this.clearStallTimer();
462
594
  this.logger.info(
463
595
  { sessionId: this.id, exitCode, signal },
464
596
  "PTY session exited"
@@ -534,8 +666,11 @@ var PTYSession = class extends EventEmitter {
534
666
  if (allRules.length === 0) {
535
667
  return false;
536
668
  }
669
+ let stripped = this.stripAnsiForStall(this.outputBuffer);
670
+ stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
671
+ stripped = stripped.replace(/ {2,}/g, " ");
537
672
  for (const rule of allRules) {
538
- if (rule.pattern.test(this.outputBuffer)) {
673
+ if (rule.pattern.test(stripped)) {
539
674
  const safe = rule.safe !== false;
540
675
  const isSessionRule = this.sessionRules.includes(rule);
541
676
  if (safe) {
@@ -626,6 +761,7 @@ var PTYSession = class extends EventEmitter {
626
761
  */
627
762
  send(message) {
628
763
  this._status = "busy";
764
+ this.resetStallTimer();
629
765
  const msg = {
630
766
  id: `${this.id}-msg-${++this.messageCounter}`,
631
767
  sessionId: this.id,
@@ -714,6 +850,7 @@ var PTYSession = class extends EventEmitter {
714
850
  kill(signal) {
715
851
  if (this.ptyProcess) {
716
852
  this._status = "stopping";
853
+ this.clearStallTimer();
717
854
  this.ptyProcess.kill(signal);
718
855
  this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
719
856
  }
@@ -783,11 +920,18 @@ var PTYManager = class extends EventEmitter2 {
783
920
  maxLogLines;
784
921
  logger;
785
922
  adapters;
923
+ // Stall detection config
924
+ _stallDetectionEnabled;
925
+ _stallTimeoutMs;
926
+ _onStallClassify;
786
927
  constructor(config = {}) {
787
928
  super();
788
929
  this.adapters = new AdapterRegistry();
789
930
  this.logger = config.logger || consoleLogger2;
790
931
  this.maxLogLines = config.maxLogLines || 1e3;
932
+ this._stallDetectionEnabled = config.stallDetectionEnabled ?? false;
933
+ this._stallTimeoutMs = config.stallTimeoutMs ?? 8e3;
934
+ this._onStallClassify = config.onStallClassify;
791
935
  }
792
936
  /**
793
937
  * Register a CLI adapter
@@ -810,7 +954,13 @@ var PTYManager = class extends EventEmitter2 {
810
954
  { type: config.type, name: config.name },
811
955
  "Spawning session"
812
956
  );
813
- const session = new PTYSession(adapter, config, this.logger);
957
+ const session = new PTYSession(
958
+ adapter,
959
+ config,
960
+ this.logger,
961
+ this._stallDetectionEnabled,
962
+ this._stallTimeoutMs
963
+ );
814
964
  this.setupSessionEvents(session);
815
965
  this.sessions.set(session.id, session);
816
966
  this.outputLogs.set(session.id, []);
@@ -854,6 +1004,21 @@ var PTYManager = class extends EventEmitter2 {
854
1004
  session.on("error", (error) => {
855
1005
  this.emit("session_error", session.toHandle(), error.message);
856
1006
  });
1007
+ session.on("stall_detected", (recentOutput, stallDurationMs) => {
1008
+ const handle = session.toHandle();
1009
+ this.emit("stall_detected", handle, recentOutput, stallDurationMs);
1010
+ if (this._onStallClassify) {
1011
+ this._onStallClassify(session.id, recentOutput, stallDurationMs).then((classification) => {
1012
+ session.handleStallClassification(classification);
1013
+ }).catch((err) => {
1014
+ this.logger.error(
1015
+ { sessionId: session.id, error: err },
1016
+ "Stall classification callback failed"
1017
+ );
1018
+ session.handleStallClassification(null);
1019
+ });
1020
+ }
1021
+ });
857
1022
  }
858
1023
  /**
859
1024
  * Stop a session
@@ -1025,6 +1190,22 @@ var PTYManager = class extends EventEmitter2 {
1025
1190
  return this.sessions.get(sessionId);
1026
1191
  }
1027
1192
  // ─────────────────────────────────────────────────────────────────────────────
1193
+ // Stall Detection Configuration
1194
+ // ─────────────────────────────────────────────────────────────────────────────
1195
+ /**
1196
+ * Configure stall detection at runtime.
1197
+ * Affects newly spawned sessions only — existing sessions keep their config.
1198
+ */
1199
+ configureStallDetection(enabled, timeoutMs, classify) {
1200
+ this._stallDetectionEnabled = enabled;
1201
+ if (timeoutMs !== void 0) {
1202
+ this._stallTimeoutMs = timeoutMs;
1203
+ }
1204
+ if (classify !== void 0) {
1205
+ this._onStallClassify = classify;
1206
+ }
1207
+ }
1208
+ // ─────────────────────────────────────────────────────────────────────────────
1028
1209
  // Runtime Auto-Response Rules API
1029
1210
  // ─────────────────────────────────────────────────────────────────────────────
1030
1211
  /**
@@ -1114,7 +1295,9 @@ var BaseCLIAdapter = class {
1114
1295
  * Subclasses should override for CLI-specific detection.
1115
1296
  */
1116
1297
  detectBlockingPrompt(output) {
1117
- const stripped = this.stripAnsi(output);
1298
+ let stripped = this.stripAnsi(output);
1299
+ stripped = stripped.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⏺←→↑↓]/g, " ");
1300
+ stripped = stripped.replace(/ {2,}/g, " ");
1118
1301
  const loginDetection = this.detectLogin(output);
1119
1302
  if (loginDetection.required) {
1120
1303
  return {
@@ -1294,7 +1477,8 @@ var BaseCLIAdapter = class {
1294
1477
  * Helper to strip ANSI escape codes from output
1295
1478
  */
1296
1479
  stripAnsi(str) {
1297
- return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1480
+ const withSpaces = str.replace(/\x1b\[\d*C/g, " ");
1481
+ return withSpaces.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
1298
1482
  }
1299
1483
  };
1300
1484
 
@@ -1497,12 +1681,18 @@ var BunCompatiblePTYManager = class extends EventEmitter3 {
1497
1681
  workerPath;
1498
1682
  env;
1499
1683
  adapterModules;
1684
+ _stallDetectionEnabled;
1685
+ _stallTimeoutMs;
1686
+ _onStallClassify;
1500
1687
  constructor(options = {}) {
1501
1688
  super();
1502
1689
  this.nodePath = options.nodePath || "node";
1503
1690
  this.workerPath = options.workerPath || this.findWorkerPath();
1504
1691
  this.env = options.env || {};
1505
1692
  this.adapterModules = options.adapterModules || [];
1693
+ this._stallDetectionEnabled = options.stallDetectionEnabled ?? false;
1694
+ this._stallTimeoutMs = options.stallTimeoutMs ?? 8e3;
1695
+ this._onStallClassify = options.onStallClassify;
1506
1696
  this.readyPromise = new Promise((resolve) => {
1507
1697
  this.readyResolve = resolve;
1508
1698
  });
@@ -1564,6 +1754,13 @@ var BunCompatiblePTYManager = class extends EventEmitter3 {
1564
1754
  if (this.adapterModules.length > 0) {
1565
1755
  this.sendCommand({ cmd: "registerAdapters", modules: this.adapterModules });
1566
1756
  }
1757
+ if (this._stallDetectionEnabled) {
1758
+ this.sendCommand({
1759
+ cmd: "configureStallDetection",
1760
+ enabled: true,
1761
+ timeoutMs: this._stallTimeoutMs
1762
+ });
1763
+ }
1567
1764
  this.ready = true;
1568
1765
  this.readyResolve();
1569
1766
  this.emit("ready");
@@ -1654,6 +1851,30 @@ var BunCompatiblePTYManager = class extends EventEmitter3 {
1654
1851
  }
1655
1852
  break;
1656
1853
  }
1854
+ case "stall_detected": {
1855
+ const session = this.sessions.get(id);
1856
+ if (session) {
1857
+ const recentOutput = event.recentOutput;
1858
+ const stallDurationMs = event.stallDurationMs;
1859
+ this.emit("stall_detected", session, recentOutput, stallDurationMs);
1860
+ if (this._onStallClassify) {
1861
+ this._onStallClassify(id, recentOutput, stallDurationMs).then((classification) => {
1862
+ this.sendCommand({
1863
+ cmd: "classifyStallResult",
1864
+ id,
1865
+ classification
1866
+ });
1867
+ }).catch(() => {
1868
+ this.sendCommand({
1869
+ cmd: "classifyStallResult",
1870
+ id,
1871
+ classification: null
1872
+ });
1873
+ });
1874
+ }
1875
+ }
1876
+ break;
1877
+ }
1657
1878
  case "list": {
1658
1879
  const sessions = event.sessions.map((s) => ({
1659
1880
  ...s,