@vaur94/agz-memory 0.4.0 → 0.4.1

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/ARCHITECTURE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AGZ Memory Architecture
2
2
 
3
- This document describes the `0.4.0` runtime and SQLite schema v10.
3
+ This document describes the `0.4.1` runtime and SQLite schema v10.
4
4
 
5
5
  ## System Boundaries
6
6
 
@@ -20,7 +20,7 @@ OpenCode V2 beta-18743
20
20
 
21
21
  SQLite outbox
22
22
  -> optional derived retrieval backend
23
- -> disabled in 0.4.0; backend = none
23
+ -> disabled in 0.4.1; backend = none
24
24
  ```
25
25
 
26
26
  The MCP adapter owns tool schemas, annotations, and result envelopes. The core
@@ -128,7 +128,7 @@ original OpenCode context unchanged.
128
128
 
129
129
  Semantic providers implement an optional backend contract: project-filtered
130
130
  query, idempotent upsert, deterministic delete, full project purge, and health.
131
- No provider is enabled in `0.4.0`. The SQLite lexical/graph path remains fully
131
+ No provider is enabled in `0.4.1`. The SQLite lexical/graph path remains fully
132
132
  functional without one.
133
133
 
134
134
  ## Derived Index Outbox
package/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes to AGZ Memory are recorded here. The project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.4.1] - 2026-09-01
7
+
8
+ ### Fixed
9
+
10
+ - Re-read the SQLite schema after acquiring the migration lock so concurrent
11
+ MCP processes do not act on a stale pre-lock version.
12
+ - Skip backup and migration work when another process already completed the
13
+ target schema, preventing redundant backups whose manifest version could
14
+ differ from the copied database.
15
+ - Retry migration-lock handoff when the previous owner releases the lock
16
+ between a competing process's create attempt and ownership check.
17
+ - Reopen the canonical SQLite file after taking the migration lock so a waiter
18
+ cannot continue on a database file replaced during automatic recovery.
19
+ - Verify versioned backup manifests against the schema stored in their SQLite
20
+ database instead of trusting the caller-provided source label.
21
+ - Added a six-process regression test that requires exactly one v9 backup, a
22
+ v9 backup database, and one healthy v10 canonical database.
23
+
6
24
  ## [0.4.0] - 2026-09-01
7
25
 
8
26
  ### Added
@@ -56,5 +74,6 @@ All notable changes to AGZ Memory are recorded here. The project follows
56
74
  - Preserved the nine-tool MCP interface and project-isolation guarantees while
57
75
  establishing the AGZ Memory public packages and repository.
58
76
 
77
+ [0.4.1]: https://github.com/ugur-murat-alt/agz-memory/releases/tag/v0.4.1
59
78
  [0.4.0]: https://github.com/ugur-murat-alt/agz-memory/releases/tag/v0.4.0
60
79
  [0.4.0-beta.1]: https://github.com/ugur-murat-alt/agz-memory/releases/tag/v0.4.0-beta.1
package/README.md CHANGED
@@ -32,8 +32,8 @@ binding and rollout mode are configured.
32
32
 
33
33
  | Component | Supported version |
34
34
  |---|---|
35
- | Core and MCP | `0.4.0` |
36
- | OpenCode plugin | `0.4.0` |
35
+ | Core and MCP | `0.4.1` |
36
+ | OpenCode plugin | `0.4.1` |
37
37
  | OpenCode V2 | `0.0.0-beta-18743` |
38
38
  | `@opencode-ai/plugin` | `0.0.0-beta-18743` |
39
39
  | Bun | `>=1.3.14` |
@@ -47,7 +47,7 @@ itself unless the running OpenCode version exactly matches the supported beta.
47
47
  Run the server directly:
48
48
 
49
49
  ```sh
50
- bunx @vaur94/agz-memory@0.4.0
50
+ bunx @vaur94/agz-memory@0.4.1
51
51
  ```
52
52
 
53
53
  Or register it in OpenCode V2 under `mcp.servers`:
@@ -58,7 +58,7 @@ Or register it in OpenCode V2 under `mcp.servers`:
58
58
  "servers": {
59
59
  "agz-memory": {
60
60
  "type": "local",
61
- "command": ["bunx", "@vaur94/agz-memory@0.4.0"],
61
+ "command": ["bunx", "@vaur94/agz-memory@0.4.1"],
62
62
  "environment": {
63
63
  "OPENCODE_MEMORY_DATABASE_PATH": "{env:OPENCODE_MEMORY_DATABASE_PATH}"
64
64
  },
@@ -113,7 +113,7 @@ options:
113
113
  {
114
114
  "plugins": [
115
115
  {
116
- "package": "@vaur94/agz-memory-plugin@0.4.0",
116
+ "package": "@vaur94/agz-memory-plugin@0.4.1",
117
117
  "options": {
118
118
  "mode": "off",
119
119
  "autoCreateProjects": false,
@@ -186,11 +186,11 @@ quality, and latency gates.
186
186
  The admin CLI reads the same `OPENCODE_MEMORY_DATABASE_PATH`:
187
187
 
188
188
  ```sh
189
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin doctor
190
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin backup
191
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin upgrade --to 10
192
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin capture status
193
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin outbox status
189
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin doctor
190
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin backup
191
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin upgrade --to 10
192
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin capture status
193
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin outbox status
194
194
  ```
195
195
 
196
196
  Upgrades take an exclusive migration lock and create a verified backup before
@@ -199,7 +199,7 @@ restore. Restore and backup deletion use dry-run output plus explicit
199
199
  confirmation values; never guess them.
200
200
 
201
201
  Use [the backup and restore runbook](docs/backup-restore-runbook.md) for a full
202
- rehearsal. Final `0.4.0` backup manifests use `agz-memory-backup/1`; prerelease
202
+ rehearsal. Final `0.4.1` backup manifests use `agz-memory-backup/1`; prerelease
203
203
  manifests must be handled by the prerelease that created them.
204
204
 
205
205
  ## Security Model
package/README.tr.md CHANGED
@@ -32,8 +32,8 @@ bağlam eklemez.
32
32
 
33
33
  | Bileşen | Desteklenen sürüm |
34
34
  |---|---|
35
- | Çekirdek ve MCP | `0.4.0` |
36
- | OpenCode eklentisi | `0.4.0` |
35
+ | Çekirdek ve MCP | `0.4.1` |
36
+ | OpenCode eklentisi | `0.4.1` |
37
37
  | OpenCode V2 | `0.0.0-beta-18743` |
38
38
  | `@opencode-ai/plugin` | `0.0.0-beta-18743` |
39
39
  | Bun | `>=1.3.14` |
@@ -47,7 +47,7 @@ MCP sunucusu bir OpenCode beta sürümüne bağlı değildir. İsteğe bağlı e
47
47
  Sunucuyu doğrudan çalıştırın:
48
48
 
49
49
  ```sh
50
- bunx @vaur94/agz-memory@0.4.0
50
+ bunx @vaur94/agz-memory@0.4.1
51
51
  ```
52
52
 
53
53
  Ya da OpenCode V2 içinde `mcp.servers` altına kaydedin:
@@ -58,7 +58,7 @@ Ya da OpenCode V2 içinde `mcp.servers` altına kaydedin:
58
58
  "servers": {
59
59
  "agz-memory": {
60
60
  "type": "local",
61
- "command": ["bunx", "@vaur94/agz-memory@0.4.0"],
61
+ "command": ["bunx", "@vaur94/agz-memory@0.4.1"],
62
62
  "environment": {
63
63
  "OPENCODE_MEMORY_DATABASE_PATH": "{env:OPENCODE_MEMORY_DATABASE_PATH}"
64
64
  },
@@ -114,7 +114,7 @@ ayarlarla ekleyin:
114
114
  {
115
115
  "plugins": [
116
116
  {
117
- "package": "@vaur94/agz-memory-plugin@0.4.0",
117
+ "package": "@vaur94/agz-memory-plugin@0.4.1",
118
118
  "options": {
119
119
  "mode": "off",
120
120
  "autoCreateProjects": false,
@@ -187,11 +187,11 @@ kapılarından geçene kadar `semanticBackend` değeri `none` olmalıdır.
187
187
  Yönetim aracı aynı `OPENCODE_MEMORY_DATABASE_PATH` değerini okur:
188
188
 
189
189
  ```sh
190
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin doctor
191
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin backup
192
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin upgrade --to 10
193
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin capture status
194
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin outbox status
190
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin doctor
191
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin backup
192
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin upgrade --to 10
193
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin capture status
194
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin outbox status
195
195
  ```
196
196
 
197
197
  Yükseltmeler özel bir geçiş kilidi alır ve veritabanını değiştirmeden önce
@@ -200,7 +200,7 @@ dener. Geri yükleme ve yedek silme işlemleri önce deneme çıktısı, sonra a
200
200
  onay değerleri ister; bu değerleri tahmin etmeyin.
201
201
 
202
202
  Tam prova için [yedekleme ve geri yükleme runbook'unu](docs/backup-restore-runbook.tr.md)
203
- kullanın. Final `0.4.0` yedek manifestleri `agz-memory-backup/1` kullanır;
203
+ kullanın. Final `0.4.1` yedek manifestleri `agz-memory-backup/1` kullanır;
204
204
  ön sürüm manifestleri onları oluşturan ön sürümle işlenmelidir.
205
205
 
206
206
  ## Güvenlik Modeli
package/dist/admin.js CHANGED
@@ -107,6 +107,9 @@ function hasTable(db, table) {
107
107
  var BACKUP_FORMAT = "agz-memory-backup/1";
108
108
  function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, productVersion) {
109
109
  const sourceHealth = assertHealthyDatabase(db);
110
+ if (sourceHealth.schemaVersion !== undefined && sourceHealth.schemaVersion !== sourceSchema) {
111
+ throw new Error(`backup source schema v${sourceSchema} does not match database schema v${sourceHealth.schemaVersion}`);
112
+ }
110
113
  const checkpoint = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
111
114
  if (checkpoint.busy !== 0)
112
115
  throw new Error("database WAL checkpoint is busy");
@@ -134,6 +137,9 @@ function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, prod
134
137
  if (JSON.stringify(backupHealth.counts) !== JSON.stringify(sourceHealth.counts)) {
135
138
  throw new Error("backup row counts differ from source database");
136
139
  }
140
+ if (backupHealth.schemaVersion !== undefined && backupHealth.schemaVersion !== sourceSchema) {
141
+ throw new Error("backup schema does not match source schema");
142
+ }
137
143
  const bytes = readFileSync(temporaryDatabasePath);
138
144
  const manifest = {
139
145
  format: BACKUP_FORMAT,
@@ -206,6 +212,9 @@ function verifyBackupManifest(manifestPath) {
206
212
  if (JSON.stringify(health.counts) !== JSON.stringify(manifest.counts)) {
207
213
  throw new Error("backup manifest row counts do not match");
208
214
  }
215
+ if (health.schemaVersion !== undefined && health.schemaVersion !== manifest.sourceSchema) {
216
+ throw new Error("backup manifest source schema does not match database");
217
+ }
209
218
  } finally {
210
219
  db.close();
211
220
  }
@@ -391,7 +400,7 @@ function acquireMigrationLock(databasePath, targetSchema, timeoutMs = 30000) {
391
400
  rmSync2(path, { recursive: true, force: true });
392
401
  throw error;
393
402
  }
394
- if (!existsSync2(path))
403
+ if (!isAlreadyExistsError(error))
395
404
  throw error;
396
405
  if (Date.now() >= deadline) {
397
406
  const current = readMigrationLockOwner(path);
@@ -464,6 +473,9 @@ function processIsAlive(pid) {
464
473
  return false;
465
474
  }
466
475
  }
476
+ function isAlreadyExistsError(error) {
477
+ return Boolean(error && typeof error === "object" && "code" in error && String(error.code) === "EEXIST");
478
+ }
467
479
 
468
480
  // src/db/migrations/v009.ts
469
481
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
@@ -866,7 +878,7 @@ function migrateV9ToV10(db) {
866
878
  }
867
879
 
868
880
  // src/version.ts
869
- var PRODUCT_VERSION = "0.4.0";
881
+ var PRODUCT_VERSION = "0.4.1";
870
882
 
871
883
  // src/db.ts
872
884
  var DDL = `
@@ -909,13 +921,11 @@ CREATE INDEX IF NOT EXISTS note_edges_target_idx ON note_edges(project_id, targe
909
921
  CREATE TABLE IF NOT EXISTS schema_state (version INTEGER PRIMARY KEY);
910
922
  `;
911
923
  function openMemoryDatabase(path) {
912
- const db = new Database2(path, { create: true });
913
- chmodSync2(path, 384);
924
+ let db = openDatabase(path);
925
+ let dbOpen = true;
914
926
  let lock;
915
927
  let backup;
916
928
  try {
917
- db.exec("PRAGMA busy_timeout=5000");
918
- db.exec("PRAGMA journal_mode=WAL");
919
929
  const existingVersion = getSchemaVersion(db);
920
930
  if (existingVersion && existingVersion.version > SCHEMA_VERSION) {
921
931
  throw new Error(`database schema v${existingVersion.version} is newer than supported v${SCHEMA_VERSION}`);
@@ -928,14 +938,31 @@ function openMemoryDatabase(path) {
928
938
  return { db, close: () => db.close() };
929
939
  }
930
940
  if ((existingVersion?.version ?? 0) < SCHEMA_VERSION) {
941
+ db.close();
942
+ dbOpen = false;
931
943
  lock = acquireMigrationLock(path, SCHEMA_VERSION);
932
- backup = createVerifiedBackup(db, path, existingVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
944
+ db = openDatabase(path);
945
+ dbOpen = true;
946
+ const migrationVersion = getSchemaVersion(db);
947
+ if (migrationVersion && migrationVersion.version > SCHEMA_VERSION) {
948
+ throw new Error(`database schema v${migrationVersion.version} is newer than supported v${SCHEMA_VERSION}`);
949
+ }
950
+ if ((migrationVersion?.version ?? 0) === SCHEMA_VERSION) {
951
+ lock.release();
952
+ lock = undefined;
953
+ db.exec("PRAGMA foreign_keys=ON");
954
+ db.exec(SCHEMA_TABLES);
955
+ db.exec(FTS_V9);
956
+ assertHealthyDatabase(db);
957
+ return { db, close: () => db.close() };
958
+ }
959
+ backup = createVerifiedBackup(db, path, migrationVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
933
960
  db.exec("PRAGMA foreign_keys=OFF");
934
- if (!existingVersion && hasLegacyV2(db)) {
961
+ if (!migrationVersion && hasLegacyV2(db)) {
935
962
  db.exec(DDL);
936
963
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
937
964
  migrateFromV2(db, path);
938
- } else if (!existingVersion) {
965
+ } else if (!migrationVersion) {
939
966
  db.exec(DDL);
940
967
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
941
968
  db.transaction(() => {
@@ -943,7 +970,7 @@ function openMemoryDatabase(path) {
943
970
  db.query("DELETE FROM schema_state").run();
944
971
  db.query("INSERT INTO schema_state (version) VALUES (8)").run();
945
972
  })();
946
- } else if (existingVersion.version < 8) {
973
+ } else if (migrationVersion.version < 8) {
947
974
  db.exec(DDL);
948
975
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
949
976
  migrateToV8(db);
@@ -972,7 +999,8 @@ function openMemoryDatabase(path) {
972
999
  db.exec("PRAGMA foreign_keys=ON");
973
1000
  return { db, close: () => db.close() };
974
1001
  } catch (error) {
975
- db.close();
1002
+ if (dbOpen)
1003
+ db.close();
976
1004
  if (backup) {
977
1005
  try {
978
1006
  restoreVerifiedBackup(backup.manifestPath, path, "RESTORE_DATABASE_FROM_VERIFIED_BACKUP");
@@ -985,6 +1013,18 @@ function openMemoryDatabase(path) {
985
1013
  lock?.release();
986
1014
  }
987
1015
  }
1016
+ function openDatabase(path) {
1017
+ const db = new Database2(path, { create: true });
1018
+ try {
1019
+ chmodSync2(path, 384);
1020
+ db.exec("PRAGMA busy_timeout=5000");
1021
+ db.exec("PRAGMA journal_mode=WAL");
1022
+ return db;
1023
+ } catch (error) {
1024
+ db.close();
1025
+ throw error;
1026
+ }
1027
+ }
988
1028
  function getSchemaVersion(db) {
989
1029
  if (!hasTable2(db, "schema_state"))
990
1030
  return;
package/dist/core.js CHANGED
@@ -95,6 +95,9 @@ function hasTable(db, table) {
95
95
  var BACKUP_FORMAT = "agz-memory-backup/1";
96
96
  function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, productVersion) {
97
97
  const sourceHealth = assertHealthyDatabase(db);
98
+ if (sourceHealth.schemaVersion !== undefined && sourceHealth.schemaVersion !== sourceSchema) {
99
+ throw new Error(`backup source schema v${sourceSchema} does not match database schema v${sourceHealth.schemaVersion}`);
100
+ }
98
101
  const checkpoint = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
99
102
  if (checkpoint.busy !== 0)
100
103
  throw new Error("database WAL checkpoint is busy");
@@ -122,6 +125,9 @@ function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, prod
122
125
  if (JSON.stringify(backupHealth.counts) !== JSON.stringify(sourceHealth.counts)) {
123
126
  throw new Error("backup row counts differ from source database");
124
127
  }
128
+ if (backupHealth.schemaVersion !== undefined && backupHealth.schemaVersion !== sourceSchema) {
129
+ throw new Error("backup schema does not match source schema");
130
+ }
125
131
  const bytes = readFileSync(temporaryDatabasePath);
126
132
  const manifest = {
127
133
  format: BACKUP_FORMAT,
@@ -194,6 +200,9 @@ function verifyBackupManifest(manifestPath) {
194
200
  if (JSON.stringify(health.counts) !== JSON.stringify(manifest.counts)) {
195
201
  throw new Error("backup manifest row counts do not match");
196
202
  }
203
+ if (health.schemaVersion !== undefined && health.schemaVersion !== manifest.sourceSchema) {
204
+ throw new Error("backup manifest source schema does not match database");
205
+ }
197
206
  } finally {
198
207
  db.close();
199
208
  }
@@ -379,7 +388,7 @@ function acquireMigrationLock(databasePath, targetSchema, timeoutMs = 30000) {
379
388
  rmSync2(path, { recursive: true, force: true });
380
389
  throw error;
381
390
  }
382
- if (!existsSync2(path))
391
+ if (!isAlreadyExistsError(error))
383
392
  throw error;
384
393
  if (Date.now() >= deadline) {
385
394
  const current = readMigrationLockOwner(path);
@@ -422,6 +431,9 @@ function processStartMarker(pid) {
422
431
  return;
423
432
  }
424
433
  }
434
+ function isAlreadyExistsError(error) {
435
+ return Boolean(error && typeof error === "object" && "code" in error && String(error.code) === "EEXIST");
436
+ }
425
437
 
426
438
  // src/db/migrations/v009.ts
427
439
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
@@ -824,7 +836,7 @@ function migrateV9ToV10(db) {
824
836
  }
825
837
 
826
838
  // src/version.ts
827
- var PRODUCT_VERSION = "0.4.0";
839
+ var PRODUCT_VERSION = "0.4.1";
828
840
 
829
841
  // src/db.ts
830
842
  var DDL = `
@@ -867,13 +879,11 @@ CREATE INDEX IF NOT EXISTS note_edges_target_idx ON note_edges(project_id, targe
867
879
  CREATE TABLE IF NOT EXISTS schema_state (version INTEGER PRIMARY KEY);
868
880
  `;
869
881
  function openMemoryDatabase(path) {
870
- const db = new Database2(path, { create: true });
871
- chmodSync2(path, 384);
882
+ let db = openDatabase(path);
883
+ let dbOpen = true;
872
884
  let lock;
873
885
  let backup;
874
886
  try {
875
- db.exec("PRAGMA busy_timeout=5000");
876
- db.exec("PRAGMA journal_mode=WAL");
877
887
  const existingVersion = getSchemaVersion(db);
878
888
  if (existingVersion && existingVersion.version > SCHEMA_VERSION) {
879
889
  throw new Error(`database schema v${existingVersion.version} is newer than supported v${SCHEMA_VERSION}`);
@@ -886,14 +896,31 @@ function openMemoryDatabase(path) {
886
896
  return { db, close: () => db.close() };
887
897
  }
888
898
  if ((existingVersion?.version ?? 0) < SCHEMA_VERSION) {
899
+ db.close();
900
+ dbOpen = false;
889
901
  lock = acquireMigrationLock(path, SCHEMA_VERSION);
890
- backup = createVerifiedBackup(db, path, existingVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
902
+ db = openDatabase(path);
903
+ dbOpen = true;
904
+ const migrationVersion = getSchemaVersion(db);
905
+ if (migrationVersion && migrationVersion.version > SCHEMA_VERSION) {
906
+ throw new Error(`database schema v${migrationVersion.version} is newer than supported v${SCHEMA_VERSION}`);
907
+ }
908
+ if ((migrationVersion?.version ?? 0) === SCHEMA_VERSION) {
909
+ lock.release();
910
+ lock = undefined;
911
+ db.exec("PRAGMA foreign_keys=ON");
912
+ db.exec(SCHEMA_TABLES);
913
+ db.exec(FTS_V9);
914
+ assertHealthyDatabase(db);
915
+ return { db, close: () => db.close() };
916
+ }
917
+ backup = createVerifiedBackup(db, path, migrationVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
891
918
  db.exec("PRAGMA foreign_keys=OFF");
892
- if (!existingVersion && hasLegacyV2(db)) {
919
+ if (!migrationVersion && hasLegacyV2(db)) {
893
920
  db.exec(DDL);
894
921
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
895
922
  migrateFromV2(db, path);
896
- } else if (!existingVersion) {
923
+ } else if (!migrationVersion) {
897
924
  db.exec(DDL);
898
925
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
899
926
  db.transaction(() => {
@@ -901,7 +928,7 @@ function openMemoryDatabase(path) {
901
928
  db.query("DELETE FROM schema_state").run();
902
929
  db.query("INSERT INTO schema_state (version) VALUES (8)").run();
903
930
  })();
904
- } else if (existingVersion.version < 8) {
931
+ } else if (migrationVersion.version < 8) {
905
932
  db.exec(DDL);
906
933
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
907
934
  migrateToV8(db);
@@ -930,7 +957,8 @@ function openMemoryDatabase(path) {
930
957
  db.exec("PRAGMA foreign_keys=ON");
931
958
  return { db, close: () => db.close() };
932
959
  } catch (error) {
933
- db.close();
960
+ if (dbOpen)
961
+ db.close();
934
962
  if (backup) {
935
963
  try {
936
964
  restoreVerifiedBackup(backup.manifestPath, path, "RESTORE_DATABASE_FROM_VERIFIED_BACKUP");
@@ -943,6 +971,18 @@ function openMemoryDatabase(path) {
943
971
  lock?.release();
944
972
  }
945
973
  }
974
+ function openDatabase(path) {
975
+ const db = new Database2(path, { create: true });
976
+ try {
977
+ chmodSync2(path, 384);
978
+ db.exec("PRAGMA busy_timeout=5000");
979
+ db.exec("PRAGMA journal_mode=WAL");
980
+ return db;
981
+ } catch (error) {
982
+ db.close();
983
+ throw error;
984
+ }
985
+ }
946
986
  function getSchemaVersion(db) {
947
987
  if (!hasTable2(db, "schema_state"))
948
988
  return;
package/dist/server.js CHANGED
@@ -110,6 +110,9 @@ function hasTable(db, table) {
110
110
  var BACKUP_FORMAT = "agz-memory-backup/1";
111
111
  function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, productVersion) {
112
112
  const sourceHealth = assertHealthyDatabase(db);
113
+ if (sourceHealth.schemaVersion !== undefined && sourceHealth.schemaVersion !== sourceSchema) {
114
+ throw new Error(`backup source schema v${sourceSchema} does not match database schema v${sourceHealth.schemaVersion}`);
115
+ }
113
116
  const checkpoint = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
114
117
  if (checkpoint.busy !== 0)
115
118
  throw new Error("database WAL checkpoint is busy");
@@ -137,6 +140,9 @@ function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, prod
137
140
  if (JSON.stringify(backupHealth.counts) !== JSON.stringify(sourceHealth.counts)) {
138
141
  throw new Error("backup row counts differ from source database");
139
142
  }
143
+ if (backupHealth.schemaVersion !== undefined && backupHealth.schemaVersion !== sourceSchema) {
144
+ throw new Error("backup schema does not match source schema");
145
+ }
140
146
  const bytes = readFileSync(temporaryDatabasePath);
141
147
  const manifest = {
142
148
  format: BACKUP_FORMAT,
@@ -209,6 +215,9 @@ function verifyBackupManifest(manifestPath) {
209
215
  if (JSON.stringify(health.counts) !== JSON.stringify(manifest.counts)) {
210
216
  throw new Error("backup manifest row counts do not match");
211
217
  }
218
+ if (health.schemaVersion !== undefined && health.schemaVersion !== manifest.sourceSchema) {
219
+ throw new Error("backup manifest source schema does not match database");
220
+ }
212
221
  } finally {
213
222
  db.close();
214
223
  }
@@ -394,7 +403,7 @@ function acquireMigrationLock(databasePath, targetSchema, timeoutMs = 30000) {
394
403
  rmSync2(path, { recursive: true, force: true });
395
404
  throw error;
396
405
  }
397
- if (!existsSync2(path))
406
+ if (!isAlreadyExistsError(error))
398
407
  throw error;
399
408
  if (Date.now() >= deadline) {
400
409
  const current = readMigrationLockOwner(path);
@@ -437,6 +446,9 @@ function processStartMarker(pid) {
437
446
  return;
438
447
  }
439
448
  }
449
+ function isAlreadyExistsError(error) {
450
+ return Boolean(error && typeof error === "object" && "code" in error && String(error.code) === "EEXIST");
451
+ }
440
452
 
441
453
  // src/db/migrations/v009.ts
442
454
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
@@ -839,7 +851,7 @@ function migrateV9ToV10(db) {
839
851
  }
840
852
 
841
853
  // src/version.ts
842
- var PRODUCT_VERSION = "0.4.0";
854
+ var PRODUCT_VERSION = "0.4.1";
843
855
 
844
856
  // src/db.ts
845
857
  var DDL = `
@@ -882,13 +894,11 @@ CREATE INDEX IF NOT EXISTS note_edges_target_idx ON note_edges(project_id, targe
882
894
  CREATE TABLE IF NOT EXISTS schema_state (version INTEGER PRIMARY KEY);
883
895
  `;
884
896
  function openMemoryDatabase(path) {
885
- const db = new Database2(path, { create: true });
886
- chmodSync2(path, 384);
897
+ let db = openDatabase(path);
898
+ let dbOpen = true;
887
899
  let lock;
888
900
  let backup;
889
901
  try {
890
- db.exec("PRAGMA busy_timeout=5000");
891
- db.exec("PRAGMA journal_mode=WAL");
892
902
  const existingVersion = getSchemaVersion(db);
893
903
  if (existingVersion && existingVersion.version > SCHEMA_VERSION) {
894
904
  throw new Error(`database schema v${existingVersion.version} is newer than supported v${SCHEMA_VERSION}`);
@@ -901,14 +911,31 @@ function openMemoryDatabase(path) {
901
911
  return { db, close: () => db.close() };
902
912
  }
903
913
  if ((existingVersion?.version ?? 0) < SCHEMA_VERSION) {
914
+ db.close();
915
+ dbOpen = false;
904
916
  lock = acquireMigrationLock(path, SCHEMA_VERSION);
905
- backup = createVerifiedBackup(db, path, existingVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
917
+ db = openDatabase(path);
918
+ dbOpen = true;
919
+ const migrationVersion = getSchemaVersion(db);
920
+ if (migrationVersion && migrationVersion.version > SCHEMA_VERSION) {
921
+ throw new Error(`database schema v${migrationVersion.version} is newer than supported v${SCHEMA_VERSION}`);
922
+ }
923
+ if ((migrationVersion?.version ?? 0) === SCHEMA_VERSION) {
924
+ lock.release();
925
+ lock = undefined;
926
+ db.exec("PRAGMA foreign_keys=ON");
927
+ db.exec(SCHEMA_TABLES);
928
+ db.exec(FTS_V9);
929
+ assertHealthyDatabase(db);
930
+ return { db, close: () => db.close() };
931
+ }
932
+ backup = createVerifiedBackup(db, path, migrationVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
906
933
  db.exec("PRAGMA foreign_keys=OFF");
907
- if (!existingVersion && hasLegacyV2(db)) {
934
+ if (!migrationVersion && hasLegacyV2(db)) {
908
935
  db.exec(DDL);
909
936
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
910
937
  migrateFromV2(db, path);
911
- } else if (!existingVersion) {
938
+ } else if (!migrationVersion) {
912
939
  db.exec(DDL);
913
940
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
914
941
  db.transaction(() => {
@@ -916,7 +943,7 @@ function openMemoryDatabase(path) {
916
943
  db.query("DELETE FROM schema_state").run();
917
944
  db.query("INSERT INTO schema_state (version) VALUES (8)").run();
918
945
  })();
919
- } else if (existingVersion.version < 8) {
946
+ } else if (migrationVersion.version < 8) {
920
947
  db.exec(DDL);
921
948
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
922
949
  migrateToV8(db);
@@ -945,7 +972,8 @@ function openMemoryDatabase(path) {
945
972
  db.exec("PRAGMA foreign_keys=ON");
946
973
  return { db, close: () => db.close() };
947
974
  } catch (error) {
948
- db.close();
975
+ if (dbOpen)
976
+ db.close();
949
977
  if (backup) {
950
978
  try {
951
979
  restoreVerifiedBackup(backup.manifestPath, path, "RESTORE_DATABASE_FROM_VERIFIED_BACKUP");
@@ -958,6 +986,18 @@ function openMemoryDatabase(path) {
958
986
  lock?.release();
959
987
  }
960
988
  }
989
+ function openDatabase(path) {
990
+ const db = new Database2(path, { create: true });
991
+ try {
992
+ chmodSync2(path, 384);
993
+ db.exec("PRAGMA busy_timeout=5000");
994
+ db.exec("PRAGMA journal_mode=WAL");
995
+ return db;
996
+ } catch (error) {
997
+ db.close();
998
+ throw error;
999
+ }
1000
+ }
961
1001
  function getSchemaVersion(db) {
962
1002
  if (!hasTable2(db, "schema_state"))
963
1003
  return;
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
2
  import type { MemoryStore } from "./store";
3
3
  export declare const SERVER_NAME = "agz-memory";
4
- export declare const SERVER_VERSION: "0.4.0";
4
+ export declare const SERVER_VERSION: "0.4.1";
5
5
  export declare function createMemoryServer(store: MemoryStore): McpServer;
@@ -1 +1 @@
1
- export declare const PRODUCT_VERSION: "0.4.0";
1
+ export declare const PRODUCT_VERSION: "0.4.1";
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [Türkçe](backup-restore-runbook.tr.md)
4
4
 
5
- This runbook applies to `@vaur94/agz-memory@0.4.0` and SQLite schema v10.
5
+ This runbook applies to `@vaur94/agz-memory@0.4.1` and SQLite schema v10.
6
6
 
7
7
  ## Preconditions
8
8
 
@@ -25,16 +25,16 @@ Do not proceed with a guessed or empty path.
25
25
  Run a read-only health report first:
26
26
 
27
27
  ```sh
28
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin doctor
28
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin doctor
29
29
  ```
30
30
 
31
31
  `ok` must be `true`. Record `schemaVersion`, row counts, and invariant counts.
32
32
  Then create a standalone verified backup and upgrade:
33
33
 
34
34
  ```sh
35
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin backup
36
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin upgrade --to 10
37
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin doctor
35
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin backup
36
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin upgrade --to 10
37
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin doctor
38
38
  ```
39
39
 
40
40
  The upgrade itself creates another verified pre-migration backup when the
@@ -54,16 +54,16 @@ The manifest format is `agz-memory-backup/1`. `agz-memory-admin restore` verifie
54
54
  that the manifest and database are regular files in the same backup directory,
55
55
  then checks size, SHA-256, SQLite integrity, foreign keys, and row counts.
56
56
 
57
- Final `0.4.0` does not accept prerelease manifest formats. Use the originating
57
+ Final `0.4.1` does not accept prerelease manifest formats. Use the originating
58
58
  prerelease to restore such a backup, run its doctor check, and only then upgrade
59
- that restored database with `0.4.0`.
59
+ that restored database with `0.4.1`.
60
60
 
61
61
  ## Restore Rehearsal
62
62
 
63
63
  Keep all writers stopped. First request a dry run by omitting confirmation:
64
64
 
65
65
  ```sh
66
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin restore \
66
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin restore \
67
67
  "$OPENCODE_MEMORY_DATABASE_PATH.backup/<backup>.manifest.json"
68
68
  ```
69
69
 
@@ -71,7 +71,7 @@ Compare `targetPath`, `sourceSchema`, `targetSchema`, row counts, size, and
71
71
  SHA-256 with the recorded backup. Then use the exact manifest hash:
72
72
 
73
73
  ```sh
74
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin restore \
74
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin restore \
75
75
  "$OPENCODE_MEMORY_DATABASE_PATH.backup/<backup>.manifest.json" \
76
76
  --sha256 <manifest-database-sha256> \
77
77
  --confirm RESTORE_DATABASE_FROM_VERIFIED_BACKUP
@@ -84,9 +84,9 @@ database passes all checks.
84
84
  ## Post-Restore Validation
85
85
 
86
86
  ```sh
87
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin doctor
88
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin capture status
89
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin outbox status
87
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin doctor
88
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin capture status
89
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin outbox status
90
90
  ```
91
91
 
92
92
  Start only the MCP server and perform read-only `project_list`, `memory_recall`,
@@ -104,7 +104,7 @@ style error first if uncertain. Break only a proven stale lock with the exact
104
104
  owner ID and confirmation:
105
105
 
106
106
  ```sh
107
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin unlock \
107
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin unlock \
108
108
  --owner <owner-id> \
109
109
  --confirm BREAK_STALE_MIGRATION_LOCK
110
110
  ```
@@ -118,13 +118,13 @@ The first command is non-destructive and returns a digest over the exact backup
118
118
  set:
119
119
 
120
120
  ```sh
121
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin backup prune
121
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin backup prune
122
122
  ```
123
123
 
124
124
  Review every listed manifest/database pair. Delete only that unchanged set:
125
125
 
126
126
  ```sh
127
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin backup prune \
127
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin backup prune \
128
128
  --digest <dry-run-digest> \
129
129
  --confirm DELETE_VERIFIED_BACKUPS
130
130
  ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](backup-restore-runbook.md) | Türkçe
4
4
 
5
- Bu runbook `@vaur94/agz-memory@0.4.0` ve SQLite schema v10 için geçerlidir.
5
+ Bu runbook `@vaur94/agz-memory@0.4.1` ve SQLite schema v10 için geçerlidir.
6
6
 
7
7
  ## Ön Koşullar
8
8
 
@@ -25,16 +25,16 @@ Tahmin edilmiş veya boş bir yolla devam etmeyin.
25
25
  Önce salt-okunur sağlık raporu alın:
26
26
 
27
27
  ```sh
28
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin doctor
28
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin doctor
29
29
  ```
30
30
 
31
31
  `ok` değeri `true` olmalıdır. `schemaVersion`, satır sayıları ve değişmez kural
32
32
  sayılarını kaydedin. Sonra bağımsız doğrulanmış yedek oluşturup yükseltin:
33
33
 
34
34
  ```sh
35
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin backup
36
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin upgrade --to 10
37
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin doctor
35
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin backup
36
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin upgrade --to 10
37
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin doctor
38
38
  ```
39
39
 
40
40
  Veritabanı v10'dan eskiyse yükseltme ayrıca değişiklikten önce doğrulanmış yedek
@@ -54,16 +54,16 @@ Manifest formatı `agz-memory-backup/1` olur. `agz-memory-admin restore`, manife
54
54
  ile veritabanının aynı yedek dizinindeki normal dosyalar olduğunu doğrular;
55
55
  ardından boyut, SHA-256, SQLite bütünlüğü, foreign key ve satır sayılarını denetler.
56
56
 
57
- Final `0.4.0` ön sürüm manifest formatlarını kabul etmez. Böyle bir yedeği onu
57
+ Final `0.4.1` ön sürüm manifest formatlarını kabul etmez. Böyle bir yedeği onu
58
58
  oluşturan ön sürümle geri yükleyin, o sürümün doctor kontrolünü çalıştırın ve
59
- yalnız bundan sonra geri yüklenen veritabanını `0.4.0` ile yükseltin.
59
+ yalnız bundan sonra geri yüklenen veritabanını `0.4.1` ile yükseltin.
60
60
 
61
61
  ## Geri Yükleme Provası
62
62
 
63
63
  Tüm yazıcıları kapalı tutun. Önce onay vermeden deneme yapın:
64
64
 
65
65
  ```sh
66
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin restore \
66
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin restore \
67
67
  "$OPENCODE_MEMORY_DATABASE_PATH.backup/<backup>.manifest.json"
68
68
  ```
69
69
 
@@ -71,7 +71,7 @@ bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin restore \
71
71
  değerlerini kaydedilen yedekle karşılaştırın. Sonra tam manifest özetini kullanın:
72
72
 
73
73
  ```sh
74
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin restore \
74
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin restore \
75
75
  "$OPENCODE_MEMORY_DATABASE_PATH.backup/<backup>.manifest.json" \
76
76
  --sha256 <manifest-database-sha256> \
77
77
  --confirm RESTORE_DATABASE_FROM_VERIFIED_BACKUP
@@ -84,9 +84,9 @@ veritabanı tüm kontrollerden geçmeden bunu silmeyin.
84
84
  ## Geri Yükleme Sonrası Doğrulama
85
85
 
86
86
  ```sh
87
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin doctor
88
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin capture status
89
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin outbox status
87
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin doctor
88
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin capture status
89
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin outbox status
90
90
  ```
91
91
 
92
92
  Yalnız MCP sunucusunu başlatın ve salt-okunur `project_list`, `memory_recall` ve
@@ -103,7 +103,7 @@ Sahip dosyasındaki PID, makine ve başlangıç zamanını doğrulayın. Yalnız
103
103
  kanıtlanan kilidi tam sahip ID'si ve onayla kırın:
104
104
 
105
105
  ```sh
106
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin unlock \
106
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin unlock \
107
107
  --owner <owner-id> \
108
108
  --confirm BREAK_STALE_MIGRATION_LOCK
109
109
  ```
@@ -116,14 +116,14 @@ kanıtıdır; veritabanı doğrulamasını atlama izni değildir.
116
116
  İlk komut silme yapmaz ve tam yedek kümesinin özetini döndürür:
117
117
 
118
118
  ```sh
119
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin backup prune
119
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin backup prune
120
120
  ```
121
121
 
122
122
  Listelenen her manifest/veritabanı çiftini inceleyin. Yalnız değişmemiş kümeyi
123
123
  silin:
124
124
 
125
125
  ```sh
126
- bunx --package @vaur94/agz-memory@0.4.0 agz-memory-admin backup prune \
126
+ bunx --package @vaur94/agz-memory@0.4.1 agz-memory-admin backup prune \
127
127
  --digest <dry-run-digest> \
128
128
  --confirm DELETE_VERIFIED_BACKUPS
129
129
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaur94/agz-memory",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Project-scoped persistent linked memory MCP server for OpenCode V2",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",