@dmsdc-ai/aigentry-telepty 0.1.20 → 0.1.22

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.
@@ -0,0 +1,31 @@
1
+ # Telepty 긴급 수정 사항 3건
2
+
3
+ ## 1. 데몬 재시작 시 전체 세션 끊김 → 자동 재접속
4
+
5
+ **현상**: 데몬이 재시작되면 모든 allow 브릿지 세션이 끊기고 수동으로 다시 연결해야 함.
6
+
7
+ **필요한 수정**:
8
+ - allow 브릿지가 데몬 재시작/연결 끊김을 감지
9
+ - 자동 재접속 retry loop (exponential backoff: 1s → 2s → 4s → max 30s)
10
+ - 세션 메타데이터(ID, command, CWD)가 재접속 시 복원되어야 함
11
+
12
+ ## 2. inject 시 엔터 미전송
13
+
14
+ **현상**: inject된 메시지가 프롬프트에 표시되지만 엔터가 자동으로 안 눌림. 사용자가 수동으로 엔터를 눌러야 실행됨.
15
+
16
+ **0.1.21에서 수정했다고 했으나 여전히 발생 중.**
17
+
18
+ **필요한 수정**:
19
+ - 모든 CLI(Claude, Codex, Gemini)에서 inject 후 엔터 자동 전송 보장
20
+ - HTTP API inject (POST /api/sessions/:id/inject)도 동일하게 동작해야 함
21
+ - deferred/split_cr 전략이 실제로 엔터를 전송하는지 검증
22
+
23
+ ## 3. 통신 대상 세션 소실 시 자동 재검색
24
+
25
+ **현상**: inject 대상 세션이 사라지면 에러 후 종료. 통신 단절.
26
+
27
+ **필요한 수정**:
28
+ - 세션이 사라지면 같은 프로젝트(CWD)에서 새로 생성된 세션을 자동 검색
29
+ - 예: `aigentry-dustcraw-001`이 사라지고 `aigentry-dustcraw-002`가 생기면 자동 라우팅
30
+ - project-based routing: CWD 또는 session name prefix로 매칭
31
+ - `telepty inject --project aigentry-dustcraw "메시지"` 같은 project alias 지원 검토
package/cli.js CHANGED
@@ -716,45 +716,82 @@ async function main() {
716
716
  promptReady = false;
717
717
  }
718
718
 
719
- // Connect to daemon WebSocket for inject reception and output relay
719
+ // Connect to daemon WebSocket with auto-reconnect
720
720
  const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
721
- const daemonWs = new WebSocket(wsUrl);
721
+ let daemonWs = null;
722
722
  let wsReady = false;
723
+ let reconnectAttempts = 0;
724
+ let reconnectTimer = null;
725
+ let lastInjectTextTime = 0;
726
+ const MAX_RECONNECT_DELAY = 30000;
727
+
728
+ function connectDaemonWs() {
729
+ daemonWs = new WebSocket(wsUrl);
730
+
731
+ daemonWs.on('open', async () => {
732
+ wsReady = true;
733
+ if (reconnectAttempts > 0) {
734
+ console.error(`\n\x1b[32m⚡ Reconnected to daemon. Inject restored.\x1b[0m`);
735
+ // Re-register session on reconnect
736
+ try {
737
+ await fetchWithAuth(`${DAEMON_URL}/api/sessions/register`, {
738
+ method: 'POST',
739
+ headers: { 'Content-Type': 'application/json' },
740
+ body: JSON.stringify({ session_id: sessionId, command, cwd: process.cwd() })
741
+ });
742
+ } catch (e) {
743
+ // Registration may fail if session already exists, that's fine
744
+ }
745
+ }
746
+ reconnectAttempts = 0;
747
+ });
723
748
 
724
- daemonWs.on('open', () => {
725
- wsReady = true;
726
- });
727
-
728
- // Receive inject messages from daemon
729
- daemonWs.on('message', (message) => {
730
- try {
731
- const msg = JSON.parse(message);
732
- if (msg.type === 'inject') {
733
- if (promptReady) {
734
- child.write(msg.data);
735
- // After writing prompt text (not \r), mark as not ready until next prompt
736
- if (msg.data !== '\r' && msg.data.length > 1) {
737
- promptReady = false;
749
+ daemonWs.on('message', (message) => {
750
+ try {
751
+ const msg = JSON.parse(message);
752
+ if (msg.type === 'inject') {
753
+ const isFollowUpCr = msg.data === '\r' && (Date.now() - lastInjectTextTime) < 1000;
754
+ if (promptReady || isFollowUpCr) {
755
+ child.write(msg.data);
756
+ if (msg.data !== '\r' && msg.data.length > 1) {
757
+ promptReady = false;
758
+ lastInjectTextTime = Date.now();
759
+ }
760
+ } else {
761
+ injectQueue.push(msg.data);
738
762
  }
739
- } else {
740
- injectQueue.push(msg.data);
763
+ } else if (msg.type === 'resize') {
764
+ child.resize(msg.cols, msg.rows);
741
765
  }
742
- } else if (msg.type === 'resize') {
743
- child.resize(msg.cols, msg.rows);
766
+ } catch (e) {
767
+ // ignore malformed messages
744
768
  }
745
- } catch (e) {
746
- // ignore malformed messages
747
- }
748
- });
769
+ });
749
770
 
750
- daemonWs.on('close', () => {
751
- wsReady = false;
752
- console.error(`\n\x1b[33m⚠️ Disconnected from daemon. Inject unavailable. Session continues locally.\x1b[0m`);
753
- });
771
+ daemonWs.on('close', () => {
772
+ wsReady = false;
773
+ scheduleReconnect();
774
+ });
754
775
 
755
- daemonWs.on('error', () => {
756
- // silently handle
757
- });
776
+ daemonWs.on('error', () => {
777
+ // Error will be followed by close event
778
+ });
779
+ }
780
+
781
+ function scheduleReconnect() {
782
+ if (reconnectTimer) return;
783
+ reconnectAttempts++;
784
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), MAX_RECONNECT_DELAY);
785
+ if (reconnectAttempts === 1) {
786
+ console.error(`\n\x1b[33m⚠️ Disconnected from daemon. Reconnecting...\x1b[0m`);
787
+ }
788
+ reconnectTimer = setTimeout(() => {
789
+ reconnectTimer = null;
790
+ connectDaemonWs();
791
+ }, delay);
792
+ }
793
+
794
+ connectDaemonWs();
758
795
 
759
796
  // Set terminal title
760
797
  process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}\x07`);
@@ -801,6 +838,7 @@ async function main() {
801
838
 
802
839
  // Deregister from daemon
803
840
  fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
841
+ if (reconnectTimer) clearTimeout(reconnectTimer);
804
842
  daemonWs.close();
805
843
  process.exit(exitCode || 0);
806
844
  });
package/daemon.js CHANGED
@@ -74,6 +74,32 @@ function buildSessionEnv(sessionId) {
74
74
  return env;
75
75
  }
76
76
 
77
+ // Stable alias routing: resolve alias to latest session with matching prefix
78
+ function resolveSessionAlias(requestedId) {
79
+ // Exact match first
80
+ if (sessions[requestedId]) return requestedId;
81
+
82
+ // Strip trailing version number to get base alias (e.g., "aigentry-dustcraw-002" → "aigentry-dustcraw")
83
+ // Also handles bare alias like "aigentry-dustcraw"
84
+ const baseAlias = requestedId.replace(/-\d+$/, '');
85
+
86
+ // Find all sessions matching the base alias
87
+ const candidates = Object.keys(sessions).filter(id => {
88
+ const candidateBase = id.replace(/-\d+$/, '');
89
+ return candidateBase === baseAlias;
90
+ });
91
+
92
+ if (candidates.length === 0) return null;
93
+
94
+ // Return the most recently created session
95
+ candidates.sort((a, b) => {
96
+ const timeA = new Date(sessions[a].createdAt).getTime();
97
+ const timeB = new Date(sessions[b].createdAt).getTime();
98
+ return timeB - timeA;
99
+ });
100
+ return candidates[0];
101
+ }
102
+
77
103
  app.post('/api/sessions/spawn', (req, res) => {
78
104
  const { session_id, command, args = [], cwd = process.cwd(), cols = 80, rows = 30, type = 'AGENT' } = req.body;
79
105
  if (!session_id) return res.status(400).json({ error: 'session_id is strictly required.' });
@@ -180,6 +206,26 @@ app.post('/api/sessions/register', (req, res) => {
180
206
  clients: new Set(),
181
207
  isClosing: false
182
208
  };
209
+ // Check for existing session with same base alias and emit replaced event
210
+ const baseAlias = session_id.replace(/-\d+$/, '');
211
+ const replaced = Object.keys(sessions).find(id => {
212
+ return id !== session_id && id.replace(/-\d+$/, '') === baseAlias;
213
+ });
214
+ if (replaced) {
215
+ const replacedMsg = JSON.stringify({
216
+ type: 'session.replaced',
217
+ sender: 'daemon',
218
+ old_id: replaced,
219
+ new_id: session_id,
220
+ alias: baseAlias,
221
+ timestamp: new Date().toISOString()
222
+ });
223
+ busClients.forEach(client => {
224
+ if (client.readyState === 1) client.send(replacedMsg);
225
+ });
226
+ console.log(`[ALIAS] Session '${replaced}' replaced by '${session_id}' (alias: ${baseAlias})`);
227
+ }
228
+
183
229
  sessions[session_id] = sessionRecord;
184
230
 
185
231
  const busMsg = JSON.stringify({
@@ -211,11 +257,13 @@ app.get('/api/sessions', (req, res) => {
211
257
  });
212
258
 
213
259
  app.get('/api/sessions/:id', (req, res) => {
214
- const { id } = req.params;
215
- const session = sessions[id];
216
- if (!session) return res.status(404).json({ error: 'Session not found' });
260
+ const requestedId = req.params.id;
261
+ const resolvedId = resolveSessionAlias(requestedId);
262
+ if (!resolvedId) return res.status(404).json({ error: 'Session not found' });
263
+ const session = sessions[resolvedId];
217
264
  res.json({
218
- id,
265
+ id: resolvedId,
266
+ alias: requestedId !== resolvedId ? requestedId : null,
219
267
  type: session.type || 'spawned',
220
268
  command: session.command,
221
269
  cwd: session.cwd,
@@ -417,9 +465,11 @@ function submitViaOsascript(sessionId, keyCombo) {
417
465
 
418
466
  // POST /api/sessions/:id/submit — CLI-aware submit
419
467
  app.post('/api/sessions/:id/submit', (req, res) => {
420
- const { id } = req.params;
421
- const session = sessions[id];
422
- if (!session) return res.status(404).json({ error: 'Session not found' });
468
+ const requestedId = req.params.id;
469
+ const resolvedId = resolveSessionAlias(requestedId);
470
+ if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
471
+ const session = sessions[resolvedId];
472
+ const id = resolvedId;
423
473
 
424
474
  const strategy = getSubmitStrategy(session.command);
425
475
  console.log(`[SUBMIT] Session ${id} (${session.command}) using strategy: ${strategy}`);
@@ -475,10 +525,12 @@ app.post('/api/sessions/submit-all', (req, res) => {
475
525
  });
476
526
 
477
527
  app.post('/api/sessions/:id/inject', (req, res) => {
478
- const { id } = req.params;
528
+ const requestedId = req.params.id;
529
+ const resolvedId = resolveSessionAlias(requestedId);
530
+ if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
531
+ const session = sessions[resolvedId];
532
+ const id = resolvedId;
479
533
  const { prompt, no_enter, auto_submit, from, reply_to } = req.body;
480
- const session = sessions[id];
481
- if (!session) return res.status(404).json({ error: 'Session not found' });
482
534
  if (!prompt) return res.status(400).json({ error: 'prompt is required' });
483
535
  if (from) session.lastInjectFrom = from;
484
536
  if (reply_to) session.lastInjectReplyTo = reply_to;
@@ -529,6 +581,10 @@ app.post('/api/sessions/:id/inject', (req, res) => {
529
581
  if (client.readyState === 1) client.send(busMsg);
530
582
  });
531
583
 
584
+ if (requestedId !== resolvedId) {
585
+ console.log(`[ALIAS] Resolved '${requestedId}' → '${resolvedId}'`);
586
+ }
587
+
532
588
  if (from && reply_to) {
533
589
  const routedMsg = JSON.stringify({
534
590
  type: 'message_routed',
@@ -564,10 +620,12 @@ app.post('/api/sessions/:id/inject', (req, res) => {
564
620
  });
565
621
 
566
622
  app.patch('/api/sessions/:id', (req, res) => {
567
- const { id } = req.params;
623
+ const requestedId = req.params.id;
624
+ const resolvedId = resolveSessionAlias(requestedId);
625
+ if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
626
+ const session = sessions[resolvedId];
627
+ const id = resolvedId;
568
628
  const { new_id } = req.body;
569
- const session = sessions[id];
570
- if (!session) return res.status(404).json({ error: 'Session not found' });
571
629
  if (!new_id) return res.status(400).json({ error: 'new_id is required' });
572
630
  if (sessions[new_id]) return res.status(409).json({ error: `Session ID '${new_id}' is already in use.` });
573
631
 
@@ -593,9 +651,11 @@ app.patch('/api/sessions/:id', (req, res) => {
593
651
  });
594
652
 
595
653
  app.delete('/api/sessions/:id', (req, res) => {
596
- const { id } = req.params;
597
- const session = sessions[id];
598
- if (!session) return res.status(404).json({ error: 'Session not found' });
654
+ const requestedId = req.params.id;
655
+ const resolvedId = resolveSessionAlias(requestedId);
656
+ if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
657
+ const session = sessions[resolvedId];
658
+ const id = resolvedId;
599
659
  if (session.isClosing) return res.json({ success: true, status: 'closing' });
600
660
  try {
601
661
  session.isClosing = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",