triflux 10.9.14 → 10.9.16
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/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +22 -0
- package/config/mcp-registry.json +29 -0
- package/hub/lib/spawn-trace.mjs +39 -9
- package/hub/server.mjs +91 -0
- package/hub/team/conductor.mjs +117 -32
- package/hub/team/headless.mjs +90 -8
- package/hub/team/synapse-http.mjs +59 -0
- package/hub/team/synapse-registry.mjs +11 -5
- package/package.json +56 -21
- package/scripts/__tests__/spawn-trace.test.mjs +50 -14
- package/scripts/tfx-route.sh +14 -11
- package/tui/codex-profile.mjs +457 -0
- package/tui/core.mjs +266 -0
- package/tui/doctor.mjs +375 -0
- package/tui/gemini-profile.mjs +299 -0
- package/tui/monitor-data.mjs +152 -0
- package/tui/monitor.mjs +339 -0
- package/tui/setup.mjs +598 -0
- package/CLAUDE.md +0 -212
- package/references/hosts.json +0 -46
- package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
- package/skills/tfx-workspace/evals/evals.json +0 -79
- package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/review.html +0 -1325
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/review.html +0 -1325
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
- package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
- package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "triflux",
|
|
4
|
+
"description": "CLI-first multi-model orchestrator — Codex/Gemini/Claude routing with DAG execution, auto-triage, and cost optimization",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "tellang"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "triflux",
|
|
11
|
+
"description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
|
|
12
|
+
"version": "10.9.6",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "tellang"
|
|
15
|
+
},
|
|
16
|
+
"source": {
|
|
17
|
+
"source": "npm",
|
|
18
|
+
"package": "triflux"
|
|
19
|
+
},
|
|
20
|
+
"category": "productivity",
|
|
21
|
+
"homepage": "https://github.com/tellang/triflux",
|
|
22
|
+
"tags": [
|
|
23
|
+
"multi-model",
|
|
24
|
+
"codex",
|
|
25
|
+
"gemini",
|
|
26
|
+
"cli-routing",
|
|
27
|
+
"orchestration",
|
|
28
|
+
"cost-optimization",
|
|
29
|
+
"dag-execution"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"version": "10.9.14"
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "triflux",
|
|
3
|
+
"version": "10.3.4",
|
|
4
|
+
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "tellang"
|
|
7
|
+
},
|
|
8
|
+
"repository": "https://github.com/tellang/triflux",
|
|
9
|
+
"homepage": "https://github.com/tellang/triflux",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"claude-code",
|
|
13
|
+
"plugin",
|
|
14
|
+
"codex",
|
|
15
|
+
"gemini",
|
|
16
|
+
"cli-routing",
|
|
17
|
+
"orchestration",
|
|
18
|
+
"multi-model"
|
|
19
|
+
],
|
|
20
|
+
"skills": "./skills/",
|
|
21
|
+
"hooks": "./hooks/hooks.json"
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "mcp-registry-schema",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "MCP 서버 중앙 레지스트리 — 진실의 원천",
|
|
5
|
+
"defaults": {
|
|
6
|
+
"transport": "hub-url",
|
|
7
|
+
"hub_base": "http://127.0.0.1:27888"
|
|
8
|
+
},
|
|
9
|
+
"servers": {
|
|
10
|
+
"tfx-hub": {
|
|
11
|
+
"transport": "hub-url",
|
|
12
|
+
"url": "http://127.0.0.1:27888/mcp",
|
|
13
|
+
"safe": true,
|
|
14
|
+
"targets": ["claude", "gemini", "codex"],
|
|
15
|
+
"description": "triflux Hub MCP 서버"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"policies": {
|
|
19
|
+
"stdio_action": "replace-with-hub",
|
|
20
|
+
"unknown_server_action": "warn",
|
|
21
|
+
"watched_paths": [
|
|
22
|
+
"~/.gemini/settings.json",
|
|
23
|
+
"~/.codex/config.toml",
|
|
24
|
+
"~/.claude/settings.json",
|
|
25
|
+
"~/.claude/settings.local.json",
|
|
26
|
+
".mcp.json"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
package/hub/lib/spawn-trace.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
const LOG_DIR = join(homedir(), ".triflux", "logs");
|
|
8
8
|
const DEDUPE_WINDOW_MS = 5_000;
|
|
9
9
|
const RATE_WINDOW_MS = 1_000;
|
|
10
|
-
export
|
|
10
|
+
export let MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
11
11
|
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
12
12
|
30,
|
|
13
13
|
);
|
|
@@ -149,6 +149,18 @@ function createPolicyError(reasonCode, message, meta = {}) {
|
|
|
149
149
|
return error;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
export function getMaxSpawnPerSec() {
|
|
153
|
+
return MAX_SPAWN_PER_SEC;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function reload() {
|
|
157
|
+
MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
158
|
+
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
159
|
+
30,
|
|
160
|
+
);
|
|
161
|
+
return getMaxSpawnPerSec();
|
|
162
|
+
}
|
|
163
|
+
|
|
152
164
|
function logBlocked(traceId, command, args, options, error, extra = {}) {
|
|
153
165
|
appendTrace({
|
|
154
166
|
event: "blocked",
|
|
@@ -167,6 +179,7 @@ function enforceGuards(command, args, options) {
|
|
|
167
179
|
const now = Date.now();
|
|
168
180
|
trimRecentSpawnTimes(now);
|
|
169
181
|
trimDedupeEntries(now);
|
|
182
|
+
const maxSpawnPerSec = getMaxSpawnPerSec();
|
|
170
183
|
|
|
171
184
|
const dedupeKey = getDedupeKey(options);
|
|
172
185
|
if (dedupeKey) {
|
|
@@ -180,11 +193,11 @@ function enforceGuards(command, args, options) {
|
|
|
180
193
|
}
|
|
181
194
|
}
|
|
182
195
|
|
|
183
|
-
if (recentSpawnTimes.length >=
|
|
196
|
+
if (recentSpawnTimes.length >= maxSpawnPerSec) {
|
|
184
197
|
return createPolicyError(
|
|
185
198
|
"rate_limit",
|
|
186
|
-
`spawn-trace rate limit exceeded (${
|
|
187
|
-
{ maxPerSec:
|
|
199
|
+
`spawn-trace rate limit exceeded (${maxSpawnPerSec}/sec)`,
|
|
200
|
+
{ maxPerSec: maxSpawnPerSec },
|
|
188
201
|
);
|
|
189
202
|
}
|
|
190
203
|
|
|
@@ -365,7 +378,13 @@ export function execFile(file, args, options, callback) {
|
|
|
365
378
|
normalized.options,
|
|
366
379
|
);
|
|
367
380
|
if (blockedError) {
|
|
368
|
-
logBlocked(
|
|
381
|
+
logBlocked(
|
|
382
|
+
traceId,
|
|
383
|
+
file,
|
|
384
|
+
normalized.argsList,
|
|
385
|
+
normalized.options,
|
|
386
|
+
blockedError,
|
|
387
|
+
);
|
|
369
388
|
if (typeof normalized.callback === "function") {
|
|
370
389
|
queueMicrotask(() => normalized.callback(blockedError, "", ""));
|
|
371
390
|
return createRejectedChild(file, normalized.argsList, blockedError);
|
|
@@ -417,9 +436,16 @@ export function execFileSync(file, args, options) {
|
|
|
417
436
|
normalized.options,
|
|
418
437
|
);
|
|
419
438
|
if (blockedError) {
|
|
420
|
-
logBlocked(
|
|
421
|
-
|
|
422
|
-
|
|
439
|
+
logBlocked(
|
|
440
|
+
traceId,
|
|
441
|
+
file,
|
|
442
|
+
normalized.argsList,
|
|
443
|
+
normalized.options,
|
|
444
|
+
blockedError,
|
|
445
|
+
{
|
|
446
|
+
sync: true,
|
|
447
|
+
},
|
|
448
|
+
);
|
|
423
449
|
throw blockedError;
|
|
424
450
|
}
|
|
425
451
|
|
|
@@ -491,6 +517,10 @@ export default {
|
|
|
491
517
|
spawn,
|
|
492
518
|
execFile,
|
|
493
519
|
execFileSync,
|
|
494
|
-
MAX_SPAWN_PER_SEC
|
|
520
|
+
get MAX_SPAWN_PER_SEC() {
|
|
521
|
+
return MAX_SPAWN_PER_SEC;
|
|
522
|
+
},
|
|
495
523
|
MAX_TOTAL_DESCENDANTS,
|
|
524
|
+
getMaxSpawnPerSec,
|
|
525
|
+
reload,
|
|
496
526
|
};
|
package/hub/server.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { createAssignCallbackServer } from "./assign-callbacks.mjs";
|
|
|
28
28
|
import { DelegatorService } from "./delegator/index.mjs";
|
|
29
29
|
import { createHitlManager } from "./hitl.mjs";
|
|
30
30
|
import { cleanupOrphanNodeProcesses } from "./lib/process-utils.mjs";
|
|
31
|
+
import * as spawnTrace from "./lib/spawn-trace.mjs";
|
|
31
32
|
import { wrapRequestHandler } from "./middleware/request-logger.mjs";
|
|
32
33
|
import { createPipeServer } from "./pipe.mjs";
|
|
33
34
|
import { createRouter } from "./router.mjs";
|
|
@@ -593,6 +594,20 @@ export async function startHub({
|
|
|
593
594
|
locks: swarmLocks,
|
|
594
595
|
});
|
|
595
596
|
|
|
597
|
+
// Synapse Layer 5: emitter subscribers — bridge events to hub logging
|
|
598
|
+
synapseEmitter.on("synapse.session.started", ({ sessionId }) => {
|
|
599
|
+
hubLog.info({ sessionId }, "synapse.session.started");
|
|
600
|
+
});
|
|
601
|
+
synapseEmitter.on("synapse.session.heartbeat", ({ sessionId }) => {
|
|
602
|
+
hubLog.debug({ sessionId }, "synapse.session.heartbeat");
|
|
603
|
+
});
|
|
604
|
+
synapseEmitter.on("synapse.session.stale", ({ sessionId }) => {
|
|
605
|
+
hubLog.warn({ sessionId }, "synapse.session.stale");
|
|
606
|
+
});
|
|
607
|
+
synapseEmitter.on("synapse.session.removed", ({ sessionId }) => {
|
|
608
|
+
hubLog.info({ sessionId }, "synapse.session.removed");
|
|
609
|
+
});
|
|
610
|
+
|
|
596
611
|
const hitl = createHitlManager(store, router);
|
|
597
612
|
const pipe = createPipeServer({
|
|
598
613
|
router,
|
|
@@ -760,6 +775,82 @@ export async function startHub({
|
|
|
760
775
|
return writeJson(res, 200, { ok: true, accounts });
|
|
761
776
|
}
|
|
762
777
|
|
|
778
|
+
if (path === "/spawn-trace/reload" && req.method === "POST") {
|
|
779
|
+
return writeJson(res, 200, {
|
|
780
|
+
ok: true,
|
|
781
|
+
max_spawn_per_sec: spawnTrace.reload(),
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ── Synapse Layer 5: session registry + locks + preflight routes ──
|
|
786
|
+
if (path === "/synapse/sessions" && req.method === "GET") {
|
|
787
|
+
return writeJson(res, 200, { ok: true, ...synapseRegistry.snapshot(), ts: Date.now() });
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (path === "/synapse/register" && req.method === "POST") {
|
|
791
|
+
try {
|
|
792
|
+
const body = await parseBody(req);
|
|
793
|
+
const { sessionId } = body || {};
|
|
794
|
+
const result = synapseRegistry.register(sessionId, body);
|
|
795
|
+
if (!result?.ok) {
|
|
796
|
+
throw new Error(result?.reason || "register failed");
|
|
797
|
+
}
|
|
798
|
+
return writeJson(res, 200, { ok: true, sessionId: result.sessionId || sessionId });
|
|
799
|
+
} catch (err) {
|
|
800
|
+
return writeJson(res, 400, { ok: false, error: String(err?.message || err) });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (path === "/synapse/heartbeat" && req.method === "POST") {
|
|
805
|
+
try {
|
|
806
|
+
const body = await parseBody(req);
|
|
807
|
+
const { sessionId, partial } = body || {};
|
|
808
|
+
const ok = synapseRegistry.heartbeat(sessionId, partial);
|
|
809
|
+
if (!ok) {
|
|
810
|
+
throw new Error("heartbeat failed");
|
|
811
|
+
}
|
|
812
|
+
return writeJson(res, 200, { ok: true });
|
|
813
|
+
} catch (err) {
|
|
814
|
+
return writeJson(res, 400, { ok: false, error: String(err?.message || err) });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (path === "/synapse/unregister" && req.method === "POST") {
|
|
819
|
+
try {
|
|
820
|
+
const body = await parseBody(req);
|
|
821
|
+
const { sessionId } = body || {};
|
|
822
|
+
const ok = synapseRegistry.unregister(sessionId);
|
|
823
|
+
if (!ok) {
|
|
824
|
+
throw new Error("unregister failed");
|
|
825
|
+
}
|
|
826
|
+
return writeJson(res, 200, { ok: true });
|
|
827
|
+
} catch (err) {
|
|
828
|
+
return writeJson(res, 400, { ok: false, error: String(err?.message || err) });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (path === "/synapse/locks" && req.method === "GET") {
|
|
833
|
+
return writeJson(res, 200, { ok: true, locks: swarmLocks.snapshot(), ts: Date.now() });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (path === "/synapse/preflight" && req.method === "POST") {
|
|
837
|
+
const VALID_OPS = new Set(["checkout", "rebase", "cherry-pick", "reset", "stash-pop", "worktree-remove"]);
|
|
838
|
+
try {
|
|
839
|
+
const body = await parseBody(req);
|
|
840
|
+
const { op, args = {}, sessionContext = {} } = body;
|
|
841
|
+
if (!op || typeof op !== "string") {
|
|
842
|
+
return writeJson(res, 400, { ok: false, error: "op 필수" });
|
|
843
|
+
}
|
|
844
|
+
if (!VALID_OPS.has(op)) {
|
|
845
|
+
return writeJson(res, 400, { ok: false, error: `invalid op: ${op}` });
|
|
846
|
+
}
|
|
847
|
+
const result = gitPreflight.check(op, args, sessionContext);
|
|
848
|
+
return writeJson(res, 200, { ok: true, ...result });
|
|
849
|
+
} catch (err) {
|
|
850
|
+
return writeJson(res, 400, { ok: false, error: String(err?.message || err) });
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
763
854
|
if (path.startsWith("/bridge")) {
|
|
764
855
|
const isBridgeStatusGet =
|
|
765
856
|
path === "/bridge/status" && req.method === "GET";
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
// 3. Auto-restart (maxRestarts=3)
|
|
9
9
|
// 4. JSONL event log (블랙박스 리코더)
|
|
10
10
|
|
|
11
|
-
import { execFile, spawn } from "../lib/spawn-trace.mjs";
|
|
12
11
|
import { EventEmitter } from "node:events";
|
|
13
12
|
import {
|
|
14
13
|
copyFileSync,
|
|
@@ -20,6 +19,7 @@ import { homedir } from "node:os";
|
|
|
20
19
|
import { dirname, join } from "node:path";
|
|
21
20
|
import { createRegistry } from "../../mesh/mesh-registry.mjs";
|
|
22
21
|
import { broker } from "../account-broker.mjs";
|
|
22
|
+
import { execFile, spawn } from "../lib/spawn-trace.mjs";
|
|
23
23
|
import { killProcess } from "../platform.mjs";
|
|
24
24
|
import { createConductorMeshBridge } from "./conductor-mesh-bridge.mjs";
|
|
25
25
|
import {
|
|
@@ -28,8 +28,13 @@ import {
|
|
|
28
28
|
} from "./conductor-registry.mjs";
|
|
29
29
|
import { createEventLog } from "./event-log.mjs";
|
|
30
30
|
import { createHealthProbe } from "./health-probe.mjs";
|
|
31
|
-
import { buildLauncher
|
|
32
|
-
import {
|
|
31
|
+
import { buildLauncher } from "./launcher-template.mjs";
|
|
32
|
+
import {
|
|
33
|
+
buildSynapseTaskSummary,
|
|
34
|
+
heartbeatSynapseSession,
|
|
35
|
+
registerSynapseSession,
|
|
36
|
+
unregisterSynapseSession,
|
|
37
|
+
} from "./synapse-http.mjs";
|
|
33
38
|
|
|
34
39
|
/** 세션 상태 */
|
|
35
40
|
export const STATES = Object.freeze({
|
|
@@ -66,6 +71,40 @@ const TERMINAL_STATES = new Set([STATES.DEAD, STATES.COMPLETED]);
|
|
|
66
71
|
const DEFAULT_MAX_RESTARTS = 3;
|
|
67
72
|
const DEFAULT_GRACE_MS = 10_000;
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Auth 파일을 lease 소스에서 CLI 대상 경로로 복사.
|
|
76
|
+
* @param {object} lease — broker lease 객체 (mode, authFile 필수)
|
|
77
|
+
* @param {'codex'|'gemini'} agent
|
|
78
|
+
* @param {string} sessionId — 로깅용
|
|
79
|
+
* @param {object} eventLog — createEventLog 인스턴스
|
|
80
|
+
*/
|
|
81
|
+
function swapAuthFile(lease, agent, sessionId, eventLog) {
|
|
82
|
+
if (lease?.mode !== "auth" || !lease.authFile) return true;
|
|
83
|
+
const dests =
|
|
84
|
+
agent === "codex"
|
|
85
|
+
? [join(homedir(), ".codex", "auth.json")]
|
|
86
|
+
: [
|
|
87
|
+
join(homedir(), ".gemini", "oauth_creds.json"),
|
|
88
|
+
join(homedir(), ".gemini", "gemini-credentials.json"),
|
|
89
|
+
];
|
|
90
|
+
let allOk = true;
|
|
91
|
+
for (const dest of dests) {
|
|
92
|
+
try {
|
|
93
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
94
|
+
copyFileSync(lease.authFile, dest);
|
|
95
|
+
eventLog.append("auth_copy", { session: sessionId, agent, dest });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
allOk = false;
|
|
98
|
+
eventLog.append("auth_copy_error", {
|
|
99
|
+
session: sessionId,
|
|
100
|
+
dest,
|
|
101
|
+
error: err.message,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return allOk;
|
|
106
|
+
}
|
|
107
|
+
|
|
69
108
|
/**
|
|
70
109
|
* Conductor 팩토리.
|
|
71
110
|
* @param {object} opts
|
|
@@ -73,6 +112,7 @@ const DEFAULT_GRACE_MS = 10_000;
|
|
|
73
112
|
* @param {number} [opts.maxRestarts=3]
|
|
74
113
|
* @param {number} [opts.graceMs=10000] — shutdown grace period
|
|
75
114
|
* @param {object} [opts.probeOpts] — health-probe 옵션 오버라이드
|
|
115
|
+
* @param {object} [opts.broker] — AccountBroker 인스턴스 (tierFallback 구독용)
|
|
76
116
|
* @returns {Conductor}
|
|
77
117
|
*/
|
|
78
118
|
export function createConductor(opts = {}) {
|
|
@@ -81,6 +121,7 @@ export function createConductor(opts = {}) {
|
|
|
81
121
|
maxRestarts = DEFAULT_MAX_RESTARTS,
|
|
82
122
|
graceMs = DEFAULT_GRACE_MS,
|
|
83
123
|
probeOpts = {},
|
|
124
|
+
broker: optsBroker,
|
|
84
125
|
} = opts;
|
|
85
126
|
|
|
86
127
|
if (!logsDir) throw new Error("logsDir is required");
|
|
@@ -90,6 +131,25 @@ export function createConductor(opts = {}) {
|
|
|
90
131
|
const sessions = new Map();
|
|
91
132
|
let shuttingDown = false;
|
|
92
133
|
const publicApi = null;
|
|
134
|
+
const synapseOpts = {
|
|
135
|
+
baseUrl: opts.synapseBaseUrl,
|
|
136
|
+
fetchImpl: opts.synapseFetch,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function buildSynapseMeta(session, state = session.state, reason = "") {
|
|
140
|
+
return {
|
|
141
|
+
sessionId: session.id,
|
|
142
|
+
host:
|
|
143
|
+
typeof session.config.host === "string" &&
|
|
144
|
+
session.config.host.length > 0
|
|
145
|
+
? session.config.host
|
|
146
|
+
: "local",
|
|
147
|
+
taskSummary: buildSynapseTaskSummary(session.config.prompt),
|
|
148
|
+
status: state,
|
|
149
|
+
reason,
|
|
150
|
+
isRemote: Boolean(session.config.remote),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
93
153
|
|
|
94
154
|
// 공유 event log (모든 세션 이벤트를 하나의 JSONL에)
|
|
95
155
|
const eventLog = createEventLog(join(logsDir, "conductor-events.jsonl"));
|
|
@@ -130,6 +190,21 @@ export function createConductor(opts = {}) {
|
|
|
130
190
|
reason,
|
|
131
191
|
});
|
|
132
192
|
|
|
193
|
+
if (nextState === STATES.HEALTHY) {
|
|
194
|
+
registerSynapseSession(
|
|
195
|
+
buildSynapseMeta(session, nextState, reason),
|
|
196
|
+
synapseOpts,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
heartbeatSynapseSession(
|
|
200
|
+
session.id,
|
|
201
|
+
buildSynapseMeta(session, nextState, reason),
|
|
202
|
+
synapseOpts,
|
|
203
|
+
);
|
|
204
|
+
if (nextState === STATES.COMPLETED || nextState === STATES.DEAD) {
|
|
205
|
+
unregisterSynapseSession(session.id, synapseOpts);
|
|
206
|
+
}
|
|
207
|
+
|
|
133
208
|
// Terminal state cleanup
|
|
134
209
|
if (TERMINAL_STATES.has(nextState)) {
|
|
135
210
|
session.probe?.stop();
|
|
@@ -536,13 +611,16 @@ export function createConductor(opts = {}) {
|
|
|
536
611
|
} else if (agent === "gemini") {
|
|
537
612
|
remoteBin = "gemini -y";
|
|
538
613
|
} else {
|
|
539
|
-
remoteBin =
|
|
614
|
+
remoteBin =
|
|
615
|
+
"codex exec -s danger-full-access --dangerously-bypass-approvals-and-sandbox";
|
|
540
616
|
}
|
|
541
617
|
|
|
542
618
|
// prompt는 stdin으로 전달 — 셸 이스케이프 문제 완전 회피
|
|
543
619
|
const sshArgs = [
|
|
544
|
-
"-o",
|
|
545
|
-
"
|
|
620
|
+
"-o",
|
|
621
|
+
"ConnectTimeout=30",
|
|
622
|
+
"-o",
|
|
623
|
+
"BatchMode=yes",
|
|
546
624
|
host,
|
|
547
625
|
`${cdPrefix}${remoteBin}`,
|
|
548
626
|
];
|
|
@@ -739,32 +817,7 @@ export function createConductor(opts = {}) {
|
|
|
739
817
|
: config;
|
|
740
818
|
|
|
741
819
|
// auth file copy — broker resolved absolute path, conductor does the actual copy
|
|
742
|
-
|
|
743
|
-
const dests =
|
|
744
|
-
config.agent === "codex"
|
|
745
|
-
? [join(homedir(), ".codex", "auth.json")]
|
|
746
|
-
: [
|
|
747
|
-
join(homedir(), ".gemini", "oauth_creds.json"),
|
|
748
|
-
join(homedir(), ".gemini", "gemini-credentials.json"),
|
|
749
|
-
];
|
|
750
|
-
for (const dest of dests) {
|
|
751
|
-
try {
|
|
752
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
753
|
-
copyFileSync(lease.authFile, dest);
|
|
754
|
-
eventLog.append("auth_copy", {
|
|
755
|
-
session: config.id,
|
|
756
|
-
agent: config.agent,
|
|
757
|
-
dest,
|
|
758
|
-
});
|
|
759
|
-
} catch (err) {
|
|
760
|
-
eventLog.append("auth_copy_error", {
|
|
761
|
-
session: config.id,
|
|
762
|
-
dest,
|
|
763
|
-
error: err.message,
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
820
|
+
swapAuthFile(lease, config.agent, config.id, eventLog);
|
|
768
821
|
|
|
769
822
|
// 원격 세션은 launcher 불필요 (이미 원격에서 실행 중)
|
|
770
823
|
const launcher = resolvedConfig.remote
|
|
@@ -892,12 +945,44 @@ export function createConductor(opts = {}) {
|
|
|
892
945
|
});
|
|
893
946
|
|
|
894
947
|
await Promise.allSettled(cleanups);
|
|
948
|
+
process.removeListener("SIGINT", onSignal);
|
|
949
|
+
process.removeListener("SIGTERM", onSignal);
|
|
950
|
+
if (brokerInstance && onTierFallback) {
|
|
951
|
+
brokerInstance.removeListener("tierFallback", onTierFallback);
|
|
952
|
+
}
|
|
895
953
|
if (conductor._meshBridge) conductor._meshBridge.detach();
|
|
896
954
|
await eventLog.flush();
|
|
897
955
|
await eventLog.close();
|
|
898
956
|
emitter.emit("shutdown");
|
|
899
957
|
}
|
|
900
958
|
|
|
959
|
+
// ── broker tierFallback 구독 ──────────────────────────────────
|
|
960
|
+
// 토큰 만료 등으로 상위 tier 계정이 불가능해지면, 활성 세션의 auth를 새 lease로 교체.
|
|
961
|
+
const brokerInstance = optsBroker ?? null;
|
|
962
|
+
const onTierFallback = brokerInstance
|
|
963
|
+
? ({ provider }) => {
|
|
964
|
+
for (const session of sessions.values()) {
|
|
965
|
+
if (TERMINAL_STATES.has(session.state)) continue;
|
|
966
|
+
if (session.config.agent !== provider) continue;
|
|
967
|
+
if (session.config.remote) continue;
|
|
968
|
+
|
|
969
|
+
const newLease = brokerInstance.lease({ provider });
|
|
970
|
+
if (!newLease) continue;
|
|
971
|
+
|
|
972
|
+
swapAuthFile(newLease, provider, session.id, eventLog);
|
|
973
|
+
eventLog.append("tier_fallback_swap", {
|
|
974
|
+
session: session.id,
|
|
975
|
+
agent: provider,
|
|
976
|
+
newAccount: newLease.id,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
: null;
|
|
981
|
+
|
|
982
|
+
if (brokerInstance && onTierFallback) {
|
|
983
|
+
brokerInstance.on("tierFallback", onTierFallback);
|
|
984
|
+
}
|
|
985
|
+
|
|
901
986
|
// Shutdown traps
|
|
902
987
|
const onSignal = () => {
|
|
903
988
|
void shutdown("signal");
|