@dmsdc-ai/aigentry-telepty 0.1.21 → 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,48 +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;
723
727
 
724
- daemonWs.on('open', () => {
725
- wsReady = true;
726
- });
728
+ function connectDaemonWs() {
729
+ daemonWs = new WebSocket(wsUrl);
727
730
 
728
- // Receive inject messages from daemon
729
- let lastInjectTextTime = 0;
730
- daemonWs.on('message', (message) => {
731
- try {
732
- const msg = JSON.parse(message);
733
- if (msg.type === 'inject') {
734
- // Allow \r through if it follows a recently-written inject text (within 1s)
735
- const isFollowUpCr = msg.data === '\r' && (Date.now() - lastInjectTextTime) < 1000;
736
- if (promptReady || isFollowUpCr) {
737
- child.write(msg.data);
738
- if (msg.data !== '\r' && msg.data.length > 1) {
739
- promptReady = false;
740
- lastInjectTextTime = Date.now();
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
+ });
748
+
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);
741
762
  }
742
- } else {
743
- injectQueue.push(msg.data);
763
+ } else if (msg.type === 'resize') {
764
+ child.resize(msg.cols, msg.rows);
744
765
  }
745
- } else if (msg.type === 'resize') {
746
- child.resize(msg.cols, msg.rows);
766
+ } catch (e) {
767
+ // ignore malformed messages
747
768
  }
748
- } catch (e) {
749
- // ignore malformed messages
750
- }
751
- });
769
+ });
752
770
 
753
- daemonWs.on('close', () => {
754
- wsReady = false;
755
- console.error(`\n\x1b[33m⚠️ Disconnected from daemon. Inject unavailable. Session continues locally.\x1b[0m`);
756
- });
771
+ daemonWs.on('close', () => {
772
+ wsReady = false;
773
+ scheduleReconnect();
774
+ });
757
775
 
758
- daemonWs.on('error', () => {
759
- // silently handle
760
- });
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();
761
795
 
762
796
  // Set terminal title
763
797
  process.stdout.write(`\x1b]0;⚡ telepty :: ${sessionId}\x07`);
@@ -804,6 +838,7 @@ async function main() {
804
838
 
805
839
  // Deregister from daemon
806
840
  fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
841
+ if (reconnectTimer) clearTimeout(reconnectTimer);
807
842
  daemonWs.close();
808
843
  process.exit(exitCode || 0);
809
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.21",
3
+ "version": "0.1.22",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",