@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.
- package/URGENT_ISSUES.md +31 -0
- package/cli.js +68 -33
- package/daemon.js +76 -16
- package/package.json +1 -1
package/URGENT_ISSUES.md
ADDED
|
@@ -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
|
|
719
|
+
// Connect to daemon WebSocket with auto-reconnect
|
|
720
720
|
const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}`;
|
|
721
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
});
|
|
728
|
+
function connectDaemonWs() {
|
|
729
|
+
daemonWs = new WebSocket(wsUrl);
|
|
727
730
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
763
|
+
} else if (msg.type === 'resize') {
|
|
764
|
+
child.resize(msg.cols, msg.rows);
|
|
744
765
|
}
|
|
745
|
-
}
|
|
746
|
-
|
|
766
|
+
} catch (e) {
|
|
767
|
+
// ignore malformed messages
|
|
747
768
|
}
|
|
748
|
-
}
|
|
749
|
-
// ignore malformed messages
|
|
750
|
-
}
|
|
751
|
-
});
|
|
769
|
+
});
|
|
752
770
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
771
|
+
daemonWs.on('close', () => {
|
|
772
|
+
wsReady = false;
|
|
773
|
+
scheduleReconnect();
|
|
774
|
+
});
|
|
757
775
|
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
215
|
-
const
|
|
216
|
-
if (!
|
|
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
|
|
421
|
-
const
|
|
422
|
-
if (!
|
|
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
|
|
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
|
|
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
|
|
597
|
-
const
|
|
598
|
-
if (!
|
|
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;
|