opencode-agent-skill 15.0.0 → 15.1.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.
@@ -0,0 +1,255 @@
1
+ import { createHash } from "node:crypto"
2
+ import { existsSync } from "node:fs"
3
+ import { readdir, readFile } from "node:fs/promises"
4
+ import path from "node:path"
5
+ import { checkpointWork } from "./task-engine.mjs"
6
+
7
+ const WORK_DIR = ".ues-work"
8
+ const MAX_WORKSPACES = 3
9
+ const MAX_TASKS = 28
10
+ const MAX_RECEIPTS = 20
11
+
12
+ function digest(value) {
13
+ return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 20)
14
+ }
15
+
16
+ async function readJson(file, fallback = null) {
17
+ try {
18
+ return JSON.parse(await readFile(file, "utf8"))
19
+ } catch (error) {
20
+ if (error?.code === "ENOENT") return fallback
21
+ return fallback
22
+ }
23
+ }
24
+
25
+ function inside(base, target) {
26
+ const root = path.resolve(base)
27
+ const resolved = path.resolve(target)
28
+ return resolved === root || resolved.startsWith(root + path.sep)
29
+ }
30
+
31
+ function normalizePhasePath(value) {
32
+ return String(value || "").replaceAll("\\", "/").replace(/^\.\/+/, "")
33
+ }
34
+
35
+ async function activeWorkspaces(root, max = MAX_WORKSPACES) {
36
+ const base = path.join(path.resolve(root), WORK_DIR)
37
+ if (!existsSync(base)) return []
38
+ const entries = await readdir(base, { withFileTypes: true }).catch(() => [])
39
+ const rows = []
40
+ for (const entry of entries) {
41
+ if (!entry.isDirectory()) continue
42
+ const dir = path.join(base, entry.name)
43
+ const state = await readJson(path.join(dir, "STATE.json"), null)
44
+ if (!state || String(state.status || "").toLowerCase() === "completed") continue
45
+ rows.push({
46
+ slug: entry.name,
47
+ dir,
48
+ state,
49
+ updatedAt: Date.parse(state.updatedAt || state.createdAt || "") || 0,
50
+ })
51
+ }
52
+ return rows
53
+ .sort((a, b) => b.updatedAt - a.updatedAt || a.slug.localeCompare(b.slug))
54
+ .slice(0, Math.max(1, Math.min(10, Number(max || MAX_WORKSPACES))))
55
+ }
56
+
57
+ function taskRows(plan, state) {
58
+ const tasks = Array.isArray(plan?.tasks) ? plan.tasks : []
59
+ const rows = tasks.map((task) => {
60
+ const runtime = state?.tasks?.[task.id] || {}
61
+ return {
62
+ id: task.id,
63
+ title: String(task.title || "").slice(0, 240),
64
+ status: runtime.status || "unknown",
65
+ attempts: Number(runtime.attempts || 0),
66
+ runId: runtime.runId || null,
67
+ dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.slice(0, 24) : [],
68
+ evidenceStrength: runtime.evidenceStrength || null,
69
+ report: runtime.report || null,
70
+ lastError: runtime.lastError ? String(runtime.lastError).slice(0, 800) : null,
71
+ }
72
+ })
73
+ rows.sort((a, b) => {
74
+ const rank = (row) => row.status === "running" ? 0 : row.status === "completed" ? 2 : 1
75
+ return rank(a) - rank(b) || String(a.id).localeCompare(String(b.id))
76
+ })
77
+ return rows.slice(0, MAX_TASKS)
78
+ }
79
+
80
+ function receiptRows(evidence) {
81
+ return (Array.isArray(evidence?.receipts) ? evidence.receipts : [])
82
+ .slice(-MAX_RECEIPTS)
83
+ .map((item) => ({
84
+ id: item.id || null,
85
+ kind: item.kind || "verification-receipt",
86
+ task: item.task || null,
87
+ runId: item.runId || null,
88
+ passed: item.passed === true,
89
+ verdict: item.verdict || (item.passed === true ? "PASS" : null),
90
+ command: item.command || null,
91
+ workspaceAfter: item.workspaceAfter || null,
92
+ recordedAt: item.recordedAt || item.finishedAt || null,
93
+ }))
94
+ }
95
+
96
+ function gateRows(evidence) {
97
+ return (Array.isArray(evidence?.gateReceipts) ? evidence.gateReceipts : [])
98
+ .slice(-12)
99
+ .map((item) => ({
100
+ id: item.id || null,
101
+ kind: item.kind || null,
102
+ verdict: item.verdict || null,
103
+ verifier: item.verifier || null,
104
+ planHash: item.planHash || null,
105
+ workspaceFingerprint: item.workspaceFingerprint || null,
106
+ recordedAt: item.recordedAt || null,
107
+ }))
108
+ }
109
+
110
+ async function phaseRows(dir, manifest) {
111
+ const artifacts = Array.isArray(manifest?.artifacts) ? manifest.artifacts : []
112
+ const rows = []
113
+ for (const relative of artifacts.slice(0, 32)) {
114
+ const normalized = normalizePhasePath(relative)
115
+ const target = path.resolve(dir, normalized)
116
+ if (!normalized || !inside(dir, target)) continue
117
+ const phase = await readJson(target, null)
118
+ if (!phase) continue
119
+ rows.push({
120
+ artifact: normalized,
121
+ phase: phase.phase ?? null,
122
+ title: String(phase.title || "").slice(0, 240),
123
+ status: phase.status || "UNKNOWN",
124
+ sourceBodyHash: phase.sourceBodyHash || null,
125
+ evidenceCount: Array.isArray(phase.evidence) ? phase.evidence.length : 0,
126
+ finalVerdicts: phase.finalVerdicts || null,
127
+ })
128
+ }
129
+ return rows
130
+ }
131
+
132
+ function stateSummary(state) {
133
+ return {
134
+ status: state?.status || null,
135
+ goal: state?.goal ? String(state.goal).slice(0, 800) : null,
136
+ updatedAt: state?.updatedAt || null,
137
+ planHash: state?.planHash || null,
138
+ planApproved: state?.planApproved === true,
139
+ nextAction: state?.nextAction || null,
140
+ checkpoint: state?.checkpoint ? {
141
+ id: state.checkpoint.id || null,
142
+ reason: state.checkpoint.reason || null,
143
+ createdAt: state.checkpoint.createdAt || null,
144
+ currentTaskId: state.checkpoint.currentTaskId || null,
145
+ runId: state.checkpoint.runId || null,
146
+ planHash: state.checkpoint.planHash || null,
147
+ workspaceFingerprint: state.checkpoint.workspaceFingerprint || null,
148
+ nextAction: state.checkpoint.nextAction || null,
149
+ evidencePointers: Array.isArray(state.checkpoint.evidencePointers)
150
+ ? state.checkpoint.evidencePointers.slice(-20)
151
+ : [],
152
+ } : null,
153
+ }
154
+ }
155
+
156
+ export async function checkpointDurableWorkBeforeCompaction(root, options = {}) {
157
+ const reason = String(options.reason || "unknown")
158
+ const rows = await activeWorkspaces(root, options.maxWorkspaces || MAX_WORKSPACES)
159
+ const results = []
160
+ for (const row of rows) {
161
+ try {
162
+ const checkpoint = await checkpointWork(root, row.slug, {
163
+ reason: `pi-compaction:${reason}`,
164
+ })
165
+ results.push({ slug: row.slug, checkpoint, ok: true })
166
+ } catch (error) {
167
+ results.push({
168
+ slug: row.slug,
169
+ checkpoint: null,
170
+ ok: false,
171
+ error: error instanceof Error ? error.message : String(error),
172
+ })
173
+ }
174
+ }
175
+ return results
176
+ }
177
+
178
+ export async function buildCompactionResumeGuard(root, options = {}) {
179
+ const rows = await activeWorkspaces(root, options.maxWorkspaces || MAX_WORKSPACES)
180
+ const workspaces = []
181
+ for (const row of rows) {
182
+ const [plan, evidence, contract, manifest] = await Promise.all([
183
+ readJson(path.join(row.dir, "PLAN.json"), null),
184
+ readJson(path.join(row.dir, "EVIDENCE.json"), {}),
185
+ readJson(path.join(row.dir, "EXECUTION_CONTRACT.json"), null),
186
+ readJson(path.join(row.dir, "phases", "MANIFEST.json"), null),
187
+ ])
188
+ const phases = await phaseRows(row.dir, manifest)
189
+ const sources = {
190
+ state: path.relative(root, path.join(row.dir, "STATE.json")).replaceAll("\\", "/"),
191
+ plan: path.relative(root, path.join(row.dir, "PLAN.json")).replaceAll("\\", "/"),
192
+ evidence: path.relative(root, path.join(row.dir, "EVIDENCE.json")).replaceAll("\\", "/"),
193
+ executionContract: contract
194
+ ? path.relative(root, path.join(row.dir, "EXECUTION_CONTRACT.json")).replaceAll("\\", "/")
195
+ : null,
196
+ phaseManifest: manifest
197
+ ? path.relative(root, path.join(row.dir, "phases", "MANIFEST.json")).replaceAll("\\", "/")
198
+ : null,
199
+ }
200
+ workspaces.push({
201
+ slug: row.slug,
202
+ sources,
203
+ sourceHashes: {
204
+ state: digest(JSON.stringify(row.state)),
205
+ plan: digest(JSON.stringify(plan || {})),
206
+ evidence: digest(JSON.stringify(evidence || {})),
207
+ executionContract: contract ? digest(JSON.stringify(contract)) : null,
208
+ phaseManifest: manifest ? digest(JSON.stringify(manifest)) : null,
209
+ },
210
+ state: stateSummary(row.state),
211
+ tasks: taskRows(plan, row.state),
212
+ receipts: receiptRows(evidence),
213
+ gateReceipts: gateRows(evidence),
214
+ executionContract: contract ? {
215
+ schemaVersion: contract.schemaVersion || null,
216
+ taskHash: contract.taskHash || null,
217
+ gates: contract.gates || null,
218
+ phaseCount: Array.isArray(contract.phases) ? contract.phases.length : 0,
219
+ approvedInheritedDirtyPaths: Array.isArray(contract.approvedInheritedDirtyPaths)
220
+ ? contract.approvedInheritedDirtyPaths.slice(0, 40)
221
+ : [],
222
+ } : null,
223
+ phases,
224
+ })
225
+ }
226
+
227
+ return {
228
+ schemaVersion: 1,
229
+ kind: "ues-durable-compaction-resume-guard",
230
+ generatedAt: new Date().toISOString(),
231
+ compactionReason: options.reason || null,
232
+ source: "deterministic-filesystem-artifacts",
233
+ modelSummaryTrustedForDurableState: false,
234
+ workspaceCount: workspaces.length,
235
+ workspaces,
236
+ }
237
+ }
238
+
239
+ export function renderCompactionResumeGuard(packet, options = {}) {
240
+ if (!packet?.workspaces?.length) return ""
241
+ const maxChars = Math.max(8000, Math.min(48000, Number(options.maxChars || 28000)))
242
+ const preface = [
243
+ "## UES Durable Compaction Resume Guard",
244
+ "Authoritative durable state was rebuilt from filesystem artifacts after Pi compaction.",
245
+ "The model-generated compaction summary is advisory only for durable execution state.",
246
+ "If the summary conflicts with STATE.json, PLAN.json, EXECUTION_CONTRACT.json, phase artifacts, or evidence receipts below, the deterministic artifacts win.",
247
+ "Do not mark a task/phase complete unless the rebuilt state and fresh receipts support it.",
248
+ "Follow checkpoint.nextAction/state.nextAction and re-read the referenced artifact when more detail is required.",
249
+ "",
250
+ ].join("\n")
251
+ const encoded = JSON.stringify(packet, null, 2)
252
+ if (preface.length + encoded.length <= maxChars) return preface + encoded
253
+ return preface + encoded.slice(0, Math.max(0, maxChars - preface.length - 120)) +
254
+ "\n...[resume packet bounded; re-read referenced .ues-work artifacts for exact detail]"
255
+ }
@@ -101,6 +101,16 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
101
101
  const policy = options.policy || {}
102
102
  const capabilities = options.capabilities || inferTaskCapabilities(taskText(task), options.facts || {})
103
103
  const evidenceBudget = options.evidenceBudget || planEvidenceBudget(policy, task, capabilities.required)
104
+ const contextBuildStartedAt = Date.now()
105
+ const memoryPromise = retrieveMemories(root, taskText(task), {
106
+ files: taskFilesForMemory(task),
107
+ limit: options.memoryLimit ?? 6,
108
+ taskClass: options.taskClass || task.type || policy.executionProfile || policy.profile?.name || null,
109
+ touch: options.touchMemories !== false,
110
+ }).catch(() => ({ schemaVersion: 1, query: taskText(task), eligible: 0, results: [] }))
111
+ const fabricPromise = capabilityFabricStatus(root).catch(() => null)
112
+
113
+ const manifestStartedAt = Date.now()
104
114
  const manifest = await buildContextManifest(root, task, {
105
115
  budget: evidenceBudget.total,
106
116
  evidenceBudget,
@@ -110,21 +120,18 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
110
120
  changedFiles: options.changedFiles,
111
121
  workspaceFingerprint: options.workspaceFingerprint,
112
122
  })
123
+ const manifestMs = Date.now() - manifestStartedAt
113
124
 
125
+ const externalizeStartedAt = Date.now()
114
126
  const externalized = await externalizeContextExcerpts(root, manifest, {
115
127
  task,
116
128
  threshold: options.externalizeThreshold,
117
129
  inlineChars: options.inlineChars,
118
130
  })
131
+ const externalizeMs = Date.now() - externalizeStartedAt
119
132
 
120
- const memoryRetrieval = await retrieveMemories(root, taskText(task), {
121
- files: taskFilesForMemory(task),
122
- limit: options.memoryLimit ?? 6,
123
- taskClass: options.taskClass || task.type || policy.executionProfile || policy.profile?.name || null,
124
- touch: options.touchMemories !== false,
125
- }).catch(() => ({ schemaVersion: 1, query: taskText(task), eligible: 0, results: [] }))
133
+ const [memoryRetrieval, fabric] = await Promise.all([memoryPromise, fabricPromise])
126
134
  const memories = memoryRetrieval.results || []
127
- const fabric = await capabilityFabricStatus(root).catch(() => null)
128
135
  const providerHints = (fabric?.rows || [])
129
136
  .filter((row) => row.selected)
130
137
  .slice(0, 12)
@@ -202,6 +209,12 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
202
209
  entries: externalized.externalized,
203
210
  },
204
211
  promptEnvelope,
212
+ performance: {
213
+ parallelContextPreparation: true,
214
+ manifestMs,
215
+ externalizeMs,
216
+ totalMs: Date.now() - contextBuildStartedAt,
217
+ },
205
218
  promptCache: {
206
219
  stablePrefixHash: promptEnvelope.stablePrefixHash,
207
220
  dynamicHash: promptEnvelope.dynamicHash,
@@ -174,26 +174,39 @@ export function detectInheritedDirtyViolations(root, snapshot, approvedPaths = [
174
174
  }
175
175
  }
176
176
 
177
- export function captureInheritedDirtyState(root) {
177
+ export function captureInheritedDirtyState(root, options = {}) {
178
178
  const resolved = path.resolve(root)
179
- const probe = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
180
- cwd: resolved,
181
- encoding: "utf8",
182
- maxBuffer: 16 * 1024 * 1024,
183
- })
184
- if (probe.status !== 0) {
185
- return {
186
- schemaVersion: 1,
187
- available: false,
188
- clean: false,
189
- root: resolved,
190
- entries: [],
191
- paths: [],
192
- error: String(probe.stderr || probe.stdout || "git status failed").trim(),
179
+ let statusOutput = ""
180
+ const workspaceState = options.workspaceState || null
181
+ if (
182
+ workspaceState &&
183
+ typeof workspaceState === "object" &&
184
+ workspaceState.root &&
185
+ path.resolve(workspaceState.root) === resolved &&
186
+ typeof workspaceState.statusOutput === "string"
187
+ ) {
188
+ statusOutput = workspaceState.statusOutput
189
+ } else {
190
+ const probe = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
191
+ cwd: resolved,
192
+ encoding: "utf8",
193
+ maxBuffer: 16 * 1024 * 1024,
194
+ })
195
+ if (probe.status !== 0) {
196
+ return {
197
+ schemaVersion: 1,
198
+ available: false,
199
+ clean: false,
200
+ root: resolved,
201
+ entries: [],
202
+ paths: [],
203
+ error: String(probe.stderr || probe.stdout || "git status failed").trim(),
204
+ }
193
205
  }
206
+ statusOutput = String(probe.stdout || "")
194
207
  }
195
208
 
196
- const tokens = String(probe.stdout || "").split("\0").filter(Boolean)
209
+ const tokens = statusOutput.split("\0").filter(Boolean)
197
210
  const entries = []
198
211
  for (let index = 0; index < tokens.length; index += 1) {
199
212
  const token = tokens[index]
@@ -4,6 +4,7 @@ import { mkdir, readFile, rename, rm, 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"
@@ -44,15 +45,35 @@ function normalizedContent(value) {
44
45
  return String(value || "").trim().replace(/\s+/g, " ")
45
46
  }
46
47
 
48
+ function scopedIdentityFiles(scope, files = []) {
49
+ if (!["file", "module"].includes(String(scope || ""))) return []
50
+ return uniq((files || []).map(normalizeFile)).sort()
51
+ }
52
+
47
53
  function idFor(input) {
54
+ const scope = input.scope || "project"
55
+ const scopedFiles = scopedIdentityFiles(scope, input.files)
48
56
  const key = input.key || [
49
- input.scope || "project",
57
+ scope,
50
58
  input.type || "episodic",
59
+ scopedFiles.length ? scopedFiles.join(",") : "",
51
60
  normalizedContent(input.content),
52
61
  ].join(":")
53
62
  return createHash("sha256").update(String(key)).digest("hex").slice(0, 20)
54
63
  }
55
64
 
65
+ function sameMemoryIdentity(item, input) {
66
+ if (!item) return false
67
+ if (String(item.scope || "project") !== String(input.scope || "project")) return false
68
+ if (String(item.type || "episodic") !== String(input.type || "episodic")) return false
69
+ if (normalizedContent(item.content) !== normalizedContent(input.content)) return false
70
+ const expectedFiles = scopedIdentityFiles(input.scope, input.files)
71
+ if (!expectedFiles.length) return true
72
+ const actualFiles = scopedIdentityFiles(item.scope, item.files)
73
+ return actualFiles.length === expectedFiles.length &&
74
+ actualFiles.every((file, index) => file === expectedFiles[index])
75
+ }
76
+
56
77
  function memoryPath(root) {
57
78
  return path.join(path.resolve(root), MEMORY_DIR, MEMORY_FILE)
58
79
  }
@@ -100,7 +121,8 @@ export async function proposeMemory(root, input = {}) {
100
121
  const state = await readMemoryState(root)
101
122
  const id = idFor({ ...input, type, scope, content })
102
123
  const timestamp = now()
103
- const existing = state.memories.find((item) => item.id === id)
124
+ const existing = state.memories.find((item) => item.id === id) ||
125
+ state.memories.find((item) => sameMemoryIdentity(item, { ...input, type, scope, content }))
104
126
  if (existing) {
105
127
  const reinforced = {
106
128
  ...existing,
@@ -108,6 +130,7 @@ export async function proposeMemory(root, input = {}) {
108
130
  reinforcementCount: Number(existing.reinforcementCount || 1) + 1,
109
131
  evidenceRefs: uniq([...(existing.evidenceRefs || []), ...(input.evidenceRefs || [])]),
110
132
  files: uniq([...(existing.files || []), ...(input.files || []).map(normalizeFile)]),
133
+ dependencies: uniq([...(existing.dependencies || []), ...(input.dependencies || []).map(normalizeFile)]),
111
134
  tags: uniq([...(existing.tags || []), ...(input.tags || [])]),
112
135
  confidence: Math.max(clamp(existing.confidence), clamp(input.confidence, existing.confidence ?? 0.7)),
113
136
  taskClass: input.taskClass || existing.taskClass || null,
@@ -127,6 +150,7 @@ export async function proposeMemory(root, input = {}) {
127
150
  confidence: clamp(input.confidence),
128
151
  evidenceRefs: uniq(input.evidenceRefs || []),
129
152
  files: uniq((input.files || []).map(normalizeFile)),
153
+ dependencies: uniq((input.dependencies || []).map(normalizeFile)),
130
154
  tags: uniq(input.tags || []),
131
155
  sourceTask: input.sourceTask || null,
132
156
  sourceCommit: input.sourceCommit || null,
@@ -312,6 +336,7 @@ export function buildMemorySnapshot(memories = []) {
312
336
  content: normalizedContent(item.content),
313
337
  confidence: clamp(item.confidence),
314
338
  files: uniq((item.files || []).map(normalizeFile)).sort(),
339
+ dependencies: uniq((item.dependencies || []).map(normalizeFile)).sort(),
315
340
  taskClass: item.taskClass || null,
316
341
  verifiedAt: item.verifiedAt || null,
317
342
  evidenceRefs: uniq(item.evidenceRefs || []).sort(),
@@ -330,6 +355,96 @@ export function buildMemorySnapshot(memories = []) {
330
355
  }
331
356
  }
332
357
 
358
+ function dependencyAdjacency(edges = []) {
359
+ const graph = new Map()
360
+ const connect = (left, right) => {
361
+ if (!left || !right) return
362
+ if (!graph.has(left)) graph.set(left, new Set())
363
+ graph.get(left).add(right)
364
+ }
365
+ for (const edge of edges || []) {
366
+ const from = normalizeFile(edge?.from)
367
+ const to = normalizeFile(edge?.to)
368
+ if (!from || !to) continue
369
+ // Retrieval uses dependency proximity, not ownership direction, so both
370
+ // importers and imported modules are useful neighbors.
371
+ connect(from, to)
372
+ connect(to, from)
373
+ }
374
+ return graph
375
+ }
376
+
377
+ function dependencyGraphAffinity(queryFiles, memoryFiles, adjacency, maxDepth = 3) {
378
+ if (!queryFiles.length || !memoryFiles.length || !adjacency?.size) return 0
379
+ const targets = new Set(memoryFiles.map(normalizeFile))
380
+ let best = 0
381
+ for (const source of queryFiles.map(normalizeFile)) {
382
+ if (targets.has(source)) return 1
383
+ let frontier = new Set([source])
384
+ const seen = new Set(frontier)
385
+ for (let depth = 1; depth <= maxDepth && frontier.size; depth += 1) {
386
+ const next = new Set()
387
+ for (const current of frontier) {
388
+ for (const neighbor of adjacency.get(current) || []) {
389
+ if (targets.has(neighbor)) {
390
+ const score = depth === 1 ? 0.95 : depth === 2 ? 0.7 : 0.45
391
+ best = Math.max(best, score)
392
+ }
393
+ if (!seen.has(neighbor)) {
394
+ seen.add(neighbor)
395
+ next.add(neighbor)
396
+ }
397
+ }
398
+ }
399
+ frontier = next
400
+ }
401
+ }
402
+ return best
403
+ }
404
+
405
+ function exactFileAffinity(queryFiles, memoryFiles) {
406
+ const wanted = new Set(queryFiles.map(normalizeFile))
407
+ return memoryFiles.some((file) => wanted.has(normalizeFile(file))) ? 1 : 0
408
+ }
409
+
410
+ function scopeEligible(item, queryFiles, pathGraph) {
411
+ if (!queryFiles.length) return true
412
+ const memoryFiles = uniq((item.files || []).map(normalizeFile))
413
+ if (item.scope === "file") return exactFileAffinity(queryFiles, memoryFiles) === 1
414
+ if (item.scope === "module") return pathGraph >= 0.45
415
+ return true
416
+ }
417
+
418
+ function memoryAgeDays(item) {
419
+ const timestamp = Date.parse(item.lastSeenAt || item.verifiedAt || item.createdAt || "")
420
+ if (!timestamp) return 0
421
+ return Math.max(0, (Date.now() - timestamp) / 86_400_000)
422
+ }
423
+
424
+ function memoryHalfLifeDays(item) {
425
+ const base = {
426
+ failure: 45,
427
+ episodic: 75,
428
+ decision: 120,
429
+ procedural: 180,
430
+ semantic: 240,
431
+ }[item.type] || 120
432
+ const reinforcement = Math.max(1, Number(item.reinforcementCount || 1))
433
+ return base * (1 + Math.log2(reinforcement) * 0.2)
434
+ }
435
+
436
+ function agingScore(item) {
437
+ const age = memoryAgeDays(item)
438
+ const halfLife = memoryHalfLifeDays(item)
439
+ return Math.pow(0.5, age / Math.max(1, halfLife))
440
+ }
441
+
442
+ function reinforcementScore(item) {
443
+ const reinforcement = Math.max(0, Number(item.reinforcementCount || 0))
444
+ const use = Math.max(0, Number(item.useCount || 0))
445
+ return Math.min(1, (Math.log1p(reinforcement) + 0.55 * Math.log1p(use)) / 4)
446
+ }
447
+
333
448
  export async function retrieveMemories(root, query, options = {}) {
334
449
  const state = await readMemoryState(root)
335
450
  const memories = state.memories.filter((item) => item.status === "verified" && !item.supersededBy && !isExpired(item))
@@ -337,47 +452,99 @@ export async function retrieveMemories(root, query, options = {}) {
337
452
  const queryVector = tokenVector(query)
338
453
  const queryFiles = uniq((options.files || []).map(normalizeFile))
339
454
  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)
455
+
456
+ let repoGraph = null
457
+ if (queryFiles.length && memories.length && options.dependencyGraph !== false) {
458
+ repoGraph = await buildRepoGraphCached(root, {
459
+ maxFiles: Math.max(100, Math.min(6000, Number(options.maxGraphFiles || 2500))),
460
+ ...(options.workspaceFingerprint ? { workspaceFingerprint: options.workspaceFingerprint } : {}),
461
+ }).catch(() => null)
462
+ }
463
+ const adjacency = dependencyAdjacency(repoGraph?.edges || [])
464
+
465
+ const rows = memories.map((item, index) => {
466
+ const memoryFiles = uniq((item.files || []).map(normalizeFile))
467
+ const pathGraph = graphAffinity(queryFiles, memoryFiles)
468
+ const dependency = dependencyGraphAffinity(
469
+ queryFiles,
470
+ uniq([...memoryFiles, ...(item.dependencies || []).map(normalizeFile)]),
471
+ adjacency,
472
+ )
473
+ const graph = Math.max(pathGraph, dependency)
474
+ const aging = agingScore(item)
475
+ const reinforcement = reinforcementScore(item)
476
+ return {
477
+ item,
478
+ lexical: lexical[index] || 0,
479
+ semantic: Math.max(0, cosine(queryVector, tokenVector([item.content, ...(item.tags || []), ...memoryFiles].join(" ")))),
480
+ graph,
481
+ pathGraph,
482
+ dependency,
483
+ taskClass: taskClassAffinity(options.taskClass, item.taskClass),
484
+ recency: recencyScore(item.lastUsedAt || item.lastSeenAt || item.verifiedAt || item.createdAt),
485
+ aging,
486
+ reinforcement,
487
+ confidence: clamp(item.confidence) * (0.65 + 0.35 * aging),
488
+ scope: 0,
489
+ scopeEligible: true,
490
+ }
491
+ })
492
+ for (const row of rows) {
493
+ row.scope = scopeAffinity(row.item.scope, row.pathGraph)
494
+ row.scopeEligible = options.strictScope === false
495
+ ? true
496
+ : scopeEligible(row.item, queryFiles, row.pathGraph)
497
+ }
351
498
  const ranks = {
352
499
  lexical: rankMap(rows, "lexical"),
353
500
  semantic: rankMap(rows, "semantic"),
354
501
  graph: rankMap(rows, "graph"),
502
+ dependency: rankMap(rows, "dependency"),
355
503
  taskClass: rankMap(rows, "taskClass"),
356
504
  recency: rankMap(rows, "recency"),
505
+ aging: rankMap(rows, "aging"),
506
+ reinforcement: rankMap(rows, "reinforcement"),
357
507
  }
358
508
  for (const row of rows) {
359
509
  let rrf = 0
360
- for (const key of ["lexical", "semantic", "graph", "taskClass", "recency"]) {
510
+ for (const key of ["lexical", "semantic", "graph", "dependency", "taskClass", "recency", "aging", "reinforcement"]) {
361
511
  const rank = ranks[key].get(row.item.id)
362
512
  if (rank) rrf += 1 / (60 + rank)
363
513
  }
364
- row.score = rrf + row.confidence * 0.01 + row.graph * 0.01 + row.scope * 0.006 + row.taskClass * 0.008
514
+ row.score =
515
+ rrf +
516
+ row.confidence * 0.01 +
517
+ row.graph * 0.01 +
518
+ row.dependency * 0.012 +
519
+ row.scope * 0.006 +
520
+ row.taskClass * 0.008 +
521
+ row.reinforcement * 0.006 +
522
+ row.aging * 0.004
365
523
  }
366
524
  const limit = Math.max(1, Math.min(Number(options.limit || 6), 30))
367
525
  const results = rows
368
- .filter((row) => row.lexical > 0 || row.semantic > 0.05 || row.graph > 0)
526
+ .filter((row) =>
527
+ row.scopeEligible &&
528
+ (row.lexical > 0 || row.semantic > 0.05 || row.graph > 0 || row.dependency > 0)
529
+ )
369
530
  .sort((a, b) => b.score - a.score || b.confidence - a.confidence || a.item.id.localeCompare(b.item.id))
370
531
  .slice(0, limit)
371
532
  .map((row) => ({
372
533
  ...row.item,
373
534
  retrieval: {
535
+ version: 2,
374
536
  score: Number(row.score.toFixed(8)),
375
537
  lexical: Number(row.lexical.toFixed(6)),
376
538
  semantic: Number(row.semantic.toFixed(6)),
377
539
  graph: Number(row.graph.toFixed(6)),
540
+ pathGraph: Number(row.pathGraph.toFixed(6)),
541
+ dependency: Number(row.dependency.toFixed(6)),
378
542
  taskClass: Number(row.taskClass.toFixed(6)),
379
543
  scope: Number(row.scope.toFixed(6)),
380
544
  recency: Number(row.recency.toFixed(6)),
545
+ aging: Number(row.aging.toFixed(6)),
546
+ reinforcement: Number(row.reinforcement.toFixed(6)),
547
+ effectiveConfidence: Number(row.confidence.toFixed(6)),
381
548
  },
382
549
  }))
383
550
  if (options.touch === true && results.length) {
@@ -389,8 +556,14 @@ export async function retrieveMemories(root, query, options = {}) {
389
556
  }
390
557
  return {
391
558
  schemaVersion: SCHEMA_VERSION,
559
+ retrievalVersion: 2,
392
560
  query: String(query || ""),
393
561
  eligible: memories.length,
562
+ dependencyGraph: repoGraph ? {
563
+ scannedFiles: repoGraph.scannedFiles || 0,
564
+ edgeCount: Array.isArray(repoGraph.edges) ? repoGraph.edges.length : 0,
565
+ truncated: repoGraph.truncated === true,
566
+ } : null,
394
567
  results,
395
568
  snapshot: buildMemorySnapshot(results),
396
569
  }