@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.
Files changed (36) hide show
  1. package/dist/claude/native-bridge.js +3 -8
  2. package/dist/claude/native-integration.d.ts +12 -1
  3. package/dist/claude/native-integration.js +16 -2
  4. package/dist/claude/transcript.d.ts +0 -7
  5. package/dist/claude/transcript.js +6 -20
  6. package/dist/codex-app-server/client.d.ts +2 -1
  7. package/dist/codex-app-server/forwarder.d.ts +4 -1
  8. package/dist/codex-app-server/forwarder.js +19 -1
  9. package/dist/codex-app-server/protocol.d.ts +45 -1
  10. package/dist/codex-home.d.ts +9 -26
  11. package/dist/codex-home.js +37 -65
  12. package/dist/codex-session-store.d.ts +22 -10
  13. package/dist/codex-session-store.js +277 -12
  14. package/dist/host.d.ts +47 -47
  15. package/dist/host.js +790 -350
  16. package/dist/index.d.ts +1 -2
  17. package/dist/index.js +0 -1
  18. package/dist/models-catalog.d.ts +1 -0
  19. package/dist/models-catalog.js +43 -1
  20. package/dist/provider-workspace.d.ts +56 -0
  21. package/dist/provider-workspace.js +83 -0
  22. package/dist/runner/child.d.ts +54 -6
  23. package/dist/runner/child.js +42 -17
  24. package/dist/runner/manager.d.ts +41 -18
  25. package/dist/runner/manager.js +432 -55
  26. package/dist/runner/protocol.d.ts +7 -18
  27. package/dist/runner-main.js +12 -4
  28. package/dist/runtime-state-paths.d.ts +10 -0
  29. package/dist/runtime-state-paths.js +53 -0
  30. package/dist/terminal/claude-tui.d.ts +8 -1
  31. package/dist/terminal/claude-tui.js +7 -1
  32. package/dist/terminal/codex-tui.d.ts +5 -1
  33. package/dist/terminal/codex-tui.js +12 -3
  34. package/package.json +2 -2
  35. package/dist/codex/rollout-synth.d.ts +0 -42
  36. package/dist/codex/rollout-synth.js +0 -245
@@ -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, } from "@rynx-ai/core";
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
- ...(process.env.RYNX_RUNNER_SESSION ? { sessionId: process.env.RYNX_RUNNER_SESSION } : {}),
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 [...overrideArgs, ...codexArgs, "resume", "--remote", remoteUrl, threadId];
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.10",
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.10"
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
- }