@dmsdc-ai/aigentry-telepty 0.1.88 → 0.1.90
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/cli.js +202 -39
- package/daemon.js +238 -41
- package/package.json +1 -1
- package/protocol/mailbox.md +244 -0
- package/session-state.js +496 -0
- package/src/mailbox/config.js +36 -0
- package/src/mailbox/delivery.js +132 -0
- package/src/mailbox/index.js +384 -0
- package/src/mailbox/notifier.js +103 -0
- package/src/mailbox/storage.js +185 -0
package/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const { formatHostLabel, groupSessionsByHost, pickSessionTarget } = require('./s
|
|
|
18
18
|
const { buildSharedContextPrompt, createSharedContextDescriptor, ensureSharedContextFile } = require('./shared-context');
|
|
19
19
|
const { runInteractiveSkillInstaller } = require('./skill-installer');
|
|
20
20
|
const crossMachine = require('./cross-machine');
|
|
21
|
+
const { FileMailbox } = require('./src/mailbox/index');
|
|
21
22
|
const args = process.argv.slice(2);
|
|
22
23
|
let pendingTerminalInputError = null;
|
|
23
24
|
let simulatedPromptErrorInjected = false;
|
|
@@ -356,6 +357,61 @@ function startDetachedDaemon() {
|
|
|
356
357
|
cp.unref();
|
|
357
358
|
}
|
|
358
359
|
|
|
360
|
+
async function waitForDaemonHealth(maxMs = 5000) {
|
|
361
|
+
const deadline = Date.now() + maxMs;
|
|
362
|
+
while (Date.now() < deadline) {
|
|
363
|
+
try {
|
|
364
|
+
const meta = await getDaemonMeta('127.0.0.1');
|
|
365
|
+
if (meta && meta.version) return meta;
|
|
366
|
+
} catch {}
|
|
367
|
+
await new Promise(r => setTimeout(r, 300));
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function restartDaemonGraceful(options = {}) {
|
|
373
|
+
const maxAttempts = options.maxAttempts || 3;
|
|
374
|
+
const requiredCapabilities = options.requiredCapabilities || [];
|
|
375
|
+
|
|
376
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
377
|
+
// (a) Kill existing daemon processes
|
|
378
|
+
const results = cleanupDaemonProcesses();
|
|
379
|
+
|
|
380
|
+
// (b) Wait up to 3s for old processes to fully exit
|
|
381
|
+
if (results.stopped.length > 0) {
|
|
382
|
+
const { isProcessRunning } = require('./daemon-control');
|
|
383
|
+
const killDeadline = Date.now() + 3000;
|
|
384
|
+
for (const item of results.stopped) {
|
|
385
|
+
while (Date.now() < killDeadline && isProcessRunning(item.pid)) {
|
|
386
|
+
await new Promise(r => setTimeout(r, 100));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// (c) Start new daemon
|
|
392
|
+
startDetachedDaemon();
|
|
393
|
+
|
|
394
|
+
// (d) Wait for new daemon to respond with correct version
|
|
395
|
+
const meta = await waitForDaemonHealth(5000);
|
|
396
|
+
if (meta && meta.version === pkg.version) {
|
|
397
|
+
const hasCapabilities = requiredCapabilities.every(c => (meta.capabilities || []).includes(c));
|
|
398
|
+
if (hasCapabilities || requiredCapabilities.length === 0) {
|
|
399
|
+
return { success: true, meta, attempt };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Retry with backoff
|
|
404
|
+
if (attempt < maxAttempts) {
|
|
405
|
+
const backoff = 1000 * attempt;
|
|
406
|
+
process.stdout.write(`\x1b[33m⚠️ Daemon restart attempt ${attempt}/${maxAttempts} failed. Retrying in ${backoff / 1000}s...\x1b[0m\n`);
|
|
407
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
console.error(`\x1b[31m❌ Daemon restart failed after ${maxAttempts} attempts. Run "telepty daemon" manually to start.\x1b[0m`);
|
|
412
|
+
return { success: false, meta: null, attempt: maxAttempts };
|
|
413
|
+
}
|
|
414
|
+
|
|
359
415
|
function renderInteractiveHeader() {
|
|
360
416
|
const runtimeInfo = getRuntimeInfo(__dirname);
|
|
361
417
|
console.clear();
|
|
@@ -402,14 +458,13 @@ async function repairLocalDaemon(options = {}) {
|
|
|
402
458
|
return { stopped: results.stopped.length, failed: results.failed.length, meta: null };
|
|
403
459
|
}
|
|
404
460
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
return { stopped: results.stopped.length, failed: results.failed.length, meta, versionMatch };
|
|
461
|
+
const restartResult = await restartDaemonGraceful();
|
|
462
|
+
return {
|
|
463
|
+
stopped: results.stopped.length,
|
|
464
|
+
failed: results.failed.length,
|
|
465
|
+
meta: restartResult.meta,
|
|
466
|
+
versionMatch: restartResult.success
|
|
467
|
+
};
|
|
413
468
|
}
|
|
414
469
|
|
|
415
470
|
function getDiscoveryHosts() {
|
|
@@ -476,30 +531,22 @@ async function ensureDaemonRunning(options = {}) {
|
|
|
476
531
|
// Version mismatch: running daemon is older than installed CLI
|
|
477
532
|
if (meta && meta.version !== pkg.version) {
|
|
478
533
|
process.stdout.write(`\x1b[33m⚙️ Daemon version mismatch (running v${meta.version}, installed v${pkg.version}). Restarting...\x1b[0m\n`);
|
|
479
|
-
|
|
534
|
+
await restartDaemonGraceful({ requiredCapabilities });
|
|
535
|
+
return;
|
|
480
536
|
} else {
|
|
481
537
|
return;
|
|
482
538
|
}
|
|
483
539
|
} else if (sessionsRes.ok && !meta) {
|
|
484
540
|
process.stdout.write('\x1b[33m⚙️ Found an older local telepty daemon. Restarting it...\x1b[0m\n');
|
|
485
|
-
cleanupDaemonProcesses();
|
|
486
541
|
} else if (sessionsRes.ok && meta) {
|
|
487
542
|
process.stdout.write('\x1b[33m⚙️ Found a local telepty daemon without the required features. Restarting it...\x1b[0m\n');
|
|
488
|
-
cleanupDaemonProcesses();
|
|
489
543
|
}
|
|
490
544
|
} catch (e) {
|
|
491
545
|
// Continue to auto-start below.
|
|
492
546
|
}
|
|
493
547
|
|
|
494
548
|
process.stdout.write('\x1b[33m⚙️ Auto-starting local telepty daemon...\x1b[0m\n');
|
|
495
|
-
|
|
496
|
-
startDetachedDaemon();
|
|
497
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
498
|
-
|
|
499
|
-
const meta = await getDaemonMeta('127.0.0.1');
|
|
500
|
-
if (!meta || !requiredCapabilities.every((item) => meta.capabilities.includes(item))) {
|
|
501
|
-
console.error('❌ Failed to start a compatible local telepty daemon. Open telepty and choose "Repair local daemon", or rerun the installer.');
|
|
502
|
-
}
|
|
549
|
+
await restartDaemonGraceful({ requiredCapabilities });
|
|
503
550
|
}
|
|
504
551
|
|
|
505
552
|
async function manageInteractiveAttach(sessionId, targetHost) {
|
|
@@ -1000,10 +1047,41 @@ async function main() {
|
|
|
1000
1047
|
const cmdBase = path.basename(command).replace(/\..*$/, '');
|
|
1001
1048
|
const promptPattern = PROMPT_PATTERNS[cmdBase] || /[❯>$#%]\s*$/;
|
|
1002
1049
|
let promptReady = false; // wait for CLI prompt before accepting inject
|
|
1003
|
-
const injectQueue = [];
|
|
1004
1050
|
let lastUserInputTime = 0; // timestamp of last user keystroke
|
|
1005
1051
|
const IDLE_THRESHOLD = 2000; // ms after last user input to consider idle
|
|
1006
1052
|
|
|
1053
|
+
// Mailbox-backed inject queue (replaces in-memory array for crash resilience)
|
|
1054
|
+
const bridgeMailbox = new FileMailbox({
|
|
1055
|
+
root: path.join(os.homedir(), '.aigentry', 'mailbox', 'bridge'),
|
|
1056
|
+
});
|
|
1057
|
+
const bridgeTarget = sessionId;
|
|
1058
|
+
let bridgeMsgSeq = 0;
|
|
1059
|
+
let bridgePendingCount = 0;
|
|
1060
|
+
|
|
1061
|
+
// Recover undelivered messages from a previous crash
|
|
1062
|
+
try {
|
|
1063
|
+
const leftover = bridgeMailbox.peek(bridgeTarget).filter(m => m.state === 'pending' || m.state === 'in_flight');
|
|
1064
|
+
bridgePendingCount = leftover.length;
|
|
1065
|
+
if (bridgePendingCount > 0) {
|
|
1066
|
+
console.log(`\x1b[33m[BRIDGE] Recovered ${bridgePendingCount} undelivered message(s) from previous session\x1b[0m`);
|
|
1067
|
+
}
|
|
1068
|
+
} catch {}
|
|
1069
|
+
|
|
1070
|
+
function enqueueBridgeMessage(text) {
|
|
1071
|
+
const msgId = `${sessionId}:${Date.now()}:${++bridgeMsgSeq}`;
|
|
1072
|
+
try {
|
|
1073
|
+
bridgeMailbox.enqueue({
|
|
1074
|
+
msg_id: msgId, from: 'daemon', to: bridgeTarget,
|
|
1075
|
+
payload: text, created_at: Math.floor(Date.now() / 1000), attempt: 0,
|
|
1076
|
+
});
|
|
1077
|
+
bridgePendingCount++;
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
// Fallback: write directly if mailbox fails
|
|
1080
|
+
console.error(`[BRIDGE] Mailbox enqueue failed, writing directly: ${err.message}`);
|
|
1081
|
+
child.write(text);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1007
1085
|
function isIdle() {
|
|
1008
1086
|
return promptReady && (Date.now() - lastUserInputTime > IDLE_THRESHOLD);
|
|
1009
1087
|
}
|
|
@@ -1011,34 +1089,47 @@ async function main() {
|
|
|
1011
1089
|
let queueFlushTimer = null;
|
|
1012
1090
|
let idleCheckTimer = null;
|
|
1013
1091
|
|
|
1014
|
-
function
|
|
1092
|
+
function flushBridgeMailbox() {
|
|
1015
1093
|
if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
|
|
1016
1094
|
if (idleCheckTimer) { clearInterval(idleCheckTimer); idleCheckTimer = null; }
|
|
1017
|
-
if (
|
|
1018
|
-
const batch = injectQueue.splice(0);
|
|
1095
|
+
if (bridgePendingCount === 0) return;
|
|
1019
1096
|
let delay = 0;
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1097
|
+
const batch = [];
|
|
1098
|
+
// Dequeue all pending messages
|
|
1099
|
+
while (true) {
|
|
1100
|
+
const msg = bridgeMailbox.dequeue(bridgeTarget);
|
|
1101
|
+
if (!msg) break;
|
|
1102
|
+
batch.push(msg);
|
|
1103
|
+
}
|
|
1104
|
+
if (batch.length === 0) { bridgePendingCount = 0; return; }
|
|
1105
|
+
for (const msg of batch) {
|
|
1106
|
+
const text = msg.payload;
|
|
1107
|
+
const msgId = msg.msg_id;
|
|
1108
|
+
setTimeout(() => {
|
|
1109
|
+
child.write(text);
|
|
1110
|
+
try { bridgeMailbox.ack(bridgeTarget, msgId); } catch {}
|
|
1111
|
+
}, delay);
|
|
1112
|
+
delay += text === '\r' ? 0 : 100;
|
|
1113
|
+
}
|
|
1114
|
+
bridgePendingCount = Math.max(0, bridgePendingCount - batch.length);
|
|
1024
1115
|
promptReady = false;
|
|
1025
1116
|
}
|
|
1026
1117
|
function scheduleIdleFlush() {
|
|
1027
1118
|
if (idleCheckTimer) return;
|
|
1028
1119
|
// Poll every 500ms for idle state
|
|
1029
1120
|
idleCheckTimer = setInterval(() => {
|
|
1030
|
-
if (isIdle() &&
|
|
1031
|
-
|
|
1121
|
+
if (isIdle() && bridgePendingCount > 0) {
|
|
1122
|
+
flushBridgeMailbox();
|
|
1032
1123
|
}
|
|
1033
1124
|
}, 500);
|
|
1034
|
-
// Safety: flush after
|
|
1125
|
+
// Safety: flush after 5s regardless (prevent stuck queue when prompt not detected)
|
|
1035
1126
|
if (!queueFlushTimer) {
|
|
1036
1127
|
queueFlushTimer = setTimeout(() => {
|
|
1037
1128
|
queueFlushTimer = null;
|
|
1038
|
-
if (
|
|
1039
|
-
|
|
1129
|
+
if (bridgePendingCount > 0) {
|
|
1130
|
+
flushBridgeMailbox();
|
|
1040
1131
|
}
|
|
1041
|
-
},
|
|
1132
|
+
}, 5000);
|
|
1042
1133
|
}
|
|
1043
1134
|
}
|
|
1044
1135
|
|
|
@@ -1104,11 +1195,11 @@ async function main() {
|
|
|
1104
1195
|
}
|
|
1105
1196
|
|
|
1106
1197
|
const isCr = chunk === '\r';
|
|
1107
|
-
if (isCr &&
|
|
1198
|
+
if (isCr && bridgePendingCount > 0) {
|
|
1108
1199
|
// CR with pending queued text — queue CR too and flush immediately.
|
|
1109
|
-
|
|
1200
|
+
enqueueBridgeMessage(chunk);
|
|
1110
1201
|
if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
|
|
1111
|
-
|
|
1202
|
+
flushBridgeMailbox();
|
|
1112
1203
|
} else if (isCr) {
|
|
1113
1204
|
// CR always written immediately — never idle-gated.
|
|
1114
1205
|
child.write(chunk);
|
|
@@ -1119,7 +1210,7 @@ async function main() {
|
|
|
1119
1210
|
lastInjectTextTime = Date.now();
|
|
1120
1211
|
} else {
|
|
1121
1212
|
// Text when not idle — queue for safe delivery.
|
|
1122
|
-
|
|
1213
|
+
enqueueBridgeMessage(chunk);
|
|
1123
1214
|
scheduleIdleFlush();
|
|
1124
1215
|
}
|
|
1125
1216
|
}
|
|
@@ -1184,6 +1275,8 @@ async function main() {
|
|
|
1184
1275
|
|
|
1185
1276
|
allowSessionClosed = true;
|
|
1186
1277
|
cleanupTerminal();
|
|
1278
|
+
// Purge bridge mailbox on clean exit (undelivered messages are stale)
|
|
1279
|
+
try { bridgeMailbox.purge(bridgeTarget); } catch {}
|
|
1187
1280
|
process.stdout.write(`\x1b]0;\x07`);
|
|
1188
1281
|
fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
|
|
1189
1282
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
@@ -1220,7 +1313,7 @@ async function main() {
|
|
|
1220
1313
|
// Detect prompt in output to enable inject delivery
|
|
1221
1314
|
if (promptPattern.test(data)) {
|
|
1222
1315
|
promptReady = true;
|
|
1223
|
-
|
|
1316
|
+
flushBridgeMailbox();
|
|
1224
1317
|
// Notify daemon that CLI is ready for inject
|
|
1225
1318
|
if (!readyNotified && wsReady && daemonWs.readyState === 1) {
|
|
1226
1319
|
readyNotified = true;
|
|
@@ -1257,7 +1350,7 @@ async function main() {
|
|
|
1257
1350
|
}
|
|
1258
1351
|
if (promptPattern.test(data)) {
|
|
1259
1352
|
promptReady = true;
|
|
1260
|
-
|
|
1353
|
+
flushBridgeMailbox();
|
|
1261
1354
|
if (wsReady && daemonWs.readyState === 1) {
|
|
1262
1355
|
daemonWs.send(JSON.stringify({ type: 'ready' }));
|
|
1263
1356
|
}
|
|
@@ -1656,6 +1749,76 @@ async function main() {
|
|
|
1656
1749
|
return;
|
|
1657
1750
|
}
|
|
1658
1751
|
|
|
1752
|
+
if (cmd === 'status') {
|
|
1753
|
+
const statusSessionId = args[1] || process.env.TELEPTY_SESSION_ID;
|
|
1754
|
+
if (!statusSessionId) {
|
|
1755
|
+
console.error('❌ Usage: telepty status <session_id>');
|
|
1756
|
+
process.exit(1);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
try {
|
|
1760
|
+
const target = await resolveSessionTarget(statusSessionId);
|
|
1761
|
+
if (!target) {
|
|
1762
|
+
console.error(`❌ Session '${statusSessionId}' was not found on any discovered host.`);
|
|
1763
|
+
process.exit(1);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/state`);
|
|
1767
|
+
const data = await res.json();
|
|
1768
|
+
if (!res.ok) {
|
|
1769
|
+
console.error(`❌ ${formatApiError(data)}`);
|
|
1770
|
+
process.exit(1);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const auto = data.auto || {};
|
|
1774
|
+
const selfReport = data.self_report || {};
|
|
1775
|
+
|
|
1776
|
+
// Color-coded state display
|
|
1777
|
+
const stateColors = {
|
|
1778
|
+
running: '\x1b[32m', // green
|
|
1779
|
+
idle: '\x1b[33m', // yellow
|
|
1780
|
+
thinking: '\x1b[36m', // cyan
|
|
1781
|
+
stuck: '\x1b[31m', // red
|
|
1782
|
+
waiting_input: '\x1b[35m', // magenta
|
|
1783
|
+
};
|
|
1784
|
+
const stateColor = stateColors[auto.state] || '\x1b[37m';
|
|
1785
|
+
const reset = '\x1b[0m';
|
|
1786
|
+
|
|
1787
|
+
console.log(`\n Session: \x1b[36m${data.session_id}${reset}`);
|
|
1788
|
+
console.log(` Auto State: ${stateColor}${auto.state || 'unknown'}${reset} (confidence: ${auto.confidence != null ? (auto.confidence * 100).toFixed(0) + '%' : '?'})`);
|
|
1789
|
+
if (auto.since) {
|
|
1790
|
+
const durationMs = auto.duration_ms || 0;
|
|
1791
|
+
const durationStr = durationMs < 60000
|
|
1792
|
+
? `${(durationMs / 1000).toFixed(0)}s`
|
|
1793
|
+
: `${(durationMs / 60000).toFixed(1)}m`;
|
|
1794
|
+
console.log(` Since: ${auto.since} (${durationStr} ago)`);
|
|
1795
|
+
}
|
|
1796
|
+
if (auto.detail) {
|
|
1797
|
+
console.log(` Trigger: ${auto.detail.trigger || '-'}`);
|
|
1798
|
+
if (auto.detail.matched_line) console.log(` Matched: "${auto.detail.matched_line}"`);
|
|
1799
|
+
if (auto.detail.silence_ms) console.log(` Silence: ${(auto.detail.silence_ms / 1000).toFixed(1)}s`);
|
|
1800
|
+
if (auto.detail.repeat_count) console.log(` Error repeats: ${auto.detail.repeat_count}`);
|
|
1801
|
+
}
|
|
1802
|
+
if (auto.last_output_preview) {
|
|
1803
|
+
const preview = auto.last_output_preview.replace(/\n/g, '\\n').slice(-80);
|
|
1804
|
+
console.log(` Last output: "${preview}"`);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
if (selfReport.phase) {
|
|
1808
|
+
console.log(`\n Self-report:`);
|
|
1809
|
+
console.log(` Phase: ${selfReport.phase}`);
|
|
1810
|
+
if (selfReport.current_task) console.log(` Task: ${selfReport.current_task}`);
|
|
1811
|
+
if (selfReport.blocker) console.log(` Blocker: \x1b[31m${selfReport.blocker}${reset}`);
|
|
1812
|
+
if (selfReport.needs_input) console.log(` Needs input: \x1b[35myes${reset}`);
|
|
1813
|
+
}
|
|
1814
|
+
console.log('');
|
|
1815
|
+
} catch (e) {
|
|
1816
|
+
console.error(`❌ ${e.message || 'Failed to get session state.'}`);
|
|
1817
|
+
process.exit(1);
|
|
1818
|
+
}
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1659
1822
|
if (cmd === 'status-report') {
|
|
1660
1823
|
const reportArgs = args.slice(1);
|
|
1661
1824
|
let sessionId = process.env.TELEPTY_SESSION_ID || undefined;
|