@rynx-ai/runtime 0.1.11-beta.6 → 0.1.11-beta.8
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/dist/runner/child.js +5 -1
- package/dist/runner/manager.d.ts +12 -0
- package/dist/runner/manager.js +84 -6
- package/dist/terminal/tmux.js +24 -11
- package/package.json +3 -3
package/dist/runner/child.js
CHANGED
|
@@ -7,7 +7,11 @@ function normalizeTraexPane(pane) {
|
|
|
7
7
|
return pane.toLowerCase().replace(/\s+/g, " ").trim();
|
|
8
8
|
}
|
|
9
9
|
function isTerminalProtocolResponse(input) {
|
|
10
|
-
|
|
10
|
+
// xterm answers terminal queries through the same onData channel as real
|
|
11
|
+
// keystrokes. CSI carries device/focus/position reports; OSC carries color
|
|
12
|
+
// query replies such as `OSC 10;rgb:... ST` and `OSC 11;rgb:... ST`.
|
|
13
|
+
// Neither is evidence that the user has taken over startup prompt handling.
|
|
14
|
+
return /^(?:\x1b\[(?:(?:\?|>)[0-9;]*c|[0-9;]*(?:n|R|t)|[IO])|\x1b\][0-9]+;[^\x07\x1b]*(?:\x07|\x1b\\))+$/.test(input);
|
|
11
15
|
}
|
|
12
16
|
export class RunnerSession {
|
|
13
17
|
transport;
|
package/dist/runner/manager.d.ts
CHANGED
|
@@ -43,6 +43,9 @@ export interface RunnerManagerOptions {
|
|
|
43
43
|
runnerEntry?: string;
|
|
44
44
|
/** Idle TTL (ms) after which an unused runner is reaped. */
|
|
45
45
|
idleTtlMs?: number;
|
|
46
|
+
/** Maximum time without user/control activity that an active response may
|
|
47
|
+
* protect a detached runner from idle reaping. */
|
|
48
|
+
staleActiveTtlMs?: number;
|
|
46
49
|
/** Background reap sweep interval (ms); `0` disables the timer (tests). */
|
|
47
50
|
reapIntervalMs?: number;
|
|
48
51
|
/** Grace period between SIGTERM and SIGKILL during runner shutdown. */
|
|
@@ -136,6 +139,7 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
136
139
|
private readonly sessionStore;
|
|
137
140
|
private readonly runnerEntry;
|
|
138
141
|
private readonly idleTtlMs;
|
|
142
|
+
private readonly staleActiveTtlMs;
|
|
139
143
|
private readonly spawn;
|
|
140
144
|
private readonly childEnv;
|
|
141
145
|
private readonly sessionContextProvider;
|
|
@@ -312,5 +316,13 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
312
316
|
private failHandle;
|
|
313
317
|
private terminateHandle;
|
|
314
318
|
private reapIdle;
|
|
319
|
+
/** Close server-side runtime state before fencing a Provider that stayed
|
|
320
|
+
* active past its hard inactivity deadline. The child transport is closed by
|
|
321
|
+
* terminateHandle immediately afterwards, so these are the final events for
|
|
322
|
+
* the abandoned responses. */
|
|
323
|
+
private failStaleActiveResponses;
|
|
324
|
+
/** Track only response lifecycle, not output volume. Output deltas from a
|
|
325
|
+
* runaway TUI/Provider must not refresh the stale-active deadline. */
|
|
326
|
+
private observeHandleRuntimeEvent;
|
|
315
327
|
private openSessionContext;
|
|
316
328
|
}
|
package/dist/runner/manager.js
CHANGED
|
@@ -44,6 +44,12 @@ const DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS = 30_000;
|
|
|
44
44
|
/** Retry transient terminal-server cleanup without spinning forever. */
|
|
45
45
|
const DEFAULT_TERMINAL_INPUT_CLEANUP_RETRY_MS = 1_000;
|
|
46
46
|
const MAX_TERMINAL_INPUT_CLEANUP_RETRY_MS = 30_000;
|
|
47
|
+
/** A Provider that never closes its active response must not keep a detached
|
|
48
|
+
* runner + tmux server alive forever. This is intentionally much longer than
|
|
49
|
+
* the normal idle TTL so legitimate long-running turns are not treated as
|
|
50
|
+
* stale. User input/control activity refreshes the deadline; Provider output
|
|
51
|
+
* does not, because a runaway redraw loop is the failure mode this bounds. */
|
|
52
|
+
const DEFAULT_STALE_ACTIVE_TTL_MS = 60 * 60_000;
|
|
47
53
|
/** Keep per-Session spawn context small enough to remain an environment handoff,
|
|
48
54
|
* not an unbounded transport. */
|
|
49
55
|
const MAX_SESSION_CONTEXT_ENV_ENTRIES = 32;
|
|
@@ -144,6 +150,7 @@ export class RunnerManager {
|
|
|
144
150
|
sessionStore;
|
|
145
151
|
runnerEntry;
|
|
146
152
|
idleTtlMs;
|
|
153
|
+
staleActiveTtlMs;
|
|
147
154
|
spawn;
|
|
148
155
|
childEnv;
|
|
149
156
|
sessionContextProvider;
|
|
@@ -198,6 +205,7 @@ export class RunnerManager {
|
|
|
198
205
|
this.sessionStore = opts.sessionStore;
|
|
199
206
|
this.runnerEntry = opts.runnerEntry ?? defaultRunnerEntry();
|
|
200
207
|
this.idleTtlMs = opts.idleTtlMs ?? 300_000;
|
|
208
|
+
this.staleActiveTtlMs = Math.max(this.idleTtlMs, opts.staleActiveTtlMs ?? DEFAULT_STALE_ACTIVE_TTL_MS);
|
|
201
209
|
this.spawn = opts.spawn ?? nodeSpawn;
|
|
202
210
|
this.childEnv = opts.childEnv ?? {};
|
|
203
211
|
this.sessionContextProvider = opts.sessionContextProvider;
|
|
@@ -261,6 +269,8 @@ export class RunnerManager {
|
|
|
261
269
|
pendingEscape: "",
|
|
262
270
|
};
|
|
263
271
|
const terminal = new ManagedTerminal(attachId, (msg) => {
|
|
272
|
+
if (msg.t === "term.input")
|
|
273
|
+
handle.lastUsedAt = this.now();
|
|
264
274
|
const handoffs = msg.t === "term.input" && opts.role === "owner"
|
|
265
275
|
? this.beginTerminalInputHandoffs(handle, this.currentTerminalSessionId(handle, localThreadId), trackedTerminalSubmissions(inputTracker, Buffer.from(msg.dataB64, "base64").toString("utf8")))
|
|
266
276
|
: [];
|
|
@@ -1011,6 +1021,7 @@ export class RunnerManager {
|
|
|
1011
1021
|
if (handle.dead)
|
|
1012
1022
|
return;
|
|
1013
1023
|
if (message.t === "mirror") {
|
|
1024
|
+
this.observeHandleRuntimeEvent(handle, message.event);
|
|
1014
1025
|
this.mirrorListener?.(message.sessionId, message.event);
|
|
1015
1026
|
if (message.event.type === "response.created") {
|
|
1016
1027
|
this.settleTerminalInputHandoff(handle, message.sessionId, "turn");
|
|
@@ -1078,6 +1089,7 @@ export class RunnerManager {
|
|
|
1078
1089
|
this.settleTerminalRotationHandoff(handle, message.from, message.to);
|
|
1079
1090
|
if (handle.dead)
|
|
1080
1091
|
return;
|
|
1092
|
+
handle.activeResponseIds.clear();
|
|
1081
1093
|
this.handles.set(message.to, handle);
|
|
1082
1094
|
this.liveSessionKeys.add(message.to);
|
|
1083
1095
|
this.liveOptions.set(message.to, {
|
|
@@ -1144,6 +1156,7 @@ export class RunnerManager {
|
|
|
1144
1156
|
transport,
|
|
1145
1157
|
stderr: [],
|
|
1146
1158
|
lastUsedAt: this.now(),
|
|
1159
|
+
activeResponseIds: new Set(),
|
|
1147
1160
|
dead: false,
|
|
1148
1161
|
completion,
|
|
1149
1162
|
terminalCleanupRetryFailures: 0,
|
|
@@ -1210,6 +1223,7 @@ export class RunnerManager {
|
|
|
1210
1223
|
return;
|
|
1211
1224
|
if (this.bufferForkTargetMessage(handle, msg))
|
|
1212
1225
|
return;
|
|
1226
|
+
this.observeHandleRuntimeEvent(handle, msg.event);
|
|
1213
1227
|
this.mirrorListener?.(msg.sessionId, msg.event);
|
|
1214
1228
|
if (msg.event.type === "response.created") {
|
|
1215
1229
|
// The listener projects response.created into SessionRuntimeIndex
|
|
@@ -1331,22 +1345,86 @@ export class RunnerManager {
|
|
|
1331
1345
|
reapIdle() {
|
|
1332
1346
|
const now = this.now();
|
|
1333
1347
|
for (const [key, handle] of this.handles) {
|
|
1334
|
-
if (handle.caps.size > 0 || handle.terminals.size > 0) {
|
|
1348
|
+
if (handle.caps.size > 0 || handle.terminals.size > 0 || handle.live.size > 0) {
|
|
1335
1349
|
continue;
|
|
1336
1350
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (this.liveSessionKeys.has(key)) {
|
|
1351
|
+
const idleForMs = now - handle.lastUsedAt;
|
|
1352
|
+
if (idleForMs < this.idleTtlMs) {
|
|
1340
1353
|
continue;
|
|
1341
1354
|
}
|
|
1342
|
-
|
|
1355
|
+
const hasActiveResponse = handle.activeResponseIds.size > 0;
|
|
1356
|
+
if (hasActiveResponse && idleForMs < this.staleActiveTtlMs) {
|
|
1343
1357
|
continue;
|
|
1344
1358
|
}
|
|
1345
|
-
|
|
1359
|
+
const reason = hasActiveResponse
|
|
1360
|
+
? "stale active runner reaped"
|
|
1361
|
+
: this.liveSessionKeys.has(key)
|
|
1362
|
+
? "idle live runner reaped"
|
|
1363
|
+
: "idle runner reaped";
|
|
1364
|
+
if (hasActiveResponse) {
|
|
1365
|
+
this.failStaleActiveResponses(handle, idleForMs);
|
|
1366
|
+
}
|
|
1367
|
+
void this.terminateHandle(handle, reason).catch((error) => {
|
|
1346
1368
|
logTerminationFailure(handle, error);
|
|
1347
1369
|
});
|
|
1348
1370
|
}
|
|
1349
1371
|
}
|
|
1372
|
+
/** Close server-side runtime state before fencing a Provider that stayed
|
|
1373
|
+
* active past its hard inactivity deadline. The child transport is closed by
|
|
1374
|
+
* terminateHandle immediately afterwards, so these are the final events for
|
|
1375
|
+
* the abandoned responses. */
|
|
1376
|
+
failStaleActiveResponses(handle, idleForMs) {
|
|
1377
|
+
const responseIds = [...handle.activeResponseIds];
|
|
1378
|
+
handle.activeResponseIds.clear();
|
|
1379
|
+
const sessionId = this.currentTerminalSessionId(handle, handle.key);
|
|
1380
|
+
for (const responseId of responseIds) {
|
|
1381
|
+
this.mirrorListener?.(sessionId, {
|
|
1382
|
+
type: "response.failed",
|
|
1383
|
+
responseId,
|
|
1384
|
+
error: {
|
|
1385
|
+
source: "execution",
|
|
1386
|
+
code: "stale_runner_reaped",
|
|
1387
|
+
message: `Runner was reaped after ${Math.floor(idleForMs / 1000)}s without user activity`,
|
|
1388
|
+
},
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/** Track only response lifecycle, not output volume. Output deltas from a
|
|
1393
|
+
* runaway TUI/Provider must not refresh the stale-active deadline. */
|
|
1394
|
+
observeHandleRuntimeEvent(handle, event) {
|
|
1395
|
+
switch (event.type) {
|
|
1396
|
+
case "response.created":
|
|
1397
|
+
case "response.output_text.delta":
|
|
1398
|
+
case "response.reasoning_summary_text.delta":
|
|
1399
|
+
case "response.function_call_output.delta":
|
|
1400
|
+
case "response.output_item.done":
|
|
1401
|
+
handle.activeResponseIds.add(event.responseId);
|
|
1402
|
+
return;
|
|
1403
|
+
case "session.interaction.requested":
|
|
1404
|
+
handle.activeResponseIds.add(event.responseId);
|
|
1405
|
+
return;
|
|
1406
|
+
case "response.completed":
|
|
1407
|
+
case "response.failed":
|
|
1408
|
+
handle.activeResponseIds.delete(event.responseId);
|
|
1409
|
+
return;
|
|
1410
|
+
case "session.status":
|
|
1411
|
+
if (event.status === "running" && event.responseId) {
|
|
1412
|
+
handle.activeResponseIds.add(event.responseId);
|
|
1413
|
+
}
|
|
1414
|
+
else if (event.status !== "running") {
|
|
1415
|
+
if (event.responseId)
|
|
1416
|
+
handle.activeResponseIds.delete(event.responseId);
|
|
1417
|
+
else
|
|
1418
|
+
handle.activeResponseIds.clear();
|
|
1419
|
+
}
|
|
1420
|
+
return;
|
|
1421
|
+
case "session.rotated":
|
|
1422
|
+
handle.activeResponseIds.clear();
|
|
1423
|
+
return;
|
|
1424
|
+
default:
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1350
1428
|
openSessionContext(sessionId) {
|
|
1351
1429
|
if (!this.sessionContextProvider)
|
|
1352
1430
|
return undefined;
|
package/dist/terminal/tmux.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { execFile, execFileSync } from "node:child_process";
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
19
|
+
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
const TMUX_TARGET = "main";
|
|
@@ -54,10 +54,28 @@ export function terminateTmuxServer(name, tmuxBin = "tmux") {
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
catch (error) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// Only a real tmux exit status proves the server/session is absent. Spawn
|
|
58
|
+
// failures (missing/unexecutable tmux) leave liveness unproven and must not
|
|
59
|
+
// unlink a socket that could still belong to a live server.
|
|
60
|
+
if (!error ||
|
|
61
|
+
typeof error !== "object" ||
|
|
62
|
+
!("status" in error) ||
|
|
63
|
+
typeof error.status !== "number") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
// tmux custom-socket servers can exit while leaving the filesystem entry
|
|
68
|
+
// behind. Remove that verified-stale socket so process/socket inventories
|
|
69
|
+
// do not accumulate dead terminals forever.
|
|
70
|
+
unlinkSync(socketPath);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch (unlinkError) {
|
|
74
|
+
return Boolean(unlinkError &&
|
|
75
|
+
typeof unlinkError === "object" &&
|
|
76
|
+
"code" in unlinkError &&
|
|
77
|
+
unlinkError.code === "ENOENT");
|
|
78
|
+
}
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
81
|
/** Is a usable `tmux` on PATH? Cheap probe for the capability gate. */
|
|
@@ -389,12 +407,7 @@ export class TmuxTerminal {
|
|
|
389
407
|
}
|
|
390
408
|
/** Kill the tmux server (ends the session and all attaches). */
|
|
391
409
|
kill() {
|
|
392
|
-
|
|
393
|
-
execFileSync(this.tmuxBin, [...this.base(), "kill-server"], { stdio: "ignore" });
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
// Already gone — nothing to clean up.
|
|
397
|
-
}
|
|
410
|
+
terminateTmuxServer(this.name, this.tmuxBin);
|
|
398
411
|
this.started = false;
|
|
399
412
|
}
|
|
400
413
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/runtime",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.8",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"node-pty": "
|
|
27
|
+
"node-pty": "1.2.0-beta.15",
|
|
28
28
|
"ws": "^8.21.0",
|
|
29
|
-
"@rynx-ai/core": "0.1.11-beta.
|
|
29
|
+
"@rynx-ai/core": "0.1.11-beta.8"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/ws": "^8.18.1"
|