@rynx-ai/runtime 0.1.11-beta.34 → 0.1.11-beta.36

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.
@@ -312,6 +312,7 @@ function nativeVerdict(hookKind, payload, result, suggestions) {
312
312
  ...(behavior === "deny" && feedback ? { message: feedback } : {}),
313
313
  ...(behavior === "allow"
314
314
  ? {
315
+ updatedInput: toolInput,
315
316
  updatedPermissions: [{
316
317
  type: "setMode",
317
318
  mode: resolution.actionId === "allow_auto" ? "auto" : "default",
@@ -13,6 +13,11 @@ function isSubagentThreadStarted(params) {
13
13
  const source = params?.thread?.source?.subAgent?.thread_spawn;
14
14
  return source !== null && typeof source === "object" && !Array.isArray(source);
15
15
  }
16
+ /** Internal system threads are non-persistable and never replace the parent
17
+ * TUI thread, even though the app-server broadcasts `thread/started`. */
18
+ function isEphemeralThreadStarted(params) {
19
+ return params?.thread?.ephemeral === true;
20
+ }
16
21
  function turnIdFrom(params) {
17
22
  const p = params;
18
23
  return p?.turnId ?? p?.turn?.id;
@@ -261,7 +266,8 @@ export class CodexSessionForwarder {
261
266
  }
262
267
  handle(method, params) {
263
268
  if (method === "thread/started" || method === "thread.started") {
264
- if (method === "thread/started" && isSubagentThreadStarted(params))
269
+ if (method === "thread/started" &&
270
+ (isSubagentThreadStarted(params) || isEphemeralThreadStarted(params)))
265
271
  return;
266
272
  const tid = threadIdFrom(params);
267
273
  if (tid && tid !== this.currentThreadIdValue) {
@@ -127,10 +127,12 @@ function fileChangeSummary(changes) {
127
127
  for (const change of changes) {
128
128
  if (!isRecord(change))
129
129
  continue;
130
+ if (typeof change.path !== "string")
131
+ continue;
130
132
  const kind = isRecord(change.kind) && typeof change.kind.type === "string" && change.kind.type
131
133
  ? change.kind.type
132
134
  : "change";
133
- lines.push(`${kind} ${String(change.path)}`);
135
+ lines.push(`${kind} ${change.path}`);
134
136
  }
135
137
  return lines.join("\n");
136
138
  }
@@ -147,20 +149,37 @@ export function mapCodexItem(method, item) {
147
149
  switch (item.type) {
148
150
  case "commandExecution": {
149
151
  const commandItem = item;
152
+ const id = typeof commandItem.id === "string" ? commandItem.id : "";
153
+ const command = typeof commandItem.command === "string" ? commandItem.command : "";
154
+ if (!id || !command)
155
+ return { events };
156
+ const exitCode = typeof commandItem.exitCode === "number"
157
+ ? commandItem.exitCode
158
+ : null;
159
+ const rawOutput = commandItem.aggregatedOutput ?? "";
160
+ const aggregatedOutput = exitCode !== null && exitCode !== 0
161
+ ? `${rawOutput}${rawOutput ? "\n" : ""}[exit code: ${exitCode}]`
162
+ : rawOutput;
150
163
  events.push({
151
164
  type: "tool",
152
165
  event: isStart ? "on_tool_start" : "on_tool_end",
153
- name: "command_execution",
154
- input: isStart
155
- ? { id: commandItem.id, command: commandItem.command, cwd: commandItem.cwd }
166
+ name: "shell",
167
+ input: isStart || isEnd
168
+ ? {
169
+ id,
170
+ command,
171
+ ...(typeof commandItem.cwd === "string" && commandItem.cwd
172
+ ? { cwd: commandItem.cwd }
173
+ : {}),
174
+ }
156
175
  : undefined,
157
176
  output: isEnd
158
177
  ? {
159
- id: commandItem.id,
160
- command: commandItem.command,
178
+ id,
179
+ command,
161
180
  status: commandItem.status,
162
- aggregatedOutput: commandItem.aggregatedOutput ?? null,
163
- exitCode: commandItem.exitCode ?? null,
181
+ aggregatedOutput,
182
+ exitCode,
164
183
  }
165
184
  : undefined,
166
185
  data: { method, item },
@@ -170,14 +189,19 @@ export function mapCodexItem(method, item) {
170
189
  case "fileChange": {
171
190
  const fileChange = item;
172
191
  const changes = Array.isArray(fileChange.changes) ? fileChange.changes : [];
192
+ const id = typeof item.id === "string" ? item.id : "";
193
+ if (!id ||
194
+ changes.length === 0 ||
195
+ !changes.some((change) => isRecord(change) && typeof change.path === "string"))
196
+ return { events };
173
197
  events.push({
174
198
  type: "tool",
175
199
  event: isStart ? "on_tool_start" : "on_tool_end",
176
200
  name: "apply_patch",
177
- input: isStart || isEnd ? { id: item.id, ...(changes.length ? { changes } : {}) } : undefined,
201
+ input: isStart || isEnd ? { id, changes } : undefined,
178
202
  output: isEnd
179
203
  ? {
180
- id: item.id,
204
+ id,
181
205
  status: fileChange.status,
182
206
  aggregatedOutput: fileChangeSummary(changes),
183
207
  }
@@ -1,5 +1,16 @@
1
- import { type AgentRuntimeId } from "@rynx-ai/core";
1
+ import { type AgentRuntimeId, type ResolvedRetryPolicy } from "@rynx-ai/core";
2
2
  export type CodexLineageRuntime = Exclude<AgentRuntimeId, "claude">;
3
+ export type CodexAuthMode = "chatgpt" | "apikey" | "pat";
4
+ export type CodexConfigAuth = "provider-ready" | "provider-auth-missing" | "codex-login";
5
+ /** Read the credential mode from Codex's optional-field auth.json shape. */
6
+ export declare function codexAuthEffectiveMode(authPath: string): CodexAuthMode | null;
7
+ /** Mirror Codex's provider-specific auth decision without spawning the CLI. */
8
+ export declare function codexConfigEffectiveAuth(configPath: string, env?: NodeJS.ProcessEnv): CodexConfigAuth;
9
+ /** True only when this exact launch would defer to an absent Codex login. */
10
+ export declare function codexLaunchRequiresLogin(codexHome: string, env?: NodeJS.ProcessEnv): boolean;
11
+ /** Resolve the user's auth/config source without nesting a parent Session's
12
+ * private CODEX_HOME into a child Session. */
13
+ export declare function resolveCodexHomeConfigSource(candidate: string, defaultHome?: string): string;
3
14
  /**
4
15
  * The deterministic private CODEX_HOME path for a rynx session. PER-SESSION,
5
16
  * under the daemon-owned RYNX_HOME runtime tree,
@@ -22,8 +33,8 @@ export declare function legacyCodexHomePath(): string;
22
33
  * `_CODEX_HOME_COPY_FILES`.
23
34
  *
24
35
  * PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
25
- * agents never share a `skills/` dir. Idempotent: the symlink/copy are refreshed each
26
- * call so a re-login/config change propagates. Returns the private home dir.
36
+ * agents never share a `skills/` dir. Idempotent: existing Session-owned entries stay
37
+ * unchanged; live symlinks still observe source credential updates. Returns the private home dir.
27
38
  */
28
39
  export declare function prepareCodexHome(sessionId: string, realHome?: string): string;
29
40
  /**
@@ -32,6 +43,27 @@ export declare function prepareCodexHome(sessionId: string, realHome?: string):
32
43
  * real home and cannot wedge a managed app-server/TUI pair.
33
44
  */
34
45
  export declare function prepareRuntimeHome(sessionId: string, runtime: CodexLineageRuntime, realHome?: string): string;
46
+ /** Trust a runner-selected workspace only in the Session's private config so
47
+ * Codex cannot stop a detached TUI at its project-trust prompt. */
48
+ export declare function trustCodexProject(codexHome: string, cwd: string): void;
49
+ /** Apply the Session retry budget through Codex's supported provider settings.
50
+ * A config with no provider definitions is left untouched. */
51
+ export declare function applyCodexProviderRetryPolicy(codexHome: string, retry?: ResolvedRetryPolicy | null): void;
52
+ /** Preserve the user's private-home base instructions and append only the raw
53
+ * Agent-authored text. Repeated launches replace the managed suffix instead of
54
+ * accumulating it. Invalid user TOML is intentionally left untouched. */
55
+ export declare function syncCodexDeveloperInstructions(codexHome: string, instructions?: string | null): void;
56
+ export type CodexDeveloperInstructionsState = {
57
+ state: "present";
58
+ value: string;
59
+ } | {
60
+ state: "absent";
61
+ } | {
62
+ state: "unreadable";
63
+ };
64
+ /** Read the active private-home instructions without conflating a definite
65
+ * absence with malformed or temporarily unreadable config. */
66
+ export declare function readCodexDeveloperInstructionsState(codexHome: string): CodexDeveloperInstructionsState;
35
67
  /** Persist immutable skill copies in a native Provider home. Fork descendants
36
68
  * share that home, so entries must outlive every runner's temporary sources. */
37
69
  export declare function populateCodexSkills(codexHome: string, skills: {
@@ -1,26 +1,174 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { copyFileSync, cpSync, existsSync, mkdirSync, renameSync, rmSync, symlinkSync } from "node:fs";
2
+ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { parse, stringify } from "smol-toml";
5
6
  import { assertSkillPathComponent, getRuntimeProfile, resolveRuntimeHome, } from "@rynx-ai/core";
6
7
  import { adoptLegacyRuntimeDirectory, legacyRuntimeStateRoot, runtimeSessionDigest, runtimeSessionStateDir, } from "./runtime-state-paths.js";
7
- /** Inherit the user's LIVE login by symlink (stays in sync). */
8
- const SYMLINK_FILES = ["auth.json"];
8
+ /** Codex-owned files that must stay live across every private home. */
9
+ const CODEX_SYMLINK_FILES = [
10
+ "auth.json",
11
+ ".credentials.json",
12
+ "memories_1.sqlite",
13
+ "AGENTS.md",
14
+ "AGENTS.override.md",
15
+ "hooks.json",
16
+ ];
17
+ /** Large/shared Codex state and cross-process coordination directories. */
18
+ const CODEX_SYMLINK_DIRS = [
19
+ join("plugins", "cache"),
20
+ "mcp-oauth-locks",
21
+ "memories",
22
+ "rules",
23
+ ];
9
24
  /** Inherit the user's settings by snapshot copy (not the mutable NUX/update state). */
10
25
  const COPY_FILES = ["config.toml"];
11
26
  const RUNTIME_HOME_FILES = {
12
- codex: { symlink: SYMLINK_FILES, copy: COPY_FILES },
27
+ codex: {
28
+ symlink: CODEX_SYMLINK_FILES,
29
+ symlinkDirs: CODEX_SYMLINK_DIRS,
30
+ copy: COPY_FILES,
31
+ },
13
32
  traex: {
14
33
  // Traex keeps credentials below `cli/`, while its user configuration lives
15
34
  // at the TRAE_HOME root. Keep both the current TOML name and the legacy YAML
16
35
  // name so existing installations remain usable without an implicit migrate.
17
36
  symlink: ["cli/auth.json"],
37
+ symlinkDirs: [],
18
38
  copy: ["traecli.toml", "traecli.yaml"],
19
39
  },
20
40
  };
41
+ const CODEX_BUILTIN_PROVIDERS = new Set([
42
+ "openai",
43
+ "amazon-bedrock",
44
+ "amazon-bedrock-runtime",
45
+ "ollama",
46
+ "lmstudio",
47
+ ]);
48
+ function isTomlTable(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50
+ }
51
+ /** Resolve the effective native Provider, honoring the selected profile first. */
52
+ function effectiveCodexModelProvider(config) {
53
+ let provider = config.model_provider;
54
+ const activeProfile = config.profile;
55
+ if (typeof activeProfile === "string" && activeProfile.trim() && isTomlTable(config.profiles)) {
56
+ const profile = config.profiles[activeProfile];
57
+ if (isTomlTable(profile) && typeof profile.model_provider === "string") {
58
+ provider = profile.model_provider;
59
+ }
60
+ }
61
+ return typeof provider === "string" && provider.trim() ? provider : null;
62
+ }
63
+ /** Read the credential mode from Codex's optional-field auth.json shape. */
64
+ export function codexAuthEffectiveMode(authPath) {
65
+ let data;
66
+ try {
67
+ data = JSON.parse(readFileSync(authPath, "utf8"));
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ if (!data || typeof data !== "object" || Array.isArray(data))
73
+ return null;
74
+ const auth = data;
75
+ const tokens = auth.tokens;
76
+ if (tokens && typeof tokens === "object" && !Array.isArray(tokens)) {
77
+ const tokenRecord = tokens;
78
+ for (const field of ["access_token", "refresh_token"]) {
79
+ const value = tokenRecord[field];
80
+ if (typeof value === "string" && value.trim())
81
+ return "chatgpt";
82
+ }
83
+ }
84
+ if (typeof auth.OPENAI_API_KEY === "string" && auth.OPENAI_API_KEY.trim())
85
+ return "apikey";
86
+ if (typeof auth.personal_access_token === "string" &&
87
+ auth.personal_access_token.trim())
88
+ return "pat";
89
+ return null;
90
+ }
91
+ /** Mirror Codex's provider-specific auth decision without spawning the CLI. */
92
+ export function codexConfigEffectiveAuth(configPath, env = process.env) {
93
+ let config;
94
+ try {
95
+ config = parse(readFileSync(configPath, "utf8"));
96
+ }
97
+ catch {
98
+ return "codex-login";
99
+ }
100
+ const providerId = effectiveCodexModelProvider(config);
101
+ if (!providerId)
102
+ return "codex-login";
103
+ if (CODEX_BUILTIN_PROVIDERS.has(providerId)) {
104
+ return providerId === "openai" ? "codex-login" : "provider-ready";
105
+ }
106
+ const provider = isTomlTable(config.model_providers)
107
+ ? config.model_providers[providerId]
108
+ : undefined;
109
+ if (!isTomlTable(provider))
110
+ return "provider-auth-missing";
111
+ if (provider.requires_openai_auth === true)
112
+ return "codex-login";
113
+ const envKey = provider.env_key;
114
+ if (typeof envKey !== "string" || !envKey.trim())
115
+ return "provider-ready";
116
+ const value = env[envKey.trim()];
117
+ return typeof value === "string" && value.trim()
118
+ ? "provider-ready"
119
+ : "provider-auth-missing";
120
+ }
121
+ /** True only when this exact launch would defer to an absent Codex login. */
122
+ export function codexLaunchRequiresLogin(codexHome, env = process.env) {
123
+ return codexConfigEffectiveAuth(join(codexHome, "config.toml"), env) === "codex-login" &&
124
+ codexAuthEffectiveMode(join(codexHome, "auth.json")) === null;
125
+ }
126
+ function pathExistsOrIsSymlink(path) {
127
+ try {
128
+ lstatSync(path);
129
+ return true;
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ }
135
+ function isPrivateCodexHome(candidate) {
136
+ const normalized = resolve(candidate);
137
+ if (basename(normalized) !== "codex-home")
138
+ return false;
139
+ const ownerDir = dirname(normalized);
140
+ const namespace = dirname(ownerDir);
141
+ const root = dirname(namespace);
142
+ return (basename(namespace) === "sessions" && basename(root) === "runtime") || basename(namespace) === "codex-native";
143
+ }
144
+ function privateCodexHomeSource(candidate) {
145
+ const sourceDirs = new Set();
146
+ for (const name of ["auth.json", ".credentials.json", "memories_1.sqlite"]) {
147
+ const file = join(candidate, name);
148
+ try {
149
+ if (lstatSync(file).isSymbolicLink())
150
+ sourceDirs.add(dirname(realpathSync(file)));
151
+ }
152
+ catch {
153
+ // Missing or unreadable links do not identify a source home.
154
+ }
155
+ }
156
+ return sourceDirs.size === 1 ? [...sourceDirs][0] : null;
157
+ }
158
+ /** Resolve the user's auth/config source without nesting a parent Session's
159
+ * private CODEX_HOME into a child Session. */
160
+ export function resolveCodexHomeConfigSource(candidate, defaultHome = join(homedir(), ".codex")) {
161
+ const source = resolve(candidate);
162
+ const fallback = resolve(defaultHome);
163
+ if (source !== fallback && isPrivateCodexHome(source)) {
164
+ return privateCodexHomeSource(source) ?? fallback;
165
+ }
166
+ return source;
167
+ }
21
168
  /** The user's real CODEX_HOME (env override, else `~/.codex`). */
22
169
  function realCodexHome() {
23
- return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
170
+ const fallback = join(homedir(), ".codex");
171
+ return resolveCodexHomeConfigSource(process.env.CODEX_HOME?.trim() || fallback, fallback);
24
172
  }
25
173
  /**
26
174
  * The deterministic private CODEX_HOME path for a rynx session. PER-SESSION,
@@ -52,8 +200,8 @@ export function legacyCodexHomePath() {
52
200
  * `_CODEX_HOME_COPY_FILES`.
53
201
  *
54
202
  * PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
55
- * agents never share a `skills/` dir. Idempotent: the symlink/copy are refreshed each
56
- * call so a re-login/config change propagates. Returns the private home dir.
203
+ * agents never share a `skills/` dir. Idempotent: existing Session-owned entries stay
204
+ * unchanged; live symlinks still observe source credential updates. Returns the private home dir.
57
205
  */
58
206
  export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
59
207
  return prepareRuntimeHome(sessionId, "codex", realHome);
@@ -77,13 +225,9 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
77
225
  for (const name of files.symlink) {
78
226
  const src = join(realHome, name);
79
227
  const dst = join(dir, name);
228
+ if (pathExistsOrIsSymlink(dst))
229
+ continue;
80
230
  mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
81
- try {
82
- rmSync(dst, { recursive: true, force: true });
83
- }
84
- catch {
85
- // absent — fine
86
- }
87
231
  if (existsSync(src)) {
88
232
  try {
89
233
  symlinkSync(src, dst);
@@ -93,11 +237,35 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
93
237
  }
94
238
  }
95
239
  }
240
+ for (const name of files.symlinkDirs) {
241
+ const src = join(realHome, name);
242
+ const dst = join(dir, name);
243
+ let sourceIsDirectory = false;
244
+ try {
245
+ sourceIsDirectory = statSync(src).isDirectory();
246
+ }
247
+ catch {
248
+ // Missing/unreadable shared state is optional.
249
+ }
250
+ if (!sourceIsDirectory)
251
+ continue;
252
+ if (pathExistsOrIsSymlink(dst))
253
+ continue;
254
+ mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
255
+ try {
256
+ symlinkSync(src, dst, "dir");
257
+ }
258
+ catch {
259
+ // Best-effort: Codex recreates absent cache/state in its private home.
260
+ }
261
+ }
96
262
  for (const name of files.copy) {
97
263
  const src = join(realHome, name);
98
264
  if (existsSync(src)) {
99
265
  try {
100
266
  const dst = join(dir, name);
267
+ if (pathExistsOrIsSymlink(dst))
268
+ continue;
101
269
  mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
102
270
  copyFileSync(src, dst);
103
271
  }
@@ -108,6 +276,145 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
108
276
  }
109
277
  return dir;
110
278
  }
279
+ /** Trust a runner-selected workspace only in the Session's private config so
280
+ * Codex cannot stop a detached TUI at its project-trust prompt. */
281
+ export function trustCodexProject(codexHome, cwd) {
282
+ const configPath = join(codexHome, "config.toml");
283
+ let config = {};
284
+ if (existsSync(configPath))
285
+ config = parse(readFileSync(configPath, "utf8"));
286
+ const projects = isTomlTable(config.projects) ? config.projects : {};
287
+ config.projects = projects;
288
+ let projectPath;
289
+ try {
290
+ projectPath = realpathSync(cwd);
291
+ }
292
+ catch {
293
+ projectPath = resolve(cwd);
294
+ }
295
+ const existingProject = projects[projectPath];
296
+ const project = isTomlTable(existingProject) ? existingProject : {};
297
+ project.trust_level = "trusted";
298
+ projects[projectPath] = project;
299
+ writeCodexConfig(configPath, config);
300
+ }
301
+ const DEFAULT_CODEX_MAX_RETRIES = 7;
302
+ const DEFAULT_CODEX_REQUEST_TIMEOUT_SECONDS = 120;
303
+ const DEVELOPER_INSTRUCTIONS_BASE_FILE = ".rynx-developer-instructions-base";
304
+ /** Apply the Session retry budget through Codex's supported provider settings.
305
+ * A config with no provider definitions is left untouched. */
306
+ export function applyCodexProviderRetryPolicy(codexHome, retry) {
307
+ const configPath = join(codexHome, "config.toml");
308
+ if (!existsSync(configPath) || lstatSync(configPath).isSymbolicLink())
309
+ return;
310
+ const config = parse(readFileSync(configPath, "utf8"));
311
+ if (!isTomlTable(config.model_providers))
312
+ return;
313
+ const maxRetries = retry?.maxRetries ?? DEFAULT_CODEX_MAX_RETRIES;
314
+ const timeoutSeconds = retry?.timeoutPerRequestSeconds ??
315
+ DEFAULT_CODEX_REQUEST_TIMEOUT_SECONDS;
316
+ let changed = false;
317
+ for (const provider of Object.values(config.model_providers)) {
318
+ if (!isTomlTable(provider))
319
+ continue;
320
+ provider.request_max_retries = maxRetries;
321
+ provider.stream_max_retries = maxRetries;
322
+ provider.stream_idle_timeout_ms = Math.trunc(timeoutSeconds * 1_000);
323
+ changed = true;
324
+ }
325
+ if (changed)
326
+ writeCodexConfig(configPath, config);
327
+ }
328
+ /** Preserve the user's private-home base instructions and append only the raw
329
+ * Agent-authored text. Repeated launches replace the managed suffix instead of
330
+ * accumulating it. Invalid user TOML is intentionally left untouched. */
331
+ export function syncCodexDeveloperInstructions(codexHome, instructions) {
332
+ mkdirSync(codexHome, { recursive: true, mode: 0o700 });
333
+ const configPath = join(codexHome, "config.toml");
334
+ if (pathExistsOrIsSymlink(configPath) && lstatSync(configPath).isSymbolicLink()) {
335
+ try {
336
+ const source = realpathSync(configPath);
337
+ const content = readFileSync(source, "utf8");
338
+ rmSync(configPath, { force: true });
339
+ writeFileSync(configPath, content, { encoding: "utf8", mode: 0o600 });
340
+ }
341
+ catch {
342
+ return;
343
+ }
344
+ }
345
+ let config;
346
+ try {
347
+ config = existsSync(configPath) ? parse(readFileSync(configPath, "utf8")) : {};
348
+ }
349
+ catch {
350
+ return;
351
+ }
352
+ const current = config.developer_instructions;
353
+ if (current !== undefined && typeof current !== "string")
354
+ return;
355
+ const addition = instructions?.trim() ?? "";
356
+ const basePath = join(codexHome, DEVELOPER_INSTRUCTIONS_BASE_FILE);
357
+ let base;
358
+ if (existsSync(basePath)) {
359
+ base = readFileSync(basePath, "utf8");
360
+ }
361
+ else {
362
+ base = typeof current === "string" ? current.trim() : "";
363
+ if (addition && base === addition) {
364
+ base = "";
365
+ }
366
+ else if (addition && base.endsWith(`\n\n${addition}`)) {
367
+ base = base.slice(0, -(`\n\n${addition}`).length).trimEnd();
368
+ }
369
+ writeFileSync(basePath, base, { encoding: "utf8", mode: 0o600 });
370
+ }
371
+ const active = base && addition ? `${base}\n\n${addition}` : base || addition;
372
+ if (active)
373
+ config.developer_instructions = active;
374
+ else
375
+ delete config.developer_instructions;
376
+ writeCodexConfig(configPath, config);
377
+ }
378
+ /** Read the active private-home instructions without conflating a definite
379
+ * absence with malformed or temporarily unreadable config. */
380
+ export function readCodexDeveloperInstructionsState(codexHome) {
381
+ const configPath = join(codexHome, "config.toml");
382
+ let source;
383
+ try {
384
+ source = readFileSync(configPath, "utf8");
385
+ }
386
+ catch (error) {
387
+ const code = error.code;
388
+ return code === "ENOENT" || code === "ENOTDIR"
389
+ ? { state: "absent" }
390
+ : { state: "unreadable" };
391
+ }
392
+ let config;
393
+ try {
394
+ config = parse(source);
395
+ }
396
+ catch {
397
+ return { state: "unreadable" };
398
+ }
399
+ if (!Object.prototype.hasOwnProperty.call(config, "developer_instructions")) {
400
+ return { state: "absent" };
401
+ }
402
+ const instructions = config.developer_instructions;
403
+ return typeof instructions === "string" && instructions.trim()
404
+ ? { state: "present", value: instructions }
405
+ : { state: "unreadable" };
406
+ }
407
+ function writeCodexConfig(configPath, config) {
408
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
409
+ const staged = join(dirname(configPath), `.config.toml.${process.pid}.${randomUUID()}`);
410
+ try {
411
+ writeFileSync(staged, stringify(config), { encoding: "utf8", mode: 0o600 });
412
+ renameSync(staged, configPath);
413
+ }
414
+ finally {
415
+ rmSync(staged, { force: true });
416
+ }
417
+ }
111
418
  /** Persist immutable skill copies in a native Provider home. Fork descendants
112
419
  * share that home, so entries must outlive every runner's temporary sources. */
113
420
  export function populateCodexSkills(codexHome, skills) {
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type LiveSessionFailure, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
1
+ import { type ResolvedExecutionSnapshot, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type LiveSessionFailure, 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";
@@ -107,14 +107,6 @@ export interface LiveSessionOpts {
107
107
  execution: ResolvedExecutionSnapshot;
108
108
  retargetMirror?: RetargetMirror;
109
109
  }
110
- /**
111
- * The `OPENAI_*` retry env for a codex-lineage app-server spawn, or `undefined`
112
- * when the budget declares no retry or the runtime is claude. claude applies
113
- * its retry budget per-run in `buildOptions` (see the claude executor), so it
114
- * never needs a dedicated app-server.
115
- */
116
- export declare function codexRetryEnv(runtime: AgentRuntimeId, budget?: ResolvedExecutionBudget): Record<string, string> | undefined;
117
- export declare function codexBackendKey(runtime: AgentRuntimeId, budget?: ResolvedExecutionBudget): string;
118
110
  export declare class LocalAgentHost implements CodexCapabilities {
119
111
  private readonly config;
120
112
  private readonly sessionStore;
@@ -122,8 +114,6 @@ export declare class LocalAgentHost implements CodexCapabilities {
122
114
  private readonly defaultRuntime;
123
115
  private readonly injectedCommandRunner?;
124
116
  private readonly injectedAppServerClient?;
125
- private readonly clock;
126
- private readonly backendIdleTtlMs;
127
117
  private readonly forwarderClientFactory;
128
118
  private readonly preloadClientFactory;
129
119
  /** Builds one short-lived message/interrupt client. */
@@ -149,16 +139,12 @@ export declare class LocalAgentHost implements CodexCapabilities {
149
139
  /** Short-lived dedupe for managed fork notifications delivered after the
150
140
  * `thread/fork` response. Values are expected source Provider thread ids. */
151
141
  private readonly managedForkThreadStarts;
152
- constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, preloadClientFactory, injectionClientFactory, sessionId, runtimeHomeSessionId, }: {
142
+ constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, forwarderClientFactory, preloadClientFactory, injectionClientFactory, sessionId, runtimeHomeSessionId, }: {
153
143
  config: AppConfig;
154
144
  commandRunner?: CodexCommandRunner;
155
145
  sessionStore?: CodexSessionStore;
156
146
  allowedRoots?: string[];
157
147
  appServerClient?: CodexAppServerClient | null;
158
- /** Monotonic-ish clock for idle reaping (tests inject a controllable one). */
159
- now?: () => number;
160
- /** Idle TTL after which a per-budget composite backend is reaped (ms). */
161
- backendIdleTtlMs?: number;
162
148
  /** Override the live-session forwarder connection factory (tests inject a fake). */
163
149
  forwarderClientFactory?: (appServerUrl: string) => CodexAppServerClient;
164
150
  /** Override the short-lived known-thread preload connection factory. */
@@ -176,14 +162,6 @@ export declare class LocalAgentHost implements CodexCapabilities {
176
162
  runtimeHomeSessionId?: string;
177
163
  });
178
164
  private getBackend;
179
- /**
180
- * Stop + drop per-budget composite backends that have been idle longer than
181
- * the TTL and have no in-flight run. Base per-runtime backends (key === the
182
- * bare runtime id) are never reaped — they live for the process lifetime, as
183
- * before. There are no per-conversation close hooks, so this lazy sweep (run
184
- * on every `getBackend`) is how dedicated app-servers get reclaimed.
185
- */
186
- private reapIdleBackends;
187
165
  private createBackend;
188
166
  /** This session's private native home, shared by its app-server + TUI. */
189
167
  private runtimeHome;