@rynx-ai/runtime 0.1.11-beta.20 → 0.1.11-beta.22
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/client.d.ts +4 -3
- package/dist/codex-app-server/client.js +35 -12
- package/dist/codex-app-server/forwarder.d.ts +29 -5
- package/dist/codex-app-server/forwarder.js +140 -29
- 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 +2 -3
- package/dist/host.d.ts +27 -20
- package/dist/host.js +337 -455
- package/dist/runner/child.d.ts +12 -5
- package/dist/runner/child.js +116 -37
- package/dist/runner/manager.d.ts +36 -18
- package/dist/runner/manager.js +176 -81
- package/dist/runner/protocol.d.ts +15 -20
- package/dist/runner/startup-policy.d.ts +3 -0
- package/dist/runner/startup-policy.js +5 -0
- 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,34 +198,22 @@ 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
|
|
175
209
|
this.sink.onTurnEnd(mapped.usage);
|
|
176
210
|
}
|
|
177
211
|
}
|
|
178
|
-
/** Reconcile only the exact active turn after an observer resume. Historical
|
|
179
|
-
* items are intentionally not replayed on an existing-thread reconnect. */
|
|
180
|
-
reconcileActiveTurn(turn) {
|
|
181
|
-
if (!this.turnOpen || !turn || turn.status === "inProgress")
|
|
182
|
-
return false;
|
|
183
|
-
const resumedTurnId = turn.id ?? turn.turnId;
|
|
184
|
-
if (!this.currentTurnIdValue || resumedTurnId !== this.currentTurnIdValue)
|
|
185
|
-
return false;
|
|
186
|
-
const mapped = mapCodexNotification("turn/completed", { turn });
|
|
187
|
-
this.settle(mapped.fatalError
|
|
188
|
-
? { kind: "error", error: mapped.fatalError }
|
|
189
|
-
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
212
|
/** Fail an open response exactly once when its observer or terminal exits. */
|
|
193
213
|
failOpenTurn(error) {
|
|
194
|
-
if (!this.
|
|
214
|
+
if (!this.isTurnOpen())
|
|
195
215
|
return false;
|
|
216
|
+
this.ensureTurn();
|
|
196
217
|
this.settle({ kind: "error", error });
|
|
197
218
|
return true;
|
|
198
219
|
}
|
|
@@ -240,17 +261,63 @@ export class CodexSessionForwarder {
|
|
|
240
261
|
// behavior unchanged unless the Traex host explicitly opts in.
|
|
241
262
|
if (method === "queue/status" && !this.options.surfaceQueueStatus)
|
|
242
263
|
return;
|
|
264
|
+
if (method === "queue/status" && this.currentTurnIdValue === null)
|
|
265
|
+
return;
|
|
243
266
|
const carriedTurnId = turnIdFrom(params);
|
|
244
267
|
if (method === "turn/started") {
|
|
245
|
-
this.
|
|
246
|
-
this.
|
|
247
|
-
|
|
268
|
+
this.beginTurn(carriedTurnId);
|
|
269
|
+
this.sink.onTurnObserved?.(carriedTurnId);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (isTerminalTurnMethod(method)) {
|
|
273
|
+
if (!this.terminalBoundaryMatchesActiveTurn(carriedTurnId, notificationThreadId)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const hadActiveTurn = this.currentTurnIdValue !== null;
|
|
277
|
+
const hadOpenResponse = this.turnOpen;
|
|
278
|
+
const mapped = mapCodexNotification(method, params);
|
|
279
|
+
if (!hadActiveTurn && !hadOpenResponse) {
|
|
280
|
+
const turn = params?.turn;
|
|
281
|
+
const recoveredStatus = mapped.fatalError ||
|
|
282
|
+
method === "turn/failed" ||
|
|
283
|
+
codexResumeTerminalStatus(turn) === "failed"
|
|
284
|
+
? "failed"
|
|
285
|
+
: "idle";
|
|
286
|
+
this.sink.onRecoveredTurnStatus?.(recoveredStatus, carriedTurnId, mapped.fatalError);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (carriedTurnId && this.currentTurnIdValue === null) {
|
|
248
290
|
this.currentTurnIdValue = carriedTurnId;
|
|
291
|
+
}
|
|
249
292
|
this.ensureTurn();
|
|
293
|
+
this.pendingCompletionTurnId = carriedTurnId ?? this.currentTurnIdValue;
|
|
294
|
+
// The provider lifecycle closes immediately. A configured grace keeps
|
|
295
|
+
// only the canonical response open long enough to absorb late items.
|
|
296
|
+
this.currentTurnIdValue = null;
|
|
297
|
+
for (const event of mapped.events.filter((event) => event.type !== "runtime_debug")) {
|
|
298
|
+
this.sink.onEvent(event);
|
|
299
|
+
}
|
|
300
|
+
this.scheduleCompletion(mapped.fatalError
|
|
301
|
+
? { kind: "error", error: mapped.fatalError }
|
|
302
|
+
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
250
303
|
return;
|
|
251
304
|
}
|
|
252
|
-
|
|
253
|
-
|
|
305
|
+
// A late event from an older turn must not replace the app-server's active
|
|
306
|
+
// turn id. That was the source of stale completions closing a newer turn.
|
|
307
|
+
if (carriedTurnId &&
|
|
308
|
+
this.currentTurnIdValue &&
|
|
309
|
+
carriedTurnId !== this.currentTurnIdValue &&
|
|
310
|
+
isTurnScopedMethod(method)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (carriedTurnId && this.currentTurnIdValue === null) {
|
|
314
|
+
if (canRecoverMissedTurnFrom(method)) {
|
|
315
|
+
if (!this.notificationMatchesCurrentThread(notificationThreadId))
|
|
316
|
+
return;
|
|
317
|
+
this.currentTurnIdValue = carriedTurnId;
|
|
318
|
+
this.ensureTurn();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
254
321
|
// Completed items (user echo, assistant, tool, reasoning) go through the
|
|
255
322
|
// deduped path so a resume backfill and the live stream never double them.
|
|
256
323
|
if (method === "item/completed") {
|
|
@@ -330,6 +397,8 @@ export class CodexSessionForwarder {
|
|
|
330
397
|
settle(completion) {
|
|
331
398
|
this.flushDeferredAssistantMessage();
|
|
332
399
|
this.turnOpen = false;
|
|
400
|
+
this.currentTurnIdValue = null;
|
|
401
|
+
this.pendingCompletionTurnId = null;
|
|
333
402
|
if (completion.kind === "error")
|
|
334
403
|
this.sink.onTurnError(completion.error);
|
|
335
404
|
else
|
|
@@ -373,7 +442,7 @@ export class CodexSessionForwarder {
|
|
|
373
442
|
this.mcpStartupTimer = null;
|
|
374
443
|
}
|
|
375
444
|
emitMcpStartupStatus() {
|
|
376
|
-
if (!this.
|
|
445
|
+
if (!this.isTurnOpen())
|
|
377
446
|
return;
|
|
378
447
|
const pending = [...this.pendingMcpServers].sort();
|
|
379
448
|
if (pending.length) {
|
|
@@ -464,14 +533,15 @@ export class CodexSessionForwarder {
|
|
|
464
533
|
* {@link advanceAnonCounter}). Mirrors reference implementation `_completed_item_key`. */
|
|
465
534
|
completedItemKey(item) {
|
|
466
535
|
const threadId = this.currentThreadIdValue ?? "thread";
|
|
467
|
-
const turnId = this.currentTurnIdValue ?? "turn";
|
|
536
|
+
const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
|
|
468
537
|
if (item.id)
|
|
469
538
|
return { key: `${threadId}:${turnId}:${item.id}`, isAnon: false };
|
|
470
539
|
const n = this.anonCounters.get(`${threadId}:${turnId}`) ?? 0;
|
|
471
540
|
return { key: `${threadId}:${turnId}:anon:${n}`, isAnon: true };
|
|
472
541
|
}
|
|
473
542
|
advanceAnonCounter() {
|
|
474
|
-
const
|
|
543
|
+
const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
|
|
544
|
+
const k = `${this.currentThreadIdValue ?? "thread"}:${turnId}`;
|
|
475
545
|
this.anonCounters.set(k, (this.anonCounters.get(k) ?? 0) + 1);
|
|
476
546
|
}
|
|
477
547
|
ensureTurn() {
|
|
@@ -480,4 +550,45 @@ export class CodexSessionForwarder {
|
|
|
480
550
|
this.sink.onTurnStart(this.currentTurnIdValue ?? undefined);
|
|
481
551
|
}
|
|
482
552
|
}
|
|
553
|
+
/** Start (or confirm) the app-server's authoritative active turn. A newer
|
|
554
|
+
* start supersedes an older response whose terminal edge arrived late; a
|
|
555
|
+
* pending Traex completion is flushed first so its final item grace remains
|
|
556
|
+
* intact. */
|
|
557
|
+
beginTurn(turnId) {
|
|
558
|
+
if (this.pendingCompletion)
|
|
559
|
+
this.flushPendingCompletion();
|
|
560
|
+
const nextTurnId = turnId ?? null;
|
|
561
|
+
const changesIdentifiedTurn = this.turnOpen &&
|
|
562
|
+
this.currentTurnIdValue !== null &&
|
|
563
|
+
nextTurnId !== null &&
|
|
564
|
+
this.currentTurnIdValue !== nextTurnId;
|
|
565
|
+
if (changesIdentifiedTurn) {
|
|
566
|
+
this.flushPendingCompletion();
|
|
567
|
+
this.flushDeferredAssistantMessage();
|
|
568
|
+
if (this.turnOpen) {
|
|
569
|
+
this.turnOpen = false;
|
|
570
|
+
this.sink.onTurnEnd(undefined, "superseded");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
this.currentTurnIdValue = nextTurnId;
|
|
574
|
+
this.ensureTurn();
|
|
575
|
+
}
|
|
576
|
+
/** Active-turn clearing contract:
|
|
577
|
+
*
|
|
578
|
+
* - an identified active turn is closed only by the same id;
|
|
579
|
+
* - an id-less boundary cannot close an identified active turn;
|
|
580
|
+
* - with no observed active turn, an identified boundary may recover a
|
|
581
|
+
* missed start only when it carries the currently-bound thread id. */
|
|
582
|
+
terminalBoundaryMatchesActiveTurn(terminalTurnId, notificationThreadId) {
|
|
583
|
+
if (this.currentTurnIdValue !== null) {
|
|
584
|
+
return terminalTurnId === this.currentTurnIdValue;
|
|
585
|
+
}
|
|
586
|
+
if (terminalTurnId === undefined)
|
|
587
|
+
return true;
|
|
588
|
+
return this.notificationMatchesCurrentThread(notificationThreadId);
|
|
589
|
+
}
|
|
590
|
+
notificationMatchesCurrentThread(notificationThreadId) {
|
|
591
|
+
return this.currentThreadIdValue !== null &&
|
|
592
|
+
notificationThreadId === this.currentThreadIdValue;
|
|
593
|
+
}
|
|
483
594
|
}
|
|
@@ -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;
|
|
@@ -105,8 +105,7 @@ export interface ThreadStartParams {
|
|
|
105
105
|
}
|
|
106
106
|
export interface ThreadResumeParams extends ThreadStartParams {
|
|
107
107
|
threadId: string;
|
|
108
|
-
/**
|
|
109
|
-
* `initialTurnsPage` so recovery can fetch one summarized terminal status. */
|
|
108
|
+
/** Suppress rollout history when the caller only needs to load/subscribe. */
|
|
110
109
|
excludeTurns?: boolean;
|
|
111
110
|
initialTurnsPage?: {
|
|
112
111
|
limit?: number | null;
|
|
@@ -172,7 +171,7 @@ export interface TurnSteerParams {
|
|
|
172
171
|
expectedTurnId: string;
|
|
173
172
|
input: UserInput[];
|
|
174
173
|
}
|
|
175
|
-
export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
|
|
174
|
+
export type TurnStatus = "completed" | "interrupted" | "cancelled" | "canceled" | "failed" | "errored" | "inProgress";
|
|
176
175
|
export interface TurnPlanStep {
|
|
177
176
|
step: string;
|
|
178
177
|
status: "pending" | "inProgress" | "completed";
|
package/dist/host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
|
|
1
|
+
import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, 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";
|
|
@@ -125,7 +125,9 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
125
125
|
private readonly clock;
|
|
126
126
|
private readonly backendIdleTtlMs;
|
|
127
127
|
private readonly forwarderClientFactory;
|
|
128
|
-
private readonly
|
|
128
|
+
private readonly preloadClientFactory;
|
|
129
|
+
/** Builds one short-lived message/interrupt client. */
|
|
130
|
+
private readonly injectionClientFactory;
|
|
129
131
|
private readonly backends;
|
|
130
132
|
private readonly sessionId;
|
|
131
133
|
private readonly runtimeHomeSessionId;
|
|
@@ -146,7 +148,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
146
148
|
/** Short-lived dedupe for managed fork notifications delivered after the
|
|
147
149
|
* `thread/fork` response. Values are expected source Provider thread ids. */
|
|
148
150
|
private readonly managedForkThreadStarts;
|
|
149
|
-
constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory,
|
|
151
|
+
constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, preloadClientFactory, injectionClientFactory, sessionId, runtimeHomeSessionId, }: {
|
|
150
152
|
config: AppConfig;
|
|
151
153
|
commandRunner?: CodexCommandRunner;
|
|
152
154
|
sessionStore?: CodexSessionStore;
|
|
@@ -158,8 +160,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
158
160
|
backendIdleTtlMs?: number;
|
|
159
161
|
/** Override the live-session forwarder connection factory (tests inject a fake). */
|
|
160
162
|
forwarderClientFactory?: (appServerUrl: string) => CodexAppServerClient;
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
+
/** Override the short-lived known-thread preload connection factory. */
|
|
164
|
+
preloadClientFactory?: (appServerUrl: string) => CodexAppServerClient;
|
|
165
|
+
/** Override the short-lived message/interrupt connection factory. */
|
|
166
|
+
injectionClientFactory?: (appServerUrl: string) => CodexAppServerClient;
|
|
163
167
|
/** The rynx session (localThreadId) this host serves — a per-session runner
|
|
164
168
|
* child sets it from `RYNX_RUNNER_SESSION` so the private CODEX_HOME is
|
|
165
169
|
* session-scoped. The shared `__cap__` child / tests fall back to a sentinel
|
|
@@ -219,32 +223,39 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
219
223
|
/** Bind a session's codex thread id once known (TUI broadcast or store): persist
|
|
220
224
|
* it, unblock injection, and kick off the resume-subscribe loop (once). */
|
|
221
225
|
private onLiveThreadStarted;
|
|
226
|
+
/** Start the known-thread observer after the replacement TUI has launched.
|
|
227
|
+
* Fresh sessions already connected their discovery listener before launch;
|
|
228
|
+
* this is therefore an idempotent no-op for them and for a healthy observer. */
|
|
229
|
+
startLiveCodexObserver(localThreadId: string): void;
|
|
230
|
+
/** Omnigent treats the forwarder as a required component of one native
|
|
231
|
+
* lifecycle: if its transport dies, it closes the app-server instead of
|
|
232
|
+
* accepting turns that can no longer reach the canonical mirror. */
|
|
233
|
+
private failObserverLifecycle;
|
|
222
234
|
private shouldIgnoreManagedForkThreadStarted;
|
|
223
235
|
private rememberManagedForkThreadStart;
|
|
224
236
|
/**
|
|
225
237
|
* Subscribe the forwarder connection to a thread (reference implementation's
|
|
226
238
|
* `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
|
|
227
239
|
* turn, so `thread/resume` is retried: park until the forwarder observes the
|
|
228
|
-
* thread active, then retry.
|
|
229
|
-
*
|
|
230
|
-
* historical
|
|
240
|
+
* thread active, then retry. Like Omnigent, the first attempt always uses
|
|
241
|
+
* `excludeTurns`: a known-session cold resume therefore never reconstructs
|
|
242
|
+
* historical running/completed state. Only a fresh thread whose first attempt
|
|
243
|
+
* failed as not-ready retries without `excludeTurns`, backfilling the newly
|
|
244
|
+
* materialized first turn and de-duplicating it against live notifications.
|
|
231
245
|
*/
|
|
232
246
|
private subscribeUntilReady;
|
|
233
|
-
/** Restore the independent observer after an unexpected exit. The active turn
|
|
234
|
-
* remains open during the bounded grace so resume can reconcile its exact id. */
|
|
235
|
-
private reconnectForwarder;
|
|
236
|
-
private armObserverReconnectDeadline;
|
|
237
|
-
private clearObserverReconnectDeadline;
|
|
238
247
|
/** Await a live session's thread binding. `null` leaves the deadline to the
|
|
239
248
|
* caller; a number keeps the Provider-local bound. Returns false on timeout /
|
|
240
249
|
* no live session. Injection and the runner's `live.ready` gate on this. */
|
|
241
250
|
waitLiveReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
|
|
242
|
-
/** Await the
|
|
243
|
-
*
|
|
244
|
-
*
|
|
251
|
+
/** Await the observer subscription diagnostic. Codex/Traex Terminal startup
|
|
252
|
+
* deliberately does not gate on this promise: the dedicated preload owns resume,
|
|
253
|
+
* while the independent observer attaches in the background. */
|
|
245
254
|
waitTerminalReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
|
|
246
255
|
/** Diagnostic from the provider adapter when native discovery/resume failed. */
|
|
247
256
|
liveSessionError(localThreadId: string): string | undefined;
|
|
257
|
+
/** Structured phase + concrete cause used by the runner wire protocol. */
|
|
258
|
+
liveSessionFailure(localThreadId: string): LiveSessionFailure | undefined;
|
|
248
259
|
/** Publish the background TUI/thread discovery failure so an executor
|
|
249
260
|
* already waiting in the 60s bridge window exits immediately with the exact
|
|
250
261
|
* 30s discovery cause. */
|
|
@@ -338,7 +349,3 @@ export declare function isUnsupportedMethodError(error: unknown): boolean;
|
|
|
338
349
|
* — a fresh TUI thread before its first turn. Retryable (park until active).
|
|
339
350
|
* Mirrors reference implementation's `_is_thread_not_ready_error`. */
|
|
340
351
|
export declare function isThreadNotReadyError(error: unknown): boolean;
|
|
341
|
-
/** A persisted thread id that a freshly started app-server cannot load yet.
|
|
342
|
-
* During startup, both errors can be transient while the rollout index catches
|
|
343
|
-
* up. Retry the same id; never use either error as permission to replace it. */
|
|
344
|
-
export declare function isRetryableThreadResumeError(error: unknown): boolean;
|