@cairnkeep/cli 2.3.1 → 2.4.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +221 -6
  3. package/bin/cairn +43 -2
  4. package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
  5. package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
  6. package/claude/hooks/compaction-capture.sh +34 -0
  7. package/claude/hooks/memory-capture.sh +47 -5
  8. package/claude/hooks/memory-wakeup.sh +55 -2
  9. package/docs/building-an-overlay.md +1 -1
  10. package/docs/design/meta-agent.md +439 -0
  11. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  12. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  13. package/docs/learning/lessons/L02-installation.md +2 -2
  14. package/docs/learning/lessons/L03-first-project.md +1 -1
  15. package/docs/operating.md +531 -4
  16. package/docs/privacy-and-data-flow.md +417 -0
  17. package/docs/storage.md +252 -0
  18. package/examples/eval/README.md +42 -0
  19. package/examples/eval/adapter.json +12 -0
  20. package/examples/eval/bundled-fake.json +1 -0
  21. package/examples/eval/task-set.json +1 -0
  22. package/mcp-memory-server/dist/artifact-cli.js +184 -0
  23. package/mcp-memory-server/dist/artifact-schema.js +218 -0
  24. package/mcp-memory-server/dist/artifact-store.js +812 -0
  25. package/mcp-memory-server/dist/capability-adapter.js +286 -0
  26. package/mcp-memory-server/dist/capability-cli.js +361 -0
  27. package/mcp-memory-server/dist/capability-config.js +310 -0
  28. package/mcp-memory-server/dist/capability-harness.js +496 -0
  29. package/mcp-memory-server/dist/capability-registry.js +123 -0
  30. package/mcp-memory-server/dist/capability-schema.js +65 -0
  31. package/mcp-memory-server/dist/capability-store.js +431 -0
  32. package/mcp-memory-server/dist/compaction-normalize.js +267 -0
  33. package/mcp-memory-server/dist/eval-cli.js +523 -0
  34. package/mcp-memory-server/dist/eval-plan.js +346 -0
  35. package/mcp-memory-server/dist/eval-process.js +273 -0
  36. package/mcp-memory-server/dist/eval-report.js +609 -0
  37. package/mcp-memory-server/dist/eval-runner.js +827 -0
  38. package/mcp-memory-server/dist/eval-schema.js +368 -0
  39. package/mcp-memory-server/dist/eval-statistics.js +161 -0
  40. package/mcp-memory-server/dist/eval-workspace.js +313 -0
  41. package/mcp-memory-server/dist/failure-signature.js +141 -0
  42. package/mcp-memory-server/dist/index.js +600 -55
  43. package/mcp-memory-server/dist/node-cli.js +84 -0
  44. package/mcp-memory-server/dist/node-schema.js +170 -0
  45. package/mcp-memory-server/dist/node-store.js +455 -0
  46. package/mcp-memory-server/dist/note-cli.js +235 -0
  47. package/mcp-memory-server/dist/note-distiller.js +177 -0
  48. package/mcp-memory-server/dist/note-enrichment.js +93 -0
  49. package/mcp-memory-server/dist/note-schema.js +4 -0
  50. package/mcp-memory-server/dist/note-store.js +985 -0
  51. package/mcp-memory-server/dist/trajectory-cli.js +141 -0
  52. package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
  53. package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
  54. package/mcp-memory-server/dist/trajectory-schema.js +73 -0
  55. package/mcp-memory-server/dist/trajectory-store.js +339 -0
  56. package/mcp-memory-server/package.json +24 -2
  57. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
  58. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
  59. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
  60. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
  61. package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
  62. package/opencode/capability-contract/plugins/capability-command.ts +319 -0
  63. package/opencode/command/graphify.md +25 -14
  64. package/opencode/plugins/memory-capture.ts +76 -10
  65. package/opencode/plugins/memory-wakeup.ts +58 -6
  66. package/package.json +8 -4
  67. package/pi/extensions/cairnkeep-trajectory.ts +55 -0
  68. package/schemas/artifact.schema.json +133 -0
  69. package/schemas/capability-callback.schema.json +68 -0
  70. package/schemas/capability-contract.schema.json +126 -0
  71. package/schemas/eval-adapter.schema.json +43 -0
  72. package/schemas/eval-protocol.schema.json +114 -0
  73. package/schemas/eval-report.schema.json +423 -0
  74. package/schemas/eval-task-set.schema.json +114 -0
  75. package/schemas/memory-node.schema.json +90 -0
  76. package/schemas/note.schema.json +112 -0
  77. package/schemas/trajectory-redaction.schema.json +25 -0
  78. package/schemas/trajectory.schema.json +82 -0
  79. package/scripts/bootstrap.sh +4 -0
  80. package/scripts/completion.sh +119 -5
  81. package/scripts/doctor.sh +284 -0
  82. package/scripts/fake-eval-adapter.mjs +120 -0
  83. package/scripts/fixtures/capability-harness-contracts.json +408 -0
  84. package/scripts/install-audit-timer.sh +3 -1
  85. package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
  86. package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
  87. package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
  88. package/scripts/memory-wiki-audit.sh +49 -0
  89. package/scripts/smoke-bash32.sh +9 -0
  90. package/scripts/sync-claude-assets.sh +99 -6
  91. package/scripts/sync-opencode-graphify-assets.sh +24 -3
  92. package/scripts/sync-opencode-plugin-assets.sh +31 -5
  93. package/scripts/sync-opencode-security-assets.sh +24 -3
  94. package/scripts/sync-opencode-wiki-assets.sh +24 -3
  95. package/scripts/sync-pi-assets.sh +72 -0
  96. package/scripts/test-audit-timer.sh +98 -0
  97. package/scripts/test-bootstrap-untracked.sh +48 -0
  98. package/scripts/test-cli-dispatch.sh +41 -2
  99. package/scripts/test-compaction-hooks.sh +495 -0
  100. package/scripts/test-completion.sh +77 -0
  101. package/scripts/test-doctor.sh +348 -1
  102. package/scripts/test-eval-cli.sh +175 -0
  103. package/scripts/test-launcher-hooks.sh +602 -3
  104. package/scripts/test-notes-cli.sh +114 -0
  105. package/scripts/test-package-install.sh +251 -0
  106. package/scripts/test-phase16-node-lifecycle.sh +50 -0
  107. package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
  108. package/scripts/test-phase18-capability-lifecycle.sh +504 -0
  109. package/scripts/test-phase18-harness-boundary.sh +1060 -0
  110. package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
  111. package/scripts/test-trajectory-cli.sh +87 -0
  112. package/scripts/test-trajectory-hooks.sh +142 -0
  113. package/scripts/test-uninstall.sh +231 -5
  114. package/scripts/uninstall.sh +37 -5
  115. package/scripts/verify-dependency-security.mjs +121 -0
  116. package/scripts/verify-docs-parity.sh +527 -1
  117. package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
  118. package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
  119. package/templates/capabilities.json.template +7 -0
  120. package/templates/env.example.template +51 -0
  121. package/templates/memory-wiki-audit.service.template +1 -1
  122. package/templates/start-claude.sh.template +36 -0
  123. package/templates/start-opencode.sh.template +47 -5
  124. package/templates/start-pi.sh.template +30 -0
  125. package/templates/trajectory-redaction.json.template +4 -0
@@ -0,0 +1,812 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
3
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
4
+ import { AgentFS } from "agentfs-sdk";
5
+ import { ARTIFACT_SCHEMA_VERSION, artifactEnvelopeSchema, artifactWriteInputSchema, canonicalBytes, canonicalJson, getArtifactLimits, sha256Hex, truncateUtf8, } from "./artifact-schema.js";
6
+ import { redactLocalValue } from "./trajectory-redaction.js";
7
+ const META_KEY = "artifact/meta/schema-version";
8
+ const FULL_PREFIX = "artifact/full/";
9
+ const INDEX_PREFIX = "artifact/index/";
10
+ const DEDUPE_PREFIX = "artifact/index/dedupe/";
11
+ const SESSION_LATEST_PREFIX = "compaction/latest/session/";
12
+ const PROJECT_LATEST_KEY = "compaction/latest/project";
13
+ const SEQUENCE_PREFIX = "compaction/sequence/";
14
+ const DIAGNOSTIC_PREFIX = "artifact/meta/unsupported-adapter/";
15
+ const MAX_DIAGNOSTICS = 32;
16
+ const mutationQueues = new Map();
17
+ export function getArtifactDbPath(projectRoot = process.cwd()) {
18
+ return resolve(projectRoot, ".agentfs", "artifacts.db");
19
+ }
20
+ export function resolveArtifactDbPath(projectRoot = process.cwd()) {
21
+ return getArtifactDbPath(projectRoot);
22
+ }
23
+ export function resolveRemoteArtifactProjectRoot(baseDirectory, projectId) {
24
+ if (!/^[a-z0-9][a-z0-9._-]{0,127}$/i.test(projectId)) {
25
+ throw new Error("Remote artifact project identity is invalid.");
26
+ }
27
+ const base = resolve(baseDirectory);
28
+ const candidate = resolve(base, projectId);
29
+ const rel = relative(base, candidate);
30
+ if (rel.startsWith("..") || isAbsolute(rel))
31
+ throw new Error("Remote artifact path escapes its configured base directory.");
32
+ return candidate;
33
+ }
34
+ export function resolveRemoteArtifactDbPath(baseDirectory, projectId) {
35
+ return getArtifactDbPath(resolveRemoteArtifactProjectRoot(baseDirectory, projectId));
36
+ }
37
+ async function openArtifactStore(projectRoot, create) {
38
+ const path = getArtifactDbPath(projectRoot);
39
+ if (!create && !existsSync(path))
40
+ return null;
41
+ if (create)
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ const agent = await AgentFS.open({ id: "artifacts", path });
44
+ if (create) {
45
+ try {
46
+ chmodSync(path, 0o600);
47
+ }
48
+ catch (error) {
49
+ await agent.close();
50
+ throw error;
51
+ }
52
+ }
53
+ return agent;
54
+ }
55
+ async function inImmediateTransaction(agent, operation) {
56
+ const transaction = agent.getDatabase().transaction(operation);
57
+ const immediate = transaction.immediate;
58
+ return immediate();
59
+ }
60
+ async function withMutationLock(projectRoot, operation) {
61
+ const path = getArtifactDbPath(projectRoot);
62
+ const prior = mutationQueues.get(path) ?? Promise.resolve();
63
+ let release;
64
+ const current = new Promise((resolvePromise) => { release = resolvePromise; });
65
+ const queued = prior.then(() => current);
66
+ mutationQueues.set(path, queued);
67
+ await prior;
68
+ try {
69
+ return await operation();
70
+ }
71
+ finally {
72
+ release();
73
+ if (mutationQueues.get(path) === queued)
74
+ mutationQueues.delete(path);
75
+ }
76
+ }
77
+ function fullKey(id) {
78
+ return `${FULL_PREFIX}${id}`;
79
+ }
80
+ function paddedEpoch(createdAt) {
81
+ const epoch = Date.parse(createdAt);
82
+ if (!Number.isFinite(epoch))
83
+ throw new Error("Artifact creation timestamp is invalid.");
84
+ return String(epoch).padStart(16, "0");
85
+ }
86
+ function indexEntries(artifact) {
87
+ const epoch = paddedEpoch(artifact.created_at);
88
+ const index = {
89
+ schema_version: ARTIFACT_SCHEMA_VERSION,
90
+ artifact_id: artifact.artifact_id,
91
+ kind: artifact.kind,
92
+ created_at: artifact.created_at,
93
+ session_ref: artifact.session_ref,
94
+ ...(artifact.node_ref ? { node_ref: artifact.node_ref } : {}),
95
+ logical_bytes: artifact.logical_bytes,
96
+ content_digest: artifact.content_digest,
97
+ full_key: fullKey(artifact.artifact_id),
98
+ };
99
+ return [
100
+ [`artifact/index/created/${epoch}/${artifact.artifact_id}`, index],
101
+ [`artifact/index/session/${artifact.session_ref}/${epoch}/${artifact.artifact_id}`, index],
102
+ [`artifact/index/kind/${artifact.kind}/${epoch}/${artifact.artifact_id}`, index],
103
+ ];
104
+ }
105
+ async function assertCompatibleSchema(agent, allowMissing) {
106
+ const version = await agent.kv.get(META_KEY);
107
+ if (version === undefined && allowMissing)
108
+ return;
109
+ if (version !== ARTIFACT_SCHEMA_VERSION) {
110
+ throw new Error("Artifact store schema is missing or unsupported; run `cairn artifact doctor`.");
111
+ }
112
+ }
113
+ async function readFullArtifacts(agent) {
114
+ const rows = await agent.kv.list(FULL_PREFIX);
115
+ return rows.map((row) => artifactEnvelopeSchema.parse(row.value));
116
+ }
117
+ function sortedOldest(artifacts) {
118
+ return [...artifacts].sort((left, right) => {
119
+ const time = Date.parse(left.created_at) - Date.parse(right.created_at);
120
+ return time !== 0 ? time : left.artifact_id.localeCompare(right.artifact_id);
121
+ });
122
+ }
123
+ function contentWithTruncation(input, limits) {
124
+ let content = structuredClone(input.content);
125
+ const logicalBytes = canonicalBytes(content).byteLength;
126
+ let metadataOnly = false;
127
+ if (input.kind === "generated_file") {
128
+ const snapshot = typeof content.snapshot === "string" ? content.snapshot : undefined;
129
+ if (content.binary === true
130
+ || Number(content.logical_bytes) > limits.generatedFileSnapshotMaxBytes
131
+ || (snapshot !== undefined && Buffer.byteLength(snapshot, "utf8") > limits.generatedFileSnapshotMaxBytes)) {
132
+ delete content.snapshot;
133
+ content.metadata_only = true;
134
+ metadataOnly = true;
135
+ }
136
+ }
137
+ const textSlots = [];
138
+ for (const key of ["text", "raw_summary", "snapshot"]) {
139
+ if (typeof content[key] === "string") {
140
+ textSlots.push({ get: () => String(content[key]), set: (value) => { content[key] = value; } });
141
+ }
142
+ }
143
+ for (const key of ["task_goals", "decisions_made", "open_todos", "critical_error_traces"]) {
144
+ const entries = content[key];
145
+ if (!Array.isArray(entries))
146
+ continue;
147
+ entries.forEach((_, index) => textSlots.push({
148
+ get: () => String(content[key][index] ?? ""),
149
+ set: (value) => { content[key][index] = value; },
150
+ }));
151
+ }
152
+ let storedBytes = canonicalBytes(content).byteLength;
153
+ for (const slot of textSlots) {
154
+ if (storedBytes <= limits.artifactMaxBytes)
155
+ break;
156
+ const overage = storedBytes - limits.artifactMaxBytes;
157
+ const current = slot.get();
158
+ const currentBytes = Buffer.byteLength(current, "utf8");
159
+ slot.set(truncateUtf8(current, Math.max(0, currentBytes - overage)).text);
160
+ storedBytes = canonicalBytes(content).byteLength;
161
+ }
162
+ if (storedBytes > limits.artifactMaxBytes) {
163
+ throw new Error("Artifact metadata exceeds the configured per-artifact byte limit.");
164
+ }
165
+ return {
166
+ content,
167
+ logicalBytes,
168
+ storedBytes,
169
+ truncated: metadataOnly || storedBytes < logicalBytes,
170
+ ...(metadataOnly ? { reason: "generated_file_metadata_only" }
171
+ : storedBytes < logicalBytes ? { reason: "artifact_limit" } : {}),
172
+ };
173
+ }
174
+ function requestDigestFor(input) {
175
+ return sha256Hex(canonicalBytes({
176
+ schema_version: ARTIFACT_SCHEMA_VERSION,
177
+ kind: input.kind,
178
+ session_ref: input.session_ref,
179
+ ...(input.node_ref ? { node_ref: input.node_ref } : {}),
180
+ media_type: input.media_type,
181
+ provenance: input.provenance,
182
+ ...(input.supersedes ? { supersedes: input.supersedes } : {}),
183
+ content: input.content,
184
+ }));
185
+ }
186
+ function prepareInput(projectRoot, candidate, limits) {
187
+ let redacted;
188
+ try {
189
+ redacted = redactLocalValue(candidate, projectRoot);
190
+ }
191
+ catch {
192
+ throw new Error("Artifact candidate failed privacy validation.");
193
+ }
194
+ let input;
195
+ try {
196
+ input = artifactWriteInputSchema.parse(redacted.value);
197
+ }
198
+ catch {
199
+ throw new Error("Artifact input is invalid.");
200
+ }
201
+ if (input.session_ref === "unknown")
202
+ throw new Error("Artifact session reference must not be unknown.");
203
+ if (input.node_ref?.key.toLowerCase().split(/[\/_-]/).includes("secret")) {
204
+ throw new Error("Artifact node reference is invalid.");
205
+ }
206
+ const prepared = contentWithTruncation(input, limits);
207
+ const requestDigest = requestDigestFor({
208
+ kind: input.kind,
209
+ session_ref: input.session_ref,
210
+ ...(input.node_ref ? { node_ref: input.node_ref } : {}),
211
+ media_type: input.media_type,
212
+ provenance: input.provenance,
213
+ ...(input.supersedes ? { supersedes: input.supersedes } : {}),
214
+ content: prepared.content,
215
+ });
216
+ return {
217
+ input,
218
+ content: prepared.content,
219
+ logicalBytes: prepared.logicalBytes,
220
+ storedBytes: prepared.storedBytes,
221
+ ...(prepared.reason ? { truncationReason: prepared.reason } : {}),
222
+ redaction: { applied: redacted.applied, replacement_count: redacted.replacement_count },
223
+ requestDigest,
224
+ };
225
+ }
226
+ async function findByIdentifier(agent, identifier) {
227
+ const rows = await readFullArtifacts(agent);
228
+ const matches = rows.filter((artifact) => artifact.artifact_id === identifier || artifact.artifact_id.startsWith(identifier));
229
+ if (matches.length === 0)
230
+ throw new Error("Artifact not found.");
231
+ const exact = matches.find((artifact) => artifact.artifact_id === identifier);
232
+ if (!exact && matches.length > 1)
233
+ throw new Error("Artifact ID prefix is ambiguous.");
234
+ return exact ?? matches[0];
235
+ }
236
+ function newestCompaction(artifacts, sessionRef) {
237
+ const candidates = artifacts.filter((artifact) => artifact.kind === "compaction_summary"
238
+ && (!sessionRef || artifact.session_ref === sessionRef));
239
+ return sortedOldest(candidates).at(-1) ?? null;
240
+ }
241
+ async function writeDerivedRows(agent, artifact, requestDigest) {
242
+ await agent.kv.set(fullKey(artifact.artifact_id), artifact);
243
+ for (const [key, value] of indexEntries(artifact))
244
+ await agent.kv.set(key, value);
245
+ await agent.kv.set(`${DEDUPE_PREFIX}${requestDigest}`, artifact.artifact_id);
246
+ if (artifact.kind === "compaction_summary") {
247
+ await agent.kv.set(`${SESSION_LATEST_PREFIX}${artifact.session_ref}`, artifact.artifact_id);
248
+ await agent.kv.set(PROJECT_LATEST_KEY, artifact.artifact_id);
249
+ }
250
+ }
251
+ async function deleteArtifactRows(agent, artifact) {
252
+ const rows = await agent.kv.list("");
253
+ for (const row of rows) {
254
+ const containsId = row.key === fullKey(artifact.artifact_id)
255
+ || row.key.endsWith(`/${artifact.artifact_id}`)
256
+ || row.value === artifact.artifact_id
257
+ || canonicalJson(row.value).includes(artifact.artifact_id);
258
+ if (containsId)
259
+ await agent.kv.delete(row.key);
260
+ }
261
+ }
262
+ async function rebuildPointers(agent) {
263
+ for (const row of await agent.kv.list("compaction/latest/"))
264
+ await agent.kv.delete(row.key);
265
+ const artifacts = sortedOldest(await readFullArtifacts(agent));
266
+ const bySession = new Map();
267
+ for (const artifact of artifacts)
268
+ if (artifact.kind === "compaction_summary")
269
+ bySession.set(artifact.session_ref, artifact);
270
+ for (const [session, artifact] of bySession)
271
+ await agent.kv.set(`${SESSION_LATEST_PREFIX}${session}`, artifact.artifact_id);
272
+ const latest = newestCompaction(artifacts);
273
+ if (latest)
274
+ await agent.kv.set(PROJECT_LATEST_KEY, latest.artifact_id);
275
+ }
276
+ async function rewriteArtifactDatabase(projectRoot, rows) {
277
+ const dbPath = getArtifactDbPath(projectRoot);
278
+ const rewritePath = `${dbPath}.rewrite-${randomUUID()}`;
279
+ const rewrite = await AgentFS.open({ id: "artifacts", path: rewritePath });
280
+ try {
281
+ await inImmediateTransaction(rewrite, async () => {
282
+ for (const row of rows)
283
+ await rewrite.kv.set(row.key, row.value);
284
+ });
285
+ await rewrite.getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE)");
286
+ }
287
+ finally {
288
+ await rewrite.close();
289
+ }
290
+ chmodSync(rewritePath, 0o600);
291
+ for (const suffix of ["-wal", "-shm"]) {
292
+ rmSync(`${rewritePath}${suffix}`, { force: true });
293
+ rmSync(`${dbPath}${suffix}`, { force: true });
294
+ }
295
+ renameSync(rewritePath, dbPath);
296
+ }
297
+ function planPrune(artifacts, limits, options) {
298
+ const sorted = sortedOldest(artifacts);
299
+ const protectedId = options.includeProtected ? undefined : newestCompaction(sorted)?.artifact_id;
300
+ const removed = new Map();
301
+ const mark = (artifact, reason) => {
302
+ if (artifact.artifact_id === protectedId || removed.has(artifact.artifact_id))
303
+ return;
304
+ const index = indexEntries(artifact)[0][1];
305
+ removed.set(artifact.artifact_id, { ...index, reason });
306
+ };
307
+ if (limits.retentionDays >= 0) {
308
+ const cutoff = options.now.getTime() - limits.retentionDays * 86400000;
309
+ for (const artifact of sorted)
310
+ if (Date.parse(artifact.created_at) < cutoff)
311
+ mark(artifact, "age");
312
+ }
313
+ const sessions = new Map();
314
+ for (const artifact of sorted.filter((item) => item.kind === "compaction_summary" && !removed.has(item.artifact_id))) {
315
+ const entries = sessions.get(artifact.session_ref) ?? [];
316
+ entries.push(artifact);
317
+ sessions.set(artifact.session_ref, entries);
318
+ }
319
+ for (const entries of sessions.values()) {
320
+ const excess = Math.max(0, entries.length - limits.compactionMaxRevisions);
321
+ entries.slice(0, excess).forEach((artifact) => mark(artifact, "revision"));
322
+ }
323
+ for (const session of new Set(sorted.map((artifact) => artifact.session_ref))) {
324
+ const retained = sorted.filter((artifact) => artifact.session_ref === session && !removed.has(artifact.artifact_id));
325
+ let total = retained.reduce((sum, artifact) => sum + artifact.logical_bytes, 0);
326
+ for (const artifact of retained) {
327
+ if (total <= limits.sessionMaxBytes)
328
+ break;
329
+ if (artifact.artifact_id === protectedId)
330
+ continue;
331
+ mark(artifact, "session_budget");
332
+ total -= artifact.logical_bytes;
333
+ }
334
+ }
335
+ const retained = sorted.filter((artifact) => !removed.has(artifact.artifact_id));
336
+ let total = retained.reduce((sum, artifact) => sum + artifact.logical_bytes, 0);
337
+ for (const artifact of retained) {
338
+ if (total <= limits.storeMaxBytes)
339
+ break;
340
+ if (artifact.artifact_id === protectedId)
341
+ continue;
342
+ mark(artifact, "store_budget");
343
+ total -= artifact.logical_bytes;
344
+ }
345
+ return sorted.filter((artifact) => removed.has(artifact.artifact_id)).map((artifact) => removed.get(artifact.artifact_id));
346
+ }
347
+ async function pruneInTransaction(agent, limits, options) {
348
+ const artifacts = await readFullArtifacts(agent);
349
+ const removed = planPrune(artifacts, limits, options);
350
+ if (!options.dryRun) {
351
+ const byId = new Map(artifacts.map((artifact) => [artifact.artifact_id, artifact]));
352
+ for (const item of removed) {
353
+ const artifact = byId.get(item.artifact_id);
354
+ if (artifact)
355
+ await deleteArtifactRows(agent, artifact);
356
+ }
357
+ await rebuildPointers(agent);
358
+ }
359
+ const removedIds = new Set(removed.map((item) => item.artifact_id));
360
+ const retained = artifacts.filter((artifact) => !removedIds.has(artifact.artifact_id));
361
+ return {
362
+ schema_version: ARTIFACT_SCHEMA_VERSION,
363
+ dry_run: options.dryRun,
364
+ removed,
365
+ remaining_artifacts: retained.length,
366
+ logical_bytes: retained.reduce((sum, artifact) => sum + artifact.logical_bytes, 0),
367
+ };
368
+ }
369
+ export async function putArtifact(projectRoot, candidate, limits = getArtifactLimits(), options = {}) {
370
+ const prepared = prepareInput(projectRoot, candidate, limits);
371
+ return withMutationLock(projectRoot, async () => {
372
+ const agent = await openArtifactStore(projectRoot, true);
373
+ if (!agent)
374
+ throw new Error("Unable to open the local artifact store.");
375
+ try {
376
+ return await inImmediateTransaction(agent, async () => {
377
+ await assertCompatibleSchema(agent, true);
378
+ const priorId = await agent.kv.get(`${DEDUPE_PREFIX}${prepared.requestDigest}`);
379
+ if (priorId) {
380
+ const prior = await agent.kv.get(fullKey(priorId));
381
+ if (prior !== undefined) {
382
+ return { schema_version: ARTIFACT_SCHEMA_VERSION, artifact: artifactEnvelopeSchema.parse(prior), idempotent: true };
383
+ }
384
+ }
385
+ if (prepared.input.supersedes) {
386
+ const superseded = await findByIdentifier(agent, prepared.input.supersedes);
387
+ if (superseded.session_ref !== prepared.input.session_ref)
388
+ throw new Error("Superseded artifact must belong to the same session.");
389
+ if (superseded.kind !== prepared.input.kind)
390
+ throw new Error("Superseded artifact must have the same kind.");
391
+ }
392
+ let content = prepared.content;
393
+ if (prepared.input.kind === "compaction_summary") {
394
+ const sequenceKey = `${SEQUENCE_PREFIX}${prepared.input.session_ref}`;
395
+ const revision = (await agent.kv.get(sequenceKey) ?? 0) + 1;
396
+ await agent.kv.set(sequenceKey, revision);
397
+ content = { ...content, revision };
398
+ }
399
+ const storedBytes = canonicalBytes(content).byteLength;
400
+ const artifact = artifactEnvelopeSchema.parse({
401
+ schema_version: ARTIFACT_SCHEMA_VERSION,
402
+ artifact_id: `art_${randomUUID()}`,
403
+ kind: prepared.input.kind,
404
+ created_at: (options.now ?? new Date()).toISOString(),
405
+ session_ref: prepared.input.session_ref,
406
+ ...(prepared.input.node_ref ? { node_ref: prepared.input.node_ref } : {}),
407
+ media_type: prepared.input.media_type,
408
+ logical_bytes: Math.max(prepared.logicalBytes, storedBytes),
409
+ stored_bytes: storedBytes,
410
+ content_digest: sha256Hex(canonicalBytes(content)),
411
+ provenance: prepared.input.provenance,
412
+ redaction: prepared.redaction,
413
+ truncation: {
414
+ truncated: Boolean(prepared.truncationReason),
415
+ ...(prepared.truncationReason ? { reason: prepared.truncationReason } : {}),
416
+ original_bytes: Math.max(prepared.logicalBytes, storedBytes),
417
+ stored_bytes: storedBytes,
418
+ },
419
+ ...(prepared.input.supersedes ? { supersedes: prepared.input.supersedes } : {}),
420
+ content,
421
+ });
422
+ await agent.kv.set(META_KEY, ARTIFACT_SCHEMA_VERSION);
423
+ await writeDerivedRows(agent, artifact, prepared.requestDigest);
424
+ if (options.fault === "after-full-write")
425
+ throw new Error("Injected artifact transaction fault.");
426
+ await pruneInTransaction(agent, limits, {
427
+ dryRun: false,
428
+ includeProtected: false,
429
+ now: options.now ?? new Date(),
430
+ });
431
+ return { schema_version: ARTIFACT_SCHEMA_VERSION, artifact, idempotent: false };
432
+ });
433
+ }
434
+ finally {
435
+ await agent.close();
436
+ }
437
+ });
438
+ }
439
+ export async function putCompactionArtifact(projectRoot, candidate, limits = getArtifactLimits(), options = {}) {
440
+ const parsed = artifactWriteInputSchema.parse(candidate);
441
+ if (parsed.kind !== "compaction_summary")
442
+ throw new Error("Compaction artifact input has the wrong kind.");
443
+ return putArtifact(projectRoot, parsed, limits, options);
444
+ }
445
+ export async function readArtifact(identifier, projectRoot = process.cwd()) {
446
+ const agent = await openArtifactStore(projectRoot, false);
447
+ if (!agent)
448
+ throw new Error("Artifact not found.");
449
+ try {
450
+ await assertCompatibleSchema(agent, false);
451
+ return await findByIdentifier(agent, identifier);
452
+ }
453
+ finally {
454
+ await agent.close();
455
+ }
456
+ }
457
+ export async function listArtifacts(projectRoot = process.cwd(), filters = {}) {
458
+ const agent = await openArtifactStore(projectRoot, false);
459
+ if (!agent)
460
+ return { schema_version: ARTIFACT_SCHEMA_VERSION, artifacts: [], logical_bytes: 0 };
461
+ try {
462
+ await assertCompatibleSchema(agent, false);
463
+ let artifacts = sortedOldest(await readFullArtifacts(agent)).reverse();
464
+ if (filters.kind)
465
+ artifacts = artifacts.filter((artifact) => artifact.kind === filters.kind);
466
+ if (filters.session_ref)
467
+ artifacts = artifacts.filter((artifact) => artifact.session_ref === filters.session_ref);
468
+ if (filters.node_ref)
469
+ artifacts = artifacts.filter((artifact) => canonicalJson(artifact.node_ref) === canonicalJson(filters.node_ref));
470
+ const start = filters.cursor ? Number(Buffer.from(filters.cursor, "base64url").toString("utf8")) : 0;
471
+ if (!Number.isSafeInteger(start) || start < 0)
472
+ throw new Error("Artifact list cursor is invalid.");
473
+ const limit = filters.limit ?? 50;
474
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
475
+ throw new Error("Artifact list limit must be between 1 and 100.");
476
+ const page = artifacts.slice(start, start + limit);
477
+ return {
478
+ schema_version: ARTIFACT_SCHEMA_VERSION,
479
+ artifacts: page,
480
+ logical_bytes: artifacts.reduce((sum, artifact) => sum + artifact.logical_bytes, 0),
481
+ ...(start + limit < artifacts.length ? { next_cursor: Buffer.from(String(start + limit)).toString("base64url") } : {}),
482
+ };
483
+ }
484
+ finally {
485
+ await agent.close();
486
+ }
487
+ }
488
+ export async function readLatestCompaction(projectRoot = process.cwd(), sessionRef) {
489
+ const agent = await openArtifactStore(projectRoot, false);
490
+ if (!agent)
491
+ return null;
492
+ try {
493
+ await assertCompatibleSchema(agent, false);
494
+ const pointer = await agent.kv.get(sessionRef ? `${SESSION_LATEST_PREFIX}${sessionRef}` : PROJECT_LATEST_KEY);
495
+ if (pointer) {
496
+ const stored = await agent.kv.get(fullKey(pointer));
497
+ const parsed = artifactEnvelopeSchema.safeParse(stored);
498
+ if (parsed.success && parsed.data.kind === "compaction_summary")
499
+ return parsed.data;
500
+ }
501
+ return newestCompaction(await readFullArtifacts(agent), sessionRef);
502
+ }
503
+ finally {
504
+ await agent.close();
505
+ }
506
+ }
507
+ export const selectRecoveryArtifact = readLatestCompaction;
508
+ export async function deleteArtifact(identifier, projectRoot = process.cwd(), options = {}) {
509
+ return withMutationLock(projectRoot, async () => {
510
+ const agent = await openArtifactStore(projectRoot, false);
511
+ if (!agent)
512
+ throw new Error("Artifact not found.");
513
+ let retainedRows;
514
+ let result;
515
+ try {
516
+ await assertCompatibleSchema(agent, false);
517
+ const artifact = await findByIdentifier(agent, identifier);
518
+ if (!options.dryRun) {
519
+ await inImmediateTransaction(agent, async () => {
520
+ await deleteArtifactRows(agent, artifact);
521
+ await rebuildPointers(agent);
522
+ });
523
+ await agent.getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE)");
524
+ retainedRows = await agent.kv.list("");
525
+ }
526
+ result = {
527
+ schema_version: ARTIFACT_SCHEMA_VERSION,
528
+ artifact_id: artifact.artifact_id,
529
+ deleted: !options.dryRun,
530
+ dry_run: Boolean(options.dryRun),
531
+ };
532
+ }
533
+ finally {
534
+ await agent.close();
535
+ }
536
+ if (retainedRows)
537
+ await rewriteArtifactDatabase(projectRoot, retainedRows);
538
+ return result;
539
+ });
540
+ }
541
+ export async function pruneArtifacts(projectRoot, limits = getArtifactLimits(), options = {}) {
542
+ return withMutationLock(projectRoot, async () => {
543
+ const agent = await openArtifactStore(projectRoot, false);
544
+ if (!agent) {
545
+ return { schema_version: 1, dry_run: Boolean(options.dryRun), removed: [], remaining_artifacts: 0, logical_bytes: 0 };
546
+ }
547
+ let retainedRows;
548
+ let result;
549
+ try {
550
+ await assertCompatibleSchema(agent, false);
551
+ const resolved = {
552
+ dryRun: options.dryRun ?? false,
553
+ includeProtected: options.includeProtected ?? false,
554
+ now: options.now ?? new Date(),
555
+ };
556
+ result = await inImmediateTransaction(agent, () => pruneInTransaction(agent, limits, resolved));
557
+ if (!resolved.dryRun && result.removed.length > 0) {
558
+ await agent.getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE)");
559
+ retainedRows = await agent.kv.list("");
560
+ }
561
+ }
562
+ finally {
563
+ await agent.close();
564
+ }
565
+ if (retainedRows)
566
+ await rewriteArtifactDatabase(projectRoot, retainedRows);
567
+ return result;
568
+ });
569
+ }
570
+ export async function recordUnsupportedCompactionAdapter(projectRoot, diagnostic = {}) {
571
+ await withMutationLock(projectRoot, async () => {
572
+ const agent = await openArtifactStore(projectRoot, true);
573
+ if (!agent)
574
+ return;
575
+ try {
576
+ await inImmediateTransaction(agent, async () => {
577
+ await assertCompatibleSchema(agent, true);
578
+ if (await agent.kv.get(META_KEY) === undefined) {
579
+ await agent.kv.set(META_KEY, ARTIFACT_SCHEMA_VERSION);
580
+ }
581
+ const rows = await agent.kv.list(DIAGNOSTIC_PREFIX);
582
+ for (const row of rows.slice(0, Math.max(0, rows.length - MAX_DIAGNOSTICS + 1)))
583
+ await agent.kv.delete(row.key);
584
+ await agent.kv.set(`${DIAGNOSTIC_PREFIX}${String(Date.now()).padStart(16, "0")}-${randomUUID()}`, {
585
+ schema_version: 1,
586
+ harness_known: Boolean(diagnostic.harness),
587
+ version_known: Boolean(diagnostic.harness_version),
588
+ reason: ["unsupported_version", "invalid_shape", "no_summary"].includes(diagnostic.reason ?? "")
589
+ ? diagnostic.reason : "invalid_shape",
590
+ });
591
+ });
592
+ }
593
+ finally {
594
+ await agent.close();
595
+ }
596
+ });
597
+ }
598
+ function addIssue(issues, issue) {
599
+ if (!issues.includes(issue))
600
+ issues.push(issue);
601
+ }
602
+ function requestDigestForArtifact(artifact) {
603
+ const content = artifact.kind === "compaction_summary"
604
+ ? Object.fromEntries(Object.entries(artifact.content).filter(([key]) => key !== "revision"))
605
+ : artifact.content;
606
+ return requestDigestFor({
607
+ kind: artifact.kind,
608
+ session_ref: artifact.session_ref,
609
+ ...(artifact.node_ref ? { node_ref: artifact.node_ref } : {}),
610
+ media_type: artifact.media_type,
611
+ provenance: artifact.provenance,
612
+ ...(artifact.supersedes ? { supersedes: artifact.supersedes } : {}),
613
+ content,
614
+ });
615
+ }
616
+ async function inspectDoctorDerivedState(agent, artifacts) {
617
+ const issues = [];
618
+ const unsafeSequenceIssues = [];
619
+ let repairNeeded = false;
620
+ const expectedIndexes = new Map(artifacts.flatMap((artifact) => indexEntries(artifact)));
621
+ const actualIndexes = new Map((await agent.kv.list(INDEX_PREFIX))
622
+ .filter((row) => !row.key.startsWith(DEDUPE_PREFIX))
623
+ .map((row) => [row.key, row.value]));
624
+ for (const [key, value] of expectedIndexes) {
625
+ if (canonicalJson(actualIndexes.get(key)) !== canonicalJson(value)) {
626
+ addIssue(issues, "Missing or stale artifact index.");
627
+ repairNeeded = true;
628
+ }
629
+ }
630
+ for (const key of actualIndexes.keys()) {
631
+ if (!expectedIndexes.has(key)) {
632
+ addIssue(issues, "Orphan artifact index.");
633
+ repairNeeded = true;
634
+ }
635
+ }
636
+ const expectedDedupe = new Map();
637
+ for (const artifact of artifacts) {
638
+ expectedDedupe.set(`${DEDUPE_PREFIX}${requestDigestForArtifact(artifact)}`, artifact.artifact_id);
639
+ }
640
+ const actualDedupe = new Map((await agent.kv.list(DEDUPE_PREFIX)).map((row) => [row.key, row.value]));
641
+ for (const [key, value] of expectedDedupe) {
642
+ if (actualDedupe.get(key) !== value) {
643
+ addIssue(issues, "Missing or stale artifact dedupe binding.");
644
+ repairNeeded = true;
645
+ }
646
+ }
647
+ for (const key of actualDedupe.keys()) {
648
+ if (!expectedDedupe.has(key)) {
649
+ addIssue(issues, "Orphan artifact dedupe binding.");
650
+ repairNeeded = true;
651
+ }
652
+ }
653
+ const expectedPointers = new Map();
654
+ for (const artifact of sortedOldest(artifacts)) {
655
+ if (artifact.kind === "compaction_summary") {
656
+ expectedPointers.set(`${SESSION_LATEST_PREFIX}${artifact.session_ref}`, artifact.artifact_id);
657
+ }
658
+ }
659
+ const latest = newestCompaction(artifacts);
660
+ if (latest)
661
+ expectedPointers.set(PROJECT_LATEST_KEY, latest.artifact_id);
662
+ const actualPointers = new Map((await agent.kv.list("compaction/latest/")).map((row) => [row.key, row.value]));
663
+ for (const [key, value] of expectedPointers) {
664
+ if (actualPointers.get(key) !== value) {
665
+ addIssue(issues, "Missing or stale compaction pointer.");
666
+ repairNeeded = true;
667
+ }
668
+ }
669
+ for (const key of actualPointers.keys()) {
670
+ if (!expectedPointers.has(key)) {
671
+ addIssue(issues, "Orphan compaction pointer.");
672
+ repairNeeded = true;
673
+ }
674
+ }
675
+ const retainedMaximums = new Map();
676
+ for (const artifact of artifacts) {
677
+ if (artifact.kind !== "compaction_summary")
678
+ continue;
679
+ retainedMaximums.set(artifact.session_ref, Math.max(retainedMaximums.get(artifact.session_ref) ?? 0, artifact.content.revision));
680
+ }
681
+ const sequenceRows = new Map((await agent.kv.list(SEQUENCE_PREFIX)).map((row) => [row.key, row.value]));
682
+ const sequenceRepairs = new Map();
683
+ for (const [sessionRef, maximum] of retainedMaximums) {
684
+ const key = `${SEQUENCE_PREFIX}${sessionRef}`;
685
+ const value = sequenceRows.get(key);
686
+ if (!Number.isSafeInteger(value) || Number(value) <= 0 || Number(value) < maximum) {
687
+ addIssue(issues, "Missing or regressed compaction sequence.");
688
+ sequenceRepairs.set(key, maximum);
689
+ repairNeeded = true;
690
+ }
691
+ }
692
+ for (const [key, value] of sequenceRows) {
693
+ const sessionRef = key.slice(SEQUENCE_PREFIX.length);
694
+ if (retainedMaximums.has(sessionRef))
695
+ continue;
696
+ if (!sessionRef || !Number.isSafeInteger(value) || Number(value) <= 0) {
697
+ addIssue(unsafeSequenceIssues, "Invalid non-derivable compaction sequence.");
698
+ }
699
+ }
700
+ return {
701
+ issues,
702
+ unsafeSequenceIssues,
703
+ expectedIndexes,
704
+ expectedDedupe,
705
+ sequenceRepairs,
706
+ actualIndexCount: actualIndexes.size,
707
+ repairNeeded,
708
+ };
709
+ }
710
+ function inspectRetentionState(artifacts, limits) {
711
+ const issues = [];
712
+ const planned = planPrune(artifacts, limits, {
713
+ dryRun: true,
714
+ includeProtected: false,
715
+ now: new Date(),
716
+ });
717
+ for (const item of planned) {
718
+ if (item.reason === "age")
719
+ addIssue(issues, "Artifact retention age limit exceeded.");
720
+ if (item.reason === "revision")
721
+ addIssue(issues, "Compaction revision retention limit exceeded.");
722
+ if (item.reason === "session_budget")
723
+ addIssue(issues, "Artifact session logical byte limit exceeded.");
724
+ if (item.reason === "store_budget")
725
+ addIssue(issues, "Artifact store logical byte limit exceeded.");
726
+ }
727
+ return issues;
728
+ }
729
+ export async function doctorArtifactStore(projectRoot = process.cwd(), repair = false, limits = getArtifactLimits()) {
730
+ const dbPath = getArtifactDbPath(projectRoot);
731
+ if (!existsSync(dbPath)) {
732
+ return { schema_version: 1, exists: false, ok: true, repaired: false, integrity: "not_present", valid_artifacts: 0, indexed_artifacts: 0, issues: [] };
733
+ }
734
+ const sqliteHeader = readFileSync(dbPath).subarray(0, 16).toString("binary");
735
+ if (sqliteHeader !== "SQLite format 3\0") {
736
+ return { schema_version: 1, exists: true, ok: false, repaired: false, integrity: "failed", valid_artifacts: 0, indexed_artifacts: 0, issues: ["SQLite integrity check failed."] };
737
+ }
738
+ let agent = null;
739
+ try {
740
+ agent = await openArtifactStore(projectRoot, false);
741
+ if (!agent)
742
+ throw new Error("Artifact store is absent.");
743
+ const integrityRows = await agent.getDatabase().pragma("integrity_check", {});
744
+ const integrityOk = Array.isArray(integrityRows) && integrityRows.length > 0
745
+ && integrityRows.every((row) => Object.values(row).includes("ok"));
746
+ if (!integrityOk) {
747
+ return { schema_version: 1, exists: true, ok: false, repaired: false, integrity: "failed", valid_artifacts: 0, indexed_artifacts: 0, issues: ["SQLite integrity check failed."] };
748
+ }
749
+ const authoritativeIssues = [];
750
+ const version = await agent.kv.get(META_KEY);
751
+ if (version !== ARTIFACT_SCHEMA_VERSION)
752
+ authoritativeIssues.push("Artifact schema metadata is missing or unsupported.");
753
+ const artifacts = [];
754
+ for (const row of await agent.kv.list(FULL_PREFIX)) {
755
+ const parsed = artifactEnvelopeSchema.safeParse(row.value);
756
+ if (!parsed.success) {
757
+ addIssue(authoritativeIssues, "Invalid authoritative full record.");
758
+ continue;
759
+ }
760
+ if (sha256Hex(canonicalBytes(parsed.data.content)) !== parsed.data.content_digest) {
761
+ addIssue(authoritativeIssues, "Authoritative full record digest mismatch.");
762
+ continue;
763
+ }
764
+ artifacts.push(parsed.data);
765
+ }
766
+ let derived = await inspectDoctorDerivedState(agent, artifacts);
767
+ const retentionIssues = inspectRetentionState(artifacts, limits);
768
+ let repaired = false;
769
+ if (repair
770
+ && derived.repairNeeded
771
+ && authoritativeIssues.length === 0
772
+ && derived.unsafeSequenceIssues.length === 0) {
773
+ const repairAgent = agent;
774
+ await inImmediateTransaction(repairAgent, async () => {
775
+ for (const row of await repairAgent.kv.list(INDEX_PREFIX))
776
+ await repairAgent.kv.delete(row.key);
777
+ for (const [key, value] of derived.expectedIndexes)
778
+ await repairAgent.kv.set(key, value);
779
+ for (const [key, value] of derived.expectedDedupe)
780
+ await repairAgent.kv.set(key, value);
781
+ await rebuildPointers(repairAgent);
782
+ for (const [key, value] of derived.sequenceRepairs)
783
+ await repairAgent.kv.set(key, value);
784
+ });
785
+ repaired = true;
786
+ derived = await inspectDoctorDerivedState(agent, artifacts);
787
+ }
788
+ const issues = [
789
+ ...authoritativeIssues,
790
+ ...derived.issues,
791
+ ...derived.unsafeSequenceIssues,
792
+ ...retentionIssues,
793
+ ];
794
+ return {
795
+ schema_version: 1,
796
+ exists: true,
797
+ ok: issues.length === 0,
798
+ repaired,
799
+ integrity: "ok",
800
+ valid_artifacts: artifacts.length,
801
+ indexed_artifacts: derived.actualIndexCount,
802
+ issues,
803
+ };
804
+ }
805
+ catch {
806
+ return { schema_version: 1, exists: true, ok: false, repaired: false, integrity: "failed", valid_artifacts: 0, indexed_artifacts: 0, issues: ["SQLite integrity or authoritative store open failed."] };
807
+ }
808
+ finally {
809
+ if (agent)
810
+ await agent.close().catch(() => undefined);
811
+ }
812
+ }