triflux 10.9.7 → 10.9.9

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.
@@ -4,7 +4,7 @@
4
4
  // Singleton export. All state changes create new objects (immutable pattern).
5
5
 
6
6
  import { EventEmitter } from "node:events";
7
- import { existsSync, readFileSync } from "node:fs";
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { join, sep } from "node:path";
10
10
  import * as z from "zod";
@@ -55,6 +55,39 @@ const LEASE_TTL_MS = 30 * 60 * 1000; // 30 minutes
55
55
  const CIRCUIT_WINDOW_MS = 10 * 60_000; // 10 minutes
56
56
  const CIRCUIT_MAX_FAILURES = 3;
57
57
  const AUTH_BASE_PATH = join(homedir(), ".claude", "cache", "tfx-hub");
58
+ const STATE_PERSIST_PATH = join(AUTH_BASE_PATH, "broker-state.json");
59
+
60
+ // ── State persistence ────────────────────────────────────────────
61
+
62
+ function persistState(stateMap) {
63
+ try {
64
+ const now = Date.now();
65
+ const entries = {};
66
+ for (const [id, acct] of stateMap) {
67
+ // 활성 쿨다운 또는 circuit open만 저장 (불필요한 데이터 제거)
68
+ if (acct.cooldownUntil > now || acct.circuitOpenedAt > 0 || acct.totalSessions > 0) {
69
+ entries[id] = {
70
+ cooldownUntil: acct.cooldownUntil,
71
+ circuitOpenedAt: acct.circuitOpenedAt,
72
+ failureTimestamps: acct.failureTimestamps,
73
+ totalSessions: acct.totalSessions,
74
+ lastUsedAt: acct.lastUsedAt,
75
+ };
76
+ }
77
+ }
78
+ mkdirSync(AUTH_BASE_PATH, { recursive: true });
79
+ writeFileSync(STATE_PERSIST_PATH, JSON.stringify({ ts: now, entries }));
80
+ } catch { /* best-effort */ }
81
+ }
82
+
83
+ function loadPersistedState() {
84
+ try {
85
+ if (!existsSync(STATE_PERSIST_PATH)) return null;
86
+ return JSON.parse(readFileSync(STATE_PERSIST_PATH, "utf8"));
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
58
91
 
59
92
  // ── env var resolution ───────────────────────────────────────────
60
93
 
@@ -101,7 +134,11 @@ class AccountBroker extends EventEmitter {
101
134
  ...(parsed.gemini || []).map((a) => ({ ...a, provider: "gemini" })),
102
135
  ];
103
136
 
137
+ const persisted = loadPersistedState();
138
+ const pEntries = persisted?.entries || {};
139
+
104
140
  for (const account of allAccounts) {
141
+ const saved = pEntries[account.id];
105
142
  this.#state.set(account.id, {
106
143
  id: account.id,
107
144
  provider: account.provider,
@@ -113,13 +150,12 @@ class AccountBroker extends EventEmitter {
113
150
  tier: account.tier ?? "unknown",
114
151
  busy: false,
115
152
  leasedAt: null,
116
- cooldownUntil: 0,
117
- // per-account circuit breaker state
118
- failureTimestamps: [], // timestamp array for window-based decay
119
- circuitOpenedAt: 0,
153
+ cooldownUntil: saved?.cooldownUntil ?? 0,
154
+ failureTimestamps: saved?.failureTimestamps ?? [],
155
+ circuitOpenedAt: saved?.circuitOpenedAt ?? 0,
120
156
  circuitTrialInFlight: false,
121
- lastUsedAt: 0,
122
- totalSessions: 0,
157
+ lastUsedAt: saved?.lastUsedAt ?? 0,
158
+ totalSessions: saved?.totalSessions ?? 0,
123
159
  });
124
160
  }
125
161
  }
@@ -343,6 +379,7 @@ class AccountBroker extends EventEmitter {
343
379
  };
344
380
 
345
381
  this.#state.set(accountId, updated);
382
+ persistState(this.#state);
346
383
  this.emit("release", { id: accountId, ok });
347
384
  }
348
385
 
@@ -357,6 +394,7 @@ class AccountBroker extends EventEmitter {
357
394
  leasedAt: null,
358
395
  cooldownUntil: Date.now() + coolMs,
359
396
  });
397
+ persistState(this.#state);
360
398
  }
361
399
 
362
400
  // ── snapshot ──────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.7",
3
+ "version": "10.9.9",
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": {
package/scripts/setup.mjs CHANGED
@@ -64,6 +64,43 @@ const REQUIRED_CODEX_PROFILES = [
64
64
 
65
65
  const HUD_SYNC_EXCLUDES = new Set(["omc-hud.mjs", "omc-hud.mjs.bak"]);
66
66
 
67
+ /**
68
+ * hub/workers/*.mjs + hub/ 루트의 worker 의존성 파일을 자동 스캔.
69
+ * 수동 리스트 대신 glob으로 탐색하여 파일 추가 시 sync 누락 방지.
70
+ */
71
+ function scanHubWorkerFiles(pluginRoot, claudeDir) {
72
+ const results = [];
73
+ const hubRoot = join(pluginRoot, "hub");
74
+ if (!existsSync(hubRoot)) return results;
75
+
76
+ // hub/workers/*.mjs 전체
77
+ const workersDir = join(hubRoot, "workers");
78
+ if (existsSync(workersDir)) {
79
+ for (const f of readdirSync(workersDir).sort()) {
80
+ if (!f.endsWith(".mjs")) continue;
81
+ results.push({
82
+ src: join(workersDir, f),
83
+ dst: join(claudeDir, "scripts", "hub", "workers", f),
84
+ label: `hub/workers/${f}`,
85
+ });
86
+ }
87
+ }
88
+
89
+ // hub/ 루트: worker가 import하는 의존성 (cli-adapter-base, platform 등)
90
+ const hubRootDeps = ["cli-adapter-base.mjs", "platform.mjs", "account-broker.mjs"];
91
+ for (const f of hubRootDeps) {
92
+ if (existsSync(join(hubRoot, f))) {
93
+ results.push({
94
+ src: join(hubRoot, f),
95
+ dst: join(claudeDir, "scripts", "hub", f),
96
+ label: `hub/${f}`,
97
+ });
98
+ }
99
+ }
100
+
101
+ return results;
102
+ }
103
+
67
104
  function scanHudFiles(pluginRoot, claudeDir) {
68
105
  const hudRoot = join(pluginRoot, "hud");
69
106
  if (!existsSync(hudRoot)) return [];
@@ -124,51 +161,7 @@ const SYNC_MAP = [
124
161
  dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
125
162
  label: "tfx-route-worker.mjs",
126
163
  },
127
- {
128
- src: join(PLUGIN_ROOT, "hub", "workers", "codex-mcp.mjs"),
129
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
130
- label: "hub/workers/codex-mcp.mjs",
131
- },
132
- {
133
- src: join(PLUGIN_ROOT, "hub", "workers", "delegator-mcp.mjs"),
134
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "delegator-mcp.mjs"),
135
- label: "hub/workers/delegator-mcp.mjs",
136
- },
137
- {
138
- src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
139
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
140
- label: "hub/workers/interface.mjs",
141
- },
142
- {
143
- src: join(PLUGIN_ROOT, "hub", "workers", "gemini-worker.mjs"),
144
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
145
- label: "hub/workers/gemini-worker.mjs",
146
- },
147
- {
148
- src: join(PLUGIN_ROOT, "hub", "workers", "claude-worker.mjs"),
149
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
150
- label: "hub/workers/claude-worker.mjs",
151
- },
152
- {
153
- src: join(PLUGIN_ROOT, "hub", "workers", "worker-utils.mjs"),
154
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "worker-utils.mjs"),
155
- label: "hub/workers/worker-utils.mjs",
156
- },
157
- {
158
- src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
159
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
160
- label: "hub/workers/factory.mjs",
161
- },
162
- {
163
- src: join(PLUGIN_ROOT, "hub", "cli-adapter-base.mjs"),
164
- dst: join(CLAUDE_DIR, "scripts", "hub", "cli-adapter-base.mjs"),
165
- label: "hub/cli-adapter-base.mjs",
166
- },
167
- {
168
- src: join(PLUGIN_ROOT, "hub", "platform.mjs"),
169
- dst: join(CLAUDE_DIR, "scripts", "hub", "platform.mjs"),
170
- label: "hub/platform.mjs",
171
- },
164
+ ...scanHubWorkerFiles(PLUGIN_ROOT, CLAUDE_DIR),
172
165
  ...scanHudFiles(PLUGIN_ROOT, CLAUDE_DIR),
173
166
  {
174
167
  src: join(PLUGIN_ROOT, "scripts", "notion-read.mjs"),