@triflux/core 10.47.0 → 10.48.1
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.
- package/hooks/codex-session-hook.mjs +7 -0
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/lib/codex-session-registry.mjs +306 -0
- package/hud/context-monitor.mjs +1 -1
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +13 -13
- package/scripts/lib/codex-profile-config.mjs +10 -7
- package/scripts/lib/codex-recovery.sh +19 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { argv, exit, stdin, stdout } from "node:process";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { writeCodexSessionRecord } from "../hub/lib/codex-session-registry.mjs";
|
|
6
7
|
import { drainPendingSynapse as defaultDrainPendingSynapse } from "../hub/team/synapse-http.mjs";
|
|
7
8
|
import {
|
|
8
9
|
heartbeatInteractiveSession as defaultHeartbeatInteractiveSession,
|
|
@@ -145,6 +146,7 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
145
146
|
const mode = parsed.ok
|
|
146
147
|
? normalizeMode(opts.argvMode ?? argv[2], parsed.payload)
|
|
147
148
|
: "";
|
|
149
|
+
const writeSessionRecord = opts.writeSessionRecord || writeCodexSessionRecord;
|
|
148
150
|
const hubEnsureRun = opts.hubEnsureRun || defaultHubEnsureRun;
|
|
149
151
|
const registerInteractiveSession =
|
|
150
152
|
opts.registerInteractiveSession || defaultRegisterInteractiveSession;
|
|
@@ -157,6 +159,11 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
157
159
|
|
|
158
160
|
try {
|
|
159
161
|
await runHookSideEffectsWithStdoutSuppressed(async () => {
|
|
162
|
+
if (mode === "register" || mode === "heartbeat") {
|
|
163
|
+
try {
|
|
164
|
+
writeSessionRecord(parsed.payload);
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
160
167
|
if (mode === "register") {
|
|
161
168
|
try {
|
|
162
169
|
launchPresenceRegistration(parsed.payload);
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
13
|
+
|
|
14
|
+
export function registryDir(env = process.env) {
|
|
15
|
+
if (env.TFX_CODEX_SESSION_REGISTRY_DIR) {
|
|
16
|
+
return env.TFX_CODEX_SESSION_REGISTRY_DIR;
|
|
17
|
+
}
|
|
18
|
+
return join(
|
|
19
|
+
env.XDG_STATE_HOME || join(env.HOME || homedir(), ".local", "state"),
|
|
20
|
+
"triflux",
|
|
21
|
+
"codex-sessions",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function defaultPsFn(pid) {
|
|
26
|
+
try {
|
|
27
|
+
const output = execFileSync(
|
|
28
|
+
"ps",
|
|
29
|
+
["-o", "ppid=,comm=", "-p", String(pid)],
|
|
30
|
+
{
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
timeout: 1000,
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
const match = output.match(/^\s*(\d+)\s+(.+?)\s*$/);
|
|
36
|
+
return match ? { ppid: Number(match[1]), comm: match[2] } : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function findCodexAncestorPid({
|
|
43
|
+
startPid = process.ppid,
|
|
44
|
+
psFn = defaultPsFn,
|
|
45
|
+
maxDepth = 8,
|
|
46
|
+
} = {}) {
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
let pid = startPid;
|
|
49
|
+
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
50
|
+
if (!Number.isInteger(pid) || pid <= 0 || seen.has(pid)) return null;
|
|
51
|
+
seen.add(pid);
|
|
52
|
+
let processInfo;
|
|
53
|
+
try {
|
|
54
|
+
processInfo = psFn(pid);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!processInfo) return null;
|
|
59
|
+
if (basename(String(processInfo.comm || "")) === "codex") return pid;
|
|
60
|
+
pid = Number(processInfo.ppid);
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultTmuxFn(paneId) {
|
|
66
|
+
return execFileSync(
|
|
67
|
+
"tmux",
|
|
68
|
+
[
|
|
69
|
+
"display-message",
|
|
70
|
+
"-p",
|
|
71
|
+
"-t",
|
|
72
|
+
paneId,
|
|
73
|
+
"#{session_name}:#{window_id}.#{pane_id}",
|
|
74
|
+
],
|
|
75
|
+
{ encoding: "utf8", timeout: 1000 },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function defaultPanePidFn(paneId) {
|
|
80
|
+
return execFileSync(
|
|
81
|
+
"tmux",
|
|
82
|
+
["display-message", "-p", "-t", paneId, "#{pane_pid}"],
|
|
83
|
+
{ encoding: "utf8", timeout: 1000 },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function defaultCommandFn(pid) {
|
|
88
|
+
return execFileSync("ps", ["-o", "command=", "-p", String(pid)], {
|
|
89
|
+
encoding: "utf8",
|
|
90
|
+
timeout: 1000,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveTmuxCoordinate({ paneId, tmuxFn = defaultTmuxFn } = {}) {
|
|
95
|
+
if (typeof paneId !== "string" || !paneId.trim()) return null;
|
|
96
|
+
try {
|
|
97
|
+
const coordinate = tmuxFn(paneId).trim();
|
|
98
|
+
return /^.+:@\d+\.%\d+$/.test(coordinate) ? coordinate : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function aliveFromPid(pid) {
|
|
105
|
+
try {
|
|
106
|
+
process.kill(pid, 0);
|
|
107
|
+
return true;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return error?.code !== "ESRCH";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function pruneDeadRecords(dir) {
|
|
114
|
+
try {
|
|
115
|
+
for (const fileName of readdirSync(dir)) {
|
|
116
|
+
if (!/^\d+\.json$/.test(fileName)) continue;
|
|
117
|
+
const pid = Number(fileName.slice(0, -5));
|
|
118
|
+
if (Number.isSafeInteger(pid) && pid > 0 && !aliveFromPid(pid)) {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(join(dir, fileName));
|
|
121
|
+
} catch {
|
|
122
|
+
// Registry upkeep must never affect the hook response.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// A concurrent removal or permission change must not affect the write.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function writeCodexSessionRecord(
|
|
132
|
+
payload,
|
|
133
|
+
{
|
|
134
|
+
env = process.env,
|
|
135
|
+
now = Date.now,
|
|
136
|
+
psFn = defaultPsFn,
|
|
137
|
+
tmuxFn,
|
|
138
|
+
panePidFn = defaultPanePidFn,
|
|
139
|
+
commandFn = defaultCommandFn,
|
|
140
|
+
dir = registryDir(env),
|
|
141
|
+
} = {},
|
|
142
|
+
) {
|
|
143
|
+
if (typeof payload?.session_id !== "string" || !payload.session_id.trim()) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const paneId = typeof env.TMUX_PANE === "string" ? env.TMUX_PANE.trim() : "";
|
|
147
|
+
if (!paneId) return false;
|
|
148
|
+
const pid = findCodexAncestorPid({ psFn });
|
|
149
|
+
if (pid === null) return false;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const command = commandFn(pid);
|
|
153
|
+
if (
|
|
154
|
+
typeof command !== "string" ||
|
|
155
|
+
!command.trim() ||
|
|
156
|
+
command.includes("app-server")
|
|
157
|
+
) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
// Split ps argv on whitespace; the first non-option after the binary is the subcommand.
|
|
161
|
+
const subcommand = command
|
|
162
|
+
.trim()
|
|
163
|
+
.split(/\s+/)
|
|
164
|
+
.slice(1)
|
|
165
|
+
.find((arg) => !arg.startsWith("-"));
|
|
166
|
+
if (subcommand === "exec") return false;
|
|
167
|
+
const panePidOutput = String(panePidFn(paneId)).trim();
|
|
168
|
+
const panePid = Number(panePidOutput);
|
|
169
|
+
if (
|
|
170
|
+
!/^\d+$/.test(panePidOutput) ||
|
|
171
|
+
!Number.isSafeInteger(panePid) ||
|
|
172
|
+
panePid <= 0
|
|
173
|
+
) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
let ancestor = pid;
|
|
178
|
+
for (let depth = 0; depth < 8 && ancestor !== panePid; depth += 1) {
|
|
179
|
+
if (
|
|
180
|
+
!Number.isSafeInteger(ancestor) ||
|
|
181
|
+
ancestor <= 0 ||
|
|
182
|
+
seen.has(ancestor)
|
|
183
|
+
) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
seen.add(ancestor);
|
|
187
|
+
ancestor = Number(psFn(ancestor)?.ppid);
|
|
188
|
+
}
|
|
189
|
+
if (ancestor !== panePid) return false;
|
|
190
|
+
} catch {
|
|
191
|
+
// Daemons and stale panes cannot prove which TUI owns this thread.
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const timestamp = typeof now === "function" ? now() : now;
|
|
196
|
+
const targetDir = dir;
|
|
197
|
+
const target = join(targetDir, `${pid}.json`);
|
|
198
|
+
let startedAt = timestamp;
|
|
199
|
+
let source = payload.source || null;
|
|
200
|
+
try {
|
|
201
|
+
const existing = JSON.parse(readFileSync(target, "utf8"));
|
|
202
|
+
if (existing.pid === pid && existing.sessionId === payload.session_id) {
|
|
203
|
+
if (Number.isFinite(existing.startedAt)) startedAt = existing.startedAt;
|
|
204
|
+
if (payload.source === undefined) source = existing.source || null;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// New record, or an invalid previous record.
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const record = {
|
|
211
|
+
version: 1,
|
|
212
|
+
writer: "triflux",
|
|
213
|
+
pid,
|
|
214
|
+
sessionId: payload.session_id,
|
|
215
|
+
cwd: payload.cwd || process.cwd(),
|
|
216
|
+
tmux: resolveTmuxCoordinate({ paneId, tmuxFn }),
|
|
217
|
+
tmuxPane: paneId,
|
|
218
|
+
source,
|
|
219
|
+
startedAt,
|
|
220
|
+
updatedAt: timestamp,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
224
|
+
const temporary = join(
|
|
225
|
+
targetDir,
|
|
226
|
+
`.${pid}.${process.pid}.${randomUUID()}.tmp`,
|
|
227
|
+
);
|
|
228
|
+
try {
|
|
229
|
+
writeFileSync(temporary, `${JSON.stringify(record)}\n`, {
|
|
230
|
+
mode: 0o600,
|
|
231
|
+
flag: "wx",
|
|
232
|
+
});
|
|
233
|
+
renameSync(temporary, target);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
try {
|
|
236
|
+
unlinkSync(temporary);
|
|
237
|
+
} catch {
|
|
238
|
+
// Keep the original write error.
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
pruneDeadRecords(targetDir);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function readCodexSessionRecords({
|
|
247
|
+
dir = registryDir(),
|
|
248
|
+
isAlive = aliveFromPid,
|
|
249
|
+
} = {}) {
|
|
250
|
+
let fileNames;
|
|
251
|
+
try {
|
|
252
|
+
fileNames = readdirSync(dir);
|
|
253
|
+
} catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
const records = [];
|
|
257
|
+
for (const fileName of fileNames) {
|
|
258
|
+
if (!fileName.endsWith(".json")) continue;
|
|
259
|
+
try {
|
|
260
|
+
const record = JSON.parse(readFileSync(join(dir, fileName), "utf8"));
|
|
261
|
+
if (
|
|
262
|
+
record &&
|
|
263
|
+
Number.isSafeInteger(record.pid) &&
|
|
264
|
+
record.pid > 0 &&
|
|
265
|
+
typeof record.sessionId === "string" &&
|
|
266
|
+
record.sessionId &&
|
|
267
|
+
isAlive(record.pid)
|
|
268
|
+
) {
|
|
269
|
+
records.push(record);
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// Ignore a malformed or concurrently removed record.
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return records;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function codexThreadNames({ indexPath } = {}) {
|
|
279
|
+
const path =
|
|
280
|
+
indexPath ||
|
|
281
|
+
join(
|
|
282
|
+
process.env.CODEX_HOME || join(process.env.HOME || homedir(), ".codex"),
|
|
283
|
+
"session_index.jsonl",
|
|
284
|
+
);
|
|
285
|
+
let data;
|
|
286
|
+
try {
|
|
287
|
+
data = readFileSync(path, "utf8");
|
|
288
|
+
} catch {
|
|
289
|
+
return new Map();
|
|
290
|
+
}
|
|
291
|
+
const names = new Map();
|
|
292
|
+
for (const line of data.split("\n")) {
|
|
293
|
+
try {
|
|
294
|
+
const entry = JSON.parse(line);
|
|
295
|
+
if (
|
|
296
|
+
typeof entry.id === "string" &&
|
|
297
|
+
typeof entry.thread_name === "string"
|
|
298
|
+
) {
|
|
299
|
+
names.set(entry.id, entry.thread_name);
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// Ignore malformed or incomplete lines.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return names;
|
|
306
|
+
}
|
package/hud/context-monitor.mjs
CHANGED
|
@@ -15,7 +15,7 @@ const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
|
15
15
|
const MAX_TOP_KEYS = 20;
|
|
16
16
|
|
|
17
17
|
// stdin 이 context_window_size 를 제공하지 않을 때 모델 ID 로 한도를 추정한다.
|
|
18
|
-
// Anthropic 공식 문서(2026-09-
|
|
18
|
+
// Anthropic 공식 문서(2026-09-30 기준): Opus 4.6 이상, Sonnet 4.6/5/5.5,
|
|
19
19
|
// Fable 5/5.1 = 1M. Sonnet 4.5 / Haiku 4.5 = 200K이며 [1m]도 유지한다.
|
|
20
20
|
const MODEL_HINT_1M_PREFIXES = [
|
|
21
21
|
"claude-sonnet-4-6",
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
const policy = {
|
|
9
9
|
executor: {
|
|
10
|
-
profile: "
|
|
10
|
+
profile: "gpt61_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: "
|
|
18
|
+
profile: "gpt61_sol_high",
|
|
19
19
|
timeoutSec: 1080,
|
|
20
20
|
runMode: "fg",
|
|
21
21
|
opusOversight: false,
|
|
@@ -31,7 +31,7 @@ const policy = {
|
|
|
31
31
|
subcommand: "exec",
|
|
32
32
|
},
|
|
33
33
|
cleanup: {
|
|
34
|
-
profile: "
|
|
34
|
+
profile: "gpt61_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: "
|
|
42
|
+
profile: "gpt61_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: "
|
|
98
|
+
profile: "gpt61_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: "
|
|
106
|
+
profile: "gpt61_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: "
|
|
122
|
+
profile: "gpt61_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: "
|
|
130
|
+
profile: "gpt61_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: "
|
|
146
|
+
profile: "gpt61_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: "
|
|
154
|
+
profile: "gpt61_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: "
|
|
162
|
+
profile: "gpt61_sol_high",
|
|
163
163
|
timeoutSec: 1200,
|
|
164
164
|
runMode: "bg",
|
|
165
165
|
opusOversight: false,
|
|
@@ -212,8 +212,8 @@ export const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
|
|
|
212
212
|
new Set([
|
|
213
213
|
"gpt6_luna_low",
|
|
214
214
|
"gpt6_luna_high",
|
|
215
|
-
"
|
|
216
|
-
"
|
|
215
|
+
"gpt61_sol_med",
|
|
216
|
+
"gpt61_sol_high",
|
|
217
217
|
"gpt6_astra_xhigh",
|
|
218
218
|
"gpt6_astra_max",
|
|
219
219
|
"gpt6_astra_ultra",
|
|
@@ -21,22 +21,25 @@ const EFFORT_BY_SUFFIX = {
|
|
|
21
21
|
const CANONICAL_PROFILE_VALUES = Object.freeze({
|
|
22
22
|
gpt6_luna_low: { model: "gpt-6-luna", effort: "low" },
|
|
23
23
|
gpt6_luna_high: { model: "gpt-6-luna", effort: "high" },
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
gpt61_sol_med: { model: "gpt-6.1-sol", effort: "medium" },
|
|
25
|
+
gpt61_sol_high: { model: "gpt-6.1-sol", effort: "high" },
|
|
26
26
|
gpt6_astra_xhigh: { model: "gpt-6-astra", effort: "xhigh" },
|
|
27
27
|
gpt6_astra_max: { model: "gpt-6-astra", effort: "max" },
|
|
28
28
|
gpt6_astra_ultra: { model: "gpt-6-astra", effort: "ultra" },
|
|
29
29
|
});
|
|
30
30
|
|
|
31
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.
|
|
32
|
+
// Terra tier, so Terra lanes move to Sol. GPT-6 Sol lane names map to GPT-6.1
|
|
33
|
+
// Sol with the same effort.
|
|
33
34
|
const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
|
|
34
35
|
gpt56_sol_xhigh: "gpt6_astra_xhigh",
|
|
35
36
|
gpt56_sol_max: "gpt6_astra_max",
|
|
36
37
|
gpt56_sol_ultra: "gpt6_astra_ultra",
|
|
37
|
-
gpt56_terra_high: "
|
|
38
|
-
gpt56_terra_med: "
|
|
38
|
+
gpt56_terra_high: "gpt61_sol_high",
|
|
39
|
+
gpt56_terra_med: "gpt61_sol_med",
|
|
39
40
|
gpt56_luna_low: "gpt6_luna_low",
|
|
41
|
+
gpt6_sol_high: "gpt61_sol_high",
|
|
42
|
+
gpt6_sol_med: "gpt61_sol_med",
|
|
40
43
|
});
|
|
41
44
|
|
|
42
45
|
export function normalizeCodexProfileName(profileName) {
|
|
@@ -65,7 +68,7 @@ function readProfileScalar(raw, key) {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/**
|
|
68
|
-
* Resolve a Codex effort profile name (e.g. "
|
|
71
|
+
* Resolve a Codex effort profile name (e.g. "gpt61_sol_high") to the `-c key=value`
|
|
69
72
|
* config overrides that `codex exec` accepts, so the headless CLI lanes can
|
|
70
73
|
* select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
|
|
71
74
|
* `--profile X` whenever config.toml still contains an inline `[profiles.X]`
|
|
@@ -83,7 +86,7 @@ function readProfileScalar(raw, key) {
|
|
|
83
86
|
*
|
|
84
87
|
* @param {string} profileName
|
|
85
88
|
* @param {{ codexHome?: string }} [opts]
|
|
86
|
-
* @returns {string[]} e.g. ['model="gpt-6-sol"', 'model_reasoning_effort="high"']
|
|
89
|
+
* @returns {string[]} e.g. ['model="gpt-6.1-sol"', 'model_reasoning_effort="high"']
|
|
87
90
|
*/
|
|
88
91
|
export function resolveCodexProfileConfigValues(profileName, opts = {}) {
|
|
89
92
|
if (typeof profileName !== "string" || !profileName) {
|
|
@@ -8,9 +8,16 @@
|
|
|
8
8
|
# 4차 FALLBACK_FAILED → 모두 실패 시 marker + stderr_log 경로 출력
|
|
9
9
|
|
|
10
10
|
recover_codex_stdout() {
|
|
11
|
+
# 복구 진입 여부 플래그(전역, local 아님) — 아우터 no-op 가드가 읽는다.
|
|
12
|
+
# 0 = 진짜 stdout 존재(복구 미진입), 1 = stdout 이 애초에 비어 복구 진입함.
|
|
13
|
+
CODEX_STDOUT_WAS_RECOVERED=0
|
|
11
14
|
if [[ -s "$STDOUT_LOG" || ! -s "$STDERR_LOG" ]]; then
|
|
12
15
|
return 0
|
|
13
16
|
fi
|
|
17
|
+
# 여기 도달 = stdout 이 애초에 비어있었다. 아래에서 STDOUT_LOG 에 채워지는 내용은
|
|
18
|
+
# 전부 stderr 에서 복구된 것이지 워커의 진짜 산출물이 아니다. 이 플래그로 아우터
|
|
19
|
+
# 가드가 "복구된 stderr 노이즈"를 성공 출력으로 오인하지 않게 한다(#result-verification).
|
|
20
|
+
CODEX_STDOUT_WAS_RECOVERED=1
|
|
14
21
|
|
|
15
22
|
# 1차: codex marker
|
|
16
23
|
sed 's/\r$//' "$STDERR_LOG" \
|
|
@@ -48,3 +55,15 @@ recover_codex_stdout() {
|
|
|
48
55
|
echo "[tfx-route] 경고: codex stdout 비어있음, stderr 복구도 실패. 위 stderr_log 경로에서 raw codex 출력 확인 가능." >&2
|
|
49
56
|
fi
|
|
50
57
|
}
|
|
58
|
+
|
|
59
|
+
# ── MCP transport 채널 사망 판정 ──
|
|
60
|
+
# Codex MCP transport 채널이 실행 중 죽으면 codex exec 가 exit 0 + 빈 stdout 으로
|
|
61
|
+
# 끝나는 변형이 있다(래퍼 부재로 인한 pre-flight CODEX_MCP_TRANSPORT_EXIT_CODE=70
|
|
62
|
+
# 과 구분됨 — 이건 런타임 채널 사망). stderr 의 하드 크래시 서명으로 이 사망을
|
|
63
|
+
# 검출한다. 아우터 가드가 exit 0 이어도 결과를 실패로 승격하는 근거로 쓴다.
|
|
64
|
+
# 인자: [stderr_log] (미지정 시 $STDERR_LOG). 반환: 0=크래시 서명 있음, 1=없음.
|
|
65
|
+
codex_stdout_transport_crashed() {
|
|
66
|
+
local stderr_log="${1:-${STDERR_LOG:-}}"
|
|
67
|
+
[[ -n "$stderr_log" && -f "$stderr_log" ]] || return 1
|
|
68
|
+
grep -qE 'Transport channel closed|rmcp::transport worker quit with fatal' "$stderr_log" 2>/dev/null
|
|
69
|
+
}
|