@rynx-ai/runtime 0.1.0 → 0.1.10-beta.2

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.
Files changed (56) hide show
  1. package/dist/claude/executor.d.ts +3 -5
  2. package/dist/claude/executor.js +3 -5
  3. package/dist/claude/native-bridge.d.ts +74 -17
  4. package/dist/claude/native-bridge.js +225 -30
  5. package/dist/claude/native-hook-main.js +327 -38
  6. package/dist/claude/native-hooks.d.ts +3 -2
  7. package/dist/claude/native-hooks.js +15 -6
  8. package/dist/claude/native-integration.d.ts +123 -16
  9. package/dist/claude/native-integration.js +624 -81
  10. package/dist/claude/settings.d.ts +8 -0
  11. package/dist/claude/settings.js +50 -0
  12. package/dist/claude/transcript.d.ts +2 -2
  13. package/dist/claude/transcript.js +14 -3
  14. package/dist/codex/rollout-synth.d.ts +8 -3
  15. package/dist/codex/rollout-synth.js +65 -32
  16. package/dist/codex-app-server/client.d.ts +27 -40
  17. package/dist/codex-app-server/client.js +1134 -99
  18. package/dist/codex-app-server/forwarder.d.ts +36 -10
  19. package/dist/codex-app-server/forwarder.js +146 -28
  20. package/dist/codex-app-server/mapping.d.ts +1 -1
  21. package/dist/codex-app-server/mapping.js +64 -5
  22. package/dist/codex-app-server/protocol.d.ts +269 -4
  23. package/dist/codex-app-server/transport.d.ts +20 -5
  24. package/dist/codex-app-server/transport.js +93 -40
  25. package/dist/codex-app-server/ws-channel.d.ts +3 -3
  26. package/dist/codex-app-server/ws-channel.js +23 -7
  27. package/dist/codex-child-env.js +33 -0
  28. package/dist/codex-home.d.ts +16 -6
  29. package/dist/codex-home.js +46 -15
  30. package/dist/codex-session-store.d.ts +2 -1
  31. package/dist/host.d.ts +38 -38
  32. package/dist/host.js +626 -121
  33. package/dist/index.d.ts +4 -3
  34. package/dist/index.js +1 -1
  35. package/dist/input-resources.d.ts +13 -0
  36. package/dist/input-resources.js +67 -0
  37. package/dist/interactions.d.ts +61 -0
  38. package/dist/interactions.js +236 -0
  39. package/dist/models-catalog.d.ts +5 -13
  40. package/dist/models-catalog.js +60 -9
  41. package/dist/runner/child.d.ts +9 -1
  42. package/dist/runner/child.js +100 -19
  43. package/dist/runner/manager.d.ts +79 -11
  44. package/dist/runner/manager.js +423 -43
  45. package/dist/runner/protocol.d.ts +30 -11
  46. package/dist/runner-main.js +9 -6
  47. package/dist/runtime-status.js +1 -1
  48. package/dist/terminal/claude-tui.d.ts +8 -3
  49. package/dist/terminal/claude-tui.js +6 -2
  50. package/dist/terminal/codex-tui.d.ts +3 -3
  51. package/dist/terminal/codex-tui.js +1 -1
  52. package/dist/terminal/registry.d.ts +1 -1
  53. package/dist/terminal/registry.js +1 -1
  54. package/dist/terminal/tmux.d.ts +6 -6
  55. package/dist/terminal/tmux.js +10 -10
  56. package/package.json +8 -3
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Persistent codex session forwarder — the heart of omnigent's codex-native
2
+ * Persistent codex session forwarder — the heart of reference implementation's codex-native
3
3
  * model, ported to rynx. It subscribes to a session's app-server for the whole
4
4
  * session lifetime (not just one turn) and maps every thread notification to a
5
5
  * typed {@link AgentEvent}, so turns started by ANY client — the web composer OR
@@ -10,9 +10,9 @@
10
10
  * (or a fatal error) closes it. The turn's codex `turnId` is surfaced on
11
11
  * {@link CodexForwarderSink.onTurnStart} so the normalizer's `responseId` is
12
12
  * DETERMINISTIC (`resp_codex_<turnId>`) — the same turn always maps to the same
13
- * response id across snapshot/live, so items converge (omnigent's `codex_{turnId}`).
13
+ * response id across snapshot/live, so items converge (reference implementation's `codex_{turnId}`).
14
14
  *
15
- * omnigent's subscribe-then-mirror model (verified against real codex 0.142.5):
15
+ * reference implementation's subscribe-then-mirror model (verified against real codex 0.142.5):
16
16
  * the app-server delivers a thread's item/turn notifications to a connection that
17
17
  * has `thread/resume`d it. A fresh TUI-created thread has no rollout until its
18
18
  * first turn, so resume is parked until {@link CodexForwarderSink.onThreadActive}
@@ -21,7 +21,7 @@
21
21
  * {@link CodexSessionForwarder.replayBackfill}. Backfill and the live stream are
22
22
  * de-duplicated by codex item id.
23
23
  */
24
- import type { AgentEvent } from "@rynx-ai/core";
24
+ import type { AgentEvent, UserContentPart } from "@rynx-ai/core";
25
25
  import type { CodexAppServerClient } from "./client.js";
26
26
  import type { ResumedTurn } from "./protocol.js";
27
27
  export interface CodexForwarderSink {
@@ -36,30 +36,47 @@ export interface CodexForwarderSink {
36
36
  onTurnError(error: Error): void;
37
37
  /** The user's turn text (sourced from codex's `userMessage` item), so a
38
38
  * co-driving TUI's prompt is recorded even though this process never injected it. */
39
- onUserMessage?(text: string): void;
39
+ onUserMessage?(content: string | UserContentPart[]): void;
40
40
  /** A thread was announced on the app-server (a `--remote` TUI creating a thread,
41
41
  * or a resume). The host persists the id + starts the subscribe loop. */
42
42
  onThreadStarted?(threadId: string): void;
43
43
  /** The thread showed activity (a turn/item began, so its rollout now exists).
44
- * Fired once; lets a parked `thread/resume` retry (omnigent's ready signal). */
44
+ * Fired once; lets a parked `thread/resume` retry (reference implementation's ready signal). */
45
45
  onThreadActive?(): void;
46
46
  }
47
+ export interface CodexSessionForwarderOptions {
48
+ /** Some Codex-lineage runtimes publish the final item one frame after
49
+ * `turn/completed`. Keep the Turn open briefly so that item cannot create a
50
+ * second, permanently-running response. */
51
+ turnCompletionGraceMs?: number;
52
+ /** Some runtimes complete the final assistant message before publishing the
53
+ * preceding reasoning item. Briefly hold that message so canonical item order
54
+ * remains reasoning -> final answer. */
55
+ assistantMessageGraceMs?: number;
56
+ /** Surface Traex's provider-capacity queue as a canonical running status. */
57
+ surfaceQueueStatus?: boolean;
58
+ }
47
59
  export declare class CodexSessionForwarder {
48
60
  private readonly client;
49
61
  private readonly sink;
62
+ private readonly options;
50
63
  private unsubscribe;
51
64
  private turnOpen;
52
65
  private currentTurnIdValue;
53
66
  private currentThreadIdValue;
54
67
  private activeSignaled;
68
+ private completionTimer;
69
+ private assistantMessageTimer;
70
+ private deferredAssistantMessage;
71
+ private pendingCompletion;
55
72
  /** Completed-item dedup keys already mirrored (live vs resume backfill). Key =
56
73
  * `threadId:turnId:item.id`; anonymous items use a per-(thread,turn) position
57
- * counter. Mirrors omnigent `_completed_item_key` + `synced_item_keys`. */
74
+ * counter. Mirrors reference implementation `_completed_item_key` + `synced_item_keys`. */
58
75
  private readonly seenCompletedItems;
59
76
  /** Per-(thread,turn) position counter for items lacking a stable codex id
60
- * (peek-then-advance; advanced only on a successful claim). omnigent anon path. */
77
+ * (peek-then-advance; advanced only on a successful claim). reference implementation anon path. */
61
78
  private readonly anonCounters;
62
- constructor(client: CodexAppServerClient, sink: CodexForwarderSink);
79
+ constructor(client: CodexAppServerClient, sink: CodexForwarderSink, options?: CodexSessionForwarderOptions);
63
80
  /** Begin mirroring. Idempotent. */
64
81
  start(): void;
65
82
  stop(): void;
@@ -78,14 +95,23 @@ export declare class CodexSessionForwarder {
78
95
  */
79
96
  replayBackfill(turns: ResumedTurn[]): void;
80
97
  private handle;
98
+ private scheduleCompletion;
99
+ private refreshCompletionGrace;
100
+ private flushPendingCompletion;
101
+ private settle;
81
102
  /** Map + emit one completed codex item, deduped by a TOTAL key and routing the
82
103
  * user echo to {@link CodexForwarderSink.onUserMessage}. Shared by live + backfill. */
83
104
  private processCompletedItem;
105
+ private claimCompletedItem;
106
+ private emitCompletedItem;
107
+ private shouldDeferAssistantMessage;
108
+ private deferAssistantMessage;
109
+ private flushDeferredAssistantMessage;
84
110
  /** Build a total dedup key for one completed item. Stable-id items use
85
111
  * `threadId:turnId:item.id` — identical across replay + live, so the second
86
112
  * delivery is dropped. Items without a codex id fall back to a per-(thread,turn)
87
113
  * position counter (peeked here; advanced only on a successful claim, via
88
- * {@link advanceAnonCounter}). Mirrors omnigent `_completed_item_key`. */
114
+ * {@link advanceAnonCounter}). Mirrors reference implementation `_completed_item_key`. */
89
115
  private completedItemKey;
90
116
  private advanceAnonCounter;
91
117
  private ensureTurn;
@@ -1,3 +1,4 @@
1
+ import { codexUserContent } from "../input-resources.js";
1
2
  import { mapCodexItem, mapCodexNotification } from "./mapping.js";
2
3
  function threadIdFrom(params) {
3
4
  const p = params;
@@ -7,19 +8,20 @@ function turnIdFrom(params) {
7
8
  const p = params;
8
9
  return p?.turnId ?? p?.turn?.id;
9
10
  }
10
- function userMessageText(item) {
11
+ function userMessageContent(item) {
11
12
  if (item.type !== "userMessage")
12
13
  return undefined;
13
- const content = item.content ?? [];
14
- const text = content
15
- .filter((c) => c.type === "text" && typeof c.text === "string")
16
- .map((c) => c.text)
17
- .join("")
18
- .trim();
19
- return text || undefined;
14
+ const parts = codexUserContent(item.content);
15
+ if (parts.length === 0)
16
+ return undefined;
17
+ if (parts.every((part) => part.type === "input_text")) {
18
+ const text = parts.map((part) => part.text).join("").trim();
19
+ return text || undefined;
20
+ }
21
+ return parts;
20
22
  }
21
23
  /** Whether a notification implies the thread is now active (its first turn has
22
- * begun, so the rollout exists). Mirrors omnigent's `_event_indicates_thread_active`. */
24
+ * begun, so the rollout exists). Mirrors reference implementation's `_event_indicates_thread_active`. */
23
25
  function indicatesActive(method, params) {
24
26
  if (method.startsWith("turn/") || method.startsWith("item/"))
25
27
  return true;
@@ -32,21 +34,27 @@ function indicatesActive(method, params) {
32
34
  export class CodexSessionForwarder {
33
35
  client;
34
36
  sink;
37
+ options;
35
38
  unsubscribe = null;
36
39
  turnOpen = false;
37
40
  currentTurnIdValue = null;
38
41
  currentThreadIdValue = null;
39
42
  activeSignaled = false;
43
+ completionTimer = null;
44
+ assistantMessageTimer = null;
45
+ deferredAssistantMessage = null;
46
+ pendingCompletion = null;
40
47
  /** Completed-item dedup keys already mirrored (live vs resume backfill). Key =
41
48
  * `threadId:turnId:item.id`; anonymous items use a per-(thread,turn) position
42
- * counter. Mirrors omnigent `_completed_item_key` + `synced_item_keys`. */
49
+ * counter. Mirrors reference implementation `_completed_item_key` + `synced_item_keys`. */
43
50
  seenCompletedItems = new Set();
44
51
  /** Per-(thread,turn) position counter for items lacking a stable codex id
45
- * (peek-then-advance; advanced only on a successful claim). omnigent anon path. */
52
+ * (peek-then-advance; advanced only on a successful claim). reference implementation anon path. */
46
53
  anonCounters = new Map();
47
- constructor(client, sink) {
54
+ constructor(client, sink, options = {}) {
48
55
  this.client = client;
49
56
  this.sink = sink;
57
+ this.options = options;
50
58
  }
51
59
  /** Begin mirroring. Idempotent. */
52
60
  start() {
@@ -59,6 +67,8 @@ export class CodexSessionForwarder {
59
67
  stop() {
60
68
  this.unsubscribe?.();
61
69
  this.unsubscribe = null;
70
+ this.flushPendingCompletion();
71
+ this.flushDeferredAssistantMessage();
62
72
  if (this.turnOpen) {
63
73
  this.turnOpen = false;
64
74
  this.sink.onTurnEnd();
@@ -91,8 +101,14 @@ export class CodexSessionForwarder {
91
101
  this.ensureTurn();
92
102
  for (const item of turn.items ?? [])
93
103
  this.processCompletedItem(item);
104
+ if (turn.status === "inProgress")
105
+ continue;
106
+ const mapped = mapCodexNotification("turn/completed", { turn });
94
107
  this.turnOpen = false;
95
- this.sink.onTurnEnd();
108
+ if (mapped.fatalError)
109
+ this.sink.onTurnError(mapped.fatalError);
110
+ else
111
+ this.sink.onTurnEnd(mapped.usage);
96
112
  }
97
113
  }
98
114
  handle(method, params) {
@@ -110,63 +126,165 @@ export class CodexSessionForwarder {
110
126
  this.activeSignaled = true;
111
127
  this.sink.onThreadActive?.();
112
128
  }
129
+ // `queue/status` is a Traex extension. Keep the shared Codex forwarder
130
+ // behavior unchanged unless the Traex host explicitly opts in.
131
+ if (method === "queue/status" && !this.options.surfaceQueueStatus)
132
+ return;
113
133
  const carriedTurnId = turnIdFrom(params);
114
- if (carriedTurnId)
115
- this.currentTurnIdValue = carriedTurnId;
116
134
  if (method === "turn/started") {
135
+ this.flushPendingCompletion();
136
+ this.flushDeferredAssistantMessage();
137
+ if (carriedTurnId)
138
+ this.currentTurnIdValue = carriedTurnId;
117
139
  this.ensureTurn();
118
140
  return;
119
141
  }
142
+ if (carriedTurnId)
143
+ this.currentTurnIdValue = carriedTurnId;
120
144
  // Completed items (user echo, assistant, tool, reasoning) go through the
121
145
  // deduped path so a resume backfill and the live stream never double them.
122
146
  if (method === "item/completed") {
123
147
  const item = params?.item;
124
148
  if (item) {
149
+ if (this.shouldDeferAssistantMessage(item)) {
150
+ this.deferAssistantMessage(item);
151
+ this.refreshCompletionGrace();
152
+ return;
153
+ }
154
+ // A late reasoning item belongs before the held final answer. Any other
155
+ // completed item establishes that the held message was not final and
156
+ // must retain its original position.
157
+ if (item.type !== "reasoning")
158
+ this.flushDeferredAssistantMessage();
125
159
  this.processCompletedItem(item);
160
+ this.refreshCompletionGrace();
126
161
  return;
127
162
  }
128
163
  }
129
164
  const mapped = mapCodexNotification(method, params);
130
- if (mapped.events.length || mapped.turnCompleted || mapped.fatalError) {
165
+ // Provider startup/onboarding notifications that rynx does not model map
166
+ // to runtime_debug. They are telemetry, not evidence of a model Turn. In
167
+ // particular, Traex emits hook-trust lifecycle notifications while merely
168
+ // resuming a TUI; opening a response for those leaves Chat permanently
169
+ // running because no turn/completed will follow.
170
+ const canonicalEvents = mapped.events.filter((event) => event.type !== "runtime_debug");
171
+ if (canonicalEvents.some((event) => event.type !== "reasoning_delta" && event.type !== "reasoning_completed")) {
172
+ this.flushDeferredAssistantMessage();
173
+ }
174
+ if (canonicalEvents.length || mapped.turnCompleted || mapped.fatalError) {
131
175
  this.ensureTurn();
132
176
  }
133
- for (const event of mapped.events) {
177
+ for (const event of canonicalEvents) {
134
178
  this.sink.onEvent(event);
135
179
  }
136
180
  if (mapped.fatalError) {
137
- this.turnOpen = false;
138
- this.sink.onTurnError(mapped.fatalError);
181
+ this.scheduleCompletion({ kind: "error", error: mapped.fatalError });
139
182
  return;
140
183
  }
141
184
  if (mapped.turnCompleted) {
142
- this.turnOpen = false;
143
- this.sink.onTurnEnd(mapped.usage);
185
+ this.scheduleCompletion({ kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
186
+ }
187
+ else {
188
+ this.refreshCompletionGrace();
189
+ }
190
+ }
191
+ scheduleCompletion(completion) {
192
+ const graceMs = this.options.turnCompletionGraceMs ?? 0;
193
+ if (graceMs <= 0) {
194
+ this.settle(completion);
195
+ return;
144
196
  }
197
+ this.pendingCompletion = completion;
198
+ this.refreshCompletionGrace();
199
+ }
200
+ refreshCompletionGrace() {
201
+ if (!this.pendingCompletion)
202
+ return;
203
+ const graceMs = this.options.turnCompletionGraceMs ?? 0;
204
+ if (graceMs <= 0)
205
+ return;
206
+ if (this.completionTimer)
207
+ clearTimeout(this.completionTimer);
208
+ this.completionTimer = setTimeout(() => this.flushPendingCompletion(), graceMs);
209
+ this.completionTimer.unref?.();
210
+ }
211
+ flushPendingCompletion() {
212
+ if (this.completionTimer)
213
+ clearTimeout(this.completionTimer);
214
+ this.completionTimer = null;
215
+ const completion = this.pendingCompletion;
216
+ this.pendingCompletion = null;
217
+ if (completion)
218
+ this.settle(completion);
219
+ }
220
+ settle(completion) {
221
+ this.flushDeferredAssistantMessage();
222
+ this.turnOpen = false;
223
+ if (completion.kind === "error")
224
+ this.sink.onTurnError(completion.error);
225
+ else
226
+ this.sink.onTurnEnd(completion.usage);
145
227
  }
146
228
  /** Map + emit one completed codex item, deduped by a TOTAL key and routing the
147
229
  * user echo to {@link CodexForwarderSink.onUserMessage}. Shared by live + backfill. */
148
230
  processCompletedItem(item) {
231
+ if (!this.claimCompletedItem(item))
232
+ return;
233
+ this.emitCompletedItem(item);
234
+ }
235
+ claimCompletedItem(item) {
149
236
  const { key, isAnon } = this.completedItemKey(item);
150
237
  if (this.seenCompletedItems.has(key))
151
- return;
238
+ return false;
152
239
  this.seenCompletedItems.add(key);
153
240
  if (isAnon)
154
241
  this.advanceAnonCounter();
155
- this.ensureTurn();
156
- const userText = userMessageText(item);
157
- if (userText !== undefined) {
158
- this.sink.onUserMessage?.(userText);
242
+ return true;
243
+ }
244
+ emitCompletedItem(item) {
245
+ const userContent = userMessageContent(item);
246
+ if (userContent !== undefined) {
247
+ this.ensureTurn();
248
+ this.sink.onUserMessage?.(userContent);
159
249
  return;
160
250
  }
161
251
  const mapped = mapCodexItem("item/completed", item);
162
- for (const event of mapped.events)
252
+ const canonicalEvents = mapped.events.filter((event) => event.type !== "runtime_debug");
253
+ if (canonicalEvents.length === 0)
254
+ return;
255
+ this.ensureTurn();
256
+ for (const event of canonicalEvents)
163
257
  this.sink.onEvent(event);
164
258
  }
259
+ shouldDeferAssistantMessage(item) {
260
+ if ((this.options.assistantMessageGraceMs ?? 0) <= 0 || item.type !== "agentMessage") {
261
+ return false;
262
+ }
263
+ return Boolean(item.text?.trim());
264
+ }
265
+ deferAssistantMessage(item) {
266
+ if (!this.claimCompletedItem(item))
267
+ return;
268
+ this.flushDeferredAssistantMessage();
269
+ this.deferredAssistantMessage = item;
270
+ const graceMs = this.options.assistantMessageGraceMs ?? 0;
271
+ this.assistantMessageTimer = setTimeout(() => this.flushDeferredAssistantMessage(), graceMs);
272
+ this.assistantMessageTimer.unref?.();
273
+ }
274
+ flushDeferredAssistantMessage() {
275
+ if (this.assistantMessageTimer)
276
+ clearTimeout(this.assistantMessageTimer);
277
+ this.assistantMessageTimer = null;
278
+ const item = this.deferredAssistantMessage;
279
+ this.deferredAssistantMessage = null;
280
+ if (item)
281
+ this.emitCompletedItem(item);
282
+ }
165
283
  /** Build a total dedup key for one completed item. Stable-id items use
166
284
  * `threadId:turnId:item.id` — identical across replay + live, so the second
167
285
  * delivery is dropped. Items without a codex id fall back to a per-(thread,turn)
168
286
  * position counter (peeked here; advanced only on a successful claim, via
169
- * {@link advanceAnonCounter}). Mirrors omnigent `_completed_item_key`. */
287
+ * {@link advanceAnonCounter}). Mirrors reference implementation `_completed_item_key`. */
170
288
  completedItemKey(item) {
171
289
  const threadId = this.currentThreadIdValue ?? "thread";
172
290
  const turnId = this.currentTurnIdValue ?? "turn";
@@ -2,7 +2,7 @@
2
2
  * Pure codex app-server notification → {@link AgentEvent} mapping. Extracted from
3
3
  * the executor so BOTH the per-turn executor and the persistent session
4
4
  * forwarder (which mirrors every thread turn — web- AND TUI-initiated — into the
5
- * canonical log, omnigent's codex-native model) share one mapping.
5
+ * canonical log, reference implementation's codex-native model) share one mapping.
6
6
  */
7
7
  import type { AgentEvent } from "@rynx-ai/core";
8
8
  import type { ThreadItem } from "./protocol.js";
@@ -1,3 +1,34 @@
1
+ function turnError(error, fallback) {
2
+ if (typeof error === "string" && error.trim())
3
+ return new Error(error);
4
+ if (error && typeof error === "object" && "message" in error) {
5
+ const message = error.message;
6
+ if (typeof message === "string" && message.trim())
7
+ return new Error(message);
8
+ }
9
+ return new Error(fallback);
10
+ }
11
+ function webSearchInput(item) {
12
+ const action = item.action;
13
+ if (action?.type === "search") {
14
+ return {
15
+ id: item.id,
16
+ query: action.query || item.query,
17
+ ...(action.queries?.length ? { queries: action.queries } : {}),
18
+ };
19
+ }
20
+ if (action?.type === "openPage") {
21
+ return { id: item.id, url: action.url ?? "" };
22
+ }
23
+ if (action?.type === "findInPage") {
24
+ return {
25
+ id: item.id,
26
+ url: action.url ?? "",
27
+ pattern: action.pattern ?? "",
28
+ };
29
+ }
30
+ return { id: item.id, query: item.query };
31
+ }
1
32
  /** Map one thread item (`item/started` | `item/completed`) to events. */
2
33
  export function mapCodexItem(method, item) {
3
34
  const events = [];
@@ -68,13 +99,15 @@ export function mapCodexItem(method, item) {
68
99
  return { events };
69
100
  }
70
101
  case "webSearch": {
102
+ const webSearch = item;
71
103
  events.push({
72
104
  type: "tool",
73
105
  event: isStart ? "on_tool_start" : "on_tool_end",
74
106
  name: "web_search",
75
- input: isStart
76
- ? { id: item.id, query: item.query }
77
- : undefined,
107
+ // `item/started` commonly has an empty top-level query. The completed
108
+ // item carries the real operation in `action`; pass that richer input
109
+ // through so the canonical layer can append a same-call argument update.
110
+ input: isStart || isEnd ? webSearchInput(webSearch) : undefined,
78
111
  output: isEnd ? { id: item.id } : undefined,
79
112
  data: { method, item },
80
113
  });
@@ -112,12 +145,27 @@ export function mapCodexNotification(method, params) {
112
145
  switch (typed.method) {
113
146
  case "thread/started":
114
147
  return { events };
148
+ case "serverRequest/resolved":
149
+ // Interaction lifecycle is handled by CodexAppServerClient. It is not
150
+ // model output and must never open or close a Turn in the forwarder.
151
+ return { events };
115
152
  case "turn/started":
116
153
  return { events };
117
154
  case "turn/completed": {
118
155
  const turnPayload = typed.params?.turn;
119
- if (turnPayload?.status === "failed" && turnPayload.error?.message) {
120
- return { events, fatalError: new Error(turnPayload.error.message), turnCompleted: true };
156
+ if (turnPayload?.status === "failed") {
157
+ return {
158
+ events,
159
+ fatalError: turnError(turnPayload.error, "Codex turn failed"),
160
+ turnCompleted: true,
161
+ };
162
+ }
163
+ if (turnPayload?.status === "interrupted") {
164
+ return {
165
+ events,
166
+ fatalError: turnError(turnPayload.error, "Codex turn was interrupted"),
167
+ turnCompleted: true,
168
+ };
121
169
  }
122
170
  return { events, turnCompleted: true };
123
171
  }
@@ -174,6 +222,17 @@ export function mapCodexNotification(method, params) {
174
222
  const p = typed.params;
175
223
  return { events, usage: p.tokenUsage };
176
224
  }
225
+ case "queue/status": {
226
+ const p = typed.params;
227
+ if (p.state === "ready") {
228
+ events.push({ type: "status" });
229
+ return { events };
230
+ }
231
+ const note = p.message?.trim()
232
+ || (p.position === null ? "Waiting for provider capacity." : `Your queue position is ${p.position}.`);
233
+ events.push({ type: "status", statusKind: "queue", note });
234
+ return { events };
235
+ }
177
236
  case "error": {
178
237
  const p = typed.params;
179
238
  if (p.willRetry) {