@rynx-ai/runtime 0.1.11-beta.25 → 0.1.11-beta.26
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.d.ts +2 -0
- package/dist/claude/native-bridge.js +23 -0
- package/dist/claude/native-integration.d.ts +8 -3
- package/dist/claude/native-integration.js +37 -7
- package/dist/claude/transcript.js +27 -17
- package/dist/codex-app-server/client.js +11 -1
- package/dist/codex-app-server/forwarder.d.ts +9 -0
- package/dist/codex-app-server/forwarder.js +155 -38
- package/dist/codex-app-server/mapping.d.ts +0 -6
- package/dist/codex-app-server/mapping.js +54 -4
- package/dist/codex-app-server/protocol.d.ts +10 -1
- package/dist/host.js +87 -26
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/runner/child.d.ts +6 -9
- package/dist/runner/child.js +154 -10
- package/dist/runner/manager.d.ts +3 -1
- package/dist/runner/manager.js +153 -15
- package/dist/runner/protocol.d.ts +31 -0
- package/dist/runner/protocol.js +5 -0
- package/package.json +2 -2
|
@@ -3,6 +3,10 @@ import type { UserInput } from "./codex-app-server/protocol.js";
|
|
|
3
3
|
/** Convert the provider-native user echo back to resource references. Unknown
|
|
4
4
|
* local paths and remote URLs are deliberately omitted rather than exposed. */
|
|
5
5
|
export declare function codexUserContent(input: readonly UserInput[]): UserContentPart[];
|
|
6
|
+
/** Match the forwarder's public user echo shape exactly. Codex merges adjacent
|
|
7
|
+
* text inputs (including the internal file marker) into one string before the
|
|
8
|
+
* Host sees the completed userMessage item. */
|
|
9
|
+
export declare function codexUserEchoContent(input: readonly UserInput[]): string | UserContentPart[] | undefined;
|
|
6
10
|
/** Claude's native TUI has no structured image RPC. The target daemon supplies
|
|
7
11
|
* only managed local paths and the marker makes the transcript echo reversible. */
|
|
8
12
|
export declare function claudeInputText(input: RuntimeUserInput, attachmentToken?: string): string;
|
package/dist/input-resources.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { basename, extname } from "node:path";
|
|
3
3
|
const CLAUDE_ATTACHMENT_TOKEN = "RYNX_ATTACHMENT_SET";
|
|
4
|
-
const CLAUDE_ATTACHMENT_TOKEN_RE = /^\[RYNX_ATTACHMENT_SET (att_[0-9a-f-]{36}): inspect each RYNX_IMAGE_RESOURCE path with the Read tool before answering\.\]$/m;
|
|
4
|
+
const CLAUDE_ATTACHMENT_TOKEN_RE = /^\[RYNX_ATTACHMENT_SET (att_[0-9a-f-]{36}): inspect each RYNX_IMAGE_RESOURCE or RYNX_FILE_RESOURCE path with the Read tool before answering\.\]$/m;
|
|
5
5
|
/** Convert the provider-native user echo back to resource references. Unknown
|
|
6
6
|
* local paths and remote URLs are deliberately omitted rather than exposed. */
|
|
7
7
|
export function codexUserContent(input) {
|
|
@@ -20,21 +20,37 @@ export function codexUserContent(input) {
|
|
|
20
20
|
}
|
|
21
21
|
return parts;
|
|
22
22
|
}
|
|
23
|
+
/** Match the forwarder's public user echo shape exactly. Codex merges adjacent
|
|
24
|
+
* text inputs (including the internal file marker) into one string before the
|
|
25
|
+
* Host sees the completed userMessage item. */
|
|
26
|
+
export function codexUserEchoContent(input) {
|
|
27
|
+
const parts = codexUserContent(input);
|
|
28
|
+
if (parts.length === 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (parts.every((part) => part.type === "input_text")) {
|
|
31
|
+
const text = parts.map((part) => part.text).join("").trim();
|
|
32
|
+
return text || undefined;
|
|
33
|
+
}
|
|
34
|
+
return parts;
|
|
35
|
+
}
|
|
23
36
|
/** Claude's native TUI has no structured image RPC. The target daemon supplies
|
|
24
37
|
* only managed local paths and the marker makes the transcript echo reversible. */
|
|
25
38
|
export function claudeInputText(input, attachmentToken = `att_${randomUUID()}`) {
|
|
26
|
-
const
|
|
39
|
+
const attachments = input.content.filter((part) => part.type !== "text");
|
|
27
40
|
const text = input.content
|
|
28
41
|
.filter((part) => part.type === "text")
|
|
29
42
|
.map((part) => part.text)
|
|
30
43
|
.join("");
|
|
31
|
-
if (
|
|
44
|
+
if (attachments.length === 0)
|
|
32
45
|
return text;
|
|
33
46
|
if (!/^att_[0-9a-f-]{36}$/.test(attachmentToken)) {
|
|
34
47
|
throw new Error("Claude attachment token is invalid");
|
|
35
48
|
}
|
|
36
|
-
const preamble = `[${CLAUDE_ATTACHMENT_TOKEN} ${attachmentToken}: inspect each RYNX_IMAGE_RESOURCE path with the Read tool before answering.]`;
|
|
37
|
-
const markers =
|
|
49
|
+
const preamble = `[${CLAUDE_ATTACHMENT_TOKEN} ${attachmentToken}: inspect each RYNX_IMAGE_RESOURCE or RYNX_FILE_RESOURCE path with the Read tool before answering.]`;
|
|
50
|
+
const markers = attachments.map((part) => `[[RYNX_${part.type === "local_image" ? "IMAGE" : "FILE"}_RESOURCE ${JSON.stringify({
|
|
51
|
+
path: part.path,
|
|
52
|
+
...(part.resource.filename ? { filename: part.resource.filename } : {}),
|
|
53
|
+
})}]]`);
|
|
38
54
|
return [preamble, ...markers, text]
|
|
39
55
|
.filter((part, index) => index <= markers.length || part.length > 0)
|
|
40
56
|
.join("\n");
|
package/dist/runner/child.d.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Child-side runner session. One per runner process (i.e. per session); owns the
|
|
3
|
-
* single execution backend via the injected executor (a {@link LocalAgentHost}
|
|
4
|
-
* in production, which in a per-session process holds exactly one backend). It
|
|
5
|
-
* translates inbound {@link ToChild} control messages into the live co-drive
|
|
6
|
-
* surface (bring up the session's forwarder + TUI, inject / interrupt turns) and
|
|
7
|
-
* per-thread capabilities, answered against the same backend so the parent never
|
|
8
|
-
* needs an app-server of its own.
|
|
9
|
-
*/
|
|
10
1
|
import { type AgentCapabilities, type LiveSessionFailure, type ResolvedExecutionSnapshot, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
|
|
11
2
|
import type { SessionEvent } from "@rynx-ai/core";
|
|
12
3
|
import type { TerminalInjector } from "../claude/native-integration.js";
|
|
@@ -95,6 +86,8 @@ export declare class RunnerSession {
|
|
|
95
86
|
private readonly cancelledTerminalOpens;
|
|
96
87
|
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
97
88
|
private readonly liveIds;
|
|
89
|
+
private mirrorQueue;
|
|
90
|
+
private readonly mirrorImageAcks;
|
|
98
91
|
/** Provider name retained for asynchronous Terminal-exit diagnostics. */
|
|
99
92
|
private readonly liveRuntimes;
|
|
100
93
|
private shuttingDown;
|
|
@@ -108,6 +101,10 @@ export declare class RunnerSession {
|
|
|
108
101
|
* new session AND tells the daemon to alias the runner (terminal transfer) so
|
|
109
102
|
* the new session stays injectable. */
|
|
110
103
|
private mirrorChannel;
|
|
104
|
+
private enqueueMirror;
|
|
105
|
+
private sendMirroredEvent;
|
|
106
|
+
private sendMirrorImageFrame;
|
|
107
|
+
private rejectMirrorImageAcks;
|
|
111
108
|
/**
|
|
112
109
|
* Eagerly bring up a session's codex-native live view. Fresh sessions connect
|
|
113
110
|
* the discovery listener before launching the detached TUI; known-thread
|
package/dist/runner/child.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* per-thread capabilities, answered against the same backend so the parent never
|
|
8
8
|
* needs an app-server of its own.
|
|
9
9
|
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { AgentRuntimeError, } from "@rynx-ai/core";
|
|
11
12
|
import { TerminalRegistry } from "../terminal/registry.js";
|
|
12
|
-
import { toWireError, } from "./protocol.js";
|
|
13
|
+
import { RUNNER_IMAGE_CHUNK_CHARS, RUNNER_IMAGE_MAX_RESULT_CHARS, toWireError, } from "./protocol.js";
|
|
13
14
|
import { isCodexLineageProvider } from "./startup-policy.js";
|
|
14
15
|
/** Maximum time for a fresh Codex-lineage TUI to publish its native thread. */
|
|
15
16
|
const CODEX_THREAD_START_TIMEOUT_MS = 30_000;
|
|
@@ -48,6 +49,7 @@ const TRAEX_STARTUP_POLL_MS = 100;
|
|
|
48
49
|
const TRAEX_AUTHORIZATION_POLL_MS = 500;
|
|
49
50
|
const TRAEX_AUTHORIZATION_WAIT_MS = 15 * 60_000;
|
|
50
51
|
const TRAEX_PROMPT_RETRY_MS = 500;
|
|
52
|
+
const MIRROR_IMAGE_ACK_TIMEOUT_MS = 30_000;
|
|
51
53
|
function normalizeTraexPane(pane) {
|
|
52
54
|
return pane.toLowerCase().replace(/\s+/g, " ").trim();
|
|
53
55
|
}
|
|
@@ -63,6 +65,48 @@ function isTerminalProtocolResponse(input) {
|
|
|
63
65
|
// Neither is evidence that the user has taken over startup prompt handling.
|
|
64
66
|
return /^(?:\x1b\[(?:(?:\?|>)[0-9;]*c|[0-9;]*(?:n|R|t)|[IO])|\x1b\][0-9]+;[^\x07\x1b]*(?:\x07|\x1b\\))+$/.test(input);
|
|
65
67
|
}
|
|
68
|
+
function detachGeneratedImageResult(event) {
|
|
69
|
+
if (event.type !== "response.output_item.done" ||
|
|
70
|
+
event.item.type !== "function_call_output") {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const data = event.item.data;
|
|
74
|
+
const pending = data.__rynxGeneratedImage;
|
|
75
|
+
if (!pending?.result)
|
|
76
|
+
return undefined;
|
|
77
|
+
const { result, ...withoutResult } = pending;
|
|
78
|
+
return {
|
|
79
|
+
result,
|
|
80
|
+
event: {
|
|
81
|
+
...event,
|
|
82
|
+
item: {
|
|
83
|
+
...event.item,
|
|
84
|
+
data: {
|
|
85
|
+
...data,
|
|
86
|
+
__rynxGeneratedImage: withoutResult,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function generatedImagePreviewUnavailable(event) {
|
|
93
|
+
if (event.type !== "response.output_item.done" ||
|
|
94
|
+
event.item.type !== "function_call_output") {
|
|
95
|
+
return event;
|
|
96
|
+
}
|
|
97
|
+
const data = event.item.data;
|
|
98
|
+
const { __rynxGeneratedImage: _omitted, ...publicData } = data;
|
|
99
|
+
return {
|
|
100
|
+
...event,
|
|
101
|
+
item: {
|
|
102
|
+
...event.item,
|
|
103
|
+
data: {
|
|
104
|
+
...publicData,
|
|
105
|
+
output: `${event.item.data.output}\nImage preview unavailable.`.trim(),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
66
110
|
export class RunnerSession {
|
|
67
111
|
transport;
|
|
68
112
|
executor;
|
|
@@ -78,6 +122,8 @@ export class RunnerSession {
|
|
|
78
122
|
cancelledTerminalOpens = new Set();
|
|
79
123
|
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
80
124
|
liveIds = new Set();
|
|
125
|
+
mirrorQueue = Promise.resolve();
|
|
126
|
+
mirrorImageAcks = new Map();
|
|
81
127
|
/** Provider name retained for asynchronous Terminal-exit diagnostics. */
|
|
82
128
|
liveRuntimes = new Map();
|
|
83
129
|
shuttingDown = false;
|
|
@@ -90,10 +136,36 @@ export class RunnerSession {
|
|
|
90
136
|
transport.close();
|
|
91
137
|
});
|
|
92
138
|
this.transport.onMessage((msg) => this.handle(msg));
|
|
139
|
+
this.transport.onClose((error) => {
|
|
140
|
+
this.rejectMirrorImageAcks(error ?? new Error("runner transport closed"));
|
|
141
|
+
});
|
|
93
142
|
this.transport.send({ t: "ready" });
|
|
94
143
|
}
|
|
95
144
|
handle(msg) {
|
|
96
145
|
switch (msg.t) {
|
|
146
|
+
case "mirror.image.ack": {
|
|
147
|
+
const pending = this.mirrorImageAcks.get(msg.transferId);
|
|
148
|
+
if (!pending)
|
|
149
|
+
return;
|
|
150
|
+
if (pending.seq !== msg.seq) {
|
|
151
|
+
pending.reject(new Error("generated image acknowledgement sequence changed"));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (pending.timer)
|
|
155
|
+
clearTimeout(pending.timer);
|
|
156
|
+
this.mirrorImageAcks.delete(msg.transferId);
|
|
157
|
+
pending.resolve();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case "mirror.image.hold": {
|
|
161
|
+
const pending = this.mirrorImageAcks.get(msg.transferId);
|
|
162
|
+
if (!pending || pending.seq !== msg.seq)
|
|
163
|
+
return;
|
|
164
|
+
if (pending.timer)
|
|
165
|
+
clearTimeout(pending.timer);
|
|
166
|
+
pending.timer = undefined;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
97
169
|
case "cap":
|
|
98
170
|
void this.runCap(msg.capId, msg.name, msg.args);
|
|
99
171
|
return;
|
|
@@ -188,21 +260,92 @@ export class RunnerSession {
|
|
|
188
260
|
* the new session stays injectable. */
|
|
189
261
|
mirrorChannel(localThreadId) {
|
|
190
262
|
const target = { id: localThreadId };
|
|
191
|
-
const emit = (event) =>
|
|
263
|
+
const emit = (event) => {
|
|
264
|
+
const sessionId = target.id;
|
|
265
|
+
this.enqueueMirror(() => this.sendMirroredEvent(sessionId, event));
|
|
266
|
+
};
|
|
192
267
|
const retarget = (newId, meta) => {
|
|
193
268
|
target.id = newId;
|
|
194
|
-
this.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
269
|
+
this.enqueueMirror(() => {
|
|
270
|
+
this.transport.send({
|
|
271
|
+
t: "rotate",
|
|
272
|
+
from: localThreadId,
|
|
273
|
+
to: newId,
|
|
274
|
+
kind: meta.kind,
|
|
275
|
+
workspace: meta.workspace,
|
|
276
|
+
execution: meta.execution,
|
|
277
|
+
...(meta.parentSessionId ? { parentSessionId: meta.parentSessionId } : {}),
|
|
278
|
+
});
|
|
202
279
|
});
|
|
203
280
|
};
|
|
204
281
|
return { emit, retarget };
|
|
205
282
|
}
|
|
283
|
+
enqueueMirror(operation) {
|
|
284
|
+
this.mirrorQueue = this.mirrorQueue.then(operation).catch((error) => {
|
|
285
|
+
if (!this.shuttingDown) {
|
|
286
|
+
this.rejectMirrorImageAcks(error instanceof Error ? error : new Error(String(error)));
|
|
287
|
+
this.shutdown();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async sendMirroredEvent(sessionId, event) {
|
|
292
|
+
const detached = detachGeneratedImageResult(event);
|
|
293
|
+
if (!detached) {
|
|
294
|
+
this.transport.send({ t: "mirror", sessionId, event });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (detached.result.length > RUNNER_IMAGE_MAX_RESULT_CHARS) {
|
|
298
|
+
this.transport.send({
|
|
299
|
+
t: "mirror",
|
|
300
|
+
sessionId,
|
|
301
|
+
event: generatedImagePreviewUnavailable(detached.event),
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const transferId = `img_${randomUUID()}`;
|
|
306
|
+
let seq = 0;
|
|
307
|
+
await this.sendMirrorImageFrame(transferId, seq, {
|
|
308
|
+
t: "mirror.image.begin",
|
|
309
|
+
transferId,
|
|
310
|
+
sessionId,
|
|
311
|
+
event: detached.event,
|
|
312
|
+
totalChars: detached.result.length,
|
|
313
|
+
});
|
|
314
|
+
for (let offset = 0; offset < detached.result.length; offset += RUNNER_IMAGE_CHUNK_CHARS) {
|
|
315
|
+
seq += 1;
|
|
316
|
+
await this.sendMirrorImageFrame(transferId, seq, {
|
|
317
|
+
t: "mirror.image.chunk",
|
|
318
|
+
transferId,
|
|
319
|
+
seq,
|
|
320
|
+
data: detached.result.slice(offset, offset + RUNNER_IMAGE_CHUNK_CHARS),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
seq += 1;
|
|
324
|
+
await this.sendMirrorImageFrame(transferId, seq, {
|
|
325
|
+
t: "mirror.image.commit",
|
|
326
|
+
transferId,
|
|
327
|
+
seq,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
sendMirrorImageFrame(transferId, seq, frame) {
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
332
|
+
const timer = setTimeout(() => {
|
|
333
|
+
this.mirrorImageAcks.delete(transferId);
|
|
334
|
+
reject(new Error("generated image transfer acknowledgement timed out"));
|
|
335
|
+
}, MIRROR_IMAGE_ACK_TIMEOUT_MS);
|
|
336
|
+
timer.unref?.();
|
|
337
|
+
this.mirrorImageAcks.set(transferId, { seq, resolve, reject, timer });
|
|
338
|
+
this.transport.send(frame);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
rejectMirrorImageAcks(error) {
|
|
342
|
+
for (const pending of this.mirrorImageAcks.values()) {
|
|
343
|
+
if (pending.timer)
|
|
344
|
+
clearTimeout(pending.timer);
|
|
345
|
+
pending.reject(error);
|
|
346
|
+
}
|
|
347
|
+
this.mirrorImageAcks.clear();
|
|
348
|
+
}
|
|
206
349
|
/**
|
|
207
350
|
* Eagerly bring up a session's codex-native live view. Fresh sessions connect
|
|
208
351
|
* the discovery listener before launching the detached TUI; known-thread
|
|
@@ -626,6 +769,7 @@ export class RunnerSession {
|
|
|
626
769
|
if (this.shuttingDown)
|
|
627
770
|
return;
|
|
628
771
|
this.shuttingDown = true;
|
|
772
|
+
this.rejectMirrorImageAcks(new Error("runner is shutting down"));
|
|
629
773
|
this.stopLive();
|
|
630
774
|
this.terminals.closeAll();
|
|
631
775
|
this.liveProvider.finalizeStoppedLiveSessions?.();
|
package/dist/runner/manager.d.ts
CHANGED
|
@@ -229,7 +229,7 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
229
229
|
* session's persistent codex forwarder (web- AND TUI-initiated turns). The
|
|
230
230
|
* server wires this to `persistSessionEvent` (bus + canonical log).
|
|
231
231
|
*/
|
|
232
|
-
onMirror(listener: (sessionId: string, event: SessionEvent) => void): void;
|
|
232
|
+
onMirror(listener: (sessionId: string, event: SessionEvent) => void | Promise<void>): void;
|
|
233
233
|
onCollaborationMode(listener: (sessionId: string, mode: SessionCollaborationMode) => void): void;
|
|
234
234
|
/** Register the sink for session rotations (claude `/clear`·`/fork`): the server
|
|
235
235
|
* records the new session's meta (carry-over agent/model/title). */
|
|
@@ -331,6 +331,8 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
331
331
|
private bufferForkTargetMessage;
|
|
332
332
|
private reservedForkTarget;
|
|
333
333
|
private deliverForkBufferedMessage;
|
|
334
|
+
private deliverMirrorMessage;
|
|
335
|
+
private failMirrorImageTransfer;
|
|
334
336
|
private deliverRotateMessage;
|
|
335
337
|
private spawnHandle;
|
|
336
338
|
private onChildMessage;
|
package/dist/runner/manager.js
CHANGED
|
@@ -23,7 +23,7 @@ import { AgentRuntimeError, } from "@rynx-ai/core";
|
|
|
23
23
|
import { listRuntimeModels } from "../models-catalog.js";
|
|
24
24
|
import { probeRuntimeStatus } from "../runtime-status.js";
|
|
25
25
|
import { terminateTmuxServer, tmuxHasAttachedClient, tmuxWindowActivityAt, } from "../terminal/tmux.js";
|
|
26
|
-
import { fromWireError, } from "./protocol.js";
|
|
26
|
+
import { RUNNER_IMAGE_CHUNK_CHARS, RUNNER_IMAGE_MAX_RESULT_CHARS, fromWireError, } from "./protocol.js";
|
|
27
27
|
import { isCodexLineageProvider, isManagedNativeProvider } from "./startup-policy.js";
|
|
28
28
|
import { StdioRunnerTransport } from "./transport.js";
|
|
29
29
|
/** Routing key for the shared capability runner (slash-command RPCs). */
|
|
@@ -1081,15 +1081,56 @@ export class RunnerManager {
|
|
|
1081
1081
|
if (handle.dead)
|
|
1082
1082
|
return;
|
|
1083
1083
|
if (message.t === "mirror") {
|
|
1084
|
-
this.
|
|
1085
|
-
this.mirrorListener?.(message.sessionId, message.event);
|
|
1086
|
-
if (message.event.type === "response.created") {
|
|
1087
|
-
this.settleTerminalInputHandoff(handle, message.sessionId, "turn");
|
|
1088
|
-
}
|
|
1084
|
+
this.deliverMirrorMessage(handle, message);
|
|
1089
1085
|
return;
|
|
1090
1086
|
}
|
|
1091
1087
|
this.deliverRotateMessage(handle, message);
|
|
1092
1088
|
}
|
|
1089
|
+
deliverMirrorMessage(handle, message) {
|
|
1090
|
+
if (handle.dead)
|
|
1091
|
+
return;
|
|
1092
|
+
if (message.event.type === "session.collaboration_mode") {
|
|
1093
|
+
this.collaborationModeListener?.(message.sessionId, message.event.mode);
|
|
1094
|
+
}
|
|
1095
|
+
this.observeHandleRuntimeEvent(handle, message.event);
|
|
1096
|
+
let delivered;
|
|
1097
|
+
try {
|
|
1098
|
+
delivered = Promise.resolve(this.mirrorListener?.(message.sessionId, message.event));
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
delivered = Promise.reject(error);
|
|
1102
|
+
}
|
|
1103
|
+
if (message.event.type === "response.created") {
|
|
1104
|
+
// The listener projects response.created into SessionRuntimeIndex
|
|
1105
|
+
// synchronously. Only then may the accepted terminal reservation drain
|
|
1106
|
+
// into a maintenance activity snapshot.
|
|
1107
|
+
this.settleTerminalInputHandoff(handle, message.sessionId, "turn");
|
|
1108
|
+
}
|
|
1109
|
+
if (message.transferId && message.seq !== undefined) {
|
|
1110
|
+
void delivered.then(() => {
|
|
1111
|
+
if (!handle.dead) {
|
|
1112
|
+
handle.transport.send({
|
|
1113
|
+
t: "mirror.image.ack",
|
|
1114
|
+
transferId: message.transferId,
|
|
1115
|
+
seq: message.seq,
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}, (error) => {
|
|
1119
|
+
this.failMirrorImageTransfer(handle, `generated image publication failed: ${errorMessage(error)}`);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
void delivered.catch((error) => {
|
|
1124
|
+
console.error(`Session mirror publication failed: ${errorMessage(error)}`);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
failMirrorImageTransfer(handle, reason) {
|
|
1129
|
+
handle.imageTransfers.clear();
|
|
1130
|
+
void this.terminateHandle(handle, reason).catch((error) => {
|
|
1131
|
+
logTerminationFailure(handle, error);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1093
1134
|
deliverRotateMessage(handle, message) {
|
|
1094
1135
|
if (this.forkReservations.has(message.to) ||
|
|
1095
1136
|
this.sourceForkReservations.has(message.from)) {
|
|
@@ -1224,6 +1265,7 @@ export class RunnerManager {
|
|
|
1224
1265
|
caps: new Map(),
|
|
1225
1266
|
terminals: new Map(),
|
|
1226
1267
|
live: new Map(),
|
|
1268
|
+
imageTransfers: new Map(),
|
|
1227
1269
|
};
|
|
1228
1270
|
this.childHandles.add(handle);
|
|
1229
1271
|
void completion.then(() => this.childHandles.delete(handle));
|
|
@@ -1282,18 +1324,83 @@ export class RunnerManager {
|
|
|
1282
1324
|
return;
|
|
1283
1325
|
if (this.bufferForkTargetMessage(handle, msg))
|
|
1284
1326
|
return;
|
|
1285
|
-
|
|
1286
|
-
|
|
1327
|
+
this.deliverMirrorMessage(handle, msg);
|
|
1328
|
+
return;
|
|
1329
|
+
case "mirror.image.begin": {
|
|
1330
|
+
if (handle.dead ||
|
|
1331
|
+
handle.imageTransfers.has(msg.transferId) ||
|
|
1332
|
+
!Number.isSafeInteger(msg.totalChars) ||
|
|
1333
|
+
msg.totalChars <= 0 ||
|
|
1334
|
+
msg.totalChars > RUNNER_IMAGE_MAX_RESULT_CHARS ||
|
|
1335
|
+
!isDetachedGeneratedImageEvent(msg.event)) {
|
|
1336
|
+
this.failMirrorImageTransfer(handle, "invalid generated image transfer start");
|
|
1337
|
+
return;
|
|
1287
1338
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1339
|
+
handle.imageTransfers.set(msg.transferId, {
|
|
1340
|
+
sessionId: msg.sessionId,
|
|
1341
|
+
event: msg.event,
|
|
1342
|
+
totalChars: msg.totalChars,
|
|
1343
|
+
receivedChars: 0,
|
|
1344
|
+
nextSeq: 1,
|
|
1345
|
+
chunks: [],
|
|
1346
|
+
});
|
|
1347
|
+
handle.transport.send({ t: "mirror.image.ack", transferId: msg.transferId, seq: 0 });
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
case "mirror.image.chunk": {
|
|
1351
|
+
const transfer = handle.imageTransfers.get(msg.transferId);
|
|
1352
|
+
if (handle.dead ||
|
|
1353
|
+
!transfer ||
|
|
1354
|
+
msg.seq !== transfer.nextSeq ||
|
|
1355
|
+
!msg.data ||
|
|
1356
|
+
msg.data.length > RUNNER_IMAGE_CHUNK_CHARS ||
|
|
1357
|
+
transfer.receivedChars + msg.data.length > transfer.totalChars) {
|
|
1358
|
+
this.failMirrorImageTransfer(handle, "invalid generated image transfer chunk");
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
transfer.chunks.push(msg.data);
|
|
1362
|
+
transfer.receivedChars += msg.data.length;
|
|
1363
|
+
transfer.nextSeq += 1;
|
|
1364
|
+
handle.transport.send({
|
|
1365
|
+
t: "mirror.image.ack",
|
|
1366
|
+
transferId: msg.transferId,
|
|
1367
|
+
seq: msg.seq,
|
|
1368
|
+
});
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
case "mirror.image.commit": {
|
|
1372
|
+
const transfer = handle.imageTransfers.get(msg.transferId);
|
|
1373
|
+
if (handle.dead ||
|
|
1374
|
+
!transfer ||
|
|
1375
|
+
msg.seq !== transfer.nextSeq ||
|
|
1376
|
+
transfer.receivedChars !== transfer.totalChars) {
|
|
1377
|
+
this.failMirrorImageTransfer(handle, "invalid generated image transfer commit");
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
handle.imageTransfers.delete(msg.transferId);
|
|
1381
|
+
const event = attachGeneratedImageResult(transfer.event, transfer.chunks.join(""));
|
|
1382
|
+
if (!event) {
|
|
1383
|
+
this.failMirrorImageTransfer(handle, "generated image transfer target changed");
|
|
1384
|
+
return;
|
|
1295
1385
|
}
|
|
1386
|
+
const mirrored = {
|
|
1387
|
+
t: "mirror",
|
|
1388
|
+
sessionId: transfer.sessionId,
|
|
1389
|
+
event,
|
|
1390
|
+
transferId: msg.transferId,
|
|
1391
|
+
seq: msg.seq,
|
|
1392
|
+
};
|
|
1393
|
+
if (this.bufferForkTargetMessage(handle, mirrored)) {
|
|
1394
|
+
handle.transport.send({
|
|
1395
|
+
t: "mirror.image.hold",
|
|
1396
|
+
transferId: msg.transferId,
|
|
1397
|
+
seq: msg.seq,
|
|
1398
|
+
});
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
this.deliverMirrorMessage(handle, mirrored);
|
|
1296
1402
|
return;
|
|
1403
|
+
}
|
|
1297
1404
|
case "rotate": {
|
|
1298
1405
|
if (handle.dead)
|
|
1299
1406
|
return;
|
|
@@ -1368,6 +1475,7 @@ export class RunnerManager {
|
|
|
1368
1475
|
handle.caps.clear();
|
|
1369
1476
|
handle.terminals.clear();
|
|
1370
1477
|
handle.live.clear();
|
|
1478
|
+
handle.imageTransfers.clear();
|
|
1371
1479
|
handle.transport.close();
|
|
1372
1480
|
}
|
|
1373
1481
|
terminateHandle(handle, reason) {
|
|
@@ -1730,6 +1838,36 @@ function closeSessionContext(context) {
|
|
|
1730
1838
|
}));
|
|
1731
1839
|
}
|
|
1732
1840
|
}
|
|
1841
|
+
function isDetachedGeneratedImageEvent(event) {
|
|
1842
|
+
if (event.type !== "response.output_item.done" ||
|
|
1843
|
+
event.item.type !== "function_call_output") {
|
|
1844
|
+
return false;
|
|
1845
|
+
}
|
|
1846
|
+
const data = event.item.data;
|
|
1847
|
+
return Boolean(data.__rynxGeneratedImage && data.__rynxGeneratedImage.result === undefined);
|
|
1848
|
+
}
|
|
1849
|
+
function attachGeneratedImageResult(event, result) {
|
|
1850
|
+
if (!isDetachedGeneratedImageEvent(event))
|
|
1851
|
+
return undefined;
|
|
1852
|
+
const outputEvent = event;
|
|
1853
|
+
const item = outputEvent.item;
|
|
1854
|
+
if (item.type !== "function_call_output")
|
|
1855
|
+
return undefined;
|
|
1856
|
+
const data = item.data;
|
|
1857
|
+
return {
|
|
1858
|
+
...outputEvent,
|
|
1859
|
+
item: {
|
|
1860
|
+
...item,
|
|
1861
|
+
data: {
|
|
1862
|
+
...data,
|
|
1863
|
+
__rynxGeneratedImage: {
|
|
1864
|
+
...data.__rynxGeneratedImage,
|
|
1865
|
+
result,
|
|
1866
|
+
},
|
|
1867
|
+
},
|
|
1868
|
+
},
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1733
1871
|
function errorMessage(error) {
|
|
1734
1872
|
return error instanceof Error ? error.message : String(error);
|
|
1735
1873
|
}
|
|
@@ -26,6 +26,14 @@ export type TerminalOpenErrorCode = "terminal_not_live" | "terminal_open_failed"
|
|
|
26
26
|
/** Parent → child. Terminal PTY bytes ride `term.input` base64-encoded so the
|
|
27
27
|
* NDJSON line framing stays intact (raw bytes contain newlines). */
|
|
28
28
|
export type ToChild = {
|
|
29
|
+
t: "mirror.image.ack";
|
|
30
|
+
transferId: string;
|
|
31
|
+
seq: number;
|
|
32
|
+
} | {
|
|
33
|
+
t: "mirror.image.hold";
|
|
34
|
+
transferId: string;
|
|
35
|
+
seq: number;
|
|
36
|
+
} | {
|
|
29
37
|
t: "cap";
|
|
30
38
|
capId: string;
|
|
31
39
|
name: CapName;
|
|
@@ -151,6 +159,24 @@ export type FromChild = {
|
|
|
151
159
|
t: "mirror";
|
|
152
160
|
sessionId: string;
|
|
153
161
|
event: SessionEvent;
|
|
162
|
+
/** Parent-local completion marker used after a chunked image is rebuilt. */
|
|
163
|
+
transferId?: string;
|
|
164
|
+
seq?: number;
|
|
165
|
+
} | {
|
|
166
|
+
t: "mirror.image.begin";
|
|
167
|
+
transferId: string;
|
|
168
|
+
sessionId: string;
|
|
169
|
+
event: SessionEvent;
|
|
170
|
+
totalChars: number;
|
|
171
|
+
} | {
|
|
172
|
+
t: "mirror.image.chunk";
|
|
173
|
+
transferId: string;
|
|
174
|
+
seq: number;
|
|
175
|
+
data: string;
|
|
176
|
+
} | {
|
|
177
|
+
t: "mirror.image.commit";
|
|
178
|
+
transferId: string;
|
|
179
|
+
seq: number;
|
|
154
180
|
}
|
|
155
181
|
/** The session rotated to a fresh machine-session (claude `/clear`·`/fork`): the
|
|
156
182
|
* child re-pointed its mirror to `to` and asks the daemon to alias the runner
|
|
@@ -205,6 +231,11 @@ export type FromChild = {
|
|
|
205
231
|
localThreadId: string;
|
|
206
232
|
result: ResolveInteractionResult;
|
|
207
233
|
};
|
|
234
|
+
/** Internal runner wire bounds. Generated images are already subject to the
|
|
235
|
+
* Session image byte limit; chunks keep the NDJSON pipe and parser bounded and
|
|
236
|
+
* each chunk is acknowledged before the next is sent. */
|
|
237
|
+
export declare const RUNNER_IMAGE_CHUNK_CHARS: number;
|
|
238
|
+
export declare const RUNNER_IMAGE_MAX_RESULT_CHARS: number;
|
|
208
239
|
/** Encode a message as a single NDJSON line (newline included). */
|
|
209
240
|
export declare function encodeMessage(msg: ToChild | FromChild): string;
|
|
210
241
|
/** Parse one NDJSON line; returns `null` for blank lines or malformed JSON. */
|
package/dist/runner/protocol.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* terminal attachment; the reply channels mirror each request's `reqId`.
|
|
8
8
|
*/
|
|
9
9
|
import { AgentRuntimeError, } from "@rynx-ai/core";
|
|
10
|
+
/** Internal runner wire bounds. Generated images are already subject to the
|
|
11
|
+
* Session image byte limit; chunks keep the NDJSON pipe and parser bounded and
|
|
12
|
+
* each chunk is acknowledged before the next is sent. */
|
|
13
|
+
export const RUNNER_IMAGE_CHUNK_CHARS = 32 * 1024;
|
|
14
|
+
export const RUNNER_IMAGE_MAX_RESULT_CHARS = 8 * 1024 * 1024;
|
|
10
15
|
/** Encode a message as a single NDJSON line (newline included). */
|
|
11
16
|
export function encodeMessage(msg) {
|
|
12
17
|
return `${JSON.stringify(msg)}\n`;
|
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.26",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node-pty": "1.2.0-beta.15",
|
|
28
28
|
"smol-toml": "1.7.1",
|
|
29
29
|
"ws": "^8.21.0",
|
|
30
|
-
"@rynx-ai/core": "0.1.11-beta.
|
|
30
|
+
"@rynx-ai/core": "0.1.11-beta.26"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/ws": "^8.18.1"
|