@dmsdc-ai/aigentry-telepty 0.1.70 → 0.1.72

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.
Files changed (2) hide show
  1. package/cli.js +152 -28
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -643,6 +643,11 @@ async function main() {
643
643
  allowArgs.splice(idIndex, 2);
644
644
  }
645
645
 
646
+ // Extract --auto-restart flag
647
+ const autoRestartIndex = allowArgs.indexOf('--auto-restart');
648
+ const autoRestart = autoRestartIndex !== -1;
649
+ if (autoRestart) allowArgs.splice(autoRestartIndex, 1);
650
+
646
651
  // Strip optional -- separator for backward compat
647
652
  const sepIndex = allowArgs.indexOf('--');
648
653
  if (sepIndex !== -1) allowArgs.splice(sepIndex, 1);
@@ -708,13 +713,60 @@ async function main() {
708
713
 
709
714
  // Spawn local PTY (preserves isTTY, env, shell config)
710
715
  const pty = require('node-pty');
711
- const child = pty.spawn(command, cmdArgs, {
712
- name: 'xterm-256color',
713
- cols: process.stdout.columns || 80,
714
- rows: process.stdout.rows || 30,
715
- cwd: process.cwd(),
716
- env: { ...process.env, TELEPTY_SESSION_ID: sessionId }
717
- });
716
+ const sessionCwd = process.cwd();
717
+ const sessionEnv = { ...process.env, TELEPTY_SESSION_ID: sessionId };
718
+ let child = null;
719
+ let sessionStartTime = Date.now();
720
+ let crashCount = 0;
721
+ const MAX_CRASHES = 3;
722
+ const DEATH_LOG_PATH = path.join(os.homedir(), '.telepty', 'logs', 'session-deaths.log');
723
+
724
+ function logSessionDeath(exitCode, signal, duration) {
725
+ try {
726
+ fs.mkdirSync(path.dirname(DEATH_LOG_PATH), { recursive: true });
727
+ const entry = `[${new Date().toISOString()}] session=${sessionId} command=${command} exit=${exitCode} signal=${signal || 'none'} duration=${Math.round(duration / 1000)}s crashes=${crashCount}\n`;
728
+ fs.appendFileSync(DEATH_LOG_PATH, entry);
729
+ } catch {}
730
+ }
731
+
732
+ function emitDeathEvent(exitCode, signal, willRestart) {
733
+ if (wsReady && daemonWs && daemonWs.readyState === 1) {
734
+ daemonWs.send(JSON.stringify({
735
+ type: 'session_died',
736
+ session_id: sessionId,
737
+ exitCode, signal: signal || null,
738
+ duration: Date.now() - sessionStartTime,
739
+ crashCount,
740
+ willRestart,
741
+ timestamp: new Date().toISOString()
742
+ }));
743
+ }
744
+ }
745
+
746
+ function emitRestartEvent(attempt) {
747
+ if (wsReady && daemonWs && daemonWs.readyState === 1) {
748
+ daemonWs.send(JSON.stringify({
749
+ type: 'session_restarted',
750
+ session_id: sessionId,
751
+ attempt,
752
+ timestamp: new Date().toISOString()
753
+ }));
754
+ }
755
+ }
756
+
757
+ function spawnChild() {
758
+ child = pty.spawn(command, cmdArgs, {
759
+ name: 'xterm-256color',
760
+ cols: process.stdout.columns || 80,
761
+ rows: process.stdout.rows || 30,
762
+ cwd: sessionCwd,
763
+ env: sessionEnv
764
+ });
765
+ sessionStartTime = Date.now();
766
+ return child;
767
+ }
768
+
769
+ spawnChild();
718
770
 
719
771
  // Prompt-ready detection for safe inject delivery
720
772
  const PROMPT_PATTERNS = {
@@ -726,10 +778,19 @@ async function main() {
726
778
  const promptPattern = PROMPT_PATTERNS[cmdBase] || /[❯>$#%]\s*$/;
727
779
  let promptReady = false; // wait for CLI prompt before accepting inject
728
780
  const injectQueue = [];
781
+ let lastUserInputTime = 0; // timestamp of last user keystroke
782
+ const IDLE_THRESHOLD = 2000; // ms after last user input to consider idle
783
+
784
+ function isIdle() {
785
+ return promptReady && (Date.now() - lastUserInputTime > IDLE_THRESHOLD);
786
+ }
729
787
 
730
788
  let queueFlushTimer = null;
789
+ let idleCheckTimer = null;
790
+
731
791
  function flushInjectQueue() {
732
792
  if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
793
+ if (idleCheckTimer) { clearInterval(idleCheckTimer); idleCheckTimer = null; }
733
794
  if (injectQueue.length === 0) return;
734
795
  const batch = injectQueue.splice(0);
735
796
  let delay = 0;
@@ -739,14 +800,23 @@ async function main() {
739
800
  }
740
801
  promptReady = false;
741
802
  }
742
- function scheduleQueueFlush() {
743
- if (queueFlushTimer) return;
744
- queueFlushTimer = setTimeout(() => {
745
- queueFlushTimer = null;
746
- if (injectQueue.length > 0) {
803
+ function scheduleIdleFlush() {
804
+ if (idleCheckTimer) return;
805
+ // Poll every 500ms for idle state
806
+ idleCheckTimer = setInterval(() => {
807
+ if (isIdle() && injectQueue.length > 0) {
747
808
  flushInjectQueue();
748
809
  }
749
- }, 15000);
810
+ }, 500);
811
+ // Safety: flush after 15s regardless (prevent stuck queue)
812
+ if (!queueFlushTimer) {
813
+ queueFlushTimer = setTimeout(() => {
814
+ queueFlushTimer = null;
815
+ if (injectQueue.length > 0) {
816
+ flushInjectQueue();
817
+ }
818
+ }, 15000);
819
+ }
750
820
  }
751
821
 
752
822
  // Connect to daemon WebSocket with auto-reconnect
@@ -806,15 +876,18 @@ async function main() {
806
876
  injectQueue.push(msg.data);
807
877
  if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
808
878
  flushInjectQueue();
809
- } else if (isCr || promptReady) {
879
+ } else if (isCr && isIdle()) {
880
+ // CR when idle — write immediately
881
+ child.write(msg.data);
882
+ } else if (!isCr && isIdle()) {
883
+ // Text when idle — write immediately
810
884
  child.write(msg.data);
811
- if (!isCr && msg.data.length > 1) {
812
- promptReady = false;
813
- lastInjectTextTime = Date.now();
814
- }
885
+ promptReady = false;
886
+ lastInjectTextTime = Date.now();
815
887
  } else {
888
+ // Not idle (user typing or CLI busy) — queue for safe delivery
816
889
  injectQueue.push(msg.data);
817
- scheduleQueueFlush();
890
+ scheduleIdleFlush();
818
891
  }
819
892
  } else if (msg.type === 'resize') {
820
893
  child.resize(msg.cols, msg.rows);
@@ -855,6 +928,7 @@ async function main() {
855
928
 
856
929
  const cleanupTerminal = attachInteractiveTerminal(process.stdin, process.stdout, {
857
930
  onData: (data) => {
931
+ lastUserInputTime = Date.now();
858
932
  child.write(data.toString());
859
933
  },
860
934
  onResize: () => {
@@ -917,14 +991,64 @@ async function main() {
917
991
  }
918
992
  });
919
993
 
920
- // Handle child exit
921
- child.onExit(({ exitCode }) => {
922
- if (!closeAllowSession()) {
923
- return;
924
- }
925
- console.log(`\n\x1b[33mSession '${sessionId}' exited (code ${exitCode}).\x1b[0m`);
926
- exitAllowSession(exitCode || 0);
927
- });
994
+ // Handle child exit with death tracking + auto-restart
995
+ function attachChildExitHandler() {
996
+ child.onExit(({ exitCode, signal }) => {
997
+ const duration = Date.now() - sessionStartTime;
998
+ const isAbnormal = exitCode !== 0 || signal;
999
+ const durationStr = duration > 60000 ? `${Math.round(duration / 60000)}m ${Math.round((duration % 60000) / 1000)}s` : `${Math.round(duration / 1000)}s`;
1000
+
1001
+ logSessionDeath(exitCode, signal, duration);
1002
+
1003
+ if (isAbnormal && autoRestart && crashCount < MAX_CRASHES) {
1004
+ crashCount++;
1005
+ const backoffMs = Math.min(1000 * Math.pow(2, crashCount - 1), 8000);
1006
+ const willRestart = true;
1007
+ emitDeathEvent(exitCode, signal, willRestart);
1008
+ console.log(`\n\x1b[33m⚠️ Session '${sessionId}' died (code ${exitCode}, signal ${signal || 'none'}, ${durationStr}). Restarting in ${backoffMs}ms (attempt ${crashCount}/${MAX_CRASHES})...\x1b[0m`);
1009
+
1010
+ setTimeout(() => {
1011
+ try {
1012
+ spawnChild();
1013
+ // Re-attach output relay, prompt detection, and exit handler
1014
+ child.onData((data) => {
1015
+ const rewritten = rewriteTitleSequences(data);
1016
+ process.stdout.write(rewritten);
1017
+ if (wsReady && daemonWs.readyState === 1) {
1018
+ daemonWs.send(JSON.stringify({ type: 'output', data }));
1019
+ }
1020
+ if (promptPattern.test(data)) {
1021
+ promptReady = true;
1022
+ flushInjectQueue();
1023
+ if (wsReady && daemonWs.readyState === 1) {
1024
+ daemonWs.send(JSON.stringify({ type: 'ready' }));
1025
+ }
1026
+ }
1027
+ });
1028
+ attachChildExitHandler();
1029
+ emitRestartEvent(crashCount);
1030
+ console.log(`\x1b[32m✅ Session '${sessionId}' restarted (attempt ${crashCount}).\x1b[0m\n`);
1031
+ // Reset crash counter if session survives 30s
1032
+ setTimeout(() => { if (crashCount > 0) crashCount = 0; }, 30000);
1033
+ } catch (err) {
1034
+ console.error(`\x1b[31m❌ Failed to restart session '${sessionId}': ${err.message}\x1b[0m`);
1035
+ emitDeathEvent(exitCode, signal, false);
1036
+ if (!closeAllowSession()) return;
1037
+ exitAllowSession(exitCode || 1);
1038
+ }
1039
+ }, backoffMs);
1040
+ } else {
1041
+ if (isAbnormal && autoRestart && crashCount >= MAX_CRASHES) {
1042
+ console.log(`\n\x1b[31m❌ Session '${sessionId}' crashed ${MAX_CRASHES} times. Giving up.\x1b[0m`);
1043
+ }
1044
+ emitDeathEvent(exitCode, signal, false);
1045
+ if (!closeAllowSession()) return;
1046
+ console.log(`\n\x1b[33mSession '${sessionId}' exited (code ${exitCode}${signal ? ', signal ' + signal : ''}, ${durationStr}).\x1b[0m`);
1047
+ exitAllowSession(exitCode || 0);
1048
+ }
1049
+ });
1050
+ }
1051
+ attachChildExitHandler();
928
1052
 
929
1053
  for (const signalName of ['SIGTERM', 'SIGHUP', 'SIGQUIT']) {
930
1054
  const handler = () => {
@@ -2093,7 +2217,7 @@ Discuss the following topic from your project's perspective. Engage with other s
2093
2217
  Usage:
2094
2218
  telepty daemon Start the background daemon
2095
2219
  telepty spawn --id <id> <command> [args...] Spawn a new background CLI
2096
- telepty allow [--id <id>] <command> [args...] Allow inject on a CLI
2220
+ telepty allow [--id <id>] [--auto-restart] <command> [args...] Allow inject on a CLI (auto-restart on crash)
2097
2221
  telepty list List all active sessions across discovered hosts
2098
2222
  telepty attach [id[@host]] Attach to a session (Interactive picker if no ID)
2099
2223
  telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <id[@host]> "<prompt>" Inject text into a single session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",