@cairnkeep/cli 2.0.0 → 2.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/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable user-facing changes are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [2.1.0] - 2026-07-16
7
+
8
+ ### Added
9
+
10
+ - Add `memory_apply_reviewed` for idempotent application of externally reviewed
11
+ memory revisions with durable provenance and collision-safe history.
12
+ - Add `memory_invalidate_reviewed` for revision-matched retraction and durable
13
+ tombstones that prevent delayed writes from resurrecting invalid memory.
14
+ - Add end-to-end checks for replay, supersession, manual changes, retraction,
15
+ tombstones, and reserved provenance records.
16
+
17
+ ### Changed
18
+
19
+ - Make history snapshot keys unique when multiple revisions occur in the same
20
+ millisecond.
21
+
22
+ The existing memory tools, database locations, and remote HTTP routing contract
23
+ are unchanged. Existing clients remain compatible and reviewed-memory records
24
+ are created only when an integration explicitly invokes the new tools.
25
+
6
26
  ## [2.0.0] - 2026-07-15
7
27
 
8
28
  ### Breaking
@@ -69,6 +89,7 @@ stored memories.
69
89
  - Preserve executable permissions and Bash 3.2 portability on macOS.
70
90
  - Add backup-first uninstall and SQLite-safe memory export/import guidance.
71
91
 
92
+ [2.1.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.0.0...v2.1.0
72
93
  [2.0.0]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.3...v2.0.0
73
94
  [1.1.3]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.2...v1.1.3
74
95
  [1.1.2]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.1...v1.1.2
package/docs/storage.md CHANGED
@@ -112,6 +112,31 @@ sqlite3 /path/to/project/.agentfs/project.db \
112
112
  Treat every database and export archive as sensitive. They may contain source
113
113
  paths, decisions, incident details, and other project context.
114
114
 
115
+ ## Reviewed-memory integration
116
+
117
+ External review systems should not compose `memory_write`, `memory_read`, and
118
+ `memory_delete` into their own promotion protocol. That sequence is not atomic
119
+ and can delete a newer manual revision during lifecycle invalidation.
120
+
121
+ Cairnkeep exposes two additive MCP tools for reviewed integrations:
122
+
123
+ - `memory_apply_reviewed` takes `scope`, `review_id`, `key`, and `value`. It is
124
+ idempotent for the same review and content, preserves a displaced live value
125
+ in memory history, and rejects reuse of the review ID with different content.
126
+ - `memory_invalidate_reviewed` takes `scope`, `review_id`, `key`, and an optional
127
+ `reason`. It removes the live value only when it still matches that reviewed
128
+ revision. If apply has not arrived yet, it records a tombstone so a delayed
129
+ apply cannot resurrect invalid memory.
130
+
131
+ Provenance and tombstones use a hidden reserved namespace in the same scoped
132
+ database. Generic memory writes and deletes cannot modify those records. Review
133
+ IDs must be stable, non-secret idempotency identifiers; evidence payloads and
134
+ credentials do not belong in them.
135
+
136
+ These tools are for an explicit, trusted integration. Cairnkeep itself does not
137
+ approve candidates, discover an evidence service, or promote memory
138
+ automatically. Existing clients and the original memory tools remain unchanged.
139
+
115
140
  ## Automating trusted personal clients
116
141
 
117
142
  Keep fleet-specific values in a private dotfiles repository or secret manager,
@@ -29,6 +29,8 @@ const anythingllmSyncScript = process.env.CAIRN_ANYTHINGLLM_SYNC_SCRIPT
29
29
  ? resolve(expandHome(process.env.CAIRN_ANYTHINGLLM_SYNC_SCRIPT))
30
30
  : join(infraRoot, "examples", "anythingllm", "sync_to_anythingllm.py");
31
31
  const HISTORY_NAMESPACE = "__history__";
32
+ const REVIEWED_NAMESPACE = "__reviewed__";
33
+ const REVIEW_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
32
34
  const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
33
35
  const WORKSPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
34
36
  function expandHome(value) {
@@ -180,17 +182,52 @@ async function openScope(scope, create, options = {}) {
180
182
  function isHistoryKey(key) {
181
183
  return key === HISTORY_NAMESPACE || key.startsWith(`${HISTORY_NAMESPACE}/`);
182
184
  }
185
+ function isReviewedKey(key) {
186
+ return key === REVIEWED_NAMESPACE || key.startsWith(`${REVIEWED_NAMESPACE}/`);
187
+ }
188
+ function assertWritableMemoryKey(key) {
189
+ if (isHistoryKey(key) || isReviewedKey(key)) {
190
+ throw new Error(`Keys under ${HISTORY_NAMESPACE}/ and ${REVIEWED_NAMESPACE}/ are reserved.`);
191
+ }
192
+ }
183
193
  function historyPrefix(baseKey) {
184
194
  return `${HISTORY_NAMESPACE}/${baseKey}/`;
185
195
  }
186
196
  function historySnapshotKey(baseKey, timestamp) {
187
- return `${historyPrefix(baseKey)}${timestamp}`;
197
+ return `${historyPrefix(baseKey)}${timestamp}-${randomUUID()}`;
198
+ }
199
+ const reviewedRecordSchema = z.object({
200
+ schema_version: z.literal(1),
201
+ review_id: z.string(),
202
+ key: z.string(),
203
+ value_hash: z.string(),
204
+ state: z.enum(["active", "superseded", "invalidated"]),
205
+ applied_at: z.string(),
206
+ superseded_at: z.string().optional(),
207
+ superseded_by: z.string().optional(),
208
+ invalidated_at: z.string().optional(),
209
+ invalidation_reason: z.string().optional(),
210
+ });
211
+ function reviewedRecordKey(reviewId) {
212
+ return `${REVIEWED_NAMESPACE}/${reviewId}`;
213
+ }
214
+ function parseReviewedRecord(value) {
215
+ const parsed = reviewedRecordSchema.safeParse(value);
216
+ if (!parsed.success) {
217
+ throw new Error("Stored reviewed-memory provenance is invalid; refusing to continue.");
218
+ }
219
+ return parsed.data;
220
+ }
221
+ async function inImmediateScopeTransaction(agent, operation) {
222
+ const transaction = agent.getDatabase().transaction(operation);
223
+ const immediate = transaction.immediate;
224
+ return immediate();
188
225
  }
189
226
  function visibleEntries(entries, includeHistory) {
190
227
  if (includeHistory) {
191
228
  return entries;
192
229
  }
193
- return entries.filter((entry) => !isHistoryKey(entry.key));
230
+ return entries.filter((entry) => !isHistoryKey(entry.key) && !isReviewedKey(entry.key));
194
231
  }
195
232
  async function listEntries(scope, prefix = "", options = {}) {
196
233
  const agent = await openScope(scope, false, options);
@@ -759,9 +796,7 @@ function createMemoryServer(context = {}) {
759
796
  promote_to: z.string().optional(),
760
797
  }),
761
798
  }, async ({ scope, key, value, promote_to }) => {
762
- if (isHistoryKey(key)) {
763
- throw new Error(`Keys under ${HISTORY_NAMESPACE}/ are reserved for memory history.`);
764
- }
799
+ assertWritableMemoryKey(key);
765
800
  const targets = promote_to && promote_to !== scope ? [scope, promote_to] : [scope];
766
801
  // Collision-safe: in the unified store, writes from different repos/machines
767
802
  // can share a key. If a different value already exists, preserve the old one
@@ -824,6 +859,9 @@ function createMemoryServer(context = {}) {
824
859
  key: z.string().min(1),
825
860
  }),
826
861
  }, async ({ scope, key }) => {
862
+ if (isReviewedKey(key)) {
863
+ throw new Error(`Keys under ${REVIEWED_NAMESPACE}/ are reserved for reviewed-memory provenance.`);
864
+ }
827
865
  const agent = await openScope(scope, false, scopeOptions);
828
866
  if (agent) {
829
867
  try {
@@ -887,9 +925,7 @@ function createMemoryServer(context = {}) {
887
925
  reason: z.string().optional(),
888
926
  }),
889
927
  }, async ({ scope, key, value, reason }) => {
890
- if (isHistoryKey(key)) {
891
- throw new Error(`Keys under ${HISTORY_NAMESPACE}/ are reserved for memory history.`);
892
- }
928
+ assertWritableMemoryKey(key);
893
929
  const agent = await openScope(scope, true, scopeOptions);
894
930
  if (!agent) {
895
931
  throw new Error(`Unable to open scope ${scope}.`);
@@ -929,6 +965,207 @@ function createMemoryServer(context = {}) {
929
965
  await agent.close();
930
966
  }
931
967
  });
968
+ server.registerTool("memory_apply_reviewed", {
969
+ description: "Idempotently apply a human-reviewed memory revision with durable provenance.",
970
+ inputSchema: z.object({
971
+ scope: z.string(),
972
+ review_id: z.string().regex(REVIEW_ID_PATTERN),
973
+ key: z.string().min(1),
974
+ value: z.string(),
975
+ }),
976
+ }, async ({ scope, review_id, key, value }) => {
977
+ assertWritableMemoryKey(key);
978
+ const agent = await openScope(scope, true, scopeOptions);
979
+ if (!agent) {
980
+ throw new Error(`Unable to open scope ${scope}.`);
981
+ }
982
+ try {
983
+ const payload = await inImmediateScopeTransaction(agent, async () => {
984
+ const recordKey = reviewedRecordKey(review_id);
985
+ const valueHash = hashText(value);
986
+ const stored = await agent.kv.get(recordKey);
987
+ if (stored !== undefined) {
988
+ const existing = parseReviewedRecord(stored);
989
+ if (existing.key !== key || existing.value_hash !== valueHash) {
990
+ throw new Error(`Review id ${review_id} was already used with different content.`);
991
+ }
992
+ if (existing.state !== "active") {
993
+ throw new Error(`Review id ${review_id} is ${existing.state} and cannot be reapplied.`);
994
+ }
995
+ return {
996
+ ok: true,
997
+ scope,
998
+ review_id,
999
+ key,
1000
+ applied: false,
1001
+ idempotent: true,
1002
+ snapshot_key: null,
1003
+ };
1004
+ }
1005
+ const appliedAt = new Date().toISOString();
1006
+ const provenance = await agent.kv.list(`${REVIEWED_NAMESPACE}/`);
1007
+ for (const entry of provenance) {
1008
+ const previousRecord = parseReviewedRecord(entry.value);
1009
+ if (previousRecord.state === "active" && previousRecord.key === key) {
1010
+ await agent.kv.set(entry.key, {
1011
+ ...previousRecord,
1012
+ state: "superseded",
1013
+ superseded_at: appliedAt,
1014
+ superseded_by: review_id,
1015
+ });
1016
+ }
1017
+ }
1018
+ const previous = await agent.kv.get(key);
1019
+ const previousValue = previous === undefined ? undefined : normalizeValue(previous);
1020
+ let snapshotKey = null;
1021
+ if (previousValue !== undefined && previousValue !== value) {
1022
+ snapshotKey = historySnapshotKey(key, appliedAt);
1023
+ await agent.kv.set(snapshotKey, {
1024
+ value: previousValue,
1025
+ superseded_at: appliedAt,
1026
+ superseded_reason: `reviewed memory ${review_id}`,
1027
+ });
1028
+ }
1029
+ await agent.kv.set(key, value);
1030
+ await agent.kv.set(recordKey, {
1031
+ schema_version: 1,
1032
+ review_id,
1033
+ key,
1034
+ value_hash: valueHash,
1035
+ state: "active",
1036
+ applied_at: appliedAt,
1037
+ });
1038
+ return {
1039
+ ok: true,
1040
+ scope,
1041
+ review_id,
1042
+ key,
1043
+ applied: true,
1044
+ idempotent: false,
1045
+ snapshot_key: snapshotKey,
1046
+ };
1047
+ });
1048
+ return {
1049
+ content: [{ type: "text", text: asToolText(payload) }],
1050
+ structuredContent: payload,
1051
+ };
1052
+ }
1053
+ finally {
1054
+ await agent.close();
1055
+ }
1056
+ });
1057
+ server.registerTool("memory_invalidate_reviewed", {
1058
+ description: "Invalidate a reviewed revision, removing it only when the live value still matches.",
1059
+ inputSchema: z.object({
1060
+ scope: z.string(),
1061
+ review_id: z.string().regex(REVIEW_ID_PATTERN),
1062
+ key: z.string().min(1),
1063
+ reason: z.string().max(512).optional(),
1064
+ }),
1065
+ }, async ({ scope, review_id, key, reason }) => {
1066
+ assertWritableMemoryKey(key);
1067
+ const agent = await openScope(scope, true, scopeOptions);
1068
+ if (!agent) {
1069
+ throw new Error(`Unable to open scope ${scope}.`);
1070
+ }
1071
+ try {
1072
+ const payload = await inImmediateScopeTransaction(agent, async () => {
1073
+ const recordKey = reviewedRecordKey(review_id);
1074
+ const stored = await agent.kv.get(recordKey);
1075
+ if (stored === undefined) {
1076
+ const invalidatedAt = new Date().toISOString();
1077
+ await agent.kv.set(recordKey, {
1078
+ schema_version: 1,
1079
+ review_id,
1080
+ key,
1081
+ value_hash: "",
1082
+ state: "invalidated",
1083
+ applied_at: invalidatedAt,
1084
+ invalidated_at: invalidatedAt,
1085
+ ...(reason ? { invalidation_reason: reason } : {}),
1086
+ });
1087
+ return {
1088
+ ok: true,
1089
+ scope,
1090
+ review_id,
1091
+ key,
1092
+ invalidated: true,
1093
+ idempotent: false,
1094
+ missing: true,
1095
+ removed: false,
1096
+ current_changed: false,
1097
+ snapshot_key: null,
1098
+ };
1099
+ }
1100
+ const record = parseReviewedRecord(stored);
1101
+ if (record.key !== key) {
1102
+ throw new Error(`Review id ${review_id} belongs to a different memory key.`);
1103
+ }
1104
+ if (record.state === "invalidated") {
1105
+ return {
1106
+ ok: true,
1107
+ scope,
1108
+ review_id,
1109
+ key: record.key,
1110
+ invalidated: false,
1111
+ idempotent: true,
1112
+ missing: record.value_hash === "",
1113
+ removed: false,
1114
+ current_changed: false,
1115
+ snapshot_key: null,
1116
+ };
1117
+ }
1118
+ const invalidatedAt = new Date().toISOString();
1119
+ let currentChanged = false;
1120
+ let removed = false;
1121
+ let snapshotKey = null;
1122
+ if (record.state === "active") {
1123
+ const current = await agent.kv.get(record.key);
1124
+ if (current !== undefined) {
1125
+ const currentValue = normalizeValue(current);
1126
+ if (hashText(currentValue) === record.value_hash) {
1127
+ snapshotKey = historySnapshotKey(record.key, invalidatedAt);
1128
+ await agent.kv.set(snapshotKey, {
1129
+ value: currentValue,
1130
+ superseded_at: invalidatedAt,
1131
+ superseded_reason: reason ?? `reviewed memory ${review_id} invalidated`,
1132
+ });
1133
+ await agent.kv.delete(record.key);
1134
+ removed = true;
1135
+ }
1136
+ else {
1137
+ currentChanged = true;
1138
+ }
1139
+ }
1140
+ }
1141
+ await agent.kv.set(recordKey, {
1142
+ ...record,
1143
+ state: "invalidated",
1144
+ invalidated_at: invalidatedAt,
1145
+ ...(reason ? { invalidation_reason: reason } : {}),
1146
+ });
1147
+ return {
1148
+ ok: true,
1149
+ scope,
1150
+ review_id,
1151
+ key: record.key,
1152
+ invalidated: true,
1153
+ idempotent: false,
1154
+ missing: false,
1155
+ removed,
1156
+ current_changed: currentChanged,
1157
+ snapshot_key: snapshotKey,
1158
+ };
1159
+ });
1160
+ return {
1161
+ content: [{ type: "text", text: asToolText(payload) }],
1162
+ structuredContent: payload,
1163
+ };
1164
+ }
1165
+ finally {
1166
+ await agent.close();
1167
+ }
1168
+ });
932
1169
  server.registerTool("memory_history", {
933
1170
  description: "Read prior versions of a memory entry from the hidden history namespace.",
934
1171
  inputSchema: z.object({
@@ -22,8 +22,9 @@
22
22
  "check:explore-cache": "node scripts/smoke-explore-cache.mjs",
23
23
  "check:explore-crossref": "node scripts/smoke-explore-crossref.mjs",
24
24
  "check:route-guard": "node scripts/smoke-route-guard.mjs",
25
+ "check:reviewed-memory": "node scripts/smoke-reviewed-memory.mjs",
25
26
  "test": "npm run test:smoke",
26
- "test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard",
27
+ "test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard && npm run check:reviewed-memory",
27
28
  "test:search": "npm run build && npm run check:search",
28
29
  "test:memory": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:search",
29
30
  "test:all": "npm run test:memory"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",