@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.
- package/dist/claude/executor.d.ts +3 -5
- package/dist/claude/executor.js +3 -5
- package/dist/claude/native-bridge.d.ts +74 -17
- package/dist/claude/native-bridge.js +225 -30
- package/dist/claude/native-hook-main.js +327 -38
- package/dist/claude/native-hooks.d.ts +3 -2
- package/dist/claude/native-hooks.js +15 -6
- package/dist/claude/native-integration.d.ts +123 -16
- package/dist/claude/native-integration.js +624 -81
- package/dist/claude/settings.d.ts +8 -0
- package/dist/claude/settings.js +50 -0
- package/dist/claude/transcript.d.ts +2 -2
- package/dist/claude/transcript.js +14 -3
- package/dist/codex/rollout-synth.d.ts +8 -3
- package/dist/codex/rollout-synth.js +65 -32
- package/dist/codex-app-server/client.d.ts +27 -40
- package/dist/codex-app-server/client.js +1134 -99
- package/dist/codex-app-server/forwarder.d.ts +36 -10
- package/dist/codex-app-server/forwarder.js +146 -28
- package/dist/codex-app-server/mapping.d.ts +1 -1
- package/dist/codex-app-server/mapping.js +64 -5
- package/dist/codex-app-server/protocol.d.ts +269 -4
- package/dist/codex-app-server/transport.d.ts +20 -5
- package/dist/codex-app-server/transport.js +93 -40
- package/dist/codex-app-server/ws-channel.d.ts +3 -3
- package/dist/codex-app-server/ws-channel.js +23 -7
- package/dist/codex-child-env.js +33 -0
- package/dist/codex-home.d.ts +16 -6
- package/dist/codex-home.js +46 -15
- package/dist/codex-session-store.d.ts +2 -1
- package/dist/host.d.ts +38 -38
- package/dist/host.js +626 -121
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/input-resources.d.ts +13 -0
- package/dist/input-resources.js +67 -0
- package/dist/interactions.d.ts +61 -0
- package/dist/interactions.js +236 -0
- package/dist/models-catalog.d.ts +5 -13
- package/dist/models-catalog.js +60 -9
- package/dist/runner/child.d.ts +9 -1
- package/dist/runner/child.js +100 -19
- package/dist/runner/manager.d.ts +79 -11
- package/dist/runner/manager.js +423 -43
- package/dist/runner/protocol.d.ts +30 -11
- package/dist/runner-main.js +9 -6
- package/dist/runtime-status.js +1 -1
- package/dist/terminal/claude-tui.d.ts +8 -3
- package/dist/terminal/claude-tui.js +6 -2
- package/dist/terminal/codex-tui.d.ts +3 -3
- package/dist/terminal/codex-tui.js +1 -1
- package/dist/terminal/registry.d.ts +1 -1
- package/dist/terminal/registry.js +1 -1
- package/dist/terminal/tmux.d.ts +6 -6
- package/dist/terminal/tmux.js +10 -10
- package/package.json +8 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persistent codex session forwarder — the heart of
|
|
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 (
|
|
13
|
+
* response id across snapshot/live, so items converge (reference implementation's `codex_{turnId}`).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
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?(
|
|
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 (
|
|
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
|
|
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).
|
|
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
|
|
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
|
|
11
|
+
function userMessageContent(item) {
|
|
11
12
|
if (item.type !== "userMessage")
|
|
12
13
|
return undefined;
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.join("")
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
177
|
+
for (const event of canonicalEvents) {
|
|
134
178
|
this.sink.onEvent(event);
|
|
135
179
|
}
|
|
136
180
|
if (mapped.fatalError) {
|
|
137
|
-
this.
|
|
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.
|
|
143
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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"
|
|
120
|
-
return {
|
|
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) {
|