@lyupro/codex-bridge 0.5.3 → 0.5.4

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.
@@ -2,15 +2,9 @@
2
2
  * The half the caller can kill: every preparation and every refusal that costs no quota,
3
3
  * then the spawn of the worker and the immediate return; a repeated call waits for the reply.
4
4
  *
5
- * The order it leaves for the worker is worker.json in the run folder — after the split
6
- * that file is the only connection between the two halves. It is written here with
7
- * `agent`, `slug`, `order_id`, `repo`, `is_git_repo`, `launcher_pid`, `budget_minutes`, `scope_new`
8
- * and `args`;
9
- * worker.mjs reads `repo`, `agent`, `args`, `is_git_repo` and `budget_minutes`. The extra fields
10
- * are deliberate: `slug`, `order_id`, `launcher_pid` and `scope_new` are what a run folder read
11
- * back months later needs in order to explain itself — which order it belonged to and which new
12
- * paths it declared included. Nothing may be dropped from this shape without changing worker.mjs
13
- * and saying so out loud.
5
+ * The order it leaves for the worker is worker.json in the run folder — after the split that file
6
+ * is the only connection between the two halves. Its shape and the reason for every field live in
7
+ * worker-order.mjs, which writes it.
14
8
  */
15
9
  import fs from 'node:fs';
16
10
  import path from 'node:path';
@@ -37,7 +31,9 @@ import { continuationRefusal } from './continuation.mjs';
37
31
  import { SCHEMAS } from './schemas.mjs';
38
32
  import { INSTRUCTIONS } from './prompts.mjs';
39
33
  import { git, headSha, branchName, worktreeSnapshot, reviewScope } from './git-state.mjs';
40
- import { codexArgs, requireCodex, runMode, unsafeForCmd } from './codex-cmd.mjs';
34
+ import { codexArgs, runMode, runProfile } from './codex-args.mjs';
35
+ import { writeWorkerOrder } from './worker-order.mjs';
36
+ import { requireCodex, unsafeForCmd } from './codex-cmd.mjs';
41
37
  import { runsRoot } from './runs-root.mjs';
42
38
  import { resolveProjectRunsDir } from './project-dir.mjs';
43
39
  import { cleanupRetention } from '../retention.mjs';
@@ -344,26 +340,18 @@ export async function launcher(argv = process.argv.slice(2)) {
344
340
  console.log(reply);
345
341
  return 1;
346
342
  }
347
- fs.writeFileSync(
348
- path.join(runDir, 'worker.json'),
349
- `${JSON.stringify(
350
- {
351
- agent: opts.agent,
352
- slug: opts.slug,
353
- order_id: opts.orderId,
354
- repo: repoRoot,
355
- is_git_repo: isGitRepo,
356
- launcher_pid: process.pid,
357
- // The mode's wall-clock budget, resolved here and never re-read by the worker: a run
358
- // that consulted config.json twice could end up honouring two different limits.
359
- budget_minutes: RUN_ENV?.budgets?.[runMode(opts.agent)],
360
- scope_new: opts.scopeNewPatterns,
361
- args: codexArgv,
362
- },
363
- null,
364
- 2,
365
- )}\n`,
366
- );
343
+ writeWorkerOrder(runDir, {
344
+ agent: opts.agent,
345
+ slug: opts.slug,
346
+ orderId: opts.orderId,
347
+ repo: repoRoot,
348
+ isGitRepo,
349
+ launcherPid: process.pid,
350
+ budgetMinutes: RUN_ENV?.budgets?.[runMode(opts.agent)],
351
+ scopeNew: opts.scopeNewPatterns,
352
+ profile: runProfile({ ...opts, repo: repoRoot }),
353
+ args: codexArgv,
354
+ });
367
355
 
368
356
  // detached + unref + no stdio: the worker leaves the caller's process group, so a Ctrl+C
369
357
  // or a timeout kill aimed at the dispatcher's shell does not reach it, and it holds no
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The order the launcher leaves for the worker: worker.json in the run folder.
3
+ *
4
+ * After the launcher/worker split this file is the only connection between the two halves, and the
5
+ * worker never re-reads the CLI or the configuration — a run that consulted config.json twice could
6
+ * end up honouring two different limits. It carries `agent`, `slug`, `order_id`, `repo`,
7
+ * `is_git_repo`, `launcher_pid`, `budget_minutes`, `scope_new`, `profile` and `args`; worker.mjs
8
+ * reads `repo`, `agent`, `args`, `is_git_repo` and `budget_minutes`. The rest is deliberate: those
9
+ * fields are what a run folder read back months later needs in order to explain itself — which
10
+ * order it belonged to, which new paths it declared included, and which worker was actually
11
+ * ordered. Nothing may be dropped from this shape without changing worker.mjs and saying so out
12
+ * loud; `docs/worker-contract.md` is the document that says it.
13
+ *
14
+ * It lives beside launcher.mjs rather than inside it because writing this contract is its own job,
15
+ * and the launcher had grown past the size limit while the contract was buried in the middle of it.
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ export function workerOrder({
21
+ agent,
22
+ slug,
23
+ orderId,
24
+ repo,
25
+ isGitRepo,
26
+ launcherPid,
27
+ budgetMinutes,
28
+ scopeNew,
29
+ profile,
30
+ args,
31
+ }) {
32
+ return {
33
+ agent,
34
+ slug,
35
+ order_id: orderId,
36
+ repo,
37
+ is_git_repo: isGitRepo,
38
+ launcher_pid: launcherPid,
39
+ // The mode's wall-clock budget, resolved by the launcher and never re-read by the worker.
40
+ budget_minutes: budgetMinutes,
41
+ scope_new: scopeNew,
42
+ // Which worker was ordered, and where each half of that answer came from. Recorded separately
43
+ // from args because an absent flag says nothing: a run without `-m` looked exactly like a run
44
+ // whose configured model had never been read, which is how a pinned profile went unnoticed for
45
+ // three releases (2026-08-26).
46
+ profile,
47
+ args,
48
+ };
49
+ }
50
+
51
+ export function writeWorkerOrder(runDir, fields) {
52
+ fs.writeFileSync(
53
+ path.join(runDir, 'worker.json'),
54
+ `${JSON.stringify(workerOrder(fields), null, 2)}\n`,
55
+ );
56
+ }
@@ -28,7 +28,7 @@ import { splitRunChanges } from './meta/environment.mjs';
28
28
  import { readEvents } from './meta/events.mjs';
29
29
  import { writeStatus } from './meta/run-state.mjs';
30
30
  import { resolveStatus } from './meta/verdict.mjs';
31
- import { AGENTS, failReply, limitReply } from './meta/reply.mjs';
31
+ import { AGENTS, failReply, limitReply, withProfileRow } from './meta/reply.mjs';
32
32
 
33
33
  function readRunnerVersion() {
34
34
  try {
@@ -122,7 +122,12 @@ export function collect(runDir, agent, exitCode) {
122
122
  tokens: events.tokens,
123
123
  tokens_reported: events.tokens !== null,
124
124
  usage: events.usage,
125
- model: argValue('-m'),
125
+ // The ordered profile, straight from worker.json; null for runs made before it was recorded.
126
+ // `model` stays for readers that already know it, and now answers for a run that pinned
127
+ // nothing instead of going quiet: an absent -m used to be indistinguishable from a model
128
+ // the operator had configured and never received (2026-08-26).
129
+ profile: worker?.profile ?? null,
130
+ model: worker?.profile?.model || argValue('-m'),
126
131
  sandbox: argValue('--sandbox'),
127
132
  // Null for runs made before env.json existed: absence is not the same as "both off".
128
133
  env: readJson(path.join(runDir, 'env.json')),
@@ -143,7 +148,10 @@ export function collect(runDir, agent, exitCode) {
143
148
  file: (name) => path.join(runDir, name),
144
149
  };
145
150
  const reply =
146
- status === 'OK' ? cfg.reply(ctx) : status === 'LIMIT' ? limitReply(ctx, meta) : failReply(ctx, meta);
151
+ withProfileRow(
152
+ status === 'OK' ? cfg.reply(ctx) : status === 'LIMIT' ? limitReply(ctx, meta) : failReply(ctx, meta),
153
+ meta,
154
+ );
147
155
 
148
156
  return { meta, reply: reply.join('\n') };
149
157
  }