@rynx-ai/runtime 0.1.0 → 0.1.9

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 (53) hide show
  1. package/dist/claude/executor.d.ts +3 -5
  2. package/dist/claude/executor.js +3 -5
  3. package/dist/claude/native-bridge.d.ts +74 -17
  4. package/dist/claude/native-bridge.js +225 -30
  5. package/dist/claude/native-hook-main.js +291 -39
  6. package/dist/claude/native-hooks.d.ts +3 -2
  7. package/dist/claude/native-hooks.js +15 -6
  8. package/dist/claude/native-integration.d.ts +78 -5
  9. package/dist/claude/native-integration.js +417 -26
  10. package/dist/claude/settings.d.ts +8 -0
  11. package/dist/claude/settings.js +50 -0
  12. package/dist/claude/transcript.d.ts +2 -2
  13. package/dist/claude/transcript.js +3 -3
  14. package/dist/codex/rollout-synth.js +1 -1
  15. package/dist/codex-app-server/client.d.ts +26 -40
  16. package/dist/codex-app-server/client.js +1128 -99
  17. package/dist/codex-app-server/forwarder.d.ts +7 -7
  18. package/dist/codex-app-server/forwarder.js +11 -5
  19. package/dist/codex-app-server/mapping.d.ts +1 -1
  20. package/dist/codex-app-server/mapping.js +27 -2
  21. package/dist/codex-app-server/protocol.d.ts +238 -4
  22. package/dist/codex-app-server/transport.d.ts +20 -5
  23. package/dist/codex-app-server/transport.js +93 -40
  24. package/dist/codex-app-server/ws-channel.d.ts +3 -3
  25. package/dist/codex-app-server/ws-channel.js +23 -7
  26. package/dist/codex-child-env.js +33 -0
  27. package/dist/codex-home.d.ts +6 -6
  28. package/dist/codex-home.js +8 -9
  29. package/dist/codex-session-store.d.ts +2 -1
  30. package/dist/host.d.ts +34 -33
  31. package/dist/host.js +531 -91
  32. package/dist/index.d.ts +4 -3
  33. package/dist/index.js +1 -1
  34. package/dist/interactions.d.ts +61 -0
  35. package/dist/interactions.js +236 -0
  36. package/dist/models-catalog.d.ts +1 -1
  37. package/dist/models-catalog.js +1 -1
  38. package/dist/runner/child.d.ts +9 -1
  39. package/dist/runner/child.js +93 -15
  40. package/dist/runner/manager.d.ts +59 -10
  41. package/dist/runner/manager.js +385 -41
  42. package/dist/runner/protocol.d.ts +18 -7
  43. package/dist/runner-main.js +9 -6
  44. package/dist/runtime-status.js +1 -1
  45. package/dist/terminal/claude-tui.d.ts +8 -3
  46. package/dist/terminal/claude-tui.js +6 -2
  47. package/dist/terminal/codex-tui.d.ts +3 -3
  48. package/dist/terminal/codex-tui.js +1 -1
  49. package/dist/terminal/registry.d.ts +1 -1
  50. package/dist/terminal/registry.js +1 -1
  51. package/dist/terminal/tmux.d.ts +6 -6
  52. package/dist/terminal/tmux.js +10 -10
  53. package/package.json +3 -3
@@ -67,6 +67,9 @@ export class CodexTransportError extends Error {
67
67
  this.data = data;
68
68
  }
69
69
  }
70
+ /** A server request was resolved by another app-server client. In that case the
71
+ * local handler must settle without sending a second JSON-RPC response. */
72
+ export const NO_SERVER_RESPONSE = Symbol("NO_SERVER_RESPONSE");
70
73
  /**
71
74
  * Wraps the Codex `app-server` subprocess and exposes a typed JSON-RPC API
72
75
  * over its stdio. Frames are line-delimited NDJSON (no Content-Length
@@ -88,6 +91,7 @@ export class CodexAppServerTransport extends EventEmitter {
88
91
  logger;
89
92
  onServerRequest;
90
93
  onNotification;
94
+ onServerRequestResponseDelivery;
91
95
  child = null;
92
96
  rl = null;
93
97
  stderrRl = null;
@@ -95,7 +99,7 @@ export class CodexAppServerTransport extends EventEmitter {
95
99
  pending = new Map();
96
100
  startPromise = null;
97
101
  exited = false;
98
- constructor({ spawner, channel, logger = defaultLogger, onServerRequest, onNotification, }) {
102
+ constructor({ spawner, channel, logger = defaultLogger, onServerRequest, onNotification, onServerRequestResponseDelivery, }) {
99
103
  super();
100
104
  if (!spawner && !channel) {
101
105
  throw new Error("CodexAppServerTransport requires a spawner or a channel");
@@ -105,6 +109,7 @@ export class CodexAppServerTransport extends EventEmitter {
105
109
  this.logger = logger;
106
110
  this.onServerRequest = onServerRequest;
107
111
  this.onNotification = onNotification;
112
+ this.onServerRequestResponseDelivery = onServerRequestResponseDelivery;
108
113
  }
109
114
  emit(event, ...args) {
110
115
  return super.emit(event, ...args);
@@ -145,39 +150,46 @@ export class CodexAppServerTransport extends EventEmitter {
145
150
  async sendRequest(method, params) {
146
151
  await this.ensureStarted();
147
152
  const id = this.nextId++;
148
- return await new Promise((resolve, reject) => {
153
+ const envelope = {
154
+ jsonrpc: "2.0",
155
+ id,
156
+ method,
157
+ ...(params === undefined ? {} : { params }),
158
+ };
159
+ let rejectResponse;
160
+ const response = new Promise((resolve, reject) => {
161
+ rejectResponse = reject;
149
162
  this.pending.set(id, {
150
163
  resolve: resolve,
151
164
  reject,
152
165
  method,
153
166
  });
154
- const envelope = {
155
- jsonrpc: "2.0",
156
- id,
157
- method,
158
- ...(params === undefined ? {} : { params }),
159
- };
160
- try {
161
- this.writeLine(envelope);
162
- }
163
- catch (error) {
164
- this.pending.delete(id);
165
- reject(error instanceof Error ? error : new Error(String(error)));
166
- }
167
167
  });
168
+ try {
169
+ await this.writeLine(envelope);
170
+ }
171
+ catch (error) {
172
+ this.pending.delete(id);
173
+ rejectResponse(error instanceof Error ? error : new Error(String(error)));
174
+ }
175
+ return await response;
168
176
  }
169
- sendNotification(method, params) {
177
+ async sendNotification(method, params) {
170
178
  const envelope = {
171
179
  jsonrpc: "2.0",
172
180
  method,
173
181
  ...(params === undefined ? {} : { params }),
174
182
  };
175
- this.writeLine(envelope);
183
+ await this.writeLine(envelope);
176
184
  }
177
185
  async stop(signal = "SIGTERM") {
178
186
  if (this.channel) {
179
- this.exited = true;
180
187
  await this.channel.stop(signal);
188
+ // Channel implementations may report close asynchronously (or not at all).
189
+ // Run the same exit path synchronously so pending RPCs reject and clients
190
+ // clear pending native interactions before stop() resolves.
191
+ if (!this.exited)
192
+ this.handleExit(null, signal, null);
181
193
  return;
182
194
  }
183
195
  const child = this.child;
@@ -268,25 +280,29 @@ export class CodexAppServerTransport extends EventEmitter {
268
280
  });
269
281
  this.emit("exit", code, signal);
270
282
  }
271
- writeLine(envelope) {
283
+ async writeLine(envelope) {
272
284
  const line = `${JSON.stringify(envelope)}\n`;
273
285
  if (this.channel) {
274
286
  if (this.exited) {
275
287
  throw new CodexTransportError("Codex app-server is not running");
276
288
  }
277
- this.channel.send(line);
289
+ await this.channel.send(line);
278
290
  return;
279
291
  }
280
292
  const child = this.child;
281
293
  if (!child || this.exited) {
282
294
  throw new CodexTransportError("Codex app-server is not running");
283
295
  }
284
- const ok = child.stdin.write(line);
285
- if (!ok) {
286
- // Don't block — back-pressure on stdin is unlikely with line-sized
287
- // messages, but we surface it as a log entry if it ever shows up.
288
- this.logger.log({ event: "transport.stdin_backpressure" });
289
- }
296
+ await new Promise((resolve, reject) => {
297
+ const ok = child.stdin.write(line, (error) => {
298
+ if (error)
299
+ reject(error);
300
+ else
301
+ resolve();
302
+ });
303
+ if (!ok)
304
+ this.logger.log({ event: "transport.stdin_backpressure" });
305
+ });
290
306
  }
291
307
  handleLine(line) {
292
308
  if (!line.trim()) {
@@ -352,34 +368,71 @@ export class CodexAppServerTransport extends EventEmitter {
352
368
  }
353
369
  async handleServerRequest(envelope) {
354
370
  if (!this.onServerRequest) {
355
- this.respondToServerRequest(envelope.id, undefined, {
356
- code: -32601,
357
- message: `No handler registered for ${envelope.method}`,
358
- });
371
+ try {
372
+ await this.respondToServerRequest(envelope.id, undefined, {
373
+ code: -32601,
374
+ message: `No handler registered for ${envelope.method}`,
375
+ });
376
+ }
377
+ catch (error) {
378
+ this.logger.log({
379
+ event: "transport.server_request_error_write_failed",
380
+ error: error instanceof Error ? error.message : String(error),
381
+ });
382
+ }
359
383
  return;
360
384
  }
385
+ let result;
361
386
  try {
362
- const result = await this.onServerRequest(envelope.method, envelope.params);
363
- this.respondToServerRequest(envelope.id, result, null);
387
+ result = await this.onServerRequest(envelope.method, envelope.params, envelope.id);
364
388
  }
365
389
  catch (error) {
366
- this.respondToServerRequest(envelope.id, undefined, {
367
- code: -32000,
368
- message: error instanceof Error ? error.message : String(error),
390
+ try {
391
+ await this.respondToServerRequest(envelope.id, undefined, {
392
+ code: -32000,
393
+ message: error instanceof Error ? error.message : String(error),
394
+ });
395
+ }
396
+ catch (writeError) {
397
+ this.logger.log({
398
+ event: "transport.server_request_error_write_failed",
399
+ error: writeError instanceof Error ? writeError.message : String(writeError),
400
+ });
401
+ }
402
+ return;
403
+ }
404
+ if (result === NO_SERVER_RESPONSE)
405
+ return;
406
+ try {
407
+ await this.respondToServerRequest(envelope.id, result, null);
408
+ this.notifyServerRequestResponseDelivery(envelope.id, { delivered: true });
409
+ }
410
+ catch (writeError) {
411
+ const error = writeError instanceof Error ? writeError : new Error(String(writeError));
412
+ this.logger.log({
413
+ event: "transport.server_request_write_failed",
414
+ error: error.message,
415
+ });
416
+ this.notifyServerRequestResponseDelivery(envelope.id, {
417
+ delivered: false,
418
+ error,
369
419
  });
370
420
  }
371
421
  }
372
- respondToServerRequest(id, result, error) {
422
+ async respondToServerRequest(id, result, error) {
373
423
  const envelope = error
374
424
  ? { jsonrpc: "2.0", id, error }
375
425
  : { jsonrpc: "2.0", id, result };
426
+ await this.writeLine(envelope);
427
+ }
428
+ notifyServerRequestResponseDelivery(requestId, result) {
376
429
  try {
377
- this.writeLine(envelope);
430
+ this.onServerRequestResponseDelivery?.(requestId, result);
378
431
  }
379
- catch (writeError) {
432
+ catch (error) {
380
433
  this.logger.log({
381
- event: "transport.server_request_write_failed",
382
- error: writeError instanceof Error ? writeError.message : String(writeError),
434
+ event: "transport.server_request_delivery_handler_failed",
435
+ error: error instanceof Error ? error.message : String(error),
383
436
  });
384
437
  }
385
438
  }
@@ -28,7 +28,7 @@ export declare class WsRpcChannel implements RpcChannel {
28
28
  }) => void): void;
29
29
  isOpen(): boolean;
30
30
  start(): Promise<void>;
31
- send(line: string): void;
31
+ send(line: string): Promise<void>;
32
32
  stop(signal?: NodeJS.Signals): Promise<void>;
33
33
  private emitClose;
34
34
  private connectWithRetry;
@@ -37,7 +37,7 @@ export declare class WsRpcChannel implements RpcChannel {
37
37
  /**
38
38
  * Connect-only {@link RpcChannel}: attaches an ADDITIONAL client to an app-server
39
39
  * someone else already started (a {@link WsRpcChannel}'s `url`) — no spawn. This
40
- * is how rynx runs omnigent's multi-connection codex-native model: the backend
40
+ * is how rynx runs reference implementation's multi-connection codex-native model: the backend
41
41
  * client owns the app-server + drives injection, while a SEPARATE forwarder
42
42
  * connection `thread/resume`s the same thread to subscribe to its item/turn
43
43
  * notifications (verified: codex delivers a thread's items to every connection
@@ -65,7 +65,7 @@ export declare class ExternalWsChannel implements RpcChannel {
65
65
  }) => void): void;
66
66
  isOpen(): boolean;
67
67
  start(): Promise<void>;
68
- send(line: string): void;
68
+ send(line: string): Promise<void>;
69
69
  stop(): Promise<void>;
70
70
  private emitClose;
71
71
  private connect;
@@ -2,13 +2,13 @@
2
2
  * WebSocket {@link RpcChannel} for a codex `app-server --listen ws://IP:PORT`.
3
3
  *
4
4
  * Why a WebSocket and not the default stdio: stdio admits exactly one client
5
- * (the process that spawned it), so it cannot be co-driven. omnigent's working
5
+ * (the process that spawned it), so it cannot be co-driven. reference implementation's working
6
6
  * codex-native uses a multi-client transport (ws / uds) so a separate `codex
7
7
  * --remote` TUI can attach to the SAME app-server and resume the SAME thread.
8
8
  * This channel owns that app-server child on a loopback ws port and connects a
9
9
  * client to it; the TUI attaches to {@link WsRpcChannel.url}.
10
10
  *
11
- * Framing matches codex (verified from omnigent): one JSON-RPC object per
11
+ * Framing matches codex (verified from reference implementation): one JSON-RPC object per
12
12
  * WebSocket text frame — no newline delimiting. So `send` writes one frame per
13
13
  * message and every inbound frame is one complete JSON object.
14
14
  */
@@ -69,12 +69,20 @@ export class WsRpcChannel {
69
69
  this.ws.on("close", (code) => this.emitClose(code ?? null, null, null));
70
70
  this.ws.on("error", (error) => this.emitClose(null, null, error));
71
71
  }
72
- send(line) {
72
+ async send(line) {
73
73
  if (this.ws?.readyState !== WebSocket.OPEN) {
74
74
  throw new Error("codex ws channel is not open");
75
75
  }
76
76
  // Codex expects one JSON object per frame; drop the NDJSON newline.
77
- this.ws.send(line.endsWith("\n") ? line.slice(0, -1) : line);
77
+ const payload = line.endsWith("\n") ? line.slice(0, -1) : line;
78
+ await new Promise((resolve, reject) => {
79
+ this.ws.send(payload, (error) => {
80
+ if (error)
81
+ reject(error);
82
+ else
83
+ resolve();
84
+ });
85
+ });
78
86
  }
79
87
  async stop(signal = "SIGTERM") {
80
88
  try {
@@ -140,7 +148,7 @@ export class WsRpcChannel {
140
148
  /**
141
149
  * Connect-only {@link RpcChannel}: attaches an ADDITIONAL client to an app-server
142
150
  * someone else already started (a {@link WsRpcChannel}'s `url`) — no spawn. This
143
- * is how rynx runs omnigent's multi-connection codex-native model: the backend
151
+ * is how rynx runs reference implementation's multi-connection codex-native model: the backend
144
152
  * client owns the app-server + drives injection, while a SEPARATE forwarder
145
153
  * connection `thread/resume`s the same thread to subscribe to its item/turn
146
154
  * notifications (verified: codex delivers a thread's items to every connection
@@ -189,11 +197,19 @@ export class ExternalWsChannel {
189
197
  this.ws.on("close", (code) => this.emitClose(code ?? null, null, null));
190
198
  this.ws.on("error", (error) => this.emitClose(null, null, error));
191
199
  }
192
- send(line) {
200
+ async send(line) {
193
201
  if (this.ws?.readyState !== WebSocket.OPEN) {
194
202
  throw new Error("external codex ws channel is not open");
195
203
  }
196
- this.ws.send(line.endsWith("\n") ? line.slice(0, -1) : line);
204
+ const payload = line.endsWith("\n") ? line.slice(0, -1) : line;
205
+ await new Promise((resolve, reject) => {
206
+ this.ws.send(payload, (error) => {
207
+ if (error)
208
+ reject(error);
209
+ else
210
+ resolve();
211
+ });
212
+ });
197
213
  }
198
214
  async stop() {
199
215
  try {
@@ -6,6 +6,14 @@ const CODEX_CHILD_ENV_ALLOWLIST = [
6
6
  "TMPDIR",
7
7
  "CODEX_HOME",
8
8
  "TRAE_HOME",
9
+ // Keep Runtime-local CLIs on the same resident daemon when its state root is
10
+ // configured outside the default ~/.rynx location.
11
+ "RYNX_HOME",
12
+ // RunnerManager supplies this rotation-safe handle for the exact managed
13
+ // Session. Codex command executions need it to call their own Runtime
14
+ // Browser; the raw Browser capability and internal runner key stay outside
15
+ // the allowlist.
16
+ "RYNX_BROWSER_CONTEXT_FILE",
9
17
  "HTTP_PROXY",
10
18
  "HTTPS_PROXY",
11
19
  "ALL_PROXY",
@@ -15,6 +23,24 @@ const CODEX_CHILD_ENV_ALLOWLIST = [
15
23
  "all_proxy",
16
24
  "no_proxy",
17
25
  ];
26
+ const PROXY_ENV_KEYS = [
27
+ "HTTP_PROXY",
28
+ "HTTPS_PROXY",
29
+ "ALL_PROXY",
30
+ "http_proxy",
31
+ "https_proxy",
32
+ "all_proxy",
33
+ ];
34
+ const LOOPBACK_NO_PROXY_HOSTS = ["localhost", "127.0.0.1", "::1"];
35
+ function withLoopbackNoProxy(value) {
36
+ const entries = value?.split(",").map((entry) => entry.trim()).filter(Boolean) ?? [];
37
+ const seen = new Set(entries.map((entry) => entry.toLowerCase()));
38
+ for (const host of LOOPBACK_NO_PROXY_HOSTS) {
39
+ if (!seen.has(host))
40
+ entries.push(host);
41
+ }
42
+ return entries.join(",");
43
+ }
18
44
  export function createCodexChildEnv(env) {
19
45
  const childEnv = {};
20
46
  for (const key of CODEX_CHILD_ENV_ALLOWLIST) {
@@ -23,5 +49,12 @@ export function createCodexChildEnv(env) {
23
49
  childEnv[key] = value;
24
50
  }
25
51
  }
52
+ if (childEnv.NO_PROXY || childEnv.no_proxy || PROXY_ENV_KEYS.some((key) => childEnv[key])) {
53
+ const upper = childEnv.NO_PROXY ?? childEnv.no_proxy;
54
+ const lower = childEnv.no_proxy ?? childEnv.NO_PROXY;
55
+ // CIDR matching in NO_PROXY varies; Codex's WebSocket client needs exact loopback hosts.
56
+ childEnv.NO_PROXY = withLoopbackNoProxy(upper);
57
+ childEnv.no_proxy = withLoopbackNoProxy(lower);
58
+ }
26
59
  return childEnv;
27
60
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The deterministic private CODEX_HOME path for a rynx session. PER-SESSION
3
- * (uid-scoped + `sha256(sessionId)[:32]`), mirroring omnigent's per-session
3
+ * (uid-scoped + `sha256(sessionId)[:32]`), mirroring reference implementation's per-session
4
4
  * `bridge_dir/codex-home` and rynx's own `claudeBridgeDir`. Pure — computes the
5
5
  * path without touching disk, so the daemon (rollout-synth) and the runner child
6
6
  * (app-server) both locate the SAME session's home from `sessionId` alone.
@@ -14,17 +14,17 @@ export declare function legacyCodexHomePath(): string;
14
14
  * `auth.json`) and settings (copied `config.toml`) but NOT the real home's pending
15
15
  * update / first-run (NUX) state — so a co-driven `codex` app-server + `--remote`
16
16
  * TUI never block on an "Update now / Press enter to continue" prompt (which would
17
- * wedge terminal injection). Ports omnigent's `_CODEX_HOME_SYMLINK_FILES` /
17
+ * wedge terminal injection). Ports reference implementation's `_CODEX_HOME_SYMLINK_FILES` /
18
18
  * `_CODEX_HOME_COPY_FILES`.
19
19
  *
20
- * PER-SESSION: one private home per rynx session (matching omnigent), so concurrent
20
+ * PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
21
21
  * agents never share a `skills/` dir. Idempotent: the symlink/copy are refreshed each
22
22
  * call so a re-login/config change propagates. Returns the private home dir.
23
23
  */
24
24
  export declare function prepareCodexHome(sessionId: string, realHome?: string): string;
25
25
  /**
26
26
  * Link a resolved skill set into `<codexHome>/skills/<name>/` so the native Codex
27
- * discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism omnigent
27
+ * discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism reference implementation
28
28
  * uses (`populate_codex_skills_from_bundle` → `_populate_codex_skills`), NOT a
29
29
  * `<skills_instructions>` block injected into the prompt. Codex's app-server
30
30
  * watches this dir and re-scans on change, so a live `codex --remote` TUI — whose
@@ -32,9 +32,9 @@ export declare function prepareCodexHome(sessionId: string, realHome?: string):
32
32
  * — still sees the agent's skills.
33
33
  *
34
34
  * Each skill is a symlink to its source dir; a filesystem without symlink support
35
- * falls back to a recursive copy (matches omnigent's fallback).
35
+ * falls back to a recursive copy (matches reference implementation's fallback).
36
36
  *
37
- * omnigent boots a fresh per-session CODEX_HOME, so it only ever links into an
37
+ * reference implementation boots a fresh per-session CODEX_HOME, so it only ever links into an
38
38
  * empty dir. rynx shares ONE private home per runtime (see {@link prepareCodexHome}),
39
39
  * so this CONVERGES the dir to `skills`: it links the missing ones and removes
40
40
  * entries no longer selected, honouring the agent spec's gating. (Concurrent
@@ -17,7 +17,7 @@ function rynxUidRoot() {
17
17
  }
18
18
  /**
19
19
  * The deterministic private CODEX_HOME path for a rynx session. PER-SESSION
20
- * (uid-scoped + `sha256(sessionId)[:32]`), mirroring omnigent's per-session
20
+ * (uid-scoped + `sha256(sessionId)[:32]`), mirroring reference implementation's per-session
21
21
  * `bridge_dir/codex-home` and rynx's own `claudeBridgeDir`. Pure — computes the
22
22
  * path without touching disk, so the daemon (rollout-synth) and the runner child
23
23
  * (app-server) both locate the SAME session's home from `sessionId` alone.
@@ -36,10 +36,10 @@ export function legacyCodexHomePath() {
36
36
  * `auth.json`) and settings (copied `config.toml`) but NOT the real home's pending
37
37
  * update / first-run (NUX) state — so a co-driven `codex` app-server + `--remote`
38
38
  * TUI never block on an "Update now / Press enter to continue" prompt (which would
39
- * wedge terminal injection). Ports omnigent's `_CODEX_HOME_SYMLINK_FILES` /
39
+ * wedge terminal injection). Ports reference implementation's `_CODEX_HOME_SYMLINK_FILES` /
40
40
  * `_CODEX_HOME_COPY_FILES`.
41
41
  *
42
- * PER-SESSION: one private home per rynx session (matching omnigent), so concurrent
42
+ * PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
43
43
  * agents never share a `skills/` dir. Idempotent: the symlink/copy are refreshed each
44
44
  * call so a re-login/config change propagates. Returns the private home dir.
45
45
  */
@@ -79,7 +79,7 @@ export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
79
79
  }
80
80
  /**
81
81
  * Link a resolved skill set into `<codexHome>/skills/<name>/` so the native Codex
82
- * discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism omnigent
82
+ * discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism reference implementation
83
83
  * uses (`populate_codex_skills_from_bundle` → `_populate_codex_skills`), NOT a
84
84
  * `<skills_instructions>` block injected into the prompt. Codex's app-server
85
85
  * watches this dir and re-scans on change, so a live `codex --remote` TUI — whose
@@ -87,9 +87,9 @@ export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
87
87
  * — still sees the agent's skills.
88
88
  *
89
89
  * Each skill is a symlink to its source dir; a filesystem without symlink support
90
- * falls back to a recursive copy (matches omnigent's fallback).
90
+ * falls back to a recursive copy (matches reference implementation's fallback).
91
91
  *
92
- * omnigent boots a fresh per-session CODEX_HOME, so it only ever links into an
92
+ * reference implementation boots a fresh per-session CODEX_HOME, so it only ever links into an
93
93
  * empty dir. rynx shares ONE private home per runtime (see {@link prepareCodexHome}),
94
94
  * so this CONVERGES the dir to `skills`: it links the missing ones and removes
95
95
  * entries no longer selected, honouring the agent spec's gating. (Concurrent
@@ -99,7 +99,7 @@ export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
99
99
  export function populateCodexSkills(codexHome, skills) {
100
100
  const skillsDir = join(codexHome, "skills");
101
101
  const want = new Map(skills.map((s) => [s.name, s.dir]));
102
- // Converge: drop entries no longer selected (shared-home adaptation; omnigent's
102
+ // Converge: drop entries no longer selected (shared-home adaptation; reference implementation's
103
103
  // per-session home never needs this). `.system` holds codex's own embedded system
104
104
  // skills (the app-server installs them into `$CODEX_HOME/skills/.system`) — never
105
105
  // rynx-managed, so leave it untouched; only converge the entries we linked.
@@ -116,8 +116,7 @@ export function populateCodexSkills(codexHome, skills) {
116
116
  mkdirSync(skillsDir, { recursive: true });
117
117
  for (const [name, src] of want) {
118
118
  const link = join(skillsDir, name);
119
- if (existsSync(link))
120
- continue; // already linked (a skill's dir for a name is stable)
119
+ rmSync(link, { recursive: true, force: true });
121
120
  try {
122
121
  symlinkSync(src, link);
123
122
  }
@@ -1,10 +1,11 @@
1
- import { type AgentRuntimeId } from "@rynx-ai/core";
1
+ import { type AgentRuntimeId, type ReasoningEffort } from "@rynx-ai/core";
2
2
  import type { AppConfig } from "@rynx-ai/core";
3
3
  export interface CodexSessionRecord {
4
4
  localThreadId: string;
5
5
  codexSessionId: string;
6
6
  cwd: string;
7
7
  model: string;
8
+ reasoningEffort?: ReasoningEffort;
8
9
  /** Runtime this thread is bound to. Legacy records backfill to `codex`. */
9
10
  runtime: AgentRuntimeId;
10
11
  /** Declarative agent spec name bound to this thread, if any. */