opencode-agent-skill 13.0.0-beta.2 → 14.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1571 -607
- package/bin/ocskill.mjs +172 -24
- package/docs/OPENCODE-COMPAT.md +34 -97
- package/docs/PI-COMPAT.md +203 -0
- package/docs/V14-CONTEXT-MEMORY-FABRIC.md +70 -0
- package/docs/V14.1-QUALITY-PERFORMANCE-FABRIC.md +114 -0
- package/docs/V14.2-TURBO-WEAK-MODEL-RUNTIME.md +448 -0
- package/evals/v14/tasks.json +46 -0
- package/global-config/agents/executor.md +7 -0
- package/global-config/agents/visual-verifier.md +22 -3
- package/global-config/plugins/ues-router/index.js +13 -8
- package/global-config/plugins/ues-router/policy-runtime.js +7 -0
- package/global-config/plugins/ues-router/router.js +12 -2
- package/global-config/skills/ecommerce-engineering/SKILL.md +1 -1
- package/global-config/skills/file-upload-engineering/SKILL.md +1 -1
- package/global-config/skills/git-safety/SKILL.md +1 -1
- package/global-config/skills/nestjs-engineering/SKILL.md +1 -1
- package/global-config/skills/performance-engineering/SKILL.md +1 -1
- package/global-config/skills/react-native-engineering/SKILL.md +1 -1
- package/global-config/skills/rest-api-design/SKILL.md +1 -1
- package/global-config/skills/ui-ux-engineering/SKILL.md +1 -1
- package/lib/adaptive-context-budget.mjs +97 -0
- package/lib/affected-tests.mjs +260 -0
- package/lib/benchmark-confidence.mjs +41 -2
- package/lib/browser-mcp-routing.mjs +166 -0
- package/lib/capability-fabric.mjs +359 -0
- package/lib/capability-registry.mjs +9 -0
- package/lib/code-intelligence/edit-anchor.mjs +102 -0
- package/lib/code-intelligence/index.mjs +95 -0
- package/lib/code-intelligence/lsp-provider.mjs +189 -0
- package/lib/completion-auditor.mjs +82 -0
- package/lib/context-engine-v11.mjs +65 -1
- package/lib/context-graph-rank.mjs +118 -0
- package/lib/context-manifest.mjs +97 -18
- package/lib/control-center.mjs +19 -1
- package/lib/document-ingestion.mjs +60 -0
- package/lib/dynamic-workflow.mjs +3 -1
- package/lib/evidence-store.mjs +82 -1
- package/lib/fast-verification-gate.mjs +69 -0
- package/lib/hierarchical-context.mjs +215 -0
- package/lib/mcp-health.mjs +144 -0
- package/lib/mcp-tool-policy.mjs +19 -0
- package/lib/memory-engine.mjs +493 -0
- package/lib/model-performance.mjs +33 -8
- package/lib/model-policy.mjs +3 -3
- package/lib/orchestrator-policy.mjs +5 -209
- package/lib/performance-fabric.mjs +229 -0
- package/lib/pi-rpc-pool.mjs +433 -0
- package/lib/process-hang-detector.mjs +83 -0
- package/lib/process-supervisor.mjs +193 -0
- package/lib/prompt-cache.mjs +27 -11
- package/lib/repo-graph.mjs +53 -2
- package/lib/reversible-context.mjs +49 -0
- package/lib/runtime-config.mjs +31 -0
- package/lib/safety.mjs +132 -0
- package/lib/semantic-index.mjs +52 -3
- package/lib/skill-compiler.mjs +128 -0
- package/lib/skill-quality.mjs +48 -2
- package/lib/task-engine.mjs +62 -5
- package/lib/task-policy.mjs +291 -0
- package/lib/verification-broker.mjs +284 -0
- package/lib/verification-command.mjs +111 -0
- package/lib/windows-shim.mjs +35 -0
- package/lib/workspace-fingerprint.mjs +198 -0
- package/package.json +52 -42
- package/pi/extensions/ues-child-runtime.ts +393 -0
- package/pi/extensions/ues.ts +3327 -0
- package/pi/prompts/ues-audit.md +9 -0
- package/pi/prompts/ues-critique.md +9 -0
- package/pi/prompts/ues-debug.md +9 -0
- package/pi/prompts/ues-feature.md +9 -0
- package/pi/prompts/ues-fix.md +9 -0
- package/pi/prompts/ues-plan.md +9 -0
- package/pi/prompts/ues-research.md +9 -0
- package/pi/prompts/ues-resume.md +9 -0
- package/pi/prompts/ues-review.md +7 -0
- package/pi/prompts/ues-run.md +17 -0
- package/pi/prompts/ues-verify.md +9 -0
- package/scripts/check-release-consistency.mjs +119 -185
- package/scripts/check-runtime-exports.mjs +66 -0
- package/scripts/check-source-integrity.mjs +246 -0
- package/scripts/eval-pi.mjs +492 -0
- package/scripts/install.mjs +16 -0
- package/scripts/smoke-package-closure.mjs +110 -0
- package/scripts/smoke-packed-install.mjs +24 -11
- package/scripts/smoke-pi-extension.mjs +144 -0
- package/scripts/uninstall.mjs +44 -0
- package/CHANGELOG.md +0 -415
- package/docs/DETERMINISTIC-TOOLS.md +0 -105
- package/docs/ENGINEERING-DESIGN.md +0 -194
- package/docs/EVALS.md +0 -158
- package/docs/GITHUB-RULESET.md +0 -50
- package/docs/NPM-PUBLISH.md +0 -116
- package/docs/RESEARCH-SOURCES.md +0 -37
- package/docs/TRACE-SCHEMA.md +0 -122
- package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +0 -75
- package/docs/V11-PERCEPTION-ADAPTIVE.md +0 -220
- package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +0 -27
- package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +0 -86
- package/docs/V7-INTELLIGENCE-RUNTIME.md +0 -166
- package/docs/V8-INTELLIGENCE-RELIABILITY.md +0 -206
- package/docs/V9-SPEED-INTELLIGENCE.md +0 -102
package/lib/control-center.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import path from "node:path"
|
|
|
4
4
|
import { readLearningState } from "./learning-engine.mjs"
|
|
5
5
|
import { readRuntimeEvents } from "./runtime-events.mjs"
|
|
6
6
|
import { evidenceStoreStatus } from "./evidence-store.mjs"
|
|
7
|
+
import { memoryStatus } from "./memory-engine.mjs"
|
|
8
|
+
import { capabilityFabricStatus } from "./capability-fabric.mjs"
|
|
7
9
|
|
|
8
10
|
function escapeHtml(value) {
|
|
9
11
|
return String(value ?? "")
|
|
@@ -65,11 +67,13 @@ async function collectEvalSummary(root) {
|
|
|
65
67
|
|
|
66
68
|
export async function collectControlCenterData(root = process.cwd()) {
|
|
67
69
|
root = path.resolve(root)
|
|
68
|
-
const [work, learning, evals, evidenceStore] = await Promise.all([
|
|
70
|
+
const [work, learning, evals, evidenceStore, memory, capabilityFabric] = await Promise.all([
|
|
69
71
|
collectWork(root),
|
|
70
72
|
readLearningState(root),
|
|
71
73
|
collectEvalSummary(root),
|
|
72
74
|
evidenceStoreStatus(root),
|
|
75
|
+
memoryStatus(root),
|
|
76
|
+
capabilityFabricStatus(root),
|
|
73
77
|
])
|
|
74
78
|
return {
|
|
75
79
|
schemaVersion: 2,
|
|
@@ -82,6 +86,16 @@ export async function collectControlCenterData(root = process.cwd()) {
|
|
|
82
86
|
evidenceStore,
|
|
83
87
|
runtime: "perception-adaptive-execution",
|
|
84
88
|
},
|
|
89
|
+
v14: {
|
|
90
|
+
memory,
|
|
91
|
+
capabilityFabric: {
|
|
92
|
+
healthyCapabilities: capabilityFabric.healthyCapabilities,
|
|
93
|
+
totalCapabilities: capabilityFabric.totalCapabilities,
|
|
94
|
+
rows: capabilityFabric.rows,
|
|
95
|
+
observationState: capabilityFabric.observationState,
|
|
96
|
+
},
|
|
97
|
+
runtime: "context-memory-capability-fabric",
|
|
98
|
+
},
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
|
|
@@ -110,6 +124,7 @@ details{margin-top:10px;border-top:1px solid #243049;padding-top:8px}summary{cur
|
|
|
110
124
|
<div class="section"><h2>Long-horizon work</h2><div class="grid" id="work"></div></div>
|
|
111
125
|
<div class="section"><h2>Learning loop</h2><div class="grid" id="learning"></div></div>
|
|
112
126
|
<div class="section"><h2>V11 runtime efficiency</h2><div class="grid" id="v11"></div></div>
|
|
127
|
+
<div class="section"><h2>V14 context & memory fabric</h2><div class="grid" id="v14"></div></div>
|
|
113
128
|
<div class="section"><h2>Recent runtime events</h2><div class="card"><table><thead><tr><th>Work</th><th>Event</th><th>Task</th><th>Time</th></tr></thead><tbody id="events"></tbody></table></div></div>
|
|
114
129
|
<div class="section"><h2>Recent evaluations</h2><div class="card"><table><thead><tr><th>Suite</th><th>Model</th><th>Baseline</th><th>UES</th></tr></thead><tbody id="evals"></tbody></table></div></div>
|
|
115
130
|
</div>
|
|
@@ -136,6 +151,9 @@ function render(next){
|
|
|
136
151
|
const v11=document.querySelector("#v11");
|
|
137
152
|
const store=data.v11?.evidenceStore||{};
|
|
138
153
|
v11.innerHTML='<div class="card"><h3>Evidence store</h3><strong>'+esc(store.entries||0)+'</strong><div class="muted">'+esc(store.bytes||0)+' bytes externalized</div></div><div class="card"><h3>Runtime</h3><strong>V11</strong><div class="muted">'+esc(data.v11?.runtime||'adaptive')+'</div></div>';
|
|
154
|
+
const v14=document.querySelector("#v14");
|
|
155
|
+
const memory=data.v14?.memory||{};
|
|
156
|
+
v14.innerHTML='<div class="card"><h3>Verified memory</h3><strong>'+esc(memory.byStatus?.verified||0)+'</strong><div class="muted">'+esc(memory.entries||0)+' total entries</div></div><div class="card"><h3>Runtime</h3><strong>V14</strong><div class="muted">'+esc(data.v14?.runtime||'context-memory-fabric')+'</div></div>';
|
|
139
157
|
const eventBody=document.querySelector("#events");
|
|
140
158
|
eventBody.innerHTML="";
|
|
141
159
|
for(const item of data.work){
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from "node:fs/promises"
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
const BUILTIN_TEXT_EXTENSIONS = new Set([
|
|
6
|
+
".txt", ".md", ".markdown", ".json", ".jsonl", ".csv", ".tsv", ".xml", ".html", ".htm",
|
|
7
|
+
".yaml", ".yml", ".toml", ".ini", ".log", ".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx",
|
|
8
|
+
".py", ".java", ".kt", ".kts", ".cs", ".go", ".rs", ".rb", ".php", ".sql", ".css", ".scss",
|
|
9
|
+
])
|
|
10
|
+
const MARKITDOWN_EXTENSIONS = new Set([".pdf", ".docx", ".pptx", ".xlsx", ".xls", ".epub", ".zip"])
|
|
11
|
+
|
|
12
|
+
function inside(root, target) { return target === root || target.startsWith(root + path.sep) }
|
|
13
|
+
async function safeFile(root, relative) {
|
|
14
|
+
const base = await realpath(path.resolve(root)).catch(() => path.resolve(root))
|
|
15
|
+
const requested = path.resolve(base, String(relative || ""))
|
|
16
|
+
if (!inside(base, requested)) throw new Error("document path escapes workspace root")
|
|
17
|
+
const info = await lstat(requested)
|
|
18
|
+
if (!info.isFile()) throw new Error("document path is not a file")
|
|
19
|
+
const actual = await realpath(requested)
|
|
20
|
+
if (!inside(base, actual)) throw new Error("document symlink escapes workspace root")
|
|
21
|
+
return { base, file: actual, info }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function runMarkItDown(file, options = {}) {
|
|
25
|
+
const timeout = Math.max(5_000, Math.min(120_000, Number(options.timeoutMs || 30_000)))
|
|
26
|
+
const maxBuffer = Math.max(256 * 1024, Math.min(16 * 1024 * 1024, Number(options.maxBuffer || 4 * 1024 * 1024)))
|
|
27
|
+
const attempts = process.platform === "win32"
|
|
28
|
+
? [["markitdown.exe", [file]], ["markitdown", [file]], ["py", ["-m", "markitdown", file]], ["python", ["-m", "markitdown", file]]]
|
|
29
|
+
: [["markitdown", [file]], ["python3", ["-m", "markitdown", file]], ["python", ["-m", "markitdown", file]]]
|
|
30
|
+
for (const [command, args] of attempts) {
|
|
31
|
+
const result = spawnSync(command, args, { encoding: "utf8", timeout, maxBuffer, windowsHide: true })
|
|
32
|
+
if (result.status === 0 && String(result.stdout || "").trim()) return { provider: command.includes("python") || command === "py" ? "markitdown-python" : "markitdown-cli", markdown: String(result.stdout) }
|
|
33
|
+
if (result.error?.code !== "ENOENT" && result.status != null) return { provider: "markitdown-unavailable", error: String(result.stderr || result.error?.message || `exit ${result.status}`) }
|
|
34
|
+
}
|
|
35
|
+
return { provider: "markitdown-unavailable", error: "MarkItDown command/module was not found" }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function documentIngestionSupport(file = "") {
|
|
39
|
+
const ext = path.extname(String(file)).toLowerCase()
|
|
40
|
+
return { schemaVersion: 1, extension: ext, builtin: BUILTIN_TEXT_EXTENSIONS.has(ext), markitdown: MARKITDOWN_EXTENSIONS.has(ext), supported: BUILTIN_TEXT_EXTENSIONS.has(ext) || MARKITDOWN_EXTENSIONS.has(ext) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function ingestDocument(root, relative, options = {}) {
|
|
44
|
+
const target = await safeFile(root, relative)
|
|
45
|
+
const support = documentIngestionSupport(target.file)
|
|
46
|
+
const maxBytes = Math.max(16 * 1024, Math.min(32 * 1024 * 1024, Number(options.maxBytes || 4 * 1024 * 1024)))
|
|
47
|
+
if (target.info.size > maxBytes) throw new Error(`document exceeds ingestion limit (${target.info.size} > ${maxBytes} bytes)`)
|
|
48
|
+
if (support.builtin) {
|
|
49
|
+
const content = await readFile(target.file, "utf8")
|
|
50
|
+
return { schemaVersion: 1, provider: "builtin-text", file: path.relative(target.base, target.file).replaceAll("\\", "/"), bytes: target.info.size, markdown: content, optionalDependency: false }
|
|
51
|
+
}
|
|
52
|
+
if (support.markitdown) {
|
|
53
|
+
const converted = runMarkItDown(target.file, options)
|
|
54
|
+
if (converted.markdown) return { schemaVersion: 1, provider: converted.provider, file: path.relative(target.base, target.file).replaceAll("\\", "/"), bytes: target.info.size, markdown: converted.markdown, optionalDependency: true }
|
|
55
|
+
const error = new Error(`${converted.error}. Install Microsoft MarkItDown only when Office/PDF ingestion is needed.`)
|
|
56
|
+
error.code = "UES_MARKITDOWN_UNAVAILABLE"
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`unsupported document extension: ${support.extension || "(none)"}`)
|
|
60
|
+
}
|
package/lib/dynamic-workflow.mjs
CHANGED
|
@@ -50,6 +50,7 @@ function conflict(a, b) {
|
|
|
50
50
|
|
|
51
51
|
function executionMode(classification, options) {
|
|
52
52
|
if (classification.kind === "deterministic") return "deterministic"
|
|
53
|
+
if (options.allowInline === false) return "agent"
|
|
53
54
|
if (classification.kind === "vision") {
|
|
54
55
|
return classification.estimatedCost >= options.minVisionAgentCost ? "agent" : "inline"
|
|
55
56
|
}
|
|
@@ -81,6 +82,7 @@ export function planDynamicWorkflow(tasks = [], inputOptions = {}) {
|
|
|
81
82
|
minAgentCost: clampInt(inputOptions.minAgentCost, 5, 2, 12),
|
|
82
83
|
minVisionAgentCost: clampInt(inputOptions.minVisionAgentCost, 4, 2, 12),
|
|
83
84
|
deterministicFirst: inputOptions.deterministicFirst !== false,
|
|
85
|
+
allowInline: inputOptions.allowInline !== false,
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
const byID = new Map(tasks.map((task) => [task.id, task]))
|
|
@@ -176,4 +178,4 @@ export function planDynamicWorkflow(tasks = [], inputOptions = {}) {
|
|
|
176
178
|
visionAgentTaskCount: flat.filter((task) => task.spawnAgent && task.kind === "vision").length,
|
|
177
179
|
estimatedCoordinationSaved: flat.filter((task) => task.execution !== "agent").reduce((sum, task) => sum + task.estimatedCost, 0),
|
|
178
180
|
}
|
|
179
|
-
}
|
|
181
|
+
}
|
package/lib/evidence-store.mjs
CHANGED
|
@@ -99,6 +99,87 @@ export async function getEvidence(root, ref, options = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
|
|
103
|
+
function evidenceSelector(ref) {
|
|
104
|
+
const value = String(ref || "").trim()
|
|
105
|
+
const index = value.indexOf("#")
|
|
106
|
+
if (index < 0) return { ref: value, selector: null }
|
|
107
|
+
return {
|
|
108
|
+
ref: value.slice(0, index),
|
|
109
|
+
selector: value.slice(index + 1),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function selectorParts(selector) {
|
|
114
|
+
const value = String(selector || "").trim()
|
|
115
|
+
if (!value) return []
|
|
116
|
+
if (value.startsWith("/")) {
|
|
117
|
+
return value
|
|
118
|
+
.split("/")
|
|
119
|
+
.slice(1)
|
|
120
|
+
.map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"))
|
|
121
|
+
}
|
|
122
|
+
return value
|
|
123
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
124
|
+
.split(".")
|
|
125
|
+
.map((part) => part.trim())
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function selectJsonValue(value, selector) {
|
|
130
|
+
let current = value
|
|
131
|
+
for (const part of selectorParts(selector)) {
|
|
132
|
+
if (Array.isArray(current)) {
|
|
133
|
+
if (!/^\d+$/.test(part)) throw new Error("Invalid array selector segment: " + part)
|
|
134
|
+
const index = Number(part)
|
|
135
|
+
if (index < 0 || index >= current.length) throw new Error("Evidence selector index out of range: " + part)
|
|
136
|
+
current = current[index]
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (!current || typeof current !== "object" || !Object.hasOwn(current, part)) {
|
|
140
|
+
throw new Error("Evidence selector path not found: " + part)
|
|
141
|
+
}
|
|
142
|
+
current = current[part]
|
|
143
|
+
}
|
|
144
|
+
return current
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function getEvidenceSelected(root, refWithSelector, options = {}) {
|
|
148
|
+
const split = evidenceSelector(refWithSelector)
|
|
149
|
+
if (!split.selector) return getEvidence(root, split.ref, options)
|
|
150
|
+
|
|
151
|
+
const hash = normalizeHash(split.ref)
|
|
152
|
+
const target = evidencePaths(root, hash)
|
|
153
|
+
const metadata = JSON.parse(await readFile(target.meta, "utf8"))
|
|
154
|
+
if (metadata.encoding !== "utf8" || !String(metadata.mediaType || "").includes("json")) {
|
|
155
|
+
throw new Error("Evidence selectors require JSON evidence")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const maxObjectBytes = Math.max(1024, Math.min(16 * 1024 * 1024, Number(options.maxObjectBytes || 4 * 1024 * 1024)))
|
|
159
|
+
if (Number(metadata.bytes || 0) > maxObjectBytes) {
|
|
160
|
+
throw new Error("JSON evidence is too large for selector retrieval; use byte slices instead")
|
|
161
|
+
}
|
|
162
|
+
const source = await readFile(target.data, "utf8")
|
|
163
|
+
const selected = selectJsonValue(JSON.parse(source), split.selector)
|
|
164
|
+
const encoded = typeof selected === "string"
|
|
165
|
+
? selected
|
|
166
|
+
: JSON.stringify(stableValue(selected), null, 2)
|
|
167
|
+
const maxBytes = Math.max(1, Number(options.maxBytes || options.maxChars || 24_000))
|
|
168
|
+
const bytes = Buffer.from(encoded, "utf8")
|
|
169
|
+
const start = Math.max(0, Number(options.start || 0))
|
|
170
|
+
const slice = bytes.subarray(start, Math.min(bytes.length, start + maxBytes))
|
|
171
|
+
return {
|
|
172
|
+
...metadata,
|
|
173
|
+
ref: split.ref + "#" + split.selector,
|
|
174
|
+
selector: split.selector,
|
|
175
|
+
selectedBytes: bytes.length,
|
|
176
|
+
truncated: start + slice.length < bytes.length,
|
|
177
|
+
start,
|
|
178
|
+
returnedBytes: slice.length,
|
|
179
|
+
content: slice.toString("utf8"),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
102
183
|
async function metadataFiles(root) {
|
|
103
184
|
const base = evidenceStoreRoot(root)
|
|
104
185
|
const prefixes = await readdir(base, { withFileTypes: true }).catch(() => [])
|
|
@@ -175,4 +256,4 @@ export async function evidenceExists(root, ref) {
|
|
|
175
256
|
} catch {
|
|
176
257
|
return false
|
|
177
258
|
}
|
|
178
|
-
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const BEHAVIORAL_TEST_COMMAND = [
|
|
2
|
+
/(?:^|\s)(?:node|bun)\s+--test(?:\s|$)/i,
|
|
3
|
+
/(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test(?:\s|$|:)/i,
|
|
4
|
+
/(?:^|\s)(?:npx\s+)?(?:jest|vitest|pytest)(?:\s|$)/i,
|
|
5
|
+
/(?:^|\s)go\s+test(?:\s|$)/i,
|
|
6
|
+
/(?:^|\s)cargo\s+test(?:\s|$)/i,
|
|
7
|
+
/(?:^|\s)dotnet\s+test(?:\s|$)/i,
|
|
8
|
+
/(?:^|\s)(?:mvn|mvnw|gradle|gradlew|\.\/gradlew|gradlew\.bat)\b[^\n]*\btest\b/i,
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
function commandText(row = {}) {
|
|
12
|
+
const receipt = row?.receipt || {}
|
|
13
|
+
return [receipt.command, ...(Array.isArray(receipt.args) ? receipt.args : [])].filter(Boolean).join(" ").trim()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function finishedAtMs(row = {}) {
|
|
17
|
+
const value = Date.parse(row?.finishedAt || row?.receipt?.finishedAt || "")
|
|
18
|
+
return Number.isFinite(value) ? value : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isBehavioralVerificationReceipt(row = {}) {
|
|
22
|
+
const receipt = row?.receipt || {}
|
|
23
|
+
if (receipt.passed !== true || Number(receipt.exitCode) !== 0) return false
|
|
24
|
+
const command = commandText(row)
|
|
25
|
+
return Boolean(command) && BEHAVIORAL_TEST_COMMAND.some((pattern) => pattern.test(command))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function evaluateFastVerificationGate(input = {}) {
|
|
29
|
+
const policy = input.policy || {}
|
|
30
|
+
const implementation = input.implementation || {}
|
|
31
|
+
const receipts = Array.isArray(input.receipts) ? input.receipts : []
|
|
32
|
+
const attemptStartedAtMs = Number(input.attemptStartedAtMs || 0)
|
|
33
|
+
const eligible =
|
|
34
|
+
policy.executionProfile === "fast" &&
|
|
35
|
+
policy.singleFileBounded === true &&
|
|
36
|
+
policy.risk === "low" &&
|
|
37
|
+
input.visualRequired !== true
|
|
38
|
+
const implementationOk =
|
|
39
|
+
Number(implementation.exitCode) === 0 &&
|
|
40
|
+
String(implementation.stopReason || "").toLowerCase() !== "error"
|
|
41
|
+
const verificationText = String(implementation?.report?.sections?.verification || "").trim()
|
|
42
|
+
const implementationReportedVerification =
|
|
43
|
+
Boolean(verificationText) &&
|
|
44
|
+
!/(?:not\s+run|not\s+available|skipped|unverified|no\s+(?:tests?|checks?)\s+(?:run|available))/i.test(verificationText)
|
|
45
|
+
const behavioralReceipts = receipts
|
|
46
|
+
.filter((row) => {
|
|
47
|
+
const finished = finishedAtMs(row)
|
|
48
|
+
return isBehavioralVerificationReceipt(row) &&
|
|
49
|
+
(attemptStartedAtMs <= 0 || (finished != null && finished >= attemptStartedAtMs))
|
|
50
|
+
})
|
|
51
|
+
.map((row) => ({
|
|
52
|
+
command: commandText(row),
|
|
53
|
+
finishedAt: row?.finishedAt || row?.receipt?.finishedAt || null,
|
|
54
|
+
receiptId: row?.receipt?.id || null,
|
|
55
|
+
}))
|
|
56
|
+
let reason = "fresh-behavioral-evidence"
|
|
57
|
+
if (!eligible) reason = "not-fast-bounded"
|
|
58
|
+
else if (!implementationOk) reason = "implementation-not-clean"
|
|
59
|
+
else if (!implementationReportedVerification) reason = "implementation-did-not-report-verification"
|
|
60
|
+
else if (!behavioralReceipts.length) reason = "no-fresh-behavioral-receipt"
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
eligible,
|
|
64
|
+
passed: eligible && implementationOk && implementationReportedVerification && behavioralReceipts.length > 0,
|
|
65
|
+
reason,
|
|
66
|
+
behavioralReceiptCount: behavioralReceipts.length,
|
|
67
|
+
behavioralReceipts,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { buildSemanticIndex } from "./semantic-index.mjs"
|
|
3
|
+
|
|
4
|
+
const DEFAULT_L0 = 256
|
|
5
|
+
const DEFAULT_L1 = 4000
|
|
6
|
+
const STOP = new Set([
|
|
7
|
+
"this","that","with","from","into","then","than","when","where","what","your","have","will",
|
|
8
|
+
"task","code","file","files","fix","update","change","changes","được","các","cho","với","trong","này","một","những","không","theo",
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
function terms(value) {
|
|
12
|
+
return [...new Set(
|
|
13
|
+
String(value || "")
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.split(/[^\p{L}\p{N}_$.-]+/u)
|
|
16
|
+
.map((item) => item.replace(/^[-.$]+|[-.$]+$/g, ""))
|
|
17
|
+
.filter((item) => item.length >= 2 && !STOP.has(item)),
|
|
18
|
+
)].slice(0, 32)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ancestors(file) {
|
|
22
|
+
const parts = String(file || "").replaceAll("\\", "/").split("/").filter(Boolean)
|
|
23
|
+
const result = ["."]
|
|
24
|
+
let current = ""
|
|
25
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
26
|
+
current = current ? `${current}/${parts[index]}` : parts[index]
|
|
27
|
+
result.push(current)
|
|
28
|
+
}
|
|
29
|
+
return result
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parentDir(dir) {
|
|
33
|
+
if (!dir || dir === ".") return null
|
|
34
|
+
const value = path.posix.dirname(dir)
|
|
35
|
+
return value === "." ? "." : value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clip(value, limit) {
|
|
39
|
+
const text = String(value || "")
|
|
40
|
+
return text.length <= limit ? text : text.slice(0, Math.max(0, limit - 15)) + "...[truncated]"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function topCounts(map, limit) {
|
|
44
|
+
return [...map.entries()]
|
|
45
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
46
|
+
.slice(0, limit)
|
|
47
|
+
.map(([name, count]) => ({ name, count }))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildNodeSummary(node, children, options = {}) {
|
|
51
|
+
const l0Limit = Math.max(96, Number(options.l0Chars || DEFAULT_L0))
|
|
52
|
+
const l1Limit = Math.max(512, Number(options.l1Chars || DEFAULT_L1))
|
|
53
|
+
const symbols = topCounts(node.symbols, 18)
|
|
54
|
+
const extensions = topCounts(node.extensions, 10)
|
|
55
|
+
const files = [...node.files].sort()
|
|
56
|
+
const childNames = (children.get(node.path) || []).sort()
|
|
57
|
+
const l0 = clip([
|
|
58
|
+
`${node.path}: ${files.length} source file(s), ${node.bytes} bytes.`,
|
|
59
|
+
symbols.length ? `Key symbols: ${symbols.slice(0, 8).map((item) => item.name).join(", ")}.` : "",
|
|
60
|
+
childNames.length ? `Child areas: ${childNames.slice(0, 6).join(", ")}.` : "",
|
|
61
|
+
].filter(Boolean).join(" "), l0Limit)
|
|
62
|
+
const l1 = clip([
|
|
63
|
+
l0,
|
|
64
|
+
childNames.length ? `Children: ${childNames.join(", ")}` : "",
|
|
65
|
+
extensions.length ? `Extensions: ${extensions.map((item) => `${item.name || "(none)"}:${item.count}`).join(", ")}` : "",
|
|
66
|
+
symbols.length ? `Symbols: ${symbols.map((item) => `${item.name}:${item.count}`).join(", ")}` : "",
|
|
67
|
+
files.length ? `Files: ${files.slice(0, 60).join(", ")}` : "",
|
|
68
|
+
].filter(Boolean).join("\n"), l1Limit)
|
|
69
|
+
return {
|
|
70
|
+
path: node.path,
|
|
71
|
+
fileCount: files.length,
|
|
72
|
+
bytes: node.bytes,
|
|
73
|
+
files,
|
|
74
|
+
symbols,
|
|
75
|
+
children: childNames,
|
|
76
|
+
l0,
|
|
77
|
+
l1,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function buildContextHierarchy(root = process.cwd(), options = {}) {
|
|
82
|
+
root = path.resolve(root)
|
|
83
|
+
const built = options.builtIndex || await buildSemanticIndex(root, {
|
|
84
|
+
maxFiles: options.maxFiles ?? 6000,
|
|
85
|
+
maxDepth: options.maxDepth ?? 14,
|
|
86
|
+
})
|
|
87
|
+
const nodes = new Map()
|
|
88
|
+
const children = new Map()
|
|
89
|
+
const ensure = (dir) => {
|
|
90
|
+
if (!nodes.has(dir)) {
|
|
91
|
+
nodes.set(dir, { path: dir, files: new Set(), bytes: 0, symbols: new Map(), extensions: new Map() })
|
|
92
|
+
}
|
|
93
|
+
return nodes.get(dir)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ensure(".")
|
|
97
|
+
for (const [file, entry] of Object.entries(built.index.files || {})) {
|
|
98
|
+
const dirs = ancestors(file)
|
|
99
|
+
const extension = path.posix.extname(file).toLowerCase()
|
|
100
|
+
for (const dir of dirs) {
|
|
101
|
+
const node = ensure(dir)
|
|
102
|
+
node.files.add(file)
|
|
103
|
+
node.bytes += Number(entry.bytes || 0)
|
|
104
|
+
node.extensions.set(extension, (node.extensions.get(extension) || 0) + 1)
|
|
105
|
+
for (const symbol of entry.symbols || []) {
|
|
106
|
+
const name = String(symbol.name || "")
|
|
107
|
+
if (!name) continue
|
|
108
|
+
node.symbols.set(name, (node.symbols.get(name) || 0) + 1)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const dir of nodes.keys()) {
|
|
114
|
+
const parent = parentDir(dir)
|
|
115
|
+
if (!parent) continue
|
|
116
|
+
const list = children.get(parent) || []
|
|
117
|
+
if (!list.includes(dir)) list.push(dir)
|
|
118
|
+
children.set(parent, list)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const summaries = [...nodes.values()]
|
|
122
|
+
.map((node) => buildNodeSummary(node, children, options))
|
|
123
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
kind: "ues-context-hierarchy",
|
|
128
|
+
root,
|
|
129
|
+
generatedAt: new Date().toISOString(),
|
|
130
|
+
levels: {
|
|
131
|
+
L0: { maxChars: Math.max(96, Number(options.l0Chars || DEFAULT_L0)), purpose: "routing abstract" },
|
|
132
|
+
L1: { maxChars: Math.max(512, Number(options.l1Chars || DEFAULT_L1)), purpose: "scope overview" },
|
|
133
|
+
L2: { purpose: "source excerpts loaded on demand by the context manifest" },
|
|
134
|
+
},
|
|
135
|
+
nodes: summaries,
|
|
136
|
+
stats: built.stats,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function containsFile(scope, file) {
|
|
141
|
+
if (scope === ".") return true
|
|
142
|
+
return file === scope || file.startsWith(scope + "/")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function scopesOverlap(left, right) {
|
|
146
|
+
if (left === "." || right === ".") return true
|
|
147
|
+
return left === right || left.startsWith(right + "/") || right.startsWith(left + "/")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function selectDiverseHierarchyScopes(rows = [], maxScopes = 6) {
|
|
151
|
+
const limit = Math.max(1, Math.min(Number(maxScopes || 6), 20))
|
|
152
|
+
const selected = []
|
|
153
|
+
for (const row of rows) {
|
|
154
|
+
if (selected.length >= limit) break
|
|
155
|
+
if (selected.some((item) => scopesOverlap(item.path, row.path))) continue
|
|
156
|
+
selected.push(row)
|
|
157
|
+
}
|
|
158
|
+
if (!selected.length && rows.length) selected.push(rows[0])
|
|
159
|
+
return selected
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function queryContextHierarchy(root, query, options = {}) {
|
|
163
|
+
const hierarchy = await buildContextHierarchy(root, options)
|
|
164
|
+
const wanted = terms(query)
|
|
165
|
+
const declared = (options.declaredFiles || []).map((file) => String(file || "").replaceAll("\\", "/")).filter(Boolean)
|
|
166
|
+
const scored = hierarchy.nodes.map((node) => {
|
|
167
|
+
const lowerPath = node.path.toLowerCase()
|
|
168
|
+
const symbolText = node.symbols.map((item) => item.name.toLowerCase()).join(" ")
|
|
169
|
+
const fileText = node.files.slice(0, 100).join(" ").toLowerCase()
|
|
170
|
+
let score = node.path === "." ? 0.25 : 0
|
|
171
|
+
const reasons = []
|
|
172
|
+
for (const term of wanted) {
|
|
173
|
+
if (lowerPath.includes(term)) {
|
|
174
|
+
score += 8
|
|
175
|
+
reasons.push("path:" + term)
|
|
176
|
+
}
|
|
177
|
+
if (symbolText.includes(term)) {
|
|
178
|
+
score += 5
|
|
179
|
+
reasons.push("symbol:" + term)
|
|
180
|
+
}
|
|
181
|
+
if (fileText.includes(term)) {
|
|
182
|
+
score += 2
|
|
183
|
+
reasons.push("file:" + term)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const declaredHits = declared.filter((file) => containsFile(node.path, file)).length
|
|
187
|
+
if (declaredHits) {
|
|
188
|
+
score += 20 + declaredHits * 4
|
|
189
|
+
reasons.push("declared:" + declaredHits)
|
|
190
|
+
}
|
|
191
|
+
const depth = node.path === "." ? 0 : node.path.split("/").length
|
|
192
|
+
if (score > 0) score += Math.min(3, depth * 0.4)
|
|
193
|
+
score -= Math.min(2.5, Math.log1p(node.fileCount) * 0.15)
|
|
194
|
+
return { ...node, score: Number(score.toFixed(3)), reasons: [...new Set(reasons)].slice(0, 12) }
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const maxScopes = Math.max(1, Math.min(Number(options.maxScopes || 6), 20))
|
|
198
|
+
const positive = scored
|
|
199
|
+
.filter((item) => item.score > 0)
|
|
200
|
+
.sort((a, b) => b.score - a.score || a.fileCount - b.fileCount || a.path.localeCompare(b.path))
|
|
201
|
+
const scopes = selectDiverseHierarchyScopes(
|
|
202
|
+
positive.length ? positive : scored.filter((item) => item.path === "."),
|
|
203
|
+
maxScopes,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
schemaVersion: 1,
|
|
208
|
+
kind: "ues-context-hierarchy-query",
|
|
209
|
+
query: String(query || ""),
|
|
210
|
+
terms: wanted,
|
|
211
|
+
levels: hierarchy.levels,
|
|
212
|
+
scopes,
|
|
213
|
+
stats: hierarchy.stats,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const TRANSIENT_ERROR =
|
|
2
|
+
/(timeout|timed out|econnreset|econnrefused|socket|connection (?:closed|reset|lost)|transport|network|temporar(?:y|ily)|service unavailable|bad gateway|gateway timeout|\b502\b|\b503\b|\b504\b|rate limit|\b429\b)/i
|
|
3
|
+
|
|
4
|
+
function nowMs(value) {
|
|
5
|
+
const number = Number(value)
|
|
6
|
+
return Number.isFinite(number) ? number : Date.now()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isTransientMcpFailure(value = "") {
|
|
10
|
+
return TRANSIENT_ERROR.test(String(value || ""))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function mcpReconnectAdvice(input = {}) {
|
|
14
|
+
const policy = input.policy || {}
|
|
15
|
+
const attempt = Math.max(1, Math.trunc(Number(input.attempt || 1)))
|
|
16
|
+
const transient = isTransientMcpFailure(input.error || input.message || "")
|
|
17
|
+
const destructive = policy.confirmationRequired === true || policy.annotations?.destructiveHint === true
|
|
18
|
+
const retryable = policy.retryAllowed === true && transient && !destructive && attempt < Math.max(1, Number(input.maxAttempts || 3))
|
|
19
|
+
return {
|
|
20
|
+
schemaVersion: 1,
|
|
21
|
+
retryable,
|
|
22
|
+
reconnectRecommended: retryable,
|
|
23
|
+
reason: destructive
|
|
24
|
+
? "destructive-tool-never-auto-reconnect"
|
|
25
|
+
: !transient
|
|
26
|
+
? "non-transient-failure"
|
|
27
|
+
: policy.retryAllowed !== true
|
|
28
|
+
? "tool-not-explicitly-idempotent"
|
|
29
|
+
: retryable
|
|
30
|
+
? "transient-idempotent-failure"
|
|
31
|
+
: "retry-budget-exhausted",
|
|
32
|
+
backoffMs: retryable ? Math.min(5_000, 250 * (2 ** Math.max(0, attempt - 1))) : 0,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class McpHealthTracker {
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.failureThreshold = Math.max(1, Math.min(10, Number(options.failureThreshold || 2)))
|
|
39
|
+
this.cooldownMs = Math.max(1_000, Math.min(5 * 60_000, Number(options.cooldownMs || 15_000)))
|
|
40
|
+
this.maxEntries = Math.max(16, Math.min(2_000, Number(options.maxEntries || 256)))
|
|
41
|
+
this.state = new Map()
|
|
42
|
+
this.active = new Map()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear() {
|
|
46
|
+
this.state.clear()
|
|
47
|
+
this.active.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
begin(callId, toolName, policy = {}, at = Date.now()) {
|
|
51
|
+
const id = String(callId || "")
|
|
52
|
+
const name = String(toolName || "")
|
|
53
|
+
if (id && name) this.active.set(id, { name, policy, startedAt: nowMs(at) })
|
|
54
|
+
this.#trim()
|
|
55
|
+
return this.status(name, at)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
finish(callId, result = {}, at = Date.now()) {
|
|
59
|
+
const id = String(callId || "")
|
|
60
|
+
const active = this.active.get(id)
|
|
61
|
+
if (!active) return null
|
|
62
|
+
this.active.delete(id)
|
|
63
|
+
const timestamp = nowMs(at)
|
|
64
|
+
const previous = this.state.get(active.name) || {
|
|
65
|
+
successes: 0,
|
|
66
|
+
failures: 0,
|
|
67
|
+
consecutiveFailures: 0,
|
|
68
|
+
cooldownUntil: 0,
|
|
69
|
+
lastError: null,
|
|
70
|
+
lastFinishedAt: 0,
|
|
71
|
+
}
|
|
72
|
+
const errorText = [
|
|
73
|
+
result.error,
|
|
74
|
+
result.message,
|
|
75
|
+
result.stderr,
|
|
76
|
+
result.text,
|
|
77
|
+
].filter(Boolean).join("\n").slice(0, 4000)
|
|
78
|
+
const failed = result.isError === true || Number(result.exitCode) > 0
|
|
79
|
+
const transient = failed && isTransientMcpFailure(errorText)
|
|
80
|
+
const consecutiveFailures = failed ? Number(previous.consecutiveFailures || 0) + 1 : 0
|
|
81
|
+
const cooldownUntil = transient && consecutiveFailures >= this.failureThreshold
|
|
82
|
+
? timestamp + this.cooldownMs
|
|
83
|
+
: failed
|
|
84
|
+
? Number(previous.cooldownUntil || 0)
|
|
85
|
+
: 0
|
|
86
|
+
const reconnectAdvice = failed
|
|
87
|
+
? mcpReconnectAdvice({ policy: active.policy, error: errorText, attempt: consecutiveFailures })
|
|
88
|
+
: null
|
|
89
|
+
const next = {
|
|
90
|
+
successes: Number(previous.successes || 0) + (failed ? 0 : 1),
|
|
91
|
+
failures: Number(previous.failures || 0) + (failed ? 1 : 0),
|
|
92
|
+
consecutiveFailures,
|
|
93
|
+
cooldownUntil,
|
|
94
|
+
lastError: failed ? errorText || "tool-result-error" : null,
|
|
95
|
+
lastTransientFailure: transient,
|
|
96
|
+
reconnectAdvice,
|
|
97
|
+
lastFinishedAt: timestamp,
|
|
98
|
+
policy: active.policy,
|
|
99
|
+
}
|
|
100
|
+
this.state.set(active.name, next)
|
|
101
|
+
this.#trim()
|
|
102
|
+
return this.status(active.name, timestamp)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
status(toolName, at = Date.now()) {
|
|
106
|
+
const name = String(toolName || "")
|
|
107
|
+
const row = this.state.get(name)
|
|
108
|
+
const timestamp = nowMs(at)
|
|
109
|
+
if (!row) {
|
|
110
|
+
return { schemaVersion: 1, tool: name, status: "unknown", available: true, consecutiveFailures: 0, cooldownUntil: null }
|
|
111
|
+
}
|
|
112
|
+
const coolingDown = Number(row.cooldownUntil || 0) > timestamp
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
tool: name,
|
|
116
|
+
status: coolingDown ? "degraded" : row.consecutiveFailures > 0 ? "recovering" : "healthy",
|
|
117
|
+
available: !coolingDown,
|
|
118
|
+
successes: row.successes,
|
|
119
|
+
failures: row.failures,
|
|
120
|
+
consecutiveFailures: row.consecutiveFailures,
|
|
121
|
+
cooldownUntil: coolingDown ? new Date(row.cooldownUntil).toISOString() : null,
|
|
122
|
+
lastError: row.lastError,
|
|
123
|
+
lastTransientFailure: row.lastTransientFailure === true,
|
|
124
|
+
reconnectAdvice: row.reconnectAdvice || null,
|
|
125
|
+
lastFinishedAt: row.lastFinishedAt ? new Date(row.lastFinishedAt).toISOString() : null,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
available(toolName, at = Date.now()) {
|
|
130
|
+
return this.status(toolName, at).available !== false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
snapshot(at = Date.now()) {
|
|
134
|
+
return [...this.state.keys()]
|
|
135
|
+
.sort()
|
|
136
|
+
.map((name) => this.status(name, at))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#trim() {
|
|
140
|
+
if (this.state.size <= this.maxEntries) return
|
|
141
|
+
const ordered = [...this.state.entries()].sort((a, b) => Number(a[1]?.lastFinishedAt || 0) - Number(b[1]?.lastFinishedAt || 0))
|
|
142
|
+
for (const [name] of ordered.slice(0, Math.max(0, this.state.size - this.maxEntries))) this.state.delete(name)
|
|
143
|
+
}
|
|
144
|
+
}
|