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.
Files changed (84) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/config/mcp-registry.json +29 -0
  4. package/hub/lib/spawn-trace.mjs +39 -9
  5. package/hub/server.mjs +91 -0
  6. package/hub/team/conductor.mjs +117 -32
  7. package/hub/team/headless.mjs +90 -8
  8. package/hub/team/synapse-http.mjs +59 -0
  9. package/hub/team/synapse-registry.mjs +11 -5
  10. package/package.json +56 -21
  11. package/scripts/__tests__/spawn-trace.test.mjs +50 -14
  12. package/scripts/tfx-route.sh +14 -11
  13. package/tui/codex-profile.mjs +457 -0
  14. package/tui/core.mjs +266 -0
  15. package/tui/doctor.mjs +375 -0
  16. package/tui/gemini-profile.mjs +299 -0
  17. package/tui/monitor-data.mjs +152 -0
  18. package/tui/monitor.mjs +339 -0
  19. package/tui/setup.mjs +598 -0
  20. package/CLAUDE.md +0 -212
  21. package/references/hosts.json +0 -46
  22. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  23. package/skills/tfx-workspace/evals/evals.json +0 -79
  24. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  25. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  26. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  27. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  28. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  29. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  30. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  31. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  32. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  33. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  34. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  35. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  36. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  37. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  38. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  39. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  40. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  41. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  42. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  43. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  44. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  45. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  46. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  47. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  48. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  49. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  50. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  51. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  52. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  53. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  54. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  55. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  56. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  57. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  58. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  59. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  60. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  61. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  62. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  63. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  64. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  65. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  66. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  67. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  68. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  69. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  70. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  71. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  72. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  73. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  74. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  75. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  76. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  77. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  78. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  79. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  80. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  81. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  82. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  83. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  84. 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
+ }
@@ -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 const MAX_SPAWN_PER_SEC = resolvePositiveInteger(
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 >= MAX_SPAWN_PER_SEC) {
196
+ if (recentSpawnTimes.length >= maxSpawnPerSec) {
184
197
  return createPolicyError(
185
198
  "rate_limit",
186
- `spawn-trace rate limit exceeded (${MAX_SPAWN_PER_SEC}/sec)`,
187
- { maxPerSec: MAX_SPAWN_PER_SEC },
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(traceId, file, normalized.argsList, normalized.options, blockedError);
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(traceId, file, normalized.argsList, normalized.options, blockedError, {
421
- sync: true,
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";
@@ -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, getAdapter } from "./launcher-template.mjs";
32
- import { createRemoteProbe } from "./remote-probe.mjs";
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 = "codex exec -s danger-full-access --dangerously-bypass-approvals-and-sandbox";
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", "ConnectTimeout=30",
545
- "-o", "BatchMode=yes",
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
- if (lease?.mode === "auth" && lease.authFile) {
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");