@rynx-ai/runtime 0.1.10 → 0.1.11-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/native-bridge.js +3 -8
- package/dist/claude/native-integration.d.ts +12 -1
- package/dist/claude/native-integration.js +16 -2
- package/dist/claude/transcript.d.ts +0 -7
- package/dist/claude/transcript.js +6 -20
- package/dist/codex-app-server/client.d.ts +2 -1
- package/dist/codex-app-server/forwarder.d.ts +4 -1
- package/dist/codex-app-server/forwarder.js +19 -1
- package/dist/codex-app-server/protocol.d.ts +45 -1
- package/dist/codex-home.d.ts +9 -26
- package/dist/codex-home.js +37 -65
- package/dist/codex-session-store.d.ts +22 -10
- package/dist/codex-session-store.js +277 -12
- package/dist/host.d.ts +47 -47
- package/dist/host.js +790 -350
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/models-catalog.d.ts +1 -0
- package/dist/models-catalog.js +43 -1
- package/dist/provider-workspace.d.ts +56 -0
- package/dist/provider-workspace.js +83 -0
- package/dist/runner/child.d.ts +54 -6
- package/dist/runner/child.js +42 -17
- package/dist/runner/manager.d.ts +41 -18
- package/dist/runner/manager.js +432 -55
- package/dist/runner/protocol.d.ts +7 -18
- package/dist/runner-main.js +12 -4
- package/dist/runtime-state-paths.d.ts +10 -0
- package/dist/runtime-state-paths.js +53 -0
- package/dist/terminal/claude-tui.d.ts +8 -1
- package/dist/terminal/claude-tui.js +7 -1
- package/dist/terminal/codex-tui.d.ts +5 -1
- package/dist/terminal/codex-tui.js +12 -3
- package/package.json +2 -2
- package/dist/codex/rollout-synth.d.ts +0 -42
- package/dist/codex/rollout-synth.js +0 -245
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { constants as fsConstants } from "node:fs";
|
|
3
|
-
import { access, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { access, mkdir, readFile, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import {
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { rynxHome, } from "@rynx-ai/core";
|
|
8
|
+
const FILE_LOCK_LEASE_MS = 5_000;
|
|
9
|
+
const FILE_LOCK_HEARTBEAT_MS = 1_000;
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
let ownProcessIdentity;
|
|
6
12
|
export class FileCodexSessionStore {
|
|
7
13
|
filePath;
|
|
8
14
|
tail = Promise.resolve();
|
|
@@ -44,6 +50,27 @@ export class FileCodexSessionStore {
|
|
|
44
50
|
}
|
|
45
51
|
return null;
|
|
46
52
|
}
|
|
53
|
+
async getClaudeForkIntent(targetSessionId) {
|
|
54
|
+
const data = await this.readAll();
|
|
55
|
+
return data.claudeForkIntents?.[targetSessionId] ?? null;
|
|
56
|
+
}
|
|
57
|
+
async setClaudeForkIntent(intent) {
|
|
58
|
+
await this.runExclusive(async () => {
|
|
59
|
+
const data = await this.readAll();
|
|
60
|
+
data.claudeForkIntents ??= {};
|
|
61
|
+
data.claudeForkIntents[intent.targetSessionId] = intent;
|
|
62
|
+
await this.writeAll(data);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async deleteClaudeForkIntent(targetSessionId) {
|
|
66
|
+
await this.runExclusive(async () => {
|
|
67
|
+
const data = await this.readAll();
|
|
68
|
+
if (!data.claudeForkIntents?.[targetSessionId])
|
|
69
|
+
return;
|
|
70
|
+
delete data.claudeForkIntents[targetSessionId];
|
|
71
|
+
await this.writeAll(data);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
47
74
|
async isWritable() {
|
|
48
75
|
try {
|
|
49
76
|
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
@@ -66,14 +93,22 @@ export class FileCodexSessionStore {
|
|
|
66
93
|
if (!parsed || typeof parsed !== "object") {
|
|
67
94
|
return {};
|
|
68
95
|
}
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
96
|
+
// The provider binding no longer owns workspace/execution state. Keep
|
|
97
|
+
// accepting legacy JSON fields, but strip them from the in-memory record
|
|
98
|
+
// so callers cannot accidentally treat them as a second snapshot.
|
|
72
99
|
if (parsed.sessions) {
|
|
73
|
-
for (const record of Object.
|
|
74
|
-
if (record)
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
for (const [id, record] of Object.entries(parsed.sessions)) {
|
|
101
|
+
if (!record)
|
|
102
|
+
continue;
|
|
103
|
+
parsed.sessions[id] = {
|
|
104
|
+
localThreadId: record.localThreadId,
|
|
105
|
+
codexSessionId: record.codexSessionId,
|
|
106
|
+
...(record.parentSessionId ? { parentSessionId: record.parentSessionId } : {}),
|
|
107
|
+
...(record.runtimeHomeOwnerSessionId
|
|
108
|
+
? { runtimeHomeOwnerSessionId: record.runtimeHomeOwnerSessionId }
|
|
109
|
+
: {}),
|
|
110
|
+
updatedAt: record.updatedAt,
|
|
111
|
+
};
|
|
77
112
|
}
|
|
78
113
|
}
|
|
79
114
|
return parsed;
|
|
@@ -98,16 +133,113 @@ export class FileCodexSessionStore {
|
|
|
98
133
|
release = resolve;
|
|
99
134
|
});
|
|
100
135
|
await previous.catch(() => undefined);
|
|
136
|
+
let releaseFileLock;
|
|
101
137
|
try {
|
|
138
|
+
releaseFileLock = await this.acquireFileLock();
|
|
102
139
|
return await task();
|
|
103
140
|
}
|
|
104
141
|
finally {
|
|
105
|
-
|
|
142
|
+
try {
|
|
143
|
+
await releaseFileLock?.();
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
release();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async acquireFileLock() {
|
|
151
|
+
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
152
|
+
const lockPath = `${this.filePath}.lock`;
|
|
153
|
+
const recoveryPath = `${lockPath}.recovery`;
|
|
154
|
+
const token = randomUUID();
|
|
155
|
+
const ownerPath = path.join(lockPath, "owner");
|
|
156
|
+
ownProcessIdentity ??= processIdentity(process.pid);
|
|
157
|
+
const processIdentityValue = await ownProcessIdentity;
|
|
158
|
+
if (!processIdentityValue) {
|
|
159
|
+
throw new Error("could not resolve Session binding lock process identity");
|
|
106
160
|
}
|
|
161
|
+
for (let attempt = 0; attempt < 1_000; attempt += 1) {
|
|
162
|
+
if (await pathExists(recoveryPath)) {
|
|
163
|
+
await recoverExpiredLockDirectory(recoveryPath);
|
|
164
|
+
await lockRetryDelay();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (await publishOwnedLockDirectory(lockPath, token, processIdentityValue)) {
|
|
168
|
+
const heartbeat = startLockHeartbeat(lockPath);
|
|
169
|
+
let recoveryWaits = 0;
|
|
170
|
+
while (await pathExists(recoveryPath)) {
|
|
171
|
+
await recoverExpiredLockDirectory(recoveryPath);
|
|
172
|
+
recoveryWaits += 1;
|
|
173
|
+
if (recoveryWaits >= 1_000) {
|
|
174
|
+
clearInterval(heartbeat);
|
|
175
|
+
await releaseOwnedLockDirectory(lockPath, token, "release");
|
|
176
|
+
throw new Error(`timed out waiting for Session binding store recovery: ${this.filePath}`);
|
|
177
|
+
}
|
|
178
|
+
await lockRetryDelay();
|
|
179
|
+
}
|
|
180
|
+
if (await lockOwnerToken(ownerPath) !== token) {
|
|
181
|
+
clearInterval(heartbeat);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
return async () => {
|
|
185
|
+
clearInterval(heartbeat);
|
|
186
|
+
await releaseOwnedLockDirectory(lockPath, token, "release");
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const lockStat = await stat(lockPath);
|
|
191
|
+
if (Date.now() - lockStat.mtimeMs > FILE_LOCK_LEASE_MS) {
|
|
192
|
+
const recoveryToken = randomUUID();
|
|
193
|
+
const recoveryOwnerPath = path.join(recoveryPath, "owner");
|
|
194
|
+
if (!await publishOwnedLockDirectory(recoveryPath, recoveryToken, processIdentityValue)) {
|
|
195
|
+
await lockRetryDelay();
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const recoveryHeartbeat = startLockHeartbeat(recoveryPath);
|
|
199
|
+
try {
|
|
200
|
+
const currentStat = await stat(lockPath);
|
|
201
|
+
if (Date.now() - currentStat.mtimeMs <= FILE_LOCK_LEASE_MS) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const owner = await readLockOwner(ownerPath);
|
|
205
|
+
if (owner && await lockOwnerProcessIsCurrent(owner)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (await lockOwnerToken(recoveryOwnerPath) !== recoveryToken) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const stalePath = `${lockPath}.stale-${randomUUID()}`;
|
|
212
|
+
try {
|
|
213
|
+
await rename(lockPath, stalePath);
|
|
214
|
+
await rm(stalePath, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (!isMissingFileError(error))
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
clearInterval(recoveryHeartbeat);
|
|
223
|
+
await releaseOwnedLockDirectory(recoveryPath, recoveryToken, "recovery-release");
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!isMissingFileError(error))
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
await lockRetryDelay();
|
|
233
|
+
}
|
|
234
|
+
throw new Error(`timed out acquiring Session binding store lock: ${this.filePath}`);
|
|
107
235
|
}
|
|
108
236
|
}
|
|
109
|
-
export function resolveCodexSessionStorePath(config) {
|
|
110
|
-
|
|
237
|
+
export function resolveCodexSessionStorePath(config, env = process.env) {
|
|
238
|
+
const home = rynxHome(env);
|
|
239
|
+
const configured = config.AGENT_SESSION_STORE_PATH;
|
|
240
|
+
return configured
|
|
241
|
+
? path.resolve(home, configured)
|
|
242
|
+
: path.join(home, ".codex-proxy", "sessions.json");
|
|
111
243
|
}
|
|
112
244
|
async function pathExists(targetPath) {
|
|
113
245
|
try {
|
|
@@ -124,3 +256,136 @@ async function pathExists(targetPath) {
|
|
|
124
256
|
function isMissingFileError(error) {
|
|
125
257
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
126
258
|
}
|
|
259
|
+
async function readLockOwner(ownerPath) {
|
|
260
|
+
try {
|
|
261
|
+
const owner = JSON.parse(await readFile(ownerPath, "utf8"));
|
|
262
|
+
if (!Number.isInteger(owner.pid)
|
|
263
|
+
|| owner.pid <= 0
|
|
264
|
+
|| typeof owner.token !== "string"
|
|
265
|
+
|| (owner.processIdentity !== null && typeof owner.processIdentity !== "string")) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return owner;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function lockOwnerToken(ownerPath) {
|
|
275
|
+
return (await readLockOwner(ownerPath))?.token ?? null;
|
|
276
|
+
}
|
|
277
|
+
async function lockOwnerProcessIsCurrent(owner) {
|
|
278
|
+
if (!owner.processIdentity)
|
|
279
|
+
return false;
|
|
280
|
+
return await processIdentity(owner.pid) === owner.processIdentity;
|
|
281
|
+
}
|
|
282
|
+
async function processIdentity(pid) {
|
|
283
|
+
try {
|
|
284
|
+
if (process.platform === "linux") {
|
|
285
|
+
const [statLine, bootId] = await Promise.all([
|
|
286
|
+
readFile(`/proc/${pid}/stat`, "utf8"),
|
|
287
|
+
readFile("/proc/sys/kernel/random/boot_id", "utf8"),
|
|
288
|
+
]);
|
|
289
|
+
const closeParen = statLine.lastIndexOf(")");
|
|
290
|
+
const fields = statLine.slice(closeParen + 2).trim().split(/\s+/);
|
|
291
|
+
const startTicks = fields[19];
|
|
292
|
+
return startTicks ? `linux:${bootId.trim()}:${startTicks}` : null;
|
|
293
|
+
}
|
|
294
|
+
if (process.platform === "darwin") {
|
|
295
|
+
const { stdout } = await execFileAsync("/bin/ps", ["-p", String(pid), "-o", "lstart=", "-o", "command="], {
|
|
296
|
+
encoding: "utf8",
|
|
297
|
+
env: { ...process.env, LANG: "C", LC_ALL: "C" },
|
|
298
|
+
});
|
|
299
|
+
const startedAt = stdout.trim();
|
|
300
|
+
return startedAt ? `darwin:${startedAt}` : null;
|
|
301
|
+
}
|
|
302
|
+
if (process.platform === "win32") {
|
|
303
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
304
|
+
"-NoProfile",
|
|
305
|
+
"-Command",
|
|
306
|
+
`(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
|
|
307
|
+
], { encoding: "utf8" });
|
|
308
|
+
const startedAt = stdout.trim();
|
|
309
|
+
return startedAt ? `win32:${startedAt}` : null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
function lockRetryDelay() {
|
|
318
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
319
|
+
}
|
|
320
|
+
function startLockHeartbeat(lockPath) {
|
|
321
|
+
const heartbeat = setInterval(() => {
|
|
322
|
+
const now = new Date();
|
|
323
|
+
void utimes(lockPath, now, now).catch(() => undefined);
|
|
324
|
+
}, FILE_LOCK_HEARTBEAT_MS);
|
|
325
|
+
heartbeat.unref?.();
|
|
326
|
+
return heartbeat;
|
|
327
|
+
}
|
|
328
|
+
async function publishOwnedLockDirectory(lockPath, token, processIdentityValue) {
|
|
329
|
+
const candidatePath = `${lockPath}.claim-${process.pid}-${token}`;
|
|
330
|
+
await mkdir(candidatePath);
|
|
331
|
+
try {
|
|
332
|
+
await writeFile(path.join(candidatePath, "owner"), JSON.stringify({
|
|
333
|
+
pid: process.pid,
|
|
334
|
+
processIdentity: processIdentityValue,
|
|
335
|
+
token,
|
|
336
|
+
}), "utf8");
|
|
337
|
+
try {
|
|
338
|
+
await rename(candidatePath, lockPath);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (isLockContentionError(error))
|
|
343
|
+
return false;
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
await rm(candidatePath, { recursive: true, force: true });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function recoverExpiredLockDirectory(lockPath) {
|
|
352
|
+
try {
|
|
353
|
+
const lockStat = await stat(lockPath);
|
|
354
|
+
if (Date.now() - lockStat.mtimeMs <= FILE_LOCK_LEASE_MS)
|
|
355
|
+
return false;
|
|
356
|
+
const owner = await readLockOwner(path.join(lockPath, "owner"));
|
|
357
|
+
if (owner && await lockOwnerProcessIsCurrent(owner))
|
|
358
|
+
return false;
|
|
359
|
+
const stalePath = `${lockPath}.stale-${randomUUID()}`;
|
|
360
|
+
await rename(lockPath, stalePath);
|
|
361
|
+
await rm(stalePath, { recursive: true, force: true });
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (isMissingFileError(error))
|
|
366
|
+
return false;
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function releaseOwnedLockDirectory(lockPath, token, suffix) {
|
|
371
|
+
try {
|
|
372
|
+
if (await lockOwnerToken(path.join(lockPath, "owner")) !== token)
|
|
373
|
+
return;
|
|
374
|
+
const releasePath = `${lockPath}.${suffix}-${token}`;
|
|
375
|
+
await rename(lockPath, releasePath);
|
|
376
|
+
await rm(releasePath, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (!isMissingFileError(error))
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function isLockContentionError(error) {
|
|
384
|
+
return Boolean(error
|
|
385
|
+
&& typeof error === "object"
|
|
386
|
+
&& "code" in error
|
|
387
|
+
&& (error.code === "EEXIST"
|
|
388
|
+
|| error.code === "ENOTEMPTY"
|
|
389
|
+
|| error.code === "EACCES"
|
|
390
|
+
|| error.code === "EPERM"));
|
|
391
|
+
}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, 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";
|
|
@@ -7,9 +7,9 @@ import type { ResolveInteractionResult } from "./interactions.js";
|
|
|
7
7
|
import { CodexAppServerClient } from "./codex-app-server/client.js";
|
|
8
8
|
import type { ModelListResponse, ThreadGoal } from "./codex-app-server/protocol.js";
|
|
9
9
|
import { type TerminalInjector } from "./claude/native-integration.js";
|
|
10
|
-
import { FileCodexSessionStore, resolveCodexSessionStorePath, type CodexSessionRecord, type CodexSessionStore } from "./codex-session-store.js";
|
|
10
|
+
import { FileCodexSessionStore, resolveCodexSessionStorePath, type ClaudeForkIntent, type CodexSessionRecord, type CodexSessionStore } from "./codex-session-store.js";
|
|
11
11
|
import { AgentRuntimeError as CodexRuntimeError } from "@rynx-ai/core";
|
|
12
|
-
export { FileCodexSessionStore, resolveCodexSessionStorePath, type CodexSessionRecord, type CodexSessionStore, };
|
|
12
|
+
export { FileCodexSessionStore, resolveCodexSessionStorePath, type CodexSessionRecord, type CodexSessionStore, type ClaudeForkIntent, };
|
|
13
13
|
export interface CodexRuntimeStatus {
|
|
14
14
|
codex_available: boolean;
|
|
15
15
|
logged_in: boolean;
|
|
@@ -84,12 +84,6 @@ export interface CodexCapabilities {
|
|
|
84
84
|
getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
|
|
85
85
|
setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
|
|
86
86
|
clearGoal(localThreadId: string): Promise<CapabilityResult>;
|
|
87
|
-
/**
|
|
88
|
-
* Fork the Codex thread bound to `currentLocalThreadId` and bind the forked
|
|
89
|
-
* thread (carrying full context) to `newLocalThreadId`, so a new Lark
|
|
90
|
-
* conversation resumes from it. Used by `/fork`.
|
|
91
|
-
*/
|
|
92
|
-
forkSession(currentLocalThreadId: string, newLocalThreadId: string): Promise<CapabilityResult>;
|
|
93
87
|
}
|
|
94
88
|
/**
|
|
95
89
|
* Per-session claude-native live handle. claude has no app-server, so the
|
|
@@ -102,20 +96,15 @@ export interface CodexCapabilities {
|
|
|
102
96
|
* The runner-child owns the mirror emitter + transport, so it supplies this. */
|
|
103
97
|
export type RetargetMirror = (newSessionId: string, meta: {
|
|
104
98
|
kind: "clear" | "fork";
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
cwd?: string;
|
|
99
|
+
workspace: SessionWorkspaceSnapshot;
|
|
100
|
+
execution: ResolvedExecutionSnapshot;
|
|
108
101
|
parentSessionId?: string;
|
|
109
102
|
}) => void;
|
|
110
103
|
export interface LiveSessionOpts {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
* launch applies the agent's model / skills / instructions — not just the
|
|
116
|
-
* runtime. Absent ⇒ falls back to the config defaults (prior behavior). */
|
|
117
|
-
agentName?: string;
|
|
118
|
-
agentSpec?: AgentSpec;
|
|
104
|
+
/** Immutable Session-owned snapshots. Provider launch/resume never re-opens a
|
|
105
|
+
* Project or Agent template. */
|
|
106
|
+
workspace: SessionWorkspaceSnapshot;
|
|
107
|
+
execution: ResolvedExecutionSnapshot;
|
|
119
108
|
retargetMirror?: RetargetMirror;
|
|
120
109
|
}
|
|
121
110
|
/**
|
|
@@ -138,6 +127,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
138
127
|
private readonly forwarderClientFactory;
|
|
139
128
|
private readonly backends;
|
|
140
129
|
private readonly sessionId;
|
|
130
|
+
private readonly runtimeHomeSessionId;
|
|
141
131
|
private sessionSandbox?;
|
|
142
132
|
private sessionApprovalPolicy?;
|
|
143
133
|
private readonly runtimeHomes;
|
|
@@ -147,7 +137,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
147
137
|
* finalizes these synchronously afterwards to scrub raw interaction answers. */
|
|
148
138
|
private readonly pendingClaudeFinalizers;
|
|
149
139
|
private readonly liveEnsuring;
|
|
150
|
-
|
|
140
|
+
private readonly forkingTargets;
|
|
141
|
+
/** Short-lived dedupe for managed fork notifications delivered after the
|
|
142
|
+
* `thread/fork` response. Values are expected source Provider thread ids. */
|
|
143
|
+
private readonly managedForkThreadStarts;
|
|
144
|
+
constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, sessionId, runtimeHomeSessionId, }: {
|
|
151
145
|
config: AppConfig;
|
|
152
146
|
commandRunner?: CodexCommandRunner;
|
|
153
147
|
sessionStore?: CodexSessionStore;
|
|
@@ -164,6 +158,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
164
158
|
* session-scoped. The shared `__cap__` child / tests fall back to a sentinel
|
|
165
159
|
* (its home only backs home-agnostic caps like listModels). */
|
|
166
160
|
sessionId?: string;
|
|
161
|
+
/** Provider-internal home owner loaded from the durable native binding.
|
|
162
|
+
* Forked Sessions keep independent runners/contexts while sharing the
|
|
163
|
+
* Provider store in which thread/fork atomically persisted their rollout. */
|
|
164
|
+
runtimeHomeSessionId?: string;
|
|
167
165
|
});
|
|
168
166
|
private getBackend;
|
|
169
167
|
/**
|
|
@@ -203,19 +201,18 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
203
201
|
* DETERMINISTIC (`resp_codex_<turnId>`) so items converge across snapshot/live.
|
|
204
202
|
*
|
|
205
203
|
* Returns false for claude / no app-server (the caller falls back to the
|
|
206
|
-
* streaming run path).
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* runner child, which owns the terminal registry) merely `resume`s this thread
|
|
211
|
-
* to display it. The thread id is persisted so `codexTerminalSpec` resumes it.
|
|
204
|
+
* streaming run path). For a fresh session the native remote TUI creates the
|
|
205
|
+
* Codex thread and its `thread/started` notification supplies the id. For an
|
|
206
|
+
* existing session, the app-server resumes the persisted id. Rynx never writes
|
|
207
|
+
* or repairs Codex's private rollout files.
|
|
212
208
|
*/
|
|
213
|
-
ensureLiveCodexSession(localThreadId: string, emit: (event: SessionEvent) => void, opts
|
|
209
|
+
ensureLiveCodexSession(localThreadId: string, emit: (event: SessionEvent) => void, opts: LiveSessionOpts): Promise<boolean>;
|
|
214
210
|
private startLiveCodexSession;
|
|
215
|
-
private refreshLiveCodexSession;
|
|
216
211
|
/** Bind a session's codex thread id once known (TUI broadcast or store): persist
|
|
217
212
|
* it, unblock injection, and kick off the resume-subscribe loop (once). */
|
|
218
213
|
private onLiveThreadStarted;
|
|
214
|
+
private shouldIgnoreManagedForkThreadStarted;
|
|
215
|
+
private rememberManagedForkThreadStart;
|
|
219
216
|
/**
|
|
220
217
|
* Subscribe the forwarder connection to a thread (reference implementation's
|
|
221
218
|
* `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
|
|
@@ -228,11 +225,17 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
228
225
|
/** Await a live session's thread binding (bounded). Returns false on timeout /
|
|
229
226
|
* no live session. Injection and the runner's `live.ready` gate on this. */
|
|
230
227
|
waitLiveReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
|
|
228
|
+
/** Await the stronger Terminal gate: another app-server connection has
|
|
229
|
+
* successfully resumed the thread, so the detached TUI cannot race rollout
|
|
230
|
+
* discovery or indexing. */
|
|
231
|
+
waitTerminalReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
|
|
232
|
+
/** Diagnostic from the provider adapter when native discovery/resume failed. */
|
|
233
|
+
liveSessionError(localThreadId: string): string | undefined;
|
|
231
234
|
/**
|
|
232
235
|
* Inject a user turn into a session's live codex thread — reference implementation's
|
|
233
236
|
* single-writer web send. `turn/steer` when a turn is open (mid-turn
|
|
234
|
-
* supplement), else `turn/start`.
|
|
235
|
-
*
|
|
237
|
+
* supplement), else `turn/start`. The thread was created by the native TUI or
|
|
238
|
+
* resumed from the persisted native id. Serialized per session so two
|
|
236
239
|
* injects can't double-open a turn.
|
|
237
240
|
*
|
|
238
241
|
* Returns an {@link InjectOutcome}: `notLive` when this session has no live
|
|
@@ -264,21 +267,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
264
267
|
* prompt. Questions and PermissionRequest approvals use the runtime-local
|
|
265
268
|
* generic interaction bridge and remain inside the active Turn. */
|
|
266
269
|
private claudeTerminalSpec;
|
|
267
|
-
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
|
|
271
|
-
* for a LIVE native session, reusing the same core resolvers the non-live
|
|
272
|
-
* run path uses ({@link resolveAgentExecution} for the model,
|
|
273
|
-
* {@link resolveAgent} for instructions and skills).
|
|
274
|
-
*
|
|
275
|
-
* Skills are spec-rooted: each declared ref is replayed into a
|
|
276
|
-
* SESSION-scoped temp dir (cache-accelerated); the owner's catalog plays no
|
|
277
|
-
* role, and no spec / no `skills` means ZERO skills. Every declared skill
|
|
278
|
-
* must resolve at its declared content hash; missing, failed, or drifted
|
|
279
|
-
* materialization aborts launch before a native session starts.
|
|
280
|
-
* `skillsCleanup` removes the session dir (call on session stop). */
|
|
281
|
-
private resolveLiveAgentConfig;
|
|
270
|
+
/** Materialize only the immutable skill declarations stored on the Session.
|
|
271
|
+
* Provider startup never re-opens an Agent template or process-level plugin
|
|
272
|
+
* snapshot. */
|
|
273
|
+
private prepareExecutionSkills;
|
|
282
274
|
private startLiveClaudeSession;
|
|
283
275
|
/** Persist claude's discovered session id (reusing the `codexSessionId` store
|
|
284
276
|
* field, as the claude executor already does) and release the readiness gate. */
|
|
@@ -309,7 +301,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
309
301
|
getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
|
|
310
302
|
setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
|
|
311
303
|
clearGoal(localThreadId: string): Promise<CapabilityResult>;
|
|
312
|
-
forkSession(currentLocalThreadId: string, newLocalThreadId: string
|
|
304
|
+
forkSession(currentLocalThreadId: string, newLocalThreadId: string, options?: {
|
|
305
|
+
workspace: SessionWorkspaceSnapshot;
|
|
306
|
+
execution: ResolvedExecutionSnapshot;
|
|
307
|
+
}): Promise<CapabilityResult>;
|
|
308
|
+
private performForkSession;
|
|
313
309
|
}
|
|
314
310
|
export declare function parseCodexLoginStatus(exitCode: number, output: string): {
|
|
315
311
|
loggedIn: boolean;
|
|
@@ -322,3 +318,7 @@ export declare function isUnsupportedMethodError(error: unknown): boolean;
|
|
|
322
318
|
* — a fresh TUI thread before its first turn. Retryable (park until active).
|
|
323
319
|
* Mirrors reference implementation's `_is_thread_not_ready_error`. */
|
|
324
320
|
export declare function isThreadNotReadyError(error: unknown): boolean;
|
|
321
|
+
/** A persisted thread id that a freshly started app-server cannot load yet.
|
|
322
|
+
* During startup, both errors can be transient while the rollout index catches
|
|
323
|
+
* up. Retry the same id; never use either error as permission to replace it. */
|
|
324
|
+
export declare function isRetryableThreadResumeError(error: unknown): boolean;
|