@rynx-ai/runtime 0.1.10 → 0.1.11-beta.10
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/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- 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 +48 -47
- package/dist/host.js +810 -355
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/models-catalog.d.ts +3 -1
- package/dist/models-catalog.js +125 -4
- package/dist/provider-workspace.d.ts +56 -0
- package/dist/provider-workspace.js +83 -0
- package/dist/runner/child.d.ts +59 -6
- package/dist/runner/child.js +138 -19
- package/dist/runner/manager.d.ts +104 -19
- package/dist/runner/manager.js +922 -89
- 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 +10 -1
- package/dist/terminal/claude-tui.js +9 -1
- package/dist/terminal/codex-tui.d.ts +5 -1
- package/dist/terminal/codex-tui.js +12 -3
- package/dist/terminal/tmux.d.ts +11 -1
- package/dist/terminal/tmux.js +61 -12
- package/package.json +3 -3
- 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
|
/**
|
|
@@ -193,6 +191,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
193
191
|
args: string[];
|
|
194
192
|
cwd: string;
|
|
195
193
|
env?: Record<string, string>;
|
|
194
|
+
skipTraexStartupPrompts?: boolean;
|
|
196
195
|
} | null>;
|
|
197
196
|
/**
|
|
198
197
|
* Bring up (idempotently) a session's persistent codex forwarder and emit its
|
|
@@ -203,19 +202,18 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
203
202
|
* DETERMINISTIC (`resp_codex_<turnId>`) so items converge across snapshot/live.
|
|
204
203
|
*
|
|
205
204
|
* 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.
|
|
205
|
+
* streaming run path). For a fresh session the native remote TUI creates the
|
|
206
|
+
* Codex thread and its `thread/started` notification supplies the id. For an
|
|
207
|
+
* existing session, the app-server resumes the persisted id. Rynx never writes
|
|
208
|
+
* or repairs Codex's private rollout files.
|
|
212
209
|
*/
|
|
213
|
-
ensureLiveCodexSession(localThreadId: string, emit: (event: SessionEvent) => void, opts
|
|
210
|
+
ensureLiveCodexSession(localThreadId: string, emit: (event: SessionEvent) => void, opts: LiveSessionOpts): Promise<boolean>;
|
|
214
211
|
private startLiveCodexSession;
|
|
215
|
-
private refreshLiveCodexSession;
|
|
216
212
|
/** Bind a session's codex thread id once known (TUI broadcast or store): persist
|
|
217
213
|
* it, unblock injection, and kick off the resume-subscribe loop (once). */
|
|
218
214
|
private onLiveThreadStarted;
|
|
215
|
+
private shouldIgnoreManagedForkThreadStarted;
|
|
216
|
+
private rememberManagedForkThreadStart;
|
|
219
217
|
/**
|
|
220
218
|
* Subscribe the forwarder connection to a thread (reference implementation's
|
|
221
219
|
* `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
|
|
@@ -228,11 +226,17 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
228
226
|
/** Await a live session's thread binding (bounded). Returns false on timeout /
|
|
229
227
|
* no live session. Injection and the runner's `live.ready` gate on this. */
|
|
230
228
|
waitLiveReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
|
|
229
|
+
/** Await the stronger Terminal gate: another app-server connection has
|
|
230
|
+
* successfully resumed the thread, so the detached TUI cannot race rollout
|
|
231
|
+
* discovery or indexing. */
|
|
232
|
+
waitTerminalReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
|
|
233
|
+
/** Diagnostic from the provider adapter when native discovery/resume failed. */
|
|
234
|
+
liveSessionError(localThreadId: string): string | undefined;
|
|
231
235
|
/**
|
|
232
236
|
* Inject a user turn into a session's live codex thread — reference implementation's
|
|
233
237
|
* single-writer web send. `turn/steer` when a turn is open (mid-turn
|
|
234
|
-
* supplement), else `turn/start`.
|
|
235
|
-
*
|
|
238
|
+
* supplement), else `turn/start`. The thread was created by the native TUI or
|
|
239
|
+
* resumed from the persisted native id. Serialized per session so two
|
|
236
240
|
* injects can't double-open a turn.
|
|
237
241
|
*
|
|
238
242
|
* Returns an {@link InjectOutcome}: `notLive` when this session has no live
|
|
@@ -264,21 +268,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
264
268
|
* prompt. Questions and PermissionRequest approvals use the runtime-local
|
|
265
269
|
* generic interaction bridge and remain inside the active Turn. */
|
|
266
270
|
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;
|
|
271
|
+
/** Materialize only the immutable skill declarations stored on the Session.
|
|
272
|
+
* Provider startup never re-opens an Agent template or process-level plugin
|
|
273
|
+
* snapshot. */
|
|
274
|
+
private prepareExecutionSkills;
|
|
282
275
|
private startLiveClaudeSession;
|
|
283
276
|
/** Persist claude's discovered session id (reusing the `codexSessionId` store
|
|
284
277
|
* field, as the claude executor already does) and release the readiness gate. */
|
|
@@ -309,7 +302,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
309
302
|
getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
|
|
310
303
|
setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
|
|
311
304
|
clearGoal(localThreadId: string): Promise<CapabilityResult>;
|
|
312
|
-
forkSession(currentLocalThreadId: string, newLocalThreadId: string
|
|
305
|
+
forkSession(currentLocalThreadId: string, newLocalThreadId: string, options?: {
|
|
306
|
+
workspace: SessionWorkspaceSnapshot;
|
|
307
|
+
execution: ResolvedExecutionSnapshot;
|
|
308
|
+
}): Promise<CapabilityResult>;
|
|
309
|
+
private performForkSession;
|
|
313
310
|
}
|
|
314
311
|
export declare function parseCodexLoginStatus(exitCode: number, output: string): {
|
|
315
312
|
loggedIn: boolean;
|
|
@@ -322,3 +319,7 @@ export declare function isUnsupportedMethodError(error: unknown): boolean;
|
|
|
322
319
|
* — a fresh TUI thread before its first turn. Retryable (park until active).
|
|
323
320
|
* Mirrors reference implementation's `_is_thread_not_ready_error`. */
|
|
324
321
|
export declare function isThreadNotReadyError(error: unknown): boolean;
|
|
322
|
+
/** A persisted thread id that a freshly started app-server cannot load yet.
|
|
323
|
+
* During startup, both errors can be transient while the rollout index catches
|
|
324
|
+
* up. Retry the same id; never use either error as permission to replace it. */
|
|
325
|
+
export declare function isRetryableThreadResumeError(error: unknown): boolean;
|