@rynx-ai/runtime 0.1.11-beta.1 → 0.1.11-beta.3
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 +799 -351
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- 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 +92 -19
- package/dist/runner/manager.js +838 -83
- 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/dist/terminal/tmux.d.ts +8 -0
- package/dist/terminal/tmux.js +36 -3
- package/package.json +2 -2
- package/dist/codex/rollout-synth.d.ts +0 -42
- package/dist/codex/rollout-synth.js +0 -245
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
16
|
import { join } from "node:path";
|
|
17
|
+
import { adoptLegacyRuntimeDirectory, legacyRuntimeStateRoot, runtimeSessionDigest, runtimeSessionStateDir, } from "../runtime-state-paths.js";
|
|
18
18
|
export const HOOKS_FILE = "hooks.jsonl";
|
|
19
19
|
export const STATE_FILE = "state.json";
|
|
20
20
|
export const DELTAS_FILE = "message_deltas.jsonl";
|
|
@@ -26,15 +26,9 @@ export const INTERACTION_RESULTS_DIR = "interaction-results";
|
|
|
26
26
|
export const INTERACTION_CLAIMS_DIR = "interaction-claims";
|
|
27
27
|
export const INTERACTION_LEASES_DIR = "interaction-leases";
|
|
28
28
|
export const MANAGED_SETTINGS_FILE = "managed-settings.json";
|
|
29
|
-
/** Uid-scoped root so other users on a shared host cannot read interaction data. */
|
|
30
|
-
function bridgeRoot() {
|
|
31
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : "nouid";
|
|
32
|
-
return join(tmpdir(), `rynx-${uid}`, "claude-native");
|
|
33
|
-
}
|
|
34
29
|
/** The deterministic bridge directory for a rynx session id. */
|
|
35
30
|
export function claudeBridgeDir(sessionId) {
|
|
36
|
-
|
|
37
|
-
return join(bridgeRoot(), digest);
|
|
31
|
+
return join(runtimeSessionStateDir(sessionId), "claude-bridge");
|
|
38
32
|
}
|
|
39
33
|
/**
|
|
40
34
|
* Create/refresh the bridge dir and clear stale files. Native interactions use
|
|
@@ -43,6 +37,7 @@ export function claudeBridgeDir(sessionId) {
|
|
|
43
37
|
*/
|
|
44
38
|
export function prepareClaudeBridgeDir(sessionId) {
|
|
45
39
|
const dir = claudeBridgeDir(sessionId);
|
|
40
|
+
adoptLegacyRuntimeDirectory(join(legacyRuntimeStateRoot(), "claude-native", runtimeSessionDigest(sessionId)), dir);
|
|
46
41
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
47
42
|
for (const file of [
|
|
48
43
|
HOOKS_FILE,
|
|
@@ -32,6 +32,9 @@ export interface ClaudeForwarderSink {
|
|
|
32
32
|
/** Fired once SessionStart reveals claude's session id + transcript path, so
|
|
33
33
|
* the host can persist the id and release its readiness gate. */
|
|
34
34
|
onSessionDiscovered?(claudeSessionId: string, transcriptPath: string): void;
|
|
35
|
+
/** A resumed TUI announced a different native session id than the persisted
|
|
36
|
+
* binding. The host keeps the original binding and fails readiness. */
|
|
37
|
+
onSessionResumeError?(error: Error): void;
|
|
35
38
|
/** The claude session rotated (`/clear` → fresh, `/fork` → derived): a new
|
|
36
39
|
* SessionStart reported a new session id + transcript. The forwarder has
|
|
37
40
|
* already re-pointed to the new transcript and reset its per-session state; the
|
|
@@ -45,8 +48,13 @@ export interface ClaudeLiveSessionOptions {
|
|
|
45
48
|
/** Resume: a known transcript path to tail immediately (else await SessionStart). */
|
|
46
49
|
transcriptPath?: string;
|
|
47
50
|
/** Resume: the claude session id for that transcript — used to match the
|
|
48
|
-
* persisted
|
|
51
|
+
* persisted binding. It may be supplied without `transcriptPath`; Claude's
|
|
52
|
+
* SessionStart hook remains the source of truth for the actual file path. */
|
|
49
53
|
claudeSessionId?: string;
|
|
54
|
+
/** Rynx already owns the persisted history for a resumed Session. When no
|
|
55
|
+
* matching forwarder cursor exists, follow only records appended after the
|
|
56
|
+
* SessionStart discovery instead of mirroring native history a second time. */
|
|
57
|
+
resumeAtEndOnDiscovery?: boolean;
|
|
50
58
|
/** Poll interval (ms). The files are append-only, so polling is simplest. */
|
|
51
59
|
pollMs?: number;
|
|
52
60
|
/** Inactivity (ms) FALLBACK close for a turn whose Stop hook never fired. */
|
|
@@ -83,6 +91,9 @@ export declare class ClaudeLiveSession {
|
|
|
83
91
|
private discovered;
|
|
84
92
|
/** claude's current session uuid (changes on `/clear`·`/fork`·resume). */
|
|
85
93
|
private currentClaudeSessionId?;
|
|
94
|
+
/** Persisted native id requested through `claude --resume`. */
|
|
95
|
+
private readonly expectedClaudeSessionId?;
|
|
96
|
+
private readonly resumeAtEndOnDiscovery;
|
|
86
97
|
/** Every claude session uuid seen — a resume into an UNSEEN id + a forkedFrom
|
|
87
98
|
* marker signals a `/fork` (vs. resuming a known branch). */
|
|
88
99
|
private readonly seenClaudeSessionIds;
|
|
@@ -155,6 +155,9 @@ export class ClaudeLiveSession {
|
|
|
155
155
|
discovered = false;
|
|
156
156
|
/** claude's current session uuid (changes on `/clear`·`/fork`·resume). */
|
|
157
157
|
currentClaudeSessionId;
|
|
158
|
+
/** Persisted native id requested through `claude --resume`. */
|
|
159
|
+
expectedClaudeSessionId;
|
|
160
|
+
resumeAtEndOnDiscovery;
|
|
158
161
|
/** Every claude session uuid seen — a resume into an UNSEEN id + a forkedFrom
|
|
159
162
|
* marker signals a `/fork` (vs. resuming a known branch). */
|
|
160
163
|
seenClaudeSessionIds = new Set();
|
|
@@ -223,6 +226,8 @@ export class ClaudeLiveSession {
|
|
|
223
226
|
this.isProcessAlive = opts.isProcessAlive ?? processIsAlive;
|
|
224
227
|
this.leaseUpdatedAt = opts.interactionLeaseUpdatedAt ??
|
|
225
228
|
((interactionId) => interactionLeaseUpdatedAt(this.bridgeDir, interactionId));
|
|
229
|
+
this.expectedClaudeSessionId = opts.claudeSessionId;
|
|
230
|
+
this.resumeAtEndOnDiscovery = opts.resumeAtEndOnDiscovery ?? false;
|
|
226
231
|
this.transcriptPath = opts.transcriptPath;
|
|
227
232
|
// Resume path: bind the session id + restore the persisted forwarder cursor so
|
|
228
233
|
// we continue from where a prior forwarder left off (no re-mirror on relaunch).
|
|
@@ -245,7 +250,7 @@ export class ClaudeLiveSession {
|
|
|
245
250
|
restoreForwardState(transcriptPath) {
|
|
246
251
|
const state = readForwardState(this.bridgeDir);
|
|
247
252
|
if (!state || state.transcriptPath !== transcriptPath)
|
|
248
|
-
return;
|
|
253
|
+
return false;
|
|
249
254
|
for (const id of state.seenSourceIds)
|
|
250
255
|
this.seenSourceIds.add(id);
|
|
251
256
|
const fingerprint = jsonlCursorFingerprint(transcriptPath, state.byteOffset);
|
|
@@ -253,6 +258,7 @@ export class ClaudeLiveSession {
|
|
|
253
258
|
fingerprint !== undefined && fingerprint === state.cursorFingerprint
|
|
254
259
|
? state.byteOffset // cursor valid → resume here
|
|
255
260
|
: fileSize(transcriptPath); // truncated/replaced → skip to EOF
|
|
261
|
+
return true;
|
|
256
262
|
}
|
|
257
263
|
/** Persist the durable forwarder cursor (byte offset + seen ids + fingerprint). */
|
|
258
264
|
persistForwardState() {
|
|
@@ -437,6 +443,11 @@ export class ClaudeLiveSession {
|
|
|
437
443
|
return;
|
|
438
444
|
// First discovery: bind the transcript + reveal the session id.
|
|
439
445
|
if (!this.transcriptPath) {
|
|
446
|
+
if (this.expectedClaudeSessionId &&
|
|
447
|
+
ev.sessionId !== this.expectedClaudeSessionId) {
|
|
448
|
+
this.sink.onSessionResumeError?.(new Error(`Claude resumed ${ev.sessionId ?? "an unknown session"} instead of ${this.expectedClaudeSessionId}`));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
440
451
|
this.transcriptPath = ev.transcriptPath;
|
|
441
452
|
this.transcriptOffset = 0;
|
|
442
453
|
this.currentClaudeSessionId = ev.sessionId;
|
|
@@ -444,7 +455,10 @@ export class ClaudeLiveSession {
|
|
|
444
455
|
this.seenClaudeSessionIds.add(ev.sessionId);
|
|
445
456
|
// Resume from the persisted cursor so a relaunched forwarder doesn't re-mirror
|
|
446
457
|
// already-logged turns.
|
|
447
|
-
this.restoreForwardState(ev.transcriptPath);
|
|
458
|
+
const restored = this.restoreForwardState(ev.transcriptPath);
|
|
459
|
+
if (!restored && this.resumeAtEndOnDiscovery) {
|
|
460
|
+
this.transcriptOffset = fileSize(ev.transcriptPath);
|
|
461
|
+
}
|
|
448
462
|
if (!this.discovered && ev.sessionId) {
|
|
449
463
|
this.discovered = true;
|
|
450
464
|
this.sink.onSessionDiscovered?.(ev.sessionId, ev.transcriptPath);
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { AgentEvent, TerminalCommandData } from "@rynx-ai/core";
|
|
2
|
-
/** Claude Code encodes a cwd into a project-dir name by replacing `/` and `.`
|
|
3
|
-
* with `-` (e.g. `/Users/x/Workspace/rynx` → `-Users-x-Workspace-rynx`). */
|
|
4
|
-
export declare function encodeClaudeProjectDir(cwd: string): string;
|
|
5
|
-
/** The directory Claude Code writes this cwd's session transcripts into. */
|
|
6
|
-
export declare function claudeProjectDir(cwd: string, home?: string): string;
|
|
7
|
-
/** The transcript file for a specific claude session id. */
|
|
8
|
-
export declare function claudeTranscriptPath(cwd: string, sessionId: string, home?: string): string;
|
|
9
2
|
/** A sub-agent (Task) writes its own transcript to
|
|
10
3
|
* `<project>/<sessionId>/subagents/agent-<agentId>.jsonl`, alongside the parent
|
|
11
4
|
* `<sessionId>.jsonl`. Derive that path from the parent transcript path. */
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code transcript reader (Phase E, claude-native). Claude Code appends a
|
|
3
|
-
* JSONL transcript per session
|
|
4
|
-
*
|
|
5
|
-
* stdout is a full-screen UI, so
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* JSONL transcript per session. Its `SessionStart` hook supplies the actual
|
|
4
|
+
* transcript path; Rynx never derives that path from Claude's private directory
|
|
5
|
+
* layout. The interactive TUI's stdout is a full-screen UI, so we read this
|
|
6
|
+
* hook-provided file for the UI mirror only. Each `user`/`assistant` record wraps
|
|
7
|
+
* an Anthropic message (`{ role, content: [blocks] }`), which we map to the same
|
|
8
|
+
* typed {@link AgentEvent}s the headless executor emits.
|
|
9
9
|
*
|
|
10
10
|
* Records are full messages (not deltas), so text → a completed message,
|
|
11
11
|
* thinking → completed reasoning, tool_use → tool start, tool_result → tool end.
|
|
@@ -14,21 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
16
|
import { open, stat } from "node:fs/promises";
|
|
17
|
-
import { homedir } from "node:os";
|
|
18
17
|
import { join } from "node:path";
|
|
19
|
-
/** Claude Code encodes a cwd into a project-dir name by replacing `/` and `.`
|
|
20
|
-
* with `-` (e.g. `/Users/x/Workspace/rynx` → `-Users-x-Workspace-rynx`). */
|
|
21
|
-
export function encodeClaudeProjectDir(cwd) {
|
|
22
|
-
return cwd.replace(/[/.]/g, "-");
|
|
23
|
-
}
|
|
24
|
-
/** The directory Claude Code writes this cwd's session transcripts into. */
|
|
25
|
-
export function claudeProjectDir(cwd, home = homedir()) {
|
|
26
|
-
return join(home, ".claude", "projects", encodeClaudeProjectDir(cwd));
|
|
27
|
-
}
|
|
28
|
-
/** The transcript file for a specific claude session id. */
|
|
29
|
-
export function claudeTranscriptPath(cwd, sessionId, home = homedir()) {
|
|
30
|
-
return join(claudeProjectDir(cwd, home), `${sessionId}.jsonl`);
|
|
31
|
-
}
|
|
32
18
|
/** A sub-agent (Task) writes its own transcript to
|
|
33
19
|
* `<project>/<sessionId>/subagents/agent-<agentId>.jsonl`, alongside the parent
|
|
34
20
|
* `<sessionId>.jsonl`. Derive that path from the parent transcript path. */
|
|
@@ -67,7 +67,8 @@ export declare class CodexAppServerClient {
|
|
|
67
67
|
threadFork(params: ThreadForkParams): Promise<{
|
|
68
68
|
thread: {
|
|
69
69
|
id: string;
|
|
70
|
-
|
|
70
|
+
forkedFromId?: string | null;
|
|
71
|
+
} & Record<string, unknown>;
|
|
71
72
|
} & Record<string, unknown>>;
|
|
72
73
|
/** Update per-thread settings (model / mode / personality / sandbox …). */
|
|
73
74
|
threadSettingsUpdate(params: ThreadSettingsUpdateParams): Promise<Record<string, unknown>>;
|
|
@@ -37,9 +37,12 @@ export interface CodexForwarderSink {
|
|
|
37
37
|
/** The user's turn text (sourced from codex's `userMessage` item), so a
|
|
38
38
|
* co-driving TUI's prompt is recorded even though this process never injected it. */
|
|
39
39
|
onUserMessage?(content: string | UserContentPart[]): void;
|
|
40
|
+
/** A managed Core fork also broadcasts `thread/started`, but does not switch
|
|
41
|
+
* the source TUI. Discard that notification before changing the bound thread. */
|
|
42
|
+
shouldIgnoreThreadStarted?(threadId: string, forkedFromId?: string): boolean;
|
|
40
43
|
/** A thread was announced on the app-server (a `--remote` TUI creating a thread,
|
|
41
44
|
* or a resume). The host persists the id + starts the subscribe loop. */
|
|
42
|
-
onThreadStarted?(threadId: string): void;
|
|
45
|
+
onThreadStarted?(threadId: string, forkedFromId?: string): void;
|
|
43
46
|
/** The thread showed activity (a turn/item began, so its rollout now exists).
|
|
44
47
|
* Fired once; lets a parked `thread/resume` retry (reference implementation's ready signal). */
|
|
45
48
|
onThreadActive?(): void;
|
|
@@ -115,11 +115,29 @@ export class CodexSessionForwarder {
|
|
|
115
115
|
if (method === "thread/started" || method === "thread.started") {
|
|
116
116
|
const tid = threadIdFrom(params);
|
|
117
117
|
if (tid && tid !== this.currentThreadIdValue) {
|
|
118
|
+
const forkedFromId = params?.thread?.forkedFromId;
|
|
119
|
+
const normalizedForkedFromId = typeof forkedFromId === "string" ? forkedFromId : undefined;
|
|
120
|
+
if (this.sink.shouldIgnoreThreadStarted?.(tid, normalizedForkedFromId)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.flushPendingCompletion();
|
|
124
|
+
this.flushDeferredAssistantMessage();
|
|
125
|
+
if (this.turnOpen) {
|
|
126
|
+
this.turnOpen = false;
|
|
127
|
+
this.sink.onTurnEnd();
|
|
128
|
+
}
|
|
129
|
+
this.currentTurnIdValue = null;
|
|
130
|
+
this.activeSignaled = false;
|
|
118
131
|
this.currentThreadIdValue = tid;
|
|
119
|
-
this.sink.onThreadStarted?.(tid);
|
|
132
|
+
this.sink.onThreadStarted?.(tid, normalizedForkedFromId);
|
|
120
133
|
}
|
|
121
134
|
return;
|
|
122
135
|
}
|
|
136
|
+
const notificationThreadId = threadIdFrom(params);
|
|
137
|
+
if (this.currentThreadIdValue &&
|
|
138
|
+
notificationThreadId &&
|
|
139
|
+
notificationThreadId !== this.currentThreadIdValue)
|
|
140
|
+
return;
|
|
123
141
|
// Release a parked resume as soon as the thread shows activity (rollout now
|
|
124
142
|
// exists). Fire once.
|
|
125
143
|
if (!this.activeSignaled && indicatesActive(method, params)) {
|
|
@@ -58,14 +58,46 @@ export interface GetAuthStatusResponse {
|
|
|
58
58
|
}
|
|
59
59
|
export type AskForApproval = "untrusted" | "on-failure" | "on-request" | "never";
|
|
60
60
|
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
|
61
|
+
export type SandboxPolicy = {
|
|
62
|
+
type: "readOnly";
|
|
63
|
+
networkAccess: boolean;
|
|
64
|
+
} | {
|
|
65
|
+
type: "externalSandbox";
|
|
66
|
+
networkAccess: "restricted" | "enabled";
|
|
67
|
+
} | {
|
|
68
|
+
type: "workspaceWrite";
|
|
69
|
+
writableRoots: string[];
|
|
70
|
+
networkAccess: boolean;
|
|
71
|
+
excludeTmpdirEnvVar: boolean;
|
|
72
|
+
excludeSlashTmp: boolean;
|
|
73
|
+
} | {
|
|
74
|
+
type: "dangerFullAccess";
|
|
75
|
+
};
|
|
76
|
+
/** Traex 0.200 named permission profile selection. Codex currently accepts a
|
|
77
|
+
* string profile id instead, so callers must choose the runtime-specific shape. */
|
|
78
|
+
export interface PermissionProfileSelectionParams {
|
|
79
|
+
type: "profile";
|
|
80
|
+
id: string;
|
|
81
|
+
modifications?: PermissionProfileModificationParams[] | null;
|
|
82
|
+
}
|
|
83
|
+
export interface PermissionProfileModificationParams {
|
|
84
|
+
type: "additionalWritableRoot";
|
|
85
|
+
path: string;
|
|
86
|
+
}
|
|
87
|
+
export type PermissionSelection = string | PermissionProfileSelectionParams;
|
|
61
88
|
/** Open since Codex App Server 0.144; values are advertised by `model/list`. */
|
|
62
89
|
export type ReasoningEffort = string;
|
|
63
90
|
export interface ThreadStartParams {
|
|
64
91
|
model?: string | null;
|
|
65
92
|
modelProvider?: string | null;
|
|
66
93
|
cwd?: string | null;
|
|
94
|
+
/** Codex-only runtime workspace roots. Traex uses a permission selection with
|
|
95
|
+
* `additionalWritableRoot` modifications instead. */
|
|
96
|
+
runtimeWorkspaceRoots?: string[] | null;
|
|
67
97
|
approvalPolicy?: AskForApproval | null;
|
|
68
98
|
sandbox?: SandboxMode | null;
|
|
99
|
+
/** Named permissions profile. Cannot be combined with `sandbox`. */
|
|
100
|
+
permissions?: PermissionSelection | null;
|
|
69
101
|
config?: Record<string, unknown> | null;
|
|
70
102
|
baseInstructions?: string | null;
|
|
71
103
|
developerInstructions?: string | null;
|
|
@@ -115,7 +147,12 @@ export interface TurnStartParams {
|
|
|
115
147
|
threadId: string;
|
|
116
148
|
input: UserInput[];
|
|
117
149
|
cwd?: string | null;
|
|
150
|
+
/** Codex-only sticky runtime workspace roots. */
|
|
151
|
+
runtimeWorkspaceRoots?: string[] | null;
|
|
118
152
|
approvalPolicy?: AskForApproval | null;
|
|
153
|
+
sandboxPolicy?: SandboxPolicy | null;
|
|
154
|
+
/** Named permissions profile. Cannot be combined with `sandboxPolicy`. */
|
|
155
|
+
permissions?: PermissionSelection | null;
|
|
119
156
|
model?: string | null;
|
|
120
157
|
effort?: ReasoningEffort | null;
|
|
121
158
|
}
|
|
@@ -240,18 +277,25 @@ export interface ThreadListResponse {
|
|
|
240
277
|
}
|
|
241
278
|
export interface ThreadForkParams {
|
|
242
279
|
threadId: string;
|
|
280
|
+
/** Omit the copied turn backlog from the RPC response. The Provider still
|
|
281
|
+
* persists and inherits the complete thread context. */
|
|
282
|
+
excludeTurns?: boolean;
|
|
243
283
|
path?: string | null;
|
|
244
284
|
model?: string | null;
|
|
245
285
|
modelProvider?: string | null;
|
|
246
286
|
cwd?: string | null;
|
|
287
|
+
/** Codex-only runtime workspace roots. */
|
|
288
|
+
runtimeWorkspaceRoots?: string[] | null;
|
|
247
289
|
approvalPolicy?: AskForApproval | null;
|
|
248
290
|
sandbox?: SandboxMode | null;
|
|
291
|
+
/** Named permissions profile. Cannot be combined with `sandbox`. */
|
|
292
|
+
permissions?: PermissionSelection | null;
|
|
249
293
|
}
|
|
250
294
|
export interface ThreadSettingsUpdateParams {
|
|
251
295
|
threadId: string;
|
|
252
296
|
cwd?: string | null;
|
|
253
297
|
approvalPolicy?: AskForApproval | null;
|
|
254
|
-
sandboxPolicy?:
|
|
298
|
+
sandboxPolicy?: SandboxPolicy | null;
|
|
255
299
|
/** Named permissions profile id; cannot be combined with `sandboxPolicy`. */
|
|
256
300
|
permissions?: string | null;
|
|
257
301
|
model?: string | null;
|
package/dist/codex-home.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type AgentRuntimeId } from "@rynx-ai/core";
|
|
2
2
|
export type CodexLineageRuntime = Exclude<AgentRuntimeId, "claude">;
|
|
3
3
|
/**
|
|
4
|
-
* The deterministic private CODEX_HOME path for a rynx session. PER-SESSION
|
|
5
|
-
*
|
|
4
|
+
* The deterministic private CODEX_HOME path for a rynx session. PER-SESSION,
|
|
5
|
+
* under the daemon-owned RYNX_HOME runtime tree,
|
|
6
6
|
* `bridge_dir/codex-home` and rynx's own `claudeBridgeDir`. Pure — computes the
|
|
7
|
-
* path without touching disk, so the
|
|
8
|
-
*
|
|
7
|
+
* path without touching disk, so the runner child and its app-server locate the
|
|
8
|
+
* SAME Rynx-owned runtime home from `sessionId` alone.
|
|
9
9
|
*/
|
|
10
10
|
export declare function codexHomePath(sessionId: string): string;
|
|
11
11
|
/** Deterministic private home for a Codex-lineage runtime. */
|
|
@@ -16,9 +16,9 @@ export declare function legacyCodexHomePath(): string;
|
|
|
16
16
|
/**
|
|
17
17
|
* Prepare a private CODEX_HOME that inherits the user's login (symlinked
|
|
18
18
|
* `auth.json`) and settings (copied `config.toml`) but NOT the real home's pending
|
|
19
|
-
* update / first-run (NUX) state
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* update / first-run (NUX) state. Managed TUI launches separately disable
|
|
20
|
+
* startup update checks, while keeping this mutable state isolated from the
|
|
21
|
+
* user's real home. Ports reference implementation's `_CODEX_HOME_SYMLINK_FILES` /
|
|
22
22
|
* `_CODEX_HOME_COPY_FILES`.
|
|
23
23
|
*
|
|
24
24
|
* PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
|
|
@@ -32,25 +32,8 @@ export declare function prepareCodexHome(sessionId: string, realHome?: string):
|
|
|
32
32
|
* real home and cannot wedge a managed app-server/TUI pair.
|
|
33
33
|
*/
|
|
34
34
|
export declare function prepareRuntimeHome(sessionId: string, runtime: CodexLineageRuntime, realHome?: string): string;
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
* discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism reference implementation
|
|
38
|
-
* uses (`populate_codex_skills_from_bundle` → `_populate_codex_skills`), NOT a
|
|
39
|
-
* `<skills_instructions>` block injected into the prompt. Codex's app-server
|
|
40
|
-
* watches this dir and re-scans on change, so a live `codex --remote` TUI — whose
|
|
41
|
-
* thread rynx never creates, so it can't carry `threadStart.developerInstructions`
|
|
42
|
-
* — still sees the agent's skills.
|
|
43
|
-
*
|
|
44
|
-
* Each skill is a symlink to its source dir; a filesystem without symlink support
|
|
45
|
-
* falls back to a recursive copy (matches reference implementation's fallback).
|
|
46
|
-
*
|
|
47
|
-
* reference implementation boots a fresh per-session CODEX_HOME, so it only ever links into an
|
|
48
|
-
* empty dir. rynx shares ONE private home per runtime (see {@link prepareCodexHome}),
|
|
49
|
-
* so this CONVERGES the dir to `skills`: it links the missing ones and removes
|
|
50
|
-
* entries no longer selected, honouring the agent spec's gating. (Concurrent
|
|
51
|
-
* sessions of DIFFERENT agents on one runtime share this dir — the same shared-home
|
|
52
|
-
* tradeoff already accepted for auth/config.)
|
|
53
|
-
*/
|
|
35
|
+
/** Persist immutable skill copies in a native Provider home. Fork descendants
|
|
36
|
+
* share that home, so entries must outlive every runner's temporary sources. */
|
|
54
37
|
export declare function populateCodexSkills(codexHome: string, skills: {
|
|
55
38
|
name: string;
|
|
56
39
|
dir: string;
|
package/dist/codex-home.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, renameSync, rmSync, symlinkSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { getRuntimeProfile, resolveRuntimeHome, } from "@rynx-ai/core";
|
|
5
|
+
import { getRuntimeProfile, resolveRuntimeHome, SAFE_SKILL_NAME, } from "@rynx-ai/core";
|
|
6
|
+
import { adoptLegacyRuntimeDirectory, legacyRuntimeStateRoot, runtimeSessionDigest, runtimeSessionStateDir, } from "./runtime-state-paths.js";
|
|
6
7
|
/** Inherit the user's LIVE login by symlink (stays in sync). */
|
|
7
8
|
const SYMLINK_FILES = ["auth.json"];
|
|
8
9
|
/** Inherit the user's settings by snapshot copy (not the mutable NUX/update state). */
|
|
@@ -21,40 +22,33 @@ const RUNTIME_HOME_FILES = {
|
|
|
21
22
|
function realCodexHome() {
|
|
22
23
|
return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
|
|
23
24
|
}
|
|
24
|
-
/** Uid-scoped root so other users on a shared host can't read the tree. */
|
|
25
|
-
function rynxUidRoot() {
|
|
26
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : "nouid";
|
|
27
|
-
return join(tmpdir(), `rynx-${uid}`);
|
|
28
|
-
}
|
|
29
25
|
/**
|
|
30
|
-
* The deterministic private CODEX_HOME path for a rynx session. PER-SESSION
|
|
31
|
-
*
|
|
26
|
+
* The deterministic private CODEX_HOME path for a rynx session. PER-SESSION,
|
|
27
|
+
* under the daemon-owned RYNX_HOME runtime tree,
|
|
32
28
|
* `bridge_dir/codex-home` and rynx's own `claudeBridgeDir`. Pure — computes the
|
|
33
|
-
* path without touching disk, so the
|
|
34
|
-
*
|
|
29
|
+
* path without touching disk, so the runner child and its app-server locate the
|
|
30
|
+
* SAME Rynx-owned runtime home from `sessionId` alone.
|
|
35
31
|
*/
|
|
36
32
|
export function codexHomePath(sessionId) {
|
|
37
|
-
|
|
38
|
-
return join(rynxUidRoot(), "codex-native", digest, "codex-home");
|
|
33
|
+
return join(runtimeSessionStateDir(sessionId), "codex-home");
|
|
39
34
|
}
|
|
40
35
|
/** Deterministic private home for a Codex-lineage runtime. */
|
|
41
36
|
export function runtimeHomePath(sessionId, runtime) {
|
|
42
37
|
if (runtime === "codex")
|
|
43
38
|
return codexHomePath(sessionId);
|
|
44
|
-
|
|
45
|
-
return join(rynxUidRoot(), "traex-native", digest, "trae-home");
|
|
39
|
+
return join(runtimeSessionStateDir(sessionId), "trae-home");
|
|
46
40
|
}
|
|
47
41
|
/** The OLD uid-scoped shared home (pre per-session). Kept ONLY for back-compat
|
|
48
42
|
* resume fallback — a session's rollout may still live under here. */
|
|
49
43
|
export function legacyCodexHomePath() {
|
|
50
|
-
return join(
|
|
44
|
+
return join(legacyRuntimeStateRoot(), "codex-home");
|
|
51
45
|
}
|
|
52
46
|
/**
|
|
53
47
|
* Prepare a private CODEX_HOME that inherits the user's login (symlinked
|
|
54
48
|
* `auth.json`) and settings (copied `config.toml`) but NOT the real home's pending
|
|
55
|
-
* update / first-run (NUX) state
|
|
56
|
-
*
|
|
57
|
-
*
|
|
49
|
+
* update / first-run (NUX) state. Managed TUI launches separately disable
|
|
50
|
+
* startup update checks, while keeping this mutable state isolated from the
|
|
51
|
+
* user's real home. Ports reference implementation's `_CODEX_HOME_SYMLINK_FILES` /
|
|
58
52
|
* `_CODEX_HOME_COPY_FILES`.
|
|
59
53
|
*
|
|
60
54
|
* PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
|
|
@@ -73,6 +67,11 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
|
|
|
73
67
|
? realCodexHome()
|
|
74
68
|
: resolveRuntimeHome(getRuntimeProfile(runtime))) {
|
|
75
69
|
const dir = runtimeHomePath(sessionId, runtime);
|
|
70
|
+
const digest = runtimeSessionDigest(sessionId);
|
|
71
|
+
const legacy = runtime === "codex"
|
|
72
|
+
? join(legacyRuntimeStateRoot(), "codex-native", digest, "codex-home")
|
|
73
|
+
: join(legacyRuntimeStateRoot(), "traex-native", digest, "trae-home");
|
|
74
|
+
adoptLegacyRuntimeDirectory(legacy, dir);
|
|
76
75
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
77
76
|
const files = RUNTIME_HOME_FILES[runtime];
|
|
78
77
|
for (const name of files.symlink) {
|
|
@@ -109,58 +108,31 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
|
|
|
109
108
|
}
|
|
110
109
|
return dir;
|
|
111
110
|
}
|
|
112
|
-
/**
|
|
113
|
-
*
|
|
114
|
-
* discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism reference implementation
|
|
115
|
-
* uses (`populate_codex_skills_from_bundle` → `_populate_codex_skills`), NOT a
|
|
116
|
-
* `<skills_instructions>` block injected into the prompt. Codex's app-server
|
|
117
|
-
* watches this dir and re-scans on change, so a live `codex --remote` TUI — whose
|
|
118
|
-
* thread rynx never creates, so it can't carry `threadStart.developerInstructions`
|
|
119
|
-
* — still sees the agent's skills.
|
|
120
|
-
*
|
|
121
|
-
* Each skill is a symlink to its source dir; a filesystem without symlink support
|
|
122
|
-
* falls back to a recursive copy (matches reference implementation's fallback).
|
|
123
|
-
*
|
|
124
|
-
* reference implementation boots a fresh per-session CODEX_HOME, so it only ever links into an
|
|
125
|
-
* empty dir. rynx shares ONE private home per runtime (see {@link prepareCodexHome}),
|
|
126
|
-
* so this CONVERGES the dir to `skills`: it links the missing ones and removes
|
|
127
|
-
* entries no longer selected, honouring the agent spec's gating. (Concurrent
|
|
128
|
-
* sessions of DIFFERENT agents on one runtime share this dir — the same shared-home
|
|
129
|
-
* tradeoff already accepted for auth/config.)
|
|
130
|
-
*/
|
|
111
|
+
/** Persist immutable skill copies in a native Provider home. Fork descendants
|
|
112
|
+
* share that home, so entries must outlive every runner's temporary sources. */
|
|
131
113
|
export function populateCodexSkills(codexHome, skills) {
|
|
132
114
|
const skillsDir = join(codexHome, "skills");
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// skills (the app-server installs them into `$CODEX_HOME/skills/.system`) — never
|
|
137
|
-
// rynx-managed, so leave it untouched; only converge the entries we linked.
|
|
138
|
-
if (existsSync(skillsDir)) {
|
|
139
|
-
for (const name of readdirSync(skillsDir)) {
|
|
140
|
-
if (name === ".system")
|
|
141
|
-
continue;
|
|
142
|
-
if (!want.has(name))
|
|
143
|
-
rmSync(join(skillsDir, name), { recursive: true, force: true });
|
|
115
|
+
for (const skill of skills) {
|
|
116
|
+
if (!SAFE_SKILL_NAME.test(skill.name)) {
|
|
117
|
+
throw new Error(`unsafe skill name: ${skill.name}`);
|
|
144
118
|
}
|
|
145
119
|
}
|
|
146
|
-
if (
|
|
120
|
+
if (skills.length === 0)
|
|
147
121
|
return;
|
|
148
122
|
mkdirSync(skillsDir, { recursive: true });
|
|
149
|
-
for (const
|
|
150
|
-
const
|
|
151
|
-
|
|
123
|
+
for (const { name, dir } of skills) {
|
|
124
|
+
const target = join(skillsDir, name);
|
|
125
|
+
if (existsSync(target))
|
|
126
|
+
continue;
|
|
127
|
+
const staged = join(skillsDir, `.rynx-${name}-${process.pid}-${randomUUID()}`);
|
|
152
128
|
try {
|
|
153
|
-
|
|
129
|
+
cpSync(dir, staged, { recursive: true, errorOnExist: true, force: false });
|
|
130
|
+
renameSync(staged, target);
|
|
154
131
|
}
|
|
155
|
-
catch {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
cpSync(src, link, { recursive: true });
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
// best-effort
|
|
163
|
-
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
rmSync(staged, { recursive: true, force: true });
|
|
134
|
+
if (!existsSync(target))
|
|
135
|
+
throw error;
|
|
164
136
|
}
|
|
165
137
|
}
|
|
166
138
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { type AgentRuntimeId, type ReasoningEffort } from "@rynx-ai/core";
|
|
2
1
|
import type { AppConfig } from "@rynx-ai/core";
|
|
3
2
|
export interface CodexSessionRecord {
|
|
4
3
|
localThreadId: string;
|
|
5
4
|
codexSessionId: string;
|
|
6
|
-
|
|
7
|
-
model: string;
|
|
8
|
-
reasoningEffort?: ReasoningEffort;
|
|
9
|
-
/** Runtime this thread is bound to. Legacy records backfill to `codex`. */
|
|
10
|
-
runtime: AgentRuntimeId;
|
|
11
|
-
/** Declarative agent spec name bound to this thread, if any. */
|
|
12
|
-
agent?: string;
|
|
13
|
-
/** The source rynx session this one was derived from (claude `/fork`). */
|
|
5
|
+
/** The source rynx Session this Provider-native fork was derived from. */
|
|
14
6
|
parentSessionId?: string;
|
|
7
|
+
/** Provider-internal durable home ownership for native forks. Machine Session
|
|
8
|
+
* metadata remains self-contained; this only tells a runner where the
|
|
9
|
+
* Provider persisted its rollout/state. */
|
|
10
|
+
runtimeHomeOwnerSessionId?: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ClaudeForkIntent {
|
|
14
|
+
targetSessionId: string;
|
|
15
|
+
sourceSessionId: string;
|
|
16
|
+
sourceClaudeSessionId: string;
|
|
17
|
+
targetClaudeSessionId: string;
|
|
15
18
|
updatedAt: string;
|
|
16
19
|
}
|
|
17
20
|
export interface CodexSessionStore {
|
|
@@ -25,6 +28,11 @@ export interface CodexSessionStore {
|
|
|
25
28
|
listAll(): Promise<CodexSessionRecord[]>;
|
|
26
29
|
/** Reverse lookup by Codex thread id (the rollout-file UUID). */
|
|
27
30
|
findByCodexSessionId(codexSessionId: string): Promise<CodexSessionRecord | null>;
|
|
31
|
+
/** A Claude fork is not a Provider binding until the target TUI has emitted a
|
|
32
|
+
* matching SessionStart. Keep its launch intent in a separate namespace. */
|
|
33
|
+
getClaudeForkIntent?(targetSessionId: string): Promise<ClaudeForkIntent | null>;
|
|
34
|
+
setClaudeForkIntent?(intent: ClaudeForkIntent): Promise<void>;
|
|
35
|
+
deleteClaudeForkIntent?(targetSessionId: string): Promise<void>;
|
|
28
36
|
}
|
|
29
37
|
export declare class FileCodexSessionStore implements CodexSessionStore {
|
|
30
38
|
readonly filePath: string;
|
|
@@ -35,9 +43,13 @@ export declare class FileCodexSessionStore implements CodexSessionStore {
|
|
|
35
43
|
delete(localThreadId: string): Promise<void>;
|
|
36
44
|
listAll(): Promise<CodexSessionRecord[]>;
|
|
37
45
|
findByCodexSessionId(codexSessionId: string): Promise<CodexSessionRecord | null>;
|
|
46
|
+
getClaudeForkIntent(targetSessionId: string): Promise<ClaudeForkIntent | null>;
|
|
47
|
+
setClaudeForkIntent(intent: ClaudeForkIntent): Promise<void>;
|
|
48
|
+
deleteClaudeForkIntent(targetSessionId: string): Promise<void>;
|
|
38
49
|
isWritable(): Promise<boolean>;
|
|
39
50
|
private readAll;
|
|
40
51
|
private writeAll;
|
|
41
52
|
private runExclusive;
|
|
53
|
+
private acquireFileLock;
|
|
42
54
|
}
|
|
43
|
-
export declare function resolveCodexSessionStorePath(config: AppConfig): string;
|
|
55
|
+
export declare function resolveCodexSessionStorePath(config: AppConfig, env?: NodeJS.ProcessEnv): string;
|