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.
- package/README.md +2 -0
- package/adapters/codex/skills/tfx-harness/SKILL.md +30 -0
- package/adapters/codex/skills/tfx-harness/skill.json +5 -0
- package/bin/tfx-live.mjs +3 -1
- package/bin/triflux.mjs +14 -0
- package/config/routing-policy.json +1 -1
- package/hooks/keyword-rules.json +139 -19
- package/hooks/pipeline-stop.mjs +1 -1
- package/hub/bridge.mjs +80 -1
- package/hub/cli-adapter-base.mjs +111 -21
- package/hub/codex-adapter.mjs +8 -1
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/gemini-adapter.mjs +3 -1
- package/hub/intent.mjs +24 -28
- package/hub/lib/cto-env.mjs +46 -0
- package/hub/lib/memory-store.mjs +25 -13
- package/hub/lib/timeout-defaults.mjs +45 -0
- package/hub/lib/worker-lifecycle.mjs +101 -0
- package/hub/middleware/request-logger.mjs +4 -1
- package/hub/pipe.mjs +9 -0
- package/hub/role-contract.mjs +314 -0
- package/hub/role-control-events.mjs +113 -0
- package/hub/router.mjs +383 -29
- package/hub/schema.sql +62 -2
- package/hub/server.mjs +130 -43
- package/hub/store.mjs +609 -19
- package/hub/team/claude-daemon-control.mjs +28 -15
- package/hub/team/cli/commands/start/parse-args.mjs +2 -2
- package/hub/team/conductor.mjs +23 -2
- package/hub/team/execution-mode.mjs +12 -2
- package/hub/team/headless.mjs +311 -24
- package/hub/team/intervention.mjs +530 -0
- package/hub/team/retry-state-machine.mjs +12 -3
- package/hub/team/swarm-hypervisor.mjs +21 -2
- package/hub/team/swarm-locks.mjs +17 -1
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/uds-orchestrator.mjs +62 -9
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +19 -3
- package/hub/workers/codex-app-server-worker.mjs +54 -5
- package/hub/workers/codex-mcp.mjs +21 -5
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/hud/constants.mjs +21 -0
- package/hud/context-monitor.mjs +18 -21
- package/hud/hud-qos-status.mjs +1 -1
- package/hud/providers/codex-probe.mjs +216 -0
- package/hud/providers/codex.mjs +206 -29
- package/hud/renderers.mjs +6 -9
- package/package.json +2 -1
- package/scripts/__tests__/lint-skills.test.mjs +68 -0
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +42 -6
- package/scripts/headless-guard.mjs +70 -23
- package/scripts/keyword-detector.mjs +55 -8
- package/scripts/lib/agent-route-policy.mjs +360 -0
- package/scripts/lib/cli-agy.mjs +1 -0
- package/scripts/lib/cli-claude.mjs +1 -0
- package/scripts/lib/cli-codex.mjs +23 -160
- package/scripts/lib/codex-profile-config.mjs +31 -5
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/scripts/lint-skills.mjs +65 -0
- package/scripts/pack.mjs +13 -0
- package/scripts/release/check-packages-mirror.mjs +5 -0
- package/scripts/setup.mjs +108 -12
- package/scripts/tfx-gate-activate.mjs +1 -1
- package/scripts/tfx-route-worker.mjs +5 -0
- package/scripts/tfx-route.mjs +14 -1
- package/scripts/tfx-route.sh +333 -118
- package/skills/tfx-analysis/SKILL.md +3 -1
- package/skills/tfx-harness/SKILL.md +19 -76
- package/skills/tfx-hub/SKILL.md +2 -2
- package/skills/tfx-interview/SKILL.md +3 -1
- package/skills/tfx-interview/skill.json +1 -13
- package/skills/tfx-plan/SKILL.md +3 -1
- package/skills/tfx-profile/SKILL.md +32 -22
- package/skills/tfx-prune/SKILL.md +7 -3
- package/skills/tfx-prune/skill.json +1 -8
- package/skills/tfx-qa/SKILL.md +6 -2
- package/skills/tfx-research/SKILL.md +3 -1
- package/skills/tfx-review/SKILL.md +5 -3
- package/skills/tfx-setup/SKILL.md +1 -1
- package/tui/codex-profile.mjs +14 -3
- package/tui/setup.mjs +6 -4
package/hub/codex-adapter.mjs
CHANGED
|
@@ -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:
|
|
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
|
|
package/hub/gemini-adapter.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 =
|
|
303
|
+
const routing = routingFor(category);
|
|
308
304
|
return {
|
|
309
305
|
category,
|
|
310
306
|
confidence,
|
|
311
307
|
reasoning,
|
|
312
|
-
routing
|
|
308
|
+
routing,
|
|
313
309
|
};
|
|
314
310
|
}
|
|
315
311
|
|
package/hub/lib/cto-env.mjs
CHANGED
|
@@ -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
|
}
|
package/hub/lib/memory-store.mjs
CHANGED
|
@@ -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 =
|
|
468
|
-
timeout_ms =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|