@triflux/remote 10.45.0 → 10.46.0

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.
@@ -23,7 +23,10 @@ import {
23
23
  } from "node:fs";
24
24
  import { dirname, join } from "node:path";
25
25
 
26
- import { normalizeCodexProfileName } from "../../scripts/lib/codex-profile-config.mjs";
26
+ import {
27
+ canonicalCodexProfileModel,
28
+ normalizeCodexProfileName,
29
+ } from "../../scripts/lib/codex-profile-config.mjs";
27
30
 
28
31
  export const STATES = Object.freeze({
29
32
  PLANNING: "PLANNING",
@@ -318,6 +321,11 @@ function normalizeEscalationChainEntry(entry, index, source) {
318
321
  const profile = entry.profile.trim();
319
322
  normalized.profile =
320
323
  entry.cli === "codex" ? normalizeEscalationProfile(profile) : profile;
324
+ // A remapped legacy profile must not keep reporting its old model.
325
+ if (normalized.profile !== profile) {
326
+ normalized.model =
327
+ canonicalCodexProfileModel(normalized.profile) ?? normalized.model;
328
+ }
321
329
  }
322
330
  return normalized;
323
331
  }
@@ -1,9 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import crypto from "node:crypto";
3
- import { mkdtemp, rm, stat } from "node:fs/promises";
3
+ import { mkdtemp, realpath, rm, stat } from "node:fs/promises";
4
4
  import net from "node:net";
5
5
  import { tmpdir } from "node:os";
6
- import { join as pathJoin } from "node:path";
6
+ import { join as pathJoin, resolve as pathResolve } from "node:path";
7
7
 
8
8
  import { execute as defaultExecuteCodex } from "@triflux/core/hub/codex-adapter.mjs";
9
9
  import {
@@ -23,6 +23,378 @@ const DEFAULT_TIMEOUT_MS = 90_000;
23
23
  const DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS = 120_000;
24
24
  const DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS = 12_000;
25
25
 
26
+ function codexClient(socketPath, timeoutMs, clientFactory) {
27
+ return clientFactory({ socketPath, connectTimeoutMs: timeoutMs });
28
+ }
29
+
30
+ async function initializeCodexClient(client, timeoutMs) {
31
+ await client.connect();
32
+ await client.request(
33
+ "initialize",
34
+ { clientInfo: { name: "triflux-tfx-live", version: "1.0.0" } },
35
+ timeoutMs,
36
+ );
37
+ client.notify("initialized", {});
38
+ }
39
+
40
+ async function runCodexAppServerTurn({
41
+ client,
42
+ threadId,
43
+ prompt,
44
+ timeoutMs,
45
+ maxTurnMs,
46
+ expectedTurnId,
47
+ }) {
48
+ const messages = new Map();
49
+ const pending = [];
50
+ let turnId = null;
51
+ let startedTurnId = null;
52
+ let retries = 0;
53
+ let settled = false;
54
+ let timer = null;
55
+ let resolveCompletion;
56
+ const completion = new Promise((resolve) => {
57
+ resolveCompletion = resolve;
58
+ });
59
+ const lifecycle = createActivityLifecycle({
60
+ enabled: isActivityLifecycleEnabled(),
61
+ interventionMs: timeoutMs,
62
+ hardCeilingMs: maxTurnMs,
63
+ });
64
+ const finish = (result) => {
65
+ if (settled) return;
66
+ settled = true;
67
+ resolveCompletion(result);
68
+ };
69
+ const matching = (params) =>
70
+ (!params?.threadId || params.threadId === threadId) &&
71
+ (!params?.turnId || params.turnId === turnId) &&
72
+ (!params?.turn?.id || params.turn.id === turnId);
73
+ const receive = (kind, params) => {
74
+ if (!turnId) {
75
+ pending.push([kind, params]);
76
+ return;
77
+ }
78
+ if (!matching(params)) return;
79
+ if (kind === "started" && params?.turn?.id === turnId) {
80
+ startedTurnId = turnId;
81
+ } else if (kind === "delta" && typeof params?.delta === "string") {
82
+ const itemId = params?.itemId || "__unidentified_agent_message__";
83
+ if (!messages.has(itemId))
84
+ messages.set(itemId, { text: "", phase: null });
85
+ messages.get(itemId).text += params.delta;
86
+ lifecycle.observe();
87
+ } else if (
88
+ kind === "itemCompleted" &&
89
+ params?.item?.type === "agentMessage"
90
+ ) {
91
+ const itemId = params.item.id;
92
+ if (itemId) {
93
+ if (!messages.has(itemId))
94
+ messages.set(itemId, { text: "", phase: null });
95
+ const message = messages.get(itemId);
96
+ if (typeof params.item.text === "string")
97
+ message.text = params.item.text;
98
+ message.phase = params.item.phase || null;
99
+ }
100
+ } else if (kind === "completed" && params?.turn?.id === turnId) {
101
+ finish({
102
+ status: params?.turn?.status || "unknown",
103
+ matchedCompletion: true,
104
+ });
105
+ } else if (kind === "error") {
106
+ if (params?.willRetry === true) {
107
+ retries++;
108
+ lifecycle.observe();
109
+ return;
110
+ }
111
+ finish({
112
+ status: "failed",
113
+ message:
114
+ params?.error?.message || params?.message || "codex app-server error",
115
+ matchedCompletion: false,
116
+ });
117
+ }
118
+ };
119
+ const offDelta = client.onNotification("item/agentMessage/delta", (p) =>
120
+ receive("delta", p),
121
+ );
122
+ const offItemCompleted = client.onNotification("item/completed", (p) =>
123
+ receive("itemCompleted", p),
124
+ );
125
+ const offStarted = client.onNotification("turn/started", (p) =>
126
+ receive("started", p),
127
+ );
128
+ const offDone = client.onNotification("turn/completed", (p) =>
129
+ receive("completed", p),
130
+ );
131
+ const offError = client.onNotification("error", (p) => receive("error", p));
132
+ const offActivity = client.onNotification("*", (params) => {
133
+ if (matching(params)) lifecycle.observe();
134
+ });
135
+ const armTimer = () => {
136
+ timer = setTimeout(async () => {
137
+ if (settled) return;
138
+ if (!isActivityLifecycleEnabled()) {
139
+ finish({ status: "timeout", matchedCompletion: false });
140
+ return;
141
+ }
142
+ const reason = await lifecycle.check();
143
+ if (settled) return;
144
+ if (reason)
145
+ finish({ status: "timeout", reason, matchedCompletion: false });
146
+ else armTimer();
147
+ }, timeoutMs);
148
+ };
149
+ armTimer();
150
+ try {
151
+ const input = [{ type: "text", text: prompt }];
152
+ const started = expectedTurnId
153
+ ? await client.request(
154
+ "turn/steer",
155
+ { threadId, input, expectedTurnId },
156
+ timeoutMs,
157
+ )
158
+ : await client.request("turn/start", { threadId, input }, timeoutMs);
159
+ turnId = started?.turn?.id || started?.turnId;
160
+ if (!turnId)
161
+ throw new Error("codex app-server turn/start returned no turn id");
162
+ for (const [kind, params] of pending) receive(kind, params);
163
+ const result = await completion;
164
+ const entries = [...messages.values()].map((item) => ({
165
+ ...item,
166
+ text: item.text.trim(),
167
+ }));
168
+ const finals = entries.filter((item) => item.phase === "final_answer");
169
+ const answer = finals.length ? finals : entries.slice(-1);
170
+ const commentary = entries.filter(
171
+ (item) => item.phase === "commentary" && item.text,
172
+ );
173
+ return {
174
+ ...result,
175
+ turnId,
176
+ response: answer
177
+ .map((item) => item.text)
178
+ .filter(Boolean)
179
+ .join("\n\n"),
180
+ commentary: commentary.map((item) => item.text),
181
+ retries,
182
+ steered:
183
+ result.matchedCompletion &&
184
+ (expectedTurnId === turnId || startedTurnId !== turnId),
185
+ timedOut: result.status === "timeout",
186
+ };
187
+ } finally {
188
+ clearTimeout(timer);
189
+ offDelta();
190
+ offItemCompleted();
191
+ offStarted();
192
+ offDone();
193
+ offError();
194
+ offActivity();
195
+ }
196
+ }
197
+
198
+ async function readCodexThread(client, threadId, timeoutMs) {
199
+ const result = await client.request("thread/read", { threadId }, timeoutMs);
200
+ if (!result?.thread)
201
+ throw new Error(`codex thread/read returned no thread: ${threadId}`);
202
+ return result.thread;
203
+ }
204
+
205
+ async function normalizedCwd(cwd) {
206
+ if (!cwd) return null;
207
+ try {
208
+ return await realpath(cwd);
209
+ } catch (error) {
210
+ if (error?.code === "ENOENT") return pathResolve(cwd);
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ async function loadedCodexThreads(client, cwd, timeoutMs) {
216
+ const wantedCwd = await normalizedCwd(cwd);
217
+ const threads = [];
218
+ let cursor;
219
+ do {
220
+ const result = await client.request(
221
+ "thread/loaded/list",
222
+ cursor ? { cursor } : {},
223
+ timeoutMs,
224
+ );
225
+ for (const threadId of result?.data || []) {
226
+ const thread = await readCodexThread(client, threadId, timeoutMs);
227
+ if (wantedCwd && (await normalizedCwd(thread.cwd)) !== wantedCwd)
228
+ continue;
229
+ threads.push({
230
+ threadId,
231
+ cwd: thread.cwd,
232
+ name: thread.name,
233
+ status: thread.status,
234
+ source: thread.source,
235
+ });
236
+ }
237
+ cursor = result?.nextCursor;
238
+ } while (cursor);
239
+ return threads;
240
+ }
241
+
242
+ export async function listCodexAppServerThreads({
243
+ socketPath,
244
+ cwd,
245
+ clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
246
+ } = {}) {
247
+ const client = codexClient(
248
+ socketPath,
249
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
250
+ clientFactory,
251
+ );
252
+ try {
253
+ await initializeCodexClient(
254
+ client,
255
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
256
+ );
257
+ return await loadedCodexThreads(
258
+ client,
259
+ cwd,
260
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
261
+ );
262
+ } finally {
263
+ client.close();
264
+ }
265
+ }
266
+
267
+ export async function askCodexAppServerThread({
268
+ socketPath,
269
+ threadId,
270
+ cwd,
271
+ prompt,
272
+ timeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS,
273
+ maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
274
+ ifBusy = "wait",
275
+ busyTimeoutMs = timeoutMs,
276
+ pollIntervalMs = 200,
277
+ clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
278
+ } = {}) {
279
+ if (typeof prompt !== "string" || !prompt.trim())
280
+ throw new Error("codex UDS ask requires a non-empty prompt");
281
+ if (!threadId) throw new Error("codex UDS ask requires --thread <id|auto>");
282
+ if (!["wait", "fail", "steer"].includes(ifBusy))
283
+ throw new Error(`invalid --if-busy: ${ifBusy}`);
284
+ const client = codexClient(
285
+ socketPath,
286
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
287
+ clientFactory,
288
+ );
289
+ try {
290
+ await initializeCodexClient(
291
+ client,
292
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
293
+ );
294
+ let selectedId = threadId;
295
+ if (threadId === "auto") {
296
+ const candidates = await loadedCodexThreads(
297
+ client,
298
+ cwd,
299
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
300
+ );
301
+ if (!candidates.length)
302
+ throw new Error(
303
+ "no loaded Codex threads; open the TUI and send its first message before using --thread auto",
304
+ );
305
+ if (candidates.length !== 1)
306
+ throw new Error(
307
+ `multiple loaded Codex threads; pass --thread ID: ${JSON.stringify(candidates)}`,
308
+ );
309
+ selectedId = candidates[0].threadId;
310
+ }
311
+ const busyStarted = Date.now();
312
+ let thread = await readCodexThread(
313
+ client,
314
+ selectedId,
315
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
316
+ );
317
+ const initiallyBusy = thread.status?.type === "active";
318
+ if (thread.status?.type === "active" && ifBusy === "fail")
319
+ throw new Error(`target busy (--if-busy fail): ${selectedId}`);
320
+ const busyDeadline = busyStarted + busyTimeoutMs;
321
+ const busyError = () =>
322
+ new Error(
323
+ `target busy after ${Math.ceil(busyTimeoutMs / 1000)}s (--if-busy wait)`,
324
+ );
325
+ while (thread.status?.type === "active" && ifBusy === "wait") {
326
+ const waitMs = Math.min(
327
+ pollIntervalMs,
328
+ Math.max(0, busyDeadline - Date.now()),
329
+ );
330
+ if (waitMs > 0)
331
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
332
+ const remainingMs = busyDeadline - Date.now();
333
+ try {
334
+ thread = await readCodexThread(
335
+ client,
336
+ selectedId,
337
+ Math.min(
338
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
339
+ Math.max(1, remainingMs),
340
+ ),
341
+ );
342
+ } catch (error) {
343
+ if (Date.now() >= busyDeadline) throw busyError();
344
+ throw error;
345
+ }
346
+ if (Date.now() >= busyDeadline && thread.status?.type === "active")
347
+ throw busyError();
348
+ }
349
+ const busyWaitedMs =
350
+ ifBusy === "wait" && initiallyBusy ? Date.now() - busyStarted : 0;
351
+ const resumed = await client.request(
352
+ "thread/resume",
353
+ { threadId: selectedId, excludeTurns: true },
354
+ DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
355
+ );
356
+ let activeTurns = null;
357
+ if (resumed?.thread?.status?.type === "active")
358
+ activeTurns = resumed.thread.turns?.length
359
+ ? resumed.thread.turns
360
+ : thread.turns;
361
+ else if (!resumed?.thread && thread.status?.type === "active")
362
+ activeTurns = thread.turns;
363
+ const expectedTurnId =
364
+ ifBusy === "steer"
365
+ ? activeTurns?.findLast((turn) => turn.status === "inProgress")?.id
366
+ : undefined;
367
+ const turn = await runCodexAppServerTurn({
368
+ client,
369
+ threadId: selectedId,
370
+ prompt,
371
+ timeoutMs,
372
+ maxTurnMs,
373
+ expectedTurnId,
374
+ });
375
+ if (turn.message) throw new Error(turn.message);
376
+ return {
377
+ cli: "codex",
378
+ transport: "uds",
379
+ threadId: selectedId,
380
+ turnId: turn.turnId,
381
+ response: turn.response,
382
+ done: turn.status === "completed",
383
+ matchedCompletion: turn.matchedCompletion,
384
+ timedOut: turn.timedOut,
385
+ status: turn.status,
386
+ commentary: turn.commentary,
387
+ meta: { retries: turn.retries },
388
+ ifBusy,
389
+ busyWaitedMs,
390
+ steered: turn.steered,
391
+ socketPath,
392
+ };
393
+ } finally {
394
+ client.close();
395
+ }
396
+ }
397
+
26
398
  function requireEndpoint(endpoint, name) {
27
399
  if (!endpoint || typeof endpoint.ask !== "function") {
28
400
  throw new Error(`${name} endpoint with ask(prompt) is required`);
@@ -532,17 +904,8 @@ export function createCodexAppServerUdsEndpoint({
532
904
  );
533
905
  }
534
906
 
535
- client = clientFactory({
536
- socketPath: resolvedSock,
537
- connectTimeoutMs: bootstrapTimeoutMs,
538
- });
539
- await client.connect();
540
- await client.request(
541
- "initialize",
542
- { clientInfo: { name: "triflux-tfx-live", version: "1.0.0" } },
543
- bootstrapTimeoutMs,
544
- );
545
- client.notify("initialized", {});
907
+ client = codexClient(resolvedSock, bootstrapTimeoutMs, clientFactory);
908
+ await initializeCodexClient(client, bootstrapTimeoutMs);
546
909
 
547
910
  const threadParams = {
548
911
  sandbox: "read-only",
@@ -563,85 +926,23 @@ export function createCodexAppServerUdsEndpoint({
563
926
  );
564
927
  }
565
928
 
566
- const parts = [];
567
- let offDelta = () => {};
568
- let offError = () => {};
569
- let offDone = () => {};
570
- let offActivity = () => {};
571
- const cleanup = () => {
572
- offDelta();
573
- offError();
574
- offDone();
575
- offActivity();
576
- };
577
- const completion = new Promise((resolve) => {
578
- offDelta = client.onNotification(
579
- "item/agentMessage/delta",
580
- (params) => {
581
- if (typeof params?.delta === "string") parts.push(params.delta);
582
- },
583
- );
584
- offError = client.onNotification("error", (params) => {
585
- cleanup();
586
- resolve({
587
- status: "failed",
588
- message: params?.message || "codex app-server error",
589
- });
590
- });
591
- offDone = client.onNotification("turn/completed", (params) => {
592
- cleanup();
593
- resolve({ status: params?.turn?.status || "unknown" });
594
- });
595
- });
596
-
597
- await client.request(
598
- "turn/start",
599
- { threadId, input: [{ type: "text", text: prompt }] },
929
+ const settled = await runCodexAppServerTurn({
930
+ client,
931
+ threadId,
932
+ prompt,
600
933
  timeoutMs,
601
- );
602
- const activityEnabled = isActivityLifecycleEnabled();
603
- const lifecycle = createActivityLifecycle({
604
- enabled: activityEnabled,
605
- interventionMs: timeoutMs,
606
- hardCeilingMs: maxTurnMs,
607
- });
608
- offActivity = client.onNotification("*", () => lifecycle.observe());
609
- let timeoutHandle = null;
610
- const timeoutPromise = new Promise((resolve) => {
611
- const arm = () => {
612
- timeoutHandle = setTimeout(async () => {
613
- if (!activityEnabled) {
614
- cleanup();
615
- resolve({ status: "timeout" });
616
- return;
617
- }
618
- const reason = await lifecycle.check();
619
- if (reason) {
620
- cleanup();
621
- resolve({ status: "timeout", reason });
622
- return;
623
- }
624
- arm();
625
- }, timeoutMs);
626
- };
627
- arm();
934
+ maxTurnMs,
628
935
  });
629
- let settled;
630
- try {
631
- settled = await Promise.race([completion, timeoutPromise]);
632
- } finally {
633
- // Clear the timer whichever side won, so a completed turn does not
634
- // keep the event loop alive until timeoutMs.
635
- if (timeoutHandle) clearTimeout(timeoutHandle);
636
- }
637
936
 
638
937
  return {
639
938
  endpoint: "codex-app-server-uds",
640
- text: parts.join("").trim(),
939
+ text: settled.response,
641
940
  done: settled.status === "completed",
642
941
  meta: {
643
942
  threadId,
644
943
  status: settled.status,
944
+ retries: settled.retries,
945
+ commentary: settled.commentary,
645
946
  socketPath: resolvedSock,
646
947
  spawned: Boolean(child),
647
948
  ...(settled.message ? { error: settled.message } : {}),
@@ -120,7 +120,7 @@ function readTomlScalar(raw, key) {
120
120
  }
121
121
 
122
122
  /**
123
- * Resolve a Codex effort profile name (e.g. "gpt56_terra_high") to its concrete
123
+ * Resolve a Codex effort profile name (e.g. "gpt6_sol_high") to its concrete
124
124
  * `model` + reasoning effort. SSOT = the per-profile file written by
125
125
  * `tfx setup` at `~/.codex/<profile>.config.toml`. Falls back to deriving the
126
126
  * effort from the `<...>_<effort>` naming convention when the file is absent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.45.0",
3
+ "version": "10.46.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  const policy = {
9
9
  executor: {
10
- profile: "gpt56_terra_high",
10
+ profile: "gpt6_sol_high",
11
11
  timeoutSec: 1080,
12
12
  runMode: "fg",
13
13
  opusOversight: false,
@@ -15,7 +15,7 @@ const policy = {
15
15
  subcommand: "exec",
16
16
  },
17
17
  codex: {
18
- profile: "gpt56_terra_high",
18
+ profile: "gpt6_sol_high",
19
19
  timeoutSec: 1080,
20
20
  runMode: "fg",
21
21
  opusOversight: false,
@@ -23,7 +23,7 @@ const policy = {
23
23
  subcommand: "exec",
24
24
  },
25
25
  "build-fixer": {
26
- profile: "gpt56_luna_low",
26
+ profile: "gpt6_luna_high",
27
27
  timeoutSec: 540,
28
28
  runMode: "fg",
29
29
  opusOversight: false,
@@ -31,7 +31,7 @@ const policy = {
31
31
  subcommand: "exec",
32
32
  },
33
33
  cleanup: {
34
- profile: "gpt56_terra_med",
34
+ profile: "gpt6_sol_med",
35
35
  timeoutSec: 540,
36
36
  runMode: "fg",
37
37
  opusOversight: false,
@@ -39,7 +39,7 @@ const policy = {
39
39
  subcommand: "exec",
40
40
  },
41
41
  deslop: {
42
- profile: "gpt56_terra_med",
42
+ profile: "gpt6_sol_med",
43
43
  timeoutSec: 540,
44
44
  runMode: "fg",
45
45
  opusOversight: false,
@@ -95,7 +95,7 @@ const policy = {
95
95
  subcommand: "exec",
96
96
  },
97
97
  "code-reviewer": {
98
- profile: "gpt56_terra_high",
98
+ profile: "gpt6_sol_high",
99
99
  timeoutSec: 1800,
100
100
  runMode: "bg",
101
101
  opusOversight: false,
@@ -103,7 +103,7 @@ const policy = {
103
103
  subcommand: "review",
104
104
  },
105
105
  "quality-reviewer": {
106
- profile: "gpt56_terra_high",
106
+ profile: "gpt6_sol_high",
107
107
  timeoutSec: 1800,
108
108
  runMode: "bg",
109
109
  opusOversight: false,
@@ -119,7 +119,7 @@ const policy = {
119
119
  subcommand: "review",
120
120
  },
121
121
  scientist: {
122
- profile: "gpt56_terra_high",
122
+ profile: "gpt6_sol_high",
123
123
  timeoutSec: 1440,
124
124
  runMode: "bg",
125
125
  opusOversight: false,
@@ -127,7 +127,7 @@ const policy = {
127
127
  subcommand: "exec",
128
128
  },
129
129
  "document-specialist": {
130
- profile: "gpt56_terra_high",
130
+ profile: "gpt6_sol_high",
131
131
  timeoutSec: 1440,
132
132
  runMode: "bg",
133
133
  opusOversight: false,
@@ -143,7 +143,7 @@ const policy = {
143
143
  subcommand: "exec",
144
144
  },
145
145
  verifier: {
146
- profile: "gpt56_terra_high",
146
+ profile: "gpt6_sol_high",
147
147
  timeoutSec: 1200,
148
148
  runMode: "fg",
149
149
  opusOversight: false,
@@ -151,7 +151,7 @@ const policy = {
151
151
  subcommand: "review",
152
152
  },
153
153
  "test-engineer": {
154
- profile: "gpt56_terra_high",
154
+ profile: "gpt6_sol_high",
155
155
  timeoutSec: 1200,
156
156
  runMode: "bg",
157
157
  opusOversight: false,
@@ -159,7 +159,7 @@ const policy = {
159
159
  subcommand: "exec",
160
160
  },
161
161
  "qa-tester": {
162
- profile: "gpt56_terra_high",
162
+ profile: "gpt6_sol_high",
163
163
  timeoutSec: 1200,
164
164
  runMode: "bg",
165
165
  opusOversight: false,
@@ -167,7 +167,7 @@ const policy = {
167
167
  subcommand: "review",
168
168
  },
169
169
  spark: {
170
- profile: "gpt56_luna_low",
170
+ profile: "gpt6_luna_low",
171
171
  timeoutSec: 180,
172
172
  runMode: "fg",
173
173
  opusOversight: false,
@@ -185,7 +185,7 @@ const policy = {
185
185
  subcommand: "exec",
186
186
  },
187
187
  writer: {
188
- profile: "gpt56_luna_low",
188
+ profile: "gpt6_luna_high",
189
189
  timeoutSec: 900,
190
190
  runMode: "bg",
191
191
  opusOversight: false,
@@ -210,9 +210,10 @@ export const DEFAULT_CODEX_AGENT = "executor";
210
210
 
211
211
  export const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
212
212
  new Set([
213
- "gpt56_luna_low",
214
- "gpt56_terra_med",
215
- "gpt56_terra_high",
213
+ "gpt6_luna_low",
214
+ "gpt6_luna_high",
215
+ "gpt6_sol_med",
216
+ "gpt6_sol_high",
216
217
  "gpt6_astra_xhigh",
217
218
  "gpt6_astra_max",
218
219
  "gpt6_astra_ultra",
@@ -19,18 +19,24 @@ const EFFORT_BY_SUFFIX = {
19
19
  };
20
20
 
21
21
  const CANONICAL_PROFILE_VALUES = Object.freeze({
22
- gpt56_luna_low: { model: "gpt-5.6-luna", effort: "low" },
23
- gpt56_terra_med: { model: "gpt-5.6-terra", effort: "medium" },
24
- gpt56_terra_high: { model: "gpt-5.6-terra", effort: "high" },
22
+ gpt6_luna_low: { model: "gpt-6-luna", effort: "low" },
23
+ gpt6_luna_high: { model: "gpt-6-luna", effort: "high" },
24
+ gpt6_sol_med: { model: "gpt-6-sol", effort: "medium" },
25
+ gpt6_sol_high: { model: "gpt-6-sol", effort: "high" },
25
26
  gpt6_astra_xhigh: { model: "gpt-6-astra", effort: "xhigh" },
26
27
  gpt6_astra_max: { model: "gpt-6-astra", effort: "max" },
27
28
  gpt6_astra_ultra: { model: "gpt-6-astra", effort: "ultra" },
28
29
  });
29
30
 
31
+ // GPT-5.6 lane names map to the GPT-6 lane with the same effort. GPT-6 has no
32
+ // Terra tier, so Terra lanes move to Sol.
30
33
  const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
31
34
  gpt56_sol_xhigh: "gpt6_astra_xhigh",
32
35
  gpt56_sol_max: "gpt6_astra_max",
33
36
  gpt56_sol_ultra: "gpt6_astra_ultra",
37
+ gpt56_terra_high: "gpt6_sol_high",
38
+ gpt56_terra_med: "gpt6_sol_med",
39
+ gpt56_luna_low: "gpt6_luna_low",
34
40
  });
35
41
 
36
42
  export function normalizeCodexProfileName(profileName) {
@@ -38,6 +44,14 @@ export function normalizeCodexProfileName(profileName) {
38
44
  return LEGACY_CANONICAL_PROFILE_ALIASES[normalized] || normalized;
39
45
  }
40
46
 
47
+ /** Model ID of a canonical profile (after legacy normalization), or null. */
48
+ export function canonicalCodexProfileModel(profileName) {
49
+ return (
50
+ CANONICAL_PROFILE_VALUES[normalizeCodexProfileName(profileName)]?.model ??
51
+ null
52
+ );
53
+ }
54
+
41
55
  function readProfileScalar(raw, key) {
42
56
  const match = String(raw).match(
43
57
  new RegExp(
@@ -51,7 +65,7 @@ function readProfileScalar(raw, key) {
51
65
  }
52
66
 
53
67
  /**
54
- * Resolve a Codex effort profile name (e.g. "gpt56_terra_high") to the `-c key=value`
68
+ * Resolve a Codex effort profile name (e.g. "gpt6_sol_high") to the `-c key=value`
55
69
  * config overrides that `codex exec` accepts, so the headless CLI lanes can
56
70
  * select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
57
71
  * `--profile X` whenever config.toml still contains an inline `[profiles.X]`
@@ -69,7 +83,7 @@ function readProfileScalar(raw, key) {
69
83
  *
70
84
  * @param {string} profileName
71
85
  * @param {{ codexHome?: string }} [opts]
72
- * @returns {string[]} e.g. ['model="gpt-5.6-terra"', 'model_reasoning_effort="high"']
86
+ * @returns {string[]} e.g. ['model="gpt-6-sol"', 'model_reasoning_effort="high"']
73
87
  */
74
88
  export function resolveCodexProfileConfigValues(profileName, opts = {}) {
75
89
  if (typeof profileName !== "string" || !profileName) {