@rynx-ai/runtime 0.1.11-beta.23 → 0.1.11-beta.25

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.
@@ -1,15 +1,29 @@
1
1
  import type { SkillMeta } from "@rynx-ai/core";
2
+ export interface MaterializedClaudePlugin {
3
+ pluginDir: string;
4
+ }
5
+ /** Stable Claude `--plugin-dir` owned by one Rynx Session. */
6
+ export declare function claudePluginDir(sessionId: string): string;
2
7
  /**
3
- * Materialize a selected catalog skill subset into a throwaway claude
8
+ * Return the existing Session-owned plugin directory as-is. It is the durable
9
+ * Provider input for this Session, not an immutable shared cache: runtime files
10
+ * created inside it (for example Python `__pycache__`) remain part of that
11
+ * Session and do not trigger SkillRef replay.
12
+ */
13
+ export declare function reuseClaudePlugin(sessionId: string): Promise<MaterializedClaudePlugin | null>;
14
+ /**
15
+ * Materialize a selected catalog skill subset into a Session-owned Claude
4
16
  * `--plugin-dir` (a dir with `skills/<name>/SKILL.md` + a `.claude-plugin/plugin.json`
5
17
  * manifest — claude's mechanism for exposing skills outside its host dirs, since
6
18
  * it doesn't scan `~/.rynx/skills`). Consumed by the claude-native live launch.
7
19
  *
20
+ * The committed path is deterministic and survives Provider/runner restarts.
21
+ * An existing directory is returned as-is. The first materialization is built
22
+ * in a sibling staging directory and renamed into the stable path so a crash
23
+ * cannot expose a partially copied plugin.
24
+ *
8
25
  * Returns `null` when there's nothing to add. Rynx-managed Claude sessions load
9
26
  * no host/project/local setting sources, so this explicit plugin directory is
10
27
  * the only native skill channel; the selected subset is enforced exactly.
11
28
  */
12
- export declare function materializeSkillPlugin(selected: SkillMeta[] | null | undefined): Promise<{
13
- pluginDir?: string;
14
- cleanup?: () => Promise<void>;
15
- } | null>;
29
+ export declare function materializeClaudePlugin(sessionId: string, selected: SkillMeta[] | null | undefined): Promise<MaterializedClaudePlugin | null>;
@@ -1,26 +1,70 @@
1
- import { cp, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
1
+ import { cp, mkdir, mkdtemp, rename, rm, stat, writeFile } from "node:fs/promises";
3
2
  import path from "node:path";
3
+ import { runtimeSessionStateDir } from "../runtime-state-paths.js";
4
+ /** Stable Claude `--plugin-dir` owned by one Rynx Session. */
5
+ export function claudePluginDir(sessionId) {
6
+ return path.join(runtimeSessionStateDir(sessionId), "claude-plugin");
7
+ }
4
8
  /**
5
- * Materialize a selected catalog skill subset into a throwaway claude
9
+ * Return the existing Session-owned plugin directory as-is. It is the durable
10
+ * Provider input for this Session, not an immutable shared cache: runtime files
11
+ * created inside it (for example Python `__pycache__`) remain part of that
12
+ * Session and do not trigger SkillRef replay.
13
+ */
14
+ export async function reuseClaudePlugin(sessionId) {
15
+ const pluginDir = claudePluginDir(sessionId);
16
+ try {
17
+ return (await stat(pluginDir)).isDirectory() ? { pluginDir } : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * Materialize a selected catalog skill subset into a Session-owned Claude
6
25
  * `--plugin-dir` (a dir with `skills/<name>/SKILL.md` + a `.claude-plugin/plugin.json`
7
26
  * manifest — claude's mechanism for exposing skills outside its host dirs, since
8
27
  * it doesn't scan `~/.rynx/skills`). Consumed by the claude-native live launch.
9
28
  *
29
+ * The committed path is deterministic and survives Provider/runner restarts.
30
+ * An existing directory is returned as-is. The first materialization is built
31
+ * in a sibling staging directory and renamed into the stable path so a crash
32
+ * cannot expose a partially copied plugin.
33
+ *
10
34
  * Returns `null` when there's nothing to add. Rynx-managed Claude sessions load
11
35
  * no host/project/local setting sources, so this explicit plugin directory is
12
36
  * the only native skill channel; the selected subset is enforced exactly.
13
37
  */
14
- export async function materializeSkillPlugin(selected) {
38
+ export async function materializeClaudePlugin(sessionId, selected) {
15
39
  if (selected == null || selected.length === 0)
16
40
  return null;
17
- const dir = await mkdtemp(path.join(tmpdir(), "rynx-claude-skills-"));
18
- const skillsRoot = path.join(dir, "skills");
19
- await mkdir(skillsRoot, { recursive: true });
20
- for (const skill of selected) {
21
- await cp(skill.dir, path.join(skillsRoot, skill.name), { recursive: true });
41
+ const reused = await reuseClaudePlugin(sessionId);
42
+ if (reused)
43
+ return reused;
44
+ const pluginDir = claudePluginDir(sessionId);
45
+ const stateDir = runtimeSessionStateDir(sessionId);
46
+ await mkdir(stateDir, { recursive: true, mode: 0o700 });
47
+ const stagingDir = await mkdtemp(path.join(stateDir, ".claude-plugin-stage-"));
48
+ try {
49
+ const skillsRoot = path.join(stagingDir, "skills");
50
+ await mkdir(skillsRoot, { recursive: true });
51
+ for (const skill of selected) {
52
+ await cp(skill.dir, path.join(skillsRoot, skill.name), { recursive: true });
53
+ }
54
+ await mkdir(path.join(stagingDir, ".claude-plugin"), { recursive: true });
55
+ await writeFile(path.join(stagingDir, ".claude-plugin", "plugin.json"), `${JSON.stringify({ name: "rynx-agent-skills", description: "Per-agent skill subset" }, null, 2)}\n`);
56
+ try {
57
+ await rename(stagingDir, pluginDir);
58
+ }
59
+ catch (error) {
60
+ const concurrent = await reuseClaudePlugin(sessionId);
61
+ if (!concurrent)
62
+ throw error;
63
+ return concurrent;
64
+ }
65
+ return { pluginDir };
66
+ }
67
+ finally {
68
+ await rm(stagingDir, { recursive: true, force: true });
22
69
  }
23
- await mkdir(path.join(dir, ".claude-plugin"), { recursive: true });
24
- await writeFile(path.join(dir, ".claude-plugin", "plugin.json"), `${JSON.stringify({ name: "rynx-agent-skills", description: "Per-agent skill subset" }, null, 2)}\n`);
25
- return { pluginDir: dir, cleanup: () => rm(dir, { recursive: true, force: true }) };
26
70
  }
@@ -176,6 +176,43 @@ function permissionRequest(id, payload, toolInput, suggestions) {
176
176
  const toolName = asString(payload.tool_name) ?? "tool";
177
177
  const command = asString(toolInput.command) ?? asString(toolInput.file_path);
178
178
  const cwd = asString(payload.cwd);
179
+ if (toolName === "ExitPlanMode") {
180
+ const plan = asString(toolInput.plan) ?? "Plan details were not provided by Claude.";
181
+ return {
182
+ interactionId: id,
183
+ kind: "permission",
184
+ title: "Plan review",
185
+ fields: [{
186
+ id: "feedback",
187
+ type: "text",
188
+ label: "What should change about the plan?",
189
+ required: false,
190
+ multiline: true,
191
+ placeholder: "Revision feedback (optional)",
192
+ }],
193
+ actions: [
194
+ {
195
+ id: "allow_auto",
196
+ label: "Yes, and use auto mode",
197
+ style: "primary",
198
+ requiresAnswers: false,
199
+ },
200
+ {
201
+ id: "allow_manual",
202
+ label: "Yes, manually approve edits",
203
+ requiresAnswers: false,
204
+ },
205
+ {
206
+ id: "deny_feedback",
207
+ label: "Reject with feedback",
208
+ style: "danger",
209
+ requiresAnswers: false,
210
+ },
211
+ ],
212
+ context: { toolName, summary: plan },
213
+ createdAt: Date.now(),
214
+ };
215
+ }
179
216
  return {
180
217
  interactionId: id,
181
218
  kind: "permission",
@@ -261,6 +298,31 @@ function nativeVerdict(hookKind, payload, result, suggestions) {
261
298
  },
262
299
  };
263
300
  }
301
+ if (toolName === "ExitPlanMode") {
302
+ const feedback = answerText(resolution.answers?.feedback).trim();
303
+ const behavior = resolution.actionId === "allow_auto" ||
304
+ resolution.actionId === "allow_manual"
305
+ ? "allow"
306
+ : "deny";
307
+ return {
308
+ hookSpecificOutput: {
309
+ hookEventName: "PermissionRequest",
310
+ decision: {
311
+ behavior,
312
+ ...(behavior === "deny" && feedback ? { message: feedback } : {}),
313
+ ...(behavior === "allow"
314
+ ? {
315
+ updatedPermissions: [{
316
+ type: "setMode",
317
+ mode: resolution.actionId === "allow_auto" ? "auto" : "default",
318
+ destination: "session",
319
+ }],
320
+ }
321
+ : {}),
322
+ },
323
+ },
324
+ };
325
+ }
264
326
  const suggestionMatch = /^allow_suggestion_(\d+)$/.exec(resolution.actionId);
265
327
  const suggestionIndex = suggestionMatch ? Number(suggestionMatch[1]) : -1;
266
328
  const selectedSuggestion = Number.isSafeInteger(suggestionIndex)
@@ -29,7 +29,7 @@ const MAX_SUBMISSION_OBSERVATIONS = 64;
29
29
  const INTERACTION_ACK_TIMEOUT_MS = 5_000;
30
30
  // Claude writes this synthetic user record after Escape (including a tool-use
31
31
  // interruption). It is lifecycle, not a new prompt. Kept aligned with
32
- // Omnigent's `_CLAUDE_INTERRUPT_RECORD_RE`.
32
+ // reference implementation's `_CLAUDE_INTERRUPT_RECORD_RE`.
33
33
  const CLAUDE_INTERRUPT_RECORD_RE = /^\[Request interrupted by user(?: for tool use)?\]$/;
34
34
  const INTERACTION_LEASE_TIMEOUT_MS = 30_000;
35
35
  function processIsAlive(pid) {
@@ -24,7 +24,7 @@
24
24
  import type { AgentEvent, UserContentPart } from "@rynx-ai/core";
25
25
  import type { CodexAppServerClient } from "./client.js";
26
26
  import type { McpStartupPlan } from "./mcp-startup.js";
27
- import type { ResumedTurn } from "./protocol.js";
27
+ import type { CollaborationModeKind, ResumedTurn } from "./protocol.js";
28
28
  export interface CodexForwarderSink {
29
29
  /** A turn began. `turnId` is codex's turn id, used to derive a stable
30
30
  * `responseId`. Start a fresh normalizer/response. */
@@ -58,6 +58,16 @@ export interface CodexForwarderSink {
58
58
  /** The thread showed activity (a turn/item began, so its rollout now exists).
59
59
  * Fired once; lets a parked `thread/resume` retry (reference implementation's ready signal). */
60
60
  onThreadActive?(): void;
61
+ /** The native TUI or another app-server client changed collaboration mode. */
62
+ onCollaborationModeChanged?(mode: CollaborationModeKind): void;
63
+ /** Codex's terminal-local Plan picker is not emitted by app-server today.
64
+ * Synthesize it only after a live Plan item and its Turn both complete. */
65
+ onPlanImplementationPrompt?(prompt: CodexPlanImplementationPrompt): void;
66
+ }
67
+ export interface CodexPlanImplementationPrompt {
68
+ threadId: string;
69
+ turnId: string;
70
+ text: string;
61
71
  }
62
72
  export interface CodexSessionForwarderOptions {
63
73
  /** Some Codex-lineage runtimes publish the final item one frame after
@@ -101,6 +111,8 @@ export declare class CodexSessionForwarder {
101
111
  /** Per-(thread,turn) position counter for items lacking a stable codex id
102
112
  * (peek-then-advance; advanced only on a successful claim). reference implementation anon path. */
103
113
  private readonly anonCounters;
114
+ private pendingPlanImplementation;
115
+ private replayingBackfill;
104
116
  constructor(client: CodexAppServerClient, sink: CodexForwarderSink, options?: CodexSessionForwarderOptions);
105
117
  /** Begin mirroring. Idempotent. */
106
118
  start(): void;
@@ -136,6 +148,9 @@ export declare class CodexSessionForwarder {
136
148
  * not doubled.
137
149
  */
138
150
  replayBackfill(turns: ResumedTurn[]): void;
151
+ /** Suppress our synthesized picker when a future app-server emits the native
152
+ * `plan_implementation` request itself. */
153
+ noteNativePlanImplementationPrompt(turnId?: string): void;
139
154
  /** Fail an open response exactly once when its observer or terminal exits. */
140
155
  failOpenTurn(error: Error): boolean;
141
156
  private handle;
@@ -86,6 +86,8 @@ export class CodexSessionForwarder {
86
86
  /** Per-(thread,turn) position counter for items lacking a stable codex id
87
87
  * (peek-then-advance; advanced only on a successful claim). reference implementation anon path. */
88
88
  anonCounters = new Map();
89
+ pendingPlanImplementation = null;
90
+ replayingBackfill = false;
89
91
  constructor(client, sink, options = {}) {
90
92
  this.client = client;
91
93
  this.sink = sink;
@@ -191,22 +193,37 @@ export class CodexSessionForwarder {
191
193
  * not doubled.
192
194
  */
193
195
  replayBackfill(turns) {
194
- for (const turn of turns) {
195
- const turnId = turn.id ?? turn.turnId;
196
- if (turnId)
197
- this.currentTurnIdValue = turnId;
198
- this.ensureTurn();
199
- for (const item of turn.items ?? [])
200
- this.processCompletedItem(item);
201
- if (codexResumeTerminalStatus(turn) === undefined)
202
- continue;
203
- const mapped = mapCodexNotification("turn/completed", { turn });
204
- this.turnOpen = false;
205
- this.currentTurnIdValue = null;
206
- if (mapped.fatalError)
207
- this.sink.onTurnError(mapped.fatalError);
208
- else
209
- this.sink.onTurnEnd(mapped.usage);
196
+ this.replayingBackfill = true;
197
+ try {
198
+ for (const turn of turns) {
199
+ const turnId = turn.id ?? turn.turnId;
200
+ if (turnId)
201
+ this.currentTurnIdValue = turnId;
202
+ this.ensureTurn();
203
+ for (const item of turn.items ?? [])
204
+ this.processCompletedItem(item);
205
+ if (codexResumeTerminalStatus(turn) === undefined)
206
+ continue;
207
+ const mapped = mapCodexNotification("turn/completed", { turn });
208
+ this.turnOpen = false;
209
+ this.currentTurnIdValue = null;
210
+ if (mapped.fatalError)
211
+ this.sink.onTurnError(mapped.fatalError);
212
+ else
213
+ this.sink.onTurnEnd(mapped.usage);
214
+ }
215
+ }
216
+ finally {
217
+ this.replayingBackfill = false;
218
+ }
219
+ }
220
+ /** Suppress our synthesized picker when a future app-server emits the native
221
+ * `plan_implementation` request itself. */
222
+ noteNativePlanImplementationPrompt(turnId) {
223
+ if (!this.pendingPlanImplementation)
224
+ return;
225
+ if (!turnId || this.pendingPlanImplementation.turnId === turnId) {
226
+ this.pendingPlanImplementation = null;
210
227
  }
211
228
  }
212
229
  /** Fail an open response exactly once when its observer or terminal exits. */
@@ -248,6 +265,14 @@ export class CodexSessionForwarder {
248
265
  this.handleMcpStartupStatus(params);
249
266
  return;
250
267
  }
268
+ if (method === "thread/settings/updated") {
269
+ const threadSettings = params?.threadSettings;
270
+ const mode = (threadSettings?.collaborationMode ?? threadSettings?.collaboration_mode)?.mode;
271
+ if (mode === "plan" || mode === "default") {
272
+ this.sink.onCollaborationModeChanged?.(mode);
273
+ }
274
+ return;
275
+ }
251
276
  if (isThreadIdle(method, params) || isModelOutput(method, params)) {
252
277
  this.settleMcpStartup();
253
278
  }
@@ -400,6 +425,14 @@ export class CodexSessionForwarder {
400
425
  }
401
426
  settle(completion) {
402
427
  this.flushDeferredAssistantMessage();
428
+ const completedTurnId = this.pendingCompletionTurnId ?? this.currentTurnIdValue;
429
+ const planPrompt = completion.kind === "end" &&
430
+ this.pendingPlanImplementation?.turnId === completedTurnId
431
+ ? this.pendingPlanImplementation
432
+ : null;
433
+ if (this.pendingPlanImplementation?.turnId === completedTurnId) {
434
+ this.pendingPlanImplementation = null;
435
+ }
403
436
  this.turnOpen = false;
404
437
  this.currentTurnIdValue = null;
405
438
  this.pendingCompletionTurnId = null;
@@ -411,8 +444,11 @@ export class CodexSessionForwarder {
411
444
  else
412
445
  this.sink.onTurnEnd(completion.usage);
413
446
  }
414
- else
447
+ else {
415
448
  this.sink.onTurnEnd(completion.usage);
449
+ if (planPrompt)
450
+ this.sink.onPlanImplementationPrompt?.(planPrompt);
451
+ }
416
452
  }
417
453
  handleMcpStartupStatus(params) {
418
454
  const update = params;
@@ -504,6 +540,14 @@ export class CodexSessionForwarder {
504
540
  this.sink.onUserMessage?.(userContent);
505
541
  return;
506
542
  }
543
+ if (item.type === "plan" && !this.replayingBackfill) {
544
+ const text = item.text?.trim();
545
+ const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId;
546
+ const threadId = this.currentThreadIdValue;
547
+ if (text && turnId && threadId) {
548
+ this.pendingPlanImplementation = { threadId, turnId, text };
549
+ }
550
+ }
507
551
  const mapped = mapCodexItem("item/completed", item);
508
552
  const canonicalEvents = mapped.events.filter((event) => event.type !== "runtime_debug");
509
553
  if (canonicalEvents.length === 0)
@@ -208,7 +208,11 @@ export function mapCodexItem(method, item) {
208
208
  return { events };
209
209
  }
210
210
  case "plan": {
211
- events.push({ type: "runtime_debug", channel: "codexEvent", data: { method, item } });
211
+ const text = item.text?.trim() ?? "";
212
+ if (isEnd && text) {
213
+ events.push({ type: "message_completed", itemId: item.id, text });
214
+ return { events, finalText: text };
215
+ }
212
216
  return { events };
213
217
  }
214
218
  case "reasoning": {
@@ -276,6 +280,17 @@ export function mapCodexNotification(method, params) {
276
280
  }
277
281
  return { events };
278
282
  }
283
+ case "item/plan/delta": {
284
+ const delta = typed.params.delta ?? "";
285
+ if (delta) {
286
+ events.push({
287
+ type: "token",
288
+ text: delta,
289
+ metadata: { source: "app_server", itemId: typed.params.itemId },
290
+ });
291
+ }
292
+ return { events };
293
+ }
279
294
  case "item/reasoning/summaryTextDelta":
280
295
  case "item/reasoning/textDelta": {
281
296
  const p = typed.params;
@@ -87,6 +87,16 @@ export interface PermissionProfileModificationParams {
87
87
  export type PermissionSelection = string | PermissionProfileSelectionParams;
88
88
  /** Open since Codex App Server 0.144; values are advertised by `model/list`. */
89
89
  export type ReasoningEffort = string;
90
+ export type CollaborationModeKind = "plan" | "default";
91
+ /** Full Codex-lineage mode snapshot required by the App Server wire protocol. */
92
+ export interface CollaborationMode {
93
+ mode: CollaborationModeKind;
94
+ settings: {
95
+ model: string;
96
+ reasoning_effort: ReasoningEffort | null;
97
+ developer_instructions: string | null;
98
+ };
99
+ }
90
100
  export interface ThreadStartParams {
91
101
  model?: string | null;
92
102
  modelProvider?: string | null;
@@ -102,6 +112,8 @@ export interface ThreadStartParams {
102
112
  baseInstructions?: string | null;
103
113
  developerInstructions?: string | null;
104
114
  ephemeral?: boolean | null;
115
+ /** Native UI intent for a context-clearing fresh thread. */
116
+ sessionStartSource?: "clear" | string;
105
117
  }
106
118
  export interface ThreadResumeParams extends ThreadStartParams {
107
119
  threadId: string;
@@ -157,6 +169,7 @@ export interface TurnStartParams {
157
169
  permissions?: PermissionSelection | null;
158
170
  model?: string | null;
159
171
  effort?: ReasoningEffort | null;
172
+ collaborationMode?: CollaborationMode | null;
160
173
  }
161
174
  export interface TurnInterruptParams {
162
175
  threadId: string;
@@ -304,7 +317,7 @@ export interface ThreadSettingsUpdateParams {
304
317
  serviceTier?: string | null;
305
318
  effort?: ReasoningEffort | null;
306
319
  summary?: string | null;
307
- collaborationMode?: Record<string, unknown> | null;
320
+ collaborationMode?: CollaborationMode | null;
308
321
  personality?: string | null;
309
322
  }
310
323
  export interface ModelListParams {
@@ -478,6 +491,14 @@ export interface QueueStatusNotificationParams {
478
491
  export type ServerNotification = {
479
492
  method: "thread/started";
480
493
  params: ThreadStartedNotificationParams;
494
+ } | {
495
+ method: "thread/settings/updated";
496
+ params: {
497
+ threadId: string;
498
+ threadSettings: {
499
+ collaborationMode?: CollaborationMode | null;
500
+ } & Record<string, unknown>;
501
+ };
481
502
  } | {
482
503
  method: "turn/started";
483
504
  params: TurnStartedNotificationParams;
@@ -499,6 +520,9 @@ export type ServerNotification = {
499
520
  } | {
500
521
  method: "item/agentMessage/delta";
501
522
  params: AgentMessageDeltaNotificationParams;
523
+ } | {
524
+ method: "item/plan/delta";
525
+ params: AgentMessageDeltaNotificationParams;
502
526
  } | {
503
527
  method: "item/reasoning/summaryTextDelta";
504
528
  params: ReasoningSummaryTextDeltaNotificationParams;
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, type LiveSessionFailure, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
1
+ import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type LiveSessionFailure, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
2
2
  import { type AgentRuntimeId } from "@rynx-ai/core";
3
3
  import { type AppConfig } from "@rynx-ai/core";
4
4
  import { createCodexChildEnv } from "./codex-child-env.js";
@@ -189,6 +189,13 @@ export declare class LocalAgentHost implements CodexCapabilities {
189
189
  private createAppServerClient;
190
190
  /** Resolve a pending native question/approval without opening a new Turn. */
191
191
  resolveInteraction(localThreadId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<ResolveInteractionResult>;
192
+ /** Apply collaboration mode to an already-loaded Codex-lineage thread. The
193
+ * caller persists only after this native RPC succeeds. */
194
+ updateCollaborationMode(localThreadId: string, mode: SessionCollaborationMode): Promise<void>;
195
+ /** Apply one explicit collaboration mode to the current native thread and
196
+ * remember only a successful RPC. A supplied client is already initialized
197
+ * and remains owned by its caller (used by cold-resume preload). */
198
+ private applyLiveCollaborationMode;
192
199
  /**
193
200
  * The command to run in a session's live terminal so it co-drives the codex
194
201
  * app-server thread (Phase D). Returns `null` when live-terminal is off, the
@@ -227,7 +234,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
227
234
  * Fresh sessions already connected their discovery listener before launch;
228
235
  * this is therefore an idempotent no-op for them and for a healthy observer. */
229
236
  startLiveCodexObserver(localThreadId: string): void;
230
- /** Omnigent treats the forwarder as a required component of one native
237
+ /** reference implementation treats the forwarder as a required component of one native
231
238
  * lifecycle: if its transport dies, it closes the app-server instead of
232
239
  * accepting turns that can no longer reach the canonical mirror. */
233
240
  private failObserverLifecycle;
@@ -237,7 +244,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
237
244
  * Subscribe the forwarder connection to a thread (reference implementation's
238
245
  * `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
239
246
  * turn, so `thread/resume` is retried: park until the forwarder observes the
240
- * thread active, then retry. Like Omnigent, the first attempt always uses
247
+ * thread active, then retry. Like reference implementation, the first attempt always uses
241
248
  * `excludeTurns`: a known-session cold resume therefore never reconstructs
242
249
  * historical running/completed state. Only a fresh thread whose first attempt
243
250
  * failed as not-ready retries without `excludeTurns`, backfilling the newly
@@ -273,7 +280,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
273
280
  * forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
274
281
  * the bridge instead of a short race that falls back to a second output path.
275
282
  */
276
- injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
283
+ injectMessage(localThreadId: string, input: RuntimeUserInput | string, options?: RuntimeTurnOptions): Promise<InjectResult>;
277
284
  /**
278
285
  * Interrupt the session's active turn — the web Stop button. codex: the
279
286
  * app-server `turn/interrupt` on the active `{threadId, turnId}` (exactly what
@@ -288,7 +295,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
288
295
  deferClaudeInteractionCleanup?: boolean;
289
296
  }): void;
290
297
  /** Tear down one codex-lineage native runtime without deleting its durable
291
- * session-store binding. Omnigent couples its auxiliary Terminal, observer,
298
+ * session-store binding. reference implementation couples its auxiliary Terminal, observer,
292
299
  * forwarder and per-session app-server as one disposable runtime envelope;
293
300
  * the next message recreates that envelope and cold-resumes the native id. */
294
301
  teardownLiveCodexSession(localThreadId: string, error?: Error): boolean;
@@ -321,8 +328,9 @@ export declare class LocalAgentHost implements CodexCapabilities {
321
328
  * would fail and (pre-reference implementation) fall through to a second output path. */
322
329
  private waitInjector;
323
330
  /** Attach a session's tmux pane injector (from the runner-child, which owns the
324
- * terminal registry — the host does not hold tmux). Ignored for codex sessions
325
- * (they inject via the app-server). Idempotent. */
331
+ * terminal registry — the host does not hold tmux). Claude uses it for normal
332
+ * message injection; Codex uses it only to dismiss the exact TUI-local Plan
333
+ * picker after its synthetic Web form is submitted. Idempotent. */
326
334
  attachTerminalInjector(localThreadId: string, injector: TerminalInjector): void;
327
335
  /** Close an active native response when its terminal or runner disappears. */
328
336
  failLiveSession(localThreadId: string, error: Error): boolean;