@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.
- package/hooks/agy-session-hook.mjs +11 -56
- package/hooks/codex-session-hook.mjs +11 -101
- package/hub/bridge.mjs +65 -1250
- package/hub/cli-adapter-base.mjs +12 -90
- package/hub/codex-adapter.mjs +0 -1
- package/hub/codex-preflight.mjs +5 -28
- package/hub/index.mjs +3 -19
- package/hub/lib/process-utils.mjs +1 -156
- package/hub/lib/spawn-trace.mjs +3 -2
- package/hub/lib/timeout-defaults.mjs +0 -1
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/nativeProxy.mjs +759 -0
- package/hub/team/session-context.mjs +1 -1
- package/hud/cli-policy.mjs +0 -22
- package/hud/constants.mjs +13 -26
- package/hud/hud-qos-status.mjs +42 -54
- package/hud/providers/gemini.mjs +125 -20
- package/hud/renderers.mjs +101 -122
- package/hud/utils.mjs +10 -6
- package/package.json +2 -6
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/session-presence.mjs +2 -100
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/adaptive-diagnostic.mjs +0 -350
- package/hub/adaptive-inject.mjs +0 -224
- package/hub/adaptive-memory.mjs +0 -277
- package/hub/adaptive.mjs +0 -172
- package/hub/assign-callbacks.mjs +0 -170
- package/hub/delegator/contracts.mjs +0 -38
- package/hub/delegator/index.mjs +0 -14
- package/hub/delegator/schema/delegator-tools.schema.json +0 -250
- package/hub/delegator/service.mjs +0 -336
- package/hub/delegator/tool-definitions.mjs +0 -35
- package/hub/dynamic-routing-engine.mjs +0 -511
- package/hub/hitl.mjs +0 -202
- package/hub/lib/known-errors.json +0 -72
- package/hub/lib/memory-store.mjs +0 -1001
- package/hub/lib/phase-manager.mjs +0 -493
- package/hub/lib/risk-tier.mjs +0 -53
- package/hub/lib/ssh-command.mjs +0 -231
- package/hub/lib/staged-file-detect.mjs +0 -43
- package/hub/lib/state-snapshot.mjs +0 -293
- package/hub/lib/tfx-route-args.mjs +0 -353
- package/hub/lib/trace-recorder.mjs +0 -153
- package/hub/lib/uuidv7.mjs +0 -45
- package/hub/memory-doctor.mjs +0 -640
- package/hub/middleware/quota-middleware.mjs +0 -70
- package/hub/middleware/request-logger.mjs +0 -191
- package/hub/paths.mjs +0 -24
- package/hub/pipeline/index.mjs +0 -163
- package/hub/pipeline/state.mjs +0 -191
- package/hub/pipeline/transitions.mjs +0 -131
- package/hub/reflexion.mjs +0 -290
- package/hub/role-contract.mjs +0 -314
- package/hub/router.mjs +0 -2026
- package/hub/routing-snapshot.mjs +0 -261
- package/hub/session-fingerprint.mjs +0 -418
- package/hub/state.mjs +0 -300
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team-bridge.mjs +0 -27
- package/mesh/index.mjs +0 -69
- package/mesh/mesh-budget.mjs +0 -137
- package/mesh/mesh-heartbeat.mjs +0 -100
- package/mesh/mesh-protocol.mjs +0 -96
- package/mesh/mesh-queue.mjs +0 -163
- package/mesh/mesh-registry.mjs +0 -78
- package/mesh/mesh-router.mjs +0 -77
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
package/hub/routing-snapshot.mjs
DELETED
|
@@ -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;
|