@triflux/core 10.50.2 → 10.52.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.
Files changed (84) hide show
  1. package/hooks/agy-session-hook.mjs +11 -56
  2. package/hooks/codex-session-hook.mjs +11 -101
  3. package/hub/bridge.mjs +65 -1250
  4. package/hub/cli-adapter-base.mjs +12 -90
  5. package/hub/codex-adapter.mjs +0 -1
  6. package/hub/codex-preflight.mjs +5 -28
  7. package/hub/index.mjs +3 -19
  8. package/hub/lib/process-utils.mjs +1 -156
  9. package/hub/lib/spawn-trace.mjs +3 -2
  10. package/hub/lib/timeout-defaults.mjs +0 -1
  11. package/hub/team/claude-daemon-control.mjs +1 -2
  12. package/hub/team/claude-session-projection.mjs +5 -73
  13. package/hub/team/nativeProxy.mjs +759 -0
  14. package/hub/team/session-context.mjs +1 -1
  15. package/hud/cli-policy.mjs +0 -22
  16. package/hud/constants.mjs +13 -26
  17. package/hud/hud-qos-status.mjs +42 -54
  18. package/hud/providers/gemini.mjs +125 -20
  19. package/hud/renderers.mjs +101 -122
  20. package/hud/utils.mjs +10 -6
  21. package/package.json +2 -6
  22. package/scripts/lib/agent-route-policy.mjs +143 -62
  23. package/scripts/lib/cli-claude.mjs +0 -14
  24. package/scripts/lib/env-probe.mjs +2 -131
  25. package/scripts/lib/gemini-profiles.mjs +2 -69
  26. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  27. package/scripts/lib/machine-profile.mjs +3 -17
  28. package/scripts/lib/mcp-filter.mjs +109 -53
  29. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  30. package/scripts/lib/mcp-health.mjs +2 -2
  31. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  32. package/scripts/lib/psmux-info.mjs +11 -59
  33. package/scripts/lib/session-presence.mjs +2 -100
  34. package/scripts/lib/skill-template.mjs +0 -245
  35. package/hub/adaptive-diagnostic.mjs +0 -350
  36. package/hub/adaptive-inject.mjs +0 -224
  37. package/hub/adaptive-memory.mjs +0 -277
  38. package/hub/adaptive.mjs +0 -172
  39. package/hub/assign-callbacks.mjs +0 -170
  40. package/hub/delegator/contracts.mjs +0 -38
  41. package/hub/delegator/index.mjs +0 -14
  42. package/hub/delegator/schema/delegator-tools.schema.json +0 -250
  43. package/hub/delegator/service.mjs +0 -336
  44. package/hub/delegator/tool-definitions.mjs +0 -35
  45. package/hub/dynamic-routing-engine.mjs +0 -511
  46. package/hub/hitl.mjs +0 -202
  47. package/hub/lib/known-errors.json +0 -72
  48. package/hub/lib/memory-store.mjs +0 -1001
  49. package/hub/lib/phase-manager.mjs +0 -493
  50. package/hub/lib/risk-tier.mjs +0 -53
  51. package/hub/lib/ssh-command.mjs +0 -231
  52. package/hub/lib/staged-file-detect.mjs +0 -43
  53. package/hub/lib/state-snapshot.mjs +0 -293
  54. package/hub/lib/tfx-route-args.mjs +0 -353
  55. package/hub/lib/trace-recorder.mjs +0 -153
  56. package/hub/lib/uuidv7.mjs +0 -45
  57. package/hub/memory-doctor.mjs +0 -640
  58. package/hub/middleware/quota-middleware.mjs +0 -70
  59. package/hub/middleware/request-logger.mjs +0 -191
  60. package/hub/paths.mjs +0 -24
  61. package/hub/pipeline/index.mjs +0 -163
  62. package/hub/pipeline/state.mjs +0 -191
  63. package/hub/pipeline/transitions.mjs +0 -131
  64. package/hub/reflexion.mjs +0 -290
  65. package/hub/role-contract.mjs +0 -314
  66. package/hub/router.mjs +0 -2026
  67. package/hub/routing-snapshot.mjs +0 -261
  68. package/hub/session-fingerprint.mjs +0 -418
  69. package/hub/state.mjs +0 -300
  70. package/hub/team/synapse-http.mjs +0 -311
  71. package/hub/team-bridge.mjs +0 -27
  72. package/mesh/index.mjs +0 -69
  73. package/mesh/mesh-budget.mjs +0 -137
  74. package/mesh/mesh-heartbeat.mjs +0 -100
  75. package/mesh/mesh-protocol.mjs +0 -96
  76. package/mesh/mesh-queue.mjs +0 -163
  77. package/mesh/mesh-registry.mjs +0 -78
  78. package/mesh/mesh-router.mjs +0 -77
  79. package/scripts/lib/claudemd-scanner.mjs +0 -223
  80. package/scripts/lib/context.mjs +0 -67
  81. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  82. package/scripts/lib/handoff.mjs +0 -187
  83. package/scripts/lib/logger.mjs +0 -122
  84. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -1,7 +1,7 @@
1
1
  // hub/cli-adapter-base.mjs — codex/gemini 공통 CLI adapter 인터페이스
2
2
  // Phase 2: codex-adapter.mjs에서 추출한 재사용 가능 유틸리티
3
3
 
4
- import { execSync, spawn } from "node:child_process";
4
+ import { spawn } from "node:child_process";
5
5
  import { existsSync, readFileSync, statSync } from "node:fs";
6
6
 
7
7
  import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
@@ -70,71 +70,6 @@ export function parseRetryAfterMs(text, provider) {
70
70
  return FALLBACK_COOLDOWN_MS[provider] || 5 * 3600_000;
71
71
  }
72
72
 
73
- // ── Codex CLI compatibility ─────────────────────────────────────
74
-
75
- let _cachedVersion = null;
76
-
77
- /**
78
- * `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
79
- * 파싱 실패 시 0 반환 (구버전으로 간주).
80
- * @returns {number} 마이너 버전 (예: 0.117.0 → 117)
81
- */
82
- export function getCodexVersion() {
83
- if (_cachedVersion !== null) return _cachedVersion;
84
- const override = Number(process.env.TFX_CODEX_VERSION_MINOR);
85
- if (Number.isFinite(override) && override > 0) {
86
- _cachedVersion = override;
87
- return _cachedVersion;
88
- }
89
- try {
90
- const out = execSync("codex --version", {
91
- encoding: "utf8",
92
- timeout: 5000,
93
- }).trim();
94
- const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
95
- _cachedVersion = match ? Number.parseInt(match[2], 10) : 0;
96
- } catch {
97
- // Command builders should remain stable in CI even when the real Codex
98
- // CLI is absent. Runtime preflight still reports/install-gates Codex
99
- // separately; this fallback only selects the modern argv shape.
100
- _cachedVersion = 117;
101
- }
102
- return _cachedVersion;
103
- }
104
-
105
- /**
106
- * 최소 마이너 버전 이상인지 확인.
107
- * @param {number} minMinor
108
- * @returns {boolean}
109
- */
110
- export function gte(minMinor) {
111
- return getCodexVersion() >= minMinor;
112
- }
113
-
114
- /**
115
- * Codex CLI 기능별 분기 객체.
116
- * 실측 기반 임계값: 0.114.0에서 exec/skip-git-repo-check/color 확인됨.
117
- * --output-last-message는 0.114.0에 없음 (0.117+ 추정).
118
- */
119
- export const FEATURES = {
120
- /** exec 서브커맨드 사용 가능 여부 (0.110+ 이전부터 존재) */
121
- get execSubcommand() {
122
- return gte(110);
123
- },
124
- /** --output-last-message 플래그 지원 여부 (0.117+) */
125
- get outputLastMessage() {
126
- return gte(117);
127
- },
128
- /** --color <COLOR> 플래그 지원 여부 (exec와 동시 도입) */
129
- get colorNever() {
130
- return gte(110);
131
- },
132
- /** 플러그인 시스템 지원 여부 (향후 확장용) */
133
- get pluginSystem() {
134
- return gte(120);
135
- },
136
- };
137
-
138
73
  // ── Shell utilities ─────────────────────────────────────────────
139
74
 
140
75
  export function normalizePathForShell(value) {
@@ -182,7 +117,7 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
182
117
  enforceCanonicalProfile,
183
118
  } = opts;
184
119
 
185
- const parts = ["codex"];
120
+ const parts = ["codex", "exec"];
186
121
  // Select the effort profile via `-c` config overrides instead of
187
122
  // `--profile <name>`. codex 0.134+ rejects `--profile X` whenever config.toml
188
123
  // still contains an inline [profiles.X] table (and codex re-injects such
@@ -196,30 +131,17 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
196
131
  })
197
132
  : [];
198
133
 
199
- if (FEATURES.execSubcommand) {
200
- parts.push("exec");
201
- if (sandboxBypass) parts.push("--dangerously-bypass-approvals-and-sandbox");
202
- if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
203
- if (resultFile && FEATURES.outputLastMessage) {
204
- parts.push("--output-last-message", resultFile);
205
- }
206
- if (FEATURES.colorNever) parts.push("--color", "never");
207
- for (const override of profileOverrides)
208
- parts.push("-c", shellQuote(override));
209
- // NOTE: `codex exec`는 --cwd 플래그를 지원하지 않는다. Node spawn의 cwd
210
- // 옵션으로 child process의 working directory를 제어한다 (conductor.mjs 참조).
211
- // opts.cwd는 기록용으로만 받아두고 CLI command에는 반영하지 않는다.
212
- // (이전 커밋에서 잘못 추가되어 shard 전체가 exit 2로 크래시한 회귀 #94 후속 이슈)
213
- if (Array.isArray(mcpServers)) {
214
- for (const server of mcpServers) {
215
- parts.push("-c", `mcp_servers.${server}.enabled=true`);
216
- }
134
+ if (sandboxBypass) parts.push("--dangerously-bypass-approvals-and-sandbox");
135
+ if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
136
+ if (resultFile) parts.push("--output-last-message", resultFile);
137
+ parts.push("--color", "never");
138
+ for (const override of profileOverrides)
139
+ parts.push("-c", shellQuote(override));
140
+ // `codex exec`는 --cwd를 받지 않아 child process의 cwd로 제어한다.
141
+ if (Array.isArray(mcpServers)) {
142
+ for (const server of mcpServers) {
143
+ parts.push("-c", `mcp_servers.${server}.enabled=true`);
217
144
  }
218
- } else {
219
- parts.push("--dangerously-bypass-approvals-and-sandbox");
220
- if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
221
- for (const override of profileOverrides)
222
- parts.push("-c", shellQuote(override));
223
145
  }
224
146
 
225
147
  const useStdin = resolveStdinPromptMode(stdinPrompt);
@@ -126,7 +126,6 @@ export function buildExecArgs(opts = {}) {
126
126
  cwd: opts.cwd,
127
127
  // stdinPrompt 가 명시되면 buildExecCommand 로 forward.
128
128
  // headless 워커 spawn (backend.mjs) 은 default (stdin on, macOS hook fix).
129
- // launcher path (launcher-template.mjs) 는 결정론 위해 false 전달.
130
129
  stdinPrompt: opts.stdinPrompt,
131
130
  });
132
131
 
@@ -5,7 +5,6 @@ import { join } from "node:path";
5
5
 
6
6
  import { whichCommandAsync } from "./platform.mjs";
7
7
 
8
- const MIN_RECOMMENDED_MINOR = 118;
9
8
  let _cachedVersion = null;
10
9
 
11
10
  function escapeRegExp(value) {
@@ -74,25 +73,11 @@ async function checkCodexInstalled() {
74
73
  };
75
74
  }
76
75
 
77
- function checkCodexVersion() {
78
- const version = getCodexVersion();
79
- const warnings =
80
- version >= MIN_RECOMMENDED_MINOR
81
- ? []
82
- : [
83
- `Codex CLI 0.${version}.x detected; 0.${MIN_RECOMMENDED_MINOR}.x or newer is recommended.`,
84
- ];
85
- return { version, warnings };
86
- }
87
-
88
76
  function checkApprovalMode(configText, opts = {}) {
89
- const approvalMode = readTomlString(configText, "approval_mode");
90
- const sandbox = readTomlString(configText, "sandbox");
77
+ const approvalPolicy = readTomlString(configText, "approval_policy");
91
78
  const subcommand = opts.subcommand || "exec";
92
79
  return {
93
- needsBypass: subcommand === "exec" || approvalMode !== "full-auto",
94
- approvalMode,
95
- sandbox,
80
+ needsBypass: subcommand === "exec" || approvalPolicy !== "never",
96
81
  };
97
82
  }
98
83
 
@@ -154,7 +139,7 @@ async function checkMcpHealth(mcpServers, configText) {
154
139
  /**
155
140
  * Detects codex config drift between global (~/.codex/config.toml) and the
156
141
  * target workdir. Covers:
157
- * 1. workdir-local `.codex/config.toml` overriding global approval_mode/sandbox
142
+ * 1. workdir-local `.codex/config.toml` overriding global approval_policy/sandbox_mode
158
143
  * 2. instruction-file asymmetry (AGENTS.md vs CLAUDE.md)
159
144
  *
160
145
  * @param {string} [workdir] absolute path to the invocation workdir
@@ -173,7 +158,7 @@ export function detectWorkdirDrift(workdir, globalConfigText = "") {
173
158
  } catch {
174
159
  localText = "";
175
160
  }
176
- const keys = ["approval_mode", "sandbox", "model"];
161
+ const keys = ["approval_policy", "sandbox_mode", "model"];
177
162
  for (const key of keys) {
178
163
  const local = readTomlString(localText, key);
179
164
  const global = readTomlString(globalConfigText, key);
@@ -212,18 +197,10 @@ export async function runPreflight(opts = {}) {
212
197
  }
213
198
 
214
199
  const warnings = [...install.warnings];
215
- const { version, warnings: versionWarnings } = checkCodexVersion();
216
- warnings.push(...versionWarnings);
200
+ const version = getCodexVersion();
217
201
 
218
202
  const configText = readConfigText(opts.configPath);
219
203
  const approval = checkApprovalMode(configText, opts);
220
- if (approval.approvalMode !== "full-auto") {
221
- warnings.push(
222
- `approval_mode is '${approval.approvalMode || "unset"}'; bypass flag will be used.`,
223
- );
224
- }
225
- if (approval.sandbox)
226
- warnings.push(`sandbox mode from config.toml: ${approval.sandbox}`);
227
204
 
228
205
  const mcp = await checkMcpHealth(opts.mcpServers, configText);
229
206
  warnings.push(...mcp.warnings);
package/hub/index.mjs CHANGED
@@ -1,28 +1,12 @@
1
1
  // @triflux/core — barrel export (auto-generated by pack.mjs)
2
2
  // Adapters
3
- export { buildExecCommand, sleep, gte, FEATURES, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
3
+ export { buildExecCommand, sleep, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
4
4
  export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript, getCircuitState as getCodexCircuit } from './codex-adapter.mjs';
5
5
  export { getCodexVersion, runPreflight } from './codex-preflight.mjs';
6
6
 
7
- // Routing
8
- export { createRouter } from './router.mjs';
9
-
10
- // State & Paths & Platform
11
- export * from './paths.mjs';
7
+ // Platform
12
8
  export { IS_WINDOWS, IS_MAC, IS_LINUX, TEMP_DIR, normalizePath, whichCommand, killProcess, pipePath } from './platform.mjs';
13
9
 
14
10
  // Core services
15
- export { createHitlManager } from './hitl.mjs';
16
- export { createAssignCallbackServer } from './assign-callbacks.mjs';
17
- export { getHubUrl, getHubPipePath, requestJson, post, connectPipe, parseArgs, parseJsonSafe } from './bridge.mjs';
18
-
19
- // Reflexion (adaptive error learning)
20
- export { normalizeError, adaptiveRuleFromError } from './reflexion.mjs';
21
-
22
- // Neural Memory (Lake 1.5)
23
- export { createAdaptiveEngine } from './adaptive.mjs';
24
- export { createAdaptiveMemory } from './adaptive-memory.mjs';
25
- export { createDiagnosticPipeline } from './adaptive-diagnostic.mjs';
26
- export { createAdaptiveInjector } from './adaptive-inject.mjs';
27
- export { buildAdaptiveFingerprint, createAdaptiveFingerprintService } from './session-fingerprint.mjs';
11
+ export { parseArgs, parseJsonSafe } from './bridge.mjs';
28
12
  export { broker as accountBroker } from './account-broker.mjs';
@@ -9,7 +9,7 @@ import {
9
9
  unlinkSync,
10
10
  writeFileSync,
11
11
  } from "node:fs";
12
- import { homedir, tmpdir } from "node:os";
12
+ import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { IS_WINDOWS, killProcess } from "../platform.mjs";
15
15
 
@@ -387,23 +387,6 @@ function getProcessSnapshot({
387
387
  }
388
388
  }
389
389
 
390
- function addHubPid(protectedPids) {
391
- try {
392
- const hubPidPath = join(
393
- homedir(),
394
- ".claude",
395
- "cache",
396
- "tfx-hub",
397
- "hub.pid",
398
- );
399
- if (existsSync(hubPidPath)) {
400
- const hubInfo = JSON.parse(readFileSync(hubPidPath, "utf8"));
401
- const hubPid = normalizePid(hubInfo?.pid);
402
- if (hubPid) protectedPids.add(hubPid);
403
- }
404
- } catch {}
405
- }
406
-
407
390
  function getProtectedPids({
408
391
  protectedPids,
409
392
  isWindows = IS_WINDOWS,
@@ -415,7 +398,6 @@ function getProtectedPids({
415
398
  if (Number.isInteger(process.ppid) && process.ppid > 0) {
416
399
  result.add(process.ppid);
417
400
  }
418
- addHubPid(result);
419
401
  if (!includeAncestorScan) return result;
420
402
 
421
403
  const snapshot = getProcessSnapshot({ isWindows, spawnSyncFn });
@@ -488,68 +470,6 @@ function hasExactGbrainServe(commandLine) {
488
470
  );
489
471
  }
490
472
 
491
- function hasGbrainServeCommand(commandLine) {
492
- return /(^|\s)gbrain\s+serve(\s|$)/i.test(commandLine.trim());
493
- }
494
-
495
- function hasFsmonitorDaemonCommand(commandLine) {
496
- return FSMONITOR_DAEMON_PATTERN.test(commandLine);
497
- }
498
-
499
- function categoryForShardProcess(proc, context) {
500
- const name = normalizeName(proc.name);
501
- const commandLine = normalizeCommandLine(proc.commandLine);
502
- const worktreePath = normalizeCommandLine(context.worktreePath || "");
503
- const shardMarker = context.shardName
504
- ? `.codex-swarm\\wt-${context.shardName}`
505
- : "";
506
- const hasWorktree = worktreePath && commandLine.includes(worktreePath);
507
- const hasShardMarker = shardMarker && commandLine.includes(shardMarker);
508
-
509
- if (name === "node.exe" || name === "node") {
510
- if (hasWorktree || hasShardMarker) return "node";
511
- }
512
-
513
- if (name === "bash.exe" || name === "bash" || name === "sh") {
514
- if (hasWorktree) return "bash";
515
- }
516
-
517
- if (name === "conhost.exe" || name === "conhost") {
518
- if (
519
- context.sessionIds?.length > 0 &&
520
- context.sessionIds.some((id) => id && commandLine.includes(id))
521
- ) {
522
- return "conhost";
523
- }
524
- }
525
-
526
- if (name === "bun.exe" || name === "bun") {
527
- if ((hasWorktree || hasShardMarker) && hasGbrainServeCommand(commandLine)) {
528
- return "bun";
529
- }
530
- }
531
-
532
- if (name === "git.exe" || name === "git") {
533
- if (
534
- (hasWorktree || hasShardMarker) &&
535
- hasFsmonitorDaemonCommand(commandLine)
536
- ) {
537
- return "git";
538
- }
539
- }
540
-
541
- return null;
542
- }
543
-
544
- function killPid(
545
- pid,
546
- { killFn = process.kill, protectedPids = new Set() } = {},
547
- ) {
548
- if (protectedPids.has(pid)) return false;
549
- killFn(pid, "SIGKILL");
550
- return true;
551
- }
552
-
553
473
  /**
554
474
  * Return a process tree rooted at `rootPid`.
555
475
  *
@@ -730,67 +650,6 @@ export function findProcessesByCommandLine(
730
650
  });
731
651
  }
732
652
 
733
- /**
734
- * Cleanup processes associated with a swarm shard.
735
- *
736
- * Sequence: kill known top PID snapshots, scan scoped command lines, skip
737
- * protected PIDs, and kill only narrowly gated runtime categories.
738
- *
739
- * @param {{worktreePath?: string, sessionIds?: string[], topPids?: Array<number | {pid: number}>, runId?: string, shardName?: string, dryRun?: boolean, isWindows?: boolean, spawnSyncFn?: typeof spawnSync, killFn?: typeof process.kill, protectedPids?: Set<number>}} opts
740
- * @returns {{killed: number, scanned: number, skipped: number, byCategory: {node: number, bash: number, conhost: number, bun: number, git: number}}}
741
- */
742
- export function cleanupShardProcesses({
743
- worktreePath,
744
- sessionIds = [],
745
- topPids = [],
746
- runId,
747
- shardName,
748
- dryRun = false,
749
- isWindows = IS_WINDOWS,
750
- spawnSyncFn = spawnSync,
751
- killFn = process.kill,
752
- protectedPids,
753
- } = {}) {
754
- const protectedSet = getProtectedPids({
755
- protectedPids,
756
- isWindows,
757
- spawnSyncFn,
758
- includeAncestorScan: true,
759
- });
760
- const byCategory = { node: 0, bash: 0, conhost: 0, bun: 0, git: 0 };
761
- let killed = 0;
762
- let skipped = 0;
763
-
764
- if (!dryRun && topPids.length > 0) {
765
- killed += killProcessTreeSnapshot(topPids, {
766
- killFn,
767
- protectedPids: protectedSet,
768
- }).killed;
769
- }
770
-
771
- const scanned = getProcessSnapshot({ isWindows, spawnSyncFn });
772
-
773
- for (const proc of scanned) {
774
- const category = categoryForShardProcess(proc, {
775
- worktreePath,
776
- sessionIds,
777
- shardName,
778
- });
779
- if (!category) continue;
780
- byCategory[category]++;
781
- if (protectedSet.has(proc.pid)) {
782
- skipped++;
783
- continue;
784
- }
785
- if (dryRun) continue;
786
- try {
787
- if (killPid(proc.pid, { killFn, protectedPids: protectedSet })) killed++;
788
- } catch {}
789
- }
790
-
791
- return { killed, scanned: scanned.length, skipped, byCategory };
792
- }
793
-
794
653
  /**
795
654
  * Cleanup narrowly gated orphan runtime processes.
796
655
  *
@@ -968,22 +827,8 @@ export function cleanupStaleFsmonitorDaemons({
968
827
  function cleanupOrphansUnix() {
969
828
  const myPid = process.pid;
970
829
 
971
- // Hub PID 보호
972
830
  const protectedPids = new Set();
973
831
  protectedPids.add(myPid);
974
- try {
975
- const hubPidPath = join(
976
- homedir(),
977
- ".claude",
978
- "cache",
979
- "tfx-hub",
980
- "hub.pid",
981
- );
982
- if (existsSync(hubPidPath)) {
983
- const hubPid = Number(JSON.parse(readFileSync(hubPidPath, "utf8"))?.pid);
984
- if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
985
- }
986
- } catch {}
987
832
 
988
833
  // 현재 프로세스의 조상 트리 보호
989
834
  try {
@@ -7,13 +7,14 @@ 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
+ const DEFAULT_MAX_SPAWN_PER_SEC = 100;
10
11
  // multi-worker headless dispatch 가 1 초 안에 ~25-30+ psmux/tmux 명령을 호출한다.
11
12
  // 2-worker dispatchBatch 가 default 30 limit 을 초과해 `rate_limit` throw 로 mac
12
13
  // 호환성 회귀 (smoke test 발견, 2026-05-15). 100 으로 상향 — 폭주 안전망은
13
14
  // 여전히 (default 100/sec) 유지, env override 로 보수 설정 가능.
14
15
  export let MAX_SPAWN_PER_SEC = resolvePositiveInteger(
15
16
  process.env.TRIFLUX_MAX_SPAWN_RATE,
16
- 100,
17
+ DEFAULT_MAX_SPAWN_PER_SEC,
17
18
  );
18
19
  export const MAX_TOTAL_DESCENDANTS = resolvePositiveInteger(
19
20
  process.env.TRIFLUX_MAX_DESCENDANTS,
@@ -160,7 +161,7 @@ export function getMaxSpawnPerSec() {
160
161
  export function reload() {
161
162
  MAX_SPAWN_PER_SEC = resolvePositiveInteger(
162
163
  process.env.TRIFLUX_MAX_SPAWN_RATE,
163
- 30,
164
+ DEFAULT_MAX_SPAWN_PER_SEC,
164
165
  );
165
166
  return getMaxSpawnPerSec();
166
167
  }
@@ -5,7 +5,6 @@ export const MAX_DURATION_MS = 86_400_000;
5
5
  export const DEFAULT_ASSIGN_TIMEOUT_MS = 600_000;
6
6
  export const DEFAULT_ASSIGN_TTL_MS = 600_000;
7
7
  export const DEFAULT_HANDOFF_TTL_MS = 600_000;
8
- export const DEFAULT_SWARM_LOCK_TTL_MS = 600_000;
9
8
  export const DEFAULT_REGISTER_TIMEOUT_SEC = 600;
10
9
  export const REGISTER_HEARTBEAT_GRACE_MS = 120_000;
11
10
  export const DEFAULT_STALL_INTERVENTION_SEC = 1_200;
@@ -1775,8 +1775,7 @@ export async function dispatchClaudeDaemonJob({
1775
1775
  /**
1776
1776
  * dispatchClaudeDaemonJob 의 대칭 정리 경로.
1777
1777
  * removeProjection + killDaemonJob (+ optional removeClaudeJobState) 를
1778
- * 모두 시도하며, 각 스텝 오류는 기존 close()/cleanupDaemonDispatches 와
1779
- * 동일하게 .catch 로 삼킨다.
1778
+ * 모두 시도하며, 각 스텝 오류는 .catch 로 삼킨다.
1780
1779
  */
1781
1780
  export async function teardownClaudeDaemonJob({
1782
1781
  controlSock,
@@ -1,12 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- export function normalizeClaudeBridgeSessionId(bridgeSessionId, short) {
4
+ // 실제 bridge ID 가 없으면 만들어 내지 않는다. 추정 ID 는 붙을 수 있는 세션처럼 보이게 한다.
5
+ export function normalizeClaudeBridgeSessionId(bridgeSessionId) {
5
6
  const value = String(bridgeSessionId || "").trim();
6
- if (value.startsWith("session_")) return value;
7
7
  if (value.startsWith("cse_")) return `session_${value.slice(4)}`;
8
- if (value) return value;
9
- return `session_${short}`;
8
+ return value;
10
9
  }
11
10
 
12
11
  export function buildClaudeSessionProjection({
@@ -30,6 +29,7 @@ export function buildClaudeSessionProjection({
30
29
  if (!cwd) throw new Error("cwd is required");
31
30
  if (!name) throw new Error("name is required");
32
31
  if (!agent) throw new Error("agent is required");
32
+ const bridge = normalizeClaudeBridgeSessionId(bridgeSessionId);
33
33
  return {
34
34
  pid,
35
35
  sessionId,
@@ -45,7 +45,7 @@ export function buildClaudeSessionProjection({
45
45
  jobId: short,
46
46
  status,
47
47
  updatedAt,
48
- bridgeSessionId: normalizeClaudeBridgeSessionId(bridgeSessionId, short),
48
+ ...(bridge ? { bridgeSessionId: bridge } : {}),
49
49
  };
50
50
  }
51
51
 
@@ -58,74 +58,6 @@ export async function writeClaudeSessionProjection(sessionsDir, projection) {
58
58
  return sessionPath;
59
59
  }
60
60
 
61
- export async function updateClaudeSessionProjection(sessionPath, patch) {
62
- const parsed = JSON.parse(await fs.readFile(sessionPath, "utf8"));
63
- const next = {
64
- ...parsed,
65
- ...patch,
66
- updatedAt: patch.updatedAt || Date.now(),
67
- };
68
- await fs.writeFile(sessionPath, `${JSON.stringify(next)}\n`, "utf8");
69
- return next;
70
- }
71
-
72
- function projectionSessionId(projection) {
73
- return String(
74
- projection?.sessionId ?? projection?.session_id ?? projection?.id ?? "",
75
- ).trim();
76
- }
77
-
78
- export async function findClaudeSessionProjectionBySessionId(
79
- sessionsDir,
80
- sessionId,
81
- ) {
82
- const expected = String(sessionId || "").trim();
83
- if (!sessionsDir || !expected) return null;
84
- let entries;
85
- try {
86
- entries = await fs.readdir(sessionsDir, { withFileTypes: true });
87
- } catch (error) {
88
- if (error?.code === "ENOENT") return null;
89
- throw error;
90
- }
91
-
92
- for (const entry of entries) {
93
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
94
- const filePath = path.join(sessionsDir, entry.name);
95
- try {
96
- const projection = JSON.parse(await fs.readFile(filePath, "utf8"));
97
- if (projectionSessionId(projection) === expected) {
98
- return { path: filePath, projection };
99
- }
100
- } catch (error) {
101
- if (error instanceof SyntaxError) continue;
102
- if (error?.code === "ENOENT") continue;
103
- throw error;
104
- }
105
- }
106
- return null;
107
- }
108
-
109
- export async function refreshClaudeSessionProjectionCwd({
110
- sessionsDir,
111
- sessionId,
112
- cwd,
113
- updatedAt = Date.now(),
114
- } = {}) {
115
- const nextCwd = String(cwd || "").trim();
116
- if (!nextCwd) return { updated: false, reason: "missing_cwd" };
117
- const found = await findClaudeSessionProjectionBySessionId(
118
- sessionsDir,
119
- sessionId,
120
- );
121
- if (!found) return { updated: false, reason: "projection_not_found" };
122
- const projection = await updateClaudeSessionProjection(found.path, {
123
- cwd: nextCwd,
124
- updatedAt,
125
- });
126
- return { updated: true, path: found.path, projection };
127
- }
128
-
129
61
  export async function removeClaudeSessionProjection(sessionPath) {
130
62
  await fs.rm(sessionPath, { force: true });
131
63
  }