flint-agent 1.14.0 → 1.14.2
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/.env.example +1 -1
- package/CHANGELOG.md +33 -0
- package/FEATURES.md +1 -1
- package/package.json +2 -2
- package/src/api/server.js +4 -0
- package/src/components/App.js +5 -1
- package/src/components/LiveZone.js +1 -1
- package/src/config.js +1 -1
- package/src/index.js +30 -17
- package/src/launcher.js +9 -3
- package/src/tools/agent-tools.js +54 -25
package/.env.example
CHANGED
|
@@ -72,7 +72,7 @@ INTENT_MODEL=openai/gpt-5.4-mini
|
|
|
72
72
|
|
|
73
73
|
# Timeouts
|
|
74
74
|
# AGENT_IDLE_TIMEOUT=0 # Auto-exit after N seconds idle (0=disabled)
|
|
75
|
-
# AGENT_CHILD_IDLE_TIMEOUT=
|
|
75
|
+
# AGENT_CHILD_IDLE_TIMEOUT=0 # Child agent idle timeout (seconds, 0 = lives as long as the parent)
|
|
76
76
|
# AGENT_CHILD_CLEANUP_DELAY=5000 # Delay before cleaning up dead children (ms)
|
|
77
77
|
|
|
78
78
|
# Context management
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ All notable changes to Flint are written here. The format follows
|
|
|
7
7
|
Each version is a `## [x.y.z] - YYYY-MM-DD` heading: `/update` reads these
|
|
8
8
|
headings to show what changed between your version and the newest one.
|
|
9
9
|
|
|
10
|
+
## [1.14.2] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Child agents opened in their own window (`spawn_agent` with `visible`)
|
|
14
|
+
stay reachable. The process that opens the window exits at once, and Flint
|
|
15
|
+
took that for the agent's exit: it reported "exited", forgot the agent,
|
|
16
|
+
and `ask_agent` answered "has stopped", so every question started a new
|
|
17
|
+
agent. Whether such an agent is alive is now decided by its heartbeat, and
|
|
18
|
+
Esc stops the agent itself rather than the long-gone window launcher.
|
|
19
|
+
- The API key is no longer written in plain text to a temporary `.bat` file
|
|
20
|
+
when a visible child agent starts on Windows; it is passed in the
|
|
21
|
+
environment.
|
|
22
|
+
- A piece of the "loading..." splash no longer stays on the top line of the
|
|
23
|
+
console now and then: Flint waits until the splash has stopped before it
|
|
24
|
+
clears the screen.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- A child agent lives as long as its parent instead of stopping after 60
|
|
28
|
+
seconds idle, so you can talk to it across several questions. It still
|
|
29
|
+
stops about a minute after its parent is gone. `AGENT_CHILD_IDLE_TIMEOUT`
|
|
30
|
+
turns an idle limit back on.
|
|
31
|
+
|
|
32
|
+
## [1.14.1] - 2026-10-02
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Free mode no longer crashes the console. After `/model free` or
|
|
36
|
+
`/model free auto` chose a model, Flint stopped with React error #185, and
|
|
37
|
+
then on every start, because the chosen models are remembered. The status
|
|
38
|
+
line built its free-request counter as a new object on each check, so the
|
|
39
|
+
screen redrew without end.
|
|
40
|
+
- `bin` in package.json written the way npm expects, so publishing no longer
|
|
41
|
+
warns that it was corrected.
|
|
42
|
+
|
|
10
43
|
## [1.14.0] - 2026-10-02
|
|
11
44
|
|
|
12
45
|
### Added
|
package/FEATURES.md
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
### 5. Multi-Agent
|
|
44
44
|
- Spawn child agents with different models and profiles (`spawn_agent`)
|
|
45
45
|
- `ask_agent` waits for the child's answer; `wait_tasks` waits for delegated work
|
|
46
|
-
- Each child runs on its own port with its own data folder, and
|
|
46
|
+
- Each child runs on its own port with its own data folder, and lives as long as its parent (an idle timeout can be turned on)
|
|
47
47
|
- Child agents can push datasets back to the parent
|
|
48
48
|
|
|
49
49
|
### 6. Memory & Knowledge
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flint-agent",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
4
4
|
"description": "A lightweight AI agent for the terminal that works with any LLM, free tiers included: files, commands, web, MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"vitest": "^4.0.18"
|
|
37
37
|
},
|
|
38
38
|
"bin": {
|
|
39
|
-
"flint": "
|
|
39
|
+
"flint": "bin/flint.js"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=22.12.0"
|
package/src/api/server.js
CHANGED
|
@@ -311,6 +311,10 @@ export function startServer(port, store, processMessage, opts = {}) {
|
|
|
311
311
|
model: s.model,
|
|
312
312
|
messages: s.messages.length,
|
|
313
313
|
alive: true,
|
|
314
|
+
// A parent that opened this agent in its own window only knows the
|
|
315
|
+
// pid of the window launcher, which exits at once; it learns the
|
|
316
|
+
// real one here so it can stop the agent (tools/agent-tools.js).
|
|
317
|
+
pid: process.pid,
|
|
314
318
|
// A bench harness cannot sum the per-turn figures and get the truth:
|
|
315
319
|
// a turn it abandoned on a timeout still spent money it never saw.
|
|
316
320
|
// The session total is the only honest number, so serve it, and the
|
package/src/components/App.js
CHANGED
|
@@ -112,7 +112,11 @@ export function App({ store, onSubmit, onAbort, onQuit, onClipboard, onRecallQue
|
|
|
112
112
|
careLevel: s._careLevel || null,
|
|
113
113
|
spendLevel: s._spendLevel || getSpendLevel(),
|
|
114
114
|
// Free mode: requests made today with a free model, of the daily limit.
|
|
115
|
-
|
|
115
|
+
// Two plain numbers, not an object: the selector runs on every store
|
|
116
|
+
// check, and a fresh object each time never compares equal, so React
|
|
117
|
+
// re-rendered forever (error #185) as soon as free mode was on.
|
|
118
|
+
freeUsed: config.freeChain ? freeUsedToday() : null,
|
|
119
|
+
freeLimit: config.freeChain ? s._freeLimit || savedFreeLimit() : null,
|
|
116
120
|
}));
|
|
117
121
|
const { inputHistory } = ui;
|
|
118
122
|
|
|
@@ -221,7 +221,7 @@ export function statusText(state) {
|
|
|
221
221
|
}
|
|
222
222
|
parts.push(formatStatusLevel());
|
|
223
223
|
if (state.spendLevel) parts.push(`spend: ${state.spendLevel}`);
|
|
224
|
-
if (state.
|
|
224
|
+
if (state.freeLimit != null) parts.push(`free ${state.freeUsed}/${state.freeLimit}`);
|
|
225
225
|
parts.push("/help");
|
|
226
226
|
return ` ${parts.join(" | ")}`;
|
|
227
227
|
}
|
package/src/config.js
CHANGED
|
@@ -195,7 +195,7 @@ export const config = {
|
|
|
195
195
|
.filter(Boolean)
|
|
196
196
|
.map((p) => path.resolve(p)),
|
|
197
197
|
maxChildAgents: parseInt(process.env.AGENT_MAX_CHILDREN || "5", 10),
|
|
198
|
-
childIdleTimeout: parseInt(process.env.AGENT_CHILD_IDLE_TIMEOUT || "
|
|
198
|
+
childIdleTimeout: parseInt(process.env.AGENT_CHILD_IDLE_TIMEOUT || "0", 10), // seconds, 0 = live as long as the parent
|
|
199
199
|
childCleanupDelay: parseInt(process.env.AGENT_CHILD_CLEANUP_DELAY || "30000", 10), // ms before removing stopped agent from registry
|
|
200
200
|
shell: process.env.AGENT_SHELL || (() => {
|
|
201
201
|
if (process.platform !== "win32") return "/bin/bash";
|
package/src/index.js
CHANGED
|
@@ -235,6 +235,36 @@ function handleAbort() {
|
|
|
235
235
|
// Populate header lines BEFORE Ink render so first frame is complete
|
|
236
236
|
printHeader();
|
|
237
237
|
|
|
238
|
+
// Longest wait for the launcher to confirm its spinner stopped. It answers in
|
|
239
|
+
// a few ms; the cap only matters for a launcher that never answers.
|
|
240
|
+
const LAUNCHER_RELEASE_WAIT_MS = 500;
|
|
241
|
+
|
|
242
|
+
// Stop the launcher's splash spinner BEFORE clearing the screen, and wait
|
|
243
|
+
// until it says it has stopped.
|
|
244
|
+
//
|
|
245
|
+
// The launcher spawns us with the three standard streams inherited, so it
|
|
246
|
+
// cannot see our output: there is no pipe to watch. Its spinner repaints with
|
|
247
|
+
// a bare \r every 120 ms. "flint:ready" used to be sent after Ink's first
|
|
248
|
+
// render, so a tick landing between the clear below and that message wrote
|
|
249
|
+
// "loading..." onto the top line of the cleared screen, where Ink then drew
|
|
250
|
+
// its border on the same row (owner, 2026-10-02, intermittent). The launcher
|
|
251
|
+
// stops the interval first and answers "flint:released" after, so once the
|
|
252
|
+
// answer is here no tick can follow. The wait is capped: an older launcher
|
|
253
|
+
// never answers, and a missing answer must not hold the start.
|
|
254
|
+
//
|
|
255
|
+
// Sent only when there is a channel: run directly (`node src/index.js`,
|
|
256
|
+
// tests) there is no parent IPC and process.send is undefined, which is a
|
|
257
|
+
// normal way to start Flint and must not throw.
|
|
258
|
+
if (process.send) {
|
|
259
|
+
await new Promise((resolve) => {
|
|
260
|
+
const done = () => { clearTimeout(timer); process.off("message", onMessage); resolve(); };
|
|
261
|
+
const onMessage = (msg) => { if (msg && msg.type === "flint:released") done(); };
|
|
262
|
+
const timer = setTimeout(done, LAUNCHER_RELEASE_WAIT_MS);
|
|
263
|
+
process.on("message", onMessage);
|
|
264
|
+
process.send({ type: "flint:ready" });
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
238
268
|
// Clear entire screen (remove launcher splash) before Ink takes over
|
|
239
269
|
if (cli.action !== "headless") {
|
|
240
270
|
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
@@ -245,23 +275,6 @@ if (inkInstance) {
|
|
|
245
275
|
store.setState({ _inkClear: () => inkInstance.clear() });
|
|
246
276
|
}
|
|
247
277
|
|
|
248
|
-
// Tell the launcher we are drawing now, so it stops repainting its splash
|
|
249
|
-
// spinner over this frame.
|
|
250
|
-
//
|
|
251
|
-
// The launcher spawns us with the three standard streams inherited, so it
|
|
252
|
-
// cannot see our output: there is no pipe to watch. Its spinner repaints with
|
|
253
|
-
// a bare \r, which the terminal reads as "back to column 0 of whatever line is
|
|
254
|
-
// current now" — and once this frame exists, that is this line. The result was
|
|
255
|
-
// the startup banner and the first few commands printed several times over.
|
|
256
|
-
//
|
|
257
|
-
// The message is sent only when there is a channel to send it on: run directly
|
|
258
|
-
// (`node src/index.js`, tests, headless) there is no parent IPC and
|
|
259
|
-
// process.send is undefined, which is a normal way to start Flint and must not
|
|
260
|
-
// throw.
|
|
261
|
-
if (process.send) {
|
|
262
|
-
process.send({ type: "flint:ready" });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
278
|
// -- Graceful shutdown --
|
|
266
279
|
|
|
267
280
|
function gracefulShutdown(signal) {
|
package/src/launcher.js
CHANGED
|
@@ -76,8 +76,9 @@ const _spinner = setInterval(() => {
|
|
|
76
76
|
* streams stay inherited on purpose — piping them would cost Ink the TTY, and
|
|
77
77
|
* raw-mode key handling with it.
|
|
78
78
|
*
|
|
79
|
-
* So the child says when it
|
|
80
|
-
* and is invisible to the terminal
|
|
79
|
+
* So the child says when it is about to draw, over an IPC channel that costs
|
|
80
|
+
* nothing and is invisible to the terminal, and waits for "flint:released"
|
|
81
|
+
* before it clears the screen. The interval is kept as a ceiling rather
|
|
81
82
|
* than removed: a child that dies without saying anything must not hold the
|
|
82
83
|
* event loop open, and `_released` makes the remaining ticks no-ops.
|
|
83
84
|
*/
|
|
@@ -100,8 +101,13 @@ function start(extraArgs = []) {
|
|
|
100
101
|
});
|
|
101
102
|
if (stdioMode) releaseTerminal();
|
|
102
103
|
|
|
104
|
+
// On "flint:ready": stop first, answer after. Once the child has the answer
|
|
105
|
+
// no tick can follow, so it can clear the screen safely (src/index.js).
|
|
103
106
|
child.on("message", (msg) => {
|
|
104
|
-
if (msg && msg.type === "flint:ready")
|
|
107
|
+
if (msg && msg.type === "flint:ready") {
|
|
108
|
+
releaseTerminal();
|
|
109
|
+
try { child.send({ type: "flint:released" }); } catch {}
|
|
110
|
+
}
|
|
105
111
|
if (msg && msg.type === "flint:restart" && typeof msg.sessionId === "string") resumeSessionId = msg.sessionId;
|
|
106
112
|
});
|
|
107
113
|
|
package/src/tools/agent-tools.js
CHANGED
|
@@ -20,6 +20,16 @@ const HEARTBEAT_INTERVAL = 15000; // 15s
|
|
|
20
20
|
const MAX_MISSED_PINGS = 3;
|
|
21
21
|
let _heartbeatTimer = null;
|
|
22
22
|
|
|
23
|
+
// Keep the pid a child reports in /status: for an agent in its own window it
|
|
24
|
+
// is the only handle on the real process (the launcher's pid is dead).
|
|
25
|
+
async function notePid(agent, res) {
|
|
26
|
+
if (!agent.visible || agent.agentPid || typeof res.json !== "function") return;
|
|
27
|
+
try {
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
if (Number.isInteger(data?.pid)) agent.agentPid = data.pid;
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
// Start heartbeat monitoring for child agents
|
|
24
34
|
function startChildHeartbeat(store) {
|
|
25
35
|
if (_heartbeatTimer) return;
|
|
@@ -31,6 +41,7 @@ function startChildHeartbeat(store) {
|
|
|
31
41
|
signal: AbortSignal.timeout(3000),
|
|
32
42
|
});
|
|
33
43
|
if (res.ok) {
|
|
44
|
+
await notePid(agent, res);
|
|
34
45
|
if (agent.missedPings > 0) {
|
|
35
46
|
// Recovered
|
|
36
47
|
const label = agent.profile !== "generic" ? agent.profile : null;
|
|
@@ -49,6 +60,12 @@ function startChildHeartbeat(store) {
|
|
|
49
60
|
printChildEvent(port, `connection lost (${MAX_MISSED_PINGS} missed pings)`, label);
|
|
50
61
|
store.getState().finishProcess(agent.procId, null, "lost");
|
|
51
62
|
activeChildren.delete(agent.procId);
|
|
63
|
+
// A visible agent has no "close" of its own to clean up after it
|
|
64
|
+
// (spawn_agent), so the heartbeat does it.
|
|
65
|
+
if (agent.visible) {
|
|
66
|
+
if (agent.taskRegId) store.getState().unregisterTask(agent.taskRegId);
|
|
67
|
+
setTimeout(() => _childAgents.delete(port), config.childCleanupDelay);
|
|
68
|
+
}
|
|
52
69
|
} else {
|
|
53
70
|
log.debug(`Agent@${port} no response (${agent.missedPings}/${MAX_MISSED_PINGS})`);
|
|
54
71
|
printChildEvent(port, `no response (${agent.missedPings}/${MAX_MISSED_PINGS})`, label);
|
|
@@ -179,8 +196,12 @@ export function createAgentHandlers(store) {
|
|
|
179
196
|
childEnv.AGENT_PAIRING_SECRET = pairingSecret;
|
|
180
197
|
// Parent port for orphan protection heartbeat
|
|
181
198
|
childEnv.AGENT_PARENT_PORT = String(config.port);
|
|
182
|
-
//
|
|
183
|
-
|
|
199
|
+
// Idle exit, off by default (0): a child lives as long as its parent,
|
|
200
|
+
// and the parent heartbeat in index.js stops it when the parent is
|
|
201
|
+
// gone, which is what the idle exit was first added for. A 60 s default
|
|
202
|
+
// stopped children between two questions, so a conversation with one
|
|
203
|
+
// was impossible (owner, 2026-10-02). AGENT_CHILD_IDLE_TIMEOUT turns it on.
|
|
204
|
+
childEnv.AGENT_IDLE_TIMEOUT = String(config.childIdleTimeout || 0);
|
|
184
205
|
// Track agent depth for child-policy enforcement
|
|
185
206
|
const currentDepth = parseInt(process.env.AGENT_DEPTH || "0", 10);
|
|
186
207
|
childEnv.AGENT_DEPTH = String(currentDepth + 1);
|
|
@@ -201,35 +222,25 @@ export function createAgentHandlers(store) {
|
|
|
201
222
|
|
|
202
223
|
let child;
|
|
203
224
|
if (visible && isWin) {
|
|
204
|
-
//
|
|
205
|
-
//
|
|
225
|
+
// A temp .bat opened in a new window with `start`, so the child has a
|
|
226
|
+
// console of its own. The environment (API key, pairing secret) goes
|
|
227
|
+
// to cmd.exe, and `start` hands it to the new window. It used to be
|
|
228
|
+
// written into the .bat as `set` lines, which left the API key in
|
|
229
|
+
// plain text in %TEMP% for good, while Flint stores keys encrypted
|
|
230
|
+
// everywhere else (found 2026-10-02).
|
|
206
231
|
const os = await import("node:os");
|
|
207
232
|
const fs = await import("node:fs");
|
|
208
233
|
const batPath = path.join(os.tmpdir(), `flint-agent-${assignedPort}.bat`);
|
|
209
234
|
const batLines = [
|
|
210
235
|
"@echo off",
|
|
211
236
|
`title Flint@${assignedPort}`,
|
|
212
|
-
`
|
|
213
|
-
|
|
214
|
-
`set "AGENT_PARENT_PORT=${config.port}"`,
|
|
215
|
-
`set "AGENT_DEPTH=${childEnv.AGENT_DEPTH}"`,
|
|
216
|
-
`set "FLINT_PROVIDER=${config.provider}"`,
|
|
217
|
-
`set "FLINT_DATA_DIR=${childEnv.FLINT_DATA_DIR}"`,
|
|
237
|
+
`cd /d "${config.projectRoot}"`,
|
|
238
|
+
cmd,
|
|
218
239
|
];
|
|
219
|
-
// Pass the correct API key env var in bat file
|
|
220
|
-
if (config.apiKey) {
|
|
221
|
-
if (config.provider === "openrouter") batLines.push(`set "OPENROUTER_API_KEY=${config.apiKey}"`);
|
|
222
|
-
else if (config.provider === "openai") batLines.push(`set "OPENAI_API_KEY=${config.apiKey}"`);
|
|
223
|
-
else if (config.provider === "anthropic") batLines.push(`set "ANTHROPIC_API_KEY=${config.apiKey}"`);
|
|
224
|
-
else batLines.push(`set "OPENROUTER_API_KEY=${config.apiKey}"`);
|
|
225
|
-
}
|
|
226
|
-
if (childEnv.AGENT_DENIED_PATHS) batLines.push(`set "AGENT_DENIED_PATHS=${childEnv.AGENT_DENIED_PATHS}"`);
|
|
227
|
-
if (task_id) batLines.push(`set "AGENT_TASK_ID=${task_id}"`);
|
|
228
|
-
batLines.push(`cd /d "${config.projectRoot}"`);
|
|
229
|
-
batLines.push(cmd);
|
|
230
240
|
fs.writeFileSync(batPath, batLines.join("\r\n") + "\r\n");
|
|
231
241
|
|
|
232
242
|
child = spawn("cmd.exe", ["/c", "start", `"Flint@${assignedPort}"`, batPath], {
|
|
243
|
+
env: childEnv,
|
|
233
244
|
stdio: "ignore",
|
|
234
245
|
detached: true,
|
|
235
246
|
windowsHide: true,
|
|
@@ -297,7 +308,13 @@ export function createAgentHandlers(store) {
|
|
|
297
308
|
port: assignedPort,
|
|
298
309
|
kill: () => {
|
|
299
310
|
try {
|
|
300
|
-
|
|
311
|
+
// A visible agent's own pid comes from its /status; child.pid is
|
|
312
|
+
// the window launcher, long gone.
|
|
313
|
+
const agentPid = _childAgents.get(assignedPort)?.agentPid;
|
|
314
|
+
if (visible && agentPid) {
|
|
315
|
+
if (process.platform === "win32") spawn("taskkill", ["/PID", String(agentPid), "/T", "/F"], { stdio: "ignore" });
|
|
316
|
+
else process.kill(agentPid, "SIGTERM");
|
|
317
|
+
} else if (process.platform === "win32" && child.pid) {
|
|
301
318
|
spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
302
319
|
} else {
|
|
303
320
|
child.kill("SIGTERM");
|
|
@@ -305,6 +322,7 @@ export function createAgentHandlers(store) {
|
|
|
305
322
|
} catch {}
|
|
306
323
|
},
|
|
307
324
|
});
|
|
325
|
+
_childAgents.get(assignedPort).taskRegId = taskRegId;
|
|
308
326
|
|
|
309
327
|
// Start heartbeat monitoring
|
|
310
328
|
startChildHeartbeat(store);
|
|
@@ -325,6 +343,14 @@ export function createAgentHandlers(store) {
|
|
|
325
343
|
}
|
|
326
344
|
|
|
327
345
|
child.on("close", (code) => {
|
|
346
|
+
// A visible agent runs in a window that `start` (Windows), tmux,
|
|
347
|
+
// screen or a terminal emulator opened; the process spawned here only
|
|
348
|
+
// opens it and exits at once with 0. Taking that for the agent's exit
|
|
349
|
+
// marked a live agent stopped and forgot it 5 s later, so ask_agent
|
|
350
|
+
// answered "has stopped" and the model spawned a new agent for every
|
|
351
|
+
// question (owner, 2026-10-02). The heartbeat decides when a visible
|
|
352
|
+
// agent is gone; only a launcher that failed counts here.
|
|
353
|
+
if (visible && code === 0) return;
|
|
328
354
|
activeChildren.delete(procId);
|
|
329
355
|
store.getState().unregisterTask(taskRegId);
|
|
330
356
|
store.getState().finishProcess(procId, code);
|
|
@@ -354,7 +380,10 @@ export function createAgentHandlers(store) {
|
|
|
354
380
|
});
|
|
355
381
|
if (res.ok) {
|
|
356
382
|
const agent = _childAgents.get(assignedPort);
|
|
357
|
-
if (agent)
|
|
383
|
+
if (agent) {
|
|
384
|
+
agent.status = "running";
|
|
385
|
+
await notePid(agent, res);
|
|
386
|
+
}
|
|
358
387
|
// Send the task (authenticated with pairing secret)
|
|
359
388
|
const taskRes = await fetch(apiUrl(assignedPort, "/message"), {
|
|
360
389
|
method: "POST",
|
|
@@ -385,7 +414,7 @@ export function createAgentHandlers(store) {
|
|
|
385
414
|
async ask_agent({ port, message }) {
|
|
386
415
|
const agent = _childAgents.get(port);
|
|
387
416
|
if (!agent) return `No agent on port ${port}. Use list_agents() to see available agents.`;
|
|
388
|
-
if (agent.status === "stopped") return `Agent@${port} has stopped. Spawn a new one.`;
|
|
417
|
+
if (agent.status === "stopped" || agent.status === "lost") return `Agent@${port} has stopped. Spawn a new one.`;
|
|
389
418
|
|
|
390
419
|
// Wait for agent to be ready if it's still starting
|
|
391
420
|
if (agent.status === "starting") {
|
|
@@ -393,7 +422,7 @@ export function createAgentHandlers(store) {
|
|
|
393
422
|
await new Promise((r) => setTimeout(r, 1000));
|
|
394
423
|
try {
|
|
395
424
|
const check = await fetch(apiUrl(port, "/status"), { signal: AbortSignal.timeout(2000) });
|
|
396
|
-
if (check.ok) { agent.status = "running"; break; }
|
|
425
|
+
if (check.ok) { agent.status = "running"; await notePid(agent, check); break; }
|
|
397
426
|
} catch {}
|
|
398
427
|
}
|
|
399
428
|
if (agent.status !== "running") return `Agent@${port} failed to start within 30s.`;
|