@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.
@@ -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;
@@ -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 images = input.content.filter((part) => part.type === "local_image");
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 (images.length === 0)
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 = images.map((part) => `[[RYNX_IMAGE_RESOURCE ${JSON.stringify({ path: part.path })}]]`);
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");
@@ -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
@@ -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) => this.transport.send({ t: "mirror", sessionId: target.id, 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.transport.send({
195
- t: "rotate",
196
- from: localThreadId,
197
- to: newId,
198
- kind: meta.kind,
199
- workspace: meta.workspace,
200
- execution: meta.execution,
201
- ...(meta.parentSessionId ? { parentSessionId: meta.parentSessionId } : {}),
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?.();
@@ -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;
@@ -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.observeHandleRuntimeEvent(handle, message.event);
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
- if (msg.event.type === "session.collaboration_mode") {
1286
- this.collaborationModeListener?.(msg.sessionId, msg.event.mode);
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
- this.observeHandleRuntimeEvent(handle, msg.event);
1289
- this.mirrorListener?.(msg.sessionId, msg.event);
1290
- if (msg.event.type === "response.created") {
1291
- // The listener projects response.created into SessionRuntimeIndex
1292
- // synchronously. Only then may the accepted terminal reservation
1293
- // drain into a maintenance activity snapshot.
1294
- this.settleTerminalInputHandoff(handle, msg.sessionId, "turn");
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. */
@@ -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.25",
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.25"
30
+ "@rynx-ai/core": "0.1.11-beta.26"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/ws": "^8.18.1"