@triflux/core 10.41.1 → 10.43.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.
@@ -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;
@@ -177,6 +175,15 @@ export function createMemoryStore() {
177
175
  lease_id: uuidv7(),
178
176
  lease_expires_ms: next.lease_expires_ms,
179
177
  server_time_ms: now,
178
+ effective: {
179
+ updated: true,
180
+ online: next.status === "online",
181
+ status: next.status,
182
+ last_seen_ms: next.last_seen_ms,
183
+ lease_expires_ms: next.lease_expires_ms,
184
+ store_type: "memory",
185
+ db_path: null,
186
+ },
180
187
  };
181
188
  },
182
189
 
@@ -191,6 +198,7 @@ export function createMemoryStore() {
191
198
  agent_id: agentId,
192
199
  lease_expires_ms: Date.now() + ttlMs,
193
200
  server_time_ms: Date.now(),
201
+ effective: { updated: false, store_type: "memory", db_path: null },
194
202
  };
195
203
  const now = Date.now();
196
204
  current.last_seen_ms = now;
@@ -200,6 +208,7 @@ export function createMemoryStore() {
200
208
  agent_id: agentId,
201
209
  lease_expires_ms: current.lease_expires_ms,
202
210
  server_time_ms: now,
211
+ effective: { updated: true, store_type: "memory", db_path: null },
203
212
  };
204
213
  },
205
214
 
@@ -455,9 +464,10 @@ export function createMemoryStore() {
455
464
  retry_count = 0,
456
465
  max_retries = 0,
457
466
  priority = 5,
458
- ttl_ms = 600000,
459
- timeout_ms = 600000,
467
+ ttl_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
468
+ timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
460
469
  deadline_ms,
470
+ dispatched_at_ms = null,
461
471
  trace_id,
462
472
  correlation_id,
463
473
  last_message_id = null,
@@ -465,7 +475,10 @@ export function createMemoryStore() {
465
475
  error = null,
466
476
  }) {
467
477
  const now = Date.now();
468
- const normalizedTimeout = clampDuration(timeout_ms, 600000);
478
+ const normalizedTimeout = clampDurationMs(
479
+ timeout_ms,
480
+ DEFAULT_ASSIGN_TIMEOUT_MS,
481
+ );
469
482
  const row = {
470
483
  job_id: job_id || uuidv7(),
471
484
  supervisor_agent,
@@ -478,7 +491,7 @@ export function createMemoryStore() {
478
491
  retry_count: Math.max(0, Number(retry_count) || 0),
479
492
  max_retries: Math.max(0, Number(max_retries) || 0),
480
493
  priority: clampPriority(priority, 5),
481
- ttl_ms: clampDuration(ttl_ms, normalizedTimeout),
494
+ ttl_ms: clampDurationMs(ttl_ms, normalizedTimeout),
482
495
  timeout_ms: normalizedTimeout,
483
496
  deadline_ms: Number.isFinite(Number(deadline_ms))
484
497
  ? Math.trunc(Number(deadline_ms))
@@ -491,6 +504,10 @@ export function createMemoryStore() {
491
504
  created_at_ms: now,
492
505
  updated_at_ms: now,
493
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)),
494
511
  completed_at_ms: ["succeeded", "failed", "timed_out"].includes(status)
495
512
  ? now
496
513
  : null,
@@ -515,7 +532,7 @@ export function createMemoryStore() {
515
532
  const isTerminal = ["succeeded", "failed", "timed_out"].includes(
516
533
  nextStatus,
517
534
  );
518
- const nextTimeout = clampDuration(
535
+ const nextTimeout = clampDurationMs(
519
536
  patch.timeout_ms ?? current.timeout_ms,
520
537
  current.timeout_ms,
521
538
  );
@@ -543,7 +560,7 @@ export function createMemoryStore() {
543
560
  patch.priority ?? current.priority,
544
561
  current.priority || 5,
545
562
  ),
546
- ttl_ms: clampDuration(
563
+ ttl_ms: clampDurationMs(
547
564
  patch.ttl_ms ?? current.ttl_ms,
548
565
  current.ttl_ms || nextTimeout,
549
566
  ),
@@ -580,6 +597,9 @@ export function createMemoryStore() {
580
597
  : nextStatus === "running"
581
598
  ? current.started_at_ms || now
582
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,
583
603
  completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
584
604
  ? patch.completed_at_ms
585
605
  : isTerminal
@@ -645,7 +665,7 @@ export function createMemoryStore() {
645
665
  current.attempt + 1,
646
666
  Number(patch.attempt ?? current.attempt + 1) || 1,
647
667
  );
648
- const nextTimeout = clampDuration(
668
+ const nextTimeout = clampDurationMs(
649
669
  patch.timeout_ms ?? current.timeout_ms,
650
670
  current.timeout_ms,
651
671
  );
@@ -657,6 +677,7 @@ export function createMemoryStore() {
657
677
  deadline_ms: Date.now() + nextTimeout,
658
678
  completed_at_ms: null,
659
679
  started_at_ms: null,
680
+ dispatched_at_ms: null,
660
681
  last_retry_at_ms: Date.now(),
661
682
  result: patch.result ?? null,
662
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
+ }