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.
- package/README.md +5 -5
- package/lib/affected-tests.mjs +37 -4
- package/lib/code-intelligence/index.mjs +3 -2
- package/lib/code-intelligence/lsp-provider.mjs +326 -52
- package/lib/compaction-resume-guard.mjs +255 -0
- package/lib/context-engine-v11.mjs +20 -7
- package/lib/execution-contract.mjs +29 -16
- package/lib/memory-engine.mjs +189 -16
- package/lib/performance-fabric.mjs +262 -107
- package/lib/subagent-artifacts.mjs +182 -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 +1 -1
- package/pi/extensions/ues-child-runtime.ts +25 -2
- package/pi/extensions/ues.ts +290 -15
|
@@ -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
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 =
|
|
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]
|
package/lib/memory-engine.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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 =
|
|
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) =>
|
|
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
|
}
|