@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
package/dist/runner-main.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* active-session display, which tolerate atomic-rename reads). A parent-of-record
|
|
17
17
|
* design (ship-in / emit-out) is the remote-ready follow-up.
|
|
18
18
|
*/
|
|
19
|
-
import { loadConfig
|
|
19
|
+
import { loadConfig } from "@rynx-ai/core";
|
|
20
20
|
import { FileCodexSessionStore, LocalAgentHost, resolveCodexSessionStorePath, } from "./host.js";
|
|
21
21
|
import { RunnerSession } from "./runner/child.js";
|
|
22
22
|
import { StdioRunnerTransport } from "./runner/transport.js";
|
|
@@ -33,15 +33,20 @@ for (const stream of [process.stdout, process.stderr]) {
|
|
|
33
33
|
throw err;
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
function main() {
|
|
36
|
+
async function main() {
|
|
37
37
|
const config = loadConfig();
|
|
38
38
|
const sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(config));
|
|
39
|
+
const sessionId = process.env.RYNX_RUNNER_SESSION;
|
|
40
|
+
const binding = sessionId ? await sessionStore.get(sessionId) : null;
|
|
39
41
|
const executor = new LocalAgentHost({
|
|
40
42
|
config,
|
|
41
43
|
sessionStore,
|
|
42
44
|
// The manager spawns one child per session and passes its key here so the
|
|
43
45
|
// host's private CODEX_HOME is scoped to this session (see manager.spawnHandle).
|
|
44
|
-
...(
|
|
46
|
+
...(sessionId ? { sessionId } : {}),
|
|
47
|
+
...(binding?.runtimeHomeOwnerSessionId
|
|
48
|
+
? { runtimeHomeSessionId: binding.runtimeHomeOwnerSessionId }
|
|
49
|
+
: {}),
|
|
45
50
|
});
|
|
46
51
|
const transport = new StdioRunnerTransport(process.stdin, process.stdout);
|
|
47
52
|
let runner;
|
|
@@ -61,4 +66,7 @@ function main() {
|
|
|
61
66
|
// as the protocol shutdown before ending the process.
|
|
62
67
|
process.on("SIGTERM", () => runner?.shutdown());
|
|
63
68
|
}
|
|
64
|
-
main()
|
|
69
|
+
void main().catch((error) => {
|
|
70
|
+
console.error(error);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function runtimeSessionDigest(sessionId: string): string;
|
|
2
|
+
export declare function runtimeSessionStateDir(sessionId: string): string;
|
|
3
|
+
/** Pre-standardization uid-private temp root, read only for opaque directory adoption. */
|
|
4
|
+
export declare function legacyRuntimeStateRoot(): string;
|
|
5
|
+
/**
|
|
6
|
+
* Adopt a pre-standardization runtime directory without interpreting Provider
|
|
7
|
+
* files. Rename when possible; cross-device filesystems fall back to an opaque
|
|
8
|
+
* recursive copy followed by removal.
|
|
9
|
+
*/
|
|
10
|
+
export declare function adoptLegacyRuntimeDirectory(legacyDirectory: string, canonicalDirectory: string): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, renameSync, rmSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { rynxRuntimeDir } from "@rynx-ai/core";
|
|
6
|
+
export function runtimeSessionDigest(sessionId) {
|
|
7
|
+
return createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
8
|
+
}
|
|
9
|
+
export function runtimeSessionStateDir(sessionId) {
|
|
10
|
+
return join(rynxRuntimeDir(), "sessions", runtimeSessionDigest(sessionId));
|
|
11
|
+
}
|
|
12
|
+
/** Pre-standardization uid-private temp root, read only for opaque directory adoption. */
|
|
13
|
+
export function legacyRuntimeStateRoot() {
|
|
14
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : "nouid";
|
|
15
|
+
return join(tmpdir(), `rynx-${uid}`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Adopt a pre-standardization runtime directory without interpreting Provider
|
|
19
|
+
* files. Rename when possible; cross-device filesystems fall back to an opaque
|
|
20
|
+
* recursive copy followed by removal.
|
|
21
|
+
*/
|
|
22
|
+
export function adoptLegacyRuntimeDirectory(legacyDirectory, canonicalDirectory) {
|
|
23
|
+
if (existsSync(canonicalDirectory) || !existsSync(legacyDirectory))
|
|
24
|
+
return;
|
|
25
|
+
mkdirSync(dirname(canonicalDirectory), { recursive: true, mode: 0o700 });
|
|
26
|
+
try {
|
|
27
|
+
renameSync(legacyDirectory, canonicalDirectory);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
if (existsSync(canonicalDirectory) || !existsSync(legacyDirectory))
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const stagingDirectory = `${canonicalDirectory}.adopt-${process.pid}-${Date.now().toString(36)}`;
|
|
35
|
+
try {
|
|
36
|
+
cpSync(legacyDirectory, stagingDirectory, {
|
|
37
|
+
recursive: true,
|
|
38
|
+
errorOnExist: true,
|
|
39
|
+
force: false,
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
renameSync(stagingDirectory, canonicalDirectory);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (!existsSync(canonicalDirectory))
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
rmSync(legacyDirectory, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
rmSync(stagingDirectory, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -22,6 +22,13 @@ export interface ClaudeTuiArgs {
|
|
|
22
22
|
appendSystemPrompt?: string;
|
|
23
23
|
/** Resume a specific prior claude session (`--resume <id>`). */
|
|
24
24
|
resume?: string;
|
|
25
|
+
/** Fork the resumed conversation into a new native Session. */
|
|
26
|
+
forkSession?: boolean;
|
|
27
|
+
/** Deterministic native UUID for a fork target. */
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
/** Session workspace roots beyond the primary process cwd. Claude accepts
|
|
30
|
+
* these as one variadic `--add-dir` flag on fresh and resumed launches. */
|
|
31
|
+
additionalDirs?: string[];
|
|
25
32
|
/** Extra claude args placed before the injected flags (rarely needed). */
|
|
26
33
|
extraArgs?: string[];
|
|
27
34
|
}
|
|
@@ -29,4 +36,4 @@ export interface ClaudeTuiArgs {
|
|
|
29
36
|
* Build the `claude` argv for an interactive, co-drivable TUI:
|
|
30
37
|
* `[..extra] [--resume <id>] [--model <m>] [--append-system-prompt <text>] --settings <json>`.
|
|
31
38
|
*/
|
|
32
|
-
export declare function buildClaudeTuiArgs({ settingsJson, settingSources, model, appendSystemPrompt, resume, extraArgs, }: ClaudeTuiArgs): string[];
|
|
39
|
+
export declare function buildClaudeTuiArgs({ settingsJson, settingSources, model, appendSystemPrompt, resume, forkSession, sessionId, additionalDirs, extraArgs, }: ClaudeTuiArgs): string[];
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
* Build the `claude` argv for an interactive, co-drivable TUI:
|
|
3
3
|
* `[..extra] [--resume <id>] [--model <m>] [--append-system-prompt <text>] --settings <json>`.
|
|
4
4
|
*/
|
|
5
|
-
export function buildClaudeTuiArgs({ settingsJson, settingSources, model, appendSystemPrompt, resume, extraArgs = [], }) {
|
|
5
|
+
export function buildClaudeTuiArgs({ settingsJson, settingSources, model, appendSystemPrompt, resume, forkSession = false, sessionId, additionalDirs = [], extraArgs = [], }) {
|
|
6
6
|
const args = [...extraArgs];
|
|
7
|
+
if (additionalDirs.length > 0)
|
|
8
|
+
args.push("--add-dir", ...additionalDirs);
|
|
7
9
|
if (settingSources !== undefined)
|
|
8
10
|
args.push("--setting-sources", settingSources);
|
|
9
11
|
if (resume)
|
|
10
12
|
args.push("--resume", resume);
|
|
13
|
+
if (forkSession)
|
|
14
|
+
args.push("--fork-session");
|
|
15
|
+
if (sessionId)
|
|
16
|
+
args.push("--session-id", sessionId);
|
|
11
17
|
if (model)
|
|
12
18
|
args.push("--model", model);
|
|
13
19
|
if (appendSystemPrompt)
|
|
@@ -29,12 +29,16 @@ export interface CodexRemoteArgs {
|
|
|
29
29
|
configOverrides?: string[];
|
|
30
30
|
/** Extra codex args that precede the attach flags (e.g. `["--model", "..."]`). */
|
|
31
31
|
codexArgs?: string[];
|
|
32
|
+
/** Session workspace roots beyond the primary process cwd. Replayed on fresh
|
|
33
|
+
* launch and resume so the remote TUI uses the same immutable workspace
|
|
34
|
+
* snapshot as the App Server clients. */
|
|
35
|
+
additionalDirs?: string[];
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* Build the `codex` argv tail for an app-server-backed TUI. Mirrors reference implementation's
|
|
35
39
|
* `build_codex_remote_args` exactly: overrides → codexArgs → (resume) → --remote.
|
|
36
40
|
*/
|
|
37
|
-
export declare function buildCodexRemoteArgs({ remoteUrl, threadId, configOverrides, codexArgs, }: CodexRemoteArgs): string[];
|
|
41
|
+
export declare function buildCodexRemoteArgs({ remoteUrl, threadId, configOverrides, codexArgs, additionalDirs, }: CodexRemoteArgs): string[];
|
|
38
42
|
export interface LaunchCodexTuiOptions extends CodexRemoteArgs {
|
|
39
43
|
registry: TerminalRegistry;
|
|
40
44
|
/** Terminal id (one codex TUI per session — `terminal_codex_main`). */
|
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
* Build the `codex` argv tail for an app-server-backed TUI. Mirrors reference implementation's
|
|
3
3
|
* `build_codex_remote_args` exactly: overrides → codexArgs → (resume) → --remote.
|
|
4
4
|
*/
|
|
5
|
-
export function buildCodexRemoteArgs({ remoteUrl, threadId, configOverrides = [], codexArgs = [], }) {
|
|
5
|
+
export function buildCodexRemoteArgs({ remoteUrl, threadId, configOverrides = [], codexArgs = [], additionalDirs = [], }) {
|
|
6
6
|
const overrideArgs = configOverrides.flatMap((override) => ["-c", override]);
|
|
7
|
+
const additionalDirArgs = additionalDirs.flatMap((dir) => ["--add-dir", dir]);
|
|
7
8
|
if (threadId === undefined) {
|
|
8
|
-
return [...overrideArgs, ...codexArgs, "--remote", remoteUrl];
|
|
9
|
+
return [...overrideArgs, ...codexArgs, ...additionalDirArgs, "--remote", remoteUrl];
|
|
9
10
|
}
|
|
10
|
-
return [
|
|
11
|
+
return [
|
|
12
|
+
...overrideArgs,
|
|
13
|
+
...codexArgs,
|
|
14
|
+
...additionalDirArgs,
|
|
15
|
+
"resume",
|
|
16
|
+
"--remote",
|
|
17
|
+
remoteUrl,
|
|
18
|
+
threadId,
|
|
19
|
+
];
|
|
11
20
|
}
|
|
12
21
|
/**
|
|
13
22
|
* Create (idempotently) the codex TUI terminal in the registry, running the real
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11-beta.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"node-pty": "^1.0.0",
|
|
28
28
|
"ws": "^8.21.0",
|
|
29
|
-
"@rynx-ai/core": "0.1.
|
|
29
|
+
"@rynx-ai/core": "0.1.11-beta.2"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/ws": "^8.18.1"
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { type SessionItem } from "@rynx-ai/core";
|
|
2
|
-
import { type CodexLineageRuntime } from "../codex-home.js";
|
|
3
|
-
export interface SynthesizeRolloutOptions {
|
|
4
|
-
threadId: string;
|
|
5
|
-
cwd: string;
|
|
6
|
-
items: SessionItem[];
|
|
7
|
-
/** The rynx session (localThreadId) — resolves the per-session private CODEX_HOME
|
|
8
|
-
* (`codexHomePath(sessionId)`). Required unless `codexHome` is passed directly. */
|
|
9
|
-
sessionId?: string;
|
|
10
|
-
/** Override the private CODEX_HOME (tests, or a pre-resolved home). */
|
|
11
|
-
codexHome?: string;
|
|
12
|
-
/** Codex-lineage runtime whose private home/session layout is being written. */
|
|
13
|
-
runtime?: CodexLineageRuntime;
|
|
14
|
-
/** Runtime-neutral alias for `codexHome`; preferred for Traex callers. */
|
|
15
|
-
runtimeHome?: string;
|
|
16
|
-
/** codex CLI version for `session_meta` (informational for ≥0.133; presence
|
|
17
|
-
* matters). */
|
|
18
|
-
cliVersion?: string;
|
|
19
|
-
/** `session_meta.model_provider` — an empty/unresolvable value silently drops the
|
|
20
|
-
* carried history on resume. Defaults to `openai`. */
|
|
21
|
-
modelProvider?: string;
|
|
22
|
-
/** Clock injection (tests). */
|
|
23
|
-
now?: () => number;
|
|
24
|
-
}
|
|
25
|
-
interface RolloutRecord {
|
|
26
|
-
timestamp: string;
|
|
27
|
-
type: "session_meta" | "turn_context" | "response_item" | "event_msg";
|
|
28
|
-
payload: Record<string, unknown>;
|
|
29
|
-
}
|
|
30
|
-
/** Locate an existing rollout for `threadId` under the runtime's sessions root. */
|
|
31
|
-
export declare function findCodexRollout(runtimeHome: string, threadId: string, runtime?: CodexLineageRuntime): string | null;
|
|
32
|
-
/** Build the ordered rollout records (session_meta first, then per-turn
|
|
33
|
-
* turn_context + per-item response_item + per-message event_msg). */
|
|
34
|
-
export declare function buildRolloutRecords(opts: SynthesizeRolloutOptions): RolloutRecord[];
|
|
35
|
-
/**
|
|
36
|
-
* Ensure a resumable codex rollout exists for `threadId`. No-op if one is already
|
|
37
|
-
* present (codex owns its own runtime rollouts). Otherwise synthesize one from the
|
|
38
|
-
* session items and write it atomically. Returns `"exists"`, `"written"`, or
|
|
39
|
-
* `"skipped"` (invalid thread id / nothing to carry). Best-effort: never throws.
|
|
40
|
-
*/
|
|
41
|
-
export declare function ensureCodexResumeRollout(opts: SynthesizeRolloutOptions): "exists" | "written" | "skipped";
|
|
42
|
-
export {};
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Synthesize a codex rollout file from rynx's canonical session log, so
|
|
3
|
-
* `codex --remote resume <threadId>` works when the local rollout is missing
|
|
4
|
-
* (fork / worktree / cross-machine). Ports reference implementation's
|
|
5
|
-
* `_ensure_local_codex_resume_rollout`, adapted to rynx: the daemon (control-api)
|
|
6
|
-
* has BOTH the session items (`SessionLogStore`) and — since the private CODEX_HOME
|
|
7
|
-
* is a deterministic uid-scoped path — the app-server's rollout dir, so this runs
|
|
8
|
-
* entirely daemon-side with no runner round-trip.
|
|
9
|
-
*
|
|
10
|
-
* The record shapes match codex 0.142.5 (empirically captured). The minimal set
|
|
11
|
-
* codex needs to resume with VISIBLE turns is: a `session_meta` first line, then
|
|
12
|
-
* per item a `response_item`, and — critically — an `event_msg` mirror per message
|
|
13
|
-
* (codex ≥0.136 renders an empty thread without them). `turn_context` groups items
|
|
14
|
-
* into turns.
|
|
15
|
-
*/
|
|
16
|
-
import { createHash } from "node:crypto";
|
|
17
|
-
import { copyFileSync, mkdirSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { join, relative } from "node:path";
|
|
19
|
-
import { getRuntimeProfile, resolveRuntimeHome, rynxHome, } from "@rynx-ai/core";
|
|
20
|
-
import { legacyCodexHomePath, runtimeHomePath, } from "../codex-home.js";
|
|
21
|
-
/** codex validates the thread id straight into a filename + resume arg. */
|
|
22
|
-
const THREAD_ID_RE = /^[0-9a-fA-F-]+$/;
|
|
23
|
-
function sessionsRoot(runtimeHome, runtime) {
|
|
24
|
-
return join(runtimeHome, ...getRuntimeProfile(runtime).sessionsSubpath);
|
|
25
|
-
}
|
|
26
|
-
/** Locate an existing rollout for `threadId` under the runtime's sessions root. */
|
|
27
|
-
export function findCodexRollout(runtimeHome, threadId, runtime = "codex") {
|
|
28
|
-
const root = sessionsRoot(runtimeHome, runtime);
|
|
29
|
-
const suffix = `-${threadId}.jsonl`;
|
|
30
|
-
const stack = [root];
|
|
31
|
-
while (stack.length) {
|
|
32
|
-
const dir = stack.pop();
|
|
33
|
-
let entries;
|
|
34
|
-
try {
|
|
35
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
continue; // missing dir — fine
|
|
39
|
-
}
|
|
40
|
-
for (const e of entries) {
|
|
41
|
-
const full = join(dir, e.name);
|
|
42
|
-
if (e.isDirectory())
|
|
43
|
-
stack.push(full);
|
|
44
|
-
else if (e.name.startsWith("rollout-") && e.name.endsWith(suffix))
|
|
45
|
-
return full;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Back-compat: before per-session homes, rollouts lived under one shared
|
|
52
|
-
* {@link legacyCodexHomePath}. If a session's rollout is only there, copy it forward
|
|
53
|
-
* into this session's home (preserving the `sessions/YYYY/MM/DD/` layout codex
|
|
54
|
-
* expects) so the per-session app-server can resume it. Best-effort; gated by
|
|
55
|
-
* `RYNX_CODEX_HOME_LEGACY_FALLBACK` (default on; set `0`/`false` to disable).
|
|
56
|
-
*/
|
|
57
|
-
function adoptLegacyRollout(runtimeHome, threadId, runtime) {
|
|
58
|
-
const flag = (process.env.RYNX_RUNTIME_HOME_LEGACY_FALLBACK
|
|
59
|
-
?? process.env.RYNX_CODEX_HOME_LEGACY_FALLBACK)?.trim().toLowerCase();
|
|
60
|
-
if (flag === "0" || flag === "false")
|
|
61
|
-
return false;
|
|
62
|
-
const legacy = runtime === "codex"
|
|
63
|
-
? legacyCodexHomePath()
|
|
64
|
-
: resolveRuntimeHome(getRuntimeProfile(runtime));
|
|
65
|
-
if (legacy === runtimeHome)
|
|
66
|
-
return false;
|
|
67
|
-
const src = findCodexRollout(legacy, threadId, runtime);
|
|
68
|
-
if (!src)
|
|
69
|
-
return false;
|
|
70
|
-
const rel = relative(sessionsRoot(legacy, runtime), src);
|
|
71
|
-
const dst = join(sessionsRoot(runtimeHome, runtime), rel);
|
|
72
|
-
try {
|
|
73
|
-
mkdirSync(join(dst, ".."), { recursive: true, mode: 0o700 });
|
|
74
|
-
copyFileSync(src, dst);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function iso(ms) {
|
|
82
|
-
return new Date(ms).toISOString();
|
|
83
|
-
}
|
|
84
|
-
/** `resp_codex_<turnId>` → `<turnId>` (rynx derives the responseId from the codex
|
|
85
|
-
* turn id); anything else is used verbatim as a stable turn key. */
|
|
86
|
-
function turnIdOf(responseId) {
|
|
87
|
-
return responseId.startsWith("resp_codex_") ? responseId.slice("resp_codex_".length) : responseId;
|
|
88
|
-
}
|
|
89
|
-
function textOf(item) {
|
|
90
|
-
return item.data.content
|
|
91
|
-
.filter((part) => "text" in part)
|
|
92
|
-
.map((part) => part.text)
|
|
93
|
-
.join("");
|
|
94
|
-
}
|
|
95
|
-
/** Convert one canonical {@link SessionItem} to its codex `response_item` payload,
|
|
96
|
-
* or null for types codex doesn't carry (reasoning/terminal_command/error). */
|
|
97
|
-
function responseItemPayload(item) {
|
|
98
|
-
switch (item.type) {
|
|
99
|
-
case "message": {
|
|
100
|
-
const apiType = item.data.role === "assistant" ? "output_text" : "input_text";
|
|
101
|
-
const content = item.data.content.flatMap((part) => "text" in part && part.text
|
|
102
|
-
? [{ type: apiType, text: part.text }]
|
|
103
|
-
: []);
|
|
104
|
-
if (content.length === 0)
|
|
105
|
-
return null;
|
|
106
|
-
return { type: "message", role: item.data.role, content };
|
|
107
|
-
}
|
|
108
|
-
case "function_call":
|
|
109
|
-
return {
|
|
110
|
-
type: "function_call",
|
|
111
|
-
name: item.data.name,
|
|
112
|
-
call_id: item.data.callId,
|
|
113
|
-
arguments: item.data.arguments,
|
|
114
|
-
};
|
|
115
|
-
case "function_call_output":
|
|
116
|
-
return { type: "function_call_output", call_id: item.data.callId, output: item.data.output };
|
|
117
|
-
default:
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
/** The `event_msg` mirror for a message (required for a VISIBLE turn on codex ≥0.136). */
|
|
122
|
-
function eventMsgPayload(item, sessionId) {
|
|
123
|
-
const message = textOf(item).trim();
|
|
124
|
-
const localImages = item.data.role === "user" && sessionId
|
|
125
|
-
? item.data.content.flatMap((part) => part.type === "input_image" ? [resourcePath(sessionId, part)] : [])
|
|
126
|
-
: [];
|
|
127
|
-
if (!message && localImages.length === 0)
|
|
128
|
-
return null;
|
|
129
|
-
if (item.data.role === "user") {
|
|
130
|
-
return {
|
|
131
|
-
type: "user_message",
|
|
132
|
-
message,
|
|
133
|
-
images: [],
|
|
134
|
-
local_images: localImages,
|
|
135
|
-
text_elements: [],
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
if (item.data.role === "assistant") {
|
|
139
|
-
return { type: "agent_message", message, phase: "final_answer", memory_citation: null };
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
function resourcePath(sessionId, part) {
|
|
144
|
-
const sessionKey = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
145
|
-
const extension = part.mediaType === "image/png"
|
|
146
|
-
? "png"
|
|
147
|
-
: part.mediaType === "image/jpeg"
|
|
148
|
-
? "jpg"
|
|
149
|
-
: "webp";
|
|
150
|
-
return join(rynxHome(), "resources", sessionKey, `${part.resourceId}.${extension}`);
|
|
151
|
-
}
|
|
152
|
-
/** Build the ordered rollout records (session_meta first, then per-turn
|
|
153
|
-
* turn_context + per-item response_item + per-message event_msg). */
|
|
154
|
-
export function buildRolloutRecords(opts) {
|
|
155
|
-
const now = opts.now ?? (() => Date.now());
|
|
156
|
-
const modelProvider = opts.modelProvider ?? (opts.runtime === "traex" ? "trae" : "openai");
|
|
157
|
-
const cliVersion = opts.cliVersion ?? "0.0.0";
|
|
158
|
-
const metaTs = iso(opts.items[0]?.createdAt ?? now());
|
|
159
|
-
const records = [
|
|
160
|
-
{
|
|
161
|
-
timestamp: metaTs,
|
|
162
|
-
type: "session_meta",
|
|
163
|
-
payload: {
|
|
164
|
-
id: opts.threadId,
|
|
165
|
-
session_id: opts.threadId,
|
|
166
|
-
timestamp: metaTs,
|
|
167
|
-
cwd: opts.cwd,
|
|
168
|
-
originator: "rynx",
|
|
169
|
-
cli_version: cliVersion,
|
|
170
|
-
source: "rynx",
|
|
171
|
-
thread_source: "user",
|
|
172
|
-
model_provider: modelProvider,
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
];
|
|
176
|
-
const seenTurns = new Set();
|
|
177
|
-
for (const item of opts.items) {
|
|
178
|
-
const payload = responseItemPayload(item);
|
|
179
|
-
const eventPayload = item.type === "message"
|
|
180
|
-
? eventMsgPayload(item, opts.sessionId)
|
|
181
|
-
: null;
|
|
182
|
-
if (!payload && !eventPayload)
|
|
183
|
-
continue;
|
|
184
|
-
const ts = iso(item.createdAt);
|
|
185
|
-
const turnId = turnIdOf(item.responseId);
|
|
186
|
-
if (!seenTurns.has(turnId)) {
|
|
187
|
-
seenTurns.add(turnId);
|
|
188
|
-
records.push({
|
|
189
|
-
timestamp: ts,
|
|
190
|
-
type: "turn_context",
|
|
191
|
-
payload: { turn_id: turnId, cwd: opts.cwd, approval_policy: "on-request" },
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
if (payload)
|
|
195
|
-
records.push({ timestamp: ts, type: "response_item", payload });
|
|
196
|
-
if (eventPayload)
|
|
197
|
-
records.push({ timestamp: ts, type: "event_msg", payload: eventPayload });
|
|
198
|
-
}
|
|
199
|
-
return records;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Ensure a resumable codex rollout exists for `threadId`. No-op if one is already
|
|
203
|
-
* present (codex owns its own runtime rollouts). Otherwise synthesize one from the
|
|
204
|
-
* session items and write it atomically. Returns `"exists"`, `"written"`, or
|
|
205
|
-
* `"skipped"` (invalid thread id / nothing to carry). Best-effort: never throws.
|
|
206
|
-
*/
|
|
207
|
-
export function ensureCodexResumeRollout(opts) {
|
|
208
|
-
if (!THREAD_ID_RE.test(opts.threadId))
|
|
209
|
-
return "skipped";
|
|
210
|
-
const runtime = opts.runtime ?? "codex";
|
|
211
|
-
const runtimeHome = opts.runtimeHome
|
|
212
|
-
?? opts.codexHome
|
|
213
|
-
?? (opts.sessionId ? runtimeHomePath(opts.sessionId, runtime) : undefined);
|
|
214
|
-
if (!runtimeHome)
|
|
215
|
-
return "skipped"; // no session context → can't locate the home
|
|
216
|
-
if (findCodexRollout(runtimeHome, opts.threadId, runtime))
|
|
217
|
-
return "exists";
|
|
218
|
-
// Back-compat: a pre-per-session rollout may live under the OLD shared home. Copy
|
|
219
|
-
// it forward into this session's home so the per-session app-server can resume it.
|
|
220
|
-
if (adoptLegacyRollout(runtimeHome, opts.threadId, runtime))
|
|
221
|
-
return "exists";
|
|
222
|
-
const records = buildRolloutRecords(opts);
|
|
223
|
-
// Nothing but the session_meta header → no history to carry; skip (a fresh
|
|
224
|
-
// thread resumes fine without a rollout once it has run a turn).
|
|
225
|
-
if (records.length <= 1)
|
|
226
|
-
return "skipped";
|
|
227
|
-
const now = opts.now ?? (() => Date.now());
|
|
228
|
-
const d = new Date(now());
|
|
229
|
-
const yyyy = String(d.getUTCFullYear());
|
|
230
|
-
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
231
|
-
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
232
|
-
const stamp = iso(d.getTime()).slice(0, 19).replace(/:/g, "-"); // YYYY-MM-DDTHH-MM-SS
|
|
233
|
-
const dir = join(sessionsRoot(runtimeHome, runtime), yyyy, mm, dd);
|
|
234
|
-
const target = join(dir, `rollout-${stamp}-${opts.threadId}.jsonl`);
|
|
235
|
-
const tmp = `${target}.tmp`;
|
|
236
|
-
try {
|
|
237
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
238
|
-
writeFileSync(tmp, records.map((r) => JSON.stringify(r)).join("\n") + "\n");
|
|
239
|
-
renameSync(tmp, target);
|
|
240
|
-
return "written";
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
return "skipped"; // best-effort: a failed synth just leaves resume to fail as before
|
|
244
|
-
}
|
|
245
|
-
}
|