@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,261 +0,0 @@
1
- // hub/routing-snapshot.mjs
2
- //
3
- // PRD: .omx/plans/prd-dynamic-routing-engine.md
4
- //
5
- // Phase 0 — pure read-only context projector. Wraps HUD providers and the
6
- // AccountBroker public snapshot into a single deterministic snapshot that
7
- // `evaluateDynamicRoute(request, snapshot, policy)` consumes.
8
- //
9
- // Phase 0 contract:
10
- // - no callers yet (dormant). Only `evaluateDynamicRoute()` is meant to read this.
11
- // - lazy import of HUD providers + broker so this module imports cleanly even
12
- // when those surfaces are absent in tests.
13
- // - never mutates upstream state — strictly read-only.
14
- // - all output fields are derivable from `publicSnapshot()` / HUD redacted API,
15
- // so writing the resulting snapshot to disk does not leak credentials.
16
-
17
- const CLAUDE_HUD_MODULE = "../hud/providers/claude.mjs";
18
- const CODEX_HUD_MODULE = "../hud/providers/codex.mjs";
19
- const BROKER_MODULE = "./account-broker.mjs";
20
-
21
- async function loadHudClaude() {
22
- try {
23
- return await import(CLAUDE_HUD_MODULE);
24
- } catch {
25
- return null;
26
- }
27
- }
28
-
29
- async function loadHudCodex() {
30
- try {
31
- return await import(CODEX_HUD_MODULE);
32
- } catch {
33
- return null;
34
- }
35
- }
36
-
37
- async function loadBroker() {
38
- try {
39
- const mod = await import(BROKER_MODULE);
40
- return mod.broker ?? mod.default ?? null;
41
- } catch {
42
- return null;
43
- }
44
- }
45
-
46
- function normalizeClaudeUsage(raw, now) {
47
- if (!raw || typeof raw !== "object") return null;
48
- const data = raw.data ?? raw;
49
- if (!data) return null;
50
- return {
51
- fiveHourPercent: numericOrNull(data.fiveHourPercent),
52
- weeklyPercent: numericOrNull(data.weeklyPercent),
53
- fiveHourResetsAt: toEpoch(data.fiveHourResetsAt),
54
- weeklyResetsAt: toEpoch(data.weeklyResetsAt),
55
- observedAt: toEpoch(data.observedAt) ?? now,
56
- isStale: Boolean(raw.isStale ?? data.isStale),
57
- source: data.source ?? "hud/providers/claude.mjs",
58
- };
59
- }
60
-
61
- function normalizeCodexUsage(raw, now) {
62
- if (!raw || typeof raw !== "object") return null;
63
- const buckets = raw.buckets ?? raw.data?.buckets ?? raw;
64
- if (!buckets || typeof buckets !== "object") return null;
65
- const entries = Object.entries(buckets);
66
- if (entries.length === 0) return null;
67
- const out = {};
68
- for (const [id, bucket] of entries) {
69
- if (!bucket || typeof bucket !== "object") continue;
70
- out[id] = {
71
- primary: normalizeCodexBucketLeg(bucket.primary),
72
- secondary: normalizeCodexBucketLeg(bucket.secondary),
73
- };
74
- }
75
- return {
76
- buckets: out,
77
- observedAt: toEpoch(raw.observedAt) ?? now,
78
- isStale: Boolean(raw.shouldRefresh === false ? false : raw.isStale),
79
- source: raw.source ?? "hud/providers/codex.mjs",
80
- };
81
- }
82
-
83
- function normalizeCodexBucketLeg(leg) {
84
- if (!leg || typeof leg !== "object") return null;
85
- return {
86
- usedPercent: numericOrNull(leg.used_percent ?? leg.usedPercent),
87
- resetsAt: toEpoch(leg.resets_at ?? leg.resetsAt),
88
- };
89
- }
90
-
91
- function normalizeBrokerSnapshot(raw) {
92
- if (!Array.isArray(raw)) return null;
93
- const grouped = { codex: [], claude: [], gemini: [] };
94
- for (const entry of raw) {
95
- if (!entry || typeof entry !== "object") continue;
96
- const provider = entry.provider;
97
- if (!grouped[provider]) continue;
98
- grouped[provider].push({
99
- id: entry.id,
100
- tier: entry.tier ?? "unknown",
101
- busy: Boolean(entry.busy),
102
- cooldownUntil: toEpoch(entry.cooldownUntil),
103
- lastUsedAt: toEpoch(entry.lastUsedAt),
104
- remainingMs: numericOrNull(entry.remainingMs),
105
- circuitState: entry.circuitState ?? "closed",
106
- failureCount: numericOrNull(entry.failureCount) ?? 0,
107
- expiresAt: toEpoch(entry.expiresAt),
108
- drainAfter: toEpoch(entry.drainAfter),
109
- });
110
- }
111
- return grouped;
112
- }
113
-
114
- function numericOrNull(v) {
115
- if (v === null || v === undefined) return null;
116
- const n = Number(v);
117
- return Number.isFinite(n) ? n : null;
118
- }
119
-
120
- function toEpoch(v) {
121
- if (v === null || v === undefined) return null;
122
- if (typeof v === "number") return Number.isFinite(v) ? v : null;
123
- const parsed = Date.parse(v);
124
- return Number.isNaN(parsed) ? null : parsed;
125
- }
126
-
127
- export async function buildRoutingSnapshot({
128
- forceRefresh = false,
129
- now = Date.now(),
130
- } = {}) {
131
- const [hudClaude, hudCodex, broker] = await Promise.all([
132
- loadHudClaude(),
133
- loadHudCodex(),
134
- loadBroker(),
135
- ]);
136
-
137
- let claude = null;
138
- if (hudClaude?.readClaudeUsageSnapshot) {
139
- try {
140
- claude = normalizeClaudeUsage(
141
- hudClaude.readClaudeUsageSnapshot({ forceRefresh }),
142
- now,
143
- );
144
- } catch (err) {
145
- claude = {
146
- error: String(err?.message ?? err),
147
- observedAt: now,
148
- isStale: true,
149
- };
150
- }
151
- }
152
-
153
- let codex = null;
154
- if (hudCodex?.readCodexRateLimitSnapshot) {
155
- try {
156
- codex = normalizeCodexUsage(
157
- hudCodex.readCodexRateLimitSnapshot({ forceRefresh }),
158
- now,
159
- );
160
- } catch (err) {
161
- codex = {
162
- error: String(err?.message ?? err),
163
- observedAt: now,
164
- isStale: true,
165
- };
166
- }
167
- }
168
-
169
- let brokerState = null;
170
- if (broker?.publicSnapshot) {
171
- try {
172
- brokerState = normalizeBrokerSnapshot(broker.publicSnapshot());
173
- } catch (err) {
174
- brokerState = { error: String(err?.message ?? err) };
175
- }
176
- }
177
-
178
- return {
179
- schemaVersion: 1,
180
- observedAt: now,
181
- claude,
182
- codex,
183
- broker: brokerState,
184
- };
185
- }
186
-
187
- // Phase 1 — in-process snapshot cache.
188
- // 의도: caller (Phase 1+ conductor / swarm-hypervisor) 가 spawn loop 에서
189
- // 매번 HUD/broker IO 를 트리거하지 않도록 정해진 TTL 안에서 같은 snapshot
190
- // 을 재사용한다. cache 는 모듈-local 이라 process 단위 (테스트 격리 위해
191
- // `resetSnapshotCache()` 노출).
192
-
193
- const DEFAULT_CACHE_TTL_MS = 900_000; // policy freshness max 와 정합
194
-
195
- let _cache = null; // { snapshot, fetchedAt }
196
-
197
- export function resetSnapshotCache() {
198
- _cache = null;
199
- }
200
-
201
- /**
202
- * Cached snapshot shortcut. cache TTL 안이면 재사용, 아니면 새로 빌드.
203
- *
204
- * @param {{
205
- * forceRefresh?: boolean, // true 면 cache 무시하고 새로 빌드
206
- * maxAgeMs?: number, // override cache TTL (default 900_000)
207
- * now?: number, // pure-fn: caller 가 epoch 주입 (default Date.now())
208
- * builder?: function, // 테스트용 builder 주입 (default buildRoutingSnapshot)
209
- * }} [opts]
210
- * @returns {Promise<object>}
211
- */
212
- export async function getDefaultSnapshot(opts = {}) {
213
- const now = typeof opts.now === "number" ? opts.now : Date.now();
214
- const maxAgeMs =
215
- typeof opts.maxAgeMs === "number" ? opts.maxAgeMs : DEFAULT_CACHE_TTL_MS;
216
- const builder = opts.builder || buildRoutingSnapshot;
217
- const forceRefresh = Boolean(opts.forceRefresh);
218
-
219
- if (!forceRefresh && _cache && now - _cache.fetchedAt < maxAgeMs) {
220
- return _cache.snapshot;
221
- }
222
-
223
- const snapshot = await builder({ forceRefresh, now });
224
- _cache = { snapshot, fetchedAt: now };
225
- return snapshot;
226
- }
227
-
228
- /**
229
- * Synchronous cache shortcut. cache TTL 안에 fresh snapshot이 있으면 즉시 반환,
230
- * 아니면 null. caller (sync hot path — e.g. conductor.spawnSession) 가 broker.lease
231
- * atomic 인변량을 깨지 않고 dynamic routing decision 을 sync 로 평가할 수 있도록 한다.
232
- * builder 호출(IO)은 하지 않는다 — cache miss 면 caller 가 fallback decision 사용.
233
- *
234
- * @param {{
235
- * maxAgeMs?: number, // override cache TTL (default 900_000)
236
- * now?: number, // pure-fn: caller 가 epoch 주입 (default Date.now())
237
- * }} [opts]
238
- * @returns {object|null} cache fresh이면 snapshot, 아니면 null
239
- */
240
- export function tryGetCachedSnapshot(opts = {}) {
241
- const now = typeof opts.now === "number" ? opts.now : Date.now();
242
- const maxAgeMs =
243
- typeof opts.maxAgeMs === "number" ? opts.maxAgeMs : DEFAULT_CACHE_TTL_MS;
244
- if (_cache && now - _cache.fetchedAt < maxAgeMs) {
245
- return _cache.snapshot;
246
- }
247
- return null;
248
- }
249
-
250
- // Test helpers — pure normalizers exposed so unit tests can build snapshots
251
- // from mock provider responses without running real HUD I/O.
252
- export const __internal__ = {
253
- normalizeClaudeUsage,
254
- normalizeCodexUsage,
255
- normalizeBrokerSnapshot,
256
- // Phase 1 cache 테스트 helpers
257
- getCacheStateForTest: () => (_cache ? { ..._cache } : null),
258
- setCacheForTest: (state) => {
259
- _cache = state ? { ...state } : null;
260
- },
261
- };
@@ -1,418 +0,0 @@
1
- import { createHash } from "node:crypto";
2
-
3
- import { normalizePath } from "./platform.mjs";
4
- import { withRetry } from "./workers/worker-utils.mjs";
5
-
6
- const ADAPTIVE_FINGERPRINT_VERSION = 1;
7
- const DEFAULT_SCOPE = "default";
8
- const META_PREFIX = "adaptive_fingerprint:";
9
- const DEFAULT_RETRY_OPTIONS = Object.freeze({
10
- maxAttempts: 3,
11
- baseDelayMs: 50,
12
- maxDelayMs: 250,
13
- });
14
- const TIME_WINDOWS = Object.freeze([
15
- { name: "overnight", start: 0, end: 5 },
16
- { name: "morning", start: 6, end: 11 },
17
- { name: "afternoon", start: 12, end: 17 },
18
- { name: "evening", start: 18, end: 23 },
19
- ]);
20
- const MEMORY_FINGERPRINT_CACHE = new WeakMap();
21
-
22
- function clone(value) {
23
- return value == null ? value : JSON.parse(JSON.stringify(value));
24
- }
25
-
26
- function toIsoTimestamp(value = Date.now()) {
27
- const time = Number.isFinite(Number(value)) ? Number(value) : Date.now();
28
- return new Date(time).toISOString();
29
- }
30
-
31
- function toTimestamp(value) {
32
- if (value instanceof Date) return value.getTime();
33
- if (typeof value === "number" && Number.isFinite(value)) return value;
34
- if (typeof value === "string") {
35
- const parsed = Date.parse(value);
36
- return Number.isFinite(parsed) ? parsed : null;
37
- }
38
- return null;
39
- }
40
-
41
- function normalizeRetryOptions(options = {}) {
42
- const maxAttempts = Math.max(
43
- 1,
44
- Number(options.maxAttempts ?? DEFAULT_RETRY_OPTIONS.maxAttempts) ||
45
- DEFAULT_RETRY_OPTIONS.maxAttempts,
46
- );
47
- const baseDelayMs = Math.max(
48
- 0,
49
- Number(options.baseDelayMs ?? DEFAULT_RETRY_OPTIONS.baseDelayMs) ||
50
- DEFAULT_RETRY_OPTIONS.baseDelayMs,
51
- );
52
- const maxDelayMs = Math.max(
53
- baseDelayMs,
54
- Number(options.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs) ||
55
- DEFAULT_RETRY_OPTIONS.maxDelayMs,
56
- );
57
- return { maxAttempts, baseDelayMs, maxDelayMs };
58
- }
59
-
60
- function normalizeScope(scope) {
61
- const text = String(scope ?? DEFAULT_SCOPE).trim();
62
- return text || DEFAULT_SCOPE;
63
- }
64
-
65
- function metaKey(scope) {
66
- return `${META_PREFIX}${normalizeScope(scope)}`;
67
- }
68
-
69
- function hashValue(value) {
70
- const hash = createHash("sha256");
71
- hash.update(JSON.stringify(value));
72
- return `sha256:${hash.digest("hex")}`;
73
- }
74
-
75
- function normalizeContextPath(value) {
76
- const normalized = normalizePath(String(value ?? ""));
77
- return normalized.replace(/\/+/gu, "/").replace(/\/+$/u, "") || "/";
78
- }
79
-
80
- function toRelativePath(targetPath, cwd) {
81
- if (!cwd) return targetPath;
82
- const base = normalizeContextPath(cwd);
83
- if (targetPath === base) return ".";
84
- return targetPath.startsWith(`${base}/`)
85
- ? targetPath.slice(base.length + 1)
86
- : targetPath;
87
- }
88
-
89
- function toUniqueList(values) {
90
- const seen = new Set();
91
- const next = [];
92
- for (const value of values) {
93
- if (!value || seen.has(value)) continue;
94
- seen.add(value);
95
- next.push(value);
96
- }
97
- return next;
98
- }
99
-
100
- function collectRawPathCandidates(context = {}) {
101
- const direct = [
102
- context.file_path,
103
- context.filePath,
104
- context.path,
105
- context.target_path,
106
- context.targetPath,
107
- ];
108
- const fromArrays = [context.files, context.paths, context.targets]
109
- .filter(Array.isArray)
110
- .flat()
111
- .map((entry) => (typeof entry === "string" ? entry : entry?.path));
112
- return [...direct, ...fromArrays].filter(
113
- (entry) => typeof entry === "string" && entry.trim().length > 0,
114
- );
115
- }
116
-
117
- function collectPathPattern(context = {}) {
118
- const normalized = toUniqueList(
119
- collectRawPathCandidates(context)
120
- .map((entry) => normalizeContextPath(entry))
121
- .map((entry) =>
122
- toRelativePath(
123
- entry,
124
- context.cwd || context.project_root || context.projectRoot,
125
- ),
126
- ),
127
- ).sort();
128
-
129
- const primaryPath = normalized[0] ?? null;
130
- const extensions = normalized
131
- .map((entry) => entry.split("/").pop() || "")
132
- .map((entry) =>
133
- entry.includes(".")
134
- ? entry.slice(entry.lastIndexOf(".")).toLowerCase()
135
- : "none",
136
- );
137
- const extensionCounts = extensions.reduce(
138
- (acc, ext) => ({
139
- ...acc,
140
- [ext]: (acc[ext] || 0) + 1,
141
- }),
142
- {},
143
- );
144
-
145
- return {
146
- count: normalized.length,
147
- primary_path: primaryPath,
148
- sample_paths: normalized.slice(0, 5),
149
- extension_counts: extensionCounts,
150
- checksum: hashValue(normalized),
151
- };
152
- }
153
-
154
- function normalizeWorkType(value) {
155
- const text = String(value ?? "")
156
- .trim()
157
- .toLowerCase();
158
- if (!text) {
159
- return { raw: null, normalized: "general" };
160
- }
161
- const normalized =
162
- text.replace(/\s+/gu, "-").replace(/[^a-z0-9-]/gu, "") || "general";
163
- return { raw: value, normalized };
164
- }
165
-
166
- function collectActivityTimestamps(context = {}, now = Date.now) {
167
- const nowValue = typeof now === "function" ? now() : now;
168
- const fromList = [
169
- context.activity_timestamps,
170
- context.activityTimestamps,
171
- context.timestamps,
172
- ]
173
- .filter(Array.isArray)
174
- .flat()
175
- .map(toTimestamp)
176
- .filter((entry) => entry != null);
177
- const singles = [context.timestamp, context.started_at, context.startedAt]
178
- .map(toTimestamp)
179
- .filter((entry) => entry != null);
180
- return fromList.length || singles.length
181
- ? [...fromList, ...singles]
182
- : [Number(nowValue)];
183
- }
184
-
185
- function classifyHour(hour) {
186
- const safeHour = Number.isFinite(Number(hour)) ? Number(hour) : 0;
187
- const matched = TIME_WINDOWS.find(
188
- (window) => safeHour >= window.start && safeHour <= window.end,
189
- );
190
- return matched?.name || "overnight";
191
- }
192
-
193
- function buildWindowHistogram(timestamps = []) {
194
- const histogram = TIME_WINDOWS.reduce(
195
- (acc, window) => ({ ...acc, [window.name]: 0 }),
196
- {},
197
- );
198
- for (const timestamp of timestamps) {
199
- const bucket = classifyHour(new Date(timestamp).getHours());
200
- histogram[bucket] = (histogram[bucket] || 0) + 1;
201
- }
202
- return histogram;
203
- }
204
-
205
- function dominantWindow(histogram) {
206
- const sorted = Object.entries(histogram).sort(
207
- (left, right) => right[1] - left[1] || left[0].localeCompare(right[0]),
208
- );
209
- return sorted[0]?.[0] || "overnight";
210
- }
211
-
212
- function resolveTimezoneName(context = {}) {
213
- const fromContext =
214
- typeof context.timezone === "string" ? context.timezone.trim() : "";
215
- const fromIntl = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
216
- return fromContext || fromIntl;
217
- }
218
-
219
- function collectTimezonePattern(context = {}, now = Date.now) {
220
- const timestamps = collectActivityTimestamps(context, now);
221
- const firstTimestamp = timestamps[0] ?? Date.now();
222
- const histogram = buildWindowHistogram(timestamps);
223
- return {
224
- timezone: resolveTimezoneName(context),
225
- offset_minutes: -new Date(firstTimestamp).getTimezoneOffset(),
226
- sample_count: timestamps.length,
227
- window_histogram: histogram,
228
- dominant_window: dominantWindow(histogram),
229
- };
230
- }
231
-
232
- function computeFingerprintSignature(input) {
233
- const source = {
234
- file_checksum: input.path_pattern.checksum,
235
- work_type: input.work_type.normalized,
236
- timezone: input.timezone_pattern.timezone,
237
- dominant_window: input.timezone_pattern.dominant_window,
238
- };
239
- return hashValue(source);
240
- }
241
-
242
- function getMemoryStoreMap(store) {
243
- const current = MEMORY_FINGERPRINT_CACHE.get(store);
244
- if (current) return current;
245
- const next = new Map();
246
- MEMORY_FINGERPRINT_CACHE.set(store, next);
247
- return next;
248
- }
249
-
250
- function readFingerprintFromStore(store, scope) {
251
- if (!store) return null;
252
- if (typeof store.loadAdaptiveFingerprint === "function") {
253
- return store.loadAdaptiveFingerprint(scope);
254
- }
255
- if (store.db?.prepare) {
256
- const row = store.db
257
- .prepare("SELECT value FROM _meta WHERE key = ?")
258
- .get(metaKey(scope));
259
- return row?.value ? JSON.parse(row.value) : null;
260
- }
261
- return clone(getMemoryStoreMap(store).get(normalizeScope(scope)) || null);
262
- }
263
-
264
- function writeFingerprintToStore(store, scope, record) {
265
- if (!store) return clone(record);
266
- if (typeof store.saveAdaptiveFingerprint === "function") {
267
- return store.saveAdaptiveFingerprint(scope, clone(record));
268
- }
269
- if (store.db?.prepare) {
270
- store.db
271
- .prepare("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)")
272
- .run(metaKey(scope), JSON.stringify(record));
273
- return clone(record);
274
- }
275
- getMemoryStoreMap(store).set(normalizeScope(scope), clone(record));
276
- return clone(record);
277
- }
278
-
279
- function buildHealthSnapshot(base, patch = {}) {
280
- return {
281
- state: patch.state || base.state || "healthy",
282
- retry: { ...base.retry },
283
- last_success_at: patch.last_success_at ?? base.last_success_at ?? null,
284
- last_failure_at: patch.last_failure_at ?? base.last_failure_at ?? null,
285
- last_error: patch.last_error ?? base.last_error ?? null,
286
- };
287
- }
288
-
289
- function createInitialHealth(retryOptions) {
290
- return {
291
- state: "healthy",
292
- retry: { ...retryOptions },
293
- last_success_at: null,
294
- last_failure_at: null,
295
- last_error: null,
296
- };
297
- }
298
-
299
- function resolveNowValue(now) {
300
- return typeof now === "function" ? now() : now;
301
- }
302
-
303
- function mergeFingerprintSnapshot(previous, computed, scope) {
304
- return {
305
- ...computed,
306
- scope,
307
- observation_count: (previous?.observation_count || 0) + 1,
308
- first_captured_at: previous?.first_captured_at || computed.captured_at,
309
- };
310
- }
311
-
312
- function markHealthyHealth(base, now) {
313
- return buildHealthSnapshot(base, {
314
- state: "healthy",
315
- last_success_at: toIsoTimestamp(resolveNowValue(now)),
316
- last_error: null,
317
- });
318
- }
319
-
320
- function markDegradedHealth(base, now, error) {
321
- return buildHealthSnapshot(base, {
322
- state: "degraded",
323
- last_failure_at: toIsoTimestamp(resolveNowValue(now)),
324
- last_error: {
325
- name: error?.name || "Error",
326
- message: error?.message || "unknown adaptive fingerprint error",
327
- },
328
- });
329
- }
330
-
331
- export function buildAdaptiveFingerprint(sessionContext = {}, options = {}) {
332
- const now = options.now ?? Date.now;
333
- const capturedAt = toIsoTimestamp(resolveNowValue(now));
334
- const pathPattern = collectPathPattern(sessionContext);
335
- const workType = normalizeWorkType(
336
- sessionContext.work_type ?? sessionContext.workType,
337
- );
338
- const timezonePattern = collectTimezonePattern(sessionContext, now);
339
- const fingerprintId = computeFingerprintSignature({
340
- path_pattern: pathPattern,
341
- work_type: workType,
342
- timezone_pattern: timezonePattern,
343
- });
344
-
345
- return {
346
- version: ADAPTIVE_FINGERPRINT_VERSION,
347
- captured_at: capturedAt,
348
- scope: normalizeScope(sessionContext.scope),
349
- fingerprint_id: fingerprintId,
350
- path_pattern: pathPattern,
351
- work_type: workType,
352
- timezone_pattern: timezonePattern,
353
- };
354
- }
355
-
356
- export async function loadAdaptiveFingerprint(store, scope = DEFAULT_SCOPE) {
357
- const loaded = await Promise.resolve(readFingerprintFromStore(store, scope));
358
- return clone(loaded);
359
- }
360
-
361
- export async function saveAdaptiveFingerprint(
362
- store,
363
- scope,
364
- fingerprint,
365
- options = {},
366
- ) {
367
- const retryOptions = normalizeRetryOptions(options.retryOptions);
368
- const normalizedScope = normalizeScope(scope);
369
- const write = async () =>
370
- Promise.resolve(
371
- writeFingerprintToStore(store, normalizedScope, fingerprint),
372
- );
373
- const saved = await withRetry(write, { ...retryOptions });
374
- return clone(saved);
375
- }
376
-
377
- export function createAdaptiveFingerprintService(options = {}) {
378
- const store = options.store ?? null;
379
- const retryOptions = normalizeRetryOptions(options.retryOptions);
380
- const now = options.now ?? Date.now;
381
- let health = createInitialHealth(retryOptions);
382
-
383
- async function capture(sessionContext = {}) {
384
- const computed = buildAdaptiveFingerprint(sessionContext, { now });
385
- const scope = normalizeScope(sessionContext.scope ?? computed.scope);
386
- const previous = await loadAdaptiveFingerprint(store, scope);
387
- const merged = mergeFingerprintSnapshot(previous, computed, scope);
388
-
389
- try {
390
- const saved = await saveAdaptiveFingerprint(store, scope, merged, {
391
- retryOptions,
392
- });
393
- health = markHealthyHealth(health, now);
394
- return saved;
395
- } catch (error) {
396
- health = markDegradedHealth(health, now, error);
397
- throw error;
398
- }
399
- }
400
-
401
- async function read(scope = DEFAULT_SCOPE) {
402
- return loadAdaptiveFingerprint(store, scope);
403
- }
404
-
405
- function getHealth() {
406
- return clone(health);
407
- }
408
-
409
- return Object.freeze({
410
- captureFingerprint: capture,
411
- computeFingerprint: (sessionContext = {}) =>
412
- buildAdaptiveFingerprint(sessionContext, { now }),
413
- loadFingerprint: read,
414
- getHealth,
415
- });
416
- }
417
-
418
- export default createAdaptiveFingerprintService;