@rynx-ai/runtime 0.1.0 → 0.1.9
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 +291 -39
- package/dist/claude/native-hooks.d.ts +3 -2
- package/dist/claude/native-hooks.js +15 -6
- package/dist/claude/native-integration.d.ts +78 -5
- package/dist/claude/native-integration.js +417 -26
- 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 +3 -3
- package/dist/codex/rollout-synth.js +1 -1
- package/dist/codex-app-server/client.d.ts +26 -40
- package/dist/codex-app-server/client.js +1128 -99
- package/dist/codex-app-server/forwarder.d.ts +7 -7
- package/dist/codex-app-server/forwarder.js +11 -5
- package/dist/codex-app-server/mapping.d.ts +1 -1
- package/dist/codex-app-server/mapping.js +27 -2
- package/dist/codex-app-server/protocol.d.ts +238 -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 +6 -6
- package/dist/codex-home.js +8 -9
- package/dist/codex-session-store.d.ts +2 -1
- package/dist/host.d.ts +34 -33
- package/dist/host.js +531 -91
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/interactions.d.ts +61 -0
- package/dist/interactions.js +236 -0
- package/dist/models-catalog.d.ts +1 -1
- package/dist/models-catalog.js +1 -1
- package/dist/runner/child.d.ts +9 -1
- package/dist/runner/child.js +93 -15
- package/dist/runner/manager.d.ts +59 -10
- package/dist/runner/manager.js +385 -41
- package/dist/runner/protocol.d.ts +18 -7
- 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 +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ export { LocalAgentHost, CodexRuntimeError, SpawnCodexCommandRunner, FileCodexSe
|
|
|
8
8
|
export type { CodexCapabilities, CapabilityResult, CodexRuntimeStatus, } from "./host.js";
|
|
9
9
|
export type { CodexSessionStore, CodexSessionRecord } from "./codex-session-store.js";
|
|
10
10
|
export { ensureCodexResumeRollout } from "./codex/rollout-synth.js";
|
|
11
|
-
export { RunnerManager } from "./runner/manager.js";
|
|
12
|
-
export type { RunnerManagerOptions, OpenTerminalOptions, ParentTerminal, } from "./runner/manager.js";
|
|
13
|
-
export type { InjectOutcome, TerminalRole } from "./runner/protocol.js";
|
|
11
|
+
export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
|
|
12
|
+
export type { RunnerManagerOptions, RunnerSessionContext, RunnerSessionContextProvider, OpenTerminalOptions, ParentTerminal, } from "./runner/manager.js";
|
|
13
|
+
export type { InjectOutcome, TerminalOpenErrorCode, TerminalRole } from "./runner/protocol.js";
|
|
14
|
+
export type { ResolveInteractionResult, RuntimeInteractionEvent, RuntimeInteractionListener, } from "./interactions.js";
|
|
14
15
|
export { probeRuntimeStatus } from "./runtime-status.js";
|
|
15
16
|
export { listRuntimeModels } from "./models-catalog.js";
|
|
16
17
|
export { TmuxTerminal, isTmuxAvailable } from "./terminal/tmux.js";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { ensureCodexResumeRollout } from "./codex/rollout-synth.js";
|
|
|
9
9
|
// Runner subprocess layer: the parent-side manager (an `AgentExecutor` +
|
|
10
10
|
// `AgentCapabilities` that spawns per-session runner children) plus the wire
|
|
11
11
|
// types. The composition root uses `RunnerManager` in place of `LocalAgentHost`.
|
|
12
|
-
export { RunnerManager } from "./runner/manager.js";
|
|
12
|
+
export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
|
|
13
13
|
export { probeRuntimeStatus } from "./runtime-status.js";
|
|
14
14
|
export { listRuntimeModels } from "./models-catalog.js";
|
|
15
15
|
// Live-terminal subsystem (Phase C): tmux-backed terminals + per-runner registry.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type SessionInteractionRequest, type SessionInteractionResolution } from "@rynx-ai/core";
|
|
2
|
+
/** Provider-neutral lifecycle emitted by a native runtime adapter. */
|
|
3
|
+
export type RuntimeInteractionEvent = {
|
|
4
|
+
type: "requested";
|
|
5
|
+
request: SessionInteractionRequest;
|
|
6
|
+
/** Native Turn correlation, when the provider exposes it. */
|
|
7
|
+
turnId?: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "resolved";
|
|
10
|
+
interactionId: string;
|
|
11
|
+
resolution: SessionInteractionResolution;
|
|
12
|
+
turnId?: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "cancelled";
|
|
15
|
+
interactionId: string;
|
|
16
|
+
reason?: string;
|
|
17
|
+
turnId?: string;
|
|
18
|
+
};
|
|
19
|
+
export type RuntimeInteractionListener = (event: RuntimeInteractionEvent) => void;
|
|
20
|
+
/** Result of attempting to answer a pending native interaction. */
|
|
21
|
+
export type ResolveInteractionResult = {
|
|
22
|
+
disposition: "applied";
|
|
23
|
+
} | {
|
|
24
|
+
disposition: "already_resolved";
|
|
25
|
+
} | {
|
|
26
|
+
disposition: "not_found";
|
|
27
|
+
} | {
|
|
28
|
+
disposition: "invalid";
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
export type InteractionResolution = SessionInteractionResolution;
|
|
32
|
+
export declare const INTERACTION_LIMITS: {
|
|
33
|
+
readonly idBytes: 256;
|
|
34
|
+
readonly titleBytes: 1024;
|
|
35
|
+
readonly labelBytes: 1024;
|
|
36
|
+
readonly descriptionBytes: number;
|
|
37
|
+
readonly contextBytes: number;
|
|
38
|
+
readonly requestBytes: number;
|
|
39
|
+
readonly fields: 64;
|
|
40
|
+
readonly optionsPerField: 128;
|
|
41
|
+
readonly actions: 16;
|
|
42
|
+
readonly routingSkeletonBytes: number;
|
|
43
|
+
};
|
|
44
|
+
export type BoundInteractionRequestResult = {
|
|
45
|
+
ok: true;
|
|
46
|
+
request: SessionInteractionRequest;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: string;
|
|
50
|
+
};
|
|
51
|
+
/** Bound an untrusted provider request before it enters the canonical event
|
|
52
|
+
* stream. Lists that affect native answer routing are rejected when oversized;
|
|
53
|
+
* option lists may be truncated to a still-answerable subset. */
|
|
54
|
+
export declare function boundInteractionRequest(request: SessionInteractionRequest): BoundInteractionRequestResult;
|
|
55
|
+
/** Validate a provider-neutral resolution against the exact advertised form.
|
|
56
|
+
* Unknown keys and wrong answer shapes are rejected even for deny/cancel; the
|
|
57
|
+
* selected action's `requiresAnswers` controls only whether required fields may
|
|
58
|
+
* be omitted. */
|
|
59
|
+
export declare function validateInteractionResolution(request: SessionInteractionRequest, resolution: SessionInteractionResolution): string | undefined;
|
|
60
|
+
/** Secret values reach the native runtime but never enter canonical history. */
|
|
61
|
+
export declare function redactInteractionResolution(request: SessionInteractionRequest, resolution: SessionInteractionResolution): SessionInteractionResolution;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { boundSessionInteractionRouting, sessionInteractionRoutingSkeletonBytes, SESSION_INTERACTION_STRUCTURAL_LIMITS, } from "@rynx-ai/core";
|
|
2
|
+
export const INTERACTION_LIMITS = {
|
|
3
|
+
...SESSION_INTERACTION_STRUCTURAL_LIMITS,
|
|
4
|
+
idBytes: 256,
|
|
5
|
+
titleBytes: 1024,
|
|
6
|
+
labelBytes: 1024,
|
|
7
|
+
descriptionBytes: 8 * 1024,
|
|
8
|
+
contextBytes: 64 * 1024,
|
|
9
|
+
// SessionNormalizer carries the request twice (event + durable item). 240 KiB
|
|
10
|
+
// leaves 32 KiB for both wrappers under the daemon's 512 KiB event limit.
|
|
11
|
+
requestBytes: 240 * 1024,
|
|
12
|
+
};
|
|
13
|
+
/** UTF-8 byte-safe truncation: iteration is by Unicode code point, so neither a
|
|
14
|
+
* surrogate pair nor a multi-byte sequence is split. */
|
|
15
|
+
function truncateUtf8(value, maxBytes) {
|
|
16
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
17
|
+
return value;
|
|
18
|
+
const suffix = "…";
|
|
19
|
+
const budget = Math.max(0, maxBytes - Buffer.byteLength(suffix, "utf8"));
|
|
20
|
+
let result = "";
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
for (const char of value) {
|
|
23
|
+
const width = Buffer.byteLength(char, "utf8");
|
|
24
|
+
if (bytes + width > budget)
|
|
25
|
+
break;
|
|
26
|
+
result += char;
|
|
27
|
+
bytes += width;
|
|
28
|
+
}
|
|
29
|
+
return `${result}${suffix}`;
|
|
30
|
+
}
|
|
31
|
+
function validId(id) {
|
|
32
|
+
return id.length > 0 && Buffer.byteLength(id, "utf8") <= INTERACTION_LIMITS.idBytes;
|
|
33
|
+
}
|
|
34
|
+
/** Bound an untrusted provider request before it enters the canonical event
|
|
35
|
+
* stream. Lists that affect native answer routing are rejected when oversized;
|
|
36
|
+
* option lists may be truncated to a still-answerable subset. */
|
|
37
|
+
export function boundInteractionRequest(request) {
|
|
38
|
+
if (!validId(request.interactionId)) {
|
|
39
|
+
return { ok: false, reason: "interaction id exceeds the canonical limit" };
|
|
40
|
+
}
|
|
41
|
+
if (request.fields.length > INTERACTION_LIMITS.fields) {
|
|
42
|
+
return { ok: false, reason: "interaction has too many fields" };
|
|
43
|
+
}
|
|
44
|
+
if (request.actions.length === 0 || request.actions.length > INTERACTION_LIMITS.actions) {
|
|
45
|
+
return { ok: false, reason: "interaction has an invalid number of actions" };
|
|
46
|
+
}
|
|
47
|
+
const seenFields = new Set();
|
|
48
|
+
const fields = [];
|
|
49
|
+
for (const field of request.fields) {
|
|
50
|
+
if (!validId(field.id) || seenFields.has(field.id)) {
|
|
51
|
+
return { ok: false, reason: "interaction has an invalid or duplicate field id" };
|
|
52
|
+
}
|
|
53
|
+
seenFields.add(field.id);
|
|
54
|
+
const label = truncateUtf8(field.label, INTERACTION_LIMITS.labelBytes);
|
|
55
|
+
const description = field.description !== undefined
|
|
56
|
+
? truncateUtf8(field.description, INTERACTION_LIMITS.descriptionBytes)
|
|
57
|
+
: undefined;
|
|
58
|
+
if (field.type === "text") {
|
|
59
|
+
fields.push({
|
|
60
|
+
id: field.id,
|
|
61
|
+
type: "text",
|
|
62
|
+
label,
|
|
63
|
+
...(description !== undefined ? { description } : {}),
|
|
64
|
+
...(field.required !== undefined ? { required: field.required } : {}),
|
|
65
|
+
...(field.secret !== undefined ? { secret: field.secret } : {}),
|
|
66
|
+
...(field.multiline !== undefined ? { multiline: field.multiline } : {}),
|
|
67
|
+
...(field.placeholder !== undefined
|
|
68
|
+
? { placeholder: truncateUtf8(field.placeholder, INTERACTION_LIMITS.labelBytes) }
|
|
69
|
+
: {}),
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const options = [];
|
|
74
|
+
const seenOptions = new Set();
|
|
75
|
+
for (const option of field.options.slice(0, INTERACTION_LIMITS.optionsPerField)) {
|
|
76
|
+
if (!validId(option.value) || seenOptions.has(option.value)) {
|
|
77
|
+
return { ok: false, reason: `interaction field has an invalid option id: ${field.id}` };
|
|
78
|
+
}
|
|
79
|
+
seenOptions.add(option.value);
|
|
80
|
+
options.push({
|
|
81
|
+
...option,
|
|
82
|
+
label: truncateUtf8(option.label, INTERACTION_LIMITS.labelBytes),
|
|
83
|
+
...(option.description !== undefined
|
|
84
|
+
? { description: truncateUtf8(option.description, INTERACTION_LIMITS.descriptionBytes) }
|
|
85
|
+
: {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
fields.push({
|
|
89
|
+
id: field.id,
|
|
90
|
+
type: "select",
|
|
91
|
+
label,
|
|
92
|
+
...(description !== undefined ? { description } : {}),
|
|
93
|
+
...(field.required !== undefined ? { required: field.required } : {}),
|
|
94
|
+
...(field.multiple !== undefined ? { multiple: field.multiple } : {}),
|
|
95
|
+
...(field.allowOther !== undefined ? { allowOther: field.allowOther } : {}),
|
|
96
|
+
options,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const seenActions = new Set();
|
|
100
|
+
const actions = [];
|
|
101
|
+
for (const action of request.actions) {
|
|
102
|
+
if (!validId(action.id) || seenActions.has(action.id)) {
|
|
103
|
+
return { ok: false, reason: "interaction has an invalid or duplicate action id" };
|
|
104
|
+
}
|
|
105
|
+
seenActions.add(action.id);
|
|
106
|
+
actions.push({
|
|
107
|
+
...action,
|
|
108
|
+
label: truncateUtf8(action.label, INTERACTION_LIMITS.labelBytes),
|
|
109
|
+
...(action.description !== undefined
|
|
110
|
+
? { description: truncateUtf8(action.description, INTERACTION_LIMITS.descriptionBytes) }
|
|
111
|
+
: {}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const context = request.context
|
|
115
|
+
? {
|
|
116
|
+
...(request.context.summary !== undefined
|
|
117
|
+
? { summary: truncateUtf8(request.context.summary, INTERACTION_LIMITS.descriptionBytes) }
|
|
118
|
+
: {}),
|
|
119
|
+
...(request.context.toolName !== undefined
|
|
120
|
+
? { toolName: truncateUtf8(request.context.toolName, INTERACTION_LIMITS.labelBytes) }
|
|
121
|
+
: {}),
|
|
122
|
+
...(request.context.command !== undefined
|
|
123
|
+
? { command: truncateUtf8(request.context.command, INTERACTION_LIMITS.contextBytes) }
|
|
124
|
+
: {}),
|
|
125
|
+
...(request.context.cwd !== undefined
|
|
126
|
+
? { cwd: truncateUtf8(request.context.cwd, INTERACTION_LIMITS.contextBytes) }
|
|
127
|
+
: {}),
|
|
128
|
+
...(request.context.diff !== undefined
|
|
129
|
+
? { diff: truncateUtf8(request.context.diff, INTERACTION_LIMITS.contextBytes) }
|
|
130
|
+
: {}),
|
|
131
|
+
}
|
|
132
|
+
: undefined;
|
|
133
|
+
const boundedPresentation = {
|
|
134
|
+
...request,
|
|
135
|
+
title: truncateUtf8(request.title, INTERACTION_LIMITS.titleBytes),
|
|
136
|
+
...(request.description !== undefined
|
|
137
|
+
? { description: truncateUtf8(request.description, INTERACTION_LIMITS.descriptionBytes) }
|
|
138
|
+
: {}),
|
|
139
|
+
fields,
|
|
140
|
+
actions,
|
|
141
|
+
...(context ? { context } : {}),
|
|
142
|
+
};
|
|
143
|
+
const bounded = boundSessionInteractionRouting(boundedPresentation);
|
|
144
|
+
const lostOptionCoverage = boundedPresentation.fields.some((source, index) => source.type === "select" &&
|
|
145
|
+
source.options.length > 0 &&
|
|
146
|
+
bounded.fields[index]?.type === "select" &&
|
|
147
|
+
bounded.fields[index].options.length === 0);
|
|
148
|
+
if (lostOptionCoverage) {
|
|
149
|
+
return { ok: false, reason: "interaction routing minimum coverage exceeds the canonical budget" };
|
|
150
|
+
}
|
|
151
|
+
if (sessionInteractionRoutingSkeletonBytes(bounded) > INTERACTION_LIMITS.routingSkeletonBytes) {
|
|
152
|
+
return { ok: false, reason: "interaction routing structure exceeds the canonical budget" };
|
|
153
|
+
}
|
|
154
|
+
if (Buffer.byteLength(JSON.stringify(bounded), "utf8") > INTERACTION_LIMITS.requestBytes) {
|
|
155
|
+
return { ok: false, reason: "interaction exceeds the canonical request budget" };
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, request: bounded };
|
|
158
|
+
}
|
|
159
|
+
function isRecord(value) {
|
|
160
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
/** Validate a provider-neutral resolution against the exact advertised form.
|
|
163
|
+
* Unknown keys and wrong answer shapes are rejected even for deny/cancel; the
|
|
164
|
+
* selected action's `requiresAnswers` controls only whether required fields may
|
|
165
|
+
* be omitted. */
|
|
166
|
+
export function validateInteractionResolution(request, resolution) {
|
|
167
|
+
const action = request.actions.find((candidate) => candidate.id === resolution.actionId);
|
|
168
|
+
if (!action)
|
|
169
|
+
return `unknown interaction action: ${resolution.actionId}`;
|
|
170
|
+
const rawAnswers = resolution.answers;
|
|
171
|
+
if (rawAnswers !== undefined && !isRecord(rawAnswers)) {
|
|
172
|
+
return "interaction answers must be an object";
|
|
173
|
+
}
|
|
174
|
+
const answers = rawAnswers ?? {};
|
|
175
|
+
const fields = new Map(request.fields.map((field) => [field.id, field]));
|
|
176
|
+
for (const [id, answer] of Object.entries(answers)) {
|
|
177
|
+
const field = fields.get(id);
|
|
178
|
+
if (!field)
|
|
179
|
+
return `unknown interaction answer: ${id}`;
|
|
180
|
+
if (field.type === "text") {
|
|
181
|
+
if (typeof answer !== "string")
|
|
182
|
+
return `answer must be a string: ${id}`;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const values = field.multiple
|
|
186
|
+
? Array.isArray(answer) && answer.every((value) => typeof value === "string")
|
|
187
|
+
? answer
|
|
188
|
+
: null
|
|
189
|
+
: typeof answer === "string"
|
|
190
|
+
? [answer]
|
|
191
|
+
: null;
|
|
192
|
+
if (!values) {
|
|
193
|
+
return field.multiple
|
|
194
|
+
? `answer must be a string array: ${id}`
|
|
195
|
+
: `answer must be a string: ${id}`;
|
|
196
|
+
}
|
|
197
|
+
const allowed = new Set(field.options.map((option) => option.value));
|
|
198
|
+
for (const value of values) {
|
|
199
|
+
if (allowed.has(value))
|
|
200
|
+
continue;
|
|
201
|
+
if (field.allowOther && value.trim().length > 0)
|
|
202
|
+
continue;
|
|
203
|
+
return `answer is not an allowed option: ${id}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!action.requiresAnswers)
|
|
207
|
+
return undefined;
|
|
208
|
+
for (const field of request.fields) {
|
|
209
|
+
if (!field.required)
|
|
210
|
+
continue;
|
|
211
|
+
const answer = answers[field.id];
|
|
212
|
+
const empty = answer === undefined ||
|
|
213
|
+
(typeof answer === "string" && answer.trim().length === 0) ||
|
|
214
|
+
(Array.isArray(answer) &&
|
|
215
|
+
(answer.length === 0 || answer.some((value) => value.trim().length === 0)));
|
|
216
|
+
if (empty)
|
|
217
|
+
return `answer is required: ${field.id}`;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
/** Secret values reach the native runtime but never enter canonical history. */
|
|
222
|
+
export function redactInteractionResolution(request, resolution) {
|
|
223
|
+
if (!resolution.answers)
|
|
224
|
+
return resolution;
|
|
225
|
+
const secretIds = new Set(request.fields
|
|
226
|
+
.filter((field) => field.type === "text" && field.secret)
|
|
227
|
+
.map((field) => field.id));
|
|
228
|
+
if (secretIds.size === 0)
|
|
229
|
+
return resolution;
|
|
230
|
+
const answers = { ...resolution.answers };
|
|
231
|
+
for (const id of secretIds) {
|
|
232
|
+
if (answers[id] !== undefined)
|
|
233
|
+
answers[id] = "[redacted]";
|
|
234
|
+
}
|
|
235
|
+
return { ...resolution, answers };
|
|
236
|
+
}
|
package/dist/models-catalog.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Backend-free model listing for codex/traex.
|
|
3
3
|
*
|
|
4
4
|
* The parent control plane no longer holds an app-server, so `/models` can't be
|
|
5
|
-
* a live `model/list` RPC anymore. Following
|
|
5
|
+
* a live `model/list` RPC anymore. Following reference implementation's static-catalog model, we
|
|
6
6
|
* serve a config-derived list: the runtime's configured default model (the one
|
|
7
7
|
* `resolveRuntimeModel` would pick), marked `isDefault`. This is intentionally
|
|
8
8
|
* minimal — a fuller curated catalogue can be added here later without touching
|
package/dist/models-catalog.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Backend-free model listing for codex/traex.
|
|
3
3
|
*
|
|
4
4
|
* The parent control plane no longer holds an app-server, so `/models` can't be
|
|
5
|
-
* a live `model/list` RPC anymore. Following
|
|
5
|
+
* a live `model/list` RPC anymore. Following reference implementation's static-catalog model, we
|
|
6
6
|
* serve a config-derived list: the runtime's configured default model (the one
|
|
7
7
|
* `resolveRuntimeModel` would pick), marked `isDefault`. This is intentionally
|
|
8
8
|
* minimal — a fuller curated catalogue can be added here later without touching
|
package/dist/runner/child.d.ts
CHANGED
|
@@ -23,11 +23,16 @@ export declare class RunnerSession {
|
|
|
23
23
|
/** Live terminals hosted by this session, and per-attach client handles. */
|
|
24
24
|
private readonly terminals;
|
|
25
25
|
private readonly attachments;
|
|
26
|
+
/** Opens are async; a close received before attach resolves tombstones the id. */
|
|
27
|
+
private readonly pendingTerminalOpens;
|
|
28
|
+
private readonly cancelledTerminalOpens;
|
|
26
29
|
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
27
30
|
private readonly liveIds;
|
|
31
|
+
private shuttingDown;
|
|
28
32
|
constructor({ transport, executor, onShutdown }: RunnerSessionDeps);
|
|
29
33
|
private handle;
|
|
30
34
|
private get liveProvider();
|
|
35
|
+
private resolveInteraction;
|
|
31
36
|
/** The mirror channel for a session: an `emit` that forwards every canonical
|
|
32
37
|
* event to the parent under the CURRENT session id (mutable), plus a `retarget`
|
|
33
38
|
* the host calls on a `/clear`·`/fork` rotation — it re-points the mirror to the
|
|
@@ -40,7 +45,7 @@ export declare class RunnerSession {
|
|
|
40
45
|
* launch the detached `codex --remote` TUI, which CREATES the codex thread. The
|
|
41
46
|
* forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
|
|
42
47
|
* subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
|
|
43
|
-
* rollout) and its turns mirror to chat (
|
|
48
|
+
* rollout) and its turns mirror to chat (reference implementation's model).
|
|
44
49
|
*/
|
|
45
50
|
private ensureLive;
|
|
46
51
|
private inject;
|
|
@@ -50,6 +55,9 @@ export declare class RunnerSession {
|
|
|
50
55
|
* so the web attach reuses the same detached pane. */
|
|
51
56
|
private launchCodexPane;
|
|
52
57
|
private stopLive;
|
|
58
|
+
/** Stop event forwarding, kill native terminals/hooks, then synchronously
|
|
59
|
+
* scrub provider handoff files before the child process is allowed to exit. */
|
|
60
|
+
shutdown(): void;
|
|
53
61
|
private openTerminal;
|
|
54
62
|
/** Attach an already-created terminal and forward its data/exit to the parent. */
|
|
55
63
|
private attachExisting;
|
package/dist/runner/child.js
CHANGED
|
@@ -7,15 +7,18 @@ export class RunnerSession {
|
|
|
7
7
|
/** Live terminals hosted by this session, and per-attach client handles. */
|
|
8
8
|
terminals = new TerminalRegistry();
|
|
9
9
|
attachments = new Map();
|
|
10
|
+
/** Opens are async; a close received before attach resolves tombstones the id. */
|
|
11
|
+
pendingTerminalOpens = new Set();
|
|
12
|
+
cancelledTerminalOpens = new Set();
|
|
10
13
|
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
11
14
|
liveIds = new Set();
|
|
15
|
+
shuttingDown = false;
|
|
12
16
|
constructor({ transport, executor, onShutdown }) {
|
|
13
17
|
this.transport = transport;
|
|
14
18
|
this.executor = executor;
|
|
15
19
|
this.onShutdown =
|
|
16
20
|
onShutdown ??
|
|
17
21
|
(() => {
|
|
18
|
-
this.terminals.closeAll();
|
|
19
22
|
transport.close();
|
|
20
23
|
});
|
|
21
24
|
this.transport.onMessage((msg) => this.handle(msg));
|
|
@@ -27,7 +30,20 @@ export class RunnerSession {
|
|
|
27
30
|
void this.runCap(msg.capId, msg.name, msg.args);
|
|
28
31
|
return;
|
|
29
32
|
case "term.open":
|
|
30
|
-
|
|
33
|
+
if (this.pendingTerminalOpens.has(msg.attachId) || this.attachments.has(msg.attachId)) {
|
|
34
|
+
this.transport.send({
|
|
35
|
+
t: "term.error",
|
|
36
|
+
attachId: msg.attachId,
|
|
37
|
+
code: "terminal_open_failed",
|
|
38
|
+
message: "terminal attachment id is already active",
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.pendingTerminalOpens.add(msg.attachId);
|
|
43
|
+
void this.openTerminal(msg).finally(() => {
|
|
44
|
+
this.pendingTerminalOpens.delete(msg.attachId);
|
|
45
|
+
this.cancelledTerminalOpens.delete(msg.attachId);
|
|
46
|
+
});
|
|
31
47
|
return;
|
|
32
48
|
case "term.input":
|
|
33
49
|
this.attachments.get(msg.attachId)?.write(Buffer.from(msg.dataB64, "base64").toString("utf8"));
|
|
@@ -36,14 +52,16 @@ export class RunnerSession {
|
|
|
36
52
|
this.attachments.get(msg.attachId)?.resize(msg.cols, msg.rows);
|
|
37
53
|
return;
|
|
38
54
|
case "term.close": {
|
|
55
|
+
if (this.pendingTerminalOpens.has(msg.attachId)) {
|
|
56
|
+
this.cancelledTerminalOpens.add(msg.attachId);
|
|
57
|
+
}
|
|
39
58
|
const attachment = this.attachments.get(msg.attachId);
|
|
40
59
|
this.attachments.delete(msg.attachId);
|
|
41
60
|
attachment?.kill();
|
|
42
61
|
return;
|
|
43
62
|
}
|
|
44
|
-
case "
|
|
45
|
-
|
|
46
|
-
void provider.resolveApproval?.(msg.localThreadId, msg.approvalId, msg.decision);
|
|
63
|
+
case "interaction.resolve": {
|
|
64
|
+
void this.resolveInteraction(msg);
|
|
47
65
|
return;
|
|
48
66
|
}
|
|
49
67
|
case "live.ensure":
|
|
@@ -56,15 +74,31 @@ export class RunnerSession {
|
|
|
56
74
|
void this.interruptLive(msg);
|
|
57
75
|
return;
|
|
58
76
|
case "shutdown":
|
|
59
|
-
this.
|
|
60
|
-
this.terminals.closeAll();
|
|
61
|
-
this.onShutdown();
|
|
77
|
+
this.shutdown();
|
|
62
78
|
return;
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
get liveProvider() {
|
|
66
82
|
return this.executor;
|
|
67
83
|
}
|
|
84
|
+
async resolveInteraction(msg) {
|
|
85
|
+
let result;
|
|
86
|
+
try {
|
|
87
|
+
result = await this.liveProvider.resolveInteraction?.(msg.localThreadId, msg.interactionId, msg.resolution) ?? { disposition: "not_found" };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
result = {
|
|
91
|
+
disposition: "invalid",
|
|
92
|
+
message: error instanceof Error ? error.message : String(error),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
this.transport.send({
|
|
96
|
+
t: "interaction.resolved",
|
|
97
|
+
reqId: msg.reqId,
|
|
98
|
+
localThreadId: msg.localThreadId,
|
|
99
|
+
result,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
68
102
|
/** The mirror channel for a session: an `emit` that forwards every canonical
|
|
69
103
|
* event to the parent under the CURRENT session id (mutable), plus a `retarget`
|
|
70
104
|
* the host calls on a `/clear`·`/fork` rotation — it re-points the mirror to the
|
|
@@ -94,20 +128,28 @@ export class RunnerSession {
|
|
|
94
128
|
* launch the detached `codex --remote` TUI, which CREATES the codex thread. The
|
|
95
129
|
* forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
|
|
96
130
|
* subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
|
|
97
|
-
* rollout) and its turns mirror to chat (
|
|
131
|
+
* rollout) and its turns mirror to chat (reference implementation's model).
|
|
98
132
|
*/
|
|
99
133
|
async ensureLive(msg) {
|
|
100
134
|
const provider = this.liveProvider;
|
|
101
135
|
try {
|
|
102
136
|
const { emit, retarget } = this.mirrorChannel(msg.localThreadId);
|
|
103
137
|
const started = await provider.ensureLiveCodexSession?.(msg.localThreadId, emit, {
|
|
138
|
+
...(msg.cwd ? { cwd: msg.cwd } : {}),
|
|
104
139
|
...(msg.runtime ? { runtime: msg.runtime } : {}),
|
|
140
|
+
...(msg.reasoningEffort ? { reasoningEffort: msg.reasoningEffort } : {}),
|
|
105
141
|
...(msg.agentName ? { agentName: msg.agentName } : {}),
|
|
106
142
|
...(msg.agentSpec ? { agentSpec: msg.agentSpec } : {}),
|
|
107
143
|
retargetMirror: retarget,
|
|
108
144
|
});
|
|
109
145
|
if (!started) {
|
|
110
|
-
this.transport.send({
|
|
146
|
+
this.transport.send({
|
|
147
|
+
t: "live.ready",
|
|
148
|
+
reqId: msg.reqId,
|
|
149
|
+
localThreadId: msg.localThreadId,
|
|
150
|
+
ok: false,
|
|
151
|
+
error: "live provider did not start",
|
|
152
|
+
});
|
|
111
153
|
return;
|
|
112
154
|
}
|
|
113
155
|
this.liveIds.add(msg.localThreadId);
|
|
@@ -122,7 +164,13 @@ export class RunnerSession {
|
|
|
122
164
|
const ready = provider.waitLiveReady
|
|
123
165
|
? await provider.waitLiveReady(msg.localThreadId)
|
|
124
166
|
: true;
|
|
125
|
-
this.transport.send({
|
|
167
|
+
this.transport.send({
|
|
168
|
+
t: "live.ready",
|
|
169
|
+
reqId: msg.reqId,
|
|
170
|
+
localThreadId: msg.localThreadId,
|
|
171
|
+
ok: ready,
|
|
172
|
+
...(ready ? {} : { error: "live session was not ready before timeout" }),
|
|
173
|
+
});
|
|
126
174
|
}
|
|
127
175
|
catch (error) {
|
|
128
176
|
this.transport.send({
|
|
@@ -186,13 +234,27 @@ export class RunnerSession {
|
|
|
186
234
|
this.liveProvider.attachTerminalInjector?.(localThreadId, term);
|
|
187
235
|
}
|
|
188
236
|
stopLive() {
|
|
189
|
-
for (const id of this.liveIds)
|
|
190
|
-
this.liveProvider.stopLiveCodexSession?.(id
|
|
237
|
+
for (const id of this.liveIds) {
|
|
238
|
+
this.liveProvider.stopLiveCodexSession?.(id, {
|
|
239
|
+
deferClaudeInteractionCleanup: true,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
191
242
|
this.liveIds.clear();
|
|
192
243
|
}
|
|
244
|
+
/** Stop event forwarding, kill native terminals/hooks, then synchronously
|
|
245
|
+
* scrub provider handoff files before the child process is allowed to exit. */
|
|
246
|
+
shutdown() {
|
|
247
|
+
if (this.shuttingDown)
|
|
248
|
+
return;
|
|
249
|
+
this.shuttingDown = true;
|
|
250
|
+
this.stopLive();
|
|
251
|
+
this.terminals.closeAll();
|
|
252
|
+
this.liveProvider.finalizeStoppedLiveSessions?.();
|
|
253
|
+
this.onShutdown();
|
|
254
|
+
}
|
|
193
255
|
async openTerminal(msg) {
|
|
194
256
|
try {
|
|
195
|
-
// codex/claude-native session (no explicit command): DUMB ATTACH —
|
|
257
|
+
// codex/claude-native session (no explicit command): DUMB ATTACH — reference implementation's
|
|
196
258
|
// reattach (codex_native.py:905-942, `app_server=None`). A tab switch ONLY
|
|
197
259
|
// attaches an already-live pane; it NEVER ensures the forwarder or relaunches a
|
|
198
260
|
// dead pane. Creation/relaunch happens on message-send (`live.ensure`) or an
|
|
@@ -202,7 +264,12 @@ export class RunnerSession {
|
|
|
202
264
|
if (!msg.command && msg.localThreadId) {
|
|
203
265
|
const existing = this.terminals.get(msg.terminalId);
|
|
204
266
|
if (!existing || !existing.isAlive()) {
|
|
205
|
-
this.transport.send({
|
|
267
|
+
this.transport.send({
|
|
268
|
+
t: "term.error",
|
|
269
|
+
attachId: msg.attachId,
|
|
270
|
+
code: "terminal_not_live",
|
|
271
|
+
message: "terminal not live",
|
|
272
|
+
});
|
|
206
273
|
return;
|
|
207
274
|
}
|
|
208
275
|
await this.attachExisting(msg);
|
|
@@ -222,6 +289,7 @@ export class RunnerSession {
|
|
|
222
289
|
this.transport.send({
|
|
223
290
|
t: "term.error",
|
|
224
291
|
attachId: msg.attachId,
|
|
292
|
+
code: "terminal_open_failed",
|
|
225
293
|
message: error instanceof Error ? error.message : String(error),
|
|
226
294
|
});
|
|
227
295
|
}
|
|
@@ -232,6 +300,16 @@ export class RunnerSession {
|
|
|
232
300
|
cols: msg.cols,
|
|
233
301
|
rows: msg.rows,
|
|
234
302
|
});
|
|
303
|
+
if (this.cancelledTerminalOpens.has(msg.attachId)) {
|
|
304
|
+
attachment.kill();
|
|
305
|
+
this.transport.send({
|
|
306
|
+
t: "term.error",
|
|
307
|
+
attachId: msg.attachId,
|
|
308
|
+
code: "terminal_open_failed",
|
|
309
|
+
message: "terminal attachment was cancelled while opening",
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
235
313
|
this.attachments.set(msg.attachId, attachment);
|
|
236
314
|
attachment.onData((chunk) => this.transport.send({
|
|
237
315
|
t: "term.data",
|