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.
Files changed (43) hide show
  1. package/README.md +5 -5
  2. package/docs/V15-MANAGED-RUNTIME.md +18 -0
  3. package/global-config/agents/verifier.md +6 -2
  4. package/lib/affected-tests.mjs +58 -8
  5. package/lib/capability-fabric.mjs +107 -48
  6. package/lib/code-intelligence/index.mjs +4 -15
  7. package/lib/code-intelligence/lsp-pool.mjs +694 -0
  8. package/lib/code-intelligence/lsp-provider.mjs +481 -67
  9. package/lib/compaction-resume-guard.mjs +327 -0
  10. package/lib/completion-auditor.mjs +20 -0
  11. package/lib/context-engine-v11.mjs +20 -7
  12. package/lib/context-manifest.mjs +38 -21
  13. package/lib/decision-policy.mjs +40 -2
  14. package/lib/evidence-store.mjs +121 -31
  15. package/lib/executable-probe.mjs +69 -0
  16. package/lib/execution-contract.mjs +29 -16
  17. package/lib/hierarchical-context.mjs +31 -3
  18. package/lib/learning-engine.mjs +164 -65
  19. package/lib/memory-engine.mjs +348 -98
  20. package/lib/model-config.mjs +86 -7
  21. package/lib/performance-fabric.mjs +354 -107
  22. package/lib/permission-policy.mjs +232 -0
  23. package/lib/pi-rpc-pool.mjs +170 -5
  24. package/lib/provider-recovery.mjs +51 -0
  25. package/lib/repo-graph.mjs +38 -5
  26. package/lib/runtime-events.mjs +31 -10
  27. package/lib/semantic-index.mjs +67 -20
  28. package/lib/skill-compiler.mjs +84 -18
  29. package/lib/subagent-artifacts.mjs +182 -0
  30. package/lib/task-policy.mjs +80 -7
  31. package/lib/trajectory.mjs +25 -1
  32. package/lib/untrusted-output.mjs +106 -0
  33. package/lib/verification-broker.mjs +32 -27
  34. package/lib/workspace-fingerprint.mjs +43 -8
  35. package/lib/workspace-hygiene.mjs +481 -0
  36. package/package.json +8 -6
  37. package/pi/extensions/ues-child-runtime.ts +103 -5
  38. package/pi/extensions/ues.ts +984 -95
  39. package/scripts/benchmark-lsp-pool.mjs +128 -0
  40. package/scripts/check-release-consistency.mjs +16 -4
  41. package/scripts/check-source-integrity.mjs +60 -1
  42. package/scripts/run-test-suite.mjs +143 -0
  43. package/scripts/smoke-package-closure.mjs +4 -0
@@ -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
- input.scope || "project",
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
- const timestamp = now()
103
- const existing = state.memories.find((item) => item.id === id)
104
- if (existing) {
105
- const reinforced = {
106
- ...existing,
107
- lastSeenAt: timestamp,
108
- reinforcementCount: Number(existing.reinforcementCount || 1) + 1,
109
- evidenceRefs: uniq([...(existing.evidenceRefs || []), ...(input.evidenceRefs || [])]),
110
- files: uniq([...(existing.files || []), ...(input.files || []).map(normalizeFile)]),
111
- tags: uniq([...(existing.tags || []), ...(input.tags || [])]),
112
- confidence: Math.max(clamp(existing.confidence), clamp(input.confidence, existing.confidence ?? 0.7)),
113
- taskClass: input.taskClass || existing.taskClass || null,
114
- expiresAt: input.expiresAt || existing.expiresAt || null,
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
- const memory = {
121
- id,
122
- key: input.key || null,
123
- type,
124
- scope,
125
- content,
126
- status: "candidate",
127
- confidence: clamp(input.confidence),
128
- evidenceRefs: uniq(input.evidenceRefs || []),
129
- files: uniq((input.files || []).map(normalizeFile)),
130
- tags: uniq(input.tags || []),
131
- sourceTask: input.sourceTask || null,
132
- sourceCommit: input.sourceCommit || null,
133
- taskClass: input.taskClass || null,
134
- expiresAt: input.expiresAt || null,
135
- createdAt: timestamp,
136
- lastSeenAt: timestamp,
137
- lastUsedAt: null,
138
- useCount: 0,
139
- reinforcementCount: 1,
140
- verifiedAt: null,
141
- verifier: null,
142
- supersededBy: null,
143
- }
144
- await writeMemoryState(root, [...state.memories, memory])
145
- return memory
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
- const verified = {
165
- ...current,
166
- status: "verified",
167
- evidenceRefs: refs,
168
- verifier: String(verification.verifier),
169
- verifiedAt: now(),
170
- lastSeenAt: now(),
171
- confidence: Math.max(clamp(current.confidence), clamp(verification.confidence, current.confidence ?? 0.7)),
172
- }
173
- await writeMemoryState(root, state.memories.map((item) => item.id === id ? verified : item))
174
- return verified
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
- const state = await readMemoryState(root)
180
- const current = state.memories.find((item) => item.id === id)
181
- const replacement = state.memories.find((item) => item.id === replacementId)
182
- if (!current) throw new Error("unknown memory: " + id)
183
- if (!replacement) throw new Error("unknown replacement memory: " + replacementId)
184
- if (replacement.status !== "verified") throw new Error("replacement memory must be verified before supersession")
185
- const timestamp = now()
186
- const superseded = {
187
- ...current,
188
- status: "superseded",
189
- supersededBy: replacementId,
190
- lastSeenAt: timestamp,
191
- }
192
- await writeMemoryState(root, state.memories.map((item) => item.id === id ? superseded : item))
193
- return superseded
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
- const rows = memories.map((item, index) => ({
341
- item,
342
- lexical: lexical[index] || 0,
343
- semantic: Math.max(0, cosine(queryVector, tokenVector(item.content))),
344
- graph: graphAffinity(queryFiles, item.files || []),
345
- taskClass: taskClassAffinity(options.taskClass, item.taskClass),
346
- recency: recencyScore(item.lastUsedAt || item.lastSeenAt || item.verifiedAt || item.createdAt),
347
- confidence: clamp(item.confidence),
348
- scope: 0,
349
- }))
350
- for (const row of rows) row.scope = scopeAffinity(row.item.scope, row.graph)
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 = rrf + row.confidence * 0.01 + row.graph * 0.01 + row.scope * 0.006 + row.taskClass * 0.008
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) => row.lexical > 0 || row.semantic > 0.05 || row.graph > 0)
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 writeMemoryState(root, state.memories.map((item) => used.has(item.id)
387
- ? { ...item, lastUsedAt: timestamp, useCount: Number(item.useCount || 0) + 1 }
388
- : item))
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
  }
@@ -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
- export async function writeModelPolicy(configDir, patch = {}) {
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
- await writeFile(file, JSON.stringify(merged, null, 2) + "\n", "utf8")
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
- const current = await readModelPolicy(configDir)
106
- const performance = recordPerformanceOutcome(current.performance || {}, outcome)
107
- return writeModelPolicy(configDir, { performance })
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
  }