opencode-agent-skill 15.1.0 → 15.1.2
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 +28 -7
- package/lib/capability-fabric.mjs +107 -48
- package/lib/code-intelligence/index.mjs +50 -24
- package/lib/code-intelligence/lsp-pool.mjs +831 -0
- package/lib/code-intelligence/lsp-provider.mjs +197 -20
- package/lib/compaction-resume-guard.mjs +76 -4
- package/lib/completion-auditor.mjs +20 -0
- package/lib/context-manifest.mjs +88 -36
- 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 +2 -2
- package/lib/hierarchical-context.mjs +31 -3
- package/lib/learning-engine.mjs +164 -65
- package/lib/memory-engine.mjs +177 -95
- package/lib/model-config.mjs +86 -7
- package/lib/performance-fabric.mjs +92 -0
- package/lib/permission-policy.mjs +232 -0
- package/lib/pi-rpc-pool.mjs +220 -24
- package/lib/provider-recovery.mjs +51 -0
- package/lib/repo-graph.mjs +40 -7
- package/lib/repo-inspect.mjs +3 -2
- package/lib/runtime-events.mjs +31 -10
- package/lib/semantic-index.mjs +69 -21
- package/lib/skill-compiler.mjs +84 -18
- package/lib/task-engine.mjs +28 -9
- package/lib/task-policy.mjs +80 -7
- package/lib/trajectory.mjs +25 -1
- package/lib/untrusted-output.mjs +106 -0
- package/lib/workspace-fingerprint.mjs +17 -9
- package/lib/workspace-hygiene.mjs +2 -2
- package/lib/worktree-sandbox.mjs +14 -8
- package/package.json +8 -6
- package/pi/extensions/ues-child-runtime.ts +116 -5
- package/pi/extensions/ues.ts +780 -81
- package/scripts/benchmark-lsp-pool.mjs +128 -0
- package/scripts/check-release-consistency.mjs +16 -4
- package/scripts/check-source-integrity.mjs +118 -6
- package/scripts/run-test-suite.mjs +143 -0
- package/scripts/smoke-package-closure.mjs +4 -0
package/lib/learning-engine.mjs
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"
|
|
3
3
|
import { createHash } from "node:crypto"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { pairedBenchmarkConfidence } from "./benchmark-confidence.mjs"
|
|
6
6
|
|
|
7
7
|
const LEARNING_DIR = ".ues-learning"
|
|
8
8
|
const STATE_FILE = "LEARNINGS.json"
|
|
9
|
+
const LEARNING_WRITE_TAILS = new Map()
|
|
10
|
+
const LEARNING_LOCK_STALE_MS = 15_000
|
|
11
|
+
const LEARNING_LOCK_WAIT_MS = 20_000
|
|
12
|
+
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function learningLockPath(root) {
|
|
18
|
+
return path.join(path.resolve(root), LEARNING_DIR, ".learning-write.lock")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function withCrossProcessLearningLock(root, fn) {
|
|
22
|
+
const lockDir = learningLockPath(root)
|
|
23
|
+
await mkdir(path.dirname(lockDir), { recursive: true })
|
|
24
|
+
const deadline = Date.now() + LEARNING_LOCK_WAIT_MS
|
|
25
|
+
let delayMs = 8
|
|
26
|
+
|
|
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 > LEARNING_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 learning write lock")
|
|
46
|
+
await sleep(delayMs)
|
|
47
|
+
delayMs = Math.min(100, delayMs * 2)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return await fn()
|
|
53
|
+
} finally {
|
|
54
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function withLearningWriteLock(root, fn) {
|
|
59
|
+
const key = path.resolve(root)
|
|
60
|
+
const previous = LEARNING_WRITE_TAILS.get(key) || Promise.resolve()
|
|
61
|
+
let release
|
|
62
|
+
const barrier = new Promise((resolve) => { release = resolve })
|
|
63
|
+
const tail = previous.catch(() => {}).then(() => barrier)
|
|
64
|
+
LEARNING_WRITE_TAILS.set(key, tail)
|
|
65
|
+
|
|
66
|
+
await previous.catch(() => {})
|
|
67
|
+
try {
|
|
68
|
+
return await withCrossProcessLearningLock(root, fn)
|
|
69
|
+
} finally {
|
|
70
|
+
release()
|
|
71
|
+
if (LEARNING_WRITE_TAILS.get(key) === tail) LEARNING_WRITE_TAILS.delete(key)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function atomicJson(file, value) {
|
|
76
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
77
|
+
const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
|
|
78
|
+
await writeFile(temp, JSON.stringify(value, null, 2) + "\n", "utf8")
|
|
79
|
+
try {
|
|
80
|
+
await rename(temp, file)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
await rm(temp, { force: true }).catch(() => {})
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
85
|
+
}
|
|
9
86
|
|
|
10
87
|
function idFor(type, key) {
|
|
11
88
|
return createHash("sha256").update(type + ":" + key).digest("hex").slice(0, 16)
|
|
@@ -27,6 +104,22 @@ async function walkJson(dir, limit = 500) {
|
|
|
27
104
|
return files
|
|
28
105
|
}
|
|
29
106
|
|
|
107
|
+
async function mapLimit(items, limit, worker) {
|
|
108
|
+
if (!items.length) return []
|
|
109
|
+
const output = new Array(items.length)
|
|
110
|
+
let cursor = 0
|
|
111
|
+
const width = Math.min(items.length, Math.max(1, Number(limit || 1)))
|
|
112
|
+
const runners = Array.from({ length: width }, async () => {
|
|
113
|
+
while (true) {
|
|
114
|
+
const index = cursor++
|
|
115
|
+
if (index >= items.length) return
|
|
116
|
+
output[index] = await worker(items[index], index)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
await Promise.all(runners)
|
|
120
|
+
return output
|
|
121
|
+
}
|
|
122
|
+
|
|
30
123
|
function failureReasons(item) {
|
|
31
124
|
const reasons = []
|
|
32
125
|
if (item.timedOut) reasons.push("hard-timeout")
|
|
@@ -71,15 +164,15 @@ function clampRate(value) {
|
|
|
71
164
|
|
|
72
165
|
export async function analyzeEvalTraces(evalDir) {
|
|
73
166
|
const files = await walkJson(path.resolve(evalDir))
|
|
74
|
-
const
|
|
75
|
-
for (const file of files) {
|
|
167
|
+
const batches = await mapLimit(files, 12, async (file) => {
|
|
76
168
|
try {
|
|
77
169
|
const payload = JSON.parse(await readFile(file, "utf8"))
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
}
|
|
170
|
+
return (payload.results || []).map((item) => ({ ...item, source: file }))
|
|
171
|
+
} catch {
|
|
172
|
+
return []
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
const results = batches.flat()
|
|
83
176
|
|
|
84
177
|
const reasonCounters = new Map()
|
|
85
178
|
const clusters = new Map()
|
|
@@ -186,48 +279,49 @@ export async function readLearningState(root) {
|
|
|
186
279
|
}
|
|
187
280
|
|
|
188
281
|
export async function saveLearningAnalysis(root, analysis) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
282
|
+
return withLearningWriteLock(root, async () => {
|
|
283
|
+
const current = await readLearningState(root)
|
|
284
|
+
const acceptedIDs = new Set(current.accepted.map((item) => item.id))
|
|
285
|
+
const existing = new Map(current.proposals.map((item) => [item.id, item]))
|
|
286
|
+
const proposals = analysis.proposals
|
|
287
|
+
.filter((item) => !acceptedIDs.has(item.id))
|
|
288
|
+
.map((item) => ({
|
|
289
|
+
...item,
|
|
290
|
+
firstProposedAt: existing.get(item.id)?.firstProposedAt || new Date().toISOString(),
|
|
291
|
+
lastAnalyzedAt: analysis.analyzedAt,
|
|
292
|
+
}))
|
|
293
|
+
const next = {
|
|
294
|
+
schemaVersion: 2,
|
|
295
|
+
updatedAt: new Date().toISOString(),
|
|
296
|
+
proposals,
|
|
297
|
+
accepted: current.accepted,
|
|
298
|
+
}
|
|
299
|
+
await atomicJson(learningFile(root), next)
|
|
300
|
+
return next
|
|
301
|
+
})
|
|
209
302
|
}
|
|
210
303
|
|
|
211
304
|
// Backward-compatible manual acceptance. Shadow-required proposals remain excluded
|
|
212
305
|
// from retrieval until they are promoted with measured benchmark improvement.
|
|
213
306
|
export async function acceptLearning(root, id) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
307
|
+
return withLearningWriteLock(root, async () => {
|
|
308
|
+
const current = await readLearningState(root)
|
|
309
|
+
const item = current.proposals.find((entry) => entry.id === id)
|
|
310
|
+
if (!item) throw new Error("unknown learning proposal: " + id)
|
|
311
|
+
const accepted = {
|
|
312
|
+
...item,
|
|
313
|
+
status: item.shadowRequired ? "accepted-awaiting-shadow" : "accepted",
|
|
314
|
+
acceptedAt: new Date().toISOString(),
|
|
315
|
+
}
|
|
316
|
+
const next = {
|
|
317
|
+
schemaVersion: 2,
|
|
318
|
+
updatedAt: accepted.acceptedAt,
|
|
319
|
+
proposals: current.proposals.filter((entry) => entry.id !== id),
|
|
320
|
+
accepted: [...current.accepted.filter((entry) => entry.id !== id), accepted],
|
|
321
|
+
}
|
|
322
|
+
await atomicJson(learningFile(root), next)
|
|
323
|
+
return accepted
|
|
324
|
+
})
|
|
231
325
|
}
|
|
232
326
|
|
|
233
327
|
async function benchmarkValidationFromArtifact(root, reportPath) {
|
|
@@ -295,32 +389,37 @@ async function benchmarkValidationFromArtifact(root, reportPath) {
|
|
|
295
389
|
}
|
|
296
390
|
|
|
297
391
|
export async function promoteLearning(root, id, validation = {}) {
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
301
|
-
if (
|
|
392
|
+
const initial = await readLearningState(root)
|
|
393
|
+
const initialItem = initial.accepted.find((entry) => entry.id === id)
|
|
394
|
+
if (!initialItem) {
|
|
395
|
+
if (initial.proposals.some((entry) => entry.id === id)) {
|
|
302
396
|
throw new Error("learning proposal must be explicitly accepted before shadow promotion: " + id)
|
|
303
397
|
}
|
|
304
398
|
throw new Error("unknown learning proposal: " + id)
|
|
305
399
|
}
|
|
306
400
|
|
|
307
401
|
const shadowValidation = await benchmarkValidationFromArtifact(root, validation.report)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
402
|
+
return withLearningWriteLock(root, async () => {
|
|
403
|
+
const current = await readLearningState(root)
|
|
404
|
+
const item = current.accepted.find((entry) => entry.id === id)
|
|
405
|
+
if (!item) throw new Error("learning proposal changed during promotion: " + id)
|
|
406
|
+
|
|
407
|
+
const promotedAt = new Date().toISOString()
|
|
408
|
+
const promoted = {
|
|
409
|
+
...item,
|
|
410
|
+
status: "promoted",
|
|
411
|
+
promotedAt,
|
|
412
|
+
shadowValidation,
|
|
413
|
+
}
|
|
414
|
+
const next = {
|
|
415
|
+
schemaVersion: 2,
|
|
416
|
+
updatedAt: promotedAt,
|
|
417
|
+
proposals: current.proposals.filter((entry) => entry.id !== id),
|
|
418
|
+
accepted: [...current.accepted.filter((entry) => entry.id !== id), promoted],
|
|
419
|
+
}
|
|
420
|
+
await atomicJson(learningFile(root), next)
|
|
421
|
+
return promoted
|
|
422
|
+
})
|
|
324
423
|
}
|
|
325
424
|
|
|
326
425
|
function retrievalTerms(text) {
|
package/lib/memory-engine.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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
7
|
import { buildRepoGraphCached } from "./repo-graph.mjs"
|
|
8
|
+
import { sourceFacingPaths, sourceGitPathspecs } from "./runtime-artifacts.mjs"
|
|
9
|
+
import { workspaceStatusEntries } from "./workspace-fingerprint.mjs"
|
|
8
10
|
|
|
9
11
|
const MEMORY_DIR = ".ues-memory"
|
|
10
12
|
const MEMORY_FILE = "MEMORY.json"
|
|
@@ -16,6 +18,72 @@ const STOP = new Set([
|
|
|
16
18
|
"file","files","change","changes","được","các","cho","với","trong","này","một","những","không","theo",
|
|
17
19
|
])
|
|
18
20
|
|
|
21
|
+
const MEMORY_WRITE_TAILS = new Map()
|
|
22
|
+
const MEMORY_LOCK_STALE_MS = 15_000
|
|
23
|
+
const MEMORY_LOCK_WAIT_MS = 20_000
|
|
24
|
+
|
|
25
|
+
function sleep(ms) {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function memoryLockPath(root) {
|
|
30
|
+
return path.join(path.resolve(root), MEMORY_DIR, ".memory-write.lock")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function withCrossProcessMemoryLock(root, fn) {
|
|
34
|
+
const lockDir = memoryLockPath(root)
|
|
35
|
+
await mkdir(path.dirname(lockDir), { recursive: true })
|
|
36
|
+
const deadline = Date.now() + MEMORY_LOCK_WAIT_MS
|
|
37
|
+
let delayMs = 8
|
|
38
|
+
|
|
39
|
+
while (true) {
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(lockDir)
|
|
42
|
+
break
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error?.code !== "EEXIST") throw error
|
|
45
|
+
const info = await stat(lockDir).catch(() => null)
|
|
46
|
+
if (info && Date.now() - info.mtimeMs > MEMORY_LOCK_STALE_MS) {
|
|
47
|
+
const confirmed = await stat(lockDir).catch(() => null)
|
|
48
|
+
if (
|
|
49
|
+
confirmed &&
|
|
50
|
+
confirmed.ino === info.ino &&
|
|
51
|
+
confirmed.mtimeMs === info.mtimeMs
|
|
52
|
+
) {
|
|
53
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (Date.now() >= deadline) throw new Error("Timed out waiting for memory write lock")
|
|
58
|
+
await sleep(delayMs)
|
|
59
|
+
delayMs = Math.min(100, delayMs * 2)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
return await fn()
|
|
65
|
+
} finally {
|
|
66
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function withMemoryWriteLock(root, fn) {
|
|
71
|
+
const key = path.resolve(root)
|
|
72
|
+
const previous = MEMORY_WRITE_TAILS.get(key) || Promise.resolve()
|
|
73
|
+
let release
|
|
74
|
+
const barrier = new Promise((resolve) => { release = resolve })
|
|
75
|
+
const tail = previous.catch(() => {}).then(() => barrier)
|
|
76
|
+
MEMORY_WRITE_TAILS.set(key, tail)
|
|
77
|
+
|
|
78
|
+
await previous.catch(() => {})
|
|
79
|
+
try {
|
|
80
|
+
return await withCrossProcessMemoryLock(root, fn)
|
|
81
|
+
} finally {
|
|
82
|
+
release()
|
|
83
|
+
if (MEMORY_WRITE_TAILS.get(key) === tail) MEMORY_WRITE_TAILS.delete(key)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
19
87
|
function now() {
|
|
20
88
|
return new Date().toISOString()
|
|
21
89
|
}
|
|
@@ -118,55 +186,58 @@ export async function proposeMemory(root, input = {}) {
|
|
|
118
186
|
if (!content) throw new Error("memory content is required")
|
|
119
187
|
const type = VALID_TYPES.has(input.type) ? input.type : "episodic"
|
|
120
188
|
const scope = VALID_SCOPES.has(input.scope) ? input.scope : "project"
|
|
121
|
-
const state = await readMemoryState(root)
|
|
122
189
|
const id = idFor({ ...input, type, scope, content })
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
state
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
...
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
190
|
+
|
|
191
|
+
return withMemoryWriteLock(root, async () => {
|
|
192
|
+
const state = await readMemoryState(root)
|
|
193
|
+
const timestamp = now()
|
|
194
|
+
const existing = state.memories.find((item) => item.id === id) ||
|
|
195
|
+
state.memories.find((item) => sameMemoryIdentity(item, { ...input, type, scope, content }))
|
|
196
|
+
if (existing) {
|
|
197
|
+
const reinforced = {
|
|
198
|
+
...existing,
|
|
199
|
+
lastSeenAt: timestamp,
|
|
200
|
+
reinforcementCount: Number(existing.reinforcementCount || 1) + 1,
|
|
201
|
+
evidenceRefs: uniq([...(existing.evidenceRefs || []), ...(input.evidenceRefs || [])]),
|
|
202
|
+
files: uniq([...(existing.files || []), ...(input.files || []).map(normalizeFile)]),
|
|
203
|
+
dependencies: uniq([...(existing.dependencies || []), ...(input.dependencies || []).map(normalizeFile)]),
|
|
204
|
+
tags: uniq([...(existing.tags || []), ...(input.tags || [])]),
|
|
205
|
+
confidence: Math.max(clamp(existing.confidence), clamp(input.confidence, existing.confidence ?? 0.7)),
|
|
206
|
+
taskClass: input.taskClass || existing.taskClass || null,
|
|
207
|
+
expiresAt: input.expiresAt || existing.expiresAt || null,
|
|
208
|
+
}
|
|
209
|
+
await writeMemoryState(root, state.memories.map((item) => item.id === existing.id ? reinforced : item))
|
|
210
|
+
return reinforced
|
|
138
211
|
}
|
|
139
|
-
await writeMemoryState(root, state.memories.map((item) => item.id === id ? reinforced : item))
|
|
140
|
-
return reinforced
|
|
141
|
-
}
|
|
142
212
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
const memory = {
|
|
214
|
+
id,
|
|
215
|
+
key: input.key || null,
|
|
216
|
+
type,
|
|
217
|
+
scope,
|
|
218
|
+
content,
|
|
219
|
+
status: "candidate",
|
|
220
|
+
confidence: clamp(input.confidence),
|
|
221
|
+
evidenceRefs: uniq(input.evidenceRefs || []),
|
|
222
|
+
files: uniq((input.files || []).map(normalizeFile)),
|
|
223
|
+
dependencies: uniq((input.dependencies || []).map(normalizeFile)),
|
|
224
|
+
tags: uniq(input.tags || []),
|
|
225
|
+
sourceTask: input.sourceTask || null,
|
|
226
|
+
sourceCommit: input.sourceCommit || null,
|
|
227
|
+
taskClass: input.taskClass || null,
|
|
228
|
+
expiresAt: input.expiresAt || null,
|
|
229
|
+
createdAt: timestamp,
|
|
230
|
+
lastSeenAt: timestamp,
|
|
231
|
+
lastUsedAt: null,
|
|
232
|
+
useCount: 0,
|
|
233
|
+
reinforcementCount: 1,
|
|
234
|
+
verifiedAt: null,
|
|
235
|
+
verifier: null,
|
|
236
|
+
supersededBy: null,
|
|
237
|
+
}
|
|
238
|
+
await writeMemoryState(root, [...state.memories, memory])
|
|
239
|
+
return memory
|
|
240
|
+
})
|
|
170
241
|
}
|
|
171
242
|
|
|
172
243
|
export async function verifyMemory(root, id, verification = {}) {
|
|
@@ -174,47 +245,52 @@ export async function verifyMemory(root, id, verification = {}) {
|
|
|
174
245
|
throw new Error("memory verification requires verdict PASS")
|
|
175
246
|
}
|
|
176
247
|
if (!verification.verifier) throw new Error("memory verification requires an independent verifier identity")
|
|
177
|
-
const state = await readMemoryState(root)
|
|
178
|
-
const current = state.memories.find((item) => item.id === id)
|
|
179
|
-
if (!current) throw new Error("unknown memory: " + id)
|
|
180
|
-
if (current.status === "superseded") throw new Error("cannot verify a superseded memory")
|
|
181
|
-
|
|
182
|
-
const refs = uniq([...(current.evidenceRefs || []), ...(verification.evidenceRefs || [])])
|
|
183
|
-
if (!refs.length) throw new Error("memory verification requires at least one durable evidence reference")
|
|
184
|
-
for (const ref of refs) {
|
|
185
|
-
if (!(await evidenceExists(root, ref))) throw new Error("memory evidence reference is missing: " + ref)
|
|
186
|
-
}
|
|
187
248
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
249
|
+
return withMemoryWriteLock(root, async () => {
|
|
250
|
+
const state = await readMemoryState(root)
|
|
251
|
+
const current = state.memories.find((item) => item.id === id)
|
|
252
|
+
if (!current) throw new Error("unknown memory: " + id)
|
|
253
|
+
if (current.status === "superseded") throw new Error("cannot verify a superseded memory")
|
|
254
|
+
|
|
255
|
+
const refs = uniq([...(current.evidenceRefs || []), ...(verification.evidenceRefs || [])])
|
|
256
|
+
if (!refs.length) throw new Error("memory verification requires at least one durable evidence reference")
|
|
257
|
+
for (const ref of refs) {
|
|
258
|
+
if (!(await evidenceExists(root, ref))) throw new Error("memory evidence reference is missing: " + ref)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const verified = {
|
|
262
|
+
...current,
|
|
263
|
+
status: "verified",
|
|
264
|
+
evidenceRefs: refs,
|
|
265
|
+
verifier: String(verification.verifier),
|
|
266
|
+
verifiedAt: now(),
|
|
267
|
+
lastSeenAt: now(),
|
|
268
|
+
confidence: Math.max(clamp(current.confidence), clamp(verification.confidence, current.confidence ?? 0.7)),
|
|
269
|
+
}
|
|
270
|
+
await writeMemoryState(root, state.memories.map((item) => item.id === id ? verified : item))
|
|
271
|
+
return verified
|
|
272
|
+
})
|
|
199
273
|
}
|
|
200
274
|
|
|
201
275
|
export async function supersedeMemory(root, id, replacementId) {
|
|
202
276
|
if (!replacementId || replacementId === id) throw new Error("replacement memory id must be different")
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
277
|
+
return withMemoryWriteLock(root, async () => {
|
|
278
|
+
const state = await readMemoryState(root)
|
|
279
|
+
const current = state.memories.find((item) => item.id === id)
|
|
280
|
+
const replacement = state.memories.find((item) => item.id === replacementId)
|
|
281
|
+
if (!current) throw new Error("unknown memory: " + id)
|
|
282
|
+
if (!replacement) throw new Error("unknown replacement memory: " + replacementId)
|
|
283
|
+
if (replacement.status !== "verified") throw new Error("replacement memory must be verified before supersession")
|
|
284
|
+
const timestamp = now()
|
|
285
|
+
const superseded = {
|
|
286
|
+
...current,
|
|
287
|
+
status: "superseded",
|
|
288
|
+
supersededBy: replacementId,
|
|
289
|
+
lastSeenAt: timestamp,
|
|
290
|
+
}
|
|
291
|
+
await writeMemoryState(root, state.memories.map((item) => item.id === id ? superseded : item))
|
|
292
|
+
return superseded
|
|
293
|
+
})
|
|
218
294
|
}
|
|
219
295
|
|
|
220
296
|
function tokenVector(value, dims = 96) {
|
|
@@ -550,9 +626,12 @@ export async function retrieveMemories(root, query, options = {}) {
|
|
|
550
626
|
if (options.touch === true && results.length) {
|
|
551
627
|
const used = new Set(results.map((item) => item.id))
|
|
552
628
|
const timestamp = now()
|
|
553
|
-
await
|
|
554
|
-
|
|
555
|
-
|
|
629
|
+
await withMemoryWriteLock(root, async () => {
|
|
630
|
+
const latest = await readMemoryState(root)
|
|
631
|
+
await writeMemoryState(root, latest.memories.map((item) => used.has(item.id)
|
|
632
|
+
? { ...item, lastUsedAt: timestamp, useCount: Number(item.useCount || 0) + 1 }
|
|
633
|
+
: item))
|
|
634
|
+
})
|
|
556
635
|
}
|
|
557
636
|
return {
|
|
558
637
|
schemaVersion: SCHEMA_VERSION,
|
|
@@ -594,21 +673,24 @@ export async function memoryStatus(root = process.cwd()) {
|
|
|
594
673
|
}
|
|
595
674
|
|
|
596
675
|
function changedFiles(root) {
|
|
597
|
-
const result = spawnSync("git", [
|
|
676
|
+
const result = spawnSync("git", [
|
|
677
|
+
"status",
|
|
678
|
+
"--porcelain=v1",
|
|
679
|
+
"-z",
|
|
680
|
+
"--untracked-files=all",
|
|
681
|
+
"--",
|
|
682
|
+
...sourceGitPathspecs(),
|
|
683
|
+
], {
|
|
598
684
|
cwd: path.resolve(root),
|
|
599
685
|
encoding: "utf8",
|
|
600
686
|
windowsHide: true,
|
|
601
687
|
maxBuffer: 4 * 1024 * 1024,
|
|
602
688
|
})
|
|
603
689
|
if (result.status !== 0) return []
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
.map((line) => line.slice(3).trim())
|
|
609
|
-
.map((value) => value.includes(" -> ") ? value.split(" -> ").at(-1) : value)
|
|
610
|
-
.map(normalizeFile),
|
|
611
|
-
).slice(0, 80)
|
|
690
|
+
const changed = workspaceStatusEntries(String(result.stdout || ""))
|
|
691
|
+
.filter((entry) => entry.renameSource !== true)
|
|
692
|
+
.map((entry) => normalizeFile(entry.file))
|
|
693
|
+
return uniq(sourceFacingPaths(changed)).slice(0, 80)
|
|
612
694
|
}
|
|
613
695
|
|
|
614
696
|
function compactVerification(value, limit = 1200) {
|