@rynx-ai/runtime 0.1.11-beta.44 → 0.1.11-beta.48
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/claude/native-bridge.js +16 -1
- package/dist/claude/native-integration.js +43 -0
- package/dist/codex-app-server/forwarder.d.ts +4 -0
- package/dist/codex-app-server/forwarder.js +10 -0
- package/dist/codex-app-server/transport.js +4 -0
- package/dist/codex-app-server/ws-channel.js +9 -0
- package/dist/host.js +38 -13
- package/dist/runner/manager.d.ts +4 -17
- package/dist/runner/manager.js +52 -210
- package/package.json +2 -2
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Dependency-light on purpose (node builtins plus erased protocol types) so the
|
|
12
12
|
* standalone hook entrypoints do not pull the whole runtime into every process.
|
|
13
13
|
*/
|
|
14
|
-
import { appendFileSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
14
|
+
import { appendFileSync, closeSync, copyFileSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
15
15
|
import { createHash, randomUUID } from "node:crypto";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { adoptLegacyRuntimeDirectory, legacyRuntimeStateRoot, runtimeSessionDigest, runtimeSessionStateDir, } from "../runtime-state-paths.js";
|
|
@@ -69,6 +69,21 @@ export function prepareClaudeBridgeDir(sessionId) {
|
|
|
69
69
|
const dir = claudeBridgeDir(sessionId);
|
|
70
70
|
adoptLegacyRuntimeDirectory(join(legacyRuntimeStateRoot(), "claude-native", runtimeSessionDigest(sessionId)), dir);
|
|
71
71
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
72
|
+
// Preserve original bridge logs before the normal runtime refresh clears its
|
|
73
|
+
// rendezvous. Collection itself never calls this function or changes files.
|
|
74
|
+
const history = join(dir, "history", randomUUID());
|
|
75
|
+
for (const file of [HOOKS_FILE, STATE_FILE, DELTAS_FILE, INTERACTIONS_FILE, INTERACTION_ACKS_FILE]) {
|
|
76
|
+
try {
|
|
77
|
+
if (!statSync(join(dir, file)).isFile())
|
|
78
|
+
continue;
|
|
79
|
+
mkdirSync(history, { recursive: true, mode: 0o700 });
|
|
80
|
+
copyFileSync(join(dir, file), join(history, file));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code !== "ENOENT")
|
|
84
|
+
process.stderr.write(`Claude bridge log preservation failed: ${String(error)}\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
72
87
|
for (const file of [
|
|
73
88
|
HOOKS_FILE,
|
|
74
89
|
STATE_FILE,
|
|
@@ -2407,6 +2407,28 @@ const CLAUDE_PASTE_COMMIT_TIMEOUT_MS = 5_000;
|
|
|
2407
2407
|
const CLAUDE_PASTE_SETTLE_MS = 100;
|
|
2408
2408
|
const CLAUDE_SUBMIT_CONFIRM_TIMEOUT_MS = 10_000;
|
|
2409
2409
|
const CLAUDE_SUBMIT_RETRY_MS = 1_000;
|
|
2410
|
+
const CLAUDE_UNKNOWN_COMMAND_WATCH_MS = 3_000;
|
|
2411
|
+
// Watch guessed skills, leaving known native commands on their existing
|
|
2412
|
+
// delivery path. This list does not change which commands may run.
|
|
2413
|
+
const CLAUDE_NATIVE_BUILTIN_COMMANDS = new Set([
|
|
2414
|
+
"add-dir", "agents", "bug", "config", "cost", "doctor", "exit", "fast",
|
|
2415
|
+
"feedback", "help", "hooks", "ide", "login", "logout", "mcp", "memory",
|
|
2416
|
+
"onboarding", "permissions", "plugin", "quiet", "quit", "release-notes",
|
|
2417
|
+
"resume", "save", "status", "terminal-setup", "upgrade", "verbose",
|
|
2418
|
+
"clear", "compact", "effort", "model", "ultrareview", "branch", "fork",
|
|
2419
|
+
]);
|
|
2420
|
+
/** Count rejection messages: omit composer/echo rows and ignore
|
|
2421
|
+
* wrapping, including a narrow pane wrapping inside the command name. */
|
|
2422
|
+
function unknownCommandRejections(pane, command, glyph) {
|
|
2423
|
+
const collapsed = pane.split(/\r?\n/)
|
|
2424
|
+
.filter((line) => !line.includes(glyph))
|
|
2425
|
+
.join("")
|
|
2426
|
+
.replace(/\s/g, "");
|
|
2427
|
+
// Unlike a bare substring count, keep /foo-bar's rejection from authorizing
|
|
2428
|
+
// a second delivery of /foo. Retain namespaced, path-like and Unicode names.
|
|
2429
|
+
return collapsed.split(`Unknowncommand:/${command}`).slice(1)
|
|
2430
|
+
.filter((suffix) => !/^[\p{L}\p{M}\p{N}_:./-]/u.test(suffix)).length;
|
|
2431
|
+
}
|
|
2410
2432
|
function selectedMenuRow(line, glyph) {
|
|
2411
2433
|
const index = line.indexOf(glyph);
|
|
2412
2434
|
if (index < 0)
|
|
@@ -2494,6 +2516,27 @@ async function pollUntil(pred, timeoutMs, pollMs, now, sleep, signal) {
|
|
|
2494
2516
|
* recorded neither a direct user prompt nor a type-ahead enqueue.
|
|
2495
2517
|
*/
|
|
2496
2518
|
export async function injectViaTerminal(injector, text, opts = {}) {
|
|
2519
|
+
const command = /^\s*\/(\S+)/u.exec(text)?.[1];
|
|
2520
|
+
if (!command || CLAUDE_NATIVE_BUILTIN_COMMANDS.has(command)) {
|
|
2521
|
+
return pasteAndSubmitViaTerminal(injector, text, opts);
|
|
2522
|
+
}
|
|
2523
|
+
const glyph = opts.promptGlyph ?? CLAUDE_PROMPT_GLYPH;
|
|
2524
|
+
const baseline = unknownCommandRejections(injector.capturePane(), command, glyph);
|
|
2525
|
+
if (!await pasteAndSubmitViaTerminal(injector, text, opts))
|
|
2526
|
+
return false;
|
|
2527
|
+
// A fresh, explicit rejection proves Claude dropped this guessed skill.
|
|
2528
|
+
// Stale scrollback and an unacknowledged/slow submission never justify a
|
|
2529
|
+
// second paste. Keep the watch and recovery within the Host's injectLock.
|
|
2530
|
+
const rejected = await pollUntil(() => unknownCommandRejections(injector.capturePane(), command, glyph) > baseline, CLAUDE_UNKNOWN_COMMAND_WATCH_MS, opts.pollMs ?? 150, opts.now ?? (() => Date.now()), opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))), opts.signal);
|
|
2531
|
+
if (opts.signal?.aborted)
|
|
2532
|
+
return false;
|
|
2533
|
+
if (!rejected)
|
|
2534
|
+
return true;
|
|
2535
|
+
// Exactly one recovery, using a zero-width prefix. Reuse the
|
|
2536
|
+
// existing paste/submit recipe without changing newlines or attachments.
|
|
2537
|
+
return pasteAndSubmitViaTerminal(injector, text.replace(/^(\s*)\//u, "$1\ufeff/"), opts);
|
|
2538
|
+
}
|
|
2539
|
+
async function pasteAndSubmitViaTerminal(injector, text, opts) {
|
|
2497
2540
|
const glyph = opts.promptGlyph ?? CLAUDE_PROMPT_GLYPH;
|
|
2498
2541
|
const pollMs = opts.pollMs ?? 150;
|
|
2499
2542
|
const now = opts.now ?? (() => Date.now());
|
|
@@ -49,6 +49,8 @@ export interface CodexForwarderSink {
|
|
|
49
49
|
onRecoveredTurnStatus?(status: "idle" | "failed", turnId: string | undefined, error?: Error): void;
|
|
50
50
|
/** A turn failed on the runtime. */
|
|
51
51
|
onTurnError(error: Error): void;
|
|
52
|
+
/** A steer rejection proves a recorded turn is inactive, without its outcome. */
|
|
53
|
+
onTurnInactive?(turnId: string): void;
|
|
52
54
|
/** The user's turn text (sourced from codex's `userMessage` item), so a
|
|
53
55
|
* co-driving TUI's prompt is recorded even though this process never injected it. */
|
|
54
56
|
onUserMessage?(content: string | UserContentPart[]): void;
|
|
@@ -136,6 +138,8 @@ export declare class CodexSessionForwarder {
|
|
|
136
138
|
isTurnOpen(): boolean;
|
|
137
139
|
/** The current turn's codex id (only meaningful while {@link isTurnOpen}). */
|
|
138
140
|
currentTurnId(): string | null;
|
|
141
|
+
/** Reconcile a proven stale steer without clearing a newer observed turn. */
|
|
142
|
+
clearActiveTurnIfMatches(turnId: string): void;
|
|
139
143
|
/**
|
|
140
144
|
* Record a turn accepted by the injection connection before the independent
|
|
141
145
|
* observer receives `turn/started`. This closes the read-decide-RPC-write race:
|
|
@@ -162,6 +162,16 @@ export class CodexSessionForwarder {
|
|
|
162
162
|
currentTurnId() {
|
|
163
163
|
return this.currentTurnIdValue;
|
|
164
164
|
}
|
|
165
|
+
/** Reconcile a proven stale steer without clearing a newer observed turn. */
|
|
166
|
+
clearActiveTurnIfMatches(turnId) {
|
|
167
|
+
if (this.currentTurnIdValue !== turnId)
|
|
168
|
+
return;
|
|
169
|
+
this.flushPendingCompletion();
|
|
170
|
+
this.flushDeferredAssistantMessage();
|
|
171
|
+
this.currentTurnIdValue = null;
|
|
172
|
+
this.turnOpen = false;
|
|
173
|
+
this.sink.onTurnInactive?.(turnId);
|
|
174
|
+
}
|
|
165
175
|
/**
|
|
166
176
|
* Record a turn accepted by the injection connection before the independent
|
|
167
177
|
* observer receives `turn/started`. This closes the read-decide-RPC-write race:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import readline from "node:readline";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { teeDiagnosticStream } from "@rynx-ai/core";
|
|
4
6
|
import { createCodexChildEnv } from "../codex-child-env.js";
|
|
5
7
|
const DEFAULT_SPAWN_ARGS = ["app-server", "--listen", "stdio://"];
|
|
6
8
|
/**
|
|
@@ -51,6 +53,8 @@ export class DefaultCodexAppServerProcessSpawner {
|
|
|
51
53
|
stdio: ["pipe", "pipe", "pipe"],
|
|
52
54
|
env: { ...createCodexChildEnv(process.env), ...this.extraEnv },
|
|
53
55
|
});
|
|
56
|
+
if (process.env.RYNX_RUNTIME_LOG_DIR)
|
|
57
|
+
teeDiagnosticStream(child.stderr, join(process.env.RYNX_RUNTIME_LOG_DIR, "app-server.stderr.log"));
|
|
54
58
|
if (!child.stdin || !child.stdout) {
|
|
55
59
|
throw new Error("Codex app-server child process is missing stdio handles");
|
|
56
60
|
}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
16
|
import { createServer } from "node:net";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { diagnosticEvents, teeDiagnosticStream } from "@rynx-ai/core";
|
|
17
19
|
import { WebSocket } from "ws";
|
|
18
20
|
import { createCodexChildEnv } from "../codex-child-env.js";
|
|
19
21
|
import { newRuntimeProcessTag, reconcileRuntimeProcesses, registerRuntimeProcess, runtimeProcessArgv0, runtimeProcessOwnerIdentity, unregisterRuntimeProcess, withRuntimeProcessStateArg, } from "./process-registry.js";
|
|
@@ -76,6 +78,13 @@ export class WsRpcChannel {
|
|
|
76
78
|
...(processTag ? { argv0: runtimeProcessArgv0(this.opts.cliPath, processTag) } : {}),
|
|
77
79
|
});
|
|
78
80
|
const childPid = this.child.pid;
|
|
81
|
+
if (process.env.RYNX_RUNTIME_LOG_DIR) {
|
|
82
|
+
const logDir = process.env.RYNX_RUNTIME_LOG_DIR;
|
|
83
|
+
teeDiagnosticStream(this.child.stderr, join(logDir, "app-server.stderr.log"));
|
|
84
|
+
const event = diagnosticEvents(join(logDir, "process-events.jsonl"));
|
|
85
|
+
event("app-server.spawn", { pid: childPid });
|
|
86
|
+
this.child.on("exit", (code, signal) => event("app-server.exit", { code, signal }));
|
|
87
|
+
}
|
|
79
88
|
if (processTag && this.opts.stateDir && childPid) {
|
|
80
89
|
registerRuntimeProcess({
|
|
81
90
|
pid: childPid,
|
package/dist/host.js
CHANGED
|
@@ -1299,6 +1299,16 @@ export class LocalAgentHost {
|
|
|
1299
1299
|
// transition.
|
|
1300
1300
|
onTurnEnd: completeCurrentTurn,
|
|
1301
1301
|
onTurnInterrupted: interruptCurrentTurn,
|
|
1302
|
+
onTurnInactive: (turnId) => {
|
|
1303
|
+
const responseId = `resp_codex_${turnId}`;
|
|
1304
|
+
if (currentResponseId === responseId) {
|
|
1305
|
+
closeCanonicalInteractions();
|
|
1306
|
+
normalizer = null;
|
|
1307
|
+
currentResponseId = null;
|
|
1308
|
+
}
|
|
1309
|
+
clearPendingInputsForResponse(responseId);
|
|
1310
|
+
emitCurrent({ type: "session.status", sessionId: currentSessionId, responseId, status: "idle" });
|
|
1311
|
+
},
|
|
1302
1312
|
onRecoveredTurnStatus: (status, turnId, error) => {
|
|
1303
1313
|
const responseId = turnId ? `resp_codex_${turnId}` : undefined;
|
|
1304
1314
|
if (normalizer && (!responseId || currentResponseId === responseId)) {
|
|
@@ -1901,20 +1911,35 @@ export class LocalAgentHost {
|
|
|
1901
1911
|
const turnId = live.forwarder.currentTurnId();
|
|
1902
1912
|
if (turnId) {
|
|
1903
1913
|
injectionMethod = "turn/steer";
|
|
1904
|
-
const
|
|
1905
|
-
threadId,
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1914
|
+
const steer = async (expectedTurnId) => {
|
|
1915
|
+
const steered = await injectionClient.turnSteer({ threadId, expectedTurnId, input: nativeInput });
|
|
1916
|
+
pendingInput.responseId = `resp_codex_${steered.turnId}`;
|
|
1917
|
+
if (pendingInput.observed)
|
|
1918
|
+
forgetPendingInput();
|
|
1919
|
+
else
|
|
1920
|
+
pendingInput.state = "accepted";
|
|
1921
|
+
live.forwarder.noteTurnAccepted(steered.turnId);
|
|
1922
|
+
return { outcome: "steered", responseId: pendingInput.responseId };
|
|
1923
|
+
};
|
|
1924
|
+
try {
|
|
1925
|
+
return await steer(turnId);
|
|
1926
|
+
}
|
|
1927
|
+
catch (error) {
|
|
1928
|
+
if (!(error instanceof CodexTransportError) || error.code !== -32600 ||
|
|
1929
|
+
error.message.trim().toLowerCase() !== "no active turn to steer")
|
|
1930
|
+
throw error;
|
|
1931
|
+
// Only this explicit rejection proves that resubmitting cannot
|
|
1932
|
+
// duplicate the input. A timeout or unrelated error never retries.
|
|
1933
|
+
if (live.rotationPending || live.stopped || live.threadId !== threadId)
|
|
1934
|
+
throw error;
|
|
1935
|
+
live.forwarder.clearActiveTurnIfMatches(turnId);
|
|
1936
|
+
}
|
|
1937
|
+
const currentTurnId = live.forwarder.currentTurnId();
|
|
1938
|
+
if (currentTurnId)
|
|
1939
|
+
return await steer(currentTurnId);
|
|
1916
1940
|
}
|
|
1917
1941
|
}
|
|
1942
|
+
injectionMethod = "turn/start";
|
|
1918
1943
|
// Match reference implementation's turn boundary: change the native thread settings
|
|
1919
1944
|
// under the same lock immediately before starting the next Turn. Never
|
|
1920
1945
|
// put settings on turn/start or mutate a Turn that is already open.
|
|
@@ -3187,7 +3212,7 @@ export class LocalAgentHost {
|
|
|
3187
3212
|
}
|
|
3188
3213
|
catch (error) {
|
|
3189
3214
|
live.error = error instanceof Error ? error.message : String(error);
|
|
3190
|
-
|
|
3215
|
+
throw new CodexRuntimeError(`Claude message injection via terminal failed: ${live.error}`, 503, "native_message_injection_failed");
|
|
3191
3216
|
}
|
|
3192
3217
|
finally {
|
|
3193
3218
|
if (live.injectAbort === abort)
|
package/dist/runner/manager.d.ts
CHANGED
|
@@ -64,9 +64,6 @@ export interface RunnerManagerOptions {
|
|
|
64
64
|
/** Max wait for a native interrupt acknowledgement before reporting it
|
|
65
65
|
* unproven. Callers may then use the explicit force-stop path. */
|
|
66
66
|
liveInterruptTimeoutMs?: number;
|
|
67
|
-
/** Max wait for an accepted owner TUI submission to become observable as a
|
|
68
|
-
* mirrored response or a published native rotation. */
|
|
69
|
-
terminalInputHandoffTimeoutMs?: number;
|
|
70
67
|
/** Injected for tests. */
|
|
71
68
|
spawn?: typeof nodeSpawn;
|
|
72
69
|
/** Injected for tests. Defaults to signaling the whole POSIX process group
|
|
@@ -188,7 +185,6 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
188
185
|
private readonly liveReadyTimeoutMs;
|
|
189
186
|
private readonly nativeLiveStartTimeoutMs;
|
|
190
187
|
private readonly liveInterruptTimeoutMs;
|
|
191
|
-
private readonly terminalInputHandoffTimeoutMs;
|
|
192
188
|
private reapPromise;
|
|
193
189
|
private stopping;
|
|
194
190
|
private stopPromise;
|
|
@@ -224,9 +220,6 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
224
220
|
private readonly forkOperations;
|
|
225
221
|
private readonly forkBufferedMessages;
|
|
226
222
|
private readonly forkBufferedTerminalInputs;
|
|
227
|
-
/** Owner TUI submissions accepted by the parent but not yet represented by a
|
|
228
|
-
* mirrored response or a durably published native rotation. */
|
|
229
|
-
private readonly terminalInputHandoffs;
|
|
230
223
|
constructor(opts: RunnerManagerOptions);
|
|
231
224
|
/**
|
|
232
225
|
* Open a live terminal on the session's runner child (spawning it if needed).
|
|
@@ -256,17 +249,11 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
256
249
|
/** Register the sink for session rotations (claude `/clear`·`/fork`): the server
|
|
257
250
|
* records the new session's meta (carry-over agent/model/title). */
|
|
258
251
|
onRotate(listener: (rotation: RotateInfo) => void | Promise<void>): void;
|
|
259
|
-
/**
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
private
|
|
263
|
-
private settleTerminalInputHandoff;
|
|
264
|
-
private settleTerminalRotationHandoff;
|
|
252
|
+
/** Actual transport writes and ownership transfers, independent of turn lifecycle. */
|
|
253
|
+
pendingNativeOperationCount(): number;
|
|
254
|
+
private finishTerminalWrite;
|
|
255
|
+
private finishTerminalWrites;
|
|
265
256
|
private currentTerminalSessionId;
|
|
266
|
-
private finishTerminalInputHandoff;
|
|
267
|
-
private finishAllTerminalInputHandoffs;
|
|
268
|
-
private preserveTerminalInputHandoffsAsActivity;
|
|
269
|
-
private expireTerminalInputHandoffs;
|
|
270
257
|
/**
|
|
271
258
|
* Eagerly bring up a session's codex-native live view (persistent forwarder +
|
|
272
259
|
* detached `codex --remote` TUI) in its runner child, spawning the runner if
|
package/dist/runner/manager.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import readline from "node:readline";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { diagnosticEvents, sessionDiagnosticDir, teeDiagnosticStream } from "@rynx-ai/core";
|
|
21
23
|
import { fileURLToPath } from "node:url";
|
|
22
24
|
import { AgentRuntimeError, } from "@rynx-ai/core";
|
|
23
25
|
import { listRuntimeModels } from "../models-catalog.js";
|
|
@@ -44,10 +46,6 @@ const DEFAULT_LIVE_READY_TIMEOUT_MS = 25_000;
|
|
|
44
46
|
* deadline. Codex-lineage startup instead acknowledges pane/observer startup
|
|
45
47
|
* and lets thread discovery race injection. */
|
|
46
48
|
const DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS = 60_000;
|
|
47
|
-
/** A submitted TUI command should become a mirrored turn or native rotation
|
|
48
|
-
* quickly. If it does not, the runner is fenced by a verified process-tree
|
|
49
|
-
* shutdown before maintenance may treat the submission as settled. */
|
|
50
|
-
const DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS = 30_000;
|
|
51
49
|
/** Full idle window before an inactive native pane becomes reapable. */
|
|
52
50
|
const DEFAULT_NATIVE_PANE_IDLE_TTL_MS = 60 * 60_000;
|
|
53
51
|
/** tmux output this recent independently proves that a native pane is busy. */
|
|
@@ -291,7 +289,6 @@ export class RunnerManager {
|
|
|
291
289
|
liveReadyTimeoutMs;
|
|
292
290
|
nativeLiveStartTimeoutMs;
|
|
293
291
|
liveInterruptTimeoutMs;
|
|
294
|
-
terminalInputHandoffTimeoutMs;
|
|
295
292
|
reapPromise = null;
|
|
296
293
|
stopping = false;
|
|
297
294
|
stopPromise;
|
|
@@ -327,9 +324,6 @@ export class RunnerManager {
|
|
|
327
324
|
forkOperations = new Map();
|
|
328
325
|
forkBufferedMessages = new Map();
|
|
329
326
|
forkBufferedTerminalInputs = new Map();
|
|
330
|
-
/** Owner TUI submissions accepted by the parent but not yet represented by a
|
|
331
|
-
* mirrored response or a durably published native rotation. */
|
|
332
|
-
terminalInputHandoffs = new Map();
|
|
333
327
|
constructor(opts) {
|
|
334
328
|
this.config = opts.config;
|
|
335
329
|
this.sessionStore = opts.sessionStore;
|
|
@@ -358,7 +352,6 @@ export class RunnerManager {
|
|
|
358
352
|
this.liveReadyTimeoutMs = Math.max(1, opts.liveReadyTimeoutMs ?? DEFAULT_LIVE_READY_TIMEOUT_MS);
|
|
359
353
|
this.nativeLiveStartTimeoutMs = Math.max(1, opts.nativeLiveStartTimeoutMs ?? DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS);
|
|
360
354
|
this.liveInterruptTimeoutMs = Math.max(1, opts.liveInterruptTimeoutMs ?? 10_000);
|
|
361
|
-
this.terminalInputHandoffTimeoutMs = Math.max(1, opts.terminalInputHandoffTimeoutMs ?? DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS);
|
|
362
355
|
const reapIntervalMs = opts.reapIntervalMs ?? 60_000;
|
|
363
356
|
if (reapIntervalMs > 0) {
|
|
364
357
|
this.reapTimer = setInterval(() => void this.reapIdle(), reapIntervalMs);
|
|
@@ -398,26 +391,22 @@ export class RunnerManager {
|
|
|
398
391
|
openTerminalOnHandle(handle, localThreadId, opts) {
|
|
399
392
|
handle.lastUsedAt = this.now();
|
|
400
393
|
const attachId = randomUUID();
|
|
401
|
-
const inputTracker = {
|
|
402
|
-
buffer: "",
|
|
403
|
-
previousWasCarriageReturn: false,
|
|
404
|
-
bracketedPaste: false,
|
|
405
|
-
pendingEscape: "",
|
|
406
|
-
};
|
|
407
394
|
const terminal = new ManagedTerminal(attachId, (msg) => {
|
|
408
395
|
if (msg.t === "term.input")
|
|
409
396
|
handle.lastUsedAt = this.now();
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
397
|
+
if (msg.t === "term.input" && opts.role === "owner") {
|
|
398
|
+
handle.terminalWrites.set(msg.reqId, {
|
|
399
|
+
attachId,
|
|
400
|
+
reservation: this.reserveAdmission(),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
413
403
|
const send = () => {
|
|
414
404
|
try {
|
|
415
405
|
handle.transport.send(msg);
|
|
416
406
|
}
|
|
417
407
|
catch (error) {
|
|
418
|
-
|
|
419
|
-
this.
|
|
420
|
-
}
|
|
408
|
+
if (msg.t === "term.input")
|
|
409
|
+
this.finishTerminalWrite(handle, msg.reqId);
|
|
421
410
|
throw error;
|
|
422
411
|
}
|
|
423
412
|
};
|
|
@@ -489,78 +478,29 @@ export class RunnerManager {
|
|
|
489
478
|
onRotate(listener) {
|
|
490
479
|
this.rotateListener = listener;
|
|
491
480
|
}
|
|
492
|
-
/**
|
|
493
|
-
|
|
494
|
-
pendingTerminalInputCount() {
|
|
481
|
+
/** Actual transport writes and ownership transfers, independent of turn lifecycle. */
|
|
482
|
+
pendingNativeOperationCount() {
|
|
495
483
|
let count = 0;
|
|
496
|
-
for (const
|
|
497
|
-
count +=
|
|
484
|
+
for (const handle of this.childHandles) {
|
|
485
|
+
count += handle.terminalWrites.size;
|
|
486
|
+
if (handle.pendingRotation)
|
|
487
|
+
count += 1;
|
|
488
|
+
if (handle.dead)
|
|
489
|
+
count += 1;
|
|
498
490
|
}
|
|
499
491
|
return count;
|
|
500
492
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
try {
|
|
506
|
-
for (const kind of kinds) {
|
|
507
|
-
const handoff = {
|
|
508
|
-
sessionId,
|
|
509
|
-
kind,
|
|
510
|
-
reservation: this.reserveAdmission(),
|
|
511
|
-
timer: undefined,
|
|
512
|
-
};
|
|
513
|
-
handoff.timer = setTimeout(() => {
|
|
514
|
-
this.expireTerminalInputHandoffs(handle);
|
|
515
|
-
}, this.terminalInputHandoffTimeoutMs);
|
|
516
|
-
handoff.timer.unref?.();
|
|
517
|
-
const handoffs = this.terminalInputHandoffs.get(handle);
|
|
518
|
-
if (handoffs)
|
|
519
|
-
handoffs.push(handoff);
|
|
520
|
-
else
|
|
521
|
-
this.terminalInputHandoffs.set(handle, [handoff]);
|
|
522
|
-
created.push(handoff);
|
|
523
|
-
}
|
|
524
|
-
return created;
|
|
525
|
-
}
|
|
526
|
-
catch (error) {
|
|
527
|
-
for (const handoff of created) {
|
|
528
|
-
this.finishTerminalInputHandoff(handle, handoff);
|
|
529
|
-
}
|
|
530
|
-
throw error;
|
|
531
|
-
}
|
|
493
|
+
finishTerminalWrite(handle, reqId) {
|
|
494
|
+
const write = handle.terminalWrites.get(reqId);
|
|
495
|
+
handle.terminalWrites.delete(reqId);
|
|
496
|
+
write?.reservation.release();
|
|
532
497
|
}
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
.
|
|
536
|
-
|
|
537
|
-
// Runtime observations settle accepted input in submission order. In
|
|
538
|
-
// particular, a response from the source Session while /clear or /fork is
|
|
539
|
-
// still publishing must not skip that rotation and release a later turn
|
|
540
|
-
// which has not yet been rebound or delivered.
|
|
541
|
-
if (handoff?.kind === kind)
|
|
542
|
-
this.finishTerminalInputHandoff(handle, handoff);
|
|
543
|
-
}
|
|
544
|
-
settleTerminalRotationHandoff(handle, sourceSessionId, targetSessionId) {
|
|
545
|
-
const handoffs = this.terminalInputHandoffs.get(handle);
|
|
546
|
-
const rotationIndex = handoffs?.findIndex((handoff) => handoff.sessionId === sourceSessionId && handoff.kind === "rotation") ?? -1;
|
|
547
|
-
if (!handoffs || rotationIndex < 0)
|
|
548
|
-
return;
|
|
549
|
-
// Turns submitted before /clear or /fork are superseded once the rotation
|
|
550
|
-
// is published. Inputs accepted afterwards belong to the transferred pane
|
|
551
|
-
// and must follow it to the target Session.
|
|
552
|
-
const rotation = handoffs[rotationIndex];
|
|
553
|
-
for (const handoff of [...handoffs.slice(0, rotationIndex)]) {
|
|
554
|
-
if (handoff.sessionId === sourceSessionId && handoff.kind === "turn") {
|
|
555
|
-
this.finishTerminalInputHandoff(handle, handoff);
|
|
498
|
+
finishTerminalWrites(handle, attachId) {
|
|
499
|
+
for (const [reqId, write] of handle.terminalWrites) {
|
|
500
|
+
if (attachId === undefined || write.attachId === attachId) {
|
|
501
|
+
this.finishTerminalWrite(handle, reqId);
|
|
556
502
|
}
|
|
557
503
|
}
|
|
558
|
-
if (rotation)
|
|
559
|
-
this.finishTerminalInputHandoff(handle, rotation);
|
|
560
|
-
for (const handoff of this.terminalInputHandoffs.get(handle) ?? []) {
|
|
561
|
-
if (handoff.sessionId === sourceSessionId)
|
|
562
|
-
handoff.sessionId = targetSessionId;
|
|
563
|
-
}
|
|
564
504
|
}
|
|
565
505
|
currentTerminalSessionId(handle, fallback) {
|
|
566
506
|
for (const [sessionId, candidate] of this.handles) {
|
|
@@ -569,51 +509,6 @@ export class RunnerManager {
|
|
|
569
509
|
}
|
|
570
510
|
return handle.activeSessionId || fallback;
|
|
571
511
|
}
|
|
572
|
-
finishTerminalInputHandoff(handle, handoff) {
|
|
573
|
-
if (handoff.timer) {
|
|
574
|
-
clearTimeout(handoff.timer);
|
|
575
|
-
handoff.timer = undefined;
|
|
576
|
-
}
|
|
577
|
-
handoff.reservation?.release();
|
|
578
|
-
handoff.reservation = undefined;
|
|
579
|
-
const handoffs = this.terminalInputHandoffs.get(handle);
|
|
580
|
-
if (!handoffs)
|
|
581
|
-
return;
|
|
582
|
-
const index = handoffs.indexOf(handoff);
|
|
583
|
-
if (index >= 0)
|
|
584
|
-
handoffs.splice(index, 1);
|
|
585
|
-
if (handoffs.length === 0)
|
|
586
|
-
this.terminalInputHandoffs.delete(handle);
|
|
587
|
-
}
|
|
588
|
-
finishAllTerminalInputHandoffs(handle) {
|
|
589
|
-
for (const handoff of [...(this.terminalInputHandoffs.get(handle) ?? [])]) {
|
|
590
|
-
this.finishTerminalInputHandoff(handle, handoff);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
preserveTerminalInputHandoffsAsActivity(handle) {
|
|
594
|
-
for (const handoff of this.terminalInputHandoffs.get(handle) ?? []) {
|
|
595
|
-
if (handoff.timer) {
|
|
596
|
-
clearTimeout(handoff.timer);
|
|
597
|
-
handoff.timer = undefined;
|
|
598
|
-
}
|
|
599
|
-
handoff.reservation?.release();
|
|
600
|
-
handoff.reservation = undefined;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
expireTerminalInputHandoffs(handle) {
|
|
604
|
-
const handoffs = this.terminalInputHandoffs.get(handle);
|
|
605
|
-
if (!handoffs?.length)
|
|
606
|
-
return;
|
|
607
|
-
for (const handoff of handoffs) {
|
|
608
|
-
if (handoff.timer) {
|
|
609
|
-
clearTimeout(handoff.timer);
|
|
610
|
-
handoff.timer = undefined;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
void this.terminateHandle(handle, "accepted terminal input did not become observable before maintenance timeout").catch((error) => {
|
|
614
|
-
logTerminationFailure(handle, error);
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
512
|
/**
|
|
618
513
|
* Eagerly bring up a session's codex-native live view (persistent forwarder +
|
|
619
514
|
* detached `codex --remote` TUI) in its runner child, spawning the runner if
|
|
@@ -1021,10 +916,7 @@ export class RunnerManager {
|
|
|
1021
916
|
if (this.reapTimer) {
|
|
1022
917
|
clearInterval(this.reapTimer);
|
|
1023
918
|
}
|
|
1024
|
-
const children = new Set(
|
|
1025
|
-
...this.childHandles,
|
|
1026
|
-
...this.terminalInputHandoffs.keys(),
|
|
1027
|
-
]);
|
|
919
|
+
const children = new Set(this.childHandles);
|
|
1028
920
|
const attempt = (async () => {
|
|
1029
921
|
const results = await Promise.allSettled([...children].map((handle) => this.terminateHandle(handle, "runner manager stopped")));
|
|
1030
922
|
this.handles.clear();
|
|
@@ -1052,8 +944,7 @@ export class RunnerManager {
|
|
|
1052
944
|
// ── internals ──────────────────────────────────────────────────────────────
|
|
1053
945
|
handleForCleanup(localThreadId) {
|
|
1054
946
|
return this.handles.get(localThreadId) ??
|
|
1055
|
-
[...this.
|
|
1056
|
-
handoffs.some((handoff) => handoff.sessionId === localThreadId))?.[0];
|
|
947
|
+
[...this.childHandles].find((handle) => handle.key === localThreadId || handle.activeSessionId === localThreadId);
|
|
1057
948
|
}
|
|
1058
949
|
/** Forward a capability to a runner child. Session-less caps (listModels/status)
|
|
1059
950
|
* use the shared `CAP_KEY` child; per-thread caps pass the session's key so they
|
|
@@ -1306,9 +1197,6 @@ export class RunnerManager {
|
|
|
1306
1197
|
this.collaborationModeListener?.(message.sessionId, message.event.mode);
|
|
1307
1198
|
}
|
|
1308
1199
|
this.observeHandleRuntimeEvent(handle, message.event);
|
|
1309
|
-
if (message.event.type === "response.created") {
|
|
1310
|
-
this.settleTerminalInputHandoff(handle, message.sessionId, "turn");
|
|
1311
|
-
}
|
|
1312
1200
|
};
|
|
1313
1201
|
const commitDeliveredObservation = () => {
|
|
1314
1202
|
commitObservation();
|
|
@@ -1525,6 +1413,7 @@ export class RunnerManager {
|
|
|
1525
1413
|
inFlight: false,
|
|
1526
1414
|
logicalTargetPublished: false,
|
|
1527
1415
|
publicationComplete: false,
|
|
1416
|
+
admission: this.admissionReserve?.(),
|
|
1528
1417
|
};
|
|
1529
1418
|
handle.pendingRotation = pending;
|
|
1530
1419
|
this.forkReservations.set(pending.to, targetReservation);
|
|
@@ -1657,7 +1546,6 @@ export class RunnerManager {
|
|
|
1657
1546
|
try {
|
|
1658
1547
|
// Change the active Session only after ownership transfer. In
|
|
1659
1548
|
// this IPC topology rotate.applied is the child-owned equivalent proof.
|
|
1660
|
-
this.settleTerminalRotationHandoff(handle, pending.from, pending.to);
|
|
1661
1549
|
handle.activeResponseIds.clear();
|
|
1662
1550
|
for (const [key, candidate] of this.handles) {
|
|
1663
1551
|
if (candidate !== handle)
|
|
@@ -1736,6 +1624,7 @@ export class RunnerManager {
|
|
|
1736
1624
|
this.forkBufferedTerminalInputs.delete(pending.to);
|
|
1737
1625
|
pending.releaseTarget();
|
|
1738
1626
|
pending.releaseSource();
|
|
1627
|
+
pending.admission?.release();
|
|
1739
1628
|
}
|
|
1740
1629
|
async publishNativeRotationNotice(pending) {
|
|
1741
1630
|
// Only /clear supersedes the old Session. A /fork keeps
|
|
@@ -1800,6 +1689,9 @@ export class RunnerManager {
|
|
|
1800
1689
|
await Promise.all(publications);
|
|
1801
1690
|
}
|
|
1802
1691
|
spawnHandle(key) {
|
|
1692
|
+
const logDir = join(sessionDiagnosticDir("runtime", key), "instances", randomUUID());
|
|
1693
|
+
const event = diagnosticEvents(join(logDir, "process-events.jsonl"), { sessionId: key });
|
|
1694
|
+
event("runner.spawn");
|
|
1803
1695
|
const args = this.runnerEntry.endsWith(".ts")
|
|
1804
1696
|
? ["--import", "tsx", this.runnerEntry]
|
|
1805
1697
|
: [this.runnerEntry];
|
|
@@ -1820,6 +1712,7 @@ export class RunnerManager {
|
|
|
1820
1712
|
...this.childEnv,
|
|
1821
1713
|
...(sessionContext?.childEnv ?? {}),
|
|
1822
1714
|
RYNX_RUNNER_SESSION: key,
|
|
1715
|
+
RYNX_RUNTIME_LOG_DIR: logDir,
|
|
1823
1716
|
},
|
|
1824
1717
|
});
|
|
1825
1718
|
}
|
|
@@ -1828,6 +1721,10 @@ export class RunnerManager {
|
|
|
1828
1721
|
throw error;
|
|
1829
1722
|
}
|
|
1830
1723
|
const completion = observeChildCompletion(child);
|
|
1724
|
+
teeDiagnosticStream(child.stderr, join(logDir, "runner.stderr.log"));
|
|
1725
|
+
child.on("spawn", () => event("runner.started", { pid: child.pid }));
|
|
1726
|
+
child.on("error", (error) => event("runner.error", { message: error.message }));
|
|
1727
|
+
child.on("exit", (code, signal) => event("runner.exit", { code, signal }));
|
|
1831
1728
|
const transport = new StdioRunnerTransport(child.stdout, child.stdin);
|
|
1832
1729
|
const handle = {
|
|
1833
1730
|
key,
|
|
@@ -1838,6 +1735,7 @@ export class RunnerManager {
|
|
|
1838
1735
|
lastUsedAt: this.now(),
|
|
1839
1736
|
activeResponseIds: new Set(),
|
|
1840
1737
|
terminalResponseIds: new Set(),
|
|
1738
|
+
terminalWrites: new Map(),
|
|
1841
1739
|
dead: false,
|
|
1842
1740
|
completion,
|
|
1843
1741
|
processGroup,
|
|
@@ -1849,7 +1747,10 @@ export class RunnerManager {
|
|
|
1849
1747
|
mirrorDeliveries: new Map(),
|
|
1850
1748
|
};
|
|
1851
1749
|
this.childHandles.add(handle);
|
|
1852
|
-
void completion.then(() =>
|
|
1750
|
+
void completion.then(() => {
|
|
1751
|
+
this.finishTerminalWrites(handle);
|
|
1752
|
+
this.childHandles.delete(handle);
|
|
1753
|
+
});
|
|
1853
1754
|
transport.onMessage((msg) => this.onChildMessage(handle, msg));
|
|
1854
1755
|
if (child.stderr) {
|
|
1855
1756
|
const rl = readline.createInterface({ input: child.stderr, crlfDelay: Infinity });
|
|
@@ -1911,9 +1812,15 @@ export class RunnerManager {
|
|
|
1911
1812
|
});
|
|
1912
1813
|
return;
|
|
1913
1814
|
case "term.ack":
|
|
1815
|
+
if (msg.operation === "input")
|
|
1816
|
+
this.finishTerminalWrite(handle, msg.reqId);
|
|
1914
1817
|
handle.terminals.get(msg.attachId)?._ack(msg.reqId, msg.operation);
|
|
1915
1818
|
return;
|
|
1916
1819
|
case "term.error": {
|
|
1820
|
+
if (msg.reqId)
|
|
1821
|
+
this.finishTerminalWrite(handle, msg.reqId);
|
|
1822
|
+
else
|
|
1823
|
+
this.finishTerminalWrites(handle, msg.attachId);
|
|
1917
1824
|
const terminal = handle.terminals.get(msg.attachId);
|
|
1918
1825
|
if (!msg.reqId)
|
|
1919
1826
|
handle.terminals.delete(msg.attachId);
|
|
@@ -2264,13 +2171,12 @@ export class RunnerManager {
|
|
|
2264
2171
|
})();
|
|
2265
2172
|
handle.termination = attempt;
|
|
2266
2173
|
void attempt.then(() => {
|
|
2267
|
-
this.
|
|
2174
|
+
this.finishTerminalWrites(handle);
|
|
2268
2175
|
}, () => {
|
|
2269
2176
|
if (handle.termination === attempt)
|
|
2270
2177
|
delete handle.termination;
|
|
2271
|
-
//
|
|
2272
|
-
|
|
2273
|
-
this.preserveTerminalInputHandoffsAsActivity(handle);
|
|
2178
|
+
// An unexited dead handle remains visible in pendingNativeOperationCount.
|
|
2179
|
+
this.finishTerminalWrites(handle);
|
|
2274
2180
|
});
|
|
2275
2181
|
return attempt;
|
|
2276
2182
|
}
|
|
@@ -2536,70 +2442,6 @@ function rememberTerminalResponse(handle, responseId) {
|
|
|
2536
2442
|
handle.terminalResponseIds.delete(oldest);
|
|
2537
2443
|
}
|
|
2538
2444
|
}
|
|
2539
|
-
function trackedTerminalSubmissions(tracker, data) {
|
|
2540
|
-
const pasteStart = "\u001b[200~";
|
|
2541
|
-
const pasteEnd = "\u001b[201~";
|
|
2542
|
-
const input = tracker.pendingEscape + data;
|
|
2543
|
-
tracker.pendingEscape = "";
|
|
2544
|
-
const submissions = [];
|
|
2545
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
2546
|
-
const character = input[index] ?? "";
|
|
2547
|
-
if (character === "\u001b") {
|
|
2548
|
-
const remaining = input.slice(index);
|
|
2549
|
-
if (remaining.startsWith(pasteStart)) {
|
|
2550
|
-
tracker.bracketedPaste = true;
|
|
2551
|
-
tracker.previousWasCarriageReturn = false;
|
|
2552
|
-
index += pasteStart.length - 1;
|
|
2553
|
-
continue;
|
|
2554
|
-
}
|
|
2555
|
-
if (remaining.startsWith(pasteEnd)) {
|
|
2556
|
-
tracker.bracketedPaste = false;
|
|
2557
|
-
tracker.previousWasCarriageReturn = false;
|
|
2558
|
-
index += pasteEnd.length - 1;
|
|
2559
|
-
continue;
|
|
2560
|
-
}
|
|
2561
|
-
if (pasteStart.startsWith(remaining) || pasteEnd.startsWith(remaining)) {
|
|
2562
|
-
tracker.pendingEscape = remaining;
|
|
2563
|
-
break;
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
if (tracker.bracketedPaste) {
|
|
2567
|
-
tracker.buffer += character;
|
|
2568
|
-
tracker.previousWasCarriageReturn = false;
|
|
2569
|
-
continue;
|
|
2570
|
-
}
|
|
2571
|
-
if (character === "\n" && tracker.previousWasCarriageReturn) {
|
|
2572
|
-
tracker.previousWasCarriageReturn = false;
|
|
2573
|
-
continue;
|
|
2574
|
-
}
|
|
2575
|
-
tracker.previousWasCarriageReturn = character === "\r";
|
|
2576
|
-
if (character === "\r" || character === "\n") {
|
|
2577
|
-
const command = tracker.buffer.trim();
|
|
2578
|
-
tracker.buffer = "";
|
|
2579
|
-
if (command.length > 0 &&
|
|
2580
|
-
(command.includes("\u001b") ||
|
|
2581
|
-
!command.startsWith("/") ||
|
|
2582
|
-
/^\/(?:branch|clear|fork)(?:\s|$)/u.test(command))) {
|
|
2583
|
-
submissions.push(/^\/(?:branch|clear|fork)(?:\s|$)/u.test(command) ? "rotation" : "turn");
|
|
2584
|
-
}
|
|
2585
|
-
continue;
|
|
2586
|
-
}
|
|
2587
|
-
if (character === "\b" || character === "\u007f") {
|
|
2588
|
-
tracker.buffer = tracker.buffer.slice(0, -1);
|
|
2589
|
-
continue;
|
|
2590
|
-
}
|
|
2591
|
-
if (character === "\u0015" || character === "\u0003") {
|
|
2592
|
-
tracker.buffer = "";
|
|
2593
|
-
continue;
|
|
2594
|
-
}
|
|
2595
|
-
// Printable text and tabs are enough to distinguish work-producing prompts
|
|
2596
|
-
// from local TUI slash commands. Other control sequences are ignored.
|
|
2597
|
-
if (character === "\u001b" || character === "\t" || character >= " ") {
|
|
2598
|
-
tracker.buffer += character;
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
return submissions;
|
|
2602
|
-
}
|
|
2603
2445
|
function validateSessionContext(context) {
|
|
2604
2446
|
if (!context || typeof context !== "object") {
|
|
2605
2447
|
throw new Error("Session context environment provider must return an object");
|
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.48",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"smol-toml": "1.7.1",
|
|
28
28
|
"ws": "^8.21.0",
|
|
29
|
-
"@rynx-ai/core": "0.1.11-beta.
|
|
29
|
+
"@rynx-ai/core": "0.1.11-beta.48"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/ws": "^8.18.1"
|