@triflux/core 10.39.0 → 10.40.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.
- package/hub/bridge.mjs +23 -1
- package/hub/cli-adapter-base.mjs +11 -1
- package/hub/codex-adapter.mjs +13 -5
- package/hub/router.mjs +521 -4
- package/package.json +1 -1
- package/scripts/lib/codex-profile-config.mjs +83 -2
package/hub/bridge.mjs
CHANGED
|
@@ -119,6 +119,11 @@ const HUB_OPERATIONS = Object.freeze({
|
|
|
119
119
|
action: "publish",
|
|
120
120
|
httpPath: "/bridge/publish",
|
|
121
121
|
},
|
|
122
|
+
takeoverRole: {
|
|
123
|
+
transport: "command",
|
|
124
|
+
action: "takeover_role",
|
|
125
|
+
httpPath: "/bridge/takeover-role",
|
|
126
|
+
},
|
|
122
127
|
sendInput: {
|
|
123
128
|
transport: "command",
|
|
124
129
|
action: "send_input",
|
|
@@ -365,6 +370,7 @@ export function parseArgs(argv) {
|
|
|
365
370
|
args: argv,
|
|
366
371
|
options: {
|
|
367
372
|
agent: { type: "string" },
|
|
373
|
+
"agent-id": { type: "string" },
|
|
368
374
|
cli: { type: "string" },
|
|
369
375
|
timeout: { type: "string" },
|
|
370
376
|
topics: { type: "string" },
|
|
@@ -387,6 +393,7 @@ export function parseArgs(argv) {
|
|
|
387
393
|
claim: { type: "boolean" },
|
|
388
394
|
actor: { type: "string" },
|
|
389
395
|
command: { type: "string" },
|
|
396
|
+
role: { type: "string" },
|
|
390
397
|
"session-id": { type: "string" },
|
|
391
398
|
reason: { type: "string" },
|
|
392
399
|
type: { type: "string" },
|
|
@@ -703,6 +710,19 @@ async function cmdPublish(args) {
|
|
|
703
710
|
return emitJson(result || unavailableResult());
|
|
704
711
|
}
|
|
705
712
|
|
|
713
|
+
async function cmdTakeoverRole(args) {
|
|
714
|
+
const role = args.role || args[1] || "cto";
|
|
715
|
+
const agentId = args.agent || args["agent-id"] || args[2];
|
|
716
|
+
const outcome = await requestHub(HUB_OPERATIONS.takeoverRole, {
|
|
717
|
+
role,
|
|
718
|
+
agent_id: agentId,
|
|
719
|
+
reason: args.reason || "manual",
|
|
720
|
+
requested_by: args["requested-by"] || "bridge",
|
|
721
|
+
});
|
|
722
|
+
const result = outcome?.result;
|
|
723
|
+
return emitJson(result || unavailableResult());
|
|
724
|
+
}
|
|
725
|
+
|
|
706
726
|
async function cmdSendInput(args) {
|
|
707
727
|
const outcome = await requestHub(HUB_OPERATIONS.sendInput, {
|
|
708
728
|
session_id: args["session-id"],
|
|
@@ -1554,6 +1574,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1554
1574
|
return await cmdHandoff(args);
|
|
1555
1575
|
case "publish":
|
|
1556
1576
|
return await cmdPublish(args);
|
|
1577
|
+
case "takeover-role":
|
|
1578
|
+
return await cmdTakeoverRole(args);
|
|
1557
1579
|
case "send-input":
|
|
1558
1580
|
return await cmdSendInput(args);
|
|
1559
1581
|
case "context":
|
|
@@ -1610,7 +1632,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1610
1632
|
return await cmdRetryStatus(args);
|
|
1611
1633
|
default:
|
|
1612
1634
|
console.error(
|
|
1613
|
-
"사용법: bridge.mjs <register|result|control|handoff|publish|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending|daemon-probe|daemon-attach|daemon-interrupt|retry-run|retry-status> [--옵션]",
|
|
1635
|
+
"사용법: bridge.mjs <register|result|control|handoff|publish|takeover-role|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending|daemon-probe|daemon-attach|daemon-interrupt|retry-run|retry-status> [--옵션]",
|
|
1614
1636
|
);
|
|
1615
1637
|
process.exit(1);
|
|
1616
1638
|
}
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
|
|
7
|
+
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
7
8
|
import { writePromptToTmpFile } from "./lib/prompt-tmp.mjs";
|
|
8
9
|
import { IS_WINDOWS, killProcess } from "./platform.mjs";
|
|
9
10
|
|
|
@@ -172,7 +173,12 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
172
173
|
} = opts;
|
|
173
174
|
|
|
174
175
|
const parts = ["codex"];
|
|
175
|
-
|
|
176
|
+
// Select the effort profile via `-c` config overrides instead of
|
|
177
|
+
// `--profile <name>`. codex 0.134+ rejects `--profile X` whenever config.toml
|
|
178
|
+
// still contains an inline [profiles.X] table (and codex re-injects such
|
|
179
|
+
// tables when it rewrites config.toml), so the `-c model=.. -c
|
|
180
|
+
// model_reasoning_effort=..` form is immune and mutates no config.
|
|
181
|
+
const profileOverrides = profile ? codexProfileConfigOverrides(profile) : [];
|
|
176
182
|
|
|
177
183
|
if (FEATURES.execSubcommand) {
|
|
178
184
|
parts.push("exec");
|
|
@@ -182,6 +188,8 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
182
188
|
parts.push("--output-last-message", resultFile);
|
|
183
189
|
}
|
|
184
190
|
if (FEATURES.colorNever) parts.push("--color", "never");
|
|
191
|
+
for (const override of profileOverrides)
|
|
192
|
+
parts.push("-c", shellQuote(override));
|
|
185
193
|
// NOTE: `codex exec`는 --cwd 플래그를 지원하지 않는다. Node spawn의 cwd
|
|
186
194
|
// 옵션으로 child process의 working directory를 제어한다 (conductor.mjs 참조).
|
|
187
195
|
// opts.cwd는 기록용으로만 받아두고 CLI command에는 반영하지 않는다.
|
|
@@ -194,6 +202,8 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
194
202
|
} else {
|
|
195
203
|
parts.push("--dangerously-bypass-approvals-and-sandbox");
|
|
196
204
|
if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
|
|
205
|
+
for (const override of profileOverrides)
|
|
206
|
+
parts.push("-c", shellQuote(override));
|
|
197
207
|
}
|
|
198
208
|
|
|
199
209
|
const useStdin = resolveStdinPromptMode(stdinPrompt);
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
|
+
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
4
5
|
import {
|
|
5
6
|
buildExecCommand,
|
|
6
7
|
createResult,
|
|
@@ -72,14 +73,21 @@ function buildAttempts(opts, preflight) {
|
|
|
72
73
|
// ── Launch script ───────────────────────────────────────────────
|
|
73
74
|
|
|
74
75
|
function createLaunchScriptText(opts) {
|
|
75
|
-
const parts = [
|
|
76
|
-
|
|
77
|
-
parts.push(
|
|
76
|
+
const parts = [
|
|
77
|
+
"codex",
|
|
78
78
|
"exec",
|
|
79
79
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
80
80
|
"--skip-git-repo-check",
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
];
|
|
82
|
+
// Select the effort profile via `-c` config overrides instead of
|
|
83
|
+
// `--profile <name>` (see buildExecCommand): codex 0.134+ rejects `--profile
|
|
84
|
+
// X` whenever config.toml still contains an inline [profiles.X] table.
|
|
85
|
+
if (opts.profile) {
|
|
86
|
+
for (const override of codexProfileConfigOverrides(opts.profile)) {
|
|
87
|
+
parts.push("-c", shellQuote(override));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
parts.push('$(cat "$PROMPT_FILE")');
|
|
83
91
|
return [
|
|
84
92
|
"#!/usr/bin/env bash",
|
|
85
93
|
"set -euo pipefail",
|
package/hub/router.mjs
CHANGED
|
@@ -4,6 +4,11 @@ import { EventEmitter, once } from "node:events";
|
|
|
4
4
|
import { uuidv7 } from "./lib/uuidv7.mjs";
|
|
5
5
|
|
|
6
6
|
const ASSIGN_PENDING_STATUSES = new Set(["queued", "running"]);
|
|
7
|
+
const ROLE_TOPICS = new Set(["cto"]);
|
|
8
|
+
const ROLE_SOURCE_RANK = Object.freeze({
|
|
9
|
+
explicit: 2,
|
|
10
|
+
"topic-fallback": 1,
|
|
11
|
+
});
|
|
7
12
|
|
|
8
13
|
function uniqueStrings(values = []) {
|
|
9
14
|
return Array.from(
|
|
@@ -13,6 +18,37 @@ function uniqueStrings(values = []) {
|
|
|
13
18
|
);
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
function normalizeRoleName(value) {
|
|
22
|
+
const role = String(value || "")
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
return ROLE_TOPICS.has(role) ? role : "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeRoleValues(value) {
|
|
29
|
+
if (Array.isArray(value))
|
|
30
|
+
return uniqueStrings(value).map((item) => item.toLowerCase());
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
return uniqueStrings(value.split(/[,\s]+/u)).map((item) =>
|
|
33
|
+
item.toLowerCase(),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rolePriority(metadata = {}, roleName = "") {
|
|
40
|
+
const candidates = [
|
|
41
|
+
metadata?.[`${roleName}_priority`],
|
|
42
|
+
metadata?.role_priority,
|
|
43
|
+
metadata?.priority,
|
|
44
|
+
];
|
|
45
|
+
for (const value of candidates) {
|
|
46
|
+
const numeric = Number(value);
|
|
47
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
16
52
|
function clampAssignDuration(
|
|
17
53
|
value,
|
|
18
54
|
fallback = 600000,
|
|
@@ -90,6 +126,7 @@ export function createRouter(store) {
|
|
|
90
126
|
const runtimeTopics = new Map();
|
|
91
127
|
const queuesByAgent = new Map();
|
|
92
128
|
const liveMessages = new Map();
|
|
129
|
+
const roleStates = new Map();
|
|
93
130
|
const MAX_LATENCY_SAMPLES = 100;
|
|
94
131
|
let latencyIdx = 0;
|
|
95
132
|
const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
|
|
@@ -123,6 +160,340 @@ export function createRouter(store) {
|
|
|
123
160
|
return normalizeAgentTopics(store, agentId, runtimeTopics.get(agentId));
|
|
124
161
|
}
|
|
125
162
|
|
|
163
|
+
function ensureRoleState(roleName) {
|
|
164
|
+
const role = normalizeRoleName(roleName);
|
|
165
|
+
if (!role) return null;
|
|
166
|
+
if (!roleStates.has(role)) {
|
|
167
|
+
roleStates.set(role, {
|
|
168
|
+
role,
|
|
169
|
+
leaderAgentId: null,
|
|
170
|
+
previousLeaderAgentId: null,
|
|
171
|
+
leaderEpoch: 0,
|
|
172
|
+
leaderSource: null,
|
|
173
|
+
status: "offline",
|
|
174
|
+
candidates: new Map(),
|
|
175
|
+
lastTransitionMs: null,
|
|
176
|
+
lastReason: "init",
|
|
177
|
+
transferredCount: 0,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return roleStates.get(role);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function hasRoleCapability(capabilities = [], roleName) {
|
|
184
|
+
const normalized = normalizeRoleValues(capabilities);
|
|
185
|
+
return normalized.some(
|
|
186
|
+
(capability) =>
|
|
187
|
+
capability === roleName ||
|
|
188
|
+
capability === `role:${roleName}` ||
|
|
189
|
+
capability === `${roleName}:leader` ||
|
|
190
|
+
capability === `${roleName}:candidate`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getRoleCandidateSource(agent, topics, roleName) {
|
|
195
|
+
if (!agent) return null;
|
|
196
|
+
const metadata = agent.metadata || {};
|
|
197
|
+
const roles = [
|
|
198
|
+
...normalizeRoleValues(metadata.role),
|
|
199
|
+
...normalizeRoleValues(metadata.roles),
|
|
200
|
+
];
|
|
201
|
+
const explicit =
|
|
202
|
+
roles.includes(roleName) ||
|
|
203
|
+
metadata?.[roleName] === true ||
|
|
204
|
+
metadata?.is_cto === true ||
|
|
205
|
+
hasRoleCapability(agent.capabilities, roleName);
|
|
206
|
+
if (explicit) return "explicit";
|
|
207
|
+
return topics.includes(roleName) ? "topic-fallback" : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildRoleCandidate(agentId, roleName) {
|
|
211
|
+
const role = normalizeRoleName(roleName);
|
|
212
|
+
if (!agentId || !role) return null;
|
|
213
|
+
const agent = store.getAgent(agentId);
|
|
214
|
+
if (!agent) return null;
|
|
215
|
+
const topics = listRuntimeTopics(agentId);
|
|
216
|
+
const source = getRoleCandidateSource(agent, topics, role);
|
|
217
|
+
if (!source) return null;
|
|
218
|
+
return {
|
|
219
|
+
agent_id: agent.agent_id,
|
|
220
|
+
status: agent.status,
|
|
221
|
+
source,
|
|
222
|
+
priority: rolePriority(agent.metadata, role),
|
|
223
|
+
last_seen_ms: agent.last_seen_ms || 0,
|
|
224
|
+
lease_expires_ms: agent.lease_expires_ms || 0,
|
|
225
|
+
topics,
|
|
226
|
+
capabilities: agent.capabilities || [],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function refreshRoleCandidateForAgent(agentId) {
|
|
231
|
+
if (!agentId) return;
|
|
232
|
+
for (const roleName of ROLE_TOPICS) {
|
|
233
|
+
const role = ensureRoleState(roleName);
|
|
234
|
+
const candidate = buildRoleCandidate(agentId, roleName);
|
|
235
|
+
if (candidate) role.candidates.set(agentId, candidate);
|
|
236
|
+
else role.candidates.delete(agentId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function refreshRoleCandidates(roleName) {
|
|
241
|
+
const role = ensureRoleState(roleName);
|
|
242
|
+
if (!role) return [];
|
|
243
|
+
const ids = new Set(role.candidates.keys());
|
|
244
|
+
for (const [agentId, topics] of runtimeTopics) {
|
|
245
|
+
if (topics.has(role.role)) ids.add(agentId);
|
|
246
|
+
}
|
|
247
|
+
for (const agent of store.getAgentsByTopic(role.role)) {
|
|
248
|
+
ids.add(agent.agent_id);
|
|
249
|
+
}
|
|
250
|
+
if (typeof store.listAllAgents === "function") {
|
|
251
|
+
for (const agent of store.listAllAgents()) ids.add(agent.agent_id);
|
|
252
|
+
}
|
|
253
|
+
for (const agentId of ids) refreshRoleCandidateForAgent(agentId);
|
|
254
|
+
return Array.from(role.candidates.values());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isCandidateLive(candidate, now = Date.now()) {
|
|
258
|
+
return (
|
|
259
|
+
candidate?.status === "online" && Number(candidate.lease_expires_ms) > now
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sortRoleCandidates(candidates = []) {
|
|
264
|
+
return [...candidates].sort((left, right) => {
|
|
265
|
+
const sourceDelta =
|
|
266
|
+
(ROLE_SOURCE_RANK[right.source] || 0) -
|
|
267
|
+
(ROLE_SOURCE_RANK[left.source] || 0);
|
|
268
|
+
if (sourceDelta !== 0) return sourceDelta;
|
|
269
|
+
const priorityDelta =
|
|
270
|
+
Number(right.priority || 0) - Number(left.priority || 0);
|
|
271
|
+
if (priorityDelta !== 0) return priorityDelta;
|
|
272
|
+
const seenDelta =
|
|
273
|
+
Number(right.last_seen_ms || 0) - Number(left.last_seen_ms || 0);
|
|
274
|
+
if (seenDelta !== 0) return seenDelta;
|
|
275
|
+
return String(left.agent_id).localeCompare(String(right.agent_id));
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function chooseRoleLeader(roleName) {
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
return (
|
|
282
|
+
sortRoleCandidates(refreshRoleCandidates(roleName)).find((candidate) =>
|
|
283
|
+
isCandidateLive(candidate, now),
|
|
284
|
+
) || null
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function messageMatchesRole(record, roleName) {
|
|
289
|
+
const message = record?.message || {};
|
|
290
|
+
const to = message.to_agent ?? message.to;
|
|
291
|
+
return to === `topic:${roleName}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function roleTopicForMessage(message = {}) {
|
|
295
|
+
const to = message?.to_agent ?? message?.to;
|
|
296
|
+
if (!String(to || "").startsWith("topic:")) return "";
|
|
297
|
+
return normalizeRoleName(String(to).slice(6));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isReplayAllowedForAgent(agentId, message = {}) {
|
|
301
|
+
const roleName = roleTopicForMessage(message);
|
|
302
|
+
if (!roleName) return true;
|
|
303
|
+
const targetAgentId = String(agentId || "").trim();
|
|
304
|
+
if (!targetAgentId) return false;
|
|
305
|
+
const role = getRoleSnapshot(roleName);
|
|
306
|
+
return role?.leader_agent_id === targetAgentId;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function shouldTrackWithoutRecipients(message) {
|
|
310
|
+
const to = message?.to_agent ?? message?.to;
|
|
311
|
+
if (!String(to || "").startsWith("topic:")) return false;
|
|
312
|
+
return ROLE_TOPICS.has(String(to).slice(6));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isRoleMessageFullyHandled(record, roleName) {
|
|
316
|
+
if (!messageMatchesRole(record, roleName)) return true;
|
|
317
|
+
return (
|
|
318
|
+
record.recipients.size > 0 &&
|
|
319
|
+
record.ackedBy.size >= record.recipients.size
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isRoleRecipient(agentId, roleName, previousLeaderAgentId = null) {
|
|
324
|
+
if (!agentId) return false;
|
|
325
|
+
if (agentId === previousLeaderAgentId) return true;
|
|
326
|
+
return Boolean(buildRoleCandidate(agentId, roleName));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function countPendingForRole(roleName) {
|
|
330
|
+
const now = Date.now();
|
|
331
|
+
let count = 0;
|
|
332
|
+
for (const record of liveMessages.values()) {
|
|
333
|
+
if (!messageMatchesRole(record, roleName)) continue;
|
|
334
|
+
if (record.message.expires_at_ms <= now) continue;
|
|
335
|
+
if (isRoleMessageFullyHandled(record, roleName)) continue;
|
|
336
|
+
count += 1;
|
|
337
|
+
}
|
|
338
|
+
return count;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function transferRoleBacklog(
|
|
342
|
+
roleName,
|
|
343
|
+
newLeaderAgentId,
|
|
344
|
+
previousLeaderAgentId,
|
|
345
|
+
) {
|
|
346
|
+
const role = ensureRoleState(roleName);
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
const stats = { transferred_count: 0, skipped_count: 0, removed_count: 0 };
|
|
349
|
+
if (!role || !newLeaderAgentId) return stats;
|
|
350
|
+
|
|
351
|
+
for (const record of liveMessages.values()) {
|
|
352
|
+
const { message, recipients, ackedBy } = record;
|
|
353
|
+
if (!messageMatchesRole(record, role.role)) continue;
|
|
354
|
+
if (message.expires_at_ms <= now) {
|
|
355
|
+
stats.skipped_count += 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (isRoleMessageFullyHandled(record, role.role)) {
|
|
359
|
+
stats.skipped_count += 1;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const recipient of Array.from(recipients)) {
|
|
364
|
+
if (
|
|
365
|
+
recipient !== newLeaderAgentId &&
|
|
366
|
+
isRoleRecipient(recipient, role.role, previousLeaderAgentId)
|
|
367
|
+
) {
|
|
368
|
+
queuesByAgent.get(recipient)?.delete(message.id);
|
|
369
|
+
recipients.delete(recipient);
|
|
370
|
+
ackedBy.delete(recipient);
|
|
371
|
+
stats.removed_count += 1;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
recipients.add(newLeaderAgentId);
|
|
376
|
+
const alreadyQueued = queuesByAgent
|
|
377
|
+
.get(newLeaderAgentId)
|
|
378
|
+
?.has(message.id);
|
|
379
|
+
if (!alreadyQueued && !ackedBy.has(newLeaderAgentId)) {
|
|
380
|
+
queueMessage(newLeaderAgentId, message);
|
|
381
|
+
stats.transferred_count += 1;
|
|
382
|
+
} else {
|
|
383
|
+
stats.skipped_count += 1;
|
|
384
|
+
}
|
|
385
|
+
message.status = "delivered";
|
|
386
|
+
store.updateMessageStatus(message.id, "delivered");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
role.transferredCount += stats.transferred_count;
|
|
390
|
+
return stats;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildRoleSnapshot(roleName, { refreshCandidates = true } = {}) {
|
|
394
|
+
const role = ensureRoleState(roleName);
|
|
395
|
+
if (!role) return null;
|
|
396
|
+
const now = Date.now();
|
|
397
|
+
const candidates = sortRoleCandidates(
|
|
398
|
+
refreshCandidates
|
|
399
|
+
? refreshRoleCandidates(role.role)
|
|
400
|
+
: Array.from(role.candidates.values()),
|
|
401
|
+
);
|
|
402
|
+
const leader =
|
|
403
|
+
role.leaderAgentId && buildRoleCandidate(role.leaderAgentId, role.role);
|
|
404
|
+
const leaderLive = isCandidateLive(leader, now);
|
|
405
|
+
const liveCandidates = candidates.filter((candidate) =>
|
|
406
|
+
isCandidateLive(candidate, now),
|
|
407
|
+
);
|
|
408
|
+
return {
|
|
409
|
+
role: role.role,
|
|
410
|
+
status: leaderLive
|
|
411
|
+
? "active"
|
|
412
|
+
: liveCandidates.length
|
|
413
|
+
? "electable"
|
|
414
|
+
: "offline",
|
|
415
|
+
leader_agent_id: leaderLive ? role.leaderAgentId : null,
|
|
416
|
+
previous_leader_agent_id: role.previousLeaderAgentId || null,
|
|
417
|
+
leader_epoch: role.leaderEpoch,
|
|
418
|
+
lease_expires_ms: leaderLive ? leader.lease_expires_ms : null,
|
|
419
|
+
candidate_source: leaderLive
|
|
420
|
+
? leader.source
|
|
421
|
+
: liveCandidates[0]?.source || null,
|
|
422
|
+
candidate_count: candidates.length,
|
|
423
|
+
live_candidate_count: liveCandidates.length,
|
|
424
|
+
pending_count: countPendingForRole(role.role),
|
|
425
|
+
transferred_count: role.transferredCount,
|
|
426
|
+
last_transition_ms: role.lastTransitionMs,
|
|
427
|
+
last_reason: role.lastReason,
|
|
428
|
+
candidates: candidates.slice(0, 16).map((candidate) => ({
|
|
429
|
+
agent_id: candidate.agent_id,
|
|
430
|
+
status: isCandidateLive(candidate, now) ? "online" : candidate.status,
|
|
431
|
+
source: candidate.source,
|
|
432
|
+
priority: candidate.priority,
|
|
433
|
+
last_seen_ms: candidate.last_seen_ms,
|
|
434
|
+
lease_expires_ms: candidate.lease_expires_ms,
|
|
435
|
+
})),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function ensureRoleLeader(
|
|
440
|
+
roleName,
|
|
441
|
+
{ reason = "ensure", transferBacklog = true } = {},
|
|
442
|
+
) {
|
|
443
|
+
const role = ensureRoleState(roleName);
|
|
444
|
+
if (!role) return null;
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
const current =
|
|
447
|
+
role.leaderAgentId && buildRoleCandidate(role.leaderAgentId, role.role);
|
|
448
|
+
const best = chooseRoleLeader(role.role);
|
|
449
|
+
const currentLive = isCandidateLive(current, now);
|
|
450
|
+
const shouldPreemptFallback =
|
|
451
|
+
currentLive &&
|
|
452
|
+
current?.source === "topic-fallback" &&
|
|
453
|
+
best?.source === "explicit" &&
|
|
454
|
+
best.agent_id !== current.agent_id;
|
|
455
|
+
|
|
456
|
+
if (currentLive && !shouldPreemptFallback) {
|
|
457
|
+
role.status = "active";
|
|
458
|
+
role.leaderSource = current.source;
|
|
459
|
+
return buildRoleSnapshot(role.role);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!best) {
|
|
463
|
+
if (role.leaderAgentId) {
|
|
464
|
+
role.previousLeaderAgentId = role.leaderAgentId;
|
|
465
|
+
role.leaderAgentId = null;
|
|
466
|
+
role.leaderSource = null;
|
|
467
|
+
role.leaderEpoch += 1;
|
|
468
|
+
role.lastTransitionMs = Date.now();
|
|
469
|
+
role.lastReason = reason;
|
|
470
|
+
}
|
|
471
|
+
role.status = "offline";
|
|
472
|
+
return buildRoleSnapshot(role.role);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (role.leaderAgentId !== best.agent_id) {
|
|
476
|
+
const previousLeaderAgentId = role.leaderAgentId;
|
|
477
|
+
role.previousLeaderAgentId =
|
|
478
|
+
previousLeaderAgentId || role.previousLeaderAgentId;
|
|
479
|
+
role.leaderAgentId = best.agent_id;
|
|
480
|
+
role.leaderSource = best.source;
|
|
481
|
+
role.leaderEpoch += 1;
|
|
482
|
+
role.status = "active";
|
|
483
|
+
role.lastTransitionMs = Date.now();
|
|
484
|
+
role.lastReason = reason;
|
|
485
|
+
if (transferBacklog) {
|
|
486
|
+
transferRoleBacklog(role.role, best.agent_id, previousLeaderAgentId);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return buildRoleSnapshot(role.role);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function getRoleSnapshot(roleName) {
|
|
494
|
+
return buildRoleSnapshot(roleName, { refreshCandidates: true });
|
|
495
|
+
}
|
|
496
|
+
|
|
126
497
|
function trackMessage(message, recipients) {
|
|
127
498
|
liveMessages.set(message.id, {
|
|
128
499
|
message,
|
|
@@ -162,6 +533,14 @@ export function createRouter(store) {
|
|
|
162
533
|
}
|
|
163
534
|
|
|
164
535
|
const topic = to.slice(6);
|
|
536
|
+
if (ROLE_TOPICS.has(topic)) {
|
|
537
|
+
const role = ensureRoleLeader(topic, {
|
|
538
|
+
reason: "route",
|
|
539
|
+
transferBacklog: true,
|
|
540
|
+
});
|
|
541
|
+
return role?.leader_agent_id ? [role.leader_agent_id] : [];
|
|
542
|
+
}
|
|
543
|
+
|
|
165
544
|
const recipients = new Set();
|
|
166
545
|
for (const [agentId, topics] of runtimeTopics) {
|
|
167
546
|
if (topics.has(topic)) recipients.add(agentId);
|
|
@@ -258,8 +637,10 @@ export function createRouter(store) {
|
|
|
258
637
|
correlation_id,
|
|
259
638
|
});
|
|
260
639
|
const recipients = uniqueStrings(resolveRecipients(msg));
|
|
261
|
-
if (recipients.length) {
|
|
640
|
+
if (recipients.length || shouldTrackWithoutRecipients(msg)) {
|
|
262
641
|
trackMessage(msg, recipients);
|
|
642
|
+
}
|
|
643
|
+
if (recipients.length) {
|
|
263
644
|
for (const agentId of recipients) {
|
|
264
645
|
queueMessage(agentId, msg);
|
|
265
646
|
}
|
|
@@ -422,15 +803,37 @@ export function createRouter(store) {
|
|
|
422
803
|
registerAgent(args) {
|
|
423
804
|
const result = store.registerAgent(args);
|
|
424
805
|
upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
|
|
806
|
+
refreshRoleCandidateForAgent(args.agent_id);
|
|
807
|
+
for (const roleName of ROLE_TOPICS) {
|
|
808
|
+
ensureRoleLeader(roleName, {
|
|
809
|
+
reason: "register",
|
|
810
|
+
transferBacklog: true,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
425
813
|
return result;
|
|
426
814
|
},
|
|
427
815
|
|
|
428
816
|
refreshAgentLease(agentId, ttlMs = 30000) {
|
|
429
|
-
|
|
817
|
+
const result = store.refreshLease(agentId, ttlMs);
|
|
818
|
+
refreshRoleCandidateForAgent(agentId);
|
|
819
|
+
for (const roleName of ROLE_TOPICS) {
|
|
820
|
+
ensureRoleLeader(roleName, {
|
|
821
|
+
reason: "heartbeat",
|
|
822
|
+
transferBacklog: true,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
return result;
|
|
430
826
|
},
|
|
431
827
|
|
|
432
828
|
subscribeAgent(agentId, topics, { replace = false } = {}) {
|
|
433
829
|
const nextTopics = upsertRuntimeTopics(agentId, topics, { replace });
|
|
830
|
+
refreshRoleCandidateForAgent(agentId);
|
|
831
|
+
for (const roleName of ROLE_TOPICS) {
|
|
832
|
+
ensureRoleLeader(roleName, {
|
|
833
|
+
reason: "subscribe",
|
|
834
|
+
transferBacklog: true,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
434
837
|
return { agent_id: agentId, topics: nextTopics };
|
|
435
838
|
},
|
|
436
839
|
|
|
@@ -442,12 +845,25 @@ export function createRouter(store) {
|
|
|
442
845
|
if (status === "offline") {
|
|
443
846
|
runtimeTopics.delete(agentId);
|
|
444
847
|
}
|
|
445
|
-
|
|
848
|
+
const updated = store.updateAgentStatus(agentId, status);
|
|
849
|
+
refreshRoleCandidateForAgent(agentId);
|
|
850
|
+
for (const roleName of ROLE_TOPICS) {
|
|
851
|
+
ensureRoleLeader(roleName, {
|
|
852
|
+
reason: `status:${status}`,
|
|
853
|
+
transferBacklog: true,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
return updated;
|
|
446
857
|
},
|
|
447
858
|
|
|
448
859
|
route(msg) {
|
|
449
860
|
const recipients = uniqueStrings(resolveRecipients(msg));
|
|
450
|
-
if (!recipients.length)
|
|
861
|
+
if (!recipients.length) {
|
|
862
|
+
if (shouldTrackWithoutRecipients(msg) && !getMessageRecord(msg.id)) {
|
|
863
|
+
trackMessage(msg, []);
|
|
864
|
+
}
|
|
865
|
+
return 0;
|
|
866
|
+
}
|
|
451
867
|
if (!getMessageRecord(msg.id)) {
|
|
452
868
|
trackMessage(msg, recipients);
|
|
453
869
|
}
|
|
@@ -462,6 +878,10 @@ export function createRouter(store) {
|
|
|
462
878
|
return sortedPending(agentId, options);
|
|
463
879
|
},
|
|
464
880
|
|
|
881
|
+
canReplayMessageForAgent(agentId, message) {
|
|
882
|
+
return isReplayAllowedForAgent(agentId, message);
|
|
883
|
+
},
|
|
884
|
+
|
|
465
885
|
countPendingMessages(agentId) {
|
|
466
886
|
const queue = ensureAgentQueue(agentId);
|
|
467
887
|
const now = Date.now();
|
|
@@ -610,6 +1030,87 @@ export function createRouter(store) {
|
|
|
610
1030
|
message_id: msg.id,
|
|
611
1031
|
fanout_count: recipients.length,
|
|
612
1032
|
expires_at_ms: msg.expires_at_ms,
|
|
1033
|
+
role: to === "topic:cto" ? getRoleSnapshot("cto") : undefined,
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
takeoverRole({
|
|
1039
|
+
role = "cto",
|
|
1040
|
+
agent_id,
|
|
1041
|
+
reason = "manual",
|
|
1042
|
+
requested_by = "manual",
|
|
1043
|
+
} = {}) {
|
|
1044
|
+
const roleName = normalizeRoleName(role);
|
|
1045
|
+
if (!roleName) {
|
|
1046
|
+
return {
|
|
1047
|
+
ok: false,
|
|
1048
|
+
error: {
|
|
1049
|
+
code: "ROLE_NOT_SUPPORTED",
|
|
1050
|
+
message: `unsupported role: ${role}`,
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const targetAgentId = String(agent_id || "").trim();
|
|
1055
|
+
if (!targetAgentId) {
|
|
1056
|
+
return {
|
|
1057
|
+
ok: false,
|
|
1058
|
+
error: {
|
|
1059
|
+
code: "AGENT_ID_REQUIRED",
|
|
1060
|
+
message: "agent_id required",
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
refreshRoleCandidates(roleName);
|
|
1066
|
+
const roleState = ensureRoleState(roleName);
|
|
1067
|
+
const candidate = buildRoleCandidate(targetAgentId, roleName);
|
|
1068
|
+
if (!isCandidateLive(candidate)) {
|
|
1069
|
+
return {
|
|
1070
|
+
ok: false,
|
|
1071
|
+
error: {
|
|
1072
|
+
code: "ROLE_CANDIDATE_UNAVAILABLE",
|
|
1073
|
+
message: `${targetAgentId} is not a live ${roleName} candidate`,
|
|
1074
|
+
},
|
|
1075
|
+
data: {
|
|
1076
|
+
role: buildRoleSnapshot(roleName),
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const previousLeaderAgentId =
|
|
1082
|
+
roleState.leaderAgentId && roleState.leaderAgentId !== targetAgentId
|
|
1083
|
+
? roleState.leaderAgentId
|
|
1084
|
+
: roleState.previousLeaderAgentId || null;
|
|
1085
|
+
const changed = roleState.leaderAgentId !== targetAgentId;
|
|
1086
|
+
if (changed) {
|
|
1087
|
+
roleState.previousLeaderAgentId = previousLeaderAgentId || null;
|
|
1088
|
+
roleState.leaderAgentId = targetAgentId;
|
|
1089
|
+
roleState.leaderSource = candidate.source;
|
|
1090
|
+
roleState.leaderEpoch += 1;
|
|
1091
|
+
roleState.status = "active";
|
|
1092
|
+
roleState.lastTransitionMs = Date.now();
|
|
1093
|
+
roleState.lastReason = reason || "manual";
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const transfer = transferRoleBacklog(
|
|
1097
|
+
roleName,
|
|
1098
|
+
targetAgentId,
|
|
1099
|
+
previousLeaderAgentId,
|
|
1100
|
+
);
|
|
1101
|
+
const snapshot = buildRoleSnapshot(roleName);
|
|
1102
|
+
return {
|
|
1103
|
+
ok: true,
|
|
1104
|
+
data: {
|
|
1105
|
+
role: roleName,
|
|
1106
|
+
requested_by,
|
|
1107
|
+
reason,
|
|
1108
|
+
changed,
|
|
1109
|
+
previous_leader_agent_id: previousLeaderAgentId,
|
|
1110
|
+
leader_agent_id: snapshot.leader_agent_id,
|
|
1111
|
+
leader_epoch: snapshot.leader_epoch,
|
|
1112
|
+
...transfer,
|
|
1113
|
+
status: snapshot,
|
|
613
1114
|
},
|
|
614
1115
|
};
|
|
615
1116
|
},
|
|
@@ -897,6 +1398,9 @@ export function createRouter(store) {
|
|
|
897
1398
|
realtime_transport: "named-pipe",
|
|
898
1399
|
audit_store: store.type || "sqlite",
|
|
899
1400
|
};
|
|
1401
|
+
data.roles = {
|
|
1402
|
+
cto: getRoleSnapshot("cto"),
|
|
1403
|
+
};
|
|
900
1404
|
if (include_metrics) {
|
|
901
1405
|
const depths = router.getQueueDepths();
|
|
902
1406
|
const stats = router.getDeliveryStats();
|
|
@@ -919,6 +1423,7 @@ export function createRouter(store) {
|
|
|
919
1423
|
if (scope === "agent" && agent_id) {
|
|
920
1424
|
const agent = store.getAgent(agent_id);
|
|
921
1425
|
if (agent) {
|
|
1426
|
+
refreshRoleCandidateForAgent(agent_id);
|
|
922
1427
|
data.agent = {
|
|
923
1428
|
agent_id: agent.agent_id,
|
|
924
1429
|
status: agent.status,
|
|
@@ -926,6 +1431,18 @@ export function createRouter(store) {
|
|
|
926
1431
|
last_seen_ms: agent.last_seen_ms,
|
|
927
1432
|
topics: listRuntimeTopics(agent_id),
|
|
928
1433
|
};
|
|
1434
|
+
const roles = {};
|
|
1435
|
+
for (const roleName of ROLE_TOPICS) {
|
|
1436
|
+
const candidate = buildRoleCandidate(agent_id, roleName);
|
|
1437
|
+
if (candidate) {
|
|
1438
|
+
roles[roleName] = {
|
|
1439
|
+
candidate_source: candidate.source,
|
|
1440
|
+
priority: candidate.priority,
|
|
1441
|
+
leader: getRoleSnapshot(roleName)?.leader_agent_id === agent_id,
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (Object.keys(roles).length) data.agent.roles = roles;
|
|
929
1446
|
}
|
|
930
1447
|
}
|
|
931
1448
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
2
9
|
import { dirname, join } from "node:path";
|
|
3
10
|
|
|
11
|
+
const EFFORT_BY_SUFFIX = {
|
|
12
|
+
xhigh: "xhigh",
|
|
13
|
+
high: "high",
|
|
14
|
+
med: "medium",
|
|
15
|
+
medium: "medium",
|
|
16
|
+
low: "low",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function readProfileScalar(raw, key) {
|
|
20
|
+
const match = String(raw).match(
|
|
21
|
+
new RegExp(
|
|
22
|
+
`^\\s*${key}\\s*=\\s*(?:"([^"\\n]*)"|'([^'\\n]*)'|([^#\\n]*?))\\s*(?:#.*)?$`,
|
|
23
|
+
"m",
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
const value = (match[1] ?? match[2] ?? match[3] ?? "").trim();
|
|
28
|
+
return value || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a Codex effort profile name (e.g. "gpt55_high") to the `-c key=value`
|
|
33
|
+
* config overrides that `codex exec` accepts, so the headless CLI lanes can
|
|
34
|
+
* select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
|
|
35
|
+
* `--profile X` whenever config.toml still contains an inline `[profiles.X]`
|
|
36
|
+
* table (which codex itself re-injects when it rewrites config.toml), so the
|
|
37
|
+
* `-c` form is immune and needs no config mutation / sanitize race.
|
|
38
|
+
*
|
|
39
|
+
* SSOT = the per-profile file `~/.codex/<name>.config.toml` written by
|
|
40
|
+
* `tfx setup`; falls back to deriving the effort from the `<...>_<effort>`
|
|
41
|
+
* naming convention when the file is absent. Returns the keys that resolve, in
|
|
42
|
+
* `key="value"` TOML-scalar form ready to be shell-quoted by the caller.
|
|
43
|
+
*
|
|
44
|
+
* NOTE: the Codex MCP transport does the structured-arg equivalent in
|
|
45
|
+
* hub/workers/codex-mcp.mjs (resolveCodexProfileConfig → model + config object);
|
|
46
|
+
* this returns the CLI `-c` string form for the same per-profile file.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} profileName
|
|
49
|
+
* @param {{ codexHome?: string }} [opts]
|
|
50
|
+
* @returns {string[]} e.g. ['model="gpt-5.5"', 'model_reasoning_effort="high"']
|
|
51
|
+
*/
|
|
52
|
+
export function codexProfileConfigOverrides(profileName, opts = {}) {
|
|
53
|
+
if (typeof profileName !== "string" || !profileName) return [];
|
|
54
|
+
const codexHome =
|
|
55
|
+
opts.codexHome || process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
56
|
+
let model = null;
|
|
57
|
+
let effort = null;
|
|
58
|
+
try {
|
|
59
|
+
const raw = readFileSync(
|
|
60
|
+
join(codexHome, `${profileName}.config.toml`),
|
|
61
|
+
"utf8",
|
|
62
|
+
);
|
|
63
|
+
model = readProfileScalar(raw, "model");
|
|
64
|
+
effort = readProfileScalar(raw, "model_reasoning_effort");
|
|
65
|
+
} catch {
|
|
66
|
+
const suffix = /_(xhigh|high|med|medium|low)$/.exec(profileName);
|
|
67
|
+
if (suffix) effort = EFFORT_BY_SUFFIX[suffix[1]] || null;
|
|
68
|
+
}
|
|
69
|
+
const overrides = [];
|
|
70
|
+
if (model) overrides.push(`model="${model}"`);
|
|
71
|
+
if (effort) overrides.push(`model_reasoning_effort="${effort}"`);
|
|
72
|
+
return overrides;
|
|
73
|
+
}
|
|
74
|
+
|
|
4
75
|
const PROFILE_SECTION_PREFIX = "profiles.";
|
|
5
76
|
const SAFE_PROFILE_FILE_RE = /^[A-Za-z0-9_.-]+$/;
|
|
6
77
|
|
|
@@ -137,6 +208,16 @@ export function sanitizeCodexProfileConfigFile(
|
|
|
137
208
|
const sanitized = sanitizeCodexProfileConfig(source, { codexHome });
|
|
138
209
|
if (!sanitized.changed) return sanitized;
|
|
139
210
|
writeFileSync(`${configPath}.${backupPrefix}-${stamp(now)}`, source, "utf8");
|
|
140
|
-
|
|
211
|
+
// Atomic replace. config.toml is shared global state: a codex session may be
|
|
212
|
+
// reading it while another entry point (tfx-route, the codex session hook, or
|
|
213
|
+
// a parallel swarm worker) sanitizes it. Write the sanitized content to a
|
|
214
|
+
// unique temp file in the same directory, then rename — rename is atomic on
|
|
215
|
+
// the same filesystem, so no reader ever observes a torn/half-written config.
|
|
216
|
+
// Concurrent sanitizers each rename their own complete temp; last writer wins
|
|
217
|
+
// without corruption (the content is identical anyway since the migration is
|
|
218
|
+
// deterministic). The pid keeps temp names unique across separate processes.
|
|
219
|
+
const tmpPath = `${configPath}.tmp-${process.pid}-${stamp(now)}`;
|
|
220
|
+
writeFileSync(tmpPath, sanitized.toml, "utf8");
|
|
221
|
+
renameSync(tmpPath, configPath);
|
|
141
222
|
return sanitized;
|
|
142
223
|
}
|