@rynx-ai/runtime 0.1.11-beta.20 → 0.1.11-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude/native-integration.d.ts +23 -1
- package/dist/claude/native-integration.js +69 -6
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/codex-app-server/forwarder.d.ts +33 -2
- package/dist/codex-app-server/forwarder.js +175 -16
- package/dist/codex-app-server/mapping.d.ts +2 -0
- package/dist/codex-app-server/mapping.js +97 -23
- package/dist/codex-app-server/protocol.d.ts +1 -1
- package/dist/host.d.ts +3 -2
- package/dist/host.js +111 -90
- package/dist/runner/child.js +1 -1
- package/dist/runner/manager.d.ts +26 -11
- package/dist/runner/manager.js +99 -30
- package/dist/runner/protocol.d.ts +5 -11
- package/dist/terminal/tmux.d.ts +15 -0
- package/dist/terminal/tmux.js +50 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { codexUserContent } from "../input-resources.js";
|
|
2
|
-
import { mapCodexItem, mapCodexNotification } from "./mapping.js";
|
|
2
|
+
import { codexResumeTerminalStatus, mapCodexItem, mapCodexNotification, } from "./mapping.js";
|
|
3
3
|
const MCP_STARTUP_STATUS_METHOD = "mcpServer/startupStatus/updated";
|
|
4
4
|
const MCP_TERMINAL_STATES = new Set(["ready", "failed", "cancelled"]);
|
|
5
5
|
function threadIdFrom(params) {
|
|
@@ -45,6 +45,20 @@ function isModelOutput(method, params) {
|
|
|
45
45
|
const item = params?.item;
|
|
46
46
|
return Boolean(item?.type && item.type !== "userMessage");
|
|
47
47
|
}
|
|
48
|
+
function isTerminalTurnMethod(method) {
|
|
49
|
+
return method === "turn/completed" || method === "turn/failed";
|
|
50
|
+
}
|
|
51
|
+
/** Turn-scoped notifications must never be reassigned to whichever newer turn
|
|
52
|
+
* happens to be current when an app-server frame arrives late. */
|
|
53
|
+
function isTurnScopedMethod(method) {
|
|
54
|
+
return method.startsWith("turn/") || method.startsWith("item/") ||
|
|
55
|
+
method === "queue/status" || method === "error";
|
|
56
|
+
}
|
|
57
|
+
/** Recover a missed turn/started only from visible, fully-scoped assistant/plan
|
|
58
|
+
* deltas. Other deltas require an already matching active turn. */
|
|
59
|
+
function canRecoverMissedTurnFrom(method) {
|
|
60
|
+
return method === "item/agentMessage/delta" || method === "item/plan/delta";
|
|
61
|
+
}
|
|
48
62
|
export class CodexSessionForwarder {
|
|
49
63
|
client;
|
|
50
64
|
sink;
|
|
@@ -55,6 +69,9 @@ export class CodexSessionForwarder {
|
|
|
55
69
|
currentThreadIdValue = null;
|
|
56
70
|
activeSignaled = false;
|
|
57
71
|
completionTimer = null;
|
|
72
|
+
/** Turn id retained only for late-item dedup while a terminal response waits
|
|
73
|
+
* for its bounded output-ordering grace. It is not an active provider turn. */
|
|
74
|
+
pendingCompletionTurnId = null;
|
|
58
75
|
assistantMessageTimer = null;
|
|
59
76
|
deferredAssistantMessage = null;
|
|
60
77
|
pendingCompletion = null;
|
|
@@ -97,12 +114,17 @@ export class CodexSessionForwarder {
|
|
|
97
114
|
this.flushDeferredAssistantMessage();
|
|
98
115
|
if (this.turnOpen) {
|
|
99
116
|
this.turnOpen = false;
|
|
117
|
+
this.currentTurnIdValue = null;
|
|
100
118
|
this.sink.onTurnEnd();
|
|
101
119
|
}
|
|
120
|
+
else {
|
|
121
|
+
this.currentTurnIdValue = null;
|
|
122
|
+
}
|
|
102
123
|
}
|
|
103
|
-
/** True while
|
|
124
|
+
/** True while the provider owns an active turn, including the short interval
|
|
125
|
+
* between injection acceptance and observer confirmation. */
|
|
104
126
|
isTurnOpen() {
|
|
105
|
-
return this.turnOpen;
|
|
127
|
+
return this.turnOpen || this.currentTurnIdValue !== null;
|
|
106
128
|
}
|
|
107
129
|
/** The current turn's codex id (only meaningful while {@link isTurnOpen}). */
|
|
108
130
|
currentTurnId() {
|
|
@@ -117,8 +139,9 @@ export class CodexSessionForwarder {
|
|
|
117
139
|
noteTurnAccepted(turnId) {
|
|
118
140
|
if (!turnId)
|
|
119
141
|
return;
|
|
142
|
+
if (this.pendingCompletion)
|
|
143
|
+
this.flushPendingCompletion();
|
|
120
144
|
this.currentTurnIdValue = turnId;
|
|
121
|
-
this.ensureTurn();
|
|
122
145
|
this.emitMcpStartupStatus();
|
|
123
146
|
}
|
|
124
147
|
hasPendingMcpStartup() {
|
|
@@ -131,7 +154,7 @@ export class CodexSessionForwarder {
|
|
|
131
154
|
return pending;
|
|
132
155
|
this.pendingMcpServers.clear();
|
|
133
156
|
this.clearMcpStartupTimer();
|
|
134
|
-
if (this.
|
|
157
|
+
if (this.isTurnOpen()) {
|
|
135
158
|
this.emitMcpStatus(`MCP startup cancelled: ${pending.join(", ")}`);
|
|
136
159
|
}
|
|
137
160
|
return pending;
|
|
@@ -150,6 +173,16 @@ export class CodexSessionForwarder {
|
|
|
150
173
|
threadId() {
|
|
151
174
|
return this.currentThreadIdValue;
|
|
152
175
|
}
|
|
176
|
+
/** Seed an already-persisted/resumed thread binding. The bridge retains this
|
|
177
|
+
* state even when app-server does not rebroadcast `thread/started`, so
|
|
178
|
+
* terminal-boundary recovery must know it too. */
|
|
179
|
+
noteThreadBound(threadId) {
|
|
180
|
+
if (!threadId || this.currentThreadIdValue === threadId)
|
|
181
|
+
return;
|
|
182
|
+
if (this.currentThreadIdValue !== null || this.turnOpen)
|
|
183
|
+
return;
|
|
184
|
+
this.currentThreadIdValue = threadId;
|
|
185
|
+
}
|
|
153
186
|
/**
|
|
154
187
|
* Replay the backlog turns from a `thread/resume` response as if they were live
|
|
155
188
|
* `item/completed` notifications — the fresh-thread first-turn backfill. Each
|
|
@@ -165,10 +198,11 @@ export class CodexSessionForwarder {
|
|
|
165
198
|
this.ensureTurn();
|
|
166
199
|
for (const item of turn.items ?? [])
|
|
167
200
|
this.processCompletedItem(item);
|
|
168
|
-
if (turn
|
|
201
|
+
if (codexResumeTerminalStatus(turn) === undefined)
|
|
169
202
|
continue;
|
|
170
203
|
const mapped = mapCodexNotification("turn/completed", { turn });
|
|
171
204
|
this.turnOpen = false;
|
|
205
|
+
this.currentTurnIdValue = null;
|
|
172
206
|
if (mapped.fatalError)
|
|
173
207
|
this.sink.onTurnError(mapped.fatalError);
|
|
174
208
|
else
|
|
@@ -178,21 +212,56 @@ export class CodexSessionForwarder {
|
|
|
178
212
|
/** Reconcile only the exact active turn after an observer resume. Historical
|
|
179
213
|
* items are intentionally not replayed on an existing-thread reconnect. */
|
|
180
214
|
reconcileActiveTurn(turn) {
|
|
181
|
-
if (!this.
|
|
215
|
+
if (!this.isTurnOpen() || !turn || codexResumeTerminalStatus(turn) === undefined)
|
|
182
216
|
return false;
|
|
183
217
|
const resumedTurnId = turn.id ?? turn.turnId;
|
|
184
218
|
if (!this.currentTurnIdValue || resumedTurnId !== this.currentTurnIdValue)
|
|
185
219
|
return false;
|
|
186
220
|
const mapped = mapCodexNotification("turn/completed", { turn });
|
|
221
|
+
this.ensureTurn();
|
|
187
222
|
this.settle(mapped.fatalError
|
|
188
223
|
? { kind: "error", error: mapped.fatalError }
|
|
189
224
|
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
190
225
|
return true;
|
|
191
226
|
}
|
|
227
|
+
/** Reconcile an observer reconnect from the explicit resume snapshot. An
|
|
228
|
+
* identified active turn must match exactly; with no active turn, only the
|
|
229
|
+
* newest explicit terminal status is published and no history is replayed. */
|
|
230
|
+
reconcileResumeTurns(turns) {
|
|
231
|
+
if (this.currentThreadIdValue === null)
|
|
232
|
+
return false;
|
|
233
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
234
|
+
const turn = turns[index];
|
|
235
|
+
if (!turn)
|
|
236
|
+
continue;
|
|
237
|
+
const turnId = turn.id ?? turn.turnId;
|
|
238
|
+
if (!turnId)
|
|
239
|
+
return false;
|
|
240
|
+
if (this.currentTurnIdValue !== null && this.currentTurnIdValue !== turnId) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const status = codexResumeTerminalStatus(turn);
|
|
244
|
+
if (!status)
|
|
245
|
+
return false;
|
|
246
|
+
const mapped = mapCodexNotification("turn/completed", { turn });
|
|
247
|
+
if (this.currentTurnIdValue !== null || this.turnOpen) {
|
|
248
|
+
this.ensureTurn();
|
|
249
|
+
this.settle(mapped.fatalError
|
|
250
|
+
? { kind: "error", error: mapped.fatalError }
|
|
251
|
+
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
this.sink.onRecoveredTurnStatus?.(status, turnId, mapped.fatalError);
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
192
260
|
/** Fail an open response exactly once when its observer or terminal exits. */
|
|
193
261
|
failOpenTurn(error) {
|
|
194
|
-
if (!this.
|
|
262
|
+
if (!this.isTurnOpen())
|
|
195
263
|
return false;
|
|
264
|
+
this.ensureTurn();
|
|
196
265
|
this.settle({ kind: "error", error });
|
|
197
266
|
return true;
|
|
198
267
|
}
|
|
@@ -240,17 +309,63 @@ export class CodexSessionForwarder {
|
|
|
240
309
|
// behavior unchanged unless the Traex host explicitly opts in.
|
|
241
310
|
if (method === "queue/status" && !this.options.surfaceQueueStatus)
|
|
242
311
|
return;
|
|
312
|
+
if (method === "queue/status" && this.currentTurnIdValue === null)
|
|
313
|
+
return;
|
|
243
314
|
const carriedTurnId = turnIdFrom(params);
|
|
244
315
|
if (method === "turn/started") {
|
|
245
|
-
this.
|
|
246
|
-
this.
|
|
247
|
-
|
|
316
|
+
this.beginTurn(carriedTurnId);
|
|
317
|
+
this.sink.onTurnObserved?.(carriedTurnId);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (isTerminalTurnMethod(method)) {
|
|
321
|
+
if (!this.terminalBoundaryMatchesActiveTurn(carriedTurnId, notificationThreadId)) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const hadActiveTurn = this.currentTurnIdValue !== null;
|
|
325
|
+
const hadOpenResponse = this.turnOpen;
|
|
326
|
+
const mapped = mapCodexNotification(method, params);
|
|
327
|
+
if (!hadActiveTurn && !hadOpenResponse) {
|
|
328
|
+
const turn = params?.turn;
|
|
329
|
+
const recoveredStatus = mapped.fatalError ||
|
|
330
|
+
method === "turn/failed" ||
|
|
331
|
+
codexResumeTerminalStatus(turn) === "failed"
|
|
332
|
+
? "failed"
|
|
333
|
+
: "idle";
|
|
334
|
+
this.sink.onRecoveredTurnStatus?.(recoveredStatus, carriedTurnId, mapped.fatalError);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (carriedTurnId && this.currentTurnIdValue === null) {
|
|
248
338
|
this.currentTurnIdValue = carriedTurnId;
|
|
339
|
+
}
|
|
249
340
|
this.ensureTurn();
|
|
341
|
+
this.pendingCompletionTurnId = carriedTurnId ?? this.currentTurnIdValue;
|
|
342
|
+
// The provider lifecycle closes immediately. A configured grace keeps
|
|
343
|
+
// only the canonical response open long enough to absorb late items.
|
|
344
|
+
this.currentTurnIdValue = null;
|
|
345
|
+
for (const event of mapped.events.filter((event) => event.type !== "runtime_debug")) {
|
|
346
|
+
this.sink.onEvent(event);
|
|
347
|
+
}
|
|
348
|
+
this.scheduleCompletion(mapped.fatalError
|
|
349
|
+
? { kind: "error", error: mapped.fatalError }
|
|
350
|
+
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
// A late event from an older turn must not replace the app-server's active
|
|
354
|
+
// turn id. That was the source of stale completions closing a newer turn.
|
|
355
|
+
if (carriedTurnId &&
|
|
356
|
+
this.currentTurnIdValue &&
|
|
357
|
+
carriedTurnId !== this.currentTurnIdValue &&
|
|
358
|
+
isTurnScopedMethod(method)) {
|
|
250
359
|
return;
|
|
251
360
|
}
|
|
252
|
-
if (carriedTurnId)
|
|
253
|
-
|
|
361
|
+
if (carriedTurnId && this.currentTurnIdValue === null) {
|
|
362
|
+
if (canRecoverMissedTurnFrom(method)) {
|
|
363
|
+
if (!this.notificationMatchesCurrentThread(notificationThreadId))
|
|
364
|
+
return;
|
|
365
|
+
this.currentTurnIdValue = carriedTurnId;
|
|
366
|
+
this.ensureTurn();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
254
369
|
// Completed items (user echo, assistant, tool, reasoning) go through the
|
|
255
370
|
// deduped path so a resume backfill and the live stream never double them.
|
|
256
371
|
if (method === "item/completed") {
|
|
@@ -330,6 +445,8 @@ export class CodexSessionForwarder {
|
|
|
330
445
|
settle(completion) {
|
|
331
446
|
this.flushDeferredAssistantMessage();
|
|
332
447
|
this.turnOpen = false;
|
|
448
|
+
this.currentTurnIdValue = null;
|
|
449
|
+
this.pendingCompletionTurnId = null;
|
|
333
450
|
if (completion.kind === "error")
|
|
334
451
|
this.sink.onTurnError(completion.error);
|
|
335
452
|
else
|
|
@@ -373,7 +490,7 @@ export class CodexSessionForwarder {
|
|
|
373
490
|
this.mcpStartupTimer = null;
|
|
374
491
|
}
|
|
375
492
|
emitMcpStartupStatus() {
|
|
376
|
-
if (!this.
|
|
493
|
+
if (!this.isTurnOpen())
|
|
377
494
|
return;
|
|
378
495
|
const pending = [...this.pendingMcpServers].sort();
|
|
379
496
|
if (pending.length) {
|
|
@@ -464,14 +581,15 @@ export class CodexSessionForwarder {
|
|
|
464
581
|
* {@link advanceAnonCounter}). Mirrors reference implementation `_completed_item_key`. */
|
|
465
582
|
completedItemKey(item) {
|
|
466
583
|
const threadId = this.currentThreadIdValue ?? "thread";
|
|
467
|
-
const turnId = this.currentTurnIdValue ?? "turn";
|
|
584
|
+
const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
|
|
468
585
|
if (item.id)
|
|
469
586
|
return { key: `${threadId}:${turnId}:${item.id}`, isAnon: false };
|
|
470
587
|
const n = this.anonCounters.get(`${threadId}:${turnId}`) ?? 0;
|
|
471
588
|
return { key: `${threadId}:${turnId}:anon:${n}`, isAnon: true };
|
|
472
589
|
}
|
|
473
590
|
advanceAnonCounter() {
|
|
474
|
-
const
|
|
591
|
+
const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
|
|
592
|
+
const k = `${this.currentThreadIdValue ?? "thread"}:${turnId}`;
|
|
475
593
|
this.anonCounters.set(k, (this.anonCounters.get(k) ?? 0) + 1);
|
|
476
594
|
}
|
|
477
595
|
ensureTurn() {
|
|
@@ -480,4 +598,45 @@ export class CodexSessionForwarder {
|
|
|
480
598
|
this.sink.onTurnStart(this.currentTurnIdValue ?? undefined);
|
|
481
599
|
}
|
|
482
600
|
}
|
|
601
|
+
/** Start (or confirm) the app-server's authoritative active turn. A newer
|
|
602
|
+
* start supersedes an older response whose terminal edge arrived late; a
|
|
603
|
+
* pending Traex completion is flushed first so its final item grace remains
|
|
604
|
+
* intact. */
|
|
605
|
+
beginTurn(turnId) {
|
|
606
|
+
if (this.pendingCompletion)
|
|
607
|
+
this.flushPendingCompletion();
|
|
608
|
+
const nextTurnId = turnId ?? null;
|
|
609
|
+
const changesIdentifiedTurn = this.turnOpen &&
|
|
610
|
+
this.currentTurnIdValue !== null &&
|
|
611
|
+
nextTurnId !== null &&
|
|
612
|
+
this.currentTurnIdValue !== nextTurnId;
|
|
613
|
+
if (changesIdentifiedTurn) {
|
|
614
|
+
this.flushPendingCompletion();
|
|
615
|
+
this.flushDeferredAssistantMessage();
|
|
616
|
+
if (this.turnOpen) {
|
|
617
|
+
this.turnOpen = false;
|
|
618
|
+
this.sink.onTurnEnd(undefined, "superseded");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
this.currentTurnIdValue = nextTurnId;
|
|
622
|
+
this.ensureTurn();
|
|
623
|
+
}
|
|
624
|
+
/** Active-turn clearing contract:
|
|
625
|
+
*
|
|
626
|
+
* - an identified active turn is closed only by the same id;
|
|
627
|
+
* - an id-less boundary cannot close an identified active turn;
|
|
628
|
+
* - with no observed active turn, an identified boundary may recover a
|
|
629
|
+
* missed start only when it carries the currently-bound thread id. */
|
|
630
|
+
terminalBoundaryMatchesActiveTurn(terminalTurnId, notificationThreadId) {
|
|
631
|
+
if (this.currentTurnIdValue !== null) {
|
|
632
|
+
return terminalTurnId === this.currentTurnIdValue;
|
|
633
|
+
}
|
|
634
|
+
if (terminalTurnId === undefined)
|
|
635
|
+
return true;
|
|
636
|
+
return this.notificationMatchesCurrentThread(notificationThreadId);
|
|
637
|
+
}
|
|
638
|
+
notificationMatchesCurrentThread(notificationThreadId) {
|
|
639
|
+
return this.currentThreadIdValue !== null &&
|
|
640
|
+
notificationThreadId === this.currentThreadIdValue;
|
|
641
|
+
}
|
|
483
642
|
}
|
|
@@ -13,6 +13,8 @@ export interface CodexMapResult {
|
|
|
13
13
|
turnCompleted?: boolean;
|
|
14
14
|
fatalError?: Error;
|
|
15
15
|
}
|
|
16
|
+
export declare function codexTurnStatus(turn: unknown): string | undefined;
|
|
17
|
+
export declare function codexResumeTerminalStatus(turn: unknown): "idle" | "failed" | undefined;
|
|
16
18
|
/** Map one thread item (`item/started` | `item/completed`) to events. */
|
|
17
19
|
export declare function mapCodexItem(method: string, item: ThreadItem): CodexMapResult;
|
|
18
20
|
/** Map one codex app-server notification to events (+ turn/usage/error signals). */
|
|
@@ -1,12 +1,98 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
const CODEX_AUTH_ERROR_FRAGMENTS = [
|
|
5
|
+
"401",
|
|
6
|
+
"403",
|
|
7
|
+
"unauthorized",
|
|
8
|
+
"authentication",
|
|
9
|
+
"not logged in",
|
|
10
|
+
"not authenticated",
|
|
11
|
+
"log in",
|
|
12
|
+
"login",
|
|
13
|
+
"sign in",
|
|
14
|
+
"re-authenticate",
|
|
15
|
+
"reauthenticate",
|
|
16
|
+
"credentials",
|
|
17
|
+
"access token",
|
|
18
|
+
"token expired",
|
|
19
|
+
"expired token",
|
|
20
|
+
"session expired",
|
|
21
|
+
"api key",
|
|
22
|
+
];
|
|
23
|
+
const CODEX_REAUTH_HINT = "Codex needs you to re-authenticate. Run `codex login` and retry.";
|
|
24
|
+
function codexErrorMessage(error) {
|
|
25
|
+
for (const key of ["message", "error", "text", "detail"]) {
|
|
26
|
+
const value = error[key];
|
|
27
|
+
if (typeof value === "string" && value.trim())
|
|
28
|
+
return value.trim();
|
|
29
|
+
}
|
|
30
|
+
return "Codex turn ended with an unspecified error.";
|
|
31
|
+
}
|
|
32
|
+
function codexErrorIsAuth(error, message) {
|
|
33
|
+
const info = error.codexErrorInfo;
|
|
34
|
+
let variant;
|
|
35
|
+
let httpStatusCode;
|
|
36
|
+
if (typeof info === "string") {
|
|
37
|
+
variant = info;
|
|
38
|
+
}
|
|
39
|
+
else if (isRecord(info)) {
|
|
40
|
+
variant = info.type ?? info.kind ?? info.variant;
|
|
41
|
+
httpStatusCode = info.httpStatusCode;
|
|
42
|
+
}
|
|
43
|
+
if (typeof variant === "string" && variant.toLowerCase() === "unauthorized")
|
|
44
|
+
return true;
|
|
45
|
+
if (httpStatusCode === 401 || httpStatusCode === 403)
|
|
46
|
+
return true;
|
|
47
|
+
const lowered = message.toLowerCase();
|
|
48
|
+
return CODEX_AUTH_ERROR_FRAGMENTS.some((fragment) => lowered.includes(fragment));
|
|
49
|
+
}
|
|
1
50
|
function turnError(error, fallback) {
|
|
2
|
-
if (
|
|
3
|
-
return new Error(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
51
|
+
if (!error)
|
|
52
|
+
return new Error(fallback);
|
|
53
|
+
const message = codexErrorMessage(error);
|
|
54
|
+
return new Error(codexErrorIsAuth(error, message)
|
|
55
|
+
? `${message}\n\n${CODEX_REAUTH_HINT}`
|
|
56
|
+
: message);
|
|
57
|
+
}
|
|
58
|
+
export function codexTurnStatus(turn) {
|
|
59
|
+
if (!isRecord(turn))
|
|
60
|
+
return undefined;
|
|
61
|
+
const status = turn.status;
|
|
62
|
+
if (typeof status === "string")
|
|
63
|
+
return status;
|
|
64
|
+
if (!isRecord(status))
|
|
65
|
+
return undefined;
|
|
66
|
+
const value = status.type ?? status.status;
|
|
67
|
+
return typeof value === "string" ? value : undefined;
|
|
68
|
+
}
|
|
69
|
+
export function codexResumeTerminalStatus(turn) {
|
|
70
|
+
if (terminalTurnError(turn, "turn/completed"))
|
|
71
|
+
return "failed";
|
|
72
|
+
const status = codexTurnStatus(turn);
|
|
73
|
+
if (status === "completed" || status === "interrupted" ||
|
|
74
|
+
status === "cancelled" || status === "canceled") {
|
|
75
|
+
return "idle";
|
|
8
76
|
}
|
|
9
|
-
|
|
77
|
+
if (status === "failed" || status === "errored")
|
|
78
|
+
return "failed";
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
function terminalTurnError(turn, method) {
|
|
82
|
+
if (!isRecord(turn)) {
|
|
83
|
+
return method === "turn/failed" ? new Error("Codex turn failed") : undefined;
|
|
84
|
+
}
|
|
85
|
+
let error = turn.error;
|
|
86
|
+
if (!isRecord(error) && Array.isArray(turn.items)) {
|
|
87
|
+
error = turn.items.find((item) => isRecord(item) && item.type === "error");
|
|
88
|
+
}
|
|
89
|
+
if (isRecord(error))
|
|
90
|
+
return turnError(error, "Codex turn failed");
|
|
91
|
+
const status = codexTurnStatus(turn);
|
|
92
|
+
if (method === "turn/failed" || status === "failed" || status === "errored") {
|
|
93
|
+
return turnError(undefined, "Codex turn failed");
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
10
96
|
}
|
|
11
97
|
function webSearchInput(item) {
|
|
12
98
|
const action = item.action;
|
|
@@ -151,23 +237,11 @@ export function mapCodexNotification(method, params) {
|
|
|
151
237
|
return { events };
|
|
152
238
|
case "turn/started":
|
|
153
239
|
return { events };
|
|
154
|
-
case "turn/completed":
|
|
240
|
+
case "turn/completed":
|
|
241
|
+
case "turn/failed": {
|
|
155
242
|
const turnPayload = typed.params?.turn;
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return { events, turnCompleted: true };
|
|
243
|
+
const fatalError = terminalTurnError(turnPayload, typed.method);
|
|
244
|
+
return { events, ...(fatalError ? { fatalError } : {}), turnCompleted: true };
|
|
171
245
|
}
|
|
172
246
|
case "turn/plan/updated": {
|
|
173
247
|
const planParams = typed.params;
|
|
@@ -172,7 +172,7 @@ export interface TurnSteerParams {
|
|
|
172
172
|
expectedTurnId: string;
|
|
173
173
|
input: UserInput[];
|
|
174
174
|
}
|
|
175
|
-
export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
|
|
175
|
+
export type TurnStatus = "completed" | "interrupted" | "cancelled" | "canceled" | "failed" | "errored" | "inProgress";
|
|
176
176
|
export interface TurnPlanStep {
|
|
177
177
|
step: string;
|
|
178
178
|
status: "pending" | "inProgress" | "completed";
|
package/dist/host.d.ts
CHANGED
|
@@ -226,8 +226,9 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
226
226
|
* `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
|
|
227
227
|
* turn, so `thread/resume` is retried: park until the forwarder observes the
|
|
228
228
|
* thread active, then retry. Resume only fetches the newest summarized Turn:
|
|
229
|
-
* recovery reconciles the exact active Turn
|
|
230
|
-
* historical items. Once resume succeeds,
|
|
229
|
+
* recovery reconciles the exact active Turn or publishes the newest explicit
|
|
230
|
+
* terminal status without replaying historical items. Once resume succeeds,
|
|
231
|
+
* subsequent turns arrive live.
|
|
231
232
|
*/
|
|
232
233
|
private subscribeUntilReady;
|
|
233
234
|
/** Restore the independent observer after an unexpected exit. The active turn
|