@triflux/core 10.51.0 → 10.53.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 (61) hide show
  1. package/hub/bridge.mjs +69 -1197
  2. package/hub/cli-adapter-base.mjs +4 -104
  3. package/hub/codex-adapter.mjs +7 -116
  4. package/hub/index.mjs +3 -20
  5. package/hub/lib/process-utils.mjs +1 -33
  6. package/hub/team/claude-daemon-control.mjs +1 -2
  7. package/hub/team/claude-session-projection.mjs +5 -73
  8. package/hub/team/claude-transcript.mjs +19 -0
  9. package/hub/team/nativeProxy.mjs +759 -0
  10. package/hub/team/session-context.mjs +1 -1
  11. package/hud/constants.mjs +9 -22
  12. package/hud/hud-qos-status.mjs +16 -19
  13. package/hud/providers/codex-probe.mjs +2 -31
  14. package/hud/providers/codex.mjs +16 -58
  15. package/hud/providers/gemini.mjs +20 -0
  16. package/hud/renderers.mjs +101 -77
  17. package/hud/utils.mjs +9 -13
  18. package/package.json +3 -6
  19. package/scripts/lib/agent-route-policy.mjs +0 -67
  20. package/scripts/lib/cli-claude.mjs +0 -12
  21. package/scripts/lib/env-probe.mjs +0 -70
  22. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  23. package/scripts/lib/mcp-guard-engine.mjs +42 -101
  24. package/scripts/lib/session-presence.mjs +1 -99
  25. package/hub/account-broker.mjs +0 -1221
  26. package/hub/adaptive-diagnostic.mjs +0 -350
  27. package/hub/adaptive-inject.mjs +0 -224
  28. package/hub/adaptive-memory.mjs +0 -277
  29. package/hub/adaptive.mjs +0 -172
  30. package/hub/assign-callbacks.mjs +0 -170
  31. package/hub/delegator/contracts.mjs +0 -38
  32. package/hub/delegator/index.mjs +0 -14
  33. package/hub/delegator/schema/delegator-tools.schema.json +0 -250
  34. package/hub/delegator/service.mjs +0 -336
  35. package/hub/delegator/tool-definitions.mjs +0 -35
  36. package/hub/hitl.mjs +0 -202
  37. package/hub/lib/known-errors.json +0 -72
  38. package/hub/lib/memory-store.mjs +0 -1001
  39. package/hub/lib/phase-manager.mjs +0 -493
  40. package/hub/lib/state-snapshot.mjs +0 -293
  41. package/hub/lib/trace-recorder.mjs +0 -153
  42. package/hub/lib/uuidv7.mjs +0 -45
  43. package/hub/memory-doctor.mjs +0 -640
  44. package/hub/middleware/quota-middleware.mjs +0 -70
  45. package/hub/middleware/request-logger.mjs +0 -191
  46. package/hub/paths.mjs +0 -24
  47. package/hub/pipeline/index.mjs +0 -163
  48. package/hub/pipeline/state.mjs +0 -191
  49. package/hub/pipeline/transitions.mjs +0 -131
  50. package/hub/reflexion.mjs +0 -290
  51. package/hub/role-contract.mjs +0 -312
  52. package/hub/router.mjs +0 -2026
  53. package/hub/session-fingerprint.mjs +0 -418
  54. package/hub/state.mjs +0 -300
  55. package/hub/team/synapse-http.mjs +0 -311
  56. package/hub/team-bridge.mjs +0 -27
  57. package/scripts/lib/claudemd-scanner.mjs +0 -26
  58. package/scripts/lib/context.mjs +0 -51
  59. package/scripts/lib/handoff.mjs +0 -187
  60. package/scripts/lib/logger.mjs +0 -122
  61. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -15,61 +15,6 @@ import {
15
15
  import { IS_WINDOWS, killProcess } from "./platform.mjs";
16
16
  import { createInterventionLadder } from "./team/intervention.mjs";
17
17
 
18
- // ── Quota retry-after 파싱 ──────────────────────────────────────
19
-
20
- const FALLBACK_COOLDOWN_MS = { codex: 5 * 3600_000, gemini: 86400_000 };
21
-
22
- /**
23
- * 에러 텍스트에서 retry-after 시간을 동적 파싱.
24
- * "try again at 2026-04-11T10:00:00Z", "retry after 3600 seconds",
25
- * "resets in 7 days", "wait 5 hours" 등을 감지.
26
- */
27
- export function parseRetryAfterMs(text, provider) {
28
- // 1. ISO timestamp: "try again at 2026-04-11T10:00:00"
29
- const isoMatch = text.match(
30
- /(?:try again|retry|available|resets?)\s+(?:at|after)\s+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i,
31
- );
32
- if (isoMatch) {
33
- const target = new Date(isoMatch[1]).getTime();
34
- if (target > Date.now()) return target - Date.now();
35
- }
36
-
37
- // 2. Duration: "N seconds/minutes/hours/days"
38
- const durMatch = text.match(
39
- /(?:retry|wait|resets?|again)\s+(?:in\s+|after\s+)?(\d+)\s*(second|minute|hour|day|week)/i,
40
- );
41
- if (durMatch) {
42
- const n = Number(durMatch[1]);
43
- const unit = durMatch[2].toLowerCase();
44
- const multipliers = {
45
- second: 1000,
46
- minute: 60_000,
47
- hour: 3600_000,
48
- day: 86400_000,
49
- week: 604800_000,
50
- };
51
- return n * (multipliers[unit] || 3600_000);
52
- }
53
-
54
- // 3. "7 days" / "24 hours" 단독 (context 없이)
55
- const standaloneMatch = text.match(/(\d+)\s*(day|hour|week|minute)/i);
56
- if (standaloneMatch) {
57
- const n = Number(standaloneMatch[1]);
58
- const unit = standaloneMatch[2].toLowerCase();
59
- const multipliers = {
60
- minute: 60_000,
61
- hour: 3600_000,
62
- day: 86400_000,
63
- week: 604800_000,
64
- };
65
- const parsed = n * (multipliers[unit] || 3600_000);
66
- if (parsed >= 3600_000) return parsed; // 1시간 이상만 신뢰
67
- }
68
-
69
- // 4. fallback: provider 기본값
70
- return FALLBACK_COOLDOWN_MS[provider] || 5 * 3600_000;
71
- }
72
-
73
18
  // ── Shell utilities ─────────────────────────────────────────────
74
19
 
75
20
  export function normalizePathForShell(value) {
@@ -195,47 +140,28 @@ export function appendWarnings(stderr, warnings = []) {
195
140
  return [stderr, text].filter(Boolean).join("\n");
196
141
  }
197
142
 
198
- // ── Broker-integrated execution ─────────────────────────────────
143
+ // ── Attempt execution ───────────────────────────────────────────
199
144
 
200
145
  /**
201
- * Shared execute() logic that uses account-broker for per-account circuit
202
- * breaking instead of a global breaker.
146
+ * preflight 뒤 buildAttemptsFn 이 만든 시도를 순서대로 실행한다.
203
147
  *
204
148
  * @param {object} params
205
- * @param {string} params.provider — 'codex' | 'gemini'
206
149
  * @param {(prompt: string, workdir: string, preflight: object, attempt: object) => Promise<object>} params.runFn
207
150
  * @param {(opts: object) => Promise<object>} params.preflightFn
208
151
  * @param {(opts: object, preflight: object) => object[]} params.buildAttemptsFn
209
152
  * @param {object} params.opts — caller-supplied execute options
210
153
  * @returns {Promise<object>} createResult-shaped result
211
154
  */
212
- export async function executeWithCircuitBroker({
213
- provider,
155
+ export async function executeWithAttempts({
214
156
  runFn,
215
157
  preflightFn,
216
158
  buildAttemptsFn,
217
159
  opts = {},
218
160
  }) {
219
- // late-import to avoid circular dependency at module load time
220
- const brokerMod = await import("./account-broker.mjs");
221
161
  const { withRetry } = await import("./workers/worker-utils.mjs");
222
162
 
223
- // access broker as live binding property (not destructured) so reloadBroker() propagates
224
- const hasBroker = brokerMod.broker != null;
225
- // Empty broker (TFX_DISABLE_ACCOUNT_BROKER=1 or zero accounts) must not
226
- // gate execution. lease() is null because there are no accounts to lease,
227
- // not because all accounts are busy/cooldown/circuit. Treat it like
228
- // hasBroker=false and fall through to the default auth path.
229
- const brokerDisabled = hasBroker && brokerMod.broker.isDisabled === true;
230
- const effectiveBroker = hasBroker && !brokerDisabled;
231
- const lease = effectiveBroker ? brokerMod.broker.lease({ provider }) : null;
232
- if (effectiveBroker && !lease) {
233
- return createResult(false, { fellBack: true, failureMode: "circuit_open" });
234
- }
235
-
236
163
  const preflight = await preflightFn(opts);
237
164
  if (!preflight.ok) {
238
- if (lease) brokerMod.broker.release(lease.id, { ok: false });
239
165
  return createResult(false, {
240
166
  stderr: appendWarnings("", preflight.warnings),
241
167
  fellBack: opts.fallbackToClaude !== false,
@@ -250,15 +176,11 @@ export async function executeWithCircuitBroker({
250
176
  try {
251
177
  lastResult = await withRetry(
252
178
  async () => {
253
- // PRD A1: lease 메타데이터를 runFn 에 전달해서 adapter 가 실제 spawn 시
254
- // 해당 account 의 authFile/env/profile 을 적용할 수 있게 한다. lease=null
255
- // 이면 adapter 는 default 경로 (단일 ~/.codex/auth.json) 로 동작한다.
256
179
  const result = await runFn(
257
180
  opts.prompt || "",
258
181
  opts.workdir || process.cwd(),
259
182
  preflight,
260
183
  attempts[attemptIndex],
261
- lease,
262
184
  );
263
185
  const current = {
264
186
  ...result,
@@ -286,26 +208,7 @@ export async function executeWithCircuitBroker({
286
208
  createResult(false, { stderr: String(error?.message || error) });
287
209
  }
288
210
 
289
- if (lastResult.ok) {
290
- if (lease) brokerMod.broker.release(lease.id, { ok: true });
291
- return lastResult;
292
- }
293
-
294
- if (lease) {
295
- if (lastResult.failureMode === "rate_limited") {
296
- const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
297
- const coolMs = parseRetryAfterMs(text, provider);
298
- brokerMod.broker.markRateLimited(lease.id, coolMs);
299
- brokerMod.broker.emit("cooldown", {
300
- id: lease.id,
301
- provider,
302
- coolMs,
303
- reason: "quota_exhausted",
304
- });
305
- } else {
306
- brokerMod.broker.release(lease.id, { ok: false });
307
- }
308
- }
211
+ if (lastResult.ok) return lastResult;
309
212
  return {
310
213
  ...lastResult,
311
214
  retried: attempts.length > 1,
@@ -374,13 +277,10 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
374
277
  let child;
375
278
 
376
279
  try {
377
- // PRD A1: opts.spawnEnv 가 있으면 그 env 로 spawn (lease 의 authFile/env 적용).
378
- // undefined 면 spawn 의 default 동작 (부모 process env inherit) 유지.
379
280
  child = spawnProcess(command, {
380
281
  cwd: workdir,
381
282
  shell: true,
382
283
  windowsHide: true,
383
- env: opts.spawnEnv,
384
284
  });
385
285
  } catch (error) {
386
286
  return createResult(false, {
@@ -1,11 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
- import { basename, join } from "node:path";
3
+ import { join } from "node:path";
4
4
  import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
5
5
  import {
6
6
  buildExecCommand,
7
- createResult,
8
- executeWithCircuitBroker,
7
+ executeWithAttempts,
9
8
  normalizePathForShell,
10
9
  runProcess,
11
10
  shellQuote,
@@ -113,7 +112,7 @@ export function buildLaunchScript(opts = {}) {
113
112
 
114
113
  // CTO boundary: this adapter assembles Codex prompts outside
115
114
  // scripts/tfx-route.sh. Keep north-star injection in tfx-route.sh until
116
- // launcher/circuit-broker workdir contracts have focused coverage.
115
+ // launcher workdir contracts have focused coverage.
117
116
  export function buildExecArgs(opts = {}) {
118
117
  const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
119
118
  const command = buildExecCommand(prompt, opts.resultFile || null, {
@@ -153,7 +152,7 @@ export function buildExecArgs(opts = {}) {
153
152
 
154
153
  // ── Codex execution ─────────────────────────────────────────────
155
154
 
156
- async function runCodex(prompt, workdir, preflight, attempt, lease) {
155
+ async function runCodex(prompt, workdir, preflight, attempt) {
157
156
  const dir = join(tmpdir(), "triflux-codex-exec");
158
157
  mkdirSync(dir, { recursive: true });
159
158
  const resultFile = join(
@@ -162,7 +161,7 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
162
161
  );
163
162
  const command = commandWithOverrides(
164
163
  buildExecCommand(prompt, resultFile, {
165
- profile: lease?.profile ?? attempt.profile,
164
+ profile: attempt.profile,
166
165
  skipGitRepoCheck: true,
167
166
  sandboxBypass: attempt.forceBypass,
168
167
  }),
@@ -170,125 +169,17 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
170
169
  preflight.codexPath,
171
170
  buildOverrides(attempt.requested, attempt.excluded),
172
171
  );
173
- // PRD A1 — lease 메타데이터를 spawn env 에 적용한다. lease.authFile 이 있으면
174
- // 해당 파일이 위치한 디렉토리를 CODEX_HOME 으로 export 해서 codex CLI 가 그
175
- // account 의 auth.json 을 사용하게 한다. lease 가 null 이면 default ~/.codex
176
- // 동작 유지 (회귀 없음). lease.env 는 추가 환경변수 (provider 고정 등) 주입용.
177
- const spawnEnv = lease ? buildLeaseSpawnEnv(lease) : undefined;
178
- const authCheck = validateLeaseAuth(lease, spawnEnv);
179
- if (!authCheck.ok) {
180
- return createResult(false, {
181
- stderr: authCheck.error,
182
- failureMode: "auth_sync",
183
- skipCircuit: true,
184
- });
185
- }
186
172
  return runProcess(command, workdir, attempt.timeout, {
187
173
  resultFile,
188
174
  inferStallMode,
189
- spawnEnv,
190
175
  cli: "codex",
191
- codexHome: spawnEnv?.CODEX_HOME,
192
176
  });
193
177
  }
194
178
 
195
- function buildLeaseSpawnEnv(lease) {
196
- const extra = {};
197
- if (lease.authFile) {
198
- // lease.authFile 은 보통 ~/.claude/cache/tfx-hub/codex-auth-<account>.json 형식.
199
- // codex CLI 는 CODEX_HOME 을 통해 auth.json 위치를 결정하므로, 이 cache 파일을
200
- // 직접 CODEX_HOME 후보 디렉토리로 사용한다. cache 파일 자체가 auth.json 이름이
201
- // 아니라면 후속 PR 에서 isolated dir + symlink/복사 처리한다 (PRD Open Question).
202
- // 현재는 lease.authFile 의 dirname 을 export 하되, 그 dirname 안에 auth.json 이
203
- // 실제로 있을 때만 적용한다 (false-positive 회피).
204
- try {
205
- const dir = dirnameOf(lease.authFile);
206
- if (dir) extra.CODEX_HOME = dir;
207
- } catch {}
208
- }
209
- if (lease.env && typeof lease.env === "object") {
210
- Object.assign(extra, lease.env);
211
- }
212
- return Object.keys(extra).length ? { ...process.env, ...extra } : undefined;
213
- }
214
-
215
- function validateLeaseAuth(lease, spawnEnv) {
216
- if (!lease?.authFile) return { ok: true };
217
- if (basename(lease.authFile) !== "auth.json") {
218
- return {
219
- ok: false,
220
- error: `lease authFile must resolve to auth.json for account ${lease.id}`,
221
- };
222
- }
223
- if (!existsSync(lease.authFile)) {
224
- return {
225
- ok: false,
226
- error: `lease authFile missing for account ${lease.id}: ${lease.authFile}`,
227
- };
228
- }
229
- if (dirnameOf(lease.authFile) !== spawnEnv?.CODEX_HOME) {
230
- return {
231
- ok: false,
232
- // This is currently expected to match because buildLeaseSpawnEnv derives
233
- // CODEX_HOME from authFile. Keep the guard so future lease.env overrides
234
- // cannot silently point Codex at a different account home.
235
- error: `CODEX_HOME mismatch for account ${lease.id}`,
236
- };
237
- }
238
- try {
239
- const parsed = JSON.parse(readFileSync(lease.authFile, "utf8"));
240
- const accountId =
241
- parsed?.tokens?.account_id ??
242
- parsed?.account_id ??
243
- parsed?.accountId ??
244
- null;
245
- if (!accountId) {
246
- return {
247
- ok: false,
248
- error: `lease authFile has no account_id for account ${lease.id}`,
249
- };
250
- }
251
- if (accountId !== lease.id) {
252
- return {
253
- ok: false,
254
- error: `lease authFile account mismatch: expected ${lease.id}, got ${accountId}`,
255
- };
256
- }
257
- } catch (error) {
258
- return {
259
- ok: false,
260
- error: `lease authFile unreadable for account ${lease.id}: ${error?.message || error}`,
261
- };
262
- }
263
- return { ok: true };
264
- }
265
-
266
- function dirnameOf(filePath) {
267
- if (typeof filePath !== "string" || !filePath) return null;
268
- const lastSep = Math.max(
269
- filePath.lastIndexOf("/"),
270
- filePath.lastIndexOf("\\"),
271
- );
272
- if (lastSep < 0) return null;
273
- return filePath.slice(0, lastSep);
274
- }
275
-
276
179
  // ── Public API ──────────────────────────────────────────────────
277
180
 
278
- export async function getCircuitState() {
279
- const brokerMod = await import("./account-broker.mjs");
280
- if (!brokerMod.broker) return { state: "closed", failures: [] };
281
- const snap = brokerMod.broker
282
- .snapshot()
283
- .filter((a) => a.provider === "codex");
284
- return snap.length
285
- ? { state: snap[0].circuitState, accounts: snap }
286
- : { state: "closed", failures: [] };
287
- }
288
-
289
181
  export function execute(opts = {}) {
290
- return executeWithCircuitBroker({
291
- provider: "codex",
182
+ return executeWithAttempts({
292
183
  runFn: runCodex,
293
184
  preflightFn: (o) =>
294
185
  runPreflight({
package/hub/index.mjs CHANGED
@@ -1,28 +1,11 @@
1
1
  // @triflux/core — barrel export (auto-generated by pack.mjs)
2
2
  // Adapters
3
3
  export { buildExecCommand, sleep, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
4
- export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript, getCircuitState as getCodexCircuit } from './codex-adapter.mjs';
4
+ export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript } 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';
28
- export { broker as accountBroker } from './account-broker.mjs';
11
+ export { parseArgs, parseJsonSafe } from './bridge.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 });
@@ -845,22 +827,8 @@ export function cleanupStaleFsmonitorDaemons({
845
827
  function cleanupOrphansUnix() {
846
828
  const myPid = process.pid;
847
829
 
848
- // Hub PID 보호
849
830
  const protectedPids = new Set();
850
831
  protectedPids.add(myPid);
851
- try {
852
- const hubPidPath = join(
853
- homedir(),
854
- ".claude",
855
- "cache",
856
- "tfx-hub",
857
- "hub.pid",
858
- );
859
- if (existsSync(hubPidPath)) {
860
- const hubPid = Number(JSON.parse(readFileSync(hubPidPath, "utf8"))?.pid);
861
- if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
862
- }
863
- } catch {}
864
832
 
865
833
  // 현재 프로세스의 조상 트리 보호
866
834
  try {
@@ -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
  }
@@ -157,6 +157,25 @@ function acceptLine(state, line, marker) {
157
157
  }
158
158
  }
159
159
 
160
+ // daemon 과 tmux wait 가 같은 기준으로 끝을 판정한다. 아직이면 null.
161
+ export function claudeWaitVerdict(transcript, idle) {
162
+ if (transcript?.error)
163
+ return { ok: false, status: "failed", error: transcript.error };
164
+ if (
165
+ !transcript?.userSeen ||
166
+ !transcript.turnEnded ||
167
+ !(idle || transcript.sectionClosed)
168
+ )
169
+ return null;
170
+ if (!transcript.response.trim())
171
+ return {
172
+ ok: false,
173
+ status: "failed",
174
+ error: "turn ended without assistant text",
175
+ };
176
+ return { status: "completed", done: true };
177
+ }
178
+
160
179
  export async function readClaudeTranscript(transcriptPath, { requestId } = {}) {
161
180
  if (!transcriptPath) return null;
162
181
  const marker = requestId ? `[tfx-live req=${requestId}]` : null;