triflux 10.42.0 → 10.44.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/README.md +2 -0
  2. package/adapters/codex/skills/tfx-harness/SKILL.md +30 -0
  3. package/adapters/codex/skills/tfx-harness/skill.json +5 -0
  4. package/bin/tfx-live.mjs +3 -1
  5. package/bin/triflux.mjs +14 -0
  6. package/config/routing-policy.json +1 -1
  7. package/hooks/keyword-rules.json +139 -19
  8. package/hooks/pipeline-stop.mjs +1 -1
  9. package/hub/bridge.mjs +80 -1
  10. package/hub/cli-adapter-base.mjs +111 -21
  11. package/hub/codex-adapter.mjs +8 -1
  12. package/hub/dynamic-routing-engine.mjs +1 -1
  13. package/hub/gemini-adapter.mjs +3 -1
  14. package/hub/intent.mjs +24 -28
  15. package/hub/lib/cto-env.mjs +46 -0
  16. package/hub/lib/memory-store.mjs +25 -13
  17. package/hub/lib/timeout-defaults.mjs +45 -0
  18. package/hub/lib/worker-lifecycle.mjs +101 -0
  19. package/hub/middleware/request-logger.mjs +4 -1
  20. package/hub/pipe.mjs +9 -0
  21. package/hub/role-contract.mjs +314 -0
  22. package/hub/role-control-events.mjs +113 -0
  23. package/hub/router.mjs +383 -29
  24. package/hub/schema.sql +62 -2
  25. package/hub/server.mjs +130 -43
  26. package/hub/store.mjs +609 -19
  27. package/hub/team/claude-daemon-control.mjs +28 -15
  28. package/hub/team/cli/commands/start/parse-args.mjs +2 -2
  29. package/hub/team/conductor.mjs +23 -2
  30. package/hub/team/execution-mode.mjs +12 -2
  31. package/hub/team/headless.mjs +311 -24
  32. package/hub/team/intervention.mjs +530 -0
  33. package/hub/team/retry-state-machine.mjs +12 -3
  34. package/hub/team/swarm-hypervisor.mjs +21 -2
  35. package/hub/team/swarm-locks.mjs +17 -1
  36. package/hub/team/swarm-preflight.mjs +47 -0
  37. package/hub/team/synapse-http.mjs +149 -4
  38. package/hub/team/uds-orchestrator.mjs +62 -9
  39. package/hub/team/worktree-lifecycle.mjs +56 -59
  40. package/hub/tools.mjs +19 -3
  41. package/hub/workers/codex-app-server-worker.mjs +54 -5
  42. package/hub/workers/codex-mcp.mjs +21 -5
  43. package/hub/workers/delegator-mcp.mjs +3 -49
  44. package/hud/constants.mjs +21 -0
  45. package/hud/context-monitor.mjs +18 -21
  46. package/hud/hud-qos-status.mjs +1 -1
  47. package/hud/providers/codex-probe.mjs +216 -0
  48. package/hud/providers/codex.mjs +206 -29
  49. package/hud/renderers.mjs +6 -9
  50. package/package.json +2 -1
  51. package/scripts/__tests__/lint-skills.test.mjs +68 -0
  52. package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
  53. package/scripts/__tests__/tfx-route-node-entry.test.mjs +42 -6
  54. package/scripts/headless-guard.mjs +70 -23
  55. package/scripts/keyword-detector.mjs +55 -8
  56. package/scripts/lib/agent-route-policy.mjs +360 -0
  57. package/scripts/lib/cli-agy.mjs +1 -0
  58. package/scripts/lib/cli-claude.mjs +1 -0
  59. package/scripts/lib/cli-codex.mjs +23 -160
  60. package/scripts/lib/codex-profile-config.mjs +31 -5
  61. package/scripts/lib/keyword-rules.mjs +12 -0
  62. package/scripts/lint-skills.mjs +65 -0
  63. package/scripts/pack.mjs +13 -0
  64. package/scripts/release/check-packages-mirror.mjs +5 -0
  65. package/scripts/setup.mjs +108 -12
  66. package/scripts/tfx-gate-activate.mjs +1 -1
  67. package/scripts/tfx-route-worker.mjs +5 -0
  68. package/scripts/tfx-route.mjs +14 -1
  69. package/scripts/tfx-route.sh +333 -118
  70. package/skills/tfx-analysis/SKILL.md +3 -1
  71. package/skills/tfx-harness/SKILL.md +19 -76
  72. package/skills/tfx-hub/SKILL.md +2 -2
  73. package/skills/tfx-interview/SKILL.md +3 -1
  74. package/skills/tfx-interview/skill.json +1 -13
  75. package/skills/tfx-plan/SKILL.md +3 -1
  76. package/skills/tfx-profile/SKILL.md +32 -22
  77. package/skills/tfx-prune/SKILL.md +7 -3
  78. package/skills/tfx-prune/skill.json +1 -8
  79. package/skills/tfx-qa/SKILL.md +6 -2
  80. package/skills/tfx-research/SKILL.md +3 -1
  81. package/skills/tfx-review/SKILL.md +5 -3
  82. package/skills/tfx-setup/SKILL.md +1 -1
  83. package/tui/codex-profile.mjs +14 -3
  84. package/tui/setup.mjs +6 -4
@@ -11,6 +11,7 @@ import {
11
11
  shellQuote,
12
12
  } from "./cli-adapter-base.mjs";
13
13
  import { runPreflight } from "./codex-preflight.mjs";
14
+ import { isActivityLifecycleEnabled } from "./lib/worker-lifecycle.mjs";
14
15
 
15
16
  // ── Codex-specific stall inference ──────────────────────────────
16
17
 
@@ -64,9 +65,10 @@ function buildAttempts(opts, preflight) {
64
65
  forceBypass: preflight.needsBypass,
65
66
  };
66
67
  if (opts.retryOnFail === false) return [base];
68
+ const retryTimeout = isActivityLifecycleEnabled() ? timeout : timeout * 2;
67
69
  return [
68
70
  base,
69
- { ...base, timeout: timeout * 2, excluded: requested, forceBypass: true },
71
+ { ...base, timeout: retryTimeout, excluded: requested, forceBypass: true },
70
72
  ];
71
73
  }
72
74
 
@@ -116,6 +118,9 @@ export function buildExecArgs(opts = {}) {
116
118
  const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
117
119
  const command = buildExecCommand(prompt, opts.resultFile || null, {
118
120
  profile: opts.profile,
121
+ codexHome: opts.codexHome,
122
+ disallowUltra: opts.disallowUltra,
123
+ enforceCanonicalProfile: opts.enforceCanonicalProfile,
119
124
  skipGitRepoCheck: true,
120
125
  sandboxBypass: true,
121
126
  cwd: opts.cwd,
@@ -183,6 +188,8 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
183
188
  resultFile,
184
189
  inferStallMode,
185
190
  spawnEnv,
191
+ cli: "codex",
192
+ codexHome: spawnEnv?.CODEX_HOME,
186
193
  });
187
194
  }
188
195
 
@@ -45,7 +45,7 @@ function makeFallback(policy, trigger, inputsHash, now) {
45
45
  {
46
46
  cli: fb.default_cli ?? "codex",
47
47
  accountId: null,
48
- model: fb.default_model ?? "gpt-5.5",
48
+ model: fb.default_model ?? "gpt-5.6-terra",
49
49
  riskTier: "safe",
50
50
  reason: trigger,
51
51
  },
@@ -9,6 +9,7 @@ import {
9
9
  normalizePathForShell,
10
10
  runProcess,
11
11
  } from "./cli-adapter-base.mjs";
12
+ import { isActivityLifecycleEnabled } from "./lib/worker-lifecycle.mjs";
12
13
  import { whichCommandAsync } from "./platform.mjs";
13
14
 
14
15
  function shellSingleQuote(value) {
@@ -88,7 +89,8 @@ function buildAttempts(opts, preflight) {
88
89
  excludeMcpServers: [...(preflight.excludeMcpServers || [])],
89
90
  };
90
91
  if (opts.retryOnFail === false) return [base];
91
- return [base, { ...base, timeout: timeout * 2, allowedMcpServers: [] }];
92
+ const retryTimeout = isActivityLifecycleEnabled() ? timeout : timeout * 2;
93
+ return [base, { ...base, timeout: retryTimeout, allowedMcpServers: [] }];
92
94
  }
93
95
 
94
96
  // ── Public: buildExecArgs ───────────────────────────────────────
package/hub/intent.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { execFileSync } from "node:child_process";
5
5
  import crypto from "node:crypto";
6
+ import { resolveCodexAgentPolicy } from "../scripts/lib/agent-route-policy.mjs";
6
7
  import { whichCommand } from "./platform.mjs";
7
8
 
8
9
  /** 캐시 엔트리: { category, confidence, ts } */
@@ -61,27 +62,27 @@ function _tryCodexClassify(prompt) {
61
62
  * triflux 특화 의도 카테고리 (10개)
62
63
  */
63
64
  export const INTENT_CATEGORIES = {
64
- // 모델 정책 (2026-04-25):
65
- // - gpt-5.5 = 메인 (코드 포함 모든 메인 직무, fast tier 가능)
66
- // - gpt-5.4-mini = 자잘/부가/가성비 (fast tier 가능)
67
- // - gpt-5.3-codex = escalation 가성비 중간 (fast 미지원)
68
- // - gpt-5.4 폐기, gpt-5.5 로 격상
69
- implement: { agent: "executor", mcp: "implement", effort: "gpt55_high" },
70
- debug: { agent: "debugger", mcp: "implement", effort: "gpt55_xhigh" },
71
- analyze: { agent: "analyst", mcp: "analyze", effort: "gpt55_xhigh" },
72
- design: { agent: "architect", mcp: "analyze", effort: "gpt55_xhigh" },
73
- review: { agent: "code-reviewer", mcp: "review", effort: "gpt55_high" },
74
- document: { agent: "writer", mcp: "docs", effort: "pro" },
75
- research: { agent: "scientist", mcp: "analyze", effort: "gpt55_high" },
76
- "quick-fix": {
77
- agent: "build-fixer",
78
- mcp: "implement",
79
- effort: "gpt55_low",
80
- },
81
- explain: { agent: "writer", mcp: "docs", effort: "flash" },
82
- test: { agent: "test-engineer", mcp: null, effort: null },
65
+ implement: { agent: "executor", mcp: "implement" },
66
+ debug: { agent: "debugger", mcp: "implement" },
67
+ analyze: { agent: "analyst", mcp: "analyze" },
68
+ design: { agent: "architect", mcp: "analyze" },
69
+ review: { agent: "code-reviewer", mcp: "review" },
70
+ document: { agent: "writer", mcp: "docs" },
71
+ research: { agent: "scientist", mcp: "analyze" },
72
+ "quick-fix": { agent: "build-fixer", mcp: "implement" },
73
+ explain: { agent: "writer", mcp: "docs" },
74
+ test: { agent: "test-engineer", mcp: null },
83
75
  };
84
76
 
77
+ function routingFor(category) {
78
+ const route = INTENT_CATEGORIES[category] || INTENT_CATEGORIES.implement;
79
+ return {
80
+ agent: route.agent,
81
+ mcp: route.mcp,
82
+ effort: resolveCodexAgentPolicy(route.agent).profile,
83
+ };
84
+ }
85
+
85
86
  /** @internal 키워드 → 카테고리 매핑 패턴 */
86
87
  const KEYWORD_PATTERNS = [
87
88
  {
@@ -263,17 +264,12 @@ export function classifyIntent(prompt) {
263
264
  // 캐시 확인
264
265
  const cached = _getCached(hash);
265
266
  if (cached) {
266
- const routing =
267
- INTENT_CATEGORIES[cached.category] || INTENT_CATEGORIES.implement;
267
+ const routing = routingFor(cached.category);
268
268
  return {
269
269
  category: cached.category,
270
270
  confidence: cached.confidence,
271
271
  reasoning: `cache-hit: ${cached.category} (${cached.confidence.toFixed(2)})`,
272
- routing: {
273
- agent: routing.agent,
274
- mcp: routing.mcp,
275
- effort: routing.effort,
276
- },
272
+ routing,
277
273
  };
278
274
  }
279
275
 
@@ -304,12 +300,12 @@ export function classifyIntent(prompt) {
304
300
  // 캐시 저장
305
301
  _intentCache.set(hash, { category, confidence, ts: Date.now() });
306
302
 
307
- const routing = INTENT_CATEGORIES[category] || INTENT_CATEGORIES.implement;
303
+ const routing = routingFor(category);
308
304
  return {
309
305
  category,
310
306
  confidence,
311
307
  reasoning,
312
- routing: { agent: routing.agent, mcp: routing.mcp, effort: routing.effort },
308
+ routing,
313
309
  };
314
310
  }
315
311
 
@@ -15,6 +15,52 @@ function isExplicitlyOn(name) {
15
15
  return ON_VALUES.has(normalizeEnvValue(name));
16
16
  }
17
17
 
18
+ function normalizeEnv(env, name) {
19
+ return String(env?.[name] ?? "")
20
+ .trim()
21
+ .toLowerCase();
22
+ }
23
+
24
+ function isEnvOff(env, name) {
25
+ return OFF_VALUES.has(normalizeEnv(env, name));
26
+ }
27
+
28
+ function isEnvOn(env, name) {
29
+ return ON_VALUES.has(normalizeEnv(env, name));
30
+ }
31
+
32
+ /**
33
+ * Resolve the C6a control plane without mutating environment or runtime
34
+ * state. Callers own storing and incrementing the returned generation.
35
+ */
36
+ export function resolveRoleControlSnapshot(env = process.env, options = {}) {
37
+ const masterEnabled = !isEnvOff(env, "TFX_CTO");
38
+ const ctoManagerEnabled = masterEnabled && isEnvOn(env, "TFX_CTO_MANAGER");
39
+ const leadManagerEnabled =
40
+ ctoManagerEnabled && isEnvOn(env, "TFX_LEAD_MANAGER");
41
+ const northStarEnabled =
42
+ masterEnabled && !isEnvOff(env, "TFX_CTO_NORTH_STAR");
43
+ const autoCollectEnabled =
44
+ masterEnabled && !isEnvOff(env, "TFX_CTO_AUTO_COLLECT");
45
+ const values = {
46
+ master_enabled: masterEnabled,
47
+ cto_manager_enabled: ctoManagerEnabled,
48
+ lead_manager_enabled: leadManagerEnabled,
49
+ north_star_enabled: northStarEnabled,
50
+ auto_collect_enabled: autoCollectEnabled,
51
+ };
52
+ const previous = options.previous;
53
+ const changed = Object.keys(values).some(
54
+ (field) => previous?.[field] !== values[field],
55
+ );
56
+ const generation = previous
57
+ ? Number(previous.generation || 0) + (changed ? 1 : 0)
58
+ : Number.isSafeInteger(options.generation) && options.generation >= 0
59
+ ? options.generation
60
+ : 0;
61
+ return { generation, ...values };
62
+ }
63
+
18
64
  export function isCtoDisabled() {
19
65
  return isExplicitlyOff("TFX_CTO");
20
66
  }
@@ -1,6 +1,10 @@
1
1
  // hub/lib/memory-store.mjs — In-memory store (SQLite-free fallback for @triflux/core)
2
2
 
3
3
  import { recalcConfidence } from "../reflexion.mjs";
4
+ import {
5
+ clampDurationMs,
6
+ DEFAULT_ASSIGN_TIMEOUT_MS,
7
+ } from "./timeout-defaults.mjs";
4
8
  import { uuidv7 } from "./uuidv7.mjs";
5
9
 
6
10
  export function clone(value) {
@@ -19,12 +23,6 @@ function clampPriority(value, fallback = 5) {
19
23
  return Math.max(1, Math.min(Math.trunc(num), 9));
20
24
  }
21
25
 
22
- function clampDuration(value, fallback = 600000, min = 1000, max = 86400000) {
23
- const num = Number(value);
24
- if (!Number.isFinite(num)) return fallback;
25
- return Math.max(min, Math.min(Math.trunc(num), max));
26
- }
27
-
28
26
  export function clampConfidence(value, fallback = 0.5) {
29
27
  const num = Number(value);
30
28
  if (!Number.isFinite(num)) return fallback;
@@ -200,6 +198,7 @@ export function createMemoryStore() {
200
198
  agent_id: agentId,
201
199
  lease_expires_ms: Date.now() + ttlMs,
202
200
  server_time_ms: Date.now(),
201
+ effective: { updated: false, store_type: "memory", db_path: null },
203
202
  };
204
203
  const now = Date.now();
205
204
  current.last_seen_ms = now;
@@ -209,6 +208,7 @@ export function createMemoryStore() {
209
208
  agent_id: agentId,
210
209
  lease_expires_ms: current.lease_expires_ms,
211
210
  server_time_ms: now,
211
+ effective: { updated: true, store_type: "memory", db_path: null },
212
212
  };
213
213
  },
214
214
 
@@ -464,9 +464,10 @@ export function createMemoryStore() {
464
464
  retry_count = 0,
465
465
  max_retries = 0,
466
466
  priority = 5,
467
- ttl_ms = 600000,
468
- timeout_ms = 600000,
467
+ ttl_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
468
+ timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
469
469
  deadline_ms,
470
+ dispatched_at_ms = null,
470
471
  trace_id,
471
472
  correlation_id,
472
473
  last_message_id = null,
@@ -474,7 +475,10 @@ export function createMemoryStore() {
474
475
  error = null,
475
476
  }) {
476
477
  const now = Date.now();
477
- const normalizedTimeout = clampDuration(timeout_ms, 600000);
478
+ const normalizedTimeout = clampDurationMs(
479
+ timeout_ms,
480
+ DEFAULT_ASSIGN_TIMEOUT_MS,
481
+ );
478
482
  const row = {
479
483
  job_id: job_id || uuidv7(),
480
484
  supervisor_agent,
@@ -487,7 +491,7 @@ export function createMemoryStore() {
487
491
  retry_count: Math.max(0, Number(retry_count) || 0),
488
492
  max_retries: Math.max(0, Number(max_retries) || 0),
489
493
  priority: clampPriority(priority, 5),
490
- ttl_ms: clampDuration(ttl_ms, normalizedTimeout),
494
+ ttl_ms: clampDurationMs(ttl_ms, normalizedTimeout),
491
495
  timeout_ms: normalizedTimeout,
492
496
  deadline_ms: Number.isFinite(Number(deadline_ms))
493
497
  ? Math.trunc(Number(deadline_ms))
@@ -500,6 +504,10 @@ export function createMemoryStore() {
500
504
  created_at_ms: now,
501
505
  updated_at_ms: now,
502
506
  started_at_ms: status === "running" ? now : null,
507
+ dispatched_at_ms:
508
+ dispatched_at_ms == null || !Number.isFinite(Number(dispatched_at_ms))
509
+ ? null
510
+ : Math.trunc(Number(dispatched_at_ms)),
503
511
  completed_at_ms: ["succeeded", "failed", "timed_out"].includes(status)
504
512
  ? now
505
513
  : null,
@@ -524,7 +532,7 @@ export function createMemoryStore() {
524
532
  const isTerminal = ["succeeded", "failed", "timed_out"].includes(
525
533
  nextStatus,
526
534
  );
527
- const nextTimeout = clampDuration(
535
+ const nextTimeout = clampDurationMs(
528
536
  patch.timeout_ms ?? current.timeout_ms,
529
537
  current.timeout_ms,
530
538
  );
@@ -552,7 +560,7 @@ export function createMemoryStore() {
552
560
  patch.priority ?? current.priority,
553
561
  current.priority || 5,
554
562
  ),
555
- ttl_ms: clampDuration(
563
+ ttl_ms: clampDurationMs(
556
564
  patch.ttl_ms ?? current.ttl_ms,
557
565
  current.ttl_ms || nextTimeout,
558
566
  ),
@@ -589,6 +597,9 @@ export function createMemoryStore() {
589
597
  : nextStatus === "running"
590
598
  ? current.started_at_ms || now
591
599
  : current.started_at_ms,
600
+ dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
601
+ ? patch.dispatched_at_ms
602
+ : current.dispatched_at_ms,
592
603
  completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
593
604
  ? patch.completed_at_ms
594
605
  : isTerminal
@@ -654,7 +665,7 @@ export function createMemoryStore() {
654
665
  current.attempt + 1,
655
666
  Number(patch.attempt ?? current.attempt + 1) || 1,
656
667
  );
657
- const nextTimeout = clampDuration(
668
+ const nextTimeout = clampDurationMs(
658
669
  patch.timeout_ms ?? current.timeout_ms,
659
670
  current.timeout_ms,
660
671
  );
@@ -666,6 +677,7 @@ export function createMemoryStore() {
666
677
  deadline_ms: Date.now() + nextTimeout,
667
678
  completed_at_ms: null,
668
679
  started_at_ms: null,
680
+ dispatched_at_ms: null,
669
681
  last_retry_at_ms: Date.now(),
670
682
  result: patch.result ?? null,
671
683
  error: Object.hasOwn(patch, "error") ? patch.error : current.error,
@@ -0,0 +1,45 @@
1
+ // hub 조율 평면 타임아웃/TTL의 단일 기준값. 환경은 호출 시점에 읽는다.
2
+
3
+ export const MIN_DURATION_MS = 1_000;
4
+ export const MAX_DURATION_MS = 86_400_000;
5
+ export const DEFAULT_ASSIGN_TIMEOUT_MS = 600_000;
6
+ export const DEFAULT_ASSIGN_TTL_MS = 600_000;
7
+ export const DEFAULT_HANDOFF_TTL_MS = 600_000;
8
+ export const DEFAULT_SWARM_LOCK_TTL_MS = 600_000;
9
+ export const DEFAULT_REGISTER_TIMEOUT_SEC = 600;
10
+ export const REGISTER_HEARTBEAT_GRACE_MS = 120_000;
11
+ export const DEFAULT_STALL_INTERVENTION_SEC = 1_200;
12
+ export const DEFAULT_HARD_CEILING_SEC = 21_600;
13
+
14
+ function readEnvSeconds(env, name, fallbackSec) {
15
+ const value = Number(env?.[name]);
16
+ return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallbackSec;
17
+ }
18
+
19
+ export function resolveStallInterventionMs(env = process.env) {
20
+ return (
21
+ readEnvSeconds(env, "TFX_STALL_THRESHOLD", DEFAULT_STALL_INTERVENTION_SEC) *
22
+ 1000
23
+ );
24
+ }
25
+
26
+ export function resolveHardCeilingMs(env = process.env) {
27
+ return (
28
+ readEnvSeconds(env, "TFX_HARD_CEILING_SEC", DEFAULT_HARD_CEILING_SEC) * 1000
29
+ );
30
+ }
31
+
32
+ export function resolveWorkerLeaseTtlMs(env = process.env) {
33
+ return resolveStallInterventionMs(env) + REGISTER_HEARTBEAT_GRACE_MS;
34
+ }
35
+
36
+ export function clampDurationMs(
37
+ value,
38
+ fallback = DEFAULT_ASSIGN_TIMEOUT_MS,
39
+ min = MIN_DURATION_MS,
40
+ max = MAX_DURATION_MS,
41
+ ) {
42
+ const numeric = Number(value);
43
+ if (!Number.isFinite(numeric)) return fallback;
44
+ return Math.max(min, Math.min(Math.trunc(numeric), max));
45
+ }
@@ -0,0 +1,101 @@
1
+ // 활동 기반 워커 lifecycle. 시간 기본값은 timeout-defaults SSOT에서만 가져온다.
2
+ import {
3
+ resolveHardCeilingMs,
4
+ resolveStallInterventionMs,
5
+ } from "./timeout-defaults.mjs";
6
+
7
+ export { resolveHardCeilingMs, resolveStallInterventionMs };
8
+
9
+ export function isActivityLifecycleEnabled(env = process.env) {
10
+ return env.TFX_ACTIVITY_LIFECYCLE !== "0";
11
+ }
12
+
13
+ /**
14
+ * 활동 신호를 기준으로 무활동 개입과 절대 상한을 판정한다.
15
+ * 호출자는 observe()에 자기 채널의 기존 활동 서명을 전달하고, check()에
16
+ * 개입 대상 문맥을 제공한다. 이 모듈은 신호를 생성하거나 개입을 실행하지 않는다.
17
+ */
18
+ export function createActivityLifecycle({
19
+ enabled = isActivityLifecycleEnabled(),
20
+ interventionMs = resolveStallInterventionMs(),
21
+ hardCeilingMs = resolveHardCeilingMs(),
22
+ maxInterventions = 1,
23
+ onIntervene,
24
+ now = Date.now,
25
+ } = {}) {
26
+ const startedAt = now();
27
+ let lastActivityAt = startedAt;
28
+ let lastSignature;
29
+ let hasSignature = false;
30
+ let interventions = 0;
31
+ let timeoutReason = "";
32
+ let pendingIntervention = null;
33
+
34
+ const markActivity = () => {
35
+ lastActivityAt = now();
36
+ };
37
+
38
+ async function intervene(context = {}) {
39
+ if (!enabled || typeof onIntervene !== "function") return false;
40
+ if (interventions >= maxInterventions || pendingIntervention) return false;
41
+
42
+ interventions += 1;
43
+ const inactiveMs = Math.max(0, now() - lastActivityAt);
44
+ pendingIntervention = Promise.resolve(
45
+ onIntervene({ ...context, inactiveMs, interventions }),
46
+ )
47
+ .then((handled) => handled === true)
48
+ .catch(() => false);
49
+ try {
50
+ const handled = await pendingIntervention;
51
+ if (handled) markActivity();
52
+ return handled;
53
+ } finally {
54
+ pendingIntervention = null;
55
+ }
56
+ }
57
+
58
+ return Object.freeze({
59
+ observe(...args) {
60
+ if (!enabled) return false;
61
+ const [signature] = args;
62
+ const changed =
63
+ args.length === 0 || !hasSignature || signature !== lastSignature;
64
+ if (changed) {
65
+ lastSignature = signature;
66
+ hasSignature = true;
67
+ markActivity();
68
+ }
69
+ return changed;
70
+ },
71
+ async check(context = {}) {
72
+ if (!enabled || timeoutReason) return timeoutReason;
73
+ const current = now();
74
+ if (current - startedAt >= hardCeilingMs) {
75
+ timeoutReason = "hard_ceiling";
76
+ return timeoutReason;
77
+ }
78
+ if (current - lastActivityAt < interventionMs || pendingIntervention)
79
+ return "";
80
+ if (await intervene(context)) return "";
81
+ timeoutReason = "inactivity";
82
+ return timeoutReason;
83
+ },
84
+ intervene,
85
+ get enabled() {
86
+ return enabled;
87
+ },
88
+ get hardCeilingMs() {
89
+ return hardCeilingMs;
90
+ },
91
+ get interventionMs() {
92
+ return interventionMs;
93
+ },
94
+ get interventions() {
95
+ return interventions;
96
+ },
97
+ get timeoutReason() {
98
+ return timeoutReason;
99
+ },
100
+ });
101
+ }
@@ -11,6 +11,7 @@
11
11
  * health/status 체크는 로깅을 건너뛴다.
12
12
  */
13
13
 
14
+ import { HUB_REQUEST_MONITOR_CACHE_PATH } from "../../hud/constants.mjs";
14
15
  import { createContextMonitor } from "../../hud/context-monitor.mjs";
15
16
  import {
16
17
  getCorrelationId,
@@ -19,7 +20,9 @@ import {
19
20
  import { createModuleLogger } from "../../scripts/lib/logger.mjs";
20
21
 
21
22
  const log = createModuleLogger("hub");
22
- const contextMonitor = createContextMonitor();
23
+ const contextMonitor = createContextMonitor({
24
+ cachePath: HUB_REQUEST_MONITOR_CACHE_PATH,
25
+ });
23
26
 
24
27
  const SKIP_PATHS = new Set(["/health", "/healthz", "/status", "/ready"]);
25
28
  const MAX_CAPTURE_BYTES = 256 * 1024;
package/hub/pipe.mjs CHANGED
@@ -269,6 +269,15 @@ export function createPipeServer({
269
269
  payload.heartbeat_ttl_ms || heartbeatTtlMs,
270
270
  );
271
271
  if (client) touchClient(client);
272
+ if (result?.effective?.updated === false) {
273
+ return {
274
+ ok: false,
275
+ error: {
276
+ code: "AGENT_NOT_FOUND",
277
+ message: `heartbeat target not registered: ${agentId}`,
278
+ },
279
+ };
280
+ }
272
281
  return { ok: true, data: result };
273
282
  }
274
283