@triflux/remote 10.44.0 → 10.45.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.
Files changed (39) hide show
  1. package/hub/pipe.mjs +10 -15
  2. package/hub/schema.sql +1 -0
  3. package/hub/server.mjs +172 -93
  4. package/hub/store.mjs +127 -61
  5. package/hub/team/backend.mjs +4 -2
  6. package/hub/team/claude-daemon-control.mjs +2 -0
  7. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  8. package/hub/team/cli/commands/start/start-mux.mjs +24 -3
  9. package/hub/team/cli/services/runtime-mode.mjs +23 -9
  10. package/hub/team/conductor.mjs +80 -24
  11. package/hub/team/cto-auto-collect.mjs +2 -6
  12. package/hub/team/cto-pull.mjs +4 -0
  13. package/hub/team/dashboard-open.mjs +1 -0
  14. package/hub/team/execution-mode.mjs +38 -2
  15. package/hub/team/headless.mjs +493 -82
  16. package/hub/team/interactive-tui-transport.mjs +15 -1
  17. package/hub/team/launcher-template.mjs +17 -1
  18. package/hub/team/orchestrator.mjs +36 -9
  19. package/hub/team/pane.mjs +176 -14
  20. package/hub/team/psmux.mjs +79 -70
  21. package/hub/team/retry-state-machine.mjs +39 -16
  22. package/hub/team/session.mjs +57 -0
  23. package/hub/team/synapse-http.mjs +149 -4
  24. package/hub/team/terminal-opener.mjs +68 -13
  25. package/hub/team/tui-viewer.mjs +65 -14
  26. package/hub/tools.mjs +10 -1
  27. package/hub/workers/codex-mcp.mjs +8 -3
  28. package/hub/workers/delegator-mcp.mjs +1 -1
  29. package/package.json +1 -1
  30. package/scripts/lib/agent-route-policy.mjs +84 -24
  31. package/scripts/lib/cli-agy.mjs +24 -4
  32. package/scripts/lib/cli-codex.mjs +2 -0
  33. package/scripts/lib/codex-profile-config.mjs +26 -8
  34. package/scripts/lib/codex-recovery.sh +19 -0
  35. package/scripts/lib/gemini-profiles.mjs +113 -29
  36. package/scripts/lib/keyword-rules.mjs +12 -0
  37. package/scripts/lib/logger.mjs +60 -43
  38. package/scripts/lib/machine-profile.mjs +293 -0
  39. package/scripts/lib/mcp-health.mjs +44 -18
@@ -475,6 +475,63 @@ export function attachSession(sessionName) {
475
475
  }
476
476
  }
477
477
 
478
+ /**
479
+ * non-TTY 환경용 attach fallback: 요청 프로세스가 명시적으로 넘긴 lead pane을
480
+ * 분할해 그 pane 안에서 대상 세션에 attach한다.
481
+ *
482
+ * 호출 프로세스가 pipe(non-TTY)로 실행돼 attachSession()이 불가능해도,
483
+ * tmux 서버에 보내는 split-window는 호출자 TTY가 필요 없다. 새 pane에는
484
+ * tmux가 실제 pty를 붙여주므로 그 안의 attach는 정상 동작한다.
485
+ * env -u TMUX는 nested-attach 보호 해제용이다.
486
+ *
487
+ * @param {string} sessionName attach할 대상 세션
488
+ * @param {string} targetPane 요청 lead의 `$TMUX_PANE` (`%N`)
489
+ * @param {string} tmuxEnv 요청 lead의 `$TMUX` (`socket,pid,index`)
490
+ * @returns {boolean} 분할 attach 성공 여부 (false면 호출자가 기존 생략 경로 유지)
491
+ */
492
+ export function buildSplitAttachArgs(sessionName, targetPane, tmuxEnv) {
493
+ const normalizedSessionName = String(sessionName || "");
494
+ if (!normalizedSessionName.trim()) {
495
+ throw new Error("session name is required");
496
+ }
497
+ if (!/^%\d+$/u.test(String(targetPane || ""))) {
498
+ throw new Error("requesting lead TMUX_PANE is required");
499
+ }
500
+ const socketPath = String(tmuxEnv || "").split(",", 1)[0];
501
+ if (!socketPath.trim()) {
502
+ throw new Error("requesting lead TMUX socket is required");
503
+ }
504
+ const quotedSocket = `'${socketPath.replace(/'/gu, "'\\''")}'`;
505
+ const quotedSessionName = `'${normalizedSessionName.replace(/'/gu, "'\\''")}'`;
506
+ return [
507
+ "split-window",
508
+ "-t",
509
+ targetPane,
510
+ "-v",
511
+ "-l",
512
+ "50%",
513
+ `env -u TMUX tmux -S ${quotedSocket} attach -t ${quotedSessionName}`,
514
+ ];
515
+ }
516
+
517
+ export function splitAttachToClient(sessionName, opts = {}) {
518
+ const deps = opts._deps || {};
519
+ const detectMux = deps.detectMultiplexer || detectMultiplexer;
520
+ if (detectMux() !== "tmux") return false;
521
+
522
+ let args;
523
+ try {
524
+ args = buildSplitAttachArgs(sessionName, opts.targetPane, opts.tmuxEnv);
525
+ } catch {
526
+ return false;
527
+ }
528
+
529
+ // 상하 분할: 좌우 분할은 절반 폭이 워커 pane 가독 하한(100cols) 미만인 경우가 흔함
530
+ const spawn = deps.spawnSync || spawnSync;
531
+ const split = spawn("tmux", args);
532
+ return (split.status ?? 1) === 0;
533
+ }
534
+
478
535
  /**
479
536
  * tmux 세션 존재 확인
480
537
  * @param {string} sessionName
@@ -1,6 +1,9 @@
1
- import { readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { homedir, hostname } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { normalizeRoleKey } from "@triflux/core/hub/role-contract.mjs";
4
7
 
5
8
  const HUB_DEFAULT_PORT = 27888;
6
9
  // Same token file as hub/bridge.mjs (HUB_TOKEN_FILE). Kept inline rather than
@@ -21,6 +24,144 @@ function normalizeToken(raw) {
21
24
  return token || null;
22
25
  }
23
26
 
27
+ function opaqueId(prefix, value) {
28
+ return `${prefix}_${createHash("sha256")
29
+ .update(String(value || ""))
30
+ .digest("base64url")
31
+ .slice(0, 22)}`;
32
+ }
33
+
34
+ function normalizedProjectId(value) {
35
+ try {
36
+ return normalizeRoleKey({ project_id: value, role_kind: "cto" }).project_id;
37
+ } catch {
38
+ return "";
39
+ }
40
+ }
41
+
42
+ function resolveProjectId(cwd, meta, opts = {}) {
43
+ const explicit = meta?.project_id || process.env.TFX_PROJECT_ID;
44
+ if (explicit) return normalizedProjectId(explicit);
45
+ if (typeof opts.resolveProjectId === "function") {
46
+ return normalizedProjectId(opts.resolveProjectId(cwd));
47
+ }
48
+ let current = cwd;
49
+ while (current) {
50
+ const manifest = join(current, ".triflux", "project.json");
51
+ if (existsSync(manifest)) {
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
54
+ if (parsed?.schema_version !== "tfx.project.v1") return "";
55
+ return normalizedProjectId(parsed.project_id);
56
+ } catch {
57
+ return "";
58
+ }
59
+ }
60
+ const parent = dirname(current);
61
+ if (parent === current) break;
62
+ current = parent;
63
+ }
64
+ return "";
65
+ }
66
+
67
+ function inferSessionCli(meta, opts = {}) {
68
+ const explicit = String(meta?.cli || meta?.actor_cli || "")
69
+ .trim()
70
+ .toLowerCase();
71
+ if (explicit === "codex" || explicit === "claude") return explicit;
72
+ const entrypoint = String(opts.entrypoint ?? process.argv[1] ?? "");
73
+ if (entrypoint.includes("codex-session-hook")) return "codex";
74
+ if (entrypoint.includes("agy-session-hook")) return "other";
75
+ return "claude";
76
+ }
77
+
78
+ function resolveTmuxSession(meta, opts = {}) {
79
+ const explicit =
80
+ meta?.tmux_session || meta?.session_name || process.env.TFX_TMUX_SESSION;
81
+ if (explicit) return String(explicit).trim();
82
+ if (!process.env.TMUX) return "";
83
+ try {
84
+ const run =
85
+ opts.tmuxSessionName ||
86
+ (() =>
87
+ execFileSync("tmux", ["display-message", "-p", "#{session_name}"], {
88
+ encoding: "utf8",
89
+ timeout: 200,
90
+ windowsHide: true,
91
+ }));
92
+ return String(run() || "").trim();
93
+ } catch {
94
+ return "";
95
+ }
96
+ }
97
+
98
+ export function buildSynapseRegistrationMeta(meta, opts = {}) {
99
+ if (!meta || meta.sessionKind !== "interactive") return meta;
100
+ const sessionId = String(meta.sessionId || "").trim();
101
+ if (!sessionId) return meta;
102
+ const cwd = typeof meta.cwd === "string" ? meta.cwd : process.cwd();
103
+ const cli = inferSessionCli(meta, opts);
104
+ const hostId = String(
105
+ meta.host_id ||
106
+ process.env.TFX_HOST_ID ||
107
+ opaqueId("hst", opts.hostname?.() || hostname()),
108
+ ).trim();
109
+ const expiresAtMs = Number(opts.nowMs?.() ?? Date.now()) + 299000;
110
+ const transportLocators = [];
111
+ if (cli === "claude") {
112
+ transportLocators.push({
113
+ schema_version: "tfx.transport-locator.v1",
114
+ transport_id: opaqueId("trn", `claude-uds:${hostId}:${sessionId}`),
115
+ kind: "claude-uds",
116
+ host_id: hostId,
117
+ capabilities: ["probe", "wake", "activate", "interrupt"],
118
+ priority: 100,
119
+ expires_at_ms: expiresAtMs,
120
+ locator: {
121
+ session_id: sessionId,
122
+ bridge_id: String(
123
+ meta.bridge_id || process.env.TFX_BRIDGE_ID || "local-hub",
124
+ ),
125
+ },
126
+ });
127
+ }
128
+ const tmuxSession = resolveTmuxSession(meta, opts);
129
+ if (tmuxSession) {
130
+ const muxServerSource =
131
+ meta.mux_server_id ||
132
+ process.env.TFX_MUX_SERVER_ID ||
133
+ String(process.env.TMUX || "default").split(",", 1)[0];
134
+ transportLocators.push({
135
+ schema_version: "tfx.transport-locator.v1",
136
+ transport_id: opaqueId("trn", `tmux:${hostId}:${tmuxSession}`),
137
+ kind: "tmux",
138
+ host_id: hostId,
139
+ capabilities: ["probe", "wake", "activate", "interrupt"],
140
+ priority: cli === "codex" ? 100 : 50,
141
+ expires_at_ms: expiresAtMs,
142
+ locator: {
143
+ session_name: tmuxSession,
144
+ mux_server_id: opaqueId("mux", muxServerSource),
145
+ },
146
+ });
147
+ }
148
+ const directAgentId = `session:${sessionId}`;
149
+ return {
150
+ ...meta,
151
+ agent_id:
152
+ meta.agent_id ||
153
+ (directAgentId.length <= 64 && /^[a-zA-Z0-9._:-]+$/u.test(directAgentId)
154
+ ? directAgentId
155
+ : opaqueId("session", sessionId)),
156
+ cli,
157
+ project_id: resolveProjectId(cwd, meta, opts) || undefined,
158
+ session_id: meta.session_id || sessionId,
159
+ host_id: hostId,
160
+ transport_locators_json:
161
+ meta.transport_locators_json ?? JSON.stringify(transportLocators),
162
+ };
163
+ }
164
+
24
165
  // Mirrors hub/bridge.mjs:readHubToken — env override first, then the token file.
25
166
  // Missing token → null (backward-compatible with token-less hubs).
26
167
  function readHubToken() {
@@ -139,7 +280,11 @@ export function drainPendingSynapse(timeoutMs = 1000) {
139
280
  }
140
281
 
141
282
  export function registerSynapseSession(meta, opts = {}) {
142
- return fireAndForgetSynapse("/synapse/register", meta, opts);
283
+ return fireAndForgetSynapse(
284
+ "/synapse/register",
285
+ buildSynapseRegistrationMeta(meta, opts),
286
+ opts,
287
+ );
143
288
  }
144
289
 
145
290
  export function heartbeatSynapseSession(
@@ -111,6 +111,35 @@ function wtResultSucceeded(result) {
111
111
  return result?.success !== false;
112
112
  }
113
113
 
114
+ function resolveLeadPane(value) {
115
+ const pane = String(value || "");
116
+ return /^%\d+$/u.test(pane) ? pane : null;
117
+ }
118
+
119
+ function resolveLeadWindow(tmuxExec, targetPane) {
120
+ try {
121
+ const targetWindow = String(
122
+ tmuxExec(
123
+ `display-message -p -t ${targetPane} '#{session_name}:#{window_index}'`,
124
+ ) ?? "",
125
+ ).trim();
126
+ return targetWindow || null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function resolveLeadSocket(tmuxExec, targetPane) {
133
+ try {
134
+ const socketPath = String(
135
+ tmuxExec(`display-message -p -t ${targetPane} '#{socket_path}'`) ?? "",
136
+ ).trim();
137
+ return socketPath || null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
114
143
  export function createTerminalOpener(deps = {}) {
115
144
  const platform = resolvePlatform(deps);
116
145
  const tmuxExec = deps.tmuxExec || defaultTmuxExec;
@@ -134,12 +163,22 @@ export function createTerminalOpener(deps = {}) {
134
163
 
135
164
  const mux = resolveMux(deps);
136
165
  if (isTmuxLikeMux(mux, platform)) {
137
- tmuxExec(
138
- `new-window -n ${shellQuote(title)} ${shellQuote(
139
- buildCommandString(spec),
140
- )}`,
141
- );
142
- return true;
166
+ const targetPane = resolveLeadPane(spec.targetPane);
167
+ if (!targetPane) return false;
168
+ const targetWindow = resolveLeadWindow(tmuxExec, targetPane);
169
+ if (!targetWindow) return false;
170
+ try {
171
+ // tmux new-window는 pane target(%N)을 거부하므로, 요청 pane에서 해석한
172
+ // 정확한 session:window 뒤에 새 window를 삽입한다.
173
+ tmuxExec(
174
+ `new-window -a -t ${shellQuote(targetWindow)} -n ${shellQuote(
175
+ title,
176
+ )} ${shellQuote(buildCommandString(spec))}`,
177
+ );
178
+ return true;
179
+ } catch {
180
+ return false;
181
+ }
143
182
  }
144
183
 
145
184
  if (platform === "darwin") {
@@ -174,14 +213,30 @@ export function createTerminalOpener(deps = {}) {
174
213
 
175
214
  const mux = resolveMux(deps);
176
215
  if (isTmuxLikeMux(mux, platform)) {
177
- const attachCommand = buildAttachCommand(mux, sessionName, deps);
216
+ const targetPane = resolveLeadPane(opts.targetPane);
217
+ if (!targetPane) return false;
218
+ const socketPath =
219
+ mux === "psmux" ? null : resolveLeadSocket(tmuxExec, targetPane);
220
+ if (mux !== "psmux" && !socketPath) return false;
221
+ const attachCommand =
222
+ mux === "psmux"
223
+ ? buildAttachCommand(mux, sessionName, deps)
224
+ : `env -u TMUX tmux -S ${shellQuote(
225
+ socketPath,
226
+ )} attach-session -t ${shellQuote(sessionName)}`;
178
227
  if (!attachCommand) return false;
179
- tmuxExec(
180
- `new-window -n ${shellQuote(opts.title ?? sessionName)} ${shellQuote(
181
- attachCommand,
182
- )}`,
183
- );
184
- return true;
228
+ const targetWindow = resolveLeadWindow(tmuxExec, targetPane);
229
+ if (!targetWindow) return false;
230
+ try {
231
+ tmuxExec(
232
+ `new-window -a -t ${shellQuote(targetWindow)} -n ${shellQuote(
233
+ opts.title ?? sessionName,
234
+ )} ${shellQuote(attachCommand)}`,
235
+ );
236
+ return true;
237
+ } catch {
238
+ return false;
239
+ }
185
240
  }
186
241
 
187
242
  return false;
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  import { formatPsmuxInstallGuidance } from "../../scripts/lib/psmux-info.mjs";
12
12
  import { openHeadlessDashboardTarget } from "./dashboard-open.mjs";
13
13
  import { processHandoff } from "./handoff.mjs";
14
+ import { getMultiplexerType } from "./psmux.mjs";
14
15
  import { createLogDashboard } from "./tui.mjs";
15
16
  import { createLiteDashboard } from "./tui-lite.mjs";
16
17
 
@@ -24,23 +25,36 @@ function argVal(flag) {
24
25
  const SESSION = argVal("--session");
25
26
  const RESULT_DIR = argVal("--result-dir") ?? join(tmpdir(), "tfx-headless");
26
27
  const LAYOUT = argVal("--layout") ?? "single";
27
-
28
- if (!SESSION) {
28
+ const SOURCE = argVal("--source") === "files" ? "files" : "mux";
29
+ const LEAD_PANE = argVal("--lead-pane");
30
+ const parsedWorkerCount = Number.parseInt(argVal("--workers") || "0", 10);
31
+ const WORKER_COUNT = Number.isFinite(parsedWorkerCount)
32
+ ? Math.min(64, Math.max(0, parsedWorkerCount))
33
+ : 0;
34
+ const MUX_BIN = getMultiplexerType();
35
+
36
+ if (!SESSION || !/^[a-zA-Z0-9_-]+$/u.test(SESSION)) {
29
37
  process.stderr.write(
30
38
  "Usage: node tui-viewer.mjs --session <name> [--result-dir <dir>] [--layout <name>]\n",
31
39
  );
32
40
  process.exit(1);
33
41
  }
34
-
35
- try {
36
- execFileSync("psmux", ["--version"], { encoding: "utf8", timeout: 2000 });
37
- } catch {
38
- process.stderr.write(
39
- `ERROR: psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}\n`,
40
- );
42
+ if (SOURCE === "files" && WORKER_COUNT === 0) {
43
+ process.stderr.write("ERROR: --source files requires --workers <count>\n");
41
44
  process.exit(1);
42
45
  }
43
46
 
47
+ if (SOURCE === "mux") {
48
+ try {
49
+ execFileSync(MUX_BIN, ["-V"], { encoding: "utf8", timeout: 2000 });
50
+ } catch {
51
+ process.stderr.write(
52
+ `ERROR: primary multiplexer 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}\n`,
53
+ );
54
+ process.exit(1);
55
+ }
56
+ }
57
+
44
58
  // ── 메모리 보호 상수 ──
45
59
  const MAX_BODY_BYTES = 10240;
46
60
 
@@ -59,11 +73,13 @@ const tui = tuiFactory({
59
73
  worker: workerName,
60
74
  openAll: false,
61
75
  cwd: process.cwd(),
76
+ targetPane: LEAD_PANE,
62
77
  }),
63
78
  onOpenAllWorkers: () =>
64
79
  openHeadlessDashboardTarget(SESSION, {
65
80
  openAll: true,
66
81
  cwd: process.cwd(),
82
+ targetPane: LEAD_PANE,
67
83
  }),
68
84
  });
69
85
  const startTime = Date.now();
@@ -166,9 +182,16 @@ function estimateProgress(lines, context = {}) {
166
182
 
167
183
  // ── psmux 래퍼 ──
168
184
  function listPanes() {
185
+ if (SOURCE === "files") {
186
+ return Array.from({ length: WORKER_COUNT }, (_, index) => ({
187
+ index: index + 1,
188
+ title: `worker-${index + 1} (daemon)`,
189
+ pid: "",
190
+ }));
191
+ }
169
192
  try {
170
193
  const out = execFileSync(
171
- "psmux",
194
+ MUX_BIN,
172
195
  [
173
196
  "list-panes",
174
197
  "-t",
@@ -192,9 +215,35 @@ function listPanes() {
192
215
  }
193
216
 
194
217
  function capturePane(paneIdx, lines = 20) {
218
+ if (SOURCE === "files") {
219
+ const base = join(RESULT_DIR, `${SESSION}-worker-${paneIdx}.txt`);
220
+ const artifacts = [
221
+ { path: base, label: "output" },
222
+ { path: `${base}.partial`, label: "daemon" },
223
+ { path: `${base}.err`, label: "stderr" },
224
+ ];
225
+ const snapshots = [];
226
+ for (const artifact of artifacts) {
227
+ try {
228
+ const content = readFileSync(artifact.path, "utf8").trim();
229
+ if (!content) continue;
230
+ snapshots.push(
231
+ `[${artifact.label}]\n${content.split("\n").slice(-lines).join("\n")}`,
232
+ );
233
+ } catch {
234
+ /* artifact is optional while the daemon job is starting */
235
+ }
236
+ }
237
+ return (
238
+ snapshots.join("\n") ||
239
+ `worker-${paneIdx} daemon active · waiting for output · ${Math.round(
240
+ (Date.now() - startTime) / 1000,
241
+ )}s`
242
+ );
243
+ }
195
244
  try {
196
245
  return execFileSync(
197
- "psmux",
246
+ MUX_BIN,
198
247
  ["capture-pane", "-t", `${SESSION}:0.${paneIdx}`, "-p"],
199
248
  { encoding: "utf8", timeout: 2000 },
200
249
  )
@@ -368,7 +417,7 @@ function ingest() {
368
417
  const tokens = extractTokenLabel(snapshot);
369
418
  const elapsed = Math.round((Date.now() - startTime) / 1000);
370
419
 
371
- if (resultSize > 10 || shellReturned) {
420
+ if (SOURCE !== "files" && (resultSize > 10 || shellReturned)) {
372
421
  const resultContent = existsSync(resultFile)
373
422
  ? readFileSync(resultFile, "utf8")
374
423
  : snapshot;
@@ -410,8 +459,10 @@ function ingest() {
410
459
  // 진행 중
411
460
  const progress = estimateProgress(lines, {
412
461
  tokens,
413
- resultSize,
414
- shellReturned,
462
+ // daemon output files grow while the job is still running; file size is
463
+ // activity evidence, not a completion signal in read-only file mode.
464
+ resultSize: SOURCE === "files" ? 0 : resultSize,
465
+ shellReturned: SOURCE === "files" ? false : shellReturned,
415
466
  done: false,
416
467
  });
417
468
  const verdict = lastLine;
package/hub/tools.mjs CHANGED
@@ -159,8 +159,13 @@ export function createTools(store, router, hitl, pipe = null) {
159
159
  transport_locators_json: {
160
160
  description:
161
161
  "tfx.transport-locator.v1 객체 배열 또는 그 JSON 문자열",
162
- oneOf: [{ type: "array", items: { type: "object" } }, { type: "string" }],
162
+ oneOf: [
163
+ { type: "array", items: { type: "object" } },
164
+ { type: "string" },
165
+ ],
163
166
  },
167
+ hub_instance_id: { type: "string" },
168
+ store_fingerprint: { type: "string" },
164
169
  heartbeat_ttl_ms: {
165
170
  type: "integer",
166
171
  minimum: 10000,
@@ -186,6 +191,8 @@ export function createTools(store, router, hitl, pipe = null) {
186
191
  agent_id: { type: "string" },
187
192
  trace_id: { type: "string" },
188
193
  include_metrics: { type: "boolean", default: true },
194
+ hub_instance_id: { type: "string" },
195
+ store_fingerprint: { type: "string" },
189
196
  },
190
197
  },
191
198
  handler: wrap("STATUS_FAILED", (args) => {
@@ -206,6 +213,8 @@ export function createTools(store, router, hitl, pipe = null) {
206
213
  agent_id: { type: "string", pattern: "^[a-zA-Z0-9._:-]{3,64}$" },
207
214
  reason: { type: "string" },
208
215
  requested_by: { type: "string" },
216
+ hub_instance_id: { type: "string" },
217
+ store_fingerprint: { type: "string" },
209
218
  },
210
219
  },
211
220
  handler: wrap("TAKEOVER_ROLE_FAILED", (args) => {
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
9
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
10
10
 
11
+ import { normalizeCodexProfileName } from "../../scripts/lib/codex-profile-config.mjs";
11
12
  import {
12
13
  CODEX_MCP_EXECUTION_EXIT_CODE,
13
14
  CODEX_MCP_TRANSPORT_EXIT_CODE,
@@ -134,11 +135,13 @@ function readTomlScalar(raw, key) {
134
135
  export function resolveCodexProfileConfig(profileName) {
135
136
  const result = { model: null, reasoningEffort: null };
136
137
  if (typeof profileName !== "string" || !profileName) return result;
138
+ const normalizedProfileName = normalizeCodexProfileName(profileName);
139
+ if (!normalizedProfileName) return result;
137
140
 
138
141
  const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
139
142
  try {
140
143
  const raw = readFileSync(
141
- join(codexHome, `${profileName}.config.toml`),
144
+ join(codexHome, `${normalizedProfileName}.config.toml`),
142
145
  "utf8",
143
146
  );
144
147
  result.model = readTomlScalar(raw, "model");
@@ -146,7 +149,9 @@ export function resolveCodexProfileConfig(profileName) {
146
149
  } catch {
147
150
  // Profile file absent (non-file alias). Derive effort from the naming
148
151
  // convention; leave model unset so codex uses its config.toml default.
149
- const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(profileName);
152
+ const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(
153
+ normalizedProfileName,
154
+ );
150
155
  if (suffix) {
151
156
  const map = {
152
157
  ultra: "ultra",
@@ -198,7 +203,7 @@ export function buildCodexArguments(prompt, opts = {}) {
198
203
  const profileDerivedUltra =
199
204
  resolved.reasoningEffort?.toLowerCase() === "ultra";
200
205
  if (typeof args.model !== "string") {
201
- if (profileDerivedUltra) args.model = "gpt-5.6-sol";
206
+ if (profileDerivedUltra) args.model = "gpt-6-astra";
202
207
  else if (resolved.model) args.model = resolved.model;
203
208
  }
204
209
  if (resolved.reasoningEffort) {
@@ -11,7 +11,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import * as z from "zod";
14
- import { resolveCodexAgentPolicy } from "@triflux/core/scripts/lib/agent-route-policy.mjs";
14
+ import { resolveCodexAgentPolicy } from "../../scripts/lib/agent-route-policy.mjs";
15
15
  import { resolveBashExecutable } from "@triflux/core/hub/lib/bash-path.mjs";
16
16
  import { runHeadlessWithCleanup } from "../team/headless.mjs";
17
17
  import { buildWorkerSandboxEnv } from "../team/worker-sandbox.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.44.0",
3
+ "version": "10.45.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",