agentgui 1.0.694 → 1.0.696
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/CLAUDE.md +21 -0
- package/lib/acp-sdk-manager.js +107 -102
- package/lib/acp-server-machine.js +166 -0
- package/lib/claude-runner.js +114 -169
- package/lib/execution-machine.js +147 -0
- package/lib/ws-handlers-conv.js +42 -2
- package/package.json +8 -3
- package/scripts/copy-vendor.js +20 -0
- package/server.js +14 -2
- package/static/index.html +3 -0
- package/static/js/client.js +7 -0
- package/static/js/conv-machine.js +102 -0
- package/static/js/websocket-manager.js +33 -9
- package/static/js/ws-machine.js +70 -0
- package/static/lib/xstate.umd.min.js +2 -0
- package/test-state-management.mjs +0 -269
package/CLAUDE.md
CHANGED
|
@@ -18,6 +18,8 @@ server.js HTTP server + WebSocket + all API routes (raw http.create
|
|
|
18
18
|
database.js SQLite setup (WAL mode), schema, query functions
|
|
19
19
|
lib/claude-runner.js Agent framework - spawns CLI processes, parses stream-json output
|
|
20
20
|
lib/acp-manager.js ACP tool lifecycle - auto-starts opencode/kilo HTTP servers, restart on crash
|
|
21
|
+
lib/execution-machine.js XState v5 machine per conversation: idle/streaming/draining/rate_limited states
|
|
22
|
+
lib/acp-server-machine.js XState v5 machine per ACP tool: stopped/starting/running/crashed/restarting states
|
|
21
23
|
lib/ws-protocol.js WebSocket RPC router (WsRouter class)
|
|
22
24
|
lib/ws-optimizer.js Per-client priority queue for WS event batching
|
|
23
25
|
lib/ws-handlers-conv.js Conversation/message/queue RPC handlers (~70 methods total)
|
|
@@ -41,9 +43,28 @@ static/js/syntax-highlighter.js Code syntax highlighting
|
|
|
41
43
|
static/js/voice.js Voice input/output
|
|
42
44
|
static/js/features.js View toggle, drag-drop upload, model progress indicator
|
|
43
45
|
static/js/tools-manager.js Tool install/update UI
|
|
46
|
+
static/js/ws-machine.js XState v5 WS connection machine: disconnected/connecting/connected/reconnecting
|
|
47
|
+
static/js/conv-machine.js XState v5 per-conversation UI machine: idle/streaming/queued
|
|
48
|
+
static/lib/xstate.umd.min.js XState v5 browser bundle (UMD, served locally from node_modules)
|
|
44
49
|
static/templates/ 31 HTML template fragments for event rendering
|
|
45
50
|
```
|
|
46
51
|
|
|
52
|
+
## XState State Machines
|
|
53
|
+
|
|
54
|
+
XState v5 machines provide formal state tracking alongside (not replacing) the existing Map-based execution tracking. The machines are authoritative for state validation; the Maps remain as the operational data store.
|
|
55
|
+
|
|
56
|
+
**Server machines** (ESM, `lib/`):
|
|
57
|
+
- `execution-machine.js`: One actor per conversation. States: idle → streaming → draining (queue drain) → streaming → idle. Also rate_limited state. `execMachine.send(convId, event)` API. `conv.get` and `conv.full` WS responses include `executionState` field.
|
|
58
|
+
- `acp-server-machine.js`: One actor per ACP tool (opencode/kilo/codex). States: stopped → starting → running ↔ crashed → restarting. Used by `acp-sdk-manager.js` to track health and drive restart backoff.
|
|
59
|
+
|
|
60
|
+
**Client machines** (browser UMD, `static/js/`):
|
|
61
|
+
- `ws-machine.js`: Wraps WebSocketManager. States: disconnected/connecting/connected/reconnecting. Actor accessible as `wsManager._wsActor`. State readable via `wsManager.connectionState`.
|
|
62
|
+
- `conv-machine.js`: One actor per conversation. States: idle/streaming/queued. API exposed as `window.convMachineAPI`. All actors in `window.__convMachines` Map for debug.
|
|
63
|
+
|
|
64
|
+
**XState browser loading**: UMD bundle at `static/lib/xstate.umd.min.js` (copied from `node_modules/xstate/dist/xstate.umd.min.js` during npm install). Loaded as `defer` script before ws-machine.js and conv-machine.js. Exposes `window.XState` global.
|
|
65
|
+
|
|
66
|
+
**Integration pattern**: Machines augment existing state (Maps, Set, booleans). They receive events at the same points the Maps are mutated. Machine state does NOT replace Map checks in server logic — both run in parallel, machine is the formal model.
|
|
67
|
+
|
|
47
68
|
## Key Details
|
|
48
69
|
|
|
49
70
|
- Express is used only for file upload (`/api/upload/:conversationId`) and fsbrowse file browser (`/files/:conversationId`). All other routes use raw `http.createServer` with manual routing.
|
package/lib/acp-sdk-manager.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { AbortError } from 'p-retry';
|
|
7
|
+
import * as acpMachine from './acp-server-machine.js';
|
|
6
8
|
|
|
7
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
10
|
const projectRoot = path.resolve(__dirname, '..');
|
|
@@ -16,11 +18,10 @@ const ACP_TOOLS = [
|
|
|
16
18
|
|
|
17
19
|
const HEALTH_INTERVAL_MS = 30000;
|
|
18
20
|
const STARTUP_GRACE_MS = 5000;
|
|
19
|
-
const MAX_RESTARTS = 10;
|
|
20
|
-
const RESTART_WINDOW_MS = 300000;
|
|
21
21
|
const IDLE_TIMEOUT_MS = 120000;
|
|
22
22
|
|
|
23
23
|
const processes = new Map();
|
|
24
|
+
const idleTimers = new Map();
|
|
24
25
|
let healthTimer = null;
|
|
25
26
|
let shuttingDown = false;
|
|
26
27
|
|
|
@@ -33,169 +34,174 @@ function resolveCommand(tool) {
|
|
|
33
34
|
return { bin: tool.cmd, args: tool.args };
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function resetIdleTimer(toolId) {
|
|
38
|
+
acpMachine.send(toolId, { type: 'TOUCH' });
|
|
39
|
+
const existing = idleTimers.get(toolId);
|
|
40
|
+
if (existing) clearTimeout(existing);
|
|
41
|
+
idleTimers.set(toolId, setTimeout(() => {
|
|
42
|
+
acpMachine.send(toolId, { type: 'IDLE_TIMEOUT' });
|
|
43
|
+
stopTool(toolId);
|
|
44
|
+
}, IDLE_TIMEOUT_MS));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clearIdleTimer(toolId) {
|
|
48
|
+
const t = idleTimers.get(toolId);
|
|
49
|
+
if (t) { clearTimeout(t); idleTimers.delete(toolId); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stopTool(toolId) {
|
|
53
|
+
const proc = processes.get(toolId);
|
|
54
|
+
if (!proc) return;
|
|
55
|
+
log(toolId + ' stopping');
|
|
56
|
+
clearIdleTimer(toolId);
|
|
57
|
+
try { proc.kill('SIGTERM'); } catch (_) {}
|
|
58
|
+
setTimeout(() => { try { proc.kill('SIGKILL'); } catch (_) {} }, 5000);
|
|
59
|
+
processes.delete(toolId);
|
|
60
|
+
acpMachine.send(toolId, { type: 'STOPPED' });
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
function startProcess(tool) {
|
|
37
64
|
if (shuttingDown) return null;
|
|
38
|
-
const existing = processes.get(tool.id);
|
|
39
|
-
if (existing?.process && !existing.process.killed) return existing;
|
|
40
|
-
|
|
41
65
|
const resolved = resolveCommand(tool);
|
|
42
|
-
|
|
43
|
-
id: tool.id,
|
|
44
|
-
port: tool.port,
|
|
45
|
-
startedAt: Date.now(),
|
|
46
|
-
lastUsed: Date.now(),
|
|
47
|
-
lastHealthCheck: 0,
|
|
48
|
-
healthy: false,
|
|
49
|
-
process: null,
|
|
50
|
-
pid: null,
|
|
51
|
-
restarts: [],
|
|
52
|
-
idleTimer: null,
|
|
53
|
-
providerInfo: null,
|
|
54
|
-
_stopping: false
|
|
55
|
-
};
|
|
56
|
-
|
|
66
|
+
let proc;
|
|
57
67
|
try {
|
|
58
|
-
|
|
68
|
+
proc = spawn(resolved.bin, resolved.args, {
|
|
59
69
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
60
|
-
detached: false
|
|
70
|
+
detached: false,
|
|
61
71
|
});
|
|
62
|
-
entry.pid = entry.process.pid;
|
|
63
|
-
|
|
64
|
-
entry.process.on('close', (code) => {
|
|
65
|
-
entry.healthy = false;
|
|
66
|
-
if (shuttingDown || entry._stopping) return;
|
|
67
|
-
log(tool.id + ' exited code ' + code);
|
|
68
|
-
const window = Date.now() - RESTART_WINDOW_MS;
|
|
69
|
-
entry.restarts = entry.restarts.filter(t => t > window);
|
|
70
|
-
if (entry.restarts.length < MAX_RESTARTS) {
|
|
71
|
-
const delay = Math.min(1000 * Math.pow(2, entry.restarts.length), 30000);
|
|
72
|
-
entry.restarts.push(Date.now());
|
|
73
|
-
setTimeout(() => startProcess(tool), delay);
|
|
74
|
-
} else {
|
|
75
|
-
log(tool.id + ' max restarts reached');
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
processes.set(tool.id, entry);
|
|
80
|
-
log(tool.id + ' started port ' + tool.port + ' pid ' + entry.pid);
|
|
81
|
-
setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
|
|
82
|
-
resetIdleTimer(tool.id);
|
|
83
72
|
} catch (err) {
|
|
84
73
|
log(tool.id + ' spawn failed: ' + err.message);
|
|
74
|
+
acpMachine.send(tool.id, { type: 'CRASHED' });
|
|
75
|
+
scheduleRestart(tool);
|
|
76
|
+
return null;
|
|
85
77
|
}
|
|
86
78
|
|
|
87
|
-
|
|
88
|
-
}
|
|
79
|
+
processes.set(tool.id, proc);
|
|
80
|
+
acpMachine.send(tool.id, { type: 'START' });
|
|
81
|
+
acpMachine.send(tool.id, { type: 'STARTED', process: proc, pid: proc.pid });
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
const entry = processes.get(toolId);
|
|
92
|
-
if (!entry) return;
|
|
93
|
-
entry.lastUsed = Date.now();
|
|
94
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
95
|
-
entry.idleTimer = setTimeout(() => stopTool(toolId), IDLE_TIMEOUT_MS);
|
|
96
|
-
}
|
|
83
|
+
log(tool.id + ' started port ' + tool.port + ' pid ' + proc.pid);
|
|
97
84
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
proc.on('close', (code) => {
|
|
86
|
+
processes.delete(tool.id);
|
|
87
|
+
if (shuttingDown) return;
|
|
88
|
+
log(tool.id + ' exited code ' + code);
|
|
89
|
+
acpMachine.send(tool.id, { type: 'CRASHED' });
|
|
90
|
+
const snap = acpMachine.snapshot(tool.id);
|
|
91
|
+
if (snap?.value === 'stopped') { log(tool.id + ' max restarts reached'); return; }
|
|
92
|
+
scheduleRestart(tool);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
setTimeout(() => checkHealth(tool.id, tool.port), STARTUP_GRACE_MS);
|
|
96
|
+
resetIdleTimer(tool.id);
|
|
97
|
+
return proc;
|
|
107
98
|
}
|
|
108
99
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
100
|
+
function scheduleRestart(tool) {
|
|
101
|
+
if (shuttingDown) return;
|
|
102
|
+
const delay = acpMachine.getBackoffDelay(tool.id);
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
if (!shuttingDown) startProcess(tool);
|
|
105
|
+
}, delay);
|
|
106
|
+
}
|
|
112
107
|
|
|
108
|
+
async function checkHealth(toolId, port) {
|
|
109
|
+
if (shuttingDown) return;
|
|
110
|
+
const snap = acpMachine.snapshot(toolId);
|
|
111
|
+
if (!snap || snap.value === 'stopped' || snap.value === 'idle_stopping') return;
|
|
112
|
+
const p = port || ACP_TOOLS.find(t => t.id === toolId)?.port;
|
|
113
|
+
if (!p) return;
|
|
113
114
|
try {
|
|
114
|
-
const res = await fetch('http://127.0.0.1:' +
|
|
115
|
-
signal: AbortSignal.timeout(3000)
|
|
115
|
+
const res = await fetch('http://127.0.0.1:' + p + '/provider', {
|
|
116
|
+
signal: AbortSignal.timeout(3000),
|
|
116
117
|
});
|
|
117
|
-
entry.healthy = res.ok;
|
|
118
118
|
if (res.ok) {
|
|
119
|
-
|
|
119
|
+
const providerInfo = await res.json();
|
|
120
|
+
acpMachine.send(toolId, { type: 'HEALTHY', providerInfo });
|
|
121
|
+
} else {
|
|
122
|
+
acpMachine.send(toolId, { type: 'UNHEALTHY' });
|
|
120
123
|
}
|
|
121
124
|
} catch (_) {
|
|
122
|
-
|
|
125
|
+
acpMachine.send(toolId, { type: 'UNHEALTHY' });
|
|
123
126
|
}
|
|
124
|
-
entry.lastHealthCheck = Date.now();
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
export async function ensureRunning(agentId) {
|
|
128
130
|
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
129
131
|
if (!tool) return null;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!
|
|
133
|
-
|
|
134
|
-
if (!entry) return null;
|
|
132
|
+
if (acpMachine.isHealthy(agentId)) { resetIdleTimer(agentId); return tool.port; }
|
|
133
|
+
const snap = acpMachine.snapshot(agentId);
|
|
134
|
+
if (!snap || snap.value === 'stopped' || snap.value === 'crashed') {
|
|
135
|
+
startProcess(tool);
|
|
135
136
|
}
|
|
136
137
|
for (let i = 0; i < 20; i++) {
|
|
137
138
|
await new Promise(r => setTimeout(r, 500));
|
|
138
|
-
|
|
139
|
-
if (
|
|
139
|
+
if (shuttingDown) return null;
|
|
140
|
+
if (acpMachine.isHealthy(agentId)) { resetIdleTimer(agentId); return tool.port; }
|
|
140
141
|
}
|
|
141
142
|
return null;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
export function touch(agentId) {
|
|
145
|
-
const entry = processes.get(agentId);
|
|
146
|
-
if (entry) resetIdleTimer(agentId);
|
|
147
|
-
}
|
|
145
|
+
export function touch(agentId) { resetIdleTimer(agentId); }
|
|
148
146
|
|
|
149
147
|
export async function startAll() {
|
|
150
148
|
log('ACP tools available (on-demand start)');
|
|
151
149
|
healthTimer = setInterval(() => {
|
|
152
|
-
for (const
|
|
150
|
+
for (const tool of ACP_TOOLS) {
|
|
151
|
+
const snap = acpMachine.snapshot(tool.id);
|
|
152
|
+
if (snap && (snap.value === 'running' || snap.value === 'starting')) {
|
|
153
|
+
checkHealth(tool.id, tool.port);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
153
156
|
}, HEALTH_INTERVAL_MS);
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
export async function stopAll() {
|
|
157
160
|
shuttingDown = true;
|
|
158
161
|
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
162
|
+
for (const toolId of idleTimers.keys()) clearIdleTimer(toolId);
|
|
159
163
|
const kills = [];
|
|
160
|
-
for (const [id,
|
|
161
|
-
|
|
162
|
-
log('stopping ' + id + ' pid ' + entry.pid);
|
|
164
|
+
for (const [id, proc] of processes) {
|
|
165
|
+
log('stopping ' + id + ' pid ' + proc.pid);
|
|
163
166
|
kills.push(new Promise(resolve => {
|
|
164
|
-
const t = setTimeout(() => { try {
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
+
const t = setTimeout(() => { try { proc.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
|
|
168
|
+
proc.on('close', () => { clearTimeout(t); resolve(); });
|
|
169
|
+
try { proc.kill('SIGTERM'); } catch (_) {}
|
|
167
170
|
}));
|
|
168
171
|
}
|
|
169
172
|
await Promise.all(kills);
|
|
170
173
|
processes.clear();
|
|
174
|
+
acpMachine.stopAll();
|
|
171
175
|
log('all stopped');
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
export function getStatus() {
|
|
175
179
|
return ACP_TOOLS.map(tool => {
|
|
176
|
-
const
|
|
180
|
+
const snap = acpMachine.snapshot(tool.id);
|
|
181
|
+
const ctx = snap?.context || {};
|
|
177
182
|
return {
|
|
178
183
|
id: tool.id,
|
|
179
184
|
port: tool.port,
|
|
180
|
-
running:
|
|
181
|
-
healthy:
|
|
182
|
-
pid:
|
|
183
|
-
uptime:
|
|
184
|
-
restartCount:
|
|
185
|
-
idleMs:
|
|
186
|
-
providerInfo:
|
|
185
|
+
running: snap?.value === 'running' || snap?.value === 'starting',
|
|
186
|
+
healthy: ctx.healthy || false,
|
|
187
|
+
pid: ctx.pid,
|
|
188
|
+
uptime: ctx.startedAt ? Date.now() - ctx.startedAt : 0,
|
|
189
|
+
restartCount: ctx.restarts?.length || 0,
|
|
190
|
+
idleMs: ctx.lastUsed ? Date.now() - ctx.lastUsed : 0,
|
|
191
|
+
providerInfo: ctx.providerInfo || null,
|
|
187
192
|
};
|
|
188
193
|
});
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
export function getPort(agentId) {
|
|
192
|
-
|
|
193
|
-
return e?.healthy ? e.port : null;
|
|
197
|
+
return acpMachine.isHealthy(agentId) ? (ACP_TOOLS.find(t => t.id === agentId)?.port || null) : null;
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
export function getRunningPorts() {
|
|
197
201
|
const ports = {};
|
|
198
|
-
for (const
|
|
202
|
+
for (const tool of ACP_TOOLS) {
|
|
203
|
+
if (acpMachine.isHealthy(tool.id)) ports[tool.id] = tool.port;
|
|
204
|
+
}
|
|
199
205
|
return ports;
|
|
200
206
|
}
|
|
201
207
|
|
|
@@ -212,7 +218,7 @@ export async function queryModels(agentId) {
|
|
|
212
218
|
if (!port) return [];
|
|
213
219
|
try {
|
|
214
220
|
const res = await fetch('http://127.0.0.1:' + port + '/models', {
|
|
215
|
-
signal: AbortSignal.timeout(3000)
|
|
221
|
+
signal: AbortSignal.timeout(3000),
|
|
216
222
|
});
|
|
217
223
|
if (!res.ok) return [];
|
|
218
224
|
const data = await res.json();
|
|
@@ -221,6 +227,5 @@ export async function queryModels(agentId) {
|
|
|
221
227
|
}
|
|
222
228
|
|
|
223
229
|
export function isAvailable(agentId) {
|
|
224
|
-
|
|
225
|
-
return !!tool;
|
|
230
|
+
return !!ACP_TOOLS.find(t => t.id === agentId);
|
|
226
231
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
2
|
+
|
|
3
|
+
const MAX_RESTARTS = 10;
|
|
4
|
+
const RESTART_WINDOW_MS = 300000;
|
|
5
|
+
|
|
6
|
+
function calcBackoff(restarts) {
|
|
7
|
+
return Math.min(1000 * Math.pow(2, restarts.length), 30000);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function purgeOldRestarts(restarts) {
|
|
11
|
+
const window = Date.now() - RESTART_WINDOW_MS;
|
|
12
|
+
return restarts.filter(t => t > window);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const machine = createMachine({
|
|
16
|
+
id: 'acp-server',
|
|
17
|
+
initial: 'stopped',
|
|
18
|
+
context: {
|
|
19
|
+
process: null,
|
|
20
|
+
pid: null,
|
|
21
|
+
healthy: false,
|
|
22
|
+
restarts: [],
|
|
23
|
+
lastHealthCheck: 0,
|
|
24
|
+
providerInfo: null,
|
|
25
|
+
startedAt: null,
|
|
26
|
+
lastUsed: null,
|
|
27
|
+
idleDeadline: null,
|
|
28
|
+
},
|
|
29
|
+
states: {
|
|
30
|
+
stopped: {
|
|
31
|
+
entry: assign({ process: null, pid: null, healthy: false }),
|
|
32
|
+
on: {
|
|
33
|
+
START: 'starting',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
starting: {
|
|
37
|
+
entry: assign(({ event }) => ({
|
|
38
|
+
process: event.process || null,
|
|
39
|
+
pid: event.pid || null,
|
|
40
|
+
startedAt: Date.now(),
|
|
41
|
+
lastUsed: Date.now(),
|
|
42
|
+
healthy: false,
|
|
43
|
+
})),
|
|
44
|
+
on: {
|
|
45
|
+
HEALTHY: {
|
|
46
|
+
target: 'running',
|
|
47
|
+
actions: assign(({ event }) => ({
|
|
48
|
+
healthy: true,
|
|
49
|
+
lastHealthCheck: Date.now(),
|
|
50
|
+
providerInfo: event.providerInfo || null,
|
|
51
|
+
})),
|
|
52
|
+
},
|
|
53
|
+
CRASHED: 'crashed',
|
|
54
|
+
STOP: 'idle_stopping',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
running: {
|
|
58
|
+
entry: assign({ healthy: true }),
|
|
59
|
+
on: {
|
|
60
|
+
UNHEALTHY: {
|
|
61
|
+
actions: assign({ healthy: false, lastHealthCheck: Date.now() }),
|
|
62
|
+
},
|
|
63
|
+
HEALTHY: {
|
|
64
|
+
actions: assign(({ event }) => ({
|
|
65
|
+
healthy: true,
|
|
66
|
+
lastHealthCheck: Date.now(),
|
|
67
|
+
providerInfo: event.providerInfo || null,
|
|
68
|
+
})),
|
|
69
|
+
},
|
|
70
|
+
TOUCH: {
|
|
71
|
+
actions: assign({ lastUsed: Date.now() }),
|
|
72
|
+
},
|
|
73
|
+
CRASHED: 'crashed',
|
|
74
|
+
STOP: 'idle_stopping',
|
|
75
|
+
IDLE_TIMEOUT: 'idle_stopping',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
crashed: {
|
|
79
|
+
entry: assign({ healthy: false }),
|
|
80
|
+
always: [
|
|
81
|
+
{
|
|
82
|
+
guard: ({ context }) => {
|
|
83
|
+
const recent = purgeOldRestarts(context.restarts);
|
|
84
|
+
return recent.length >= MAX_RESTARTS;
|
|
85
|
+
},
|
|
86
|
+
target: 'stopped',
|
|
87
|
+
},
|
|
88
|
+
{ target: 'restarting' },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
restarting: {
|
|
92
|
+
entry: assign(({ context }) => ({
|
|
93
|
+
restarts: [...purgeOldRestarts(context.restarts), Date.now()],
|
|
94
|
+
process: null,
|
|
95
|
+
pid: null,
|
|
96
|
+
healthy: false,
|
|
97
|
+
})),
|
|
98
|
+
on: {
|
|
99
|
+
STARTED: {
|
|
100
|
+
target: 'starting',
|
|
101
|
+
actions: assign(({ event }) => ({
|
|
102
|
+
process: event.process,
|
|
103
|
+
pid: event.pid,
|
|
104
|
+
startedAt: Date.now(),
|
|
105
|
+
})),
|
|
106
|
+
},
|
|
107
|
+
STOP: 'stopped',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
idle_stopping: {
|
|
111
|
+
entry: assign({ healthy: false }),
|
|
112
|
+
on: {
|
|
113
|
+
STOPPED: {
|
|
114
|
+
target: 'stopped',
|
|
115
|
+
actions: assign({ process: null, pid: null }),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const actors = new Map();
|
|
123
|
+
|
|
124
|
+
export function getOrCreate(toolId) {
|
|
125
|
+
if (actors.has(toolId)) return actors.get(toolId);
|
|
126
|
+
const actor = createActor(machine);
|
|
127
|
+
actor.start();
|
|
128
|
+
actors.set(toolId, actor);
|
|
129
|
+
return actor;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function get(toolId) {
|
|
133
|
+
return actors.get(toolId) || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function send(toolId, event) {
|
|
137
|
+
const actor = getOrCreate(toolId);
|
|
138
|
+
actor.send(event);
|
|
139
|
+
return actor.getSnapshot();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function snapshot(toolId) {
|
|
143
|
+
const actor = actors.get(toolId);
|
|
144
|
+
return actor ? actor.getSnapshot() : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isRunning(toolId) {
|
|
148
|
+
const s = snapshot(toolId);
|
|
149
|
+
return s ? s.value === 'running' : false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function isHealthy(toolId) {
|
|
153
|
+
const s = snapshot(toolId);
|
|
154
|
+
return s ? (s.value === 'running' && s.context.healthy) : false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getBackoffDelay(toolId) {
|
|
158
|
+
const s = snapshot(toolId);
|
|
159
|
+
if (!s) return 1000;
|
|
160
|
+
return calcBackoff(purgeOldRestarts(s.context.restarts));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function stopAll() {
|
|
164
|
+
for (const [, actor] of actors) actor.stop();
|
|
165
|
+
actors.clear();
|
|
166
|
+
}
|