@dmsdc-ai/aigentry-telepty 0.1.56 → 0.1.58
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.resolved.md +1 -0
- package/cli.js +270 -6
- package/daemon.js +66 -16
- package/package.json +2 -1
- package/session-routing.js +14 -0
- package/tui.js +301 -0
- package/URGENT_ISSUES.md +0 -31
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RESOLVED - all 3 urgent issues fixed in 40b8e47
|
package/cli.js
CHANGED
|
@@ -598,6 +598,12 @@ async function main() {
|
|
|
598
598
|
return;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
+
if (cmd === 'tui' || cmd === 'dashboard') {
|
|
602
|
+
const { TuiDashboard } = require('./tui');
|
|
603
|
+
new TuiDashboard();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
601
607
|
if (cmd === 'list') {
|
|
602
608
|
try {
|
|
603
609
|
const sessions = await discoverSessions({ silent: true });
|
|
@@ -667,10 +673,21 @@ async function main() {
|
|
|
667
673
|
process.exit(1);
|
|
668
674
|
}
|
|
669
675
|
|
|
670
|
-
// Default session ID =
|
|
676
|
+
// Default session ID = {folder}-{cli} (e.g. aigentry-dustcraw-claude)
|
|
671
677
|
if (!sessionId) {
|
|
672
|
-
|
|
678
|
+
const folder = path.basename(process.cwd());
|
|
679
|
+
const cli = path.basename(command).replace(/\..*$/, '');
|
|
680
|
+
sessionId = `${folder}-${cli}`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Override inherited TELEPTY_SESSION_ID — prevent parent session hijacking
|
|
684
|
+
// When launched via kitty @ launch, the parent's env leaks through.
|
|
685
|
+
// With --id flag, we always use the explicitly requested session ID.
|
|
686
|
+
if (process.env.TELEPTY_SESSION_ID && process.env.TELEPTY_SESSION_ID !== sessionId) {
|
|
687
|
+
console.error(`⚠️ [allow] Overriding inherited TELEPTY_SESSION_ID="${process.env.TELEPTY_SESSION_ID}" → "${sessionId}"`);
|
|
673
688
|
}
|
|
689
|
+
delete process.env.TELEPTY_SESSION_ID;
|
|
690
|
+
process.env.TELEPTY_SESSION_ID = sessionId;
|
|
674
691
|
|
|
675
692
|
await ensureDaemonRunning({ requiredCapabilities: ['wrapped-sessions'] });
|
|
676
693
|
|
|
@@ -735,7 +752,9 @@ async function main() {
|
|
|
735
752
|
}
|
|
736
753
|
|
|
737
754
|
// Connect to daemon WebSocket with auto-reconnect
|
|
738
|
-
|
|
755
|
+
// owner=1 tells daemon this is the allow bridge (owner), not an attach viewer.
|
|
756
|
+
// Daemon uses this to reclaim ownership even if a stale ownerWs is still registered.
|
|
757
|
+
const wsUrl = `ws://${REMOTE_HOST}:${PORT}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(TOKEN)}&owner=1`;
|
|
739
758
|
let daemonWs = null;
|
|
740
759
|
let wsReady = false;
|
|
741
760
|
let reconnectAttempts = 0;
|
|
@@ -776,10 +795,17 @@ async function main() {
|
|
|
776
795
|
try {
|
|
777
796
|
const msg = JSON.parse(message);
|
|
778
797
|
if (msg.type === 'inject') {
|
|
779
|
-
const
|
|
780
|
-
if (
|
|
798
|
+
const isCr = msg.data === '\r';
|
|
799
|
+
if (isCr && injectQueue.length > 0) {
|
|
800
|
+
// CR with pending queued text — queue CR too and flush immediately.
|
|
801
|
+
// Prevents the busy-session bug where CR fires before queued text,
|
|
802
|
+
// causing empty submit followed by orphaned text without Return.
|
|
803
|
+
injectQueue.push(msg.data);
|
|
804
|
+
if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
|
|
805
|
+
flushInjectQueue();
|
|
806
|
+
} else if (isCr || promptReady) {
|
|
781
807
|
child.write(msg.data);
|
|
782
|
-
if (
|
|
808
|
+
if (!isCr && msg.data.length > 1) {
|
|
783
809
|
promptReady = false;
|
|
784
810
|
lastInjectTextTime = Date.now();
|
|
785
811
|
}
|
|
@@ -1151,6 +1177,243 @@ async function main() {
|
|
|
1151
1177
|
return;
|
|
1152
1178
|
}
|
|
1153
1179
|
|
|
1180
|
+
if (cmd === 'session' && args[1] === 'start') {
|
|
1181
|
+
// Generate kitty session file and launch
|
|
1182
|
+
const configArg = args.find(a => a.startsWith('--config='));
|
|
1183
|
+
const configPath = configArg ? configArg.split('=').slice(1).join('=') : null;
|
|
1184
|
+
const cliArg = args.find(a => a.startsWith('--cli='));
|
|
1185
|
+
const cli = cliArg ? cliArg.split('=')[1] : 'claude --dangerously-skip-permissions';
|
|
1186
|
+
const projectsDir = args.find(a => a.startsWith('--dir=')) ? args.find(a => a.startsWith('--dir=')).split('=')[1] : process.cwd();
|
|
1187
|
+
|
|
1188
|
+
// Discover project folders (subdirectories with .git)
|
|
1189
|
+
let projects;
|
|
1190
|
+
if (configPath) {
|
|
1191
|
+
projects = JSON.parse(fs.readFileSync(configPath, 'utf8')).projects;
|
|
1192
|
+
} else {
|
|
1193
|
+
projects = fs.readdirSync(projectsDir, { withFileTypes: true })
|
|
1194
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(projectsDir, d.name, '.git')))
|
|
1195
|
+
.map(d => ({ name: d.name, cwd: path.join(projectsDir, d.name) }));
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (projects.length === 0) {
|
|
1199
|
+
console.error('❌ No git projects found in', projectsDir);
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Resolve full paths (kitty @ launch has no shell PATH)
|
|
1204
|
+
const cliParts = cli.split(' ');
|
|
1205
|
+
let teleptyFullPath, cliFullPath;
|
|
1206
|
+
try {
|
|
1207
|
+
teleptyFullPath = execSync('which telepty', { encoding: 'utf8' }).trim();
|
|
1208
|
+
} catch {
|
|
1209
|
+
teleptyFullPath = process.argv[1];
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
cliFullPath = execSync(`which ${cliParts[0]}`, { encoding: 'utf8' }).trim();
|
|
1213
|
+
} catch {
|
|
1214
|
+
cliFullPath = cliParts[0];
|
|
1215
|
+
}
|
|
1216
|
+
const cliFullArgs = cliParts.slice(1).join(' ');
|
|
1217
|
+
const nodeFullPath = process.execPath; // Bypass #!/usr/bin/env node shebang (nvm not in PATH for non-interactive shells)
|
|
1218
|
+
|
|
1219
|
+
// Generate kitty session file
|
|
1220
|
+
const sessionFile = path.join(os.tmpdir(), `telepty-session-${Date.now()}.conf`);
|
|
1221
|
+
let conf = '# Auto-generated telepty session\n';
|
|
1222
|
+
projects.forEach((p, i) => {
|
|
1223
|
+
const name = p.name;
|
|
1224
|
+
const cwd = p.cwd || path.join(projectsDir, name);
|
|
1225
|
+
const sessionId = `${name}-${cli.split(' ')[0]}`;
|
|
1226
|
+
if (i === 0) {
|
|
1227
|
+
conf += `new_tab ${name}\n`;
|
|
1228
|
+
} else {
|
|
1229
|
+
conf += `\nnew_tab ${name}\n`;
|
|
1230
|
+
}
|
|
1231
|
+
conf += `layout tall\n`;
|
|
1232
|
+
conf += `cd ${cwd}\n`;
|
|
1233
|
+
conf += `title ${name}\n`;
|
|
1234
|
+
conf += `env TELEPTY_SESSION_ID=\n`;
|
|
1235
|
+
conf += `env PATH=${process.env.PATH}\n`;
|
|
1236
|
+
conf += `launch --type=window /bin/zsh -c 'unset TELEPTY_SESSION_ID; ${nodeFullPath} ${teleptyFullPath} allow --id ${sessionId} ${cliFullPath}${cliFullArgs ? ' ' + cliFullArgs : ''}'\n`;
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
fs.writeFileSync(sessionFile, conf);
|
|
1240
|
+
console.log(`✅ Kitty session file: ${sessionFile}`);
|
|
1241
|
+
console.log(` ${projects.length} projects, CLI: ${cli}`);
|
|
1242
|
+
|
|
1243
|
+
// Auto-launch if --launch flag
|
|
1244
|
+
if (args.includes('--launch')) {
|
|
1245
|
+
const { spawn, execFileSync } = require('child_process');
|
|
1246
|
+
|
|
1247
|
+
// Detect existing kitty instance via remote control socket
|
|
1248
|
+
let kittySocket = null;
|
|
1249
|
+
try {
|
|
1250
|
+
const sockFiles = fs.readdirSync('/tmp').filter(f => f.startsWith('kitty-sock'));
|
|
1251
|
+
if (sockFiles.length > 0) {
|
|
1252
|
+
const candidate = '/tmp/' + sockFiles[0];
|
|
1253
|
+
// Verify socket is alive
|
|
1254
|
+
execFileSync('kitty', ['@', '--to', `unix:${candidate}`, 'ls'], {
|
|
1255
|
+
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
1256
|
+
});
|
|
1257
|
+
kittySocket = candidate;
|
|
1258
|
+
}
|
|
1259
|
+
} catch { kittySocket = null; }
|
|
1260
|
+
|
|
1261
|
+
if (kittySocket) {
|
|
1262
|
+
// Launch tabs in existing kitty instance (single Dock icon, kitty @ controllable)
|
|
1263
|
+
let launched = 0;
|
|
1264
|
+
for (const p of projects) {
|
|
1265
|
+
const name = p.name;
|
|
1266
|
+
const cwd = p.cwd || path.join(projectsDir, name);
|
|
1267
|
+
const sessionIdForProject = `${name}-${cli.split(' ')[0]}`;
|
|
1268
|
+
const shellCmd = `unset TELEPTY_SESSION_ID; ${nodeFullPath} ${teleptyFullPath} allow --id ${sessionIdForProject} ${cliFullPath}${cliFullArgs ? ' ' + cliFullArgs : ''}`;
|
|
1269
|
+
const launchArgs = ['@', '--to', `unix:${kittySocket}`,
|
|
1270
|
+
'launch', '--type=tab', '--tab-title', name, '--cwd', cwd,
|
|
1271
|
+
'--env', 'TELEPTY_SESSION_ID=',
|
|
1272
|
+
'--env', `PATH=${process.env.PATH}`,
|
|
1273
|
+
'/bin/zsh', '-c', shellCmd];
|
|
1274
|
+
try {
|
|
1275
|
+
execFileSync('kitty', launchArgs, { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1276
|
+
launched++;
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
console.error(`⚠️ Failed to launch tab for ${name}: ${e.message}`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
console.log(`🚀 ${launched}/${projects.length} tabs launched in existing kitty instance.`);
|
|
1282
|
+
} else {
|
|
1283
|
+
// No existing kitty — start a new instance with session file
|
|
1284
|
+
console.log(`\n Launch: kitty --session ${sessionFile}\n`);
|
|
1285
|
+
spawn('kitty', ['--session', sessionFile], { detached: true, stdio: 'ignore' }).unref();
|
|
1286
|
+
console.log('🚀 Kitty launched (new instance).');
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
console.log(`\n Launch: kitty --session ${sessionFile}\n`);
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (cmd === 'layout') {
|
|
1295
|
+
const layoutType = args[1] || 'grid';
|
|
1296
|
+
const validLayouts = ['grid', 'tall', 'stack'];
|
|
1297
|
+
if (!validLayouts.includes(layoutType)) {
|
|
1298
|
+
console.error(`❌ Invalid layout: ${layoutType}. Valid: ${validLayouts.join(', ')}`);
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
await ensureDaemonRunning();
|
|
1303
|
+
const { execSync } = require('child_process');
|
|
1304
|
+
|
|
1305
|
+
// Get active session count for grid calculation
|
|
1306
|
+
try {
|
|
1307
|
+
const sessionsRes = await fetchWithAuth(`${DAEMON_URL}/api/sessions`);
|
|
1308
|
+
const sessionsList = await sessionsRes.json();
|
|
1309
|
+
const activeIds = Object.keys(sessionsList);
|
|
1310
|
+
if (activeIds.length === 0) {
|
|
1311
|
+
console.log('No active sessions to arrange.');
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
console.error('❌ Could not fetch sessions:', e.message);
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Detect kitty process name
|
|
1320
|
+
let processName = 'kitty';
|
|
1321
|
+
try {
|
|
1322
|
+
execSync(`osascript -e 'tell application "System Events" to get name of first process whose name is "kitty"'`, {
|
|
1323
|
+
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
1324
|
+
});
|
|
1325
|
+
} catch {
|
|
1326
|
+
processName = 'stable';
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Get screen dimensions
|
|
1330
|
+
let screenW = 2560, screenH = 1440;
|
|
1331
|
+
try {
|
|
1332
|
+
const bounds = execSync(`osascript -e 'tell application "Finder" to get bounds of window of desktop'`, {
|
|
1333
|
+
encoding: 'utf8', timeout: 3000
|
|
1334
|
+
}).trim();
|
|
1335
|
+
const parts = bounds.split(', ');
|
|
1336
|
+
screenW = parseInt(parts[2]);
|
|
1337
|
+
screenH = parseInt(parts[3]);
|
|
1338
|
+
} catch {}
|
|
1339
|
+
|
|
1340
|
+
const menuBarH = 25;
|
|
1341
|
+
const dockH = 70;
|
|
1342
|
+
const usableH = screenH - menuBarH - dockH;
|
|
1343
|
+
|
|
1344
|
+
// Build AppleScript — collect windows from ALL kitty process instances
|
|
1345
|
+
// (kitty --session spawns separate processes, each with its own window)
|
|
1346
|
+
const collectWindows = `
|
|
1347
|
+
set wList to {}
|
|
1348
|
+
set kittyProcs to every process whose name is "${processName}"
|
|
1349
|
+
repeat with p in kittyProcs
|
|
1350
|
+
repeat with w in (every window of p)
|
|
1351
|
+
set end of wList to w
|
|
1352
|
+
end repeat
|
|
1353
|
+
end repeat
|
|
1354
|
+
set n to count of wList
|
|
1355
|
+
if n = 0 then return "0"`;
|
|
1356
|
+
|
|
1357
|
+
let layoutBody;
|
|
1358
|
+
if (layoutType === 'grid') {
|
|
1359
|
+
layoutBody = `
|
|
1360
|
+
set numCols to (n ^ 0.5) as integer
|
|
1361
|
+
if numCols * numCols < n then set numCols to numCols + 1
|
|
1362
|
+
set numRows to ((n - 1) div numCols) + 1
|
|
1363
|
+
set cellW to ${screenW} div numCols
|
|
1364
|
+
set cellH to ${usableH} div numRows
|
|
1365
|
+
repeat with i from 1 to n
|
|
1366
|
+
set c to ((i - 1) mod numCols)
|
|
1367
|
+
set r to ((i - 1) div numCols)
|
|
1368
|
+
set position of (item i of wList) to {c * cellW, ${menuBarH} + r * cellH}
|
|
1369
|
+
set size of (item i of wList) to {cellW, cellH}
|
|
1370
|
+
end repeat`;
|
|
1371
|
+
} else if (layoutType === 'tall') {
|
|
1372
|
+
layoutBody = `
|
|
1373
|
+
set halfW to ${screenW} div 2
|
|
1374
|
+
if n = 1 then
|
|
1375
|
+
set position of (item 1 of wList) to {0, ${menuBarH}}
|
|
1376
|
+
set size of (item 1 of wList) to {${screenW}, ${usableH}}
|
|
1377
|
+
else
|
|
1378
|
+
set position of (item 1 of wList) to {0, ${menuBarH}}
|
|
1379
|
+
set size of (item 1 of wList) to {halfW, ${usableH}}
|
|
1380
|
+
set rightH to ${usableH} div (n - 1)
|
|
1381
|
+
repeat with i from 2 to n
|
|
1382
|
+
set y to ${menuBarH} + ((i - 2) * rightH)
|
|
1383
|
+
set position of (item i of wList) to {halfW, y}
|
|
1384
|
+
set size of (item i of wList) to {halfW, rightH}
|
|
1385
|
+
end repeat
|
|
1386
|
+
end if`;
|
|
1387
|
+
} else if (layoutType === 'stack') {
|
|
1388
|
+
layoutBody = `
|
|
1389
|
+
set cellH to ${usableH} div n
|
|
1390
|
+
repeat with i from 1 to n
|
|
1391
|
+
set y to ${menuBarH} + ((i - 1) * cellH)
|
|
1392
|
+
set position of (item i of wList) to {0, y}
|
|
1393
|
+
set size of (item i of wList) to {${screenW}, cellH}
|
|
1394
|
+
end repeat`;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const script = `
|
|
1398
|
+
tell application "System Events"
|
|
1399
|
+
${collectWindows}
|
|
1400
|
+
${layoutBody}
|
|
1401
|
+
return n
|
|
1402
|
+
end tell`;
|
|
1403
|
+
|
|
1404
|
+
try {
|
|
1405
|
+
const result = execSync(`osascript -e '${script}'`, { encoding: 'utf8', timeout: 10000 }).trim();
|
|
1406
|
+
if (result === '0') {
|
|
1407
|
+
console.log('⚠️ No kitty windows found. Sessions may be in tabs — use kitty @ launch --type=os-window for separate windows.');
|
|
1408
|
+
} else {
|
|
1409
|
+
console.log(`✅ Layout '${layoutType}' applied to ${result} kitty windows.`);
|
|
1410
|
+
}
|
|
1411
|
+
} catch (e) {
|
|
1412
|
+
console.error(`❌ Layout failed: ${e.message}`);
|
|
1413
|
+
}
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1154
1417
|
if (cmd === 'deliberate') {
|
|
1155
1418
|
await ensureDaemonRunning();
|
|
1156
1419
|
const subCmd = args[1];
|
|
@@ -1674,6 +1937,7 @@ Usage:
|
|
|
1674
1937
|
telepty listen Listen to the event bus and print JSON to stdout
|
|
1675
1938
|
telepty monitor Human-readable real-time billboard of bus events
|
|
1676
1939
|
telepty update Update telepty to the latest version
|
|
1940
|
+
telepty layout [grid|tall|stack] Arrange kitty windows on screen (default: grid)
|
|
1677
1941
|
|
|
1678
1942
|
Handoff Commands:
|
|
1679
1943
|
handoff list [--status=S] List handoffs (filter: pending/claimed/executing/completed)
|
package/daemon.js
CHANGED
|
@@ -785,7 +785,10 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
785
785
|
});
|
|
786
786
|
kittyOk = true;
|
|
787
787
|
console.log(`[INJECT] Kitty send-text for ${id} (window ${wid})`);
|
|
788
|
-
} catch {
|
|
788
|
+
} catch {
|
|
789
|
+
// Invalidate cached window ID — window may have changed or been closed
|
|
790
|
+
session.kittyWindowId = null;
|
|
791
|
+
}
|
|
789
792
|
}
|
|
790
793
|
if (!kittyOk) {
|
|
791
794
|
// Fallback: WS (works with new allow bridges that have queue flush)
|
|
@@ -795,22 +798,37 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
795
798
|
|
|
796
799
|
if (!no_enter) {
|
|
797
800
|
setTimeout(() => {
|
|
801
|
+
// Primary: osascript keystroke (reliable across all CLIs)
|
|
802
|
+
const submitStrategy = getSubmitStrategy(session.command);
|
|
803
|
+
const keyCombo = submitStrategy === 'osascript_cmd_enter' ? 'cmd_enter' : 'return_key';
|
|
804
|
+
const osascriptOk = submitViaOsascript(id, keyCombo);
|
|
805
|
+
|
|
806
|
+
if (!osascriptOk) {
|
|
807
|
+
// Fallback: kitty send-text → WS
|
|
808
|
+
console.log(`[INJECT] osascript submit failed for ${id}, trying fallback`);
|
|
809
|
+
if (wid && sock) {
|
|
810
|
+
try {
|
|
811
|
+
require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\r'`, {
|
|
812
|
+
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
813
|
+
});
|
|
814
|
+
} catch {
|
|
815
|
+
writeToSession('\r');
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
writeToSession('\r');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Update tab title regardless of submit method
|
|
798
823
|
if (wid && sock) {
|
|
799
824
|
try {
|
|
800
|
-
require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\r'`, {
|
|
801
|
-
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
802
|
-
});
|
|
803
825
|
require('child_process').execSync(`kitty @ --to unix:${sock} set-tab-title --match id:${wid} '⚡ telepty :: ${id}'`, {
|
|
804
826
|
timeout: 2000, stdio: ['pipe', 'pipe', 'pipe']
|
|
805
827
|
});
|
|
806
|
-
} catch {
|
|
807
|
-
writeToSession('\r');
|
|
808
|
-
}
|
|
809
|
-
} else {
|
|
810
|
-
writeToSession('\r');
|
|
828
|
+
} catch {}
|
|
811
829
|
}
|
|
812
830
|
}, 500);
|
|
813
|
-
submitResult = { deferred: true, strategy:
|
|
831
|
+
submitResult = { deferred: true, strategy: 'osascript_with_fallback' };
|
|
814
832
|
}
|
|
815
833
|
} else {
|
|
816
834
|
// Spawned sessions: direct PTY write
|
|
@@ -942,14 +960,21 @@ app.delete('/api/sessions/:id', (req, res) => {
|
|
|
942
960
|
function busAutoRoute(msg) {
|
|
943
961
|
const eventType = msg.type || msg.kind;
|
|
944
962
|
const isRoutable = (eventType === 'turn_request' || eventType === 'deliberation_route_turn') && (msg.target || msg.target_session_id);
|
|
945
|
-
if (!isRoutable)
|
|
963
|
+
if (!isRoutable) {
|
|
964
|
+
// Log all bus messages for debugging (excluding health checks)
|
|
965
|
+
if (eventType && eventType !== 'session_health') {
|
|
966
|
+
console.log(`[BUS] Event: ${eventType} (not routable)`);
|
|
967
|
+
}
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
946
970
|
|
|
947
971
|
const rawTarget = (msg.target || msg.target_session_id).split('@')[0];
|
|
948
|
-
|
|
972
|
+
const turnId = (msg.payload && msg.payload.turn_id) || null;
|
|
973
|
+
console.log(`[BUS-ROUTE] ${eventType}: target=${rawTarget} turn=${turnId} msg_id=${msg.message_id || 'none'}`);
|
|
949
974
|
const targetId = resolveSessionAlias(rawTarget);
|
|
950
975
|
const targetSession = targetId ? sessions[targetId] : null;
|
|
951
976
|
if (!targetSession) {
|
|
952
|
-
console.log(`[BUS-ROUTE] Target ${rawTarget} not found`);
|
|
977
|
+
console.log(`[BUS-ROUTE] Target ${rawTarget} not found among: ${Object.keys(sessions).join(', ')}`);
|
|
953
978
|
return;
|
|
954
979
|
}
|
|
955
980
|
|
|
@@ -1002,6 +1027,8 @@ function busAutoRoute(msg) {
|
|
|
1002
1027
|
source_host: MACHINE_ID,
|
|
1003
1028
|
target_agent: targetId,
|
|
1004
1029
|
source_type: 'bus_auto_route',
|
|
1030
|
+
turn_id: (msg.payload && msg.payload.turn_id) || null,
|
|
1031
|
+
original_message_id: msg.message_id || null,
|
|
1005
1032
|
delivered,
|
|
1006
1033
|
timestamp: new Date().toISOString()
|
|
1007
1034
|
});
|
|
@@ -1327,6 +1354,21 @@ wss.on('connection', (ws, req) => {
|
|
|
1327
1354
|
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
1328
1355
|
const sessionId = url.pathname.split('/').pop();
|
|
1329
1356
|
const session = sessions[sessionId];
|
|
1357
|
+
// ?owner=1 indicates the allow bridge (PTY owner), not an attach viewer
|
|
1358
|
+
const isOwnerConnect = url.searchParams.get('owner') === '1';
|
|
1359
|
+
|
|
1360
|
+
// Ping/pong heartbeat — detect and terminate stale TCP half-open connections (30s interval)
|
|
1361
|
+
let isAlive = true;
|
|
1362
|
+
ws.on('pong', () => { isAlive = true; });
|
|
1363
|
+
const pingInterval = setInterval(() => {
|
|
1364
|
+
if (!isAlive) {
|
|
1365
|
+
console.log(`[WS] Terminating stale connection (no pong) for ${sessionId}`);
|
|
1366
|
+
ws.terminate();
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
isAlive = false;
|
|
1370
|
+
ws.ping();
|
|
1371
|
+
}, 30000);
|
|
1330
1372
|
|
|
1331
1373
|
if (!session) {
|
|
1332
1374
|
// Auto-register wrapped session on WS connect (supports reconnect after daemon restart)
|
|
@@ -1363,10 +1405,17 @@ wss.on('connection', (ws, req) => {
|
|
|
1363
1405
|
|
|
1364
1406
|
const activeSession = sessions[sessionId];
|
|
1365
1407
|
|
|
1366
|
-
// For wrapped sessions, first connector becomes the owner
|
|
1367
|
-
|
|
1408
|
+
// For wrapped sessions, first connector OR explicit ?owner=1 claim becomes the owner.
|
|
1409
|
+
// ?owner=1 reclaim handles the stale-ownerWs bug: allow bridge reconnects but stale TCP
|
|
1410
|
+
// half-open connection still holds ownerWs slot → reconnect wrongly becomes a viewer.
|
|
1411
|
+
if (activeSession.type === 'wrapped' && (!activeSession.ownerWs || isOwnerConnect)) {
|
|
1412
|
+
if (isOwnerConnect && activeSession.ownerWs && activeSession.ownerWs !== ws) {
|
|
1413
|
+
// Terminate the stale owner connection before claiming ownership
|
|
1414
|
+
console.log(`[WS] Replacing stale ownerWs for session ${sessionId}`);
|
|
1415
|
+
activeSession.ownerWs.terminate();
|
|
1416
|
+
}
|
|
1368
1417
|
activeSession.ownerWs = ws;
|
|
1369
|
-
console.log(`[WS] Wrap owner connected for session ${sessionId} (Total: ${activeSession.clients.size})`);
|
|
1418
|
+
console.log(`[WS] Wrap owner ${isOwnerConnect && activeSession.clients.size > 1 ? 're-' : ''}connected for session ${sessionId} (Total: ${activeSession.clients.size})`);
|
|
1370
1419
|
} else {
|
|
1371
1420
|
console.log(`[WS] Client attached to session ${sessionId} (Total: ${activeSession.clients.size})`);
|
|
1372
1421
|
}
|
|
@@ -1408,6 +1457,7 @@ wss.on('connection', (ws, req) => {
|
|
|
1408
1457
|
});
|
|
1409
1458
|
|
|
1410
1459
|
ws.on('close', () => {
|
|
1460
|
+
clearInterval(pingInterval);
|
|
1411
1461
|
activeSession.clients.delete(ws);
|
|
1412
1462
|
if (activeSession.type === 'wrapped' && ws === activeSession.ownerWs) {
|
|
1413
1463
|
activeSession.ownerWs = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
4
4
|
"main": "daemon.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aigentry-telepty": "install.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"license": "ISC",
|
|
18
18
|
"description": "",
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"blessed": "^0.1.81",
|
|
20
21
|
"cors": "^2.8.6",
|
|
21
22
|
"express": "^5.2.1",
|
|
22
23
|
"node-pty": "^1.2.0-beta.11",
|
package/session-routing.js
CHANGED
|
@@ -48,6 +48,20 @@ function pickSessionTarget(sessionRef, sessions, defaultHost = '127.0.0.1') {
|
|
|
48
48
|
const matches = sessions.filter((session) => session.id === parsed.id);
|
|
49
49
|
|
|
50
50
|
if (matches.length === 0) {
|
|
51
|
+
// Project-based fallback: match sessions whose ID starts with the requested prefix.
|
|
52
|
+
// e.g., "aigentry-dustcraw" matches "aigentry-dustcraw-001" or "aigentry-dustcraw-claude".
|
|
53
|
+
const prefixMatches = sessions.filter((s) =>
|
|
54
|
+
s.id.startsWith(parsed.id + '-') || s.id.startsWith(parsed.id)
|
|
55
|
+
);
|
|
56
|
+
if (prefixMatches.length === 1) {
|
|
57
|
+
return { id: prefixMatches[0].id, host: prefixMatches[0].host };
|
|
58
|
+
}
|
|
59
|
+
if (prefixMatches.length > 1) {
|
|
60
|
+
// Multiple candidates: prefer same host, then first alphabetically
|
|
61
|
+
const local = prefixMatches.find((s) => s.host === '127.0.0.1');
|
|
62
|
+
const best = local || prefixMatches.sort((a, b) => a.id.localeCompare(b.id))[0];
|
|
63
|
+
return { id: best.id, host: best.host };
|
|
64
|
+
}
|
|
51
65
|
return null;
|
|
52
66
|
}
|
|
53
67
|
|
package/tui.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const blessed = require('blessed');
|
|
4
|
+
const { getConfig } = require('./auth');
|
|
5
|
+
|
|
6
|
+
const PORT = process.env.PORT || 3848;
|
|
7
|
+
const DAEMON_URL = `http://localhost:${PORT}`;
|
|
8
|
+
const POLL_INTERVAL = 2000;
|
|
9
|
+
const STALE_THRESHOLD = 120; // seconds idle before "stale"
|
|
10
|
+
|
|
11
|
+
class TuiDashboard {
|
|
12
|
+
constructor() {
|
|
13
|
+
const cfg = getConfig();
|
|
14
|
+
this.token = cfg.authToken;
|
|
15
|
+
this.sessions = [];
|
|
16
|
+
this.selectedIndex = 0;
|
|
17
|
+
this.pollTimer = null;
|
|
18
|
+
this.busWs = null;
|
|
19
|
+
this.busLog = [];
|
|
20
|
+
this.setupScreen();
|
|
21
|
+
this.startPolling();
|
|
22
|
+
this.connectBus();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── API helpers ──────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
async apiFetch(path, options = {}) {
|
|
28
|
+
const headers = { 'x-telepty-token': this.token, ...options.headers };
|
|
29
|
+
if (options.body) headers['Content-Type'] = 'application/json';
|
|
30
|
+
const res = await fetch(`${DAEMON_URL}${path}`, { ...options, headers });
|
|
31
|
+
return res.json();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async fetchSessions() {
|
|
35
|
+
try {
|
|
36
|
+
this.sessions = await this.apiFetch('/api/sessions');
|
|
37
|
+
this.sessions.sort((a, b) => a.id.localeCompare(b.id));
|
|
38
|
+
if (this.selectedIndex >= this.sessions.length) {
|
|
39
|
+
this.selectedIndex = Math.max(0, this.sessions.length - 1);
|
|
40
|
+
}
|
|
41
|
+
this.renderSessionList();
|
|
42
|
+
} catch {
|
|
43
|
+
this.setStatus('{red-fg}Daemon unreachable{/}');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async injectToSession(id, prompt) {
|
|
48
|
+
try {
|
|
49
|
+
const res = await this.apiFetch(`/api/sessions/${encodeURIComponent(id)}/inject`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
body: JSON.stringify({ prompt })
|
|
52
|
+
});
|
|
53
|
+
if (res.success) {
|
|
54
|
+
this.setStatus(`{green-fg}Injected to ${id}{/}`);
|
|
55
|
+
} else {
|
|
56
|
+
this.setStatus(`{red-fg}Inject failed: ${res.error || 'unknown'}{/}`);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
this.setStatus(`{red-fg}Inject error: ${e.message}{/}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async broadcastMessage(prompt) {
|
|
64
|
+
try {
|
|
65
|
+
const res = await this.apiFetch('/api/sessions/broadcast/inject', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
body: JSON.stringify({ prompt })
|
|
68
|
+
});
|
|
69
|
+
const ok = res.results?.successful?.length || 0;
|
|
70
|
+
this.setStatus(`{green-fg}Broadcast to ${ok} sessions{/}`);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
this.setStatus(`{red-fg}Broadcast error: ${e.message}{/}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Event Bus ────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
connectBus() {
|
|
79
|
+
try {
|
|
80
|
+
const WebSocket = require('ws');
|
|
81
|
+
this.busWs = new WebSocket(
|
|
82
|
+
`ws://localhost:${PORT}/api/bus?token=${encodeURIComponent(this.token)}`
|
|
83
|
+
);
|
|
84
|
+
this.busWs.on('message', (data) => {
|
|
85
|
+
try {
|
|
86
|
+
const msg = JSON.parse(data);
|
|
87
|
+
const ts = new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
88
|
+
let line = `${ts} `;
|
|
89
|
+
if (msg.type === 'inject_written') {
|
|
90
|
+
line += `{cyan-fg}inject{/} -> ${msg.target || '?'}`;
|
|
91
|
+
} else if (msg.type === 'injection') {
|
|
92
|
+
line += `{cyan-fg}broadcast{/} -> ${msg.target_agent || 'all'}`;
|
|
93
|
+
} else if (msg.type === 'message_routed') {
|
|
94
|
+
line += `{yellow-fg}${msg.from || '?'}{/} -> {green-fg}${msg.to || '?'}{/}`;
|
|
95
|
+
} else {
|
|
96
|
+
line += `{white-fg}${msg.type || 'event'}{/}`;
|
|
97
|
+
}
|
|
98
|
+
this.busLog.push(line);
|
|
99
|
+
if (this.busLog.length > 100) this.busLog.shift();
|
|
100
|
+
this.renderBusLog();
|
|
101
|
+
} catch { /* ignore malformed */ }
|
|
102
|
+
});
|
|
103
|
+
this.busWs.on('close', () => {
|
|
104
|
+
setTimeout(() => this.connectBus(), 3000);
|
|
105
|
+
});
|
|
106
|
+
this.busWs.on('error', () => {});
|
|
107
|
+
} catch { /* ws not available */ }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Screen setup ─────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
setupScreen() {
|
|
113
|
+
this.screen = blessed.screen({
|
|
114
|
+
smartCSR: true,
|
|
115
|
+
title: 'telepty dashboard',
|
|
116
|
+
fullUnicode: true
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Header
|
|
120
|
+
this.header = blessed.box({
|
|
121
|
+
parent: this.screen,
|
|
122
|
+
top: 0, left: 0, width: '100%', height: 1,
|
|
123
|
+
tags: true,
|
|
124
|
+
style: { fg: 'white', bg: 'blue' },
|
|
125
|
+
content: ' {bold}telepty dashboard{/bold}'
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Session list (left panel)
|
|
129
|
+
this.sessionList = blessed.list({
|
|
130
|
+
parent: this.screen,
|
|
131
|
+
top: 1, left: 0, width: '60%', bottom: 3,
|
|
132
|
+
border: { type: 'line' },
|
|
133
|
+
label: ' Sessions ',
|
|
134
|
+
tags: true,
|
|
135
|
+
keys: true,
|
|
136
|
+
vi: true,
|
|
137
|
+
mouse: true,
|
|
138
|
+
scrollbar: { ch: '│', style: { fg: 'cyan' } },
|
|
139
|
+
style: {
|
|
140
|
+
border: { fg: 'cyan' },
|
|
141
|
+
selected: { bg: 'blue', fg: 'white', bold: true },
|
|
142
|
+
item: { fg: 'white' }
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
this.sessionList.on('select item', (item, index) => {
|
|
147
|
+
this.selectedIndex = index;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Event bus log (right panel)
|
|
151
|
+
this.busPanel = blessed.log({
|
|
152
|
+
parent: this.screen,
|
|
153
|
+
top: 1, left: '60%', right: 0, bottom: 3,
|
|
154
|
+
border: { type: 'line' },
|
|
155
|
+
label: ' Event Bus ',
|
|
156
|
+
tags: true,
|
|
157
|
+
scrollbar: { ch: '│', style: { fg: 'yellow' } },
|
|
158
|
+
style: {
|
|
159
|
+
border: { fg: 'yellow' }
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Shortcut bar
|
|
164
|
+
this.shortcutBar = blessed.box({
|
|
165
|
+
parent: this.screen,
|
|
166
|
+
bottom: 1, left: 0, width: '100%', height: 1,
|
|
167
|
+
tags: true,
|
|
168
|
+
style: { fg: 'white', bg: 'gray' },
|
|
169
|
+
content: ' {bold}i{/}:Inject {bold}b{/}:Broadcast {bold}r{/}:Refresh {bold}q{/}:Quit'
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Status bar
|
|
173
|
+
this.statusBar = blessed.box({
|
|
174
|
+
parent: this.screen,
|
|
175
|
+
bottom: 0, left: 0, width: '100%', height: 1,
|
|
176
|
+
tags: true,
|
|
177
|
+
style: { fg: 'white', bg: 'black' },
|
|
178
|
+
content: ' Ready'
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ── Keyboard handlers ──────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
this.screen.key(['q', 'C-c'], () => {
|
|
184
|
+
this.cleanup();
|
|
185
|
+
process.exit(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
this.screen.key(['i'], () => {
|
|
189
|
+
const session = this.sessions[this.selectedIndex];
|
|
190
|
+
if (!session) return this.setStatus('{red-fg}No session selected{/}');
|
|
191
|
+
this.promptInput(`Inject to ${session.id}:`, (text) => {
|
|
192
|
+
if (text) this.injectToSession(session.id, text);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.screen.key(['b'], () => {
|
|
197
|
+
this.promptInput('Broadcast to all:', (text) => {
|
|
198
|
+
if (text) this.broadcastMessage(text);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
this.screen.key(['r'], () => {
|
|
203
|
+
this.fetchSessions();
|
|
204
|
+
this.setStatus('{green-fg}Refreshed{/}');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.sessionList.focus();
|
|
208
|
+
this.screen.render();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Input prompt ─────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
promptInput(label, callback) {
|
|
214
|
+
const inputBox = blessed.textbox({
|
|
215
|
+
parent: this.screen,
|
|
216
|
+
bottom: 0, left: 0, width: '100%', height: 3,
|
|
217
|
+
border: { type: 'line' },
|
|
218
|
+
label: ` ${label} `,
|
|
219
|
+
tags: true,
|
|
220
|
+
inputOnFocus: true,
|
|
221
|
+
style: {
|
|
222
|
+
border: { fg: 'green' },
|
|
223
|
+
fg: 'white'
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
inputBox.focus();
|
|
228
|
+
inputBox.readInput((err, value) => {
|
|
229
|
+
inputBox.destroy();
|
|
230
|
+
this.sessionList.focus();
|
|
231
|
+
this.screen.render();
|
|
232
|
+
if (!err && value) callback(value);
|
|
233
|
+
});
|
|
234
|
+
this.screen.render();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Rendering ────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
getStatusInfo(session) {
|
|
240
|
+
const idle = session.idleSeconds;
|
|
241
|
+
const clients = session.active_clients || 0;
|
|
242
|
+
|
|
243
|
+
if (clients === 0) return { icon: '{red-fg}✕{/}', label: '{red-fg}dead{/}' };
|
|
244
|
+
if (idle !== null && idle > STALE_THRESHOLD) return { icon: '{yellow-fg}○{/}', label: '{yellow-fg}stale{/}' };
|
|
245
|
+
if (idle !== null && idle < 10) return { icon: '{green-fg}●{/}', label: '{green-fg}busy{/}' };
|
|
246
|
+
return { icon: '{green-fg}●{/}', label: '{white-fg}idle{/}' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
renderSessionList() {
|
|
250
|
+
const items = this.sessions.map((s) => {
|
|
251
|
+
const { icon, label } = this.getStatusInfo(s);
|
|
252
|
+
const shortId = s.id.replace(/^aigentry-/, '').replace(/-claude$/, '');
|
|
253
|
+
return ` ${icon} ${shortId.padEnd(24)} ${label} {gray-fg}C:${s.active_clients}{/}`;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
this.sessionList.setItems(items);
|
|
257
|
+
this.sessionList.setLabel(` Sessions (${this.sessions.length}) `);
|
|
258
|
+
if (this.selectedIndex < items.length) {
|
|
259
|
+
this.sessionList.select(this.selectedIndex);
|
|
260
|
+
}
|
|
261
|
+
this.screen.render();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
renderBusLog() {
|
|
265
|
+
// Show last N lines that fit
|
|
266
|
+
const height = this.busPanel.height - 2;
|
|
267
|
+
const visible = this.busLog.slice(-height);
|
|
268
|
+
this.busPanel.setContent(visible.join('\n'));
|
|
269
|
+
this.screen.render();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
setStatus(msg) {
|
|
273
|
+
this.statusBar.setContent(` ${msg}`);
|
|
274
|
+
this.screen.render();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
startPolling() {
|
|
280
|
+
this.fetchSessions();
|
|
281
|
+
this.pollTimer = setInterval(() => this.fetchSessions(), POLL_INTERVAL);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
cleanup() {
|
|
285
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
286
|
+
if (this.busWs) this.busWs.close();
|
|
287
|
+
this.screen.destroy();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Entry point ──────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function main() {
|
|
294
|
+
new TuiDashboard();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = { TuiDashboard };
|
|
298
|
+
|
|
299
|
+
if (require.main === module) {
|
|
300
|
+
main();
|
|
301
|
+
}
|
package/URGENT_ISSUES.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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 지원 검토
|