@dmsdc-ai/aigentry-telepty 0.1.87 → 0.1.89
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 +165 -26
- 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/daemon.js
CHANGED
|
@@ -9,6 +9,10 @@ const pkg = require('./package.json');
|
|
|
9
9
|
const { claimDaemonState, clearDaemonState } = require('./daemon-control');
|
|
10
10
|
const { checkEntitlement } = require('./entitlement');
|
|
11
11
|
const terminalBackend = require('./terminal-backend');
|
|
12
|
+
const { FileMailbox } = require('./src/mailbox/index');
|
|
13
|
+
const { DeliveryEngine } = require('./src/mailbox/delivery');
|
|
14
|
+
const { UnixSocketNotifier } = require('./src/mailbox/notifier');
|
|
15
|
+
const { SessionStateManager } = require('./session-state');
|
|
12
16
|
|
|
13
17
|
const config = getConfig();
|
|
14
18
|
const EXPECTED_TOKEN = config.authToken;
|
|
@@ -21,6 +25,23 @@ const SESSION_CLEANUP_SECONDS = Math.max(SESSION_STALE_SECONDS, Number(process.e
|
|
|
21
25
|
const DELIVERY_TIMEOUT_MS = Math.max(100, Number(process.env.TELEPTY_DELIVERY_TIMEOUT_MS || 5000));
|
|
22
26
|
const HEALTH_POLL_MS = Math.max(100, Number(process.env.TELEPTY_HEALTH_POLL_MS || 10000));
|
|
23
27
|
|
|
28
|
+
// Session state machine manager — auto-detects session state from PTY output
|
|
29
|
+
const sessionStateManager = new SessionStateManager({
|
|
30
|
+
idle_timeout_ms: Number(process.env.TELEPTY_STATE_IDLE_TIMEOUT_MS || 5000),
|
|
31
|
+
stuck_repeat_count: Number(process.env.TELEPTY_STATE_STUCK_REPEAT_COUNT || 3),
|
|
32
|
+
stuck_window_ms: Number(process.env.TELEPTY_STATE_STUCK_WINDOW_MS || 180000),
|
|
33
|
+
thinking_timeout_ms:Number(process.env.TELEPTY_STATE_THINKING_TIMEOUT_MS || 300000),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Broadcast state transitions to the bus
|
|
37
|
+
sessionStateManager.onTransition((sessionId, from, to, detail) => {
|
|
38
|
+
const session = sessions[sessionId];
|
|
39
|
+
if (!session) return;
|
|
40
|
+
broadcastSessionEvent('session_auto_state', sessionId, session, {
|
|
41
|
+
extra: { auto_state: to, auto_state_from: from, auto_detail: detail }
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
24
45
|
function persistSessions() {
|
|
25
46
|
try {
|
|
26
47
|
const data = {};
|
|
@@ -227,12 +248,9 @@ function getSessionHealthStatus(session, options = {}) {
|
|
|
227
248
|
const isSocketPath = endpoint.startsWith('/');
|
|
228
249
|
if (isSocketPath) {
|
|
229
250
|
try {
|
|
230
|
-
fs.
|
|
231
|
-
return 'CONNECTED';
|
|
251
|
+
const stat = fs.statSync(endpoint);
|
|
252
|
+
return stat.isSocket() ? 'CONNECTED' : 'DISCONNECTED';
|
|
232
253
|
} catch {
|
|
233
|
-
session.deliveryEndpoint = null;
|
|
234
|
-
if (session.delivery) session.delivery.address = null;
|
|
235
|
-
session.lastDisconnectedAt = session.lastDisconnectedAt || new Date().toISOString();
|
|
236
254
|
return 'DISCONNECTED';
|
|
237
255
|
}
|
|
238
256
|
}
|
|
@@ -461,6 +479,7 @@ async function writeDataToSession(id, session, data) {
|
|
|
461
479
|
try {
|
|
462
480
|
const resp = JSON.parse(responseBuf.trim());
|
|
463
481
|
if (resp.status === 'Error' || resp.success === false) {
|
|
482
|
+
console.log(`[UDS] Delivery rejected by ${id}: ${resp.error || resp.message || 'unknown'}`);
|
|
464
483
|
resolve(buildErrorBody('DELIVERY_REJECTED', resp.error || resp.message || 'Target rejected the payload.', {
|
|
465
484
|
httpStatus: 502,
|
|
466
485
|
detail: resp
|
|
@@ -470,11 +489,15 @@ async function writeDataToSession(id, session, data) {
|
|
|
470
489
|
} catch {
|
|
471
490
|
// Non-JSON response — treat as success (legacy endpoints)
|
|
472
491
|
}
|
|
492
|
+
} else {
|
|
493
|
+
console.log(`[UDS] Empty response from ${id} — delivery unconfirmed (aterm may not have processed)`);
|
|
473
494
|
}
|
|
474
495
|
resolve({ success: true });
|
|
475
496
|
});
|
|
476
497
|
sock.on('error', (err) => {
|
|
477
498
|
clearTimeout(timeout);
|
|
499
|
+
console.log(`[UDS] Connection error for ${id} at ${session.delivery.address}: ${err.message}`);
|
|
500
|
+
markSessionDisconnected(session);
|
|
478
501
|
resolve(buildErrorBody('DISCONNECTED', 'UDS endpoint is unreachable.', {
|
|
479
502
|
httpStatus: 503,
|
|
480
503
|
detail: err.message
|
|
@@ -538,34 +561,67 @@ async function deliverInjectionToSession(id, session, prompt, options = {}) {
|
|
|
538
561
|
return { success: false, ...injectFailure };
|
|
539
562
|
}
|
|
540
563
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
564
|
+
// Build the payload: text + CR for non-aterm sessions (aterm handles Enter internally)
|
|
565
|
+
const payload = (!options.noEnter && session.type !== 'aterm')
|
|
566
|
+
? prompt + '\r'
|
|
567
|
+
: prompt;
|
|
545
568
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
setTimeout(async () => {
|
|
549
|
-
const submitResult = await writeDataToSession(id, session, '\r');
|
|
550
|
-
if (!submitResult.success) {
|
|
551
|
-
emitInjectFailureEvent(id, submitResult.code, submitResult.error, {
|
|
552
|
-
phase: 'submit',
|
|
553
|
-
source: options.source || 'inject'
|
|
554
|
-
}, session);
|
|
555
|
-
}
|
|
556
|
-
}, submitDelay);
|
|
557
|
-
}
|
|
569
|
+
const from = options.from || 'daemon';
|
|
570
|
+
const msgId = `${from}:${Date.now()}:${crypto.randomUUID().slice(0, 8)}`;
|
|
558
571
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
572
|
+
try {
|
|
573
|
+
const ack = mailbox.enqueue({
|
|
574
|
+
msg_id: msgId,
|
|
575
|
+
from,
|
|
576
|
+
to: id,
|
|
577
|
+
payload,
|
|
578
|
+
created_at: Math.floor(now / 1000),
|
|
579
|
+
attempt: 0,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Notify aterm sessions immediately via UDS wake
|
|
583
|
+
if (session.type === 'aterm') {
|
|
584
|
+
mailboxNotifier.notify(id);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Trigger immediate delivery tick for low-latency
|
|
588
|
+
mailboxDelivery.tick().catch(() => {});
|
|
589
|
+
|
|
590
|
+
session.lastActivityAt = new Date(now).toISOString();
|
|
591
|
+
return {
|
|
592
|
+
success: true,
|
|
593
|
+
msg_id: msgId,
|
|
594
|
+
queued: ack.queued,
|
|
595
|
+
pending: ack.pending,
|
|
596
|
+
strategy: 'mailbox',
|
|
597
|
+
submit: options.noEnter ? 'skipped' : 'included'
|
|
598
|
+
};
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.error(`[MAILBOX] Enqueue failed for ${id}: ${err.message}`);
|
|
601
|
+
// Fallback: direct delivery (backward compat during migration)
|
|
602
|
+
const textResult = await writeDataToSession(id, session, prompt);
|
|
603
|
+
if (!textResult.success) return textResult;
|
|
604
|
+
|
|
605
|
+
if (!options.noEnter && session.type !== 'aterm') {
|
|
606
|
+
const submitDelay = session.type === 'wrapped' ? 500 : 300;
|
|
607
|
+
setTimeout(async () => {
|
|
608
|
+
const submitResult = await writeDataToSession(id, session, '\r');
|
|
609
|
+
if (!submitResult.success) {
|
|
610
|
+
emitInjectFailureEvent(id, submitResult.code, submitResult.error, {
|
|
611
|
+
phase: 'submit',
|
|
612
|
+
source: options.source || 'inject'
|
|
613
|
+
}, session);
|
|
614
|
+
}
|
|
615
|
+
}, submitDelay);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
session.lastActivityAt = new Date(now).toISOString();
|
|
619
|
+
return {
|
|
620
|
+
success: true,
|
|
621
|
+
strategy: 'direct_fallback',
|
|
622
|
+
submit: options.noEnter ? 'skipped' : 'deferred'
|
|
623
|
+
};
|
|
624
|
+
}
|
|
569
625
|
}
|
|
570
626
|
|
|
571
627
|
function appendToOutputRing(session, data) {
|
|
@@ -606,6 +662,7 @@ function serializeSession(id, session, options = {}) {
|
|
|
606
662
|
const disconnectedMs = getSessionDisconnectedMs(session, nowMs);
|
|
607
663
|
const transport = buildSessionTransportBlock(session, { nowMs });
|
|
608
664
|
const semantic = buildSessionSemanticBlock(session);
|
|
665
|
+
const autoState = sessionStateManager.getState(id);
|
|
609
666
|
|
|
610
667
|
return {
|
|
611
668
|
id,
|
|
@@ -633,7 +690,15 @@ function serializeSession(id, session, options = {}) {
|
|
|
633
690
|
disconnectedSeconds: disconnectedMs === null ? null : Math.floor(disconnectedMs / 1000),
|
|
634
691
|
lastStateReportAt: session.lastStateReportAt || null,
|
|
635
692
|
transport,
|
|
636
|
-
semantic
|
|
693
|
+
semantic,
|
|
694
|
+
autoState: autoState ? { state: autoState.state, since: autoState.since, confidence: autoState.confidence } : null,
|
|
695
|
+
mailbox: (() => {
|
|
696
|
+
try {
|
|
697
|
+
const pending = mailbox.peek(id).filter(m => m.state === 'pending' || m.state === 'in_flight');
|
|
698
|
+
const deadLetter = mailbox.peekDeadLetter(id);
|
|
699
|
+
return { pending: pending.length, dead_letter: deadLetter.length };
|
|
700
|
+
} catch { return { pending: 0, dead_letter: 0 }; }
|
|
701
|
+
})()
|
|
637
702
|
};
|
|
638
703
|
}
|
|
639
704
|
|
|
@@ -797,6 +862,7 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
797
862
|
}
|
|
798
863
|
|
|
799
864
|
appendToOutputRing(currentSession, data);
|
|
865
|
+
sessionStateManager.feed(sessionRecord.id, data);
|
|
800
866
|
|
|
801
867
|
// Send to direct WS clients
|
|
802
868
|
currentSession.clients.forEach(ws => {
|
|
@@ -804,6 +870,9 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
804
870
|
});
|
|
805
871
|
});
|
|
806
872
|
|
|
873
|
+
// Register session with state machine
|
|
874
|
+
sessionStateManager.register(session_id);
|
|
875
|
+
|
|
807
876
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
808
877
|
const currentId = sessionRecord.id;
|
|
809
878
|
console.log(`[EXIT] Session ${currentId} exited with code ${exitCode}`);
|
|
@@ -811,6 +880,7 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
811
880
|
sessionRecord.clients.forEach(ws => ws.close(1000, 'Session exited'));
|
|
812
881
|
if (sessions[currentId] === sessionRecord) {
|
|
813
882
|
delete sessions[currentId];
|
|
883
|
+
sessionStateManager.unregister(currentId);
|
|
814
884
|
}
|
|
815
885
|
});
|
|
816
886
|
|
|
@@ -908,6 +978,7 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
908
978
|
}
|
|
909
979
|
|
|
910
980
|
sessions[session_id] = sessionRecord;
|
|
981
|
+
sessionStateManager.register(session_id);
|
|
911
982
|
|
|
912
983
|
const busMsg = JSON.stringify({
|
|
913
984
|
type: 'session_register',
|
|
@@ -949,6 +1020,26 @@ app.get('/api/sessions/:id', (req, res) => {
|
|
|
949
1020
|
});
|
|
950
1021
|
});
|
|
951
1022
|
|
|
1023
|
+
// Auto-detected session state (from PTY output pattern analysis)
|
|
1024
|
+
app.get('/api/sessions/:id/state', (req, res) => {
|
|
1025
|
+
const requestedId = req.params.id;
|
|
1026
|
+
const resolvedId = resolveSessionAlias(requestedId);
|
|
1027
|
+
if (!resolvedId) return respondWithError(res, 404, 'SESSION_NOT_FOUND', 'Session not found', { requested: requestedId });
|
|
1028
|
+
if (!sessions[resolvedId]) return respondWithError(res, 404, 'SESSION_NOT_FOUND', 'Session not found', { requested: requestedId });
|
|
1029
|
+
|
|
1030
|
+
const autoState = sessionStateManager.getState(resolvedId);
|
|
1031
|
+
const session = sessions[resolvedId];
|
|
1032
|
+
const semantic = buildSessionSemanticBlock(session);
|
|
1033
|
+
|
|
1034
|
+
res.json({
|
|
1035
|
+
session_id: resolvedId,
|
|
1036
|
+
auto: autoState || { state: 'unknown', detail: 'no state machine registered' },
|
|
1037
|
+
self_report: semantic,
|
|
1038
|
+
last_state_report_at: session.lastStateReportAt || null,
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// Self-reported session state (explicit POST from session)
|
|
952
1043
|
app.post('/api/sessions/:id/state', (req, res) => {
|
|
953
1044
|
const requestedId = req.params.id;
|
|
954
1045
|
const resolvedId = resolveSessionAlias(requestedId);
|
|
@@ -977,10 +1068,59 @@ app.get('/api/meta', (req, res) => {
|
|
|
977
1068
|
port: Number(PORT),
|
|
978
1069
|
machine_id: MACHINE_ID,
|
|
979
1070
|
terminal: DETECTED_TERMINAL,
|
|
980
|
-
capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads', 'cross-machine']
|
|
1071
|
+
capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads', 'cross-machine', 'mailbox']
|
|
981
1072
|
});
|
|
982
1073
|
});
|
|
983
1074
|
|
|
1075
|
+
// --- Mailbox API endpoints ---
|
|
1076
|
+
|
|
1077
|
+
app.get('/api/sessions/:id/mailbox', (req, res) => {
|
|
1078
|
+
const id = resolveSessionAlias(req.params.id);
|
|
1079
|
+
if (!id || !sessions[id]) return res.status(404).json({ error: 'Session not found' });
|
|
1080
|
+
try {
|
|
1081
|
+
const pending = mailbox.peek(id);
|
|
1082
|
+
const deadLetter = mailbox.peekDeadLetter(id);
|
|
1083
|
+
res.json({ session_id: id, pending, dead_letter: deadLetter });
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
res.status(500).json({ error: err.message });
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
app.post('/api/sessions/:id/mailbox/ack', (req, res) => {
|
|
1090
|
+
const id = resolveSessionAlias(req.params.id);
|
|
1091
|
+
if (!id || !sessions[id]) return res.status(404).json({ error: 'Session not found' });
|
|
1092
|
+
const { msg_id } = req.body;
|
|
1093
|
+
if (!msg_id) return res.status(400).json({ error: 'msg_id is required' });
|
|
1094
|
+
try {
|
|
1095
|
+
mailbox.ack(id, msg_id);
|
|
1096
|
+
res.json({ success: true, msg_id });
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
res.status(500).json({ error: err.message });
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
app.delete('/api/sessions/:id/mailbox', (req, res) => {
|
|
1103
|
+
const id = resolveSessionAlias(req.params.id);
|
|
1104
|
+
if (!id || !sessions[id]) return res.status(404).json({ error: 'Session not found' });
|
|
1105
|
+
try {
|
|
1106
|
+
mailbox.purge(id);
|
|
1107
|
+
res.json({ success: true, session_id: id });
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
res.status(500).json({ error: err.message });
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
app.delete('/api/sessions/:id/mailbox/dead-letter', (req, res) => {
|
|
1114
|
+
const id = resolveSessionAlias(req.params.id);
|
|
1115
|
+
if (!id || !sessions[id]) return res.status(404).json({ error: 'Session not found' });
|
|
1116
|
+
try {
|
|
1117
|
+
mailbox.purgeDeadLetter(id);
|
|
1118
|
+
res.json({ success: true, session_id: id });
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
res.status(500).json({ error: err.message });
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
984
1124
|
// Peer management endpoint (for cross-machine module)
|
|
985
1125
|
app.get('/api/peers', (req, res) => {
|
|
986
1126
|
try {
|
|
@@ -1336,7 +1476,8 @@ app.post('/api/sessions/:id/inject', async (req, res) => {
|
|
|
1336
1476
|
try {
|
|
1337
1477
|
const delivery = await deliverInjectionToSession(id, session, finalPrompt, {
|
|
1338
1478
|
noEnter: !!no_enter,
|
|
1339
|
-
source: 'inject'
|
|
1479
|
+
source: 'inject',
|
|
1480
|
+
from: from || 'inject'
|
|
1340
1481
|
});
|
|
1341
1482
|
if (!delivery.success) {
|
|
1342
1483
|
emitInjectFailureEvent(id, delivery.code, delivery.error, {
|
|
@@ -1475,9 +1616,11 @@ app.patch('/api/sessions/:id', (req, res) => {
|
|
|
1475
1616
|
if (!new_id) return res.status(400).json({ error: 'new_id is required' });
|
|
1476
1617
|
if (sessions[new_id]) return res.status(409).json({ error: `Session ID '${new_id}' is already in use.` });
|
|
1477
1618
|
|
|
1478
|
-
// Move session to new key
|
|
1619
|
+
// Move session to new key (including state machine)
|
|
1479
1620
|
sessions[new_id] = session;
|
|
1480
1621
|
delete sessions[id];
|
|
1622
|
+
sessionStateManager.unregister(id);
|
|
1623
|
+
sessionStateManager.register(new_id);
|
|
1481
1624
|
session.id = new_id;
|
|
1482
1625
|
|
|
1483
1626
|
// Broadcast rename to bus
|
|
@@ -1511,12 +1654,16 @@ app.delete('/api/sessions/:id', (req, res) => {
|
|
|
1511
1654
|
session.ptyProcess.kill();
|
|
1512
1655
|
}
|
|
1513
1656
|
delete sessions[id];
|
|
1657
|
+
sessionStateManager.unregister(id);
|
|
1658
|
+
try { mailbox.purge(id); } catch {}
|
|
1514
1659
|
console.log(`[KILL] Session ${id} removed`);
|
|
1515
1660
|
persistSessions();
|
|
1516
1661
|
res.json({ success: true, status: 'closing' });
|
|
1517
1662
|
} catch (err) {
|
|
1518
1663
|
// Even if kill fails, remove from registry
|
|
1519
1664
|
delete sessions[id];
|
|
1665
|
+
sessionStateManager.unregister(id);
|
|
1666
|
+
try { mailbox.purge(id); } catch {}
|
|
1520
1667
|
persistSessions();
|
|
1521
1668
|
console.log(`[KILL] Session ${id} force-removed (process cleanup error: ${err.message})`);
|
|
1522
1669
|
res.json({ success: true, status: 'force-removed' });
|
|
@@ -1851,6 +1998,46 @@ const server = app.listen(PORT, HOST, () => {
|
|
|
1851
1998
|
console.log(`🚀 aigentry-telepty daemon listening on http://${HOST}:${PORT}`);
|
|
1852
1999
|
});
|
|
1853
2000
|
|
|
2001
|
+
// --- Mailbox system initialization ---
|
|
2002
|
+
const mailbox = new FileMailbox();
|
|
2003
|
+
const mailboxNotifier = new UnixSocketNotifier({ coalesceMs: 25 });
|
|
2004
|
+
|
|
2005
|
+
// Resolve aterm UDS socket path for a session
|
|
2006
|
+
mailboxNotifier.setSocketResolver((sessionId) => {
|
|
2007
|
+
const session = sessions[sessionId];
|
|
2008
|
+
if (!session || session.type !== 'aterm') return null;
|
|
2009
|
+
return (session.delivery && session.delivery.transport === 'unix_socket' && session.delivery.address) || null;
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
// Delivery engine: dequeue → writeDataToSession → ack/nack
|
|
2013
|
+
const mailboxDelivery = new DeliveryEngine(mailbox, {
|
|
2014
|
+
pollMs: 200,
|
|
2015
|
+
sessionResolver: () => Object.keys(sessions),
|
|
2016
|
+
deliverFn: async (sessionId, msg) => {
|
|
2017
|
+
const session = sessions[sessionId];
|
|
2018
|
+
if (!session) return { success: false, error: 'Session not found' };
|
|
2019
|
+
const result = await writeDataToSession(sessionId, session, msg.payload);
|
|
2020
|
+
if (result.success) {
|
|
2021
|
+
session.lastActivityAt = new Date().toISOString();
|
|
2022
|
+
}
|
|
2023
|
+
return result;
|
|
2024
|
+
},
|
|
2025
|
+
onDelivery: (sessionId, msgId, result) => {
|
|
2026
|
+
const session = sessions[sessionId];
|
|
2027
|
+
if (!session) return;
|
|
2028
|
+
if (result.success) {
|
|
2029
|
+
broadcastSessionEvent('mailbox_delivered', sessionId, session, {
|
|
2030
|
+
extra: { msg_id: msgId }
|
|
2031
|
+
});
|
|
2032
|
+
} else {
|
|
2033
|
+
broadcastSessionEvent('mailbox_delivery_failed', sessionId, session, {
|
|
2034
|
+
extra: { msg_id: msgId, error: result.error }
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
},
|
|
2038
|
+
});
|
|
2039
|
+
mailboxDelivery.start();
|
|
2040
|
+
|
|
1854
2041
|
const IDLE_THRESHOLD_SECONDS = 60;
|
|
1855
2042
|
setInterval(() => {
|
|
1856
2043
|
const now = Date.now();
|
|
@@ -1912,18 +2099,24 @@ setInterval(() => {
|
|
|
1912
2099
|
}
|
|
1913
2100
|
|
|
1914
2101
|
// Periodically verify aterm socket existence — triggers health transition
|
|
2102
|
+
// NOTE: Do NOT nullify delivery address here. The address is preserved so that
|
|
2103
|
+
// if aterm restarts and the socket reappears, health check recovers automatically.
|
|
1915
2104
|
if (session.type === 'aterm') {
|
|
1916
2105
|
const atermEndpoint = session.deliveryEndpoint || (session.delivery && session.delivery.address);
|
|
1917
2106
|
if (atermEndpoint && atermEndpoint.startsWith('/')) {
|
|
2107
|
+
let socketAlive = false;
|
|
1918
2108
|
try {
|
|
1919
|
-
fs.
|
|
2109
|
+
const stat = fs.statSync(atermEndpoint);
|
|
2110
|
+
socketAlive = stat.isSocket();
|
|
1920
2111
|
} catch {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
}
|
|
2112
|
+
socketAlive = false;
|
|
2113
|
+
}
|
|
2114
|
+
if (!socketAlive && !session.lastDisconnectedAt) {
|
|
2115
|
+
markSessionDisconnected(session);
|
|
1926
2116
|
console.log(`[SWEEP] aterm socket gone for ${id}: ${atermEndpoint}`);
|
|
2117
|
+
} else if (socketAlive && session.lastDisconnectedAt) {
|
|
2118
|
+
markSessionConnected(session);
|
|
2119
|
+
console.log(`[SWEEP] aterm socket recovered for ${id}: ${atermEndpoint}`);
|
|
1927
2120
|
}
|
|
1928
2121
|
}
|
|
1929
2122
|
}
|
|
@@ -1947,6 +2140,7 @@ setInterval(() => {
|
|
|
1947
2140
|
disconnectedSeconds
|
|
1948
2141
|
});
|
|
1949
2142
|
delete sessions[id];
|
|
2143
|
+
sessionStateManager.unregister(id);
|
|
1950
2144
|
console.log(`[CLEANUP] Removed stale session ${id} after ${disconnectedSeconds}s disconnected`);
|
|
1951
2145
|
persistSessions();
|
|
1952
2146
|
}
|
|
@@ -2066,6 +2260,7 @@ wss.on('connection', (ws, req) => {
|
|
|
2066
2260
|
if (type === 'output') {
|
|
2067
2261
|
activeSession.lastActivityAt = new Date().toISOString();
|
|
2068
2262
|
appendToOutputRing(activeSession, data);
|
|
2263
|
+
sessionStateManager.feed(sessionId, data);
|
|
2069
2264
|
activeSession.clients.forEach(client => {
|
|
2070
2265
|
if (client !== ws && client.readyState === 1) {
|
|
2071
2266
|
client.send(JSON.stringify({ type: 'output', data }));
|
|
@@ -2210,6 +2405,8 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
2210
2405
|
});
|
|
2211
2406
|
|
|
2212
2407
|
function shutdown(code) {
|
|
2408
|
+
mailboxDelivery.stop();
|
|
2409
|
+
mailboxNotifier.cancelAll();
|
|
2213
2410
|
clearDaemonState(process.pid);
|
|
2214
2411
|
process.exit(code);
|
|
2215
2412
|
}
|