@triflux/remote 10.29.0 → 10.30.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.
@@ -124,13 +124,18 @@ export function drainPendingSynapse(timeoutMs = 1000) {
124
124
  const settled = Promise.allSettled([...inFlightSynapse]);
125
125
  const ms = Number(timeoutMs);
126
126
  if (!Number.isFinite(ms) || ms <= 0) return settled;
127
- return Promise.race([
128
- settled,
129
- new Promise((resolve) => {
130
- const t = setTimeout(resolve, ms);
131
- if (typeof t.unref === "function") t.unref();
132
- }),
133
- ]);
127
+ let timeout = null;
128
+ const timeoutPromise = new Promise((resolve) => {
129
+ // Keep this timer ref'ed: drainPendingSynapse() is awaited specifically
130
+ // to hold the process open until either in-flight POSTs settle or the
131
+ // bounded drain budget expires. An unref'ed timer can let Node's test
132
+ // runner/process exit before the Promise resolves when no other handles
133
+ // remain.
134
+ timeout = setTimeout(resolve, ms);
135
+ });
136
+ return Promise.race([settled, timeoutPromise]).finally(() => {
137
+ if (timeout) clearTimeout(timeout);
138
+ });
134
139
  }
135
140
 
136
141
  export function registerSynapseSession(meta, opts = {}) {
@@ -1,4 +1,7 @@
1
1
  // hub/workers/codex-mcp.mjs — Codex MCP 서버 래퍼
2
+ import { readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
2
5
  import process from "node:process";
3
6
  import { fileURLToPath } from "node:url";
4
7
 
@@ -94,13 +97,78 @@ function normalizeStructuredContent(structuredContent, fallbackText = "") {
94
97
  return { threadId, content };
95
98
  }
96
99
 
97
- function buildCodexArguments(prompt, opts = {}) {
100
+ /**
101
+ * Read a top-level TOML scalar (`key = "value"` / `key = 'value'` /
102
+ * `key = value`), tolerating an inline `# comment` and surrounding whitespace.
103
+ * Returns the trimmed value, or null when the key is absent/empty. Intentionally
104
+ * tiny — the per-profile files only carry `model` + `model_reasoning_effort`.
105
+ *
106
+ * @param {string} raw
107
+ * @param {string} key
108
+ * @returns {string|null}
109
+ */
110
+ function readTomlScalar(raw, key) {
111
+ const re = new RegExp(
112
+ `^\\s*${key}\\s*=\\s*(?:"([^"\\n]*)"|'([^'\\n]*)'|([^#\\n]*?))\\s*(?:#.*)?$`,
113
+ "m",
114
+ );
115
+ const match = raw.match(re);
116
+ if (!match) return null;
117
+ const value = (match[1] ?? match[2] ?? match[3] ?? "").trim();
118
+ return value || null;
119
+ }
120
+
121
+ /**
122
+ * Resolve a Codex effort profile name (e.g. "gpt55_high") to its concrete
123
+ * `model` + reasoning effort. SSOT = the per-profile file written by
124
+ * `tfx setup` at `~/.codex/<profile>.config.toml`. Falls back to deriving the
125
+ * effort from the `<...>_<effort>` naming convention when the file is absent.
126
+ *
127
+ * codex 0.137 removed `profile` from the Codex MCP tool schema, so we translate
128
+ * the profile into the still-accepted `model` + `config` fields instead of
129
+ * forwarding `profile` (which now hard-fails with "unknown field `profile`").
130
+ *
131
+ * @param {string} profileName
132
+ * @returns {{ model: string|null, reasoningEffort: string|null }}
133
+ */
134
+ export function resolveCodexProfileConfig(profileName) {
135
+ const result = { model: null, reasoningEffort: null };
136
+ if (typeof profileName !== "string" || !profileName) return result;
137
+
138
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
139
+ try {
140
+ const raw = readFileSync(
141
+ join(codexHome, `${profileName}.config.toml`),
142
+ "utf8",
143
+ );
144
+ result.model = readTomlScalar(raw, "model");
145
+ result.reasoningEffort = readTomlScalar(raw, "model_reasoning_effort");
146
+ } catch {
147
+ // Profile file absent (non-file alias). Derive effort from the naming
148
+ // convention; leave model unset so codex uses its config.toml default.
149
+ const suffix = /_(xhigh|high|med|medium|low)$/.exec(profileName);
150
+ if (suffix) {
151
+ const map = {
152
+ xhigh: "xhigh",
153
+ high: "high",
154
+ med: "medium",
155
+ medium: "medium",
156
+ low: "low",
157
+ };
158
+ result.reasoningEffort = map[suffix[1]] || null;
159
+ }
160
+ }
161
+ return result;
162
+ }
163
+
164
+ export function buildCodexArguments(prompt, opts = {}) {
98
165
  const args = { prompt };
99
166
 
100
167
  if (typeof opts.cwd === "string" && opts.cwd) args.cwd = opts.cwd;
101
168
  if (typeof opts.model === "string" && opts.model) args.model = opts.model;
102
- if (typeof opts.profile === "string" && opts.profile)
103
- args.profile = opts.profile;
169
+ // NOTE: `profile` is intentionally NOT forwarded as a Codex tool argument.
170
+ // codex 0.137 dropped it from the tool schema; it is resolved into
171
+ // `model` + `config.model_reasoning_effort` just before return (see below).
104
172
  if (typeof opts.approvalPolicy === "string" && opts.approvalPolicy) {
105
173
  args["approval-policy"] = opts.approvalPolicy;
106
174
  }
@@ -120,6 +188,22 @@ function buildCodexArguments(prompt, opts = {}) {
120
188
  args["compact-prompt"] = opts.compactPrompt;
121
189
  }
122
190
 
191
+ // Map the effort profile to model + reasoning effort instead of forwarding the
192
+ // (0.137-rejected) `profile` field. Explicit opts.model wins over the profile;
193
+ // profile effort merges on top of any opts.config.
194
+ if (typeof opts.profile === "string" && opts.profile) {
195
+ const resolved = resolveCodexProfileConfig(opts.profile);
196
+ if (resolved.model && typeof args.model !== "string") {
197
+ args.model = resolved.model;
198
+ }
199
+ if (resolved.reasoningEffort) {
200
+ args.config = {
201
+ ...(args.config || {}),
202
+ model_reasoning_effort: resolved.reasoningEffort,
203
+ };
204
+ }
205
+ }
206
+
123
207
  return args;
124
208
  }
125
209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.29.0",
3
+ "version": "10.30.1",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",