@rynx-ai/runtime 0.1.11-beta.24 → 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,13 +1,4 @@
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
- import { type AgentCapabilities, type LiveSessionFailure, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
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";
13
4
  import type { ResolveInteractionResult } from "../interactions.js";
@@ -54,15 +45,17 @@ interface LiveCodexProvider {
54
45
  * observer/forwarder, and evict the owned app-server while preserving the
55
46
  * durable native-session binding for a later cold resume. */
56
47
  teardownLiveCodexSession?(localThreadId: string, error?: Error): boolean;
57
- injectMessage?(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
48
+ injectMessage?(localThreadId: string, input: RuntimeUserInput | string, options?: RuntimeTurnOptions): Promise<InjectResult>;
49
+ updateCollaborationMode?(localThreadId: string, mode: SessionCollaborationMode): Promise<void>;
58
50
  interruptLive?(localThreadId: string): Promise<boolean>;
59
51
  stopLiveCodexSession?(localThreadId: string, opts?: {
60
52
  deferClaudeInteractionCleanup?: boolean;
61
53
  }): void;
62
54
  finalizeStoppedLiveSessions?(): void;
63
55
  resolveInteraction?(localThreadId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<ResolveInteractionResult>;
64
- /** claude-native: hand the session's tmux pane injector to the host (which
65
- * doesn't own tmux). No-op for codex sessions (they inject via app-server). */
56
+ /** Hand the session's tmux pane injector to the host (which doesn't own
57
+ * tmux). Claude uses it for messages; Codex uses it only for TUI-local
58
+ * controls that the app-server cannot resolve, such as the final Plan picker. */
66
59
  attachTerminalInjector?(localThreadId: string, injector: TerminalInjector): void;
67
60
  /** Internal Provider operation used only by MachineSessionService through
68
61
  * RunnerManager. It is deliberately absent from public AgentCapabilities. */
@@ -93,6 +86,8 @@ export declare class RunnerSession {
93
86
  private readonly cancelledTerminalOpens;
94
87
  /** Session ids with a live codex forwarder started here (stopped on shutdown). */
95
88
  private readonly liveIds;
89
+ private mirrorQueue;
90
+ private readonly mirrorImageAcks;
96
91
  /** Provider name retained for asynchronous Terminal-exit diagnostics. */
97
92
  private readonly liveRuntimes;
98
93
  private shuttingDown;
@@ -106,6 +101,10 @@ export declare class RunnerSession {
106
101
  * new session AND tells the daemon to alias the runner (terminal transfer) so
107
102
  * the new session stays injectable. */
108
103
  private mirrorChannel;
104
+ private enqueueMirror;
105
+ private sendMirroredEvent;
106
+ private sendMirrorImageFrame;
107
+ private rejectMirrorImageAcks;
109
108
  /**
110
109
  * Eagerly bring up a session's codex-native live view. Fresh sessions connect
111
110
  * the discovery listener before launching the detached TUI; known-thread
@@ -120,6 +119,7 @@ export declare class RunnerSession {
120
119
  * already-rejected readiness promise. */
121
120
  private failCodexThreadStartup;
122
121
  private inject;
122
+ private updateCollaborationMode;
123
123
  private interruptLive;
124
124
  /** Launch (idempotently) the session's codex TUI pane from the executor's
125
125
  * `codexTerminalSpec`. Shares the `${id}-main` terminal id with `term.open`,
@@ -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;
@@ -149,6 +221,9 @@ export class RunnerSession {
149
221
  case "inject":
150
222
  void this.inject(msg);
151
223
  return;
224
+ case "collaboration.update":
225
+ void this.updateCollaborationMode(msg);
226
+ return;
152
227
  case "live.interrupt":
153
228
  void this.interruptLive(msg);
154
229
  return;
@@ -185,21 +260,92 @@ export class RunnerSession {
185
260
  * the new session stays injectable. */
186
261
  mirrorChannel(localThreadId) {
187
262
  const target = { id: localThreadId };
188
- 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
+ };
189
267
  const retarget = (newId, meta) => {
190
268
  target.id = newId;
191
- this.transport.send({
192
- t: "rotate",
193
- from: localThreadId,
194
- to: newId,
195
- kind: meta.kind,
196
- workspace: meta.workspace,
197
- execution: meta.execution,
198
- ...(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
+ });
199
279
  });
200
280
  };
201
281
  return { emit, retarget };
202
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
+ }
203
349
  /**
204
350
  * Eagerly bring up a session's codex-native live view. Fresh sessions connect
205
351
  * the discovery listener before launching the detached TUI; known-thread
@@ -274,7 +420,7 @@ export class RunnerSession {
274
420
  : nativePhaseError(runtime, "native_terminal_launch_failed", "Terminal failed to launch", error);
275
421
  }
276
422
  }
277
- // Omnigent's known-thread path launches `codex --remote resume` first,
423
+ // reference implementation's known-thread path launches `codex --remote resume` first,
278
424
  // then creates `_codex_forward_known_thread` as background work. Keep the
279
425
  // ordering explicit here: observer connection failure cannot delay or
280
426
  // reject cold-resume admission.
@@ -377,7 +523,7 @@ export class RunnerSession {
377
523
  const provider = this.liveProvider;
378
524
  try {
379
525
  const input = msg.input ?? msg.text;
380
- const result = (await provider.injectMessage?.(msg.localThreadId, input))
526
+ const result = (await provider.injectMessage?.(msg.localThreadId, input, msg.options))
381
527
  ?? { outcome: "notLive" };
382
528
  // App-server injection is independent of the Terminal TUI startup, so it
383
529
  // must not cancel prompt handling for the pane that is still starting.
@@ -413,6 +559,29 @@ export class RunnerSession {
413
559
  });
414
560
  }
415
561
  }
562
+ async updateCollaborationMode(msg) {
563
+ try {
564
+ if (!this.liveProvider.updateCollaborationMode) {
565
+ throw new AgentRuntimeError("Native collaboration mode is unavailable", 409, "collaboration_mode_unavailable");
566
+ }
567
+ await this.liveProvider.updateCollaborationMode(msg.localThreadId, msg.mode);
568
+ this.transport.send({
569
+ t: "collaboration.updated",
570
+ reqId: msg.reqId,
571
+ localThreadId: msg.localThreadId,
572
+ ok: true,
573
+ });
574
+ }
575
+ catch (error) {
576
+ this.transport.send({
577
+ t: "collaboration.updated",
578
+ reqId: msg.reqId,
579
+ localThreadId: msg.localThreadId,
580
+ ok: false,
581
+ error: toWireError(error),
582
+ });
583
+ }
584
+ }
416
585
  async interruptLive(msg) {
417
586
  const provider = this.liveProvider;
418
587
  let ok = false;
@@ -472,7 +641,7 @@ export class RunnerSession {
472
641
  ? terminal.isAliveAsync().then((alive) => alive ? "alive" : "dead")
473
642
  : Promise.resolve(terminal.isAlive() ? "alive" : "dead");
474
643
  void probe.then((liveness) => {
475
- // A failed pane probe is inconclusive. Only Omnigent's lifecycle
644
+ // A failed pane probe is inconclusive. Only reference implementation's lifecycle
476
645
  // evidence — capture target gone or explicit pane_dead — may fail it.
477
646
  if (liveness !== "dead" ||
478
647
  this.shuttingDown ||
@@ -600,6 +769,7 @@ export class RunnerSession {
600
769
  if (this.shuttingDown)
601
770
  return;
602
771
  this.shuttingDown = true;
772
+ this.rejectMirrorImageAcks(new Error("runner is shutting down"));
603
773
  this.stopLive();
604
774
  this.terminals.closeAll();
605
775
  this.liveProvider.finalizeStoppedLiveSessions?.();
@@ -16,7 +16,7 @@
16
16
  * channel.
17
17
  */
18
18
  import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
19
- import { type AdmissionReservation, type AgentCapabilities, type AgentRuntimeId, type AppConfig, type CapabilityResult, type ModelListResponse, type LiveSessionFailure, type InjectResult, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot, type ThreadGoal } from "@rynx-ai/core";
19
+ import { type AdmissionReservation, type AgentCapabilities, type AgentRuntimeId, type AppConfig, type CapabilityResult, type ModelListResponse, type LiveSessionFailure, type InjectResult, type ResolvedExecutionSnapshot, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot, type ThreadGoal } from "@rynx-ai/core";
20
20
  import { type CodexSessionStore } from "../host.js";
21
21
  import type { ResolveInteractionResult } from "../interactions.js";
22
22
  import { type TerminalOpenErrorCode, type TerminalRole } from "./protocol.js";
@@ -73,7 +73,7 @@ export interface RunnerManagerOptions {
73
73
  * created for the runner child, with direct-child fallback. */
74
74
  signalChild?: (child: ChildProcess, signal: NodeJS.Signals, processGroup: boolean) => void;
75
75
  /** Injected for tests. Best-effort close of the deterministic private tmux
76
- * server owned by a Session runner; Omnigent does not verify/retry close. */
76
+ * server owned by a Session runner; reference implementation does not verify/retry close. */
77
77
  terminateTerminalServer?: (terminalName: string) => boolean | Promise<boolean>;
78
78
  /** Injected tmux `#{window_activity}` reader. Returns epoch seconds. */
79
79
  terminalWindowActivityAt?: (terminalName: string) => number | null | Promise<number | null>;
@@ -179,6 +179,9 @@ export declare class RunnerManager implements AgentCapabilities {
179
179
  private stopPromise;
180
180
  /** Sink for mirrored {@link SessionEvent}s from every session's forwarder. */
181
181
  private mirrorListener;
182
+ /** Synchronous persistence hook for native collaboration-mode reflection.
183
+ * It runs before the event reaches the Session bus. */
184
+ private collaborationModeListener;
182
185
  /** Sink for session rotations (claude `/clear`·`/fork`) — server records meta. */
183
186
  private rotateListener;
184
187
  /** Session keys with a live codex forwarder — never reaped while present. */
@@ -226,7 +229,8 @@ export declare class RunnerManager implements AgentCapabilities {
226
229
  * session's persistent codex forwarder (web- AND TUI-initiated turns). The
227
230
  * server wires this to `persistSessionEvent` (bus + canonical log).
228
231
  */
229
- onMirror(listener: (sessionId: string, event: SessionEvent) => void): void;
232
+ onMirror(listener: (sessionId: string, event: SessionEvent) => void | Promise<void>): void;
233
+ onCollaborationMode(listener: (sessionId: string, mode: SessionCollaborationMode) => void): void;
230
234
  /** Register the sink for session rotations (claude `/clear`·`/fork`): the server
231
235
  * records the new session's meta (carry-over agent/model/title). */
232
236
  onRotate(listener: (rotation: RotateInfo) => void | Promise<void>): void;
@@ -273,7 +277,7 @@ export declare class RunnerManager implements AgentCapabilities {
273
277
  * all output. Resolves true when the app-server accepted the turn, false when
274
278
  * the session has no live forwarder (caller falls back to the run path).
275
279
  */
276
- injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
280
+ injectMessage(localThreadId: string, input: RuntimeUserInput | string, options?: RuntimeTurnOptions): Promise<InjectResult>;
277
281
  private injectMessageAdmitted;
278
282
  /**
279
283
  * Interrupt a session's active live turn — the web Stop button (codex
@@ -283,6 +287,8 @@ export declare class RunnerManager implements AgentCapabilities {
283
287
  interruptLiveSession(localThreadId: string): Promise<boolean>;
284
288
  /** Resolve a native question/approval without ever spawning a new runner. */
285
289
  resolveInteraction(localThreadId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<ResolveInteractionResult>;
290
+ /** Update a loaded Codex-lineage native thread without spawning a runner. */
291
+ updateCollaborationMode(localThreadId: string, mode: SessionCollaborationMode): Promise<void>;
286
292
  listModels(runtime?: AgentRuntimeId): Promise<ModelListResponse | null>;
287
293
  getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
288
294
  setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
@@ -325,6 +331,8 @@ export declare class RunnerManager implements AgentCapabilities {
325
331
  private bufferForkTargetMessage;
326
332
  private reservedForkTarget;
327
333
  private deliverForkBufferedMessage;
334
+ private deliverMirrorMessage;
335
+ private failMirrorImageTransfer;
328
336
  private deliverRotateMessage;
329
337
  private spawnHandle;
330
338
  private onChildMessage;