@rynx-ai/runtime 0.1.11-beta.5 → 0.1.11-beta.50
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 +19 -5
- package/dist/claude/executor.js +56 -12
- package/dist/claude/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- package/dist/claude/native-bridge.d.ts +103 -1
- package/dist/claude/native-bridge.js +460 -30
- package/dist/claude/native-hook-main.js +81 -1
- package/dist/claude/native-hooks.js +7 -0
- package/dist/claude/native-integration.d.ts +178 -26
- package/dist/claude/native-integration.js +1571 -170
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/claude/transcript-clone.d.ts +18 -0
- package/dist/claude/transcript-clone.js +497 -0
- package/dist/claude/transcript.d.ts +27 -4
- package/dist/claude/transcript.js +183 -46
- package/dist/codex-app-server/client.d.ts +10 -6
- package/dist/codex-app-server/client.js +67 -15
- package/dist/codex-app-server/forwarder.d.ts +96 -3
- package/dist/codex-app-server/forwarder.js +542 -57
- package/dist/codex-app-server/mapping.d.ts +3 -6
- package/dist/codex-app-server/mapping.js +228 -36
- package/dist/codex-app-server/mcp-startup.d.ts +13 -0
- package/dist/codex-app-server/mcp-startup.js +63 -0
- package/dist/codex-app-server/process-registry.d.ts +36 -0
- package/dist/codex-app-server/process-registry.js +320 -0
- package/dist/codex-app-server/protocol.d.ts +68 -7
- package/dist/codex-app-server/transport.d.ts +2 -2
- package/dist/codex-app-server/transport.js +4 -0
- package/dist/codex-app-server/ws-channel.d.ts +8 -1
- package/dist/codex-app-server/ws-channel.js +113 -28
- package/dist/codex-child-env.js +3 -32
- package/dist/codex-home.d.ts +35 -3
- package/dist/codex-home.js +323 -18
- package/dist/codex-session-store.d.ts +23 -0
- package/dist/codex-session-store.js +21 -0
- package/dist/host.d.ts +106 -48
- package/dist/host.js +2024 -640
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/models-catalog.js +12 -3
- package/dist/runner/child.d.ts +105 -28
- package/dist/runner/child.js +1625 -100
- package/dist/runner/manager.d.ts +114 -46
- package/dist/runner/manager.js +1523 -446
- package/dist/runner/protocol.d.ts +212 -24
- package/dist/runner/protocol.js +5 -0
- package/dist/runner/startup-policy.d.ts +7 -0
- package/dist/runner/startup-policy.js +10 -0
- package/dist/runner/transport.d.ts +18 -2
- package/dist/runner/transport.js +82 -3
- package/dist/runner-main.js +8 -3
- package/dist/terminal/codex-tui.d.ts +4 -0
- package/dist/terminal/codex-tui.js +5 -0
- package/dist/terminal/control-parser.d.ts +39 -0
- package/dist/terminal/control-parser.js +172 -0
- package/dist/terminal/registry.d.ts +18 -15
- package/dist/terminal/registry.js +44 -23
- package/dist/terminal/spool.d.ts +47 -0
- package/dist/terminal/spool.js +231 -0
- package/dist/terminal/tmux.d.ts +126 -74
- package/dist/terminal/tmux.js +807 -211
- package/package.json +4 -4
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
16
|
import { open, stat } from "node:fs/promises";
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
18
19
|
/** A sub-agent (Task) writes its own transcript to
|
|
19
20
|
* `<project>/<sessionId>/subagents/agent-<agentId>.jsonl`, alongside the parent
|
|
20
21
|
* `<sessionId>.jsonl`. Derive that path from the parent transcript path. */
|
|
@@ -22,11 +23,37 @@ export function subagentTranscriptPath(parentTranscriptPath, agentId) {
|
|
|
22
23
|
const dir = parentTranscriptPath.replace(/\.jsonl$/, "");
|
|
23
24
|
return join(dir, "subagents", `agent-${agentId}.jsonl`);
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
function strippedImagePlaceholder(source) {
|
|
27
|
+
const mediaType = source.media_type;
|
|
28
|
+
const label = typeof mediaType === "string" && mediaType ? `${mediaType} image` : "image";
|
|
29
|
+
return `[${label} omitted from history to save context — re-run the tool call above (e.g. Read the same path) to view it again]`;
|
|
30
|
+
}
|
|
31
|
+
/** Remove Claude's inline image bytes before tool output reaches canonical
|
|
32
|
+
* history. A Read image result can contain a full-resolution base64 payload;
|
|
33
|
+
* replaying it as text only bloats the transcript, while the model cannot use
|
|
34
|
+
* those encoded bytes as text. Keep a small, human-readable marker instead. */
|
|
35
|
+
function stripInlineImageData(value, preview) {
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return value.map((part) => stripInlineImageData(part, preview));
|
|
38
|
+
if (!isObject(value))
|
|
39
|
+
return value;
|
|
40
|
+
const source = isObject(value.source) ? value.source : undefined;
|
|
41
|
+
if (value.type === "image" && source) {
|
|
42
|
+
return {
|
|
43
|
+
type: "text",
|
|
44
|
+
text: value === preview
|
|
45
|
+
? `[${source.media_type} image attached]`
|
|
46
|
+
: strippedImagePlaceholder(source),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, stripInlineImageData(nested, preview)]));
|
|
50
|
+
}
|
|
51
|
+
function stringifyToolContent(content, preview) {
|
|
52
|
+
const stripped = stripInlineImageData(content, preview);
|
|
53
|
+
if (typeof stripped === "string")
|
|
54
|
+
return stripped;
|
|
55
|
+
if (Array.isArray(stripped)) {
|
|
56
|
+
return stripped
|
|
30
57
|
.map((part) => part && typeof part === "object" && "text" in part
|
|
31
58
|
? String(part.text ?? "")
|
|
32
59
|
: typeof part === "string"
|
|
@@ -34,22 +61,45 @@ function stringifyToolContent(content) {
|
|
|
34
61
|
: JSON.stringify(part))
|
|
35
62
|
.join("");
|
|
36
63
|
}
|
|
37
|
-
return
|
|
64
|
+
return stripped == null ? "" : JSON.stringify(stripped);
|
|
38
65
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
/** Native Read returns inline image bytes, sometimes transcoded from the file.
|
|
67
|
+
* Reuse the tool-image resource pipeline so the preview preserves those bytes
|
|
68
|
+
* while the canonical output text stays small. Remaining images keep markers
|
|
69
|
+
* until the shared tool-output contract supports more than one preview. */
|
|
70
|
+
function inlineImagePreview(content) {
|
|
71
|
+
if (!Array.isArray(content))
|
|
72
|
+
return undefined;
|
|
73
|
+
for (const block of content) {
|
|
74
|
+
if (!isObject(block) || block.type !== "image" || !isObject(block.source))
|
|
75
|
+
continue;
|
|
76
|
+
const { type, media_type: mediaType, data } = block.source;
|
|
77
|
+
if (type === "base64" &&
|
|
78
|
+
typeof mediaType === "string" && /^image\/[a-z0-9+.-]+$/i.test(mediaType) &&
|
|
79
|
+
typeof data === "string" && /^[A-Za-z0-9+/]+={0,2}$/.test(data)) {
|
|
80
|
+
return { block, result: `data:${mediaType};base64,${data}` };
|
|
46
81
|
}
|
|
47
82
|
}
|
|
48
|
-
return
|
|
83
|
+
return undefined;
|
|
49
84
|
}
|
|
50
85
|
const BASH_INPUT_RE = /<bash-input>([\s\S]*?)<\/bash-input>/;
|
|
51
86
|
const BASH_STDOUT_RE = /<bash-stdout>([\s\S]*?)<\/bash-stdout>/;
|
|
52
87
|
const BASH_STDERR_RE = /<bash-stderr>([\s\S]*?)<\/bash-stderr>/;
|
|
88
|
+
/** Parse either half of Claude's shell-mode record. Newer and older Claude
|
|
89
|
+
* builds may split `<bash-input>` from the later stdout/stderr record. */
|
|
90
|
+
export function parseTerminalCommandFragments(content) {
|
|
91
|
+
const input = BASH_INPUT_RE.exec(content);
|
|
92
|
+
const stdout = BASH_STDOUT_RE.exec(content);
|
|
93
|
+
const stderr = BASH_STDERR_RE.exec(content);
|
|
94
|
+
if (!input && !stdout && !stderr)
|
|
95
|
+
return undefined;
|
|
96
|
+
return {
|
|
97
|
+
...(input ? { command: input[1].trim() } : {}),
|
|
98
|
+
...(stdout ? { stdout: stdout[1] } : {}),
|
|
99
|
+
...(stderr ? { stderr: stderr[1] } : {}),
|
|
100
|
+
hasOutput: Boolean(stdout || stderr),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
53
103
|
/**
|
|
54
104
|
* Parse a claude local-command (`!` bash mode) user record's string content into
|
|
55
105
|
* a {@link TerminalCommandData}. Claude records the command and its captured
|
|
@@ -59,16 +109,14 @@ const BASH_STDERR_RE = /<bash-stderr>([\s\S]*?)<\/bash-stderr>/;
|
|
|
59
109
|
* `<system-reminder>`) are bookkeeping, not terminal commands.
|
|
60
110
|
*/
|
|
61
111
|
export function parseTerminalCommand(content) {
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
112
|
+
const fragments = parseTerminalCommandFragments(content);
|
|
113
|
+
if (!fragments || fragments.command === undefined)
|
|
64
114
|
return undefined;
|
|
65
|
-
const data = { command:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
data.
|
|
70
|
-
if (stderr)
|
|
71
|
-
data.stderr = stderr;
|
|
115
|
+
const data = { command: fragments.command };
|
|
116
|
+
if (fragments.stdout)
|
|
117
|
+
data.stdout = fragments.stdout;
|
|
118
|
+
if (fragments.stderr)
|
|
119
|
+
data.stderr = fragments.stderr;
|
|
72
120
|
return data;
|
|
73
121
|
}
|
|
74
122
|
/**
|
|
@@ -103,26 +151,42 @@ export function parseTranscriptRecord(record, opts) {
|
|
|
103
151
|
if (rec.isSidechain && !parent)
|
|
104
152
|
return []; // sub-agent turns are replayed separately
|
|
105
153
|
const message = rec.message;
|
|
106
|
-
if (!message
|
|
154
|
+
if (!message)
|
|
107
155
|
return [];
|
|
108
156
|
const out = [];
|
|
109
157
|
if (rec.type === "assistant" && message.role === "assistant") {
|
|
158
|
+
const content = typeof message.content === "string"
|
|
159
|
+
? [{ type: "text", text: message.content }]
|
|
160
|
+
: message.content;
|
|
161
|
+
if (!Array.isArray(content))
|
|
162
|
+
return [];
|
|
110
163
|
// Claude writes this zero-token synthetic filler after an accidental empty
|
|
111
164
|
// submit. It is transcript bookkeeping, not an assistant response. Filter
|
|
112
165
|
// by provider provenance and exact shape here, before normalization loses
|
|
113
166
|
// `model: "<synthetic>"`; a UI string filter could hide legitimate output.
|
|
114
167
|
if (!rec.isApiErrorMessage &&
|
|
115
168
|
message.model === "<synthetic>" &&
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
169
|
+
content.length === 1 &&
|
|
170
|
+
content[0]?.type === "text" &&
|
|
171
|
+
content[0].text === "No response requested.") {
|
|
119
172
|
return [];
|
|
120
173
|
}
|
|
121
174
|
const itemId = message.id;
|
|
122
|
-
const
|
|
123
|
-
for (const block of
|
|
175
|
+
const textBlockCount = content.filter((block) => block.type === "text" && block.text).length;
|
|
176
|
+
for (const [blockIndex, block] of content.entries()) {
|
|
124
177
|
if (block.type === "text" && block.text) {
|
|
125
|
-
|
|
178
|
+
// Preserve Claude's block order. Keep a single text block's
|
|
179
|
+
// native message id for MessageDisplay correlation; only derive an id
|
|
180
|
+
// when one record contains multiple independently ordered text blocks.
|
|
181
|
+
const textItemId = itemId && textBlockCount > 1
|
|
182
|
+
? `${itemId}:text:${blockIndex}`
|
|
183
|
+
: itemId;
|
|
184
|
+
out.push({
|
|
185
|
+
type: "message_completed",
|
|
186
|
+
text: block.text,
|
|
187
|
+
...(textItemId ? { itemId: textItemId } : {}),
|
|
188
|
+
...parentTag,
|
|
189
|
+
});
|
|
126
190
|
}
|
|
127
191
|
else if (block.type === "thinking" && block.thinking) {
|
|
128
192
|
out.push({ type: "reasoning_completed", summary: [block.thinking], ...(itemId ? { itemId } : {}), ...parentTag });
|
|
@@ -132,31 +196,35 @@ export function parseTranscriptRecord(record, opts) {
|
|
|
132
196
|
type: "tool",
|
|
133
197
|
event: "on_tool_start",
|
|
134
198
|
name: block.name,
|
|
135
|
-
input: { ...(isObject(block.input) ? block.input : {}), id: block.id
|
|
136
|
-
|
|
199
|
+
input: { ...(isObject(block.input) ? block.input : {}), id: block.id },
|
|
200
|
+
// A transcript tool_use is already a completed Provider record. Live
|
|
201
|
+
// runtimes still omit this marker and remain in_progress until their
|
|
202
|
+
// completion event arrives.
|
|
203
|
+
data: { id: block.id, itemStatus: "completed" },
|
|
137
204
|
...parentTag,
|
|
138
205
|
});
|
|
139
206
|
}
|
|
140
207
|
}
|
|
141
|
-
if (texts.length) {
|
|
142
|
-
out.push({ type: "message_completed", text: texts.join(""), ...(itemId ? { itemId } : {}), ...parentTag });
|
|
143
|
-
}
|
|
144
208
|
return out;
|
|
145
209
|
}
|
|
146
210
|
if (rec.type === "user" && message.role === "user") {
|
|
211
|
+
if (!Array.isArray(message.content))
|
|
212
|
+
return [];
|
|
147
213
|
for (const block of message.content) {
|
|
148
214
|
if (block.type === "tool_result") {
|
|
215
|
+
const preview = block.is_error ? undefined : inlineImagePreview(block.content);
|
|
149
216
|
out.push({
|
|
150
217
|
type: "tool",
|
|
151
218
|
event: "on_tool_end",
|
|
152
219
|
output: {
|
|
153
220
|
id: block.tool_use_id,
|
|
154
221
|
status: block.is_error ? "failed" : "completed",
|
|
155
|
-
aggregatedOutput: stringifyToolContent(block.content),
|
|
222
|
+
aggregatedOutput: stringifyToolContent(block.content, preview?.block),
|
|
156
223
|
exitCode: null,
|
|
224
|
+
...(preview ? { generatedImage: { result: preview.result } } : {}),
|
|
157
225
|
},
|
|
158
226
|
...(block.is_error ? { error: "tool_error" } : {}),
|
|
159
|
-
data: { tool_use_id: block.tool_use_id },
|
|
227
|
+
data: { tool_use_id: block.tool_use_id, itemStatus: "completed" },
|
|
160
228
|
...parentTag,
|
|
161
229
|
});
|
|
162
230
|
}
|
|
@@ -169,13 +237,14 @@ function isObject(value) {
|
|
|
169
237
|
}
|
|
170
238
|
/**
|
|
171
239
|
* Whether a transcript is a `/fork` (branch) of another session: claude stamps a
|
|
172
|
-
* `forkedFrom: { sessionId }` marker
|
|
240
|
+
* `forkedFrom: { sessionId }` marker on copied records pointing at the source
|
|
173
241
|
* session (reference implementation's `transcript_has_forked_from_marker`). Used to distinguish a
|
|
174
242
|
* fork from an ordinary `resume` (both arrive as `SessionStart source="resume"`).
|
|
175
|
-
*
|
|
176
|
-
*
|
|
243
|
+
* The marker must belong to the announced target and expected source; sample
|
|
244
|
+
* the first and last 200 records because long copied histories can place it at
|
|
245
|
+
* either edge.
|
|
177
246
|
*/
|
|
178
|
-
export function transcriptHasForkedFrom(path,
|
|
247
|
+
export function transcriptHasForkedFrom(path, claudeSessionId, sourceClaudeSessionId) {
|
|
179
248
|
let text;
|
|
180
249
|
try {
|
|
181
250
|
text = readFileSync(path, "utf8");
|
|
@@ -183,17 +252,23 @@ export function transcriptHasForkedFrom(path, currentSessionId) {
|
|
|
183
252
|
catch {
|
|
184
253
|
return false;
|
|
185
254
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
255
|
+
const lines = text.split("\n");
|
|
256
|
+
const sampled = lines.length <= 400
|
|
257
|
+
? lines
|
|
258
|
+
: [...lines.slice(0, 200), ...lines.slice(-200)];
|
|
259
|
+
for (const line of sampled) {
|
|
190
260
|
const trimmed = line.trim();
|
|
191
261
|
if (!trimmed || !trimmed.includes("forkedFrom"))
|
|
192
262
|
continue;
|
|
193
263
|
try {
|
|
194
264
|
const rec = JSON.parse(trimmed);
|
|
265
|
+
if (rec.sessionId !== claudeSessionId)
|
|
266
|
+
continue;
|
|
195
267
|
const from = rec.forkedFrom?.sessionId;
|
|
196
|
-
if (typeof from === "string" &&
|
|
268
|
+
if (typeof from === "string" &&
|
|
269
|
+
from &&
|
|
270
|
+
from !== claudeSessionId &&
|
|
271
|
+
(sourceClaudeSessionId === undefined || from === sourceClaudeSessionId))
|
|
197
272
|
return true;
|
|
198
273
|
}
|
|
199
274
|
catch {
|
|
@@ -202,6 +277,68 @@ export function transcriptHasForkedFrom(path, currentSessionId) {
|
|
|
202
277
|
}
|
|
203
278
|
return false;
|
|
204
279
|
}
|
|
280
|
+
const RECENT_LOCAL_COMMAND_LINE_LIMIT = 200;
|
|
281
|
+
const RECENT_LOCAL_COMMAND_WINDOW_MS = 10_000;
|
|
282
|
+
/** Claude versions without a copied-record marker still persist `/fork` and
|
|
283
|
+
* `/branch` as a recent top-level local command. Match only the new native
|
|
284
|
+
* Session and the hook's narrow time window. */
|
|
285
|
+
export function transcriptHasRecentLocalCommand(path, claudeSessionId, recordedAtMs, commandNames = new Set(["/fork", "/branch"])) {
|
|
286
|
+
let text;
|
|
287
|
+
try {
|
|
288
|
+
text = readFileSync(path, "utf8");
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
for (const line of text.split("\n").slice(-RECENT_LOCAL_COMMAND_LINE_LIMIT)) {
|
|
294
|
+
const trimmed = line.trim();
|
|
295
|
+
if (!trimmed)
|
|
296
|
+
continue;
|
|
297
|
+
try {
|
|
298
|
+
const rec = JSON.parse(trimmed);
|
|
299
|
+
if (rec.sessionId !== claudeSessionId || rec.subtype !== "local_command")
|
|
300
|
+
continue;
|
|
301
|
+
const timestamp = transcriptTimestampMs(rec.timestamp);
|
|
302
|
+
if (timestamp === undefined ||
|
|
303
|
+
Math.abs(timestamp - recordedAtMs) > RECENT_LOCAL_COMMAND_WINDOW_MS ||
|
|
304
|
+
typeof rec.content !== "string")
|
|
305
|
+
continue;
|
|
306
|
+
const command = /<command-name>([\s\S]*?)<\/command-name>/
|
|
307
|
+
.exec(rec.content)?.[1]?.trim();
|
|
308
|
+
if (command && commandNames.has(command))
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// skip malformed
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
/** Wait briefly for Claude to flush either fork signal after SessionStart.
|
|
318
|
+
* The observer hook calls this before recording the edge, allowing a
|
|
319
|
+
* one-second late-marker window without delaying ordinary transcript polling. */
|
|
320
|
+
export async function waitForTranscriptForkSignal(path, claudeSessionId, sourceClaudeSessionId, recordedAtMs, options = {}) {
|
|
321
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? 1_000);
|
|
322
|
+
const pollMs = Math.max(1, options.pollMs ?? 50);
|
|
323
|
+
const deadline = Date.now() + timeoutMs;
|
|
324
|
+
do {
|
|
325
|
+
if (transcriptHasForkedFrom(path, claudeSessionId, sourceClaudeSessionId) ||
|
|
326
|
+
transcriptHasRecentLocalCommand(path, claudeSessionId, recordedAtMs))
|
|
327
|
+
return true;
|
|
328
|
+
if (Date.now() >= deadline)
|
|
329
|
+
return false;
|
|
330
|
+
await sleep(Math.min(pollMs, Math.max(1, deadline - Date.now())));
|
|
331
|
+
} while (true);
|
|
332
|
+
}
|
|
333
|
+
function transcriptTimestampMs(value) {
|
|
334
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
335
|
+
return value < 10_000_000_000 ? value * 1_000 : value;
|
|
336
|
+
}
|
|
337
|
+
if (typeof value !== "string" || !value)
|
|
338
|
+
return undefined;
|
|
339
|
+
const parsed = Date.parse(value);
|
|
340
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
341
|
+
}
|
|
205
342
|
/**
|
|
206
343
|
* Read a sub-agent (Task) transcript in full and map it to {@link AgentEvent}s
|
|
207
344
|
* tagged with `parentToolUseId`. Called once the parent Task tool_result arrives
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RuntimeUserInput, SessionInteractionResolution } from "@rynx-ai/core";
|
|
2
|
-
import type { AskForApproval, ClientInfo, CollaborationModeListResponse, GetAuthStatusParams, GetAuthStatusResponse, InitializeResponse, ModelListParams, ModelListResponse, ReviewStartParams, ReviewStartResponse, SandboxMode, ThreadForkParams, ThreadGoalClearParams, ThreadGoalGetParams, ThreadGoalGetResponse, ThreadGoalSetParams, ThreadListParams, ThreadListResponse, ResumedThread, ThreadResumeParams, ThreadSettingsUpdateParams, ThreadStartParams, TurnInterruptParams, TurnStartParams, TurnSteerParams, UserInput } from "./protocol.js";
|
|
2
|
+
import type { AskForApproval, ClientInfo, CollaborationModeListResponse, ConfigReadParams, ConfigReadResponse, GetAuthStatusParams, GetAuthStatusResponse, InitializeResponse, ModelListParams, ModelListResponse, ReviewStartParams, ReviewStartResponse, SandboxMode, ThreadForkParams, ThreadGoalClearParams, ThreadGoalGetParams, ThreadGoalGetResponse, ThreadGoalSetParams, ThreadListParams, ThreadListResponse, ResumedThread, ThreadResumeParams, ThreadRuntimeSettings, ThreadSettingsUpdateParams, ThreadStartParams, TurnInterruptParams, TurnStartParams, TurnSteerParams, UserInput } from "./protocol.js";
|
|
3
3
|
import { CodexAppServerTransport, type CodexAppServerProcessSpawner, type RpcChannel, type TransportLogger } from "./transport.js";
|
|
4
4
|
import type { ResolveInteractionResult, RuntimeInteractionListener } from "../interactions.js";
|
|
5
5
|
export type ApprovalDecisionPolicy = "auto-approve-session" | "auto-decline" | "auto-cancel";
|
|
@@ -29,9 +29,11 @@ export declare class CodexAppServerClient {
|
|
|
29
29
|
private interactionListener;
|
|
30
30
|
private connectionListener;
|
|
31
31
|
private connectionState;
|
|
32
|
+
private connectionEstablished;
|
|
32
33
|
private readonly pendingInteractions;
|
|
33
34
|
private readonly settledInteractions;
|
|
34
35
|
private initializeResponse;
|
|
36
|
+
private initializePromise;
|
|
35
37
|
constructor({ spawner, channel, logger, clientInfo, approvalDecisionPolicy, }: CodexAppServerClientOptions);
|
|
36
38
|
/**
|
|
37
39
|
* The multi-client endpoint a `codex --remote` TUI can attach to, when this
|
|
@@ -42,13 +44,16 @@ export declare class CodexAppServerClient {
|
|
|
42
44
|
terminalRemoteUrl(): string | undefined;
|
|
43
45
|
ensureInitialized(): Promise<InitializeResponse>;
|
|
44
46
|
getAuthStatus(params?: GetAuthStatusParams): Promise<GetAuthStatusResponse>;
|
|
47
|
+
/** Read the runtime's merged effective config. This is the stable seam for
|
|
48
|
+
* discovering a fresh thread's model across Traex YAML/TOML generations. */
|
|
49
|
+
configRead(params: ConfigReadParams): Promise<ConfigReadResponse>;
|
|
45
50
|
threadStart(params: ThreadStartParams): Promise<{
|
|
46
51
|
threadId: string;
|
|
47
|
-
}>;
|
|
52
|
+
} & ThreadRuntimeSettings>;
|
|
48
53
|
threadResume(params: ThreadResumeParams): Promise<{
|
|
49
54
|
threadId: string;
|
|
50
55
|
thread: ResumedThread;
|
|
51
|
-
}>;
|
|
56
|
+
} & ThreadRuntimeSettings>;
|
|
52
57
|
turnStart(params: TurnStartParams): Promise<{
|
|
53
58
|
turnId: string;
|
|
54
59
|
}>;
|
|
@@ -94,9 +99,8 @@ export declare class CodexAppServerClient {
|
|
|
94
99
|
* resolver still wins correctly.
|
|
95
100
|
*/
|
|
96
101
|
setInteractionListener(listener: RuntimeInteractionListener | null): void;
|
|
97
|
-
/** Observe the
|
|
98
|
-
*
|
|
99
|
-
* native request still has to count as unavailable during host failover. */
|
|
102
|
+
/** Observe the initialized connection lifecycle. Registration never reports
|
|
103
|
+
* disconnected for a client that has not connected yet. */
|
|
100
104
|
setConnectionListener(listener: ((state: "connected" | "disconnected") => void) | null): void;
|
|
101
105
|
private setConnectionState;
|
|
102
106
|
resolveInteraction(interactionId: string, resolution: SessionInteractionResolution): ResolveInteractionResult;
|
|
@@ -953,9 +953,11 @@ export class CodexAppServerClient {
|
|
|
953
953
|
interactionListener = null;
|
|
954
954
|
connectionListener = null;
|
|
955
955
|
connectionState = "disconnected";
|
|
956
|
+
connectionEstablished = false;
|
|
956
957
|
pendingInteractions = new Map();
|
|
957
958
|
settledInteractions = new Set();
|
|
958
959
|
initializeResponse = null;
|
|
960
|
+
initializePromise = null;
|
|
959
961
|
constructor({ spawner, channel, logger = defaultLogger, clientInfo = DEFAULT_CLIENT_INFO, approvalDecisionPolicy = "auto-approve-session", }) {
|
|
960
962
|
this.logger = logger;
|
|
961
963
|
this.clientInfo = clientInfo;
|
|
@@ -988,30 +990,66 @@ export class CodexAppServerClient {
|
|
|
988
990
|
if (this.initializeResponse) {
|
|
989
991
|
return this.initializeResponse;
|
|
990
992
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
993
|
+
if (this.initializePromise)
|
|
994
|
+
return this.initializePromise;
|
|
995
|
+
const initializing = (async () => {
|
|
996
|
+
await this.transport.ensureStarted();
|
|
997
|
+
const response = await this.transport.sendRequest("initialize", {
|
|
998
|
+
clientInfo: this.clientInfo,
|
|
999
|
+
capabilities: { experimentalApi: true },
|
|
1000
|
+
});
|
|
1001
|
+
// Codex app-server uses the full initialize handshake: it does not accept
|
|
1002
|
+
// capability requests after merely replying to `initialize`. The client
|
|
1003
|
+
// must acknowledge that response with the `initialized` notification
|
|
1004
|
+
// before `thread/resume`, `turn/start`, and the other APIs are legal.
|
|
1005
|
+
await this.transport.sendNotification("initialized");
|
|
1006
|
+
this.initializeResponse = response;
|
|
1007
|
+
this.setConnectionState("connected");
|
|
1008
|
+
return response;
|
|
1009
|
+
})();
|
|
1010
|
+
this.initializePromise = initializing;
|
|
1011
|
+
try {
|
|
1012
|
+
return await initializing;
|
|
1013
|
+
}
|
|
1014
|
+
finally {
|
|
1015
|
+
if (this.initializePromise === initializing)
|
|
1016
|
+
this.initializePromise = null;
|
|
1017
|
+
}
|
|
999
1018
|
}
|
|
1000
1019
|
async getAuthStatus(params = {}) {
|
|
1001
1020
|
await this.ensureInitialized();
|
|
1002
1021
|
return this.transport.sendRequest("getAuthStatus", params);
|
|
1003
1022
|
}
|
|
1023
|
+
/** Read the runtime's merged effective config. This is the stable seam for
|
|
1024
|
+
* discovering a fresh thread's model across Traex YAML/TOML generations. */
|
|
1025
|
+
async configRead(params) {
|
|
1026
|
+
await this.ensureInitialized();
|
|
1027
|
+
return this.transport.sendRequest("config/read", params);
|
|
1028
|
+
}
|
|
1004
1029
|
async threadStart(params) {
|
|
1005
1030
|
await this.ensureInitialized();
|
|
1006
1031
|
const response = await this.transport.sendRequest("thread/start", params);
|
|
1007
|
-
return {
|
|
1032
|
+
return {
|
|
1033
|
+
threadId: response.thread.id,
|
|
1034
|
+
...(response.model ? { model: response.model } : {}),
|
|
1035
|
+
...(response.reasoningEffort === undefined
|
|
1036
|
+
? {}
|
|
1037
|
+
: { reasoningEffort: response.reasoningEffort }),
|
|
1038
|
+
};
|
|
1008
1039
|
}
|
|
1009
1040
|
async threadResume(params) {
|
|
1010
1041
|
await this.ensureInitialized();
|
|
1011
1042
|
const response = await this.transport.sendRequest("thread/resume", params);
|
|
1012
1043
|
// Expose the whole `thread` (not just its id): its `turns[].items[]` are the
|
|
1013
1044
|
// backfill the forwarder replays for a fresh thread's first turn.
|
|
1014
|
-
return {
|
|
1045
|
+
return {
|
|
1046
|
+
threadId: response.thread.id,
|
|
1047
|
+
thread: response.thread,
|
|
1048
|
+
...(response.model ? { model: response.model } : {}),
|
|
1049
|
+
...(response.reasoningEffort === undefined
|
|
1050
|
+
? {}
|
|
1051
|
+
: { reasoningEffort: response.reasoningEffort }),
|
|
1052
|
+
};
|
|
1015
1053
|
}
|
|
1016
1054
|
async turnStart(params) {
|
|
1017
1055
|
await this.ensureInitialized();
|
|
@@ -1146,14 +1184,18 @@ export class CodexAppServerClient {
|
|
|
1146
1184
|
setInteractionListener(listener) {
|
|
1147
1185
|
this.interactionListener = listener;
|
|
1148
1186
|
}
|
|
1149
|
-
/** Observe the
|
|
1150
|
-
*
|
|
1151
|
-
* native request still has to count as unavailable during host failover. */
|
|
1187
|
+
/** Observe the initialized connection lifecycle. Registration never reports
|
|
1188
|
+
* disconnected for a client that has not connected yet. */
|
|
1152
1189
|
setConnectionListener(listener) {
|
|
1153
1190
|
this.connectionListener = listener;
|
|
1154
|
-
listener
|
|
1191
|
+
if (listener && this.connectionEstablished)
|
|
1192
|
+
listener(this.connectionState);
|
|
1155
1193
|
}
|
|
1156
1194
|
setConnectionState(state) {
|
|
1195
|
+
if (state === "connected")
|
|
1196
|
+
this.connectionEstablished = true;
|
|
1197
|
+
if (state === "disconnected" && !this.connectionEstablished)
|
|
1198
|
+
return;
|
|
1157
1199
|
if (this.connectionState === state)
|
|
1158
1200
|
return;
|
|
1159
1201
|
this.connectionState = state;
|
|
@@ -1363,7 +1405,17 @@ export function buildTextUserInput(message) {
|
|
|
1363
1405
|
export function buildRuntimeUserInput(input) {
|
|
1364
1406
|
return input.content.map((part) => part.type === "text"
|
|
1365
1407
|
? { type: "text", text: part.text, text_elements: [] }
|
|
1366
|
-
:
|
|
1408
|
+
: part.type === "local_image"
|
|
1409
|
+
? { type: "localImage", path: part.path }
|
|
1410
|
+
: {
|
|
1411
|
+
type: "text",
|
|
1412
|
+
text: `[[RYNX_FILE_RESOURCE ${JSON.stringify({
|
|
1413
|
+
path: part.path,
|
|
1414
|
+
...(part.resource.filename ? { filename: part.resource.filename } : {}),
|
|
1415
|
+
mediaType: part.resource.mediaType,
|
|
1416
|
+
})}]]\nInspect this absolute file path with the available file-reading tools before answering.`,
|
|
1417
|
+
text_elements: [],
|
|
1418
|
+
});
|
|
1367
1419
|
}
|
|
1368
1420
|
const defaultLogger = {
|
|
1369
1421
|
log(entry) {
|