triflux 10.9.13 → 10.9.15
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 +1 -1
- package/hub/server.mjs +62 -0
- package/hub/team/conductor.mjs +69 -26
- package/hub/team/synapse-registry.mjs +51 -22
- package/package.json +56 -21
- package/scripts/tfx-route.sh +8 -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
|
@@ -9,7 +9,7 @@ const DEDUPE_WINDOW_MS = 5_000;
|
|
|
9
9
|
const RATE_WINDOW_MS = 1_000;
|
|
10
10
|
export const MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
11
11
|
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
12
|
-
|
|
12
|
+
30,
|
|
13
13
|
);
|
|
14
14
|
export const MAX_TOTAL_DESCENDANTS = resolvePositiveInteger(
|
|
15
15
|
process.env.TRIFLUX_MAX_DESCENDANTS,
|
package/hub/server.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { execSync as execSyncHub } from "node:child_process";
|
|
4
4
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
5
6
|
import {
|
|
6
7
|
existsSync,
|
|
7
8
|
mkdirSync,
|
|
@@ -40,7 +41,10 @@ import {
|
|
|
40
41
|
writeState,
|
|
41
42
|
} from "./state.mjs";
|
|
42
43
|
import { createStoreAdapter } from "./store-adapter.mjs";
|
|
44
|
+
import { createGitPreflight } from "./team/git-preflight.mjs";
|
|
43
45
|
import { nativeProxy } from "./team/nativeProxy.mjs";
|
|
46
|
+
import { createSwarmLocks } from "./team/swarm-locks.mjs";
|
|
47
|
+
import { createSynapseRegistry } from "./team/synapse-registry.mjs";
|
|
44
48
|
import { registerTeamBridge } from "./team-bridge.mjs";
|
|
45
49
|
import { createTools } from "./tools.mjs";
|
|
46
50
|
import { createDelegatorMcpWorker } from "./workers/delegator-mcp.mjs";
|
|
@@ -573,6 +577,36 @@ export async function startHub({
|
|
|
573
577
|
}
|
|
574
578
|
const delegatorService = new DelegatorService({ worker: delegatorWorker });
|
|
575
579
|
|
|
580
|
+
// Synapse Layer 4: session registry + git preflight + swarm locks
|
|
581
|
+
const synapseEmitter = new EventEmitter();
|
|
582
|
+
synapseEmitter.setMaxListeners(50);
|
|
583
|
+
const synapseRegistry = createSynapseRegistry({
|
|
584
|
+
persistPath: join(CACHE_DIR, "tfx-hub", "synapse-sessions.json"),
|
|
585
|
+
emitter: synapseEmitter,
|
|
586
|
+
});
|
|
587
|
+
const swarmLocks = createSwarmLocks({
|
|
588
|
+
repoRoot: PROJECT_ROOT,
|
|
589
|
+
persistPath: join(CACHE_DIR, "tfx-hub", "swarm-locks.json"),
|
|
590
|
+
});
|
|
591
|
+
const gitPreflight = createGitPreflight({
|
|
592
|
+
registry: synapseRegistry,
|
|
593
|
+
locks: swarmLocks,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Synapse Layer 5: emitter subscribers — bridge events to hub logging
|
|
597
|
+
synapseEmitter.on("synapse.session.started", ({ sessionId }) => {
|
|
598
|
+
hubLog.info({ sessionId }, "synapse.session.started");
|
|
599
|
+
});
|
|
600
|
+
synapseEmitter.on("synapse.session.heartbeat", ({ sessionId }) => {
|
|
601
|
+
hubLog.debug({ sessionId }, "synapse.session.heartbeat");
|
|
602
|
+
});
|
|
603
|
+
synapseEmitter.on("synapse.session.stale", ({ sessionId }) => {
|
|
604
|
+
hubLog.warn({ sessionId }, "synapse.session.stale");
|
|
605
|
+
});
|
|
606
|
+
synapseEmitter.on("synapse.session.removed", ({ sessionId }) => {
|
|
607
|
+
hubLog.info({ sessionId }, "synapse.session.removed");
|
|
608
|
+
});
|
|
609
|
+
|
|
576
610
|
const hitl = createHitlManager(store, router);
|
|
577
611
|
const pipe = createPipeServer({
|
|
578
612
|
router,
|
|
@@ -740,6 +774,33 @@ export async function startHub({
|
|
|
740
774
|
return writeJson(res, 200, { ok: true, accounts });
|
|
741
775
|
}
|
|
742
776
|
|
|
777
|
+
// ── Synapse Layer 5: session registry + locks + preflight routes ──
|
|
778
|
+
if (path === "/synapse/sessions" && req.method === "GET") {
|
|
779
|
+
return writeJson(res, 200, { ok: true, ...synapseRegistry.snapshot(), ts: Date.now() });
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (path === "/synapse/locks" && req.method === "GET") {
|
|
783
|
+
return writeJson(res, 200, { ok: true, locks: swarmLocks.snapshot(), ts: Date.now() });
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (path === "/synapse/preflight" && req.method === "POST") {
|
|
787
|
+
const VALID_OPS = new Set(["checkout", "rebase", "cherry-pick", "reset", "stash-pop", "worktree-remove"]);
|
|
788
|
+
try {
|
|
789
|
+
const body = await parseBody(req);
|
|
790
|
+
const { op, args = {}, sessionContext = {} } = body;
|
|
791
|
+
if (!op || typeof op !== "string") {
|
|
792
|
+
return writeJson(res, 400, { ok: false, error: "op 필수" });
|
|
793
|
+
}
|
|
794
|
+
if (!VALID_OPS.has(op)) {
|
|
795
|
+
return writeJson(res, 400, { ok: false, error: `invalid op: ${op}` });
|
|
796
|
+
}
|
|
797
|
+
const result = gitPreflight.check(op, args, sessionContext);
|
|
798
|
+
return writeJson(res, 200, { ok: true, ...result });
|
|
799
|
+
} catch (err) {
|
|
800
|
+
return writeJson(res, 400, { ok: false, error: String(err?.message || err) });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
743
804
|
if (path.startsWith("/bridge")) {
|
|
744
805
|
const isBridgeStatusGet =
|
|
745
806
|
path === "/bridge/status" && req.method === "GET";
|
|
@@ -1521,6 +1582,7 @@ export async function startHub({
|
|
|
1521
1582
|
await pipe.stop();
|
|
1522
1583
|
await assignCallbacks.stop();
|
|
1523
1584
|
await delegatorWorker.stop().catch(() => {});
|
|
1585
|
+
try { synapseRegistry.destroy(); } catch {}
|
|
1524
1586
|
store.close();
|
|
1525
1587
|
try {
|
|
1526
1588
|
unlinkSync(PID_FILE);
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -66,6 +66,40 @@ const TERMINAL_STATES = new Set([STATES.DEAD, STATES.COMPLETED]);
|
|
|
66
66
|
const DEFAULT_MAX_RESTARTS = 3;
|
|
67
67
|
const DEFAULT_GRACE_MS = 10_000;
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Auth 파일을 lease 소스에서 CLI 대상 경로로 복사.
|
|
71
|
+
* @param {object} lease — broker lease 객체 (mode, authFile 필수)
|
|
72
|
+
* @param {'codex'|'gemini'} agent
|
|
73
|
+
* @param {string} sessionId — 로깅용
|
|
74
|
+
* @param {object} eventLog — createEventLog 인스턴스
|
|
75
|
+
*/
|
|
76
|
+
function swapAuthFile(lease, agent, sessionId, eventLog) {
|
|
77
|
+
if (lease?.mode !== "auth" || !lease.authFile) return true;
|
|
78
|
+
const dests =
|
|
79
|
+
agent === "codex"
|
|
80
|
+
? [join(homedir(), ".codex", "auth.json")]
|
|
81
|
+
: [
|
|
82
|
+
join(homedir(), ".gemini", "oauth_creds.json"),
|
|
83
|
+
join(homedir(), ".gemini", "gemini-credentials.json"),
|
|
84
|
+
];
|
|
85
|
+
let allOk = true;
|
|
86
|
+
for (const dest of dests) {
|
|
87
|
+
try {
|
|
88
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
89
|
+
copyFileSync(lease.authFile, dest);
|
|
90
|
+
eventLog.append("auth_copy", { session: sessionId, agent, dest });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
allOk = false;
|
|
93
|
+
eventLog.append("auth_copy_error", {
|
|
94
|
+
session: sessionId,
|
|
95
|
+
dest,
|
|
96
|
+
error: err.message,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return allOk;
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
/**
|
|
70
104
|
* Conductor 팩토리.
|
|
71
105
|
* @param {object} opts
|
|
@@ -73,6 +107,7 @@ const DEFAULT_GRACE_MS = 10_000;
|
|
|
73
107
|
* @param {number} [opts.maxRestarts=3]
|
|
74
108
|
* @param {number} [opts.graceMs=10000] — shutdown grace period
|
|
75
109
|
* @param {object} [opts.probeOpts] — health-probe 옵션 오버라이드
|
|
110
|
+
* @param {object} [opts.broker] — AccountBroker 인스턴스 (tierFallback 구독용)
|
|
76
111
|
* @returns {Conductor}
|
|
77
112
|
*/
|
|
78
113
|
export function createConductor(opts = {}) {
|
|
@@ -81,6 +116,7 @@ export function createConductor(opts = {}) {
|
|
|
81
116
|
maxRestarts = DEFAULT_MAX_RESTARTS,
|
|
82
117
|
graceMs = DEFAULT_GRACE_MS,
|
|
83
118
|
probeOpts = {},
|
|
119
|
+
broker: optsBroker,
|
|
84
120
|
} = opts;
|
|
85
121
|
|
|
86
122
|
if (!logsDir) throw new Error("logsDir is required");
|
|
@@ -739,32 +775,7 @@ export function createConductor(opts = {}) {
|
|
|
739
775
|
: config;
|
|
740
776
|
|
|
741
777
|
// 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
|
-
}
|
|
778
|
+
swapAuthFile(lease, config.agent, config.id, eventLog);
|
|
768
779
|
|
|
769
780
|
// 원격 세션은 launcher 불필요 (이미 원격에서 실행 중)
|
|
770
781
|
const launcher = resolvedConfig.remote
|
|
@@ -892,12 +903,44 @@ export function createConductor(opts = {}) {
|
|
|
892
903
|
});
|
|
893
904
|
|
|
894
905
|
await Promise.allSettled(cleanups);
|
|
906
|
+
process.removeListener("SIGINT", onSignal);
|
|
907
|
+
process.removeListener("SIGTERM", onSignal);
|
|
908
|
+
if (brokerInstance && onTierFallback) {
|
|
909
|
+
brokerInstance.removeListener("tierFallback", onTierFallback);
|
|
910
|
+
}
|
|
895
911
|
if (conductor._meshBridge) conductor._meshBridge.detach();
|
|
896
912
|
await eventLog.flush();
|
|
897
913
|
await eventLog.close();
|
|
898
914
|
emitter.emit("shutdown");
|
|
899
915
|
}
|
|
900
916
|
|
|
917
|
+
// ── broker tierFallback 구독 ──────────────────────────────────
|
|
918
|
+
// 토큰 만료 등으로 상위 tier 계정이 불가능해지면, 활성 세션의 auth를 새 lease로 교체.
|
|
919
|
+
const brokerInstance = optsBroker ?? null;
|
|
920
|
+
const onTierFallback = brokerInstance
|
|
921
|
+
? ({ provider }) => {
|
|
922
|
+
for (const session of sessions.values()) {
|
|
923
|
+
if (TERMINAL_STATES.has(session.state)) continue;
|
|
924
|
+
if (session.config.agent !== provider) continue;
|
|
925
|
+
if (session.config.remote) continue;
|
|
926
|
+
|
|
927
|
+
const newLease = brokerInstance.lease({ provider });
|
|
928
|
+
if (!newLease) continue;
|
|
929
|
+
|
|
930
|
+
swapAuthFile(newLease, provider, session.id, eventLog);
|
|
931
|
+
eventLog.append("tier_fallback_swap", {
|
|
932
|
+
session: session.id,
|
|
933
|
+
agent: provider,
|
|
934
|
+
newAccount: newLease.id,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
: null;
|
|
939
|
+
|
|
940
|
+
if (brokerInstance && onTierFallback) {
|
|
941
|
+
brokerInstance.on("tierFallback", onTierFallback);
|
|
942
|
+
}
|
|
943
|
+
|
|
901
944
|
// Shutdown traps
|
|
902
945
|
const onSignal = () => {
|
|
903
946
|
void shutdown("signal");
|
|
@@ -42,6 +42,7 @@ function sanitizeSession(raw, fallbackSessionId = "") {
|
|
|
42
42
|
export function createSynapseRegistry(opts = {}) {
|
|
43
43
|
const {
|
|
44
44
|
persistPath,
|
|
45
|
+
emitter = null,
|
|
45
46
|
localHeartbeatIntervalMs = DEFAULT_LOCAL_HEARTBEAT_INTERVAL_MS,
|
|
46
47
|
localTimeoutMs = DEFAULT_LOCAL_TIMEOUT_MS,
|
|
47
48
|
remoteHeartbeatIntervalMs = DEFAULT_REMOTE_HEARTBEAT_INTERVAL_MS,
|
|
@@ -67,6 +68,9 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
67
68
|
return session.isRemote ? remoteTimeoutMs : localTimeoutMs;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
let persistTimer = null;
|
|
72
|
+
let destroyed = false;
|
|
73
|
+
|
|
70
74
|
function persist() {
|
|
71
75
|
if (!persistPath) return;
|
|
72
76
|
try {
|
|
@@ -80,6 +84,15 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
|
|
87
|
+
function schedulePersist() {
|
|
88
|
+
if (destroyed || persistTimer) return;
|
|
89
|
+
persistTimer = setTimeout(() => {
|
|
90
|
+
persistTimer = null;
|
|
91
|
+
if (!destroyed) persist();
|
|
92
|
+
}, 200);
|
|
93
|
+
if (typeof persistTimer.unref === "function") persistTimer.unref();
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
function restore() {
|
|
84
97
|
if (!persistPath || !existsSync(persistPath)) return;
|
|
85
98
|
try {
|
|
@@ -101,10 +114,11 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
function notifyStale(session) {
|
|
104
|
-
|
|
117
|
+
const clone = cloneSession(session);
|
|
118
|
+
emitter?.emit("synapse.session.stale", { sessionId: session.sessionId, session: clone });
|
|
105
119
|
for (const callback of staleCallbacks) {
|
|
106
120
|
try {
|
|
107
|
-
callback(
|
|
121
|
+
callback(clone);
|
|
108
122
|
} catch {
|
|
109
123
|
/* no-op */
|
|
110
124
|
}
|
|
@@ -112,10 +126,11 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
function notifyRemoved(session) {
|
|
115
|
-
|
|
129
|
+
const clone = cloneSession(session);
|
|
130
|
+
emitter?.emit("synapse.session.removed", { sessionId: session.sessionId, session: clone });
|
|
116
131
|
for (const callback of removedCallbacks) {
|
|
117
132
|
try {
|
|
118
|
-
callback(
|
|
133
|
+
callback(clone);
|
|
119
134
|
} catch {
|
|
120
135
|
/* no-op */
|
|
121
136
|
}
|
|
@@ -134,8 +149,10 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
134
149
|
|
|
135
150
|
const elapsedMs = now() - current.lastHeartbeat;
|
|
136
151
|
if (elapsedMs > timeoutFor(current) && current.status !== "stale") {
|
|
137
|
-
current
|
|
138
|
-
|
|
152
|
+
const staled = { ...current, status: "stale" };
|
|
153
|
+
sessions.set(sessionId, staled);
|
|
154
|
+
schedulePersist();
|
|
155
|
+
setImmediate(() => notifyStale(staled));
|
|
139
156
|
}
|
|
140
157
|
}, intervalFor(session));
|
|
141
158
|
|
|
@@ -150,8 +167,13 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
150
167
|
|
|
151
168
|
function register(meta) {
|
|
152
169
|
const sessionId = normalizeSessionId(meta?.sessionId);
|
|
153
|
-
if (!sessionId
|
|
154
|
-
return { ok: false, sessionId };
|
|
170
|
+
if (!sessionId) {
|
|
171
|
+
return { ok: false, sessionId, reason: "invalid_id" };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (sessions.has(sessionId)) {
|
|
175
|
+
console.warn("[synapse-registry] duplicate registration rejected:", sessionId);
|
|
176
|
+
return { ok: false, sessionId, reason: "duplicate" };
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
const session = sanitizeSession(
|
|
@@ -168,7 +190,7 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
168
190
|
startMonitor(sessionId);
|
|
169
191
|
persist();
|
|
170
192
|
|
|
171
|
-
|
|
193
|
+
emitter?.emit("synapse.session.started", { sessionId, session: cloneSession(session) });
|
|
172
194
|
return { ok: true, sessionId };
|
|
173
195
|
}
|
|
174
196
|
|
|
@@ -190,33 +212,35 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
190
212
|
if (!session) return false;
|
|
191
213
|
|
|
192
214
|
const wasRemote = session.isRemote;
|
|
193
|
-
|
|
194
|
-
session.lastHeartbeat = now();
|
|
195
|
-
session.status = "active";
|
|
215
|
+
const updated = { ...session, lastHeartbeat: now(), status: "active" };
|
|
196
216
|
|
|
197
217
|
if (partialMeta && typeof partialMeta === "object") {
|
|
198
|
-
if (typeof partialMeta.host === "string")
|
|
218
|
+
if (typeof partialMeta.host === "string") updated.host = partialMeta.host;
|
|
199
219
|
if (typeof partialMeta.worktreePath === "string") {
|
|
200
|
-
|
|
220
|
+
updated.worktreePath = partialMeta.worktreePath;
|
|
201
221
|
}
|
|
202
|
-
if (typeof partialMeta.branch === "string")
|
|
222
|
+
if (typeof partialMeta.branch === "string") updated.branch = partialMeta.branch;
|
|
203
223
|
if (Array.isArray(partialMeta.dirtyFiles)) {
|
|
204
|
-
|
|
224
|
+
updated.dirtyFiles = partialMeta.dirtyFiles.filter(
|
|
225
|
+
(f) => typeof f === "string" && f.length > 0,
|
|
226
|
+
);
|
|
205
227
|
}
|
|
206
228
|
if (typeof partialMeta.taskSummary === "string") {
|
|
207
|
-
|
|
229
|
+
updated.taskSummary = partialMeta.taskSummary;
|
|
208
230
|
}
|
|
209
231
|
if (typeof partialMeta.isRemote === "boolean") {
|
|
210
|
-
|
|
232
|
+
updated.isRemote = partialMeta.isRemote;
|
|
211
233
|
}
|
|
212
234
|
}
|
|
213
235
|
|
|
214
|
-
|
|
236
|
+
sessions.set(normalized, updated);
|
|
237
|
+
|
|
238
|
+
if (updated.isRemote !== wasRemote) {
|
|
215
239
|
startMonitor(normalized);
|
|
216
240
|
}
|
|
217
241
|
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
schedulePersist();
|
|
243
|
+
emitter?.emit("synapse.session.heartbeat", { sessionId: normalized, session: cloneSession(updated), partial: partialMeta });
|
|
220
244
|
return true;
|
|
221
245
|
}
|
|
222
246
|
|
|
@@ -254,9 +278,14 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
254
278
|
}
|
|
255
279
|
|
|
256
280
|
function destroy() {
|
|
281
|
+
destroyed = true;
|
|
257
282
|
for (const sessionId of monitors.keys()) {
|
|
258
283
|
stopMonitor(sessionId);
|
|
259
284
|
}
|
|
285
|
+
if (persistTimer) {
|
|
286
|
+
clearTimeout(persistTimer);
|
|
287
|
+
persistTimer = null;
|
|
288
|
+
}
|
|
260
289
|
persist();
|
|
261
290
|
}
|
|
262
291
|
|
|
@@ -272,4 +301,4 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
272
301
|
snapshot,
|
|
273
302
|
destroy,
|
|
274
303
|
});
|
|
275
|
-
}
|
|
304
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "10.9.
|
|
3
|
+
"version": "10.9.15",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,26 +13,69 @@
|
|
|
13
13
|
"tfx-doctor-tui": "bin/tfx-doctor-tui.mjs",
|
|
14
14
|
"tfx-setup-tui": "bin/tfx-setup-tui.mjs"
|
|
15
15
|
},
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=18.0.0"
|
|
18
|
-
},
|
|
19
|
-
"dependencies": {
|
|
20
|
-
"@triflux/core": "10.0.1",
|
|
21
|
-
"@triflux/remote": "^10.0.0-alpha.1"
|
|
22
|
-
},
|
|
23
16
|
"files": [
|
|
24
17
|
"bin",
|
|
18
|
+
"tui",
|
|
19
|
+
"hub",
|
|
20
|
+
"config",
|
|
25
21
|
"skills",
|
|
22
|
+
"!skills/tfx-workspace",
|
|
23
|
+
"!**/failure-reports",
|
|
24
|
+
"scripts",
|
|
26
25
|
"hooks",
|
|
27
26
|
"hud",
|
|
28
|
-
"scripts",
|
|
29
|
-
"hub",
|
|
30
27
|
"mesh",
|
|
31
|
-
"
|
|
32
|
-
"CLAUDE.md",
|
|
28
|
+
".claude-plugin",
|
|
33
29
|
"README.md",
|
|
30
|
+
"README.ko.md",
|
|
34
31
|
"LICENSE"
|
|
35
32
|
],
|
|
33
|
+
"workspaces": [
|
|
34
|
+
"packages/core",
|
|
35
|
+
"packages/remote",
|
|
36
|
+
"packages/triflux"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"pack": "node scripts/pack.mjs all",
|
|
40
|
+
"pack:core": "node scripts/pack.mjs core",
|
|
41
|
+
"pack:remote": "node scripts/pack.mjs remote",
|
|
42
|
+
"setup": "node scripts/setup.mjs",
|
|
43
|
+
"preinstall": "node scripts/preinstall.mjs",
|
|
44
|
+
"postinstall": "node scripts/setup.mjs",
|
|
45
|
+
"lint": "biome check .",
|
|
46
|
+
"lint:fix": "biome check --fix .",
|
|
47
|
+
"health": "npm test && npm run lint",
|
|
48
|
+
"test": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
|
|
49
|
+
"test:unit": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/unit/**/*.test.mjs",
|
|
50
|
+
"test:integration": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/integration/**/*.test.mjs",
|
|
51
|
+
"test:route-smoke": "node scripts/test-lock.mjs --test scripts/test-tfx-route-no-claude-native.mjs",
|
|
52
|
+
"test:contract": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/contract/**/*.test.mjs",
|
|
53
|
+
"test:coverage": "node --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=60 --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\"",
|
|
54
|
+
"gen:skill-docs": "node scripts/gen-skill-docs.mjs",
|
|
55
|
+
"gen:skill-manifest": "node scripts/gen-skill-manifest.mjs"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
},
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/tellang/triflux.git"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/tellang/triflux#readme",
|
|
65
|
+
"author": "tellang",
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
69
|
+
"better-sqlite3": "^12.6.2",
|
|
70
|
+
"pino": "^10.3.1",
|
|
71
|
+
"pino-pretty": "^13.1.3",
|
|
72
|
+
"systray2": "^2.1.4",
|
|
73
|
+
"zod": "^4.0.0"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@biomejs/biome": "^2.0.0",
|
|
77
|
+
"knip": "^6.3.0"
|
|
78
|
+
},
|
|
36
79
|
"keywords": [
|
|
37
80
|
"claude-code",
|
|
38
81
|
"plugin",
|
|
@@ -43,13 +86,5 @@
|
|
|
43
86
|
"multi-model",
|
|
44
87
|
"triflux",
|
|
45
88
|
"tfx"
|
|
46
|
-
]
|
|
47
|
-
"author": "tellang",
|
|
48
|
-
"license": "MIT",
|
|
49
|
-
"homepage": "https://github.com/tellang/triflux#readme",
|
|
50
|
-
"repository": {
|
|
51
|
-
"type": "git",
|
|
52
|
-
"url": "git+https://github.com/tellang/triflux.git",
|
|
53
|
-
"directory": "packages/triflux"
|
|
54
|
-
}
|
|
89
|
+
]
|
|
55
90
|
}
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -105,17 +105,14 @@ if [[ -f "$_CODEX_CONFIG" ]] && awk '
|
|
|
105
105
|
fi
|
|
106
106
|
|
|
107
107
|
build_codex_base() {
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
else
|
|
117
|
-
echo "--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
|
|
118
|
-
fi
|
|
108
|
+
# codex exec는 항상 non-TTY subprocess에서 실행되므로 --dangerously-bypass 필수.
|
|
109
|
+
# --dangerously-bypass는 config.toml의 approval_mode/sandbox와 충돌하지 않음
|
|
110
|
+
# (--full-auto와 달리 bypass는 config 값을 override할 뿐 에러를 던지지 않음).
|
|
111
|
+
# 검증: approval_mode="auto" config에서 --dangerously-bypass 동시 사용 → exit 0 확인.
|
|
112
|
+
#
|
|
113
|
+
# Note: 위의 _CODEX_HAS_SANDBOX awk 감지는 현재 미사용이지만, 향후 codex가
|
|
114
|
+
# bypass와 config.toml 충돌을 감지하면 분기 로직을 재활성화할 수 있으므로 유지.
|
|
115
|
+
echo "--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
# ── Async Job 디렉토리 ──
|