opencode-agent-skill 15.0.0 → 15.1.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/README.md +5 -5
- package/docs/V15-MANAGED-RUNTIME.md +18 -0
- package/global-config/agents/verifier.md +6 -2
- package/lib/affected-tests.mjs +58 -8
- package/lib/capability-fabric.mjs +107 -48
- package/lib/code-intelligence/index.mjs +4 -15
- package/lib/code-intelligence/lsp-pool.mjs +694 -0
- package/lib/code-intelligence/lsp-provider.mjs +481 -67
- package/lib/compaction-resume-guard.mjs +327 -0
- package/lib/completion-auditor.mjs +20 -0
- package/lib/context-engine-v11.mjs +20 -7
- package/lib/context-manifest.mjs +38 -21
- package/lib/decision-policy.mjs +40 -2
- package/lib/evidence-store.mjs +121 -31
- package/lib/executable-probe.mjs +69 -0
- package/lib/execution-contract.mjs +29 -16
- package/lib/hierarchical-context.mjs +31 -3
- package/lib/learning-engine.mjs +164 -65
- package/lib/memory-engine.mjs +348 -98
- package/lib/model-config.mjs +86 -7
- package/lib/performance-fabric.mjs +354 -107
- package/lib/permission-policy.mjs +232 -0
- package/lib/pi-rpc-pool.mjs +170 -5
- package/lib/provider-recovery.mjs +51 -0
- package/lib/repo-graph.mjs +38 -5
- package/lib/runtime-events.mjs +31 -10
- package/lib/semantic-index.mjs +67 -20
- package/lib/skill-compiler.mjs +84 -18
- package/lib/subagent-artifacts.mjs +182 -0
- package/lib/task-policy.mjs +80 -7
- package/lib/trajectory.mjs +25 -1
- package/lib/untrusted-output.mjs +106 -0
- package/lib/verification-broker.mjs +32 -27
- package/lib/workspace-fingerprint.mjs +43 -8
- package/lib/workspace-hygiene.mjs +481 -0
- package/package.json +8 -6
- package/pi/extensions/ues-child-runtime.ts +103 -5
- package/pi/extensions/ues.ts +984 -95
- package/scripts/benchmark-lsp-pool.mjs +128 -0
- package/scripts/check-release-consistency.mjs +16 -4
- package/scripts/check-source-integrity.mjs +60 -1
- package/scripts/run-test-suite.mjs +143 -0
- package/scripts/smoke-package-closure.mjs +4 -0
package/lib/memory-engine.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto"
|
|
2
2
|
import { existsSync } from "node:fs"
|
|
3
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|
3
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
|
|
4
4
|
import { spawnSync } from "node:child_process"
|
|
5
5
|
import path from "node:path"
|
|
6
6
|
import { evidenceExists, putEvidence } from "./evidence-store.mjs"
|
|
7
|
+
import { buildRepoGraphCached } from "./repo-graph.mjs"
|
|
7
8
|
|
|
8
9
|
const MEMORY_DIR = ".ues-memory"
|
|
9
10
|
const MEMORY_FILE = "MEMORY.json"
|
|
@@ -15,6 +16,72 @@ const STOP = new Set([
|
|
|
15
16
|
"file","files","change","changes","được","các","cho","với","trong","này","một","những","không","theo",
|
|
16
17
|
])
|
|
17
18
|
|
|
19
|
+
const MEMORY_WRITE_TAILS = new Map()
|
|
20
|
+
const MEMORY_LOCK_STALE_MS = 15_000
|
|
21
|
+
const MEMORY_LOCK_WAIT_MS = 20_000
|
|
22
|
+
|
|
23
|
+
function sleep(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function memoryLockPath(root) {
|
|
28
|
+
return path.join(path.resolve(root), MEMORY_DIR, ".memory-write.lock")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function withCrossProcessMemoryLock(root, fn) {
|
|
32
|
+
const lockDir = memoryLockPath(root)
|
|
33
|
+
await mkdir(path.dirname(lockDir), { recursive: true })
|
|
34
|
+
const deadline = Date.now() + MEMORY_LOCK_WAIT_MS
|
|
35
|
+
let delayMs = 8
|
|
36
|
+
|
|
37
|
+
while (true) {
|
|
38
|
+
try {
|
|
39
|
+
await mkdir(lockDir)
|
|
40
|
+
break
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error?.code !== "EEXIST") throw error
|
|
43
|
+
const info = await stat(lockDir).catch(() => null)
|
|
44
|
+
if (info && Date.now() - info.mtimeMs > MEMORY_LOCK_STALE_MS) {
|
|
45
|
+
const confirmed = await stat(lockDir).catch(() => null)
|
|
46
|
+
if (
|
|
47
|
+
confirmed &&
|
|
48
|
+
confirmed.ino === info.ino &&
|
|
49
|
+
confirmed.mtimeMs === info.mtimeMs
|
|
50
|
+
) {
|
|
51
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (Date.now() >= deadline) throw new Error("Timed out waiting for memory write lock")
|
|
56
|
+
await sleep(delayMs)
|
|
57
|
+
delayMs = Math.min(100, delayMs * 2)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await fn()
|
|
63
|
+
} finally {
|
|
64
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function withMemoryWriteLock(root, fn) {
|
|
69
|
+
const key = path.resolve(root)
|
|
70
|
+
const previous = MEMORY_WRITE_TAILS.get(key) || Promise.resolve()
|
|
71
|
+
let release
|
|
72
|
+
const barrier = new Promise((resolve) => { release = resolve })
|
|
73
|
+
const tail = previous.catch(() => {}).then(() => barrier)
|
|
74
|
+
MEMORY_WRITE_TAILS.set(key, tail)
|
|
75
|
+
|
|
76
|
+
await previous.catch(() => {})
|
|
77
|
+
try {
|
|
78
|
+
return await withCrossProcessMemoryLock(root, fn)
|
|
79
|
+
} finally {
|
|
80
|
+
release()
|
|
81
|
+
if (MEMORY_WRITE_TAILS.get(key) === tail) MEMORY_WRITE_TAILS.delete(key)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
18
85
|
function now() {
|
|
19
86
|
return new Date().toISOString()
|
|
20
87
|
}
|
|
@@ -44,15 +111,35 @@ function normalizedContent(value) {
|
|
|
44
111
|
return String(value || "").trim().replace(/\s+/g, " ")
|
|
45
112
|
}
|
|
46
113
|
|
|
114
|
+
function scopedIdentityFiles(scope, files = []) {
|
|
115
|
+
if (!["file", "module"].includes(String(scope || ""))) return []
|
|
116
|
+
return uniq((files || []).map(normalizeFile)).sort()
|
|
117
|
+
}
|
|
118
|
+
|
|
47
119
|
function idFor(input) {
|
|
120
|
+
const scope = input.scope || "project"
|
|
121
|
+
const scopedFiles = scopedIdentityFiles(scope, input.files)
|
|
48
122
|
const key = input.key || [
|
|
49
|
-
|
|
123
|
+
scope,
|
|
50
124
|
input.type || "episodic",
|
|
125
|
+
scopedFiles.length ? scopedFiles.join(",") : "",
|
|
51
126
|
normalizedContent(input.content),
|
|
52
127
|
].join(":")
|
|
53
128
|
return createHash("sha256").update(String(key)).digest("hex").slice(0, 20)
|
|
54
129
|
}
|
|
55
130
|
|
|
131
|
+
function sameMemoryIdentity(item, input) {
|
|
132
|
+
if (!item) return false
|
|
133
|
+
if (String(item.scope || "project") !== String(input.scope || "project")) return false
|
|
134
|
+
if (String(item.type || "episodic") !== String(input.type || "episodic")) return false
|
|
135
|
+
if (normalizedContent(item.content) !== normalizedContent(input.content)) return false
|
|
136
|
+
const expectedFiles = scopedIdentityFiles(input.scope, input.files)
|
|
137
|
+
if (!expectedFiles.length) return true
|
|
138
|
+
const actualFiles = scopedIdentityFiles(item.scope, item.files)
|
|
139
|
+
return actualFiles.length === expectedFiles.length &&
|
|
140
|
+
actualFiles.every((file, index) => file === expectedFiles[index])
|
|
141
|
+
}
|
|
142
|
+
|
|
56
143
|
function memoryPath(root) {
|
|
57
144
|
return path.join(path.resolve(root), MEMORY_DIR, MEMORY_FILE)
|
|
58
145
|
}
|
|
@@ -97,52 +184,58 @@ export async function proposeMemory(root, input = {}) {
|
|
|
97
184
|
if (!content) throw new Error("memory content is required")
|
|
98
185
|
const type = VALID_TYPES.has(input.type) ? input.type : "episodic"
|
|
99
186
|
const scope = VALID_SCOPES.has(input.scope) ? input.scope : "project"
|
|
100
|
-
const state = await readMemoryState(root)
|
|
101
187
|
const id = idFor({ ...input, type, scope, content })
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
188
|
+
|
|
189
|
+
return withMemoryWriteLock(root, async () => {
|
|
190
|
+
const state = await readMemoryState(root)
|
|
191
|
+
const timestamp = now()
|
|
192
|
+
const existing = state.memories.find((item) => item.id === id) ||
|
|
193
|
+
state.memories.find((item) => sameMemoryIdentity(item, { ...input, type, scope, content }))
|
|
194
|
+
if (existing) {
|
|
195
|
+
const reinforced = {
|
|
196
|
+
...existing,
|
|
197
|
+
lastSeenAt: timestamp,
|
|
198
|
+
reinforcementCount: Number(existing.reinforcementCount || 1) + 1,
|
|
199
|
+
evidenceRefs: uniq([...(existing.evidenceRefs || []), ...(input.evidenceRefs || [])]),
|
|
200
|
+
files: uniq([...(existing.files || []), ...(input.files || []).map(normalizeFile)]),
|
|
201
|
+
dependencies: uniq([...(existing.dependencies || []), ...(input.dependencies || []).map(normalizeFile)]),
|
|
202
|
+
tags: uniq([...(existing.tags || []), ...(input.tags || [])]),
|
|
203
|
+
confidence: Math.max(clamp(existing.confidence), clamp(input.confidence, existing.confidence ?? 0.7)),
|
|
204
|
+
taskClass: input.taskClass || existing.taskClass || null,
|
|
205
|
+
expiresAt: input.expiresAt || existing.expiresAt || null,
|
|
206
|
+
}
|
|
207
|
+
await writeMemoryState(root, state.memories.map((item) => item.id === existing.id ? reinforced : item))
|
|
208
|
+
return reinforced
|
|
115
209
|
}
|
|
116
|
-
await writeMemoryState(root, state.memories.map((item) => item.id === id ? reinforced : item))
|
|
117
|
-
return reinforced
|
|
118
|
-
}
|
|
119
210
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
211
|
+
const memory = {
|
|
212
|
+
id,
|
|
213
|
+
key: input.key || null,
|
|
214
|
+
type,
|
|
215
|
+
scope,
|
|
216
|
+
content,
|
|
217
|
+
status: "candidate",
|
|
218
|
+
confidence: clamp(input.confidence),
|
|
219
|
+
evidenceRefs: uniq(input.evidenceRefs || []),
|
|
220
|
+
files: uniq((input.files || []).map(normalizeFile)),
|
|
221
|
+
dependencies: uniq((input.dependencies || []).map(normalizeFile)),
|
|
222
|
+
tags: uniq(input.tags || []),
|
|
223
|
+
sourceTask: input.sourceTask || null,
|
|
224
|
+
sourceCommit: input.sourceCommit || null,
|
|
225
|
+
taskClass: input.taskClass || null,
|
|
226
|
+
expiresAt: input.expiresAt || null,
|
|
227
|
+
createdAt: timestamp,
|
|
228
|
+
lastSeenAt: timestamp,
|
|
229
|
+
lastUsedAt: null,
|
|
230
|
+
useCount: 0,
|
|
231
|
+
reinforcementCount: 1,
|
|
232
|
+
verifiedAt: null,
|
|
233
|
+
verifier: null,
|
|
234
|
+
supersededBy: null,
|
|
235
|
+
}
|
|
236
|
+
await writeMemoryState(root, [...state.memories, memory])
|
|
237
|
+
return memory
|
|
238
|
+
})
|
|
146
239
|
}
|
|
147
240
|
|
|
148
241
|
export async function verifyMemory(root, id, verification = {}) {
|
|
@@ -150,47 +243,52 @@ export async function verifyMemory(root, id, verification = {}) {
|
|
|
150
243
|
throw new Error("memory verification requires verdict PASS")
|
|
151
244
|
}
|
|
152
245
|
if (!verification.verifier) throw new Error("memory verification requires an independent verifier identity")
|
|
153
|
-
const state = await readMemoryState(root)
|
|
154
|
-
const current = state.memories.find((item) => item.id === id)
|
|
155
|
-
if (!current) throw new Error("unknown memory: " + id)
|
|
156
|
-
if (current.status === "superseded") throw new Error("cannot verify a superseded memory")
|
|
157
|
-
|
|
158
|
-
const refs = uniq([...(current.evidenceRefs || []), ...(verification.evidenceRefs || [])])
|
|
159
|
-
if (!refs.length) throw new Error("memory verification requires at least one durable evidence reference")
|
|
160
|
-
for (const ref of refs) {
|
|
161
|
-
if (!(await evidenceExists(root, ref))) throw new Error("memory evidence reference is missing: " + ref)
|
|
162
|
-
}
|
|
163
246
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
247
|
+
return withMemoryWriteLock(root, async () => {
|
|
248
|
+
const state = await readMemoryState(root)
|
|
249
|
+
const current = state.memories.find((item) => item.id === id)
|
|
250
|
+
if (!current) throw new Error("unknown memory: " + id)
|
|
251
|
+
if (current.status === "superseded") throw new Error("cannot verify a superseded memory")
|
|
252
|
+
|
|
253
|
+
const refs = uniq([...(current.evidenceRefs || []), ...(verification.evidenceRefs || [])])
|
|
254
|
+
if (!refs.length) throw new Error("memory verification requires at least one durable evidence reference")
|
|
255
|
+
for (const ref of refs) {
|
|
256
|
+
if (!(await evidenceExists(root, ref))) throw new Error("memory evidence reference is missing: " + ref)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const verified = {
|
|
260
|
+
...current,
|
|
261
|
+
status: "verified",
|
|
262
|
+
evidenceRefs: refs,
|
|
263
|
+
verifier: String(verification.verifier),
|
|
264
|
+
verifiedAt: now(),
|
|
265
|
+
lastSeenAt: now(),
|
|
266
|
+
confidence: Math.max(clamp(current.confidence), clamp(verification.confidence, current.confidence ?? 0.7)),
|
|
267
|
+
}
|
|
268
|
+
await writeMemoryState(root, state.memories.map((item) => item.id === id ? verified : item))
|
|
269
|
+
return verified
|
|
270
|
+
})
|
|
175
271
|
}
|
|
176
272
|
|
|
177
273
|
export async function supersedeMemory(root, id, replacementId) {
|
|
178
274
|
if (!replacementId || replacementId === id) throw new Error("replacement memory id must be different")
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
275
|
+
return withMemoryWriteLock(root, async () => {
|
|
276
|
+
const state = await readMemoryState(root)
|
|
277
|
+
const current = state.memories.find((item) => item.id === id)
|
|
278
|
+
const replacement = state.memories.find((item) => item.id === replacementId)
|
|
279
|
+
if (!current) throw new Error("unknown memory: " + id)
|
|
280
|
+
if (!replacement) throw new Error("unknown replacement memory: " + replacementId)
|
|
281
|
+
if (replacement.status !== "verified") throw new Error("replacement memory must be verified before supersession")
|
|
282
|
+
const timestamp = now()
|
|
283
|
+
const superseded = {
|
|
284
|
+
...current,
|
|
285
|
+
status: "superseded",
|
|
286
|
+
supersededBy: replacementId,
|
|
287
|
+
lastSeenAt: timestamp,
|
|
288
|
+
}
|
|
289
|
+
await writeMemoryState(root, state.memories.map((item) => item.id === id ? superseded : item))
|
|
290
|
+
return superseded
|
|
291
|
+
})
|
|
194
292
|
}
|
|
195
293
|
|
|
196
294
|
function tokenVector(value, dims = 96) {
|
|
@@ -312,6 +410,7 @@ export function buildMemorySnapshot(memories = []) {
|
|
|
312
410
|
content: normalizedContent(item.content),
|
|
313
411
|
confidence: clamp(item.confidence),
|
|
314
412
|
files: uniq((item.files || []).map(normalizeFile)).sort(),
|
|
413
|
+
dependencies: uniq((item.dependencies || []).map(normalizeFile)).sort(),
|
|
315
414
|
taskClass: item.taskClass || null,
|
|
316
415
|
verifiedAt: item.verifiedAt || null,
|
|
317
416
|
evidenceRefs: uniq(item.evidenceRefs || []).sort(),
|
|
@@ -330,6 +429,96 @@ export function buildMemorySnapshot(memories = []) {
|
|
|
330
429
|
}
|
|
331
430
|
}
|
|
332
431
|
|
|
432
|
+
function dependencyAdjacency(edges = []) {
|
|
433
|
+
const graph = new Map()
|
|
434
|
+
const connect = (left, right) => {
|
|
435
|
+
if (!left || !right) return
|
|
436
|
+
if (!graph.has(left)) graph.set(left, new Set())
|
|
437
|
+
graph.get(left).add(right)
|
|
438
|
+
}
|
|
439
|
+
for (const edge of edges || []) {
|
|
440
|
+
const from = normalizeFile(edge?.from)
|
|
441
|
+
const to = normalizeFile(edge?.to)
|
|
442
|
+
if (!from || !to) continue
|
|
443
|
+
// Retrieval uses dependency proximity, not ownership direction, so both
|
|
444
|
+
// importers and imported modules are useful neighbors.
|
|
445
|
+
connect(from, to)
|
|
446
|
+
connect(to, from)
|
|
447
|
+
}
|
|
448
|
+
return graph
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function dependencyGraphAffinity(queryFiles, memoryFiles, adjacency, maxDepth = 3) {
|
|
452
|
+
if (!queryFiles.length || !memoryFiles.length || !adjacency?.size) return 0
|
|
453
|
+
const targets = new Set(memoryFiles.map(normalizeFile))
|
|
454
|
+
let best = 0
|
|
455
|
+
for (const source of queryFiles.map(normalizeFile)) {
|
|
456
|
+
if (targets.has(source)) return 1
|
|
457
|
+
let frontier = new Set([source])
|
|
458
|
+
const seen = new Set(frontier)
|
|
459
|
+
for (let depth = 1; depth <= maxDepth && frontier.size; depth += 1) {
|
|
460
|
+
const next = new Set()
|
|
461
|
+
for (const current of frontier) {
|
|
462
|
+
for (const neighbor of adjacency.get(current) || []) {
|
|
463
|
+
if (targets.has(neighbor)) {
|
|
464
|
+
const score = depth === 1 ? 0.95 : depth === 2 ? 0.7 : 0.45
|
|
465
|
+
best = Math.max(best, score)
|
|
466
|
+
}
|
|
467
|
+
if (!seen.has(neighbor)) {
|
|
468
|
+
seen.add(neighbor)
|
|
469
|
+
next.add(neighbor)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
frontier = next
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return best
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function exactFileAffinity(queryFiles, memoryFiles) {
|
|
480
|
+
const wanted = new Set(queryFiles.map(normalizeFile))
|
|
481
|
+
return memoryFiles.some((file) => wanted.has(normalizeFile(file))) ? 1 : 0
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function scopeEligible(item, queryFiles, pathGraph) {
|
|
485
|
+
if (!queryFiles.length) return true
|
|
486
|
+
const memoryFiles = uniq((item.files || []).map(normalizeFile))
|
|
487
|
+
if (item.scope === "file") return exactFileAffinity(queryFiles, memoryFiles) === 1
|
|
488
|
+
if (item.scope === "module") return pathGraph >= 0.45
|
|
489
|
+
return true
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function memoryAgeDays(item) {
|
|
493
|
+
const timestamp = Date.parse(item.lastSeenAt || item.verifiedAt || item.createdAt || "")
|
|
494
|
+
if (!timestamp) return 0
|
|
495
|
+
return Math.max(0, (Date.now() - timestamp) / 86_400_000)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function memoryHalfLifeDays(item) {
|
|
499
|
+
const base = {
|
|
500
|
+
failure: 45,
|
|
501
|
+
episodic: 75,
|
|
502
|
+
decision: 120,
|
|
503
|
+
procedural: 180,
|
|
504
|
+
semantic: 240,
|
|
505
|
+
}[item.type] || 120
|
|
506
|
+
const reinforcement = Math.max(1, Number(item.reinforcementCount || 1))
|
|
507
|
+
return base * (1 + Math.log2(reinforcement) * 0.2)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function agingScore(item) {
|
|
511
|
+
const age = memoryAgeDays(item)
|
|
512
|
+
const halfLife = memoryHalfLifeDays(item)
|
|
513
|
+
return Math.pow(0.5, age / Math.max(1, halfLife))
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function reinforcementScore(item) {
|
|
517
|
+
const reinforcement = Math.max(0, Number(item.reinforcementCount || 0))
|
|
518
|
+
const use = Math.max(0, Number(item.useCount || 0))
|
|
519
|
+
return Math.min(1, (Math.log1p(reinforcement) + 0.55 * Math.log1p(use)) / 4)
|
|
520
|
+
}
|
|
521
|
+
|
|
333
522
|
export async function retrieveMemories(root, query, options = {}) {
|
|
334
523
|
const state = await readMemoryState(root)
|
|
335
524
|
const memories = state.memories.filter((item) => item.status === "verified" && !item.supersededBy && !isExpired(item))
|
|
@@ -337,60 +526,121 @@ export async function retrieveMemories(root, query, options = {}) {
|
|
|
337
526
|
const queryVector = tokenVector(query)
|
|
338
527
|
const queryFiles = uniq((options.files || []).map(normalizeFile))
|
|
339
528
|
const lexical = bm25Scores(memories, queryTokens)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
529
|
+
|
|
530
|
+
let repoGraph = null
|
|
531
|
+
if (queryFiles.length && memories.length && options.dependencyGraph !== false) {
|
|
532
|
+
repoGraph = await buildRepoGraphCached(root, {
|
|
533
|
+
maxFiles: Math.max(100, Math.min(6000, Number(options.maxGraphFiles || 2500))),
|
|
534
|
+
...(options.workspaceFingerprint ? { workspaceFingerprint: options.workspaceFingerprint } : {}),
|
|
535
|
+
}).catch(() => null)
|
|
536
|
+
}
|
|
537
|
+
const adjacency = dependencyAdjacency(repoGraph?.edges || [])
|
|
538
|
+
|
|
539
|
+
const rows = memories.map((item, index) => {
|
|
540
|
+
const memoryFiles = uniq((item.files || []).map(normalizeFile))
|
|
541
|
+
const pathGraph = graphAffinity(queryFiles, memoryFiles)
|
|
542
|
+
const dependency = dependencyGraphAffinity(
|
|
543
|
+
queryFiles,
|
|
544
|
+
uniq([...memoryFiles, ...(item.dependencies || []).map(normalizeFile)]),
|
|
545
|
+
adjacency,
|
|
546
|
+
)
|
|
547
|
+
const graph = Math.max(pathGraph, dependency)
|
|
548
|
+
const aging = agingScore(item)
|
|
549
|
+
const reinforcement = reinforcementScore(item)
|
|
550
|
+
return {
|
|
551
|
+
item,
|
|
552
|
+
lexical: lexical[index] || 0,
|
|
553
|
+
semantic: Math.max(0, cosine(queryVector, tokenVector([item.content, ...(item.tags || []), ...memoryFiles].join(" ")))),
|
|
554
|
+
graph,
|
|
555
|
+
pathGraph,
|
|
556
|
+
dependency,
|
|
557
|
+
taskClass: taskClassAffinity(options.taskClass, item.taskClass),
|
|
558
|
+
recency: recencyScore(item.lastUsedAt || item.lastSeenAt || item.verifiedAt || item.createdAt),
|
|
559
|
+
aging,
|
|
560
|
+
reinforcement,
|
|
561
|
+
confidence: clamp(item.confidence) * (0.65 + 0.35 * aging),
|
|
562
|
+
scope: 0,
|
|
563
|
+
scopeEligible: true,
|
|
564
|
+
}
|
|
565
|
+
})
|
|
566
|
+
for (const row of rows) {
|
|
567
|
+
row.scope = scopeAffinity(row.item.scope, row.pathGraph)
|
|
568
|
+
row.scopeEligible = options.strictScope === false
|
|
569
|
+
? true
|
|
570
|
+
: scopeEligible(row.item, queryFiles, row.pathGraph)
|
|
571
|
+
}
|
|
351
572
|
const ranks = {
|
|
352
573
|
lexical: rankMap(rows, "lexical"),
|
|
353
574
|
semantic: rankMap(rows, "semantic"),
|
|
354
575
|
graph: rankMap(rows, "graph"),
|
|
576
|
+
dependency: rankMap(rows, "dependency"),
|
|
355
577
|
taskClass: rankMap(rows, "taskClass"),
|
|
356
578
|
recency: rankMap(rows, "recency"),
|
|
579
|
+
aging: rankMap(rows, "aging"),
|
|
580
|
+
reinforcement: rankMap(rows, "reinforcement"),
|
|
357
581
|
}
|
|
358
582
|
for (const row of rows) {
|
|
359
583
|
let rrf = 0
|
|
360
|
-
for (const key of ["lexical", "semantic", "graph", "taskClass", "recency"]) {
|
|
584
|
+
for (const key of ["lexical", "semantic", "graph", "dependency", "taskClass", "recency", "aging", "reinforcement"]) {
|
|
361
585
|
const rank = ranks[key].get(row.item.id)
|
|
362
586
|
if (rank) rrf += 1 / (60 + rank)
|
|
363
587
|
}
|
|
364
|
-
row.score =
|
|
588
|
+
row.score =
|
|
589
|
+
rrf +
|
|
590
|
+
row.confidence * 0.01 +
|
|
591
|
+
row.graph * 0.01 +
|
|
592
|
+
row.dependency * 0.012 +
|
|
593
|
+
row.scope * 0.006 +
|
|
594
|
+
row.taskClass * 0.008 +
|
|
595
|
+
row.reinforcement * 0.006 +
|
|
596
|
+
row.aging * 0.004
|
|
365
597
|
}
|
|
366
598
|
const limit = Math.max(1, Math.min(Number(options.limit || 6), 30))
|
|
367
599
|
const results = rows
|
|
368
|
-
.filter((row) =>
|
|
600
|
+
.filter((row) =>
|
|
601
|
+
row.scopeEligible &&
|
|
602
|
+
(row.lexical > 0 || row.semantic > 0.05 || row.graph > 0 || row.dependency > 0)
|
|
603
|
+
)
|
|
369
604
|
.sort((a, b) => b.score - a.score || b.confidence - a.confidence || a.item.id.localeCompare(b.item.id))
|
|
370
605
|
.slice(0, limit)
|
|
371
606
|
.map((row) => ({
|
|
372
607
|
...row.item,
|
|
373
608
|
retrieval: {
|
|
609
|
+
version: 2,
|
|
374
610
|
score: Number(row.score.toFixed(8)),
|
|
375
611
|
lexical: Number(row.lexical.toFixed(6)),
|
|
376
612
|
semantic: Number(row.semantic.toFixed(6)),
|
|
377
613
|
graph: Number(row.graph.toFixed(6)),
|
|
614
|
+
pathGraph: Number(row.pathGraph.toFixed(6)),
|
|
615
|
+
dependency: Number(row.dependency.toFixed(6)),
|
|
378
616
|
taskClass: Number(row.taskClass.toFixed(6)),
|
|
379
617
|
scope: Number(row.scope.toFixed(6)),
|
|
380
618
|
recency: Number(row.recency.toFixed(6)),
|
|
619
|
+
aging: Number(row.aging.toFixed(6)),
|
|
620
|
+
reinforcement: Number(row.reinforcement.toFixed(6)),
|
|
621
|
+
effectiveConfidence: Number(row.confidence.toFixed(6)),
|
|
381
622
|
},
|
|
382
623
|
}))
|
|
383
624
|
if (options.touch === true && results.length) {
|
|
384
625
|
const used = new Set(results.map((item) => item.id))
|
|
385
626
|
const timestamp = now()
|
|
386
|
-
await
|
|
387
|
-
|
|
388
|
-
|
|
627
|
+
await withMemoryWriteLock(root, async () => {
|
|
628
|
+
const latest = await readMemoryState(root)
|
|
629
|
+
await writeMemoryState(root, latest.memories.map((item) => used.has(item.id)
|
|
630
|
+
? { ...item, lastUsedAt: timestamp, useCount: Number(item.useCount || 0) + 1 }
|
|
631
|
+
: item))
|
|
632
|
+
})
|
|
389
633
|
}
|
|
390
634
|
return {
|
|
391
635
|
schemaVersion: SCHEMA_VERSION,
|
|
636
|
+
retrievalVersion: 2,
|
|
392
637
|
query: String(query || ""),
|
|
393
638
|
eligible: memories.length,
|
|
639
|
+
dependencyGraph: repoGraph ? {
|
|
640
|
+
scannedFiles: repoGraph.scannedFiles || 0,
|
|
641
|
+
edgeCount: Array.isArray(repoGraph.edges) ? repoGraph.edges.length : 0,
|
|
642
|
+
truncated: repoGraph.truncated === true,
|
|
643
|
+
} : null,
|
|
394
644
|
results,
|
|
395
645
|
snapshot: buildMemorySnapshot(results),
|
|
396
646
|
}
|
package/lib/model-config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import { defaultModelPolicy, resolveModel } from "./model-policy.mjs"
|
|
5
5
|
import { normalizeCapabilityProfile } from "./capability-registry.mjs"
|
|
@@ -7,6 +7,70 @@ import { normalizePerformanceHistory, recordPerformanceOutcome } from "./model-p
|
|
|
7
7
|
|
|
8
8
|
const TIERS = new Set(["light", "standard", "heavy"])
|
|
9
9
|
|
|
10
|
+
const MODEL_POLICY_WRITE_TAILS = new Map()
|
|
11
|
+
const MODEL_POLICY_LOCK_STALE_MS = 15_000
|
|
12
|
+
const MODEL_POLICY_LOCK_WAIT_MS = 20_000
|
|
13
|
+
|
|
14
|
+
function sleep(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function modelPolicyLockDir(configDir) {
|
|
19
|
+
return modelPolicyFile(configDir) + ".lock"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function withCrossProcessModelPolicyLock(configDir, fn) {
|
|
23
|
+
const lockDir = modelPolicyLockDir(configDir)
|
|
24
|
+
await mkdir(path.dirname(lockDir), { recursive: true })
|
|
25
|
+
const deadline = Date.now() + MODEL_POLICY_LOCK_WAIT_MS
|
|
26
|
+
let delayMs = 8
|
|
27
|
+
while (true) {
|
|
28
|
+
try {
|
|
29
|
+
await mkdir(lockDir)
|
|
30
|
+
break
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error?.code !== "EEXIST") throw error
|
|
33
|
+
const info = await stat(lockDir).catch(() => null)
|
|
34
|
+
if (info && Date.now() - info.mtimeMs > MODEL_POLICY_LOCK_STALE_MS) {
|
|
35
|
+
const confirmed = await stat(lockDir).catch(() => null)
|
|
36
|
+
if (
|
|
37
|
+
confirmed &&
|
|
38
|
+
confirmed.ino === info.ino &&
|
|
39
|
+
confirmed.mtimeMs === info.mtimeMs
|
|
40
|
+
) {
|
|
41
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (Date.now() >= deadline) throw new Error("Timed out waiting for model-policy write lock")
|
|
46
|
+
await sleep(delayMs)
|
|
47
|
+
delayMs = Math.min(100, delayMs * 2)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return await fn()
|
|
52
|
+
} finally {
|
|
53
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function withModelPolicyWriteLock(configDir, fn) {
|
|
58
|
+
const key = path.resolve(configDir)
|
|
59
|
+
const previous = MODEL_POLICY_WRITE_TAILS.get(key) || Promise.resolve()
|
|
60
|
+
let release
|
|
61
|
+
const barrier = new Promise((resolve) => { release = resolve })
|
|
62
|
+
const tail = previous.catch(() => {}).then(() => barrier)
|
|
63
|
+
MODEL_POLICY_WRITE_TAILS.set(key, tail)
|
|
64
|
+
|
|
65
|
+
await previous.catch(() => {})
|
|
66
|
+
try {
|
|
67
|
+
return await withCrossProcessModelPolicyLock(configDir, fn)
|
|
68
|
+
} finally {
|
|
69
|
+
release()
|
|
70
|
+
if (MODEL_POLICY_WRITE_TAILS.get(key) === tail) MODEL_POLICY_WRITE_TAILS.delete(key)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
10
74
|
function validModelID(value) {
|
|
11
75
|
return typeof value === "string" && /^[^\s/]+\/[^\s#]+(?:#[^\s]+)?$/.test(value)
|
|
12
76
|
}
|
|
@@ -61,8 +125,7 @@ export async function readModelPolicy(configDir) {
|
|
|
61
125
|
}
|
|
62
126
|
}
|
|
63
127
|
|
|
64
|
-
|
|
65
|
-
const current = await readModelPolicy(configDir)
|
|
128
|
+
async function persistModelPolicy(configDir, current, patch = {}) {
|
|
66
129
|
const merged = normalize({
|
|
67
130
|
...current,
|
|
68
131
|
...patch,
|
|
@@ -73,10 +136,24 @@ export async function writeModelPolicy(configDir, patch = {}) {
|
|
|
73
136
|
})
|
|
74
137
|
const file = modelPolicyFile(configDir)
|
|
75
138
|
await mkdir(path.dirname(file), { recursive: true })
|
|
76
|
-
|
|
139
|
+
const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
|
|
140
|
+
await writeFile(temp, JSON.stringify(merged, null, 2) + "\n", "utf8")
|
|
141
|
+
try {
|
|
142
|
+
await rename(temp, file)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
await rm(temp, { force: true }).catch(() => {})
|
|
145
|
+
throw error
|
|
146
|
+
}
|
|
77
147
|
return { ...merged, file }
|
|
78
148
|
}
|
|
79
149
|
|
|
150
|
+
export async function writeModelPolicy(configDir, patch = {}) {
|
|
151
|
+
return withModelPolicyWriteLock(configDir, async () => {
|
|
152
|
+
const current = await readModelPolicy(configDir)
|
|
153
|
+
return persistModelPolicy(configDir, current, patch)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
80
157
|
export function validateModelID(value) {
|
|
81
158
|
return validModelID(value)
|
|
82
159
|
}
|
|
@@ -102,7 +179,9 @@ export function applyConfiguredModel(source, role, policy) {
|
|
|
102
179
|
}
|
|
103
180
|
|
|
104
181
|
export async function recordModelPerformance(configDir, outcome = {}) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
182
|
+
return withModelPolicyWriteLock(configDir, async () => {
|
|
183
|
+
const current = await readModelPolicy(configDir)
|
|
184
|
+
const performance = recordPerformanceOutcome(current.performance || {}, outcome)
|
|
185
|
+
return persistModelPolicy(configDir, current, { performance })
|
|
186
|
+
})
|
|
108
187
|
}
|