@triflux/core 10.1.0 → 10.25.1
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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +254 -33
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +831 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +70 -10
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,261 @@
|
|
|
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,21 +1,21 @@
|
|
|
1
|
-
import { createHash } from
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import { normalizePath } from
|
|
4
|
-
import { withRetry } from
|
|
3
|
+
import { normalizePath } from "./platform.mjs";
|
|
4
|
+
import { withRetry } from "./workers/worker-utils.mjs";
|
|
5
5
|
|
|
6
6
|
const ADAPTIVE_FINGERPRINT_VERSION = 1;
|
|
7
|
-
const DEFAULT_SCOPE =
|
|
8
|
-
const META_PREFIX =
|
|
7
|
+
const DEFAULT_SCOPE = "default";
|
|
8
|
+
const META_PREFIX = "adaptive_fingerprint:";
|
|
9
9
|
const DEFAULT_RETRY_OPTIONS = Object.freeze({
|
|
10
10
|
maxAttempts: 3,
|
|
11
11
|
baseDelayMs: 50,
|
|
12
12
|
maxDelayMs: 250,
|
|
13
13
|
});
|
|
14
14
|
const TIME_WINDOWS = Object.freeze([
|
|
15
|
-
{ name:
|
|
16
|
-
{ name:
|
|
17
|
-
{ name:
|
|
18
|
-
{ name:
|
|
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
19
|
]);
|
|
20
20
|
const MEMORY_FINGERPRINT_CACHE = new WeakMap();
|
|
21
21
|
|
|
@@ -30,8 +30,8 @@ function toIsoTimestamp(value = Date.now()) {
|
|
|
30
30
|
|
|
31
31
|
function toTimestamp(value) {
|
|
32
32
|
if (value instanceof Date) return value.getTime();
|
|
33
|
-
if (typeof value ===
|
|
34
|
-
if (typeof value ===
|
|
33
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
34
|
+
if (typeof value === "string") {
|
|
35
35
|
const parsed = Date.parse(value);
|
|
36
36
|
return Number.isFinite(parsed) ? parsed : null;
|
|
37
37
|
}
|
|
@@ -39,9 +39,21 @@ function toTimestamp(value) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function normalizeRetryOptions(options = {}) {
|
|
42
|
-
const maxAttempts = Math.max(
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
);
|
|
45
57
|
return { maxAttempts, baseDelayMs, maxDelayMs };
|
|
46
58
|
}
|
|
47
59
|
|
|
@@ -55,21 +67,23 @@ function metaKey(scope) {
|
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
function hashValue(value) {
|
|
58
|
-
const hash = createHash(
|
|
70
|
+
const hash = createHash("sha256");
|
|
59
71
|
hash.update(JSON.stringify(value));
|
|
60
|
-
return `sha256:${hash.digest(
|
|
72
|
+
return `sha256:${hash.digest("hex")}`;
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
function normalizeContextPath(value) {
|
|
64
|
-
const normalized = normalizePath(String(value ??
|
|
65
|
-
return normalized.replace(/\/+/gu,
|
|
76
|
+
const normalized = normalizePath(String(value ?? ""));
|
|
77
|
+
return normalized.replace(/\/+/gu, "/").replace(/\/+$/u, "") || "/";
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
function toRelativePath(targetPath, cwd) {
|
|
69
81
|
if (!cwd) return targetPath;
|
|
70
82
|
const base = normalizeContextPath(cwd);
|
|
71
|
-
if (targetPath === base) return
|
|
72
|
-
return targetPath.startsWith(`${base}/`)
|
|
83
|
+
if (targetPath === base) return ".";
|
|
84
|
+
return targetPath.startsWith(`${base}/`)
|
|
85
|
+
? targetPath.slice(base.length + 1)
|
|
86
|
+
: targetPath;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
function toUniqueList(values) {
|
|
@@ -84,29 +98,49 @@ function toUniqueList(values) {
|
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
function collectRawPathCandidates(context = {}) {
|
|
87
|
-
const direct = [
|
|
101
|
+
const direct = [
|
|
102
|
+
context.file_path,
|
|
103
|
+
context.filePath,
|
|
104
|
+
context.path,
|
|
105
|
+
context.target_path,
|
|
106
|
+
context.targetPath,
|
|
107
|
+
];
|
|
88
108
|
const fromArrays = [context.files, context.paths, context.targets]
|
|
89
109
|
.filter(Array.isArray)
|
|
90
110
|
.flat()
|
|
91
|
-
.map((entry) => (typeof entry ===
|
|
92
|
-
return [...direct, ...fromArrays].filter(
|
|
111
|
+
.map((entry) => (typeof entry === "string" ? entry : entry?.path));
|
|
112
|
+
return [...direct, ...fromArrays].filter(
|
|
113
|
+
(entry) => typeof entry === "string" && entry.trim().length > 0,
|
|
114
|
+
);
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
function collectPathPattern(context = {}) {
|
|
96
118
|
const normalized = toUniqueList(
|
|
97
119
|
collectRawPathCandidates(context)
|
|
98
120
|
.map((entry) => normalizeContextPath(entry))
|
|
99
|
-
.map((entry) =>
|
|
121
|
+
.map((entry) =>
|
|
122
|
+
toRelativePath(
|
|
123
|
+
entry,
|
|
124
|
+
context.cwd || context.project_root || context.projectRoot,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
100
127
|
).sort();
|
|
101
128
|
|
|
102
129
|
const primaryPath = normalized[0] ?? null;
|
|
103
130
|
const extensions = normalized
|
|
104
|
-
.map((entry) => entry.split(
|
|
105
|
-
.map((entry) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
);
|
|
110
144
|
|
|
111
145
|
return {
|
|
112
146
|
count: normalized.length,
|
|
@@ -118,17 +152,24 @@ function collectPathPattern(context = {}) {
|
|
|
118
152
|
}
|
|
119
153
|
|
|
120
154
|
function normalizeWorkType(value) {
|
|
121
|
-
const text = String(value ??
|
|
155
|
+
const text = String(value ?? "")
|
|
156
|
+
.trim()
|
|
157
|
+
.toLowerCase();
|
|
122
158
|
if (!text) {
|
|
123
|
-
return { raw: null, normalized:
|
|
159
|
+
return { raw: null, normalized: "general" };
|
|
124
160
|
}
|
|
125
|
-
const normalized =
|
|
161
|
+
const normalized =
|
|
162
|
+
text.replace(/\s+/gu, "-").replace(/[^a-z0-9-]/gu, "") || "general";
|
|
126
163
|
return { raw: value, normalized };
|
|
127
164
|
}
|
|
128
165
|
|
|
129
166
|
function collectActivityTimestamps(context = {}, now = Date.now) {
|
|
130
|
-
const nowValue = typeof now ===
|
|
131
|
-
const fromList = [
|
|
167
|
+
const nowValue = typeof now === "function" ? now() : now;
|
|
168
|
+
const fromList = [
|
|
169
|
+
context.activity_timestamps,
|
|
170
|
+
context.activityTimestamps,
|
|
171
|
+
context.timestamps,
|
|
172
|
+
]
|
|
132
173
|
.filter(Array.isArray)
|
|
133
174
|
.flat()
|
|
134
175
|
.map(toTimestamp)
|
|
@@ -136,17 +177,24 @@ function collectActivityTimestamps(context = {}, now = Date.now) {
|
|
|
136
177
|
const singles = [context.timestamp, context.started_at, context.startedAt]
|
|
137
178
|
.map(toTimestamp)
|
|
138
179
|
.filter((entry) => entry != null);
|
|
139
|
-
return fromList.length || singles.length
|
|
180
|
+
return fromList.length || singles.length
|
|
181
|
+
? [...fromList, ...singles]
|
|
182
|
+
: [Number(nowValue)];
|
|
140
183
|
}
|
|
141
184
|
|
|
142
185
|
function classifyHour(hour) {
|
|
143
186
|
const safeHour = Number.isFinite(Number(hour)) ? Number(hour) : 0;
|
|
144
|
-
const matched = TIME_WINDOWS.find(
|
|
145
|
-
|
|
187
|
+
const matched = TIME_WINDOWS.find(
|
|
188
|
+
(window) => safeHour >= window.start && safeHour <= window.end,
|
|
189
|
+
);
|
|
190
|
+
return matched?.name || "overnight";
|
|
146
191
|
}
|
|
147
192
|
|
|
148
193
|
function buildWindowHistogram(timestamps = []) {
|
|
149
|
-
const histogram = TIME_WINDOWS.reduce(
|
|
194
|
+
const histogram = TIME_WINDOWS.reduce(
|
|
195
|
+
(acc, window) => ({ ...acc, [window.name]: 0 }),
|
|
196
|
+
{},
|
|
197
|
+
);
|
|
150
198
|
for (const timestamp of timestamps) {
|
|
151
199
|
const bucket = classifyHour(new Date(timestamp).getHours());
|
|
152
200
|
histogram[bucket] = (histogram[bucket] || 0) + 1;
|
|
@@ -155,14 +203,16 @@ function buildWindowHistogram(timestamps = []) {
|
|
|
155
203
|
}
|
|
156
204
|
|
|
157
205
|
function dominantWindow(histogram) {
|
|
158
|
-
const sorted = Object.entries(histogram)
|
|
159
|
-
|
|
160
|
-
|
|
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";
|
|
161
210
|
}
|
|
162
211
|
|
|
163
212
|
function resolveTimezoneName(context = {}) {
|
|
164
|
-
const fromContext =
|
|
165
|
-
|
|
213
|
+
const fromContext =
|
|
214
|
+
typeof context.timezone === "string" ? context.timezone.trim() : "";
|
|
215
|
+
const fromIntl = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
166
216
|
return fromContext || fromIntl;
|
|
167
217
|
}
|
|
168
218
|
|
|
@@ -199,11 +249,13 @@ function getMemoryStoreMap(store) {
|
|
|
199
249
|
|
|
200
250
|
function readFingerprintFromStore(store, scope) {
|
|
201
251
|
if (!store) return null;
|
|
202
|
-
if (typeof store.loadAdaptiveFingerprint ===
|
|
252
|
+
if (typeof store.loadAdaptiveFingerprint === "function") {
|
|
203
253
|
return store.loadAdaptiveFingerprint(scope);
|
|
204
254
|
}
|
|
205
255
|
if (store.db?.prepare) {
|
|
206
|
-
const row = store.db
|
|
256
|
+
const row = store.db
|
|
257
|
+
.prepare("SELECT value FROM _meta WHERE key = ?")
|
|
258
|
+
.get(metaKey(scope));
|
|
207
259
|
return row?.value ? JSON.parse(row.value) : null;
|
|
208
260
|
}
|
|
209
261
|
return clone(getMemoryStoreMap(store).get(normalizeScope(scope)) || null);
|
|
@@ -211,11 +263,12 @@ function readFingerprintFromStore(store, scope) {
|
|
|
211
263
|
|
|
212
264
|
function writeFingerprintToStore(store, scope, record) {
|
|
213
265
|
if (!store) return clone(record);
|
|
214
|
-
if (typeof store.saveAdaptiveFingerprint ===
|
|
266
|
+
if (typeof store.saveAdaptiveFingerprint === "function") {
|
|
215
267
|
return store.saveAdaptiveFingerprint(scope, clone(record));
|
|
216
268
|
}
|
|
217
269
|
if (store.db?.prepare) {
|
|
218
|
-
store.db
|
|
270
|
+
store.db
|
|
271
|
+
.prepare("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)")
|
|
219
272
|
.run(metaKey(scope), JSON.stringify(record));
|
|
220
273
|
return clone(record);
|
|
221
274
|
}
|
|
@@ -225,7 +278,7 @@ function writeFingerprintToStore(store, scope, record) {
|
|
|
225
278
|
|
|
226
279
|
function buildHealthSnapshot(base, patch = {}) {
|
|
227
280
|
return {
|
|
228
|
-
state: patch.state || base.state ||
|
|
281
|
+
state: patch.state || base.state || "healthy",
|
|
229
282
|
retry: { ...base.retry },
|
|
230
283
|
last_success_at: patch.last_success_at ?? base.last_success_at ?? null,
|
|
231
284
|
last_failure_at: patch.last_failure_at ?? base.last_failure_at ?? null,
|
|
@@ -235,7 +288,7 @@ function buildHealthSnapshot(base, patch = {}) {
|
|
|
235
288
|
|
|
236
289
|
function createInitialHealth(retryOptions) {
|
|
237
290
|
return {
|
|
238
|
-
state:
|
|
291
|
+
state: "healthy",
|
|
239
292
|
retry: { ...retryOptions },
|
|
240
293
|
last_success_at: null,
|
|
241
294
|
last_failure_at: null,
|
|
@@ -244,7 +297,7 @@ function createInitialHealth(retryOptions) {
|
|
|
244
297
|
}
|
|
245
298
|
|
|
246
299
|
function resolveNowValue(now) {
|
|
247
|
-
return typeof now ===
|
|
300
|
+
return typeof now === "function" ? now() : now;
|
|
248
301
|
}
|
|
249
302
|
|
|
250
303
|
function mergeFingerprintSnapshot(previous, computed, scope) {
|
|
@@ -258,7 +311,7 @@ function mergeFingerprintSnapshot(previous, computed, scope) {
|
|
|
258
311
|
|
|
259
312
|
function markHealthyHealth(base, now) {
|
|
260
313
|
return buildHealthSnapshot(base, {
|
|
261
|
-
state:
|
|
314
|
+
state: "healthy",
|
|
262
315
|
last_success_at: toIsoTimestamp(resolveNowValue(now)),
|
|
263
316
|
last_error: null,
|
|
264
317
|
});
|
|
@@ -266,11 +319,11 @@ function markHealthyHealth(base, now) {
|
|
|
266
319
|
|
|
267
320
|
function markDegradedHealth(base, now, error) {
|
|
268
321
|
return buildHealthSnapshot(base, {
|
|
269
|
-
state:
|
|
322
|
+
state: "degraded",
|
|
270
323
|
last_failure_at: toIsoTimestamp(resolveNowValue(now)),
|
|
271
324
|
last_error: {
|
|
272
|
-
name: error?.name ||
|
|
273
|
-
message: error?.message ||
|
|
325
|
+
name: error?.name || "Error",
|
|
326
|
+
message: error?.message || "unknown adaptive fingerprint error",
|
|
274
327
|
},
|
|
275
328
|
});
|
|
276
329
|
}
|
|
@@ -279,7 +332,9 @@ export function buildAdaptiveFingerprint(sessionContext = {}, options = {}) {
|
|
|
279
332
|
const now = options.now ?? Date.now;
|
|
280
333
|
const capturedAt = toIsoTimestamp(resolveNowValue(now));
|
|
281
334
|
const pathPattern = collectPathPattern(sessionContext);
|
|
282
|
-
const workType = normalizeWorkType(
|
|
335
|
+
const workType = normalizeWorkType(
|
|
336
|
+
sessionContext.work_type ?? sessionContext.workType,
|
|
337
|
+
);
|
|
283
338
|
const timezonePattern = collectTimezonePattern(sessionContext, now);
|
|
284
339
|
const fingerprintId = computeFingerprintSignature({
|
|
285
340
|
path_pattern: pathPattern,
|
|
@@ -303,10 +358,18 @@ export async function loadAdaptiveFingerprint(store, scope = DEFAULT_SCOPE) {
|
|
|
303
358
|
return clone(loaded);
|
|
304
359
|
}
|
|
305
360
|
|
|
306
|
-
export async function saveAdaptiveFingerprint(
|
|
361
|
+
export async function saveAdaptiveFingerprint(
|
|
362
|
+
store,
|
|
363
|
+
scope,
|
|
364
|
+
fingerprint,
|
|
365
|
+
options = {},
|
|
366
|
+
) {
|
|
307
367
|
const retryOptions = normalizeRetryOptions(options.retryOptions);
|
|
308
368
|
const normalizedScope = normalizeScope(scope);
|
|
309
|
-
const write = async () =>
|
|
369
|
+
const write = async () =>
|
|
370
|
+
Promise.resolve(
|
|
371
|
+
writeFingerprintToStore(store, normalizedScope, fingerprint),
|
|
372
|
+
);
|
|
310
373
|
const saved = await withRetry(write, { ...retryOptions });
|
|
311
374
|
return clone(saved);
|
|
312
375
|
}
|
|
@@ -324,7 +387,9 @@ export function createAdaptiveFingerprintService(options = {}) {
|
|
|
324
387
|
const merged = mergeFingerprintSnapshot(previous, computed, scope);
|
|
325
388
|
|
|
326
389
|
try {
|
|
327
|
-
const saved = await saveAdaptiveFingerprint(store, scope, merged, {
|
|
390
|
+
const saved = await saveAdaptiveFingerprint(store, scope, merged, {
|
|
391
|
+
retryOptions,
|
|
392
|
+
});
|
|
328
393
|
health = markHealthyHealth(health, now);
|
|
329
394
|
return saved;
|
|
330
395
|
} catch (error) {
|
|
@@ -343,10 +408,11 @@ export function createAdaptiveFingerprintService(options = {}) {
|
|
|
343
408
|
|
|
344
409
|
return Object.freeze({
|
|
345
410
|
captureFingerprint: capture,
|
|
346
|
-
computeFingerprint: (sessionContext = {}) =>
|
|
411
|
+
computeFingerprint: (sessionContext = {}) =>
|
|
412
|
+
buildAdaptiveFingerprint(sessionContext, { now }),
|
|
347
413
|
loadFingerprint: read,
|
|
348
414
|
getHealth,
|
|
349
415
|
});
|
|
350
416
|
}
|
|
351
417
|
|
|
352
|
-
export default createAdaptiveFingerprintService;
|
|
418
|
+
export default createAdaptiveFingerprintService;
|