@dmsdc-ai/aigentry-telepty 0.1.96 → 0.1.97
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 +16 -8
- package/daemon.js +25 -11
- package/package.json +4 -4
- package/session-state.js +154 -51
- package/skills/telepty/SKILL.md +39 -118
- package/skills/telepty-allow/SKILL.md +78 -0
- package/skills/telepty-attach/SKILL.md +52 -0
- package/skills/telepty-broadcast/SKILL.md +63 -0
- package/skills/telepty-daemon/SKILL.md +94 -0
- package/skills/telepty-inject/SKILL.md +93 -0
- package/skills/telepty-list/SKILL.md +81 -0
- package/skills/telepty-listen/SKILL.md +86 -0
- package/skills/telepty-rename/SKILL.md +63 -0
- package/skills/telepty-session/SKILL.md +83 -0
- package/src/mailbox/config.js +4 -0
- package/src/mailbox/delivery.js +93 -32
- package/src/mailbox/index.js +11 -0
- package/src/mailbox/storage.js +84 -5
package/cli.js
CHANGED
|
@@ -682,7 +682,9 @@ async function manageInteractive() {
|
|
|
682
682
|
console.log('\x1b[1mAvailable Sessions:\x1b[0m');
|
|
683
683
|
sessions.forEach(s => {
|
|
684
684
|
const hostLabel = formatHostLabel(s.host);
|
|
685
|
-
|
|
685
|
+
const stEmoji = s.autoState ? s.autoState.emoji : '';
|
|
686
|
+
const stLabel = s.autoState ? s.autoState.state : '';
|
|
687
|
+
console.log(` - \x1b[36m${s.id}\x1b[0m (\x1b[33m${hostLabel}\x1b[0m) [${s.command}] - ${s.healthStatus || 'UNKNOWN'}${stLabel ? ` ${stEmoji} ${stLabel}` : ''} - Clients: ${s.active_clients}`);
|
|
686
688
|
});
|
|
687
689
|
}
|
|
688
690
|
console.log('\n');
|
|
@@ -858,7 +860,9 @@ async function main() {
|
|
|
858
860
|
console.log(` - ID: \x1b[36m${s.id}\x1b[0m`);
|
|
859
861
|
console.log(` Host: ${formatHostLabel(s.host)}`);
|
|
860
862
|
console.log(` Command: ${s.command}`);
|
|
861
|
-
|
|
863
|
+
const autoEmoji = s.autoState ? s.autoState.emoji : '';
|
|
864
|
+
const autoLabel = s.autoState ? s.autoState.state : '';
|
|
865
|
+
console.log(` Status: ${formatSessionHealth(s)}${autoLabel ? ` ${autoEmoji} ${autoLabel}` : ''}`);
|
|
862
866
|
console.log(` Terminal: ${formatSessionTerminal(s)}`);
|
|
863
867
|
console.log(` CWD: ${s.cwd}`);
|
|
864
868
|
console.log(` Clients: ${s.active_clients}`);
|
|
@@ -1775,17 +1779,21 @@ async function main() {
|
|
|
1775
1779
|
|
|
1776
1780
|
// Color-coded state display
|
|
1777
1781
|
const stateColors = {
|
|
1778
|
-
|
|
1779
|
-
idle: '\x1b[
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1782
|
+
starting: '\x1b[33m', // yellow
|
|
1783
|
+
idle: '\x1b[32m', // green
|
|
1784
|
+
working: '\x1b[36m', // cyan
|
|
1785
|
+
thinking: '\x1b[35m', // magenta
|
|
1786
|
+
waiting: '\x1b[33m', // yellow
|
|
1787
|
+
error: '\x1b[31m', // red
|
|
1788
|
+
restarting: '\x1b[33m', // yellow
|
|
1789
|
+
dead: '\x1b[90m', // gray
|
|
1783
1790
|
};
|
|
1784
1791
|
const stateColor = stateColors[auto.state] || '\x1b[37m';
|
|
1785
1792
|
const reset = '\x1b[0m';
|
|
1786
1793
|
|
|
1787
1794
|
console.log(`\n Session: \x1b[36m${data.session_id}${reset}`);
|
|
1788
|
-
|
|
1795
|
+
const stateEmoji = auto.emoji || '';
|
|
1796
|
+
console.log(` State: ${stateColor}${stateEmoji} ${auto.state || 'unknown'}${reset} (confidence: ${auto.confidence != null ? (auto.confidence * 100).toFixed(0) + '%' : '?'})`);
|
|
1789
1797
|
if (auto.since) {
|
|
1790
1798
|
const durationMs = auto.duration_ms || 0;
|
|
1791
1799
|
const durationStr = durationMs < 60000
|
package/daemon.js
CHANGED
|
@@ -12,7 +12,7 @@ const terminalBackend = require('./terminal-backend');
|
|
|
12
12
|
const { FileMailbox } = require('./src/mailbox/index');
|
|
13
13
|
const { DeliveryEngine } = require('./src/mailbox/delivery');
|
|
14
14
|
const { UnixSocketNotifier } = require('./src/mailbox/notifier');
|
|
15
|
-
const { SessionStateManager } = require('./session-state');
|
|
15
|
+
const { SessionStateManager, STATE_DISPLAY } = require('./session-state');
|
|
16
16
|
|
|
17
17
|
const config = getConfig();
|
|
18
18
|
const EXPECTED_TOKEN = config.authToken;
|
|
@@ -27,10 +27,10 @@ const HEALTH_POLL_MS = Math.max(100, Number(process.env.TELEPTY_HEALTH_POLL_MS |
|
|
|
27
27
|
|
|
28
28
|
// Session state machine manager — auto-detects session state from PTY output
|
|
29
29
|
const sessionStateManager = new SessionStateManager({
|
|
30
|
-
idle_timeout_ms:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
thinking_timeout_ms:Number(process.env.TELEPTY_STATE_THINKING_TIMEOUT_MS || 300000),
|
|
30
|
+
idle_timeout_ms: Number(process.env.TELEPTY_STATE_IDLE_TIMEOUT_MS || 5000),
|
|
31
|
+
error_repeat_count: Number(process.env.TELEPTY_STATE_ERROR_REPEAT_COUNT || 3),
|
|
32
|
+
error_window_ms: Number(process.env.TELEPTY_STATE_ERROR_WINDOW_MS || 180000),
|
|
33
|
+
thinking_timeout_ms: Number(process.env.TELEPTY_STATE_THINKING_TIMEOUT_MS || 300000),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
// Broadcast state transitions to the bus
|
|
@@ -267,7 +267,7 @@ function getSessionHealthStatus(session, options = {}) {
|
|
|
267
267
|
|
|
268
268
|
function getSessionHealthReason(session, healthStatus) {
|
|
269
269
|
if (session.type === 'wrapped') {
|
|
270
|
-
if (healthStatus === 'CONNECTED') return
|
|
270
|
+
if (healthStatus === 'CONNECTED') return 'OWNER_CONNECTED';
|
|
271
271
|
if (healthStatus === 'STALE') return 'OWNER_DISCONNECTED_STALE';
|
|
272
272
|
return 'OWNER_DISCONNECTED';
|
|
273
273
|
}
|
|
@@ -705,7 +705,13 @@ function serializeSession(id, session, options = {}) {
|
|
|
705
705
|
lastStateReportAt: session.lastStateReportAt || null,
|
|
706
706
|
transport,
|
|
707
707
|
semantic,
|
|
708
|
-
autoState: autoState ? {
|
|
708
|
+
autoState: autoState ? {
|
|
709
|
+
state: autoState.state,
|
|
710
|
+
emoji: (STATE_DISPLAY[autoState.state] || {}).emoji || '?',
|
|
711
|
+
since: autoState.since,
|
|
712
|
+
confidence: autoState.confidence,
|
|
713
|
+
detail: autoState.detail,
|
|
714
|
+
} : null,
|
|
709
715
|
mailbox: (() => {
|
|
710
716
|
try {
|
|
711
717
|
const pending = mailbox.peek(id).filter(m => m.state === 'pending' || m.state === 'in_flight');
|
|
@@ -738,7 +744,7 @@ for (const [id, meta] of Object.entries(_persisted)) {
|
|
|
738
744
|
lastDisconnectedAt: meta.lastDisconnectedAt || meta.lastActivityAt || new Date().toISOString(),
|
|
739
745
|
lastStateReportAt: meta.lastStateReportAt || null,
|
|
740
746
|
stateReport: meta.stateReport || null,
|
|
741
|
-
clients: new Set(), isClosing: false, outputRing: [], ready:
|
|
747
|
+
clients: new Set(), isClosing: false, outputRing: [], ready: true, };
|
|
742
748
|
console.log(`[PERSIST] Restored session ${id} (awaiting reconnect)`);
|
|
743
749
|
}
|
|
744
750
|
}
|
|
@@ -883,6 +889,7 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
883
889
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
884
890
|
const currentId = sessionRecord.id;
|
|
885
891
|
console.log(`[EXIT] Session ${currentId} exited with code ${exitCode}`);
|
|
892
|
+
sessionStateManager.markDead(currentId, exitCode, signal);
|
|
886
893
|
sessionRecord.isClosing = true;
|
|
887
894
|
sessionRecord.clients.forEach(ws => ws.close(1000, 'Session exited'));
|
|
888
895
|
if (sessions[currentId] === sessionRecord) {
|
|
@@ -953,7 +960,7 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
953
960
|
clients: new Set(),
|
|
954
961
|
isClosing: false,
|
|
955
962
|
outputRing: [],
|
|
956
|
-
ready:
|
|
963
|
+
ready: true, // all sessions are injectable once registered (#150)
|
|
957
964
|
};
|
|
958
965
|
// Check for existing session with same base alias and emit replaced event
|
|
959
966
|
const baseAlias = session_id.replace(/-\d+$/, '');
|
|
@@ -1031,7 +1038,9 @@ app.get('/api/sessions/:id/state', (req, res) => {
|
|
|
1031
1038
|
|
|
1032
1039
|
res.json({
|
|
1033
1040
|
session_id: resolvedId,
|
|
1034
|
-
auto: autoState
|
|
1041
|
+
auto: autoState
|
|
1042
|
+
? { ...autoState, emoji: (STATE_DISPLAY[autoState.state] || {}).emoji || '?' }
|
|
1043
|
+
: { state: 'unknown', emoji: '?', detail: 'no state machine registered' },
|
|
1035
1044
|
self_report: semantic,
|
|
1036
1045
|
last_state_report_at: session.lastStateReportAt || null,
|
|
1037
1046
|
});
|
|
@@ -2043,6 +2052,11 @@ const mailboxDelivery = new DeliveryEngine(mailbox, {
|
|
|
2043
2052
|
}
|
|
2044
2053
|
},
|
|
2045
2054
|
});
|
|
2055
|
+
// Startup sweep: break stale lock files before starting delivery
|
|
2056
|
+
const staleBroken = mailbox.breakStaleLocks();
|
|
2057
|
+
if (staleBroken > 0) {
|
|
2058
|
+
console.log(`[MAILBOX] Startup sweep: broke ${staleBroken} stale lock(s)`);
|
|
2059
|
+
}
|
|
2046
2060
|
mailboxDelivery.start();
|
|
2047
2061
|
|
|
2048
2062
|
const IDLE_THRESHOLD_SECONDS = 60;
|
|
@@ -2214,7 +2228,7 @@ wss.on('connection', (ws, req) => {
|
|
|
2214
2228
|
clients: new Set([ws]),
|
|
2215
2229
|
isClosing: false,
|
|
2216
2230
|
outputRing: [],
|
|
2217
|
-
ready:
|
|
2231
|
+
ready: true,
|
|
2218
2232
|
};
|
|
2219
2233
|
sessions[sessionId] = autoSession;
|
|
2220
2234
|
console.log(`[WS] Auto-registered wrapped session ${sessionId} on reconnect`);
|
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.97",
|
|
4
4
|
"main": "daemon.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aigentry-telepty": "install.js",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"telepty-mcp": "mcp-server/index.mjs"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js",
|
|
13
|
-
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js",
|
|
14
|
-
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js"
|
|
12
|
+
"test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js",
|
|
13
|
+
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js",
|
|
14
|
+
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"pty",
|
package/session-state.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// session-state.js — PTY output-based session state machine for telepty.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// idle —
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// 8-state dynamic session control (#185):
|
|
4
|
+
// starting — PTY not yet spawned / initializing
|
|
5
|
+
// idle — prompt detected (OSC 133;B or pattern), ready for input
|
|
6
|
+
// working — PTY output actively flowing (AI generating text)
|
|
7
|
+
// thinking — AI spinner/progress patterns (no substantive text)
|
|
8
|
+
// waiting — interactive prompt detected (approve, y/n, confirm)
|
|
9
|
+
// error — error patterns detected in output
|
|
10
|
+
// restarting — CLI restart triggered
|
|
11
|
+
// dead — PTY process terminated
|
|
9
12
|
//
|
|
10
13
|
// Usage:
|
|
11
14
|
// const { SessionStateMachine } = require('./session-state');
|
|
12
15
|
// const sm = new SessionStateMachine(sessionId, config);
|
|
13
16
|
// sm.feed(data); // call on every PTY output chunk
|
|
17
|
+
// sm.markStarting(); // daemon: PTY spawn initiated
|
|
18
|
+
// sm.markDead(exitCode); // daemon: PTY exited
|
|
19
|
+
// sm.markRestarting(); // daemon: auto-restart triggered
|
|
14
20
|
// sm.getState(); // → { state, since, confidence, last_output_preview, detail }
|
|
15
21
|
// sm.onTransition(callback); // (from, to, detail) => {}
|
|
16
22
|
// sm.destroy(); // cleanup timers
|
|
@@ -22,11 +28,26 @@
|
|
|
22
28
|
// ---------------------------------------------------------------------------
|
|
23
29
|
|
|
24
30
|
const STATES = Object.freeze({
|
|
25
|
-
|
|
26
|
-
IDLE:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
STARTING: 'starting',
|
|
32
|
+
IDLE: 'idle',
|
|
33
|
+
WORKING: 'working',
|
|
34
|
+
THINKING: 'thinking',
|
|
35
|
+
WAITING: 'waiting',
|
|
36
|
+
ERROR: 'error',
|
|
37
|
+
RESTARTING: 'restarting',
|
|
38
|
+
DEAD: 'dead',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// State display metadata
|
|
42
|
+
const STATE_DISPLAY = Object.freeze({
|
|
43
|
+
[STATES.STARTING]: { emoji: '🔄', color: '\x1b[33m' }, // yellow
|
|
44
|
+
[STATES.IDLE]: { emoji: '💤', color: '\x1b[32m' }, // green
|
|
45
|
+
[STATES.WORKING]: { emoji: '🔨', color: '\x1b[36m' }, // cyan
|
|
46
|
+
[STATES.THINKING]: { emoji: '🧠', color: '\x1b[35m' }, // magenta
|
|
47
|
+
[STATES.WAITING]: { emoji: '⏳', color: '\x1b[33m' }, // yellow
|
|
48
|
+
[STATES.ERROR]: { emoji: '🔴', color: '\x1b[31m' }, // red
|
|
49
|
+
[STATES.RESTARTING]: { emoji: '🔄', color: '\x1b[33m' }, // yellow
|
|
50
|
+
[STATES.DEAD]: { emoji: '☠️', color: '\x1b[90m' }, // gray
|
|
30
51
|
});
|
|
31
52
|
|
|
32
53
|
// ---------------------------------------------------------------------------
|
|
@@ -35,9 +56,9 @@ const STATES = Object.freeze({
|
|
|
35
56
|
|
|
36
57
|
const DEFAULT_CONFIG = Object.freeze({
|
|
37
58
|
idle_timeout_ms: 5000, // 5s silence + prompt → idle
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
thinking_timeout_ms: 300000, // 5 min thinking before →
|
|
59
|
+
error_repeat_count: 3, // same error N times → high confidence
|
|
60
|
+
error_window_ms: 180000, // 3 min window for error dedup
|
|
61
|
+
thinking_timeout_ms: 300000, // 5 min thinking before → error
|
|
41
62
|
poll_interval_ms: 1000, // state check tick interval
|
|
42
63
|
output_preview_len: 200, // last N chars for preview
|
|
43
64
|
error_dedup_len: 120, // error line length for dedup fingerprint
|
|
@@ -47,6 +68,11 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
47
68
|
// Pattern sets (all terminal-agnostic, CLI-agnostic)
|
|
48
69
|
// ---------------------------------------------------------------------------
|
|
49
70
|
|
|
71
|
+
// OSC 133 prompt marks — high-confidence idle detection
|
|
72
|
+
// OSC 133;A = prompt start, OSC 133;B = command start (prompt ready)
|
|
73
|
+
// Both indicate the shell/CLI is at a prompt waiting for input.
|
|
74
|
+
const OSC_133_RE = /\x1b\]133;[AB](?:\x07|\x1b\\)/;
|
|
75
|
+
|
|
50
76
|
// Shell prompt patterns — last line of output looks like a prompt
|
|
51
77
|
const PROMPT_PATTERNS = [
|
|
52
78
|
/[$#%>❯›»] *$/, // common shell prompts
|
|
@@ -74,7 +100,7 @@ const THINKING_PATTERNS = [
|
|
|
74
100
|
];
|
|
75
101
|
|
|
76
102
|
// Interactive input prompts — session is waiting for user input
|
|
77
|
-
const
|
|
103
|
+
const WAITING_PATTERNS = [
|
|
78
104
|
/\[Y\/n\]/i, // [Y/n]
|
|
79
105
|
/\(y\/N\)/i, // (y/N)
|
|
80
106
|
/\[yes\/no\]/i, // [yes/no]
|
|
@@ -91,7 +117,7 @@ const WAITING_INPUT_PATTERNS = [
|
|
|
91
117
|
/\benter .*[:\s]*$/i, // Enter something:
|
|
92
118
|
];
|
|
93
119
|
|
|
94
|
-
// Error patterns for
|
|
120
|
+
// Error patterns for error detection
|
|
95
121
|
const ERROR_PATTERNS = [
|
|
96
122
|
/\berror\b[:\[]/i,
|
|
97
123
|
/\bError:/,
|
|
@@ -109,7 +135,7 @@ const ERROR_PATTERNS = [
|
|
|
109
135
|
/\bECONNREFUSED\b/,
|
|
110
136
|
];
|
|
111
137
|
|
|
112
|
-
// ANSI escape stripper
|
|
138
|
+
// ANSI escape stripper (preserves OSC 133 detection by running after OSC check)
|
|
113
139
|
const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\x1b\[[\?]?[0-9;]*[hlm]/g;
|
|
114
140
|
|
|
115
141
|
function stripAnsi(str) {
|
|
@@ -126,9 +152,9 @@ class SessionStateMachine {
|
|
|
126
152
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
127
153
|
|
|
128
154
|
// Current state
|
|
129
|
-
this._state = STATES.
|
|
155
|
+
this._state = STATES.STARTING;
|
|
130
156
|
this._since = Date.now();
|
|
131
|
-
this._confidence = 0
|
|
157
|
+
this._confidence = 1.0;
|
|
132
158
|
this._detail = null;
|
|
133
159
|
|
|
134
160
|
// Output tracking
|
|
@@ -137,10 +163,13 @@ class SessionStateMachine {
|
|
|
137
163
|
this._recentLines = []; // last N stripped lines
|
|
138
164
|
this._maxRecentLines = 50;
|
|
139
165
|
|
|
140
|
-
//
|
|
166
|
+
// OSC 133 tracking
|
|
167
|
+
this._lastOsc133At = null;
|
|
168
|
+
|
|
169
|
+
// Error detection: error fingerprints with timestamps
|
|
141
170
|
this._errorHistory = []; // [{ fingerprint, timestamp }]
|
|
142
171
|
|
|
143
|
-
// Thinking start time (for thinking →
|
|
172
|
+
// Thinking start time (for thinking → error timeout)
|
|
144
173
|
this._thinkingStartedAt = null;
|
|
145
174
|
|
|
146
175
|
// Transition listeners
|
|
@@ -162,6 +191,16 @@ class SessionStateMachine {
|
|
|
162
191
|
const previewLen = this.config.output_preview_len;
|
|
163
192
|
this._lastOutputPreview = (this._lastOutputPreview + data).slice(-previewLen);
|
|
164
193
|
|
|
194
|
+
// Check for OSC 133 prompt mark in RAW data (before ANSI stripping)
|
|
195
|
+
if (OSC_133_RE.test(data)) {
|
|
196
|
+
this._lastOsc133At = now;
|
|
197
|
+
this._transition(STATES.IDLE, 0.95, {
|
|
198
|
+
trigger: 'osc_133_prompt',
|
|
199
|
+
timestamp: new Date(now).toISOString(),
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
165
204
|
// Strip ANSI and split into lines for pattern analysis
|
|
166
205
|
const cleaned = stripAnsi(data);
|
|
167
206
|
const lines = cleaned.split(/\r?\n/).filter(l => l.trim().length > 0);
|
|
@@ -174,6 +213,11 @@ class SessionStateMachine {
|
|
|
174
213
|
this._recentLines.shift();
|
|
175
214
|
}
|
|
176
215
|
|
|
216
|
+
// Don't run detection on lifecycle states managed externally
|
|
217
|
+
if (this._state === STATES.DEAD || this._state === STATES.RESTARTING) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
177
221
|
// Run detection pipeline (order matters: most specific first)
|
|
178
222
|
this._detect(now);
|
|
179
223
|
}
|
|
@@ -207,6 +251,24 @@ class SessionStateMachine {
|
|
|
207
251
|
this._listeners = [];
|
|
208
252
|
}
|
|
209
253
|
|
|
254
|
+
// --- Lifecycle methods (called by daemon) ---
|
|
255
|
+
|
|
256
|
+
markStarting() {
|
|
257
|
+
this._transition(STATES.STARTING, 1.0, { trigger: 'lifecycle' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
markDead(exitCode, signal) {
|
|
261
|
+
this._transition(STATES.DEAD, 1.0, {
|
|
262
|
+
trigger: 'lifecycle',
|
|
263
|
+
exit_code: exitCode ?? null,
|
|
264
|
+
signal: signal ?? null,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
markRestarting() {
|
|
269
|
+
this._transition(STATES.RESTARTING, 1.0, { trigger: 'lifecycle' });
|
|
270
|
+
}
|
|
271
|
+
|
|
210
272
|
// --- Internal ---
|
|
211
273
|
|
|
212
274
|
_transition(newState, confidence, detail) {
|
|
@@ -249,20 +311,20 @@ class SessionStateMachine {
|
|
|
249
311
|
? this._recentLines[this._recentLines.length - 1].text
|
|
250
312
|
: '';
|
|
251
313
|
|
|
252
|
-
// --- Priority 1:
|
|
253
|
-
if (this._matchesAny(lastLine,
|
|
254
|
-
this._transition(STATES.
|
|
314
|
+
// --- Priority 1: waiting (most specific, must act on immediately) ---
|
|
315
|
+
if (this._matchesAny(lastLine, WAITING_PATTERNS)) {
|
|
316
|
+
this._transition(STATES.WAITING, 0.9, {
|
|
255
317
|
trigger: 'pattern',
|
|
256
318
|
matched_line: lastLine.slice(0, 100),
|
|
257
319
|
});
|
|
258
320
|
return;
|
|
259
321
|
}
|
|
260
322
|
|
|
261
|
-
// --- Priority 2:
|
|
323
|
+
// --- Priority 2: error detection (error patterns in output) ---
|
|
262
324
|
this._trackErrors(now);
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
this._transition(STATES.
|
|
325
|
+
const errorResult = this._checkError(now);
|
|
326
|
+
if (errorResult) {
|
|
327
|
+
this._transition(STATES.ERROR, errorResult.confidence, errorResult.detail);
|
|
266
328
|
return;
|
|
267
329
|
}
|
|
268
330
|
|
|
@@ -275,8 +337,8 @@ class SessionStateMachine {
|
|
|
275
337
|
return;
|
|
276
338
|
}
|
|
277
339
|
|
|
278
|
-
// --- Priority 4:
|
|
279
|
-
this._transition(STATES.
|
|
340
|
+
// --- Priority 4: working (we just received output, not matching other patterns) ---
|
|
341
|
+
this._transition(STATES.WORKING, 0.9, {
|
|
280
342
|
trigger: 'output_received',
|
|
281
343
|
});
|
|
282
344
|
}
|
|
@@ -285,11 +347,16 @@ class SessionStateMachine {
|
|
|
285
347
|
const now = Date.now();
|
|
286
348
|
const silenceMs = now - this._lastOutputAt;
|
|
287
349
|
|
|
288
|
-
//
|
|
350
|
+
// Don't override lifecycle states
|
|
351
|
+
if (this._state === STATES.DEAD || this._state === STATES.RESTARTING || this._state === STATES.STARTING) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Thinking → error after timeout
|
|
289
356
|
if (this._state === STATES.THINKING && this._thinkingStartedAt) {
|
|
290
357
|
const thinkingDuration = now - this._thinkingStartedAt;
|
|
291
358
|
if (thinkingDuration > this.config.thinking_timeout_ms) {
|
|
292
|
-
this._transition(STATES.
|
|
359
|
+
this._transition(STATES.ERROR, 0.7, {
|
|
293
360
|
trigger: 'thinking_timeout',
|
|
294
361
|
thinking_duration_ms: thinkingDuration,
|
|
295
362
|
});
|
|
@@ -297,10 +364,10 @@ class SessionStateMachine {
|
|
|
297
364
|
}
|
|
298
365
|
}
|
|
299
366
|
|
|
300
|
-
// Silence → idle (only if last output looks like a prompt)
|
|
367
|
+
// Silence → idle (only if last output looks like a prompt or OSC 133 was recent)
|
|
301
368
|
if (silenceMs > this.config.idle_timeout_ms) {
|
|
302
|
-
// Don't override
|
|
303
|
-
if (this._state === STATES.
|
|
369
|
+
// Don't override error or waiting with idle
|
|
370
|
+
if (this._state === STATES.ERROR || this._state === STATES.WAITING) {
|
|
304
371
|
return;
|
|
305
372
|
}
|
|
306
373
|
|
|
@@ -308,11 +375,12 @@ class SessionStateMachine {
|
|
|
308
375
|
? this._recentLines[this._recentLines.length - 1].text
|
|
309
376
|
: '';
|
|
310
377
|
|
|
378
|
+
const hasOsc133 = this._lastOsc133At && (now - this._lastOsc133At) < this.config.idle_timeout_ms * 2;
|
|
311
379
|
const hasPrompt = this._matchesAny(lastLine, PROMPT_PATTERNS);
|
|
312
|
-
const confidence = hasPrompt ? 0.9 : 0.6;
|
|
380
|
+
const confidence = hasOsc133 ? 0.95 : (hasPrompt ? 0.9 : 0.6);
|
|
313
381
|
|
|
314
382
|
this._transition(STATES.IDLE, confidence, {
|
|
315
|
-
trigger: hasPrompt ? 'prompt_detected' : 'silence_timeout',
|
|
383
|
+
trigger: hasOsc133 ? 'osc_133_prompt' : (hasPrompt ? 'prompt_detected' : 'silence_timeout'),
|
|
316
384
|
silence_ms: silenceMs,
|
|
317
385
|
last_line: lastLine.slice(0, 100),
|
|
318
386
|
});
|
|
@@ -320,7 +388,7 @@ class SessionStateMachine {
|
|
|
320
388
|
}
|
|
321
389
|
|
|
322
390
|
_trackErrors(now) {
|
|
323
|
-
const cutoff = now - this.config.
|
|
391
|
+
const cutoff = now - this.config.error_window_ms;
|
|
324
392
|
// Expire old errors
|
|
325
393
|
this._errorHistory = this._errorHistory.filter(e => e.timestamp > cutoff);
|
|
326
394
|
|
|
@@ -336,8 +404,8 @@ class SessionStateMachine {
|
|
|
336
404
|
}
|
|
337
405
|
}
|
|
338
406
|
|
|
339
|
-
|
|
340
|
-
if (this._errorHistory.length
|
|
407
|
+
_checkError(now) {
|
|
408
|
+
if (this._errorHistory.length === 0) {
|
|
341
409
|
return null;
|
|
342
410
|
}
|
|
343
411
|
|
|
@@ -347,20 +415,29 @@ class SessionStateMachine {
|
|
|
347
415
|
counts[e.fingerprint] = (counts[e.fingerprint] || 0) + 1;
|
|
348
416
|
}
|
|
349
417
|
|
|
418
|
+
// Find the most repeated error
|
|
419
|
+
let maxFp = null;
|
|
420
|
+
let maxCount = 0;
|
|
350
421
|
for (const [fp, count] of Object.entries(counts)) {
|
|
351
|
-
if (count
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
detail: {
|
|
355
|
-
trigger: 'repeated_error',
|
|
356
|
-
error_fingerprint: fp,
|
|
357
|
-
repeat_count: count,
|
|
358
|
-
window_ms: this.config.stuck_window_ms,
|
|
359
|
-
},
|
|
360
|
-
};
|
|
422
|
+
if (count > maxCount) {
|
|
423
|
+
maxCount = count;
|
|
424
|
+
maxFp = fp;
|
|
361
425
|
}
|
|
362
426
|
}
|
|
363
427
|
|
|
428
|
+
// Repeated errors → high confidence error state
|
|
429
|
+
if (maxCount >= this.config.error_repeat_count) {
|
|
430
|
+
return {
|
|
431
|
+
confidence: Math.min(0.95, 0.7 + (maxCount - this.config.error_repeat_count) * 0.05),
|
|
432
|
+
detail: {
|
|
433
|
+
trigger: 'repeated_error',
|
|
434
|
+
error_fingerprint: maxFp,
|
|
435
|
+
repeat_count: maxCount,
|
|
436
|
+
window_ms: this.config.error_window_ms,
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
364
441
|
return null;
|
|
365
442
|
}
|
|
366
443
|
|
|
@@ -437,6 +514,30 @@ class SessionStateManager {
|
|
|
437
514
|
return result;
|
|
438
515
|
}
|
|
439
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Mark a session as starting (PTY spawn initiated).
|
|
519
|
+
*/
|
|
520
|
+
markStarting(sessionId) {
|
|
521
|
+
const sm = this._machines.get(sessionId);
|
|
522
|
+
if (sm) sm.markStarting();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Mark a session as dead (PTY exited).
|
|
527
|
+
*/
|
|
528
|
+
markDead(sessionId, exitCode, signal) {
|
|
529
|
+
const sm = this._machines.get(sessionId);
|
|
530
|
+
if (sm) sm.markDead(exitCode, signal);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Mark a session as restarting (auto-restart triggered).
|
|
535
|
+
*/
|
|
536
|
+
markRestarting(sessionId) {
|
|
537
|
+
const sm = this._machines.get(sessionId);
|
|
538
|
+
if (sm) sm.markRestarting();
|
|
539
|
+
}
|
|
540
|
+
|
|
440
541
|
/**
|
|
441
542
|
* Unregister and cleanup a session's state machine.
|
|
442
543
|
*/
|
|
@@ -484,13 +585,15 @@ class SessionStateManager {
|
|
|
484
585
|
|
|
485
586
|
module.exports = {
|
|
486
587
|
STATES,
|
|
588
|
+
STATE_DISPLAY,
|
|
487
589
|
DEFAULT_CONFIG,
|
|
488
590
|
SessionStateMachine,
|
|
489
591
|
SessionStateManager,
|
|
490
592
|
// Exported for testing
|
|
491
593
|
PROMPT_PATTERNS,
|
|
492
594
|
THINKING_PATTERNS,
|
|
493
|
-
|
|
595
|
+
WAITING_PATTERNS,
|
|
494
596
|
ERROR_PATTERNS,
|
|
597
|
+
OSC_133_RE,
|
|
495
598
|
stripAnsi,
|
|
496
599
|
};
|