@vaur94/agz-memory 0.4.0-beta.1 → 0.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.
package/dist/server.js CHANGED
@@ -43,7 +43,7 @@ function validateProjectName(value) {
43
43
  }
44
44
 
45
45
  // src/types.ts
46
- var SCHEMA_VERSION = 9;
46
+ var SCHEMA_VERSION = 10;
47
47
  var INLINE_LIMIT = 1200;
48
48
  var KINDS = ["decision", "fact", "procedure", "context", "research", "preference", "task"];
49
49
  var PREDICATES = ["SUPPORTS", "DERIVED_FROM", "PART_OF", "ABOUT", "PRECEDES", "SUPERSEDES"];
@@ -107,7 +107,7 @@ function hasTable(db, table) {
107
107
  }
108
108
 
109
109
  // src/db/backup.ts
110
- var BACKUP_FORMAT = "opencode2-memory-backup/1";
110
+ var BACKUP_FORMAT = "agz-memory-backup/1";
111
111
  function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, productVersion) {
112
112
  const sourceHealth = assertHealthyDatabase(db);
113
113
  const checkpoint = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
@@ -441,8 +441,77 @@ function processStartMarker(pid) {
441
441
  // src/db/migrations/v009.ts
442
442
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
443
443
 
444
+ // src/capture/contract.ts
445
+ import * as z from "zod/v4";
446
+ var CAPTURE_SCHEMA = "agz-memory.capture/1";
447
+ var SUPPORTED_OPENCODE_VERSION = "0.0.0-beta-18743";
448
+ var CAPTURE_EVENT_MAX_BYTES = 16 * 1024;
449
+ var CAPTURE_CONTENT_MAX_CHARACTERS = 4800;
450
+ var CAPTURE_SUMMARY_MAX_CHARACTERS = 1200;
451
+ var CAPTURE_SUBJECT_MAX_CHARACTERS = 240;
452
+ var candidateSchema = z.object({
453
+ kind: z.enum(KINDS),
454
+ title: z.string().min(1).max(240),
455
+ summary: z.string().min(1).max(CAPTURE_SUMMARY_MAX_CHARACTERS),
456
+ content: z.string().min(1).max(CAPTURE_CONTENT_MAX_CHARACTERS),
457
+ subjectKey: z.string().min(1).max(CAPTURE_SUBJECT_MAX_CHARACTERS).optional(),
458
+ intent: z.enum(["create", "supersede", "ignore", "review"]),
459
+ targetNoteID: z.string().min(1).max(240).optional(),
460
+ confidence: z.number().finite().min(0).max(1),
461
+ evidence: z.enum(["explicit-user", "verified-outcome", "session-summary"])
462
+ }).strict();
463
+ var signalSchema = z.object({
464
+ tool: z.string().min(1).max(160),
465
+ status: z.enum(["completed", "error"]),
466
+ errorType: z.string().min(1).max(160).optional()
467
+ }).strict();
468
+ var captureEventSchema = z.object({
469
+ schema: z.literal(CAPTURE_SCHEMA),
470
+ idempotencyKey: z.string().regex(/^[0-9a-f]{64}$/),
471
+ projectID: z.uuid(),
472
+ bindingKey: z.string().regex(/^[0-9a-f]{64}$/),
473
+ kind: z.enum(["user-candidate", "assistant-candidate", "session-summary", "tool-signal"]),
474
+ source: z.object({
475
+ system: z.literal("opencode-v2"),
476
+ opencodeVersion: z.literal(SUPPORTED_OPENCODE_VERSION),
477
+ pluginVersion: z.string().min(1).max(80),
478
+ sessionID: z.string().min(1).max(240),
479
+ messageID: z.string().min(1).max(240).optional(),
480
+ ordinal: z.number().int().nonnegative().optional(),
481
+ toolCallID: z.string().min(1).max(240).optional(),
482
+ observedAt: z.number().int().nonnegative()
483
+ }).strict(),
484
+ candidate: candidateSchema.optional(),
485
+ signal: signalSchema.optional(),
486
+ redaction: z.object({
487
+ policyVersion: z.string().min(1).max(80),
488
+ replacements: z.number().int().nonnegative(),
489
+ truncated: z.boolean()
490
+ }).strict()
491
+ }).strict().superRefine((event, context) => {
492
+ if (event.kind === "tool-signal" && !event.signal) {
493
+ context.addIssue({ code: "custom", message: "tool-signal requires signal" });
494
+ }
495
+ if (event.kind !== "tool-signal" && !event.candidate) {
496
+ context.addIssue({ code: "custom", message: `${event.kind} requires candidate` });
497
+ }
498
+ if (event.kind === "tool-signal" && event.candidate) {
499
+ context.addIssue({ code: "custom", message: "tool-signal cannot carry candidate" });
500
+ }
501
+ if (event.kind !== "tool-signal" && event.signal) {
502
+ context.addIssue({ code: "custom", message: `${event.kind} cannot carry signal` });
503
+ }
504
+ });
505
+ function parseCaptureEvent(value) {
506
+ const event = captureEventSchema.parse(value);
507
+ if (Buffer.byteLength(JSON.stringify(event), "utf8") > CAPTURE_EVENT_MAX_BYTES) {
508
+ throw new Error(`capture event exceeds ${CAPTURE_EVENT_MAX_BYTES} UTF-8 bytes`);
509
+ }
510
+ return event;
511
+ }
512
+
444
513
  // src/db/schema.ts
445
- var SCHEMA_V9_TABLES = `
514
+ var SCHEMA_TABLES = `
446
515
  CREATE TABLE IF NOT EXISTS projects (
447
516
  id TEXT PRIMARY KEY,
448
517
  name TEXT NOT NULL,
@@ -512,28 +581,7 @@ CREATE TABLE IF NOT EXISTS capture_checkpoints (
512
581
  );
513
582
  CREATE INDEX IF NOT EXISTS capture_checkpoints_due_idx
514
583
  ON capture_checkpoints(state, next_reconcile_at);
515
- CREATE TABLE IF NOT EXISTS capture_events (
516
- idempotency_key TEXT PRIMARY KEY,
517
- contract TEXT NOT NULL CHECK (contract = 'opencode2-memory.capture/1'),
518
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
519
- binding_key TEXT NOT NULL REFERENCES project_bindings(binding_key) ON DELETE CASCADE,
520
- event_kind TEXT NOT NULL CHECK (event_kind IN ('user-candidate','assistant-candidate','session-summary','tool-signal')),
521
- source_session_id TEXT NOT NULL,
522
- source_message_id TEXT,
523
- source_ordinal INTEGER CHECK (source_ordinal IS NULL OR source_ordinal >= 0),
524
- source_tool_call_id TEXT,
525
- payload_json TEXT CHECK (payload_json IS NULL OR json_valid(payload_json)),
526
- payload_hash TEXT,
527
- redaction_version TEXT NOT NULL,
528
- state TEXT NOT NULL CHECK (state IN ('pending','shadowed','review','materialized','duplicate','ignored','rejected','quarantined','failed','dead')),
529
- attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
530
- note_id TEXT,
531
- last_error_code TEXT,
532
- generation INTEGER NOT NULL DEFAULT 0 CHECK (generation >= 0),
533
- created_at INTEGER NOT NULL,
534
- updated_at INTEGER NOT NULL,
535
- processed_at INTEGER
536
- );
584
+ ${captureEventsTable()}
537
585
  CREATE INDEX IF NOT EXISTS capture_events_state_idx ON capture_events(state, updated_at);
538
586
  CREATE INDEX IF NOT EXISTS capture_events_session_idx ON capture_events(project_id, source_session_id);
539
587
  CREATE INDEX IF NOT EXISTS capture_events_note_idx ON capture_events(project_id, note_id);
@@ -617,15 +665,39 @@ CREATE TRIGGER IF NOT EXISTS notes_fts_au AFTER UPDATE OF title, summary, conten
617
665
  VALUES (new.rowid, new.title, new.summary, new.content);
618
666
  END;
619
667
  `;
620
- function createSchemaV9(db) {
621
- db.exec(SCHEMA_V9_TABLES);
668
+ function createSchema(db) {
669
+ db.exec(SCHEMA_TABLES);
622
670
  db.exec(FTS_V9);
623
671
  db.query("DELETE FROM schema_state").run();
624
- db.query("INSERT INTO schema_state(version) VALUES (9)").run();
672
+ db.query("INSERT INTO schema_state(version) VALUES (10)").run();
625
673
  }
626
674
  function rebuildFts(db) {
627
675
  db.query("INSERT INTO notes_fts(notes_fts) VALUES ('rebuild')").run();
628
676
  }
677
+ function captureEventsTable(table = "capture_events") {
678
+ return `CREATE TABLE IF NOT EXISTS ${table} (
679
+ idempotency_key TEXT PRIMARY KEY,
680
+ contract TEXT NOT NULL CHECK (contract = '${CAPTURE_SCHEMA}'),
681
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
682
+ binding_key TEXT NOT NULL REFERENCES project_bindings(binding_key) ON DELETE CASCADE,
683
+ event_kind TEXT NOT NULL CHECK (event_kind IN ('user-candidate','assistant-candidate','session-summary','tool-signal')),
684
+ source_session_id TEXT NOT NULL,
685
+ source_message_id TEXT,
686
+ source_ordinal INTEGER CHECK (source_ordinal IS NULL OR source_ordinal >= 0),
687
+ source_tool_call_id TEXT,
688
+ payload_json TEXT CHECK (payload_json IS NULL OR json_valid(payload_json)),
689
+ payload_hash TEXT,
690
+ redaction_version TEXT NOT NULL,
691
+ state TEXT NOT NULL CHECK (state IN ('pending','shadowed','review','materialized','duplicate','ignored','rejected','quarantined','failed','dead')),
692
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
693
+ note_id TEXT,
694
+ last_error_code TEXT,
695
+ generation INTEGER NOT NULL DEFAULT 0 CHECK (generation >= 0),
696
+ created_at INTEGER NOT NULL,
697
+ updated_at INTEGER NOT NULL,
698
+ processed_at INTEGER
699
+ );`;
700
+ }
629
701
 
630
702
  // src/db/migrations/v009.ts
631
703
  function migrateV8ToV9(db) {
@@ -694,7 +766,7 @@ function migrateV8ToV9(db) {
694
766
  ALTER TABLE notes_v9 RENAME TO notes;
695
767
  ALTER TABLE note_edges_v9 RENAME TO note_edges;
696
768
  `);
697
- db.exec(SCHEMA_V9_TABLES);
769
+ db.exec(SCHEMA_TABLES);
698
770
  for (const note of notes) {
699
771
  const provenanceID = randomUUID3();
700
772
  db.query(`
@@ -718,6 +790,57 @@ function noteContentHash(kind, title, summary, content) {
718
790
  return createHash3("sha256").update(`${kind}\x00${title}\x00${summary}\x00${content}`, "utf8").digest("hex");
719
791
  }
720
792
 
793
+ // src/db/migrations/v010.ts
794
+ import { createHash as createHash4 } from "crypto";
795
+ function migrateV9ToV10(db) {
796
+ const payloads = db.query("SELECT idempotency_key, payload_json FROM capture_events WHERE payload_json IS NOT NULL").all();
797
+ const migratedPayloads = payloads.map((row) => {
798
+ const event = JSON.parse(row.payload_json);
799
+ if (!event || typeof event !== "object" || Array.isArray(event)) {
800
+ throw new Error(`capture event ${row.idempotency_key} payload is not an object`);
801
+ }
802
+ event.schema = CAPTURE_SCHEMA;
803
+ const payload = JSON.stringify(parseCaptureEvent(event));
804
+ return {
805
+ idempotencyKey: row.idempotency_key,
806
+ payload,
807
+ payloadHash: createHash4("sha256").update(payload, "utf8").digest("hex")
808
+ };
809
+ });
810
+ db.exec("DROP TABLE IF EXISTS capture_events_v10");
811
+ db.exec(captureEventsTable("capture_events_v10"));
812
+ db.query(`
813
+ INSERT INTO capture_events_v10
814
+ (idempotency_key, contract, project_id, binding_key, event_kind,
815
+ source_session_id, source_message_id, source_ordinal, source_tool_call_id,
816
+ payload_json, payload_hash, redaction_version, state, attempt_count,
817
+ note_id, last_error_code, generation, created_at, updated_at, processed_at)
818
+ SELECT idempotency_key, ?, project_id, binding_key, event_kind,
819
+ source_session_id, source_message_id, source_ordinal, source_tool_call_id,
820
+ payload_json, payload_hash, redaction_version, state, attempt_count,
821
+ note_id, last_error_code, generation, created_at, updated_at, processed_at
822
+ FROM capture_events
823
+ `).run(CAPTURE_SCHEMA);
824
+ const updatePayload = db.query(`
825
+ UPDATE capture_events_v10
826
+ SET payload_json = ?, payload_hash = ?
827
+ WHERE idempotency_key = ?
828
+ `);
829
+ for (const row of migratedPayloads) {
830
+ updatePayload.run(row.payload, row.payloadHash, row.idempotencyKey);
831
+ }
832
+ db.exec(`
833
+ DROP TABLE capture_events;
834
+ ALTER TABLE capture_events_v10 RENAME TO capture_events;
835
+ `);
836
+ db.exec(SCHEMA_TABLES);
837
+ db.query("DELETE FROM schema_state").run();
838
+ db.query("INSERT INTO schema_state(version) VALUES (10)").run();
839
+ }
840
+
841
+ // src/version.ts
842
+ var PRODUCT_VERSION = "0.4.0";
843
+
721
844
  // src/db.ts
722
845
  var DDL = `
723
846
  CREATE TABLE IF NOT EXISTS projects (
@@ -773,13 +896,13 @@ function openMemoryDatabase(path) {
773
896
  const hasExistingData = hasLegacyV2(db) || hasTable2(db, "notes") || Boolean(existingVersion);
774
897
  if (!hasExistingData) {
775
898
  db.exec("PRAGMA foreign_keys=ON");
776
- db.transaction(() => createSchemaV9(db))();
899
+ db.transaction(() => createSchema(db))();
777
900
  assertHealthyDatabase(db);
778
901
  return { db, close: () => db.close() };
779
902
  }
780
903
  if ((existingVersion?.version ?? 0) < SCHEMA_VERSION) {
781
904
  lock = acquireMigrationLock(path, SCHEMA_VERSION);
782
- backup = createVerifiedBackup(db, path, existingVersion?.version ?? 2, SCHEMA_VERSION, "0.4.0-beta.1");
905
+ backup = createVerifiedBackup(db, path, existingVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
783
906
  db.exec("PRAGMA foreign_keys=OFF");
784
907
  if (!existingVersion && hasLegacyV2(db)) {
785
908
  db.exec(DDL);
@@ -798,9 +921,13 @@ function openMemoryDatabase(path) {
798
921
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
799
922
  migrateToV8(db);
800
923
  }
801
- const version = getSchemaVersion(db)?.version ?? 8;
802
- if (version < 9)
924
+ let version = getSchemaVersion(db)?.version ?? 8;
925
+ if (version < 9) {
803
926
  db.transaction(() => migrateV8ToV9(db))();
927
+ version = 9;
928
+ }
929
+ if (version < 10)
930
+ db.transaction(() => migrateV9ToV10(db))();
804
931
  db.exec("PRAGMA foreign_keys=ON");
805
932
  if (db.query("PRAGMA foreign_keys").get().foreign_keys !== 1) {
806
933
  throw new Error("failed to enable database foreign keys");
@@ -812,7 +939,7 @@ function openMemoryDatabase(path) {
812
939
  return { db, close: () => db.close() };
813
940
  }
814
941
  db.exec("PRAGMA foreign_keys=ON");
815
- db.exec(SCHEMA_V9_TABLES);
942
+ db.exec(SCHEMA_TABLES);
816
943
  db.exec(FTS_V9);
817
944
  assertHealthyDatabase(db);
818
945
  db.exec("PRAGMA foreign_keys=ON");
@@ -1093,29 +1220,29 @@ var MEMORY_GUIDANCE = `Use project-scoped memory for durable facts across sessio
1093
1220
  - project_delete permanently destroys the project and all of its memory. Call project_list first and provide the immutable ID, exact current name, and required confirmation phrase only when deletion is explicitly intended.`;
1094
1221
 
1095
1222
  // src/tools.ts
1096
- import * as z from "zod/v4";
1223
+ import * as z2 from "zod/v4";
1097
1224
  var MAX_BATCH = 10;
1098
- var projectID = z.uuid().describe("The immutable project UUID returned by project_create or project_list.");
1099
- var projectName = z.string().min(1).max(MAX_PROJECT_NAME_LENGTH).describe("The project's unique current name. Prefer projectID when retaining a long-lived reference.");
1100
- var createUpdateSchema = z.object({
1101
- kind: z.enum(KINDS),
1102
- title: z.string(),
1103
- summary: z.string(),
1104
- content: z.string().optional()
1225
+ var projectID = z2.uuid().describe("The immutable project UUID returned by project_create or project_list.");
1226
+ var projectName = z2.string().min(1).max(MAX_PROJECT_NAME_LENGTH).describe("The project's unique current name. Prefer projectID when retaining a long-lived reference.");
1227
+ var createUpdateSchema = z2.object({
1228
+ kind: z2.enum(KINDS),
1229
+ title: z2.string(),
1230
+ summary: z2.string(),
1231
+ content: z2.string().optional()
1105
1232
  }).strict();
1106
- var patchUpdateSchema = z.object({
1107
- id: z.string(),
1108
- kind: z.enum(KINDS).optional(),
1109
- title: z.string().optional(),
1110
- summary: z.string().optional(),
1111
- content: z.string().optional(),
1112
- delete: z.boolean().optional()
1233
+ var patchUpdateSchema = z2.object({
1234
+ id: z2.string(),
1235
+ kind: z2.enum(KINDS).optional(),
1236
+ title: z2.string().optional(),
1237
+ summary: z2.string().optional(),
1238
+ content: z2.string().optional(),
1239
+ delete: z2.boolean().optional()
1113
1240
  }).strict();
1114
- var updateSchema = z.union([createUpdateSchema, patchUpdateSchema]);
1115
- var linkSchema = z.object({
1116
- sourceID: z.string(),
1117
- targetID: z.string(),
1118
- predicate: z.enum(PREDICATES)
1241
+ var updateSchema = z2.union([createUpdateSchema, patchUpdateSchema]);
1242
+ var linkSchema = z2.object({
1243
+ sourceID: z2.string(),
1244
+ targetID: z2.string(),
1245
+ predicate: z2.enum(PREDICATES)
1119
1246
  }).strict();
1120
1247
  function textResult(value) {
1121
1248
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
@@ -1128,39 +1255,39 @@ function registerTools(server, store) {
1128
1255
  server.registerTool("project_list", {
1129
1256
  title: "List memory projects",
1130
1257
  description: "List all memory projects with their immutable IDs, current names, note counts, and pinned-note counts. Use this before selecting a project by ID.",
1131
- inputSchema: z.object({}).strict(),
1258
+ inputSchema: z2.object({}).strict(),
1132
1259
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
1133
1260
  }, async () => textResult({ projects: store.listProjects() }));
1134
1261
  server.registerTool("project_create", {
1135
1262
  title: "Create a memory project",
1136
1263
  description: "Create an empty memory project. The returned projectID is immutable; the unique project name may be changed later.",
1137
- inputSchema: z.object({ projectName }).strict(),
1264
+ inputSchema: z2.object({ projectName }).strict(),
1138
1265
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
1139
1266
  }, async ({ projectName: projectName2 }) => textResult({ results: [store.createProject(projectName2)] }));
1140
1267
  server.registerTool("project_update", {
1141
1268
  title: "Rename a memory project",
1142
1269
  description: "Rename one project by its immutable projectID. Renaming does not change the ID or detach any notes.",
1143
- inputSchema: z.object({ projectID, projectName }).strict(),
1270
+ inputSchema: z2.object({ projectID, projectName }).strict(),
1144
1271
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1145
1272
  }, async ({ projectID: projectID2, projectName: projectName2 }) => textResult({ results: [store.updateProject(projectID2, projectName2)] }));
1146
1273
  server.registerTool("project_delete", {
1147
1274
  title: "Permanently delete a memory project",
1148
1275
  description: "DANGER: Permanently deletes the selected project and every note, pinned note, graph edge, and search record owned by it. This cannot be undone. First call project_list, verify the immutable projectID and current name, then provide both confirmation fields exactly.",
1149
- inputSchema: z.object({
1276
+ inputSchema: z2.object({
1150
1277
  projectID,
1151
1278
  confirmProjectName: projectName.describe("Must exactly match the project's current case-sensitive name. This prevents deletion after an unnoticed rename or wrong-ID selection."),
1152
- confirmation: z.literal("DELETE_PROJECT_AND_ALL_MEMORY").describe("Required destructive-action confirmation phrase.")
1279
+ confirmation: z2.literal("DELETE_PROJECT_AND_ALL_MEMORY").describe("Required destructive-action confirmation phrase.")
1153
1280
  }).strict(),
1154
1281
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
1155
1282
  }, async ({ projectID: projectID2, confirmProjectName }) => textResult({ results: [store.deleteProject(projectID2, confirmProjectName)] }));
1156
1283
  server.registerTool("memory_recall", {
1157
1284
  title: "Search project memory",
1158
1285
  description: "Search memory only inside one project selected by immutable projectID or unique projectName. Pass one query or up to 10 queries. Indexed cards require memory_read for full content.",
1159
- inputSchema: z.union([
1160
- z.object({ projectID, query: z.string() }).strict(),
1161
- z.object({ projectID, queries: z.array(z.string()).min(1).max(MAX_BATCH) }).strict(),
1162
- z.object({ projectName, query: z.string() }).strict(),
1163
- z.object({ projectName, queries: z.array(z.string()).min(1).max(MAX_BATCH) }).strict()
1286
+ inputSchema: z2.union([
1287
+ z2.object({ projectID, query: z2.string() }).strict(),
1288
+ z2.object({ projectID, queries: z2.array(z2.string()).min(1).max(MAX_BATCH) }).strict(),
1289
+ z2.object({ projectName, query: z2.string() }).strict(),
1290
+ z2.object({ projectName, queries: z2.array(z2.string()).min(1).max(MAX_BATCH) }).strict()
1164
1291
  ]),
1165
1292
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
1166
1293
  }, async (raw) => {
@@ -1179,13 +1306,13 @@ function registerTools(server, store) {
1179
1306
  server.registerTool("memory_update", {
1180
1307
  title: "Create, patch, or delete project memory",
1181
1308
  description: "Create or patch notes only inside one selected project. Setting delete:true permanently deletes only the specified note from that project; verify the note ID before using delete. A batch contains up to 10 ordered, non-atomic updates: inspect every result because earlier items remain applied if a later item fails. Do not batch destructive deletes unless partial completion is acceptable. Pin state is changed only through memory_pin.",
1182
- inputSchema: z.union([
1309
+ inputSchema: z2.union([
1183
1310
  createUpdateSchema.extend({ projectID }),
1184
1311
  patchUpdateSchema.extend({ projectID }),
1185
- z.object({ projectID, updates: z.array(updateSchema).min(1).max(MAX_BATCH) }).strict(),
1312
+ z2.object({ projectID, updates: z2.array(updateSchema).min(1).max(MAX_BATCH) }).strict(),
1186
1313
  createUpdateSchema.extend({ projectName }),
1187
1314
  patchUpdateSchema.extend({ projectName }),
1188
- z.object({ projectName, updates: z.array(updateSchema).min(1).max(MAX_BATCH) }).strict()
1315
+ z2.object({ projectName, updates: z2.array(updateSchema).min(1).max(MAX_BATCH) }).strict()
1189
1316
  ]),
1190
1317
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
1191
1318
  }, async (raw) => {
@@ -1201,9 +1328,9 @@ function registerTools(server, store) {
1201
1328
  server.registerTool("memory_pin", {
1202
1329
  title: "Pin or unpin project memory",
1203
1330
  description: "Set the pinned state of one active note inside one selected project. Pinned matching notes are prioritized in recall results. This tool never deletes content.",
1204
- inputSchema: z.union([
1205
- z.object({ projectID, id: z.string(), pinned: z.boolean() }).strict(),
1206
- z.object({ projectName, id: z.string(), pinned: z.boolean() }).strict()
1331
+ inputSchema: z2.union([
1332
+ z2.object({ projectID, id: z2.string(), pinned: z2.boolean() }).strict(),
1333
+ z2.object({ projectName, id: z2.string(), pinned: z2.boolean() }).strict()
1207
1334
  ]),
1208
1335
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1209
1336
  }, async (raw) => {
@@ -1218,11 +1345,11 @@ function registerTools(server, store) {
1218
1345
  server.registerTool("memory_link", {
1219
1346
  title: "Link project memories",
1220
1347
  description: `Create one graph edge or up to 10 ordered, non-atomic edge operations between notes in the same selected project; inspect every result because earlier links remain applied if a later item fails. Cross-project links are rejected. Predicates: ${PREDICATES.join(", ")}.`,
1221
- inputSchema: z.union([
1348
+ inputSchema: z2.union([
1222
1349
  linkSchema.extend({ projectID }),
1223
- z.object({ projectID, links: z.array(linkSchema).min(1).max(MAX_BATCH) }).strict(),
1350
+ z2.object({ projectID, links: z2.array(linkSchema).min(1).max(MAX_BATCH) }).strict(),
1224
1351
  linkSchema.extend({ projectName }),
1225
- z.object({ projectName, links: z.array(linkSchema).min(1).max(MAX_BATCH) }).strict()
1352
+ z2.object({ projectName, links: z2.array(linkSchema).min(1).max(MAX_BATCH) }).strict()
1226
1353
  ]),
1227
1354
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1228
1355
  }, async (raw) => {
@@ -1238,11 +1365,11 @@ function registerTools(server, store) {
1238
1365
  server.registerTool("memory_read", {
1239
1366
  title: "Read project memory",
1240
1367
  description: "Read one note or up to 10 notes from one selected project, including full content, pin state, project identity, and same-project graph edges.",
1241
- inputSchema: z.union([
1242
- z.object({ projectID, id: z.string() }).strict(),
1243
- z.object({ projectID, ids: z.array(z.string()).min(1).max(MAX_BATCH) }).strict(),
1244
- z.object({ projectName, id: z.string() }).strict(),
1245
- z.object({ projectName, ids: z.array(z.string()).min(1).max(MAX_BATCH) }).strict()
1368
+ inputSchema: z2.union([
1369
+ z2.object({ projectID, id: z2.string() }).strict(),
1370
+ z2.object({ projectID, ids: z2.array(z2.string()).min(1).max(MAX_BATCH) }).strict(),
1371
+ z2.object({ projectName, id: z2.string() }).strict(),
1372
+ z2.object({ projectName, ids: z2.array(z2.string()).min(1).max(MAX_BATCH) }).strict()
1246
1373
  ]),
1247
1374
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
1248
1375
  }, async (raw) => {
@@ -1262,7 +1389,7 @@ function registerTools(server, store) {
1262
1389
 
1263
1390
  // src/server.ts
1264
1391
  var SERVER_NAME = "agz-memory";
1265
- var SERVER_VERSION = "0.4.0-beta.1";
1392
+ var SERVER_VERSION = PRODUCT_VERSION;
1266
1393
  function createMemoryServer(store) {
1267
1394
  const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions: MEMORY_GUIDANCE });
1268
1395
  registerTools(server, store);
@@ -1273,9 +1400,10 @@ function createMemoryServer(store) {
1273
1400
  import { randomUUID as randomUUID5 } from "crypto";
1274
1401
 
1275
1402
  // src/retrieval/derived.ts
1276
- import { createHash as createHash4 } from "crypto";
1403
+ import { createHash as createHash5 } from "crypto";
1277
1404
 
1278
1405
  // src/capture/redact.ts
1406
+ var REDACTION_POLICY_VERSION = "redaction/1";
1279
1407
  var RULES = [
1280
1408
  {
1281
1409
  name: "private-key",
@@ -1304,15 +1432,15 @@ function redactText(value, options = {}) {
1304
1432
  let replacements = 0;
1305
1433
  let highRisk = 0;
1306
1434
  const classes = {};
1307
- for (const literal2 of options.denylist ?? []) {
1308
- if (!literal2)
1435
+ for (const literal3 of options.denylist ?? []) {
1436
+ if (!literal3)
1309
1437
  continue;
1310
- const count = text.split(literal2).length - 1;
1438
+ const count = text.split(literal3).length - 1;
1311
1439
  if (count === 0)
1312
1440
  continue;
1313
1441
  replacements += count;
1314
1442
  classes.denylist = (classes.denylist ?? 0) + count;
1315
- text = text.replaceAll(literal2, "[REDACTED:denylist]");
1443
+ text = text.replaceAll(literal3, "[REDACTED:denylist]");
1316
1444
  }
1317
1445
  for (const rule of RULES) {
1318
1446
  text = text.replace(rule.pattern, () => {
@@ -1362,7 +1490,7 @@ function deriveDocument(source) {
1362
1490
  const content = redactText(source.content);
1363
1491
  if (title.quarantined || summary.quarantined || content.quarantined)
1364
1492
  return;
1365
- const contentHash = createHash4("sha256").update(`${source.kind}\x00${title.text}\x00${summary.text}\x00${content.text}`, "utf8").digest("hex");
1493
+ const contentHash = createHash5("sha256").update(`${source.kind}\x00${title.text}\x00${summary.text}\x00${content.text}`, "utf8").digest("hex");
1366
1494
  return {
1367
1495
  projectID: source.projectID,
1368
1496
  noteID: source.noteID,
@@ -1756,6 +1884,359 @@ function toCard(note, via) {
1756
1884
  };
1757
1885
  }
1758
1886
 
1887
+ // src/store/capture.ts
1888
+ import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
1889
+
1890
+ // src/capture/policy.ts
1891
+ var EXTRACTOR_VERSION = "deterministic-extractor/1";
1892
+ function canAutoWrite(candidate, redaction, allowedKinds = ["preference", "decision"], minConfidence = 0.95) {
1893
+ return candidate.evidence === "explicit-user" && candidate.confidence >= minConfidence && allowedKinds.includes(candidate.kind) && !redaction.truncated && !redaction.quarantined && (candidate.intent === "create" || candidate.intent === "supersede");
1894
+ }
1895
+ function normalizeSubjectKey(value) {
1896
+ return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en-US").slice(0, 240);
1897
+ }
1898
+
1899
+ // src/store/capture.ts
1900
+ class CaptureStore {
1901
+ db;
1902
+ indexBackends;
1903
+ constructor(db, indexBackends = []) {
1904
+ this.db = db;
1905
+ this.indexBackends = indexBackends;
1906
+ }
1907
+ bindProject(input) {
1908
+ const workspaceID = input.workspaceID ?? "";
1909
+ const canonicalPathHash = sha256(input.canonicalDirectory);
1910
+ const bindingKey = sha256(["opencode-v2", input.opencodeProjectID, workspaceID, canonicalPathHash].join("\x00"));
1911
+ const project = this.db.query("SELECT id FROM projects WHERE id = ?").get(input.memoryProjectID);
1912
+ if (!project)
1913
+ throw new Error(`memory project ${input.memoryProjectID} not found`);
1914
+ const existing = this.db.query(`SELECT * FROM project_bindings
1915
+ WHERE source = 'opencode-v2' AND source_project_id = ? AND workspace_id = ?`).get(input.opencodeProjectID, workspaceID);
1916
+ if (existing) {
1917
+ if (existing.binding_key !== bindingKey || existing.project_id !== input.memoryProjectID || existing.canonical_path_hash !== canonicalPathHash) {
1918
+ throw new Error("binding_conflict");
1919
+ }
1920
+ return { bindingKey, projectID: existing.project_id };
1921
+ }
1922
+ const now = Date.now();
1923
+ this.db.query(`
1924
+ INSERT INTO project_bindings
1925
+ (binding_key, project_id, source, source_project_id, workspace_id,
1926
+ canonical_path_hash, created_at, updated_at)
1927
+ VALUES (?, ?, 'opencode-v2', ?, ?, ?, ?, ?)
1928
+ `).run(bindingKey, input.memoryProjectID, input.opencodeProjectID, workspaceID, canonicalPathHash, now, now);
1929
+ return { bindingKey, projectID: input.memoryProjectID };
1930
+ }
1931
+ checkpoint(sessionID, bindingKey, projectID2, messageID, state = "active") {
1932
+ const binding = this.binding(bindingKey, projectID2);
1933
+ if (!binding)
1934
+ throw new Error("binding_conflict");
1935
+ const now = Date.now();
1936
+ this.db.query(`
1937
+ INSERT INTO capture_checkpoints
1938
+ (session_id, binding_key, project_id, state, last_message_id,
1939
+ next_reconcile_at, failure_count, created_at, updated_at)
1940
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
1941
+ ON CONFLICT(session_id) DO UPDATE SET
1942
+ binding_key = excluded.binding_key,
1943
+ project_id = excluded.project_id,
1944
+ state = excluded.state,
1945
+ last_message_id = COALESCE(excluded.last_message_id, capture_checkpoints.last_message_id),
1946
+ next_reconcile_at = excluded.next_reconcile_at,
1947
+ updated_at = excluded.updated_at
1948
+ WHERE capture_checkpoints.binding_key = excluded.binding_key
1949
+ AND capture_checkpoints.project_id = excluded.project_id
1950
+ `).run(sessionID, bindingKey, projectID2, state, messageID ?? null, now, now, now);
1951
+ const row = this.db.query("SELECT binding_key, project_id FROM capture_checkpoints WHERE session_id = ?").get(sessionID);
1952
+ if (!row || row.binding_key !== bindingKey || row.project_id !== projectID2) {
1953
+ throw new Error("checkpoint_binding_conflict");
1954
+ }
1955
+ }
1956
+ markReconciled(sessionID, state, lastMessageID, failed = false) {
1957
+ const now = Date.now();
1958
+ const result = this.db.query(`
1959
+ UPDATE capture_checkpoints
1960
+ SET state = ?,
1961
+ last_message_id = COALESCE(?, last_message_id),
1962
+ last_reconciled_at = ?,
1963
+ next_reconcile_at = ?,
1964
+ failure_count = CASE WHEN ? THEN failure_count + 1 ELSE 0 END,
1965
+ updated_at = ?
1966
+ WHERE session_id = ?
1967
+ `).run(state, lastMessageID ?? null, now, now + (failed ? 5000 : 30000), failed, now, sessionID);
1968
+ if (result.changes === 0)
1969
+ throw new Error(`checkpoint ${sessionID} not found`);
1970
+ }
1971
+ getCheckpoint(sessionID) {
1972
+ const row = this.db.query("SELECT session_id, last_message_id, state FROM capture_checkpoints WHERE session_id = ?").get(sessionID);
1973
+ return row ? {
1974
+ sessionID: row.session_id,
1975
+ ...row.last_message_id ? { lastMessageID: row.last_message_id } : {},
1976
+ state: row.state
1977
+ } : undefined;
1978
+ }
1979
+ ingest(input, mode, options = {}) {
1980
+ const parsed = parseCaptureEvent(input);
1981
+ if (!this.binding(parsed.bindingKey, parsed.projectID))
1982
+ throw new Error("binding_conflict");
1983
+ const prepared = prepareForPersistence(parsed, options.denylist);
1984
+ const now = Date.now();
1985
+ let result = {
1986
+ outcome: prepared.quarantined ? "quarantined" : "shadowed",
1987
+ idempotencyKey: parsed.idempotencyKey
1988
+ };
1989
+ this.db.transaction(() => {
1990
+ const inserted = this.db.query(`
1991
+ INSERT OR IGNORE INTO capture_events
1992
+ (idempotency_key, contract, project_id, binding_key, event_kind,
1993
+ source_session_id, source_message_id, source_ordinal, source_tool_call_id,
1994
+ payload_json, payload_hash, redaction_version, state, attempt_count,
1995
+ generation, created_at, updated_at, processed_at)
1996
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?)
1997
+ `).run(prepared.event.idempotencyKey, prepared.event.schema, prepared.event.projectID, prepared.event.bindingKey, prepared.event.kind, prepared.event.source.sessionID, prepared.event.source.messageID ?? null, prepared.event.source.ordinal ?? null, prepared.event.source.toolCallID ?? null, prepared.payload, prepared.payloadHash, REDACTION_POLICY_VERSION, prepared.quarantined ? "quarantined" : "pending", now, now, prepared.quarantined ? now : null);
1998
+ if (inserted.changes === 0) {
1999
+ const existing = this.db.query("SELECT state, note_id FROM capture_events WHERE idempotency_key = ?").get(parsed.idempotencyKey);
2000
+ result = {
2001
+ outcome: "duplicate",
2002
+ idempotencyKey: parsed.idempotencyKey,
2003
+ ...existing.note_id ? { noteID: existing.note_id } : {},
2004
+ existing: true
2005
+ };
2006
+ return;
2007
+ }
2008
+ if (prepared.quarantined)
2009
+ return;
2010
+ if (mode === "shadow") {
2011
+ this.finishEvent(parsed.idempotencyKey, "shadowed", null, now);
2012
+ result.outcome = "shadowed";
2013
+ return;
2014
+ }
2015
+ const candidate = prepared.event.candidate;
2016
+ if (!candidate) {
2017
+ this.finishEvent(parsed.idempotencyKey, "shadowed", null, now);
2018
+ result.outcome = "shadowed";
2019
+ return;
2020
+ }
2021
+ if (candidate.intent === "ignore") {
2022
+ this.finishEvent(parsed.idempotencyKey, "ignored", null, now);
2023
+ result.outcome = "ignored";
2024
+ return;
2025
+ }
2026
+ if (candidate.intent === "review" || !canAutoWrite(candidate, {
2027
+ truncated: prepared.event.redaction.truncated,
2028
+ quarantined: prepared.event.redaction.replacements > 0
2029
+ }, options.allowedKinds, options.minConfidence)) {
2030
+ this.finishEvent(parsed.idempotencyKey, "review", null, now);
2031
+ result.outcome = "review";
2032
+ return;
2033
+ }
2034
+ result = this.materialize(prepared.event, candidate, now);
2035
+ })();
2036
+ return result;
2037
+ }
2038
+ runRetention(now = Date.now(), batchSize = 100) {
2039
+ const terminalCutoff = now - 30 * 24 * 60 * 60 * 1000;
2040
+ const quarantineCutoff = now - 7 * 24 * 60 * 60 * 1000;
2041
+ const summarized = this.db.query(`
2042
+ UPDATE capture_events
2043
+ SET payload_json = NULL, payload_hash = NULL, updated_at = ?
2044
+ WHERE idempotency_key IN (
2045
+ SELECT idempotency_key FROM capture_events
2046
+ WHERE state IN ('materialized','duplicate','ignored','rejected','shadowed','review','failed','dead')
2047
+ AND processed_at < ? AND payload_json IS NOT NULL
2048
+ ORDER BY processed_at LIMIT ?
2049
+ )
2050
+ `).run(now, terminalCutoff, batchSize).changes;
2051
+ const deleted = this.db.query(`
2052
+ DELETE FROM capture_events
2053
+ WHERE idempotency_key IN (
2054
+ SELECT idempotency_key FROM capture_events
2055
+ WHERE state = 'quarantined' AND processed_at < ?
2056
+ ORDER BY processed_at LIMIT ?
2057
+ )
2058
+ `).run(quarantineCutoff, batchSize).changes;
2059
+ const checkpoints = this.db.query(`
2060
+ DELETE FROM capture_checkpoints
2061
+ WHERE session_id IN (
2062
+ SELECT session_id FROM capture_checkpoints
2063
+ WHERE (state IN ('idle','closed') AND updated_at < ?)
2064
+ OR (state = 'unavailable' AND updated_at < ?)
2065
+ ORDER BY updated_at LIMIT ?
2066
+ )
2067
+ `).run(terminalCutoff, quarantineCutoff, batchSize).changes;
2068
+ return { summarized, deleted, checkpoints };
2069
+ }
2070
+ runRetentionBacklog(now = Date.now(), batchSize = 100, maxBatches = 10) {
2071
+ const total = { summarized: 0, deleted: 0, checkpoints: 0 };
2072
+ for (let batch = 0;batch < maxBatches; batch++) {
2073
+ const result = this.runRetention(now, batchSize);
2074
+ total.summarized += result.summarized;
2075
+ total.deleted += result.deleted;
2076
+ total.checkpoints += result.checkpoints;
2077
+ if (result.summarized < batchSize && result.deleted < batchSize && result.checkpoints < batchSize)
2078
+ break;
2079
+ }
2080
+ return total;
2081
+ }
2082
+ materialize(event, candidate, now) {
2083
+ const subjectKey = candidate.subjectKey ? normalizeSubjectKey(candidate.subjectKey) : null;
2084
+ const hash = noteContentHash(candidate.kind, candidate.title, candidate.summary, candidate.content);
2085
+ const existing = subjectKey ? this.db.query(`SELECT * FROM notes
2086
+ WHERE project_id = ? AND kind = ? AND subject_key = ? AND status = 'active'`).get(event.projectID, candidate.kind, subjectKey) : undefined;
2087
+ if (existing?.content_hash === hash) {
2088
+ this.finishEvent(event.idempotencyKey, "duplicate", existing.id, now);
2089
+ return { outcome: "duplicate", idempotencyKey: event.idempotencyKey, noteID: existing.id };
2090
+ }
2091
+ if (existing) {
2092
+ if (candidate.intent !== "supersede" || candidate.targetNoteID !== existing.id || candidate.confidence < 0.95) {
2093
+ this.finishEvent(event.idempotencyKey, "review", existing.id, now);
2094
+ return { outcome: "review", idempotencyKey: event.idempotencyKey, noteID: existing.id };
2095
+ }
2096
+ this.db.query(`
2097
+ UPDATE notes
2098
+ SET status = 'superseded', current_revision = current_revision + 1, updated_at = ?
2099
+ WHERE project_id = ? AND id = ? AND status = 'active'
2100
+ `).run(now, event.projectID, existing.id);
2101
+ this.recordRevision(event, existing.id, now);
2102
+ const id2 = this.insertCapturedNote(event, candidate, subjectKey, existing.id, hash, now);
2103
+ this.db.query(`
2104
+ INSERT INTO note_edges
2105
+ (id, project_id, source_id, target_id, predicate, created_at)
2106
+ VALUES (?, ?, ?, ?, 'SUPERSEDES', ?)
2107
+ `).run(randomUUID6(), event.projectID, id2, existing.id, now);
2108
+ for (const backend of this.indexBackends) {
2109
+ this.enqueueOutbox(backend, "delete-note", event.projectID, existing.id, existing.current_revision + 1, existing.content_hash, now);
2110
+ const note2 = this.db.query("SELECT * FROM notes WHERE id = ?").get(id2);
2111
+ this.enqueueOutbox(backend, "upsert-note", event.projectID, id2, 1, derivedHash(note2), now);
2112
+ }
2113
+ this.finishEvent(event.idempotencyKey, "materialized", id2, now);
2114
+ return { outcome: "materialized", idempotencyKey: event.idempotencyKey, noteID: id2 };
2115
+ }
2116
+ if (candidate.intent === "supersede") {
2117
+ this.finishEvent(event.idempotencyKey, "review", candidate.targetNoteID ?? null, now);
2118
+ return {
2119
+ outcome: "review",
2120
+ idempotencyKey: event.idempotencyKey,
2121
+ ...candidate.targetNoteID ? { noteID: candidate.targetNoteID } : {}
2122
+ };
2123
+ }
2124
+ const id = this.insertCapturedNote(event, candidate, subjectKey, null, hash, now);
2125
+ const note = this.db.query("SELECT * FROM notes WHERE id = ?").get(id);
2126
+ for (const backend of this.indexBackends) {
2127
+ this.enqueueOutbox(backend, "upsert-note", event.projectID, id, 1, derivedHash(note), now);
2128
+ }
2129
+ this.finishEvent(event.idempotencyKey, "materialized", id, now);
2130
+ return { outcome: "materialized", idempotencyKey: event.idempotencyKey, noteID: id };
2131
+ }
2132
+ insertCapturedNote(event, candidate, subjectKey, supersedesID, contentHash, now) {
2133
+ const id = randomUUID6();
2134
+ const sizeClass = candidate.content.length <= 1200 ? "inline" : "indexed";
2135
+ this.db.query(`
2136
+ INSERT INTO notes
2137
+ (id, project_id, kind, title, summary, content, size_class, pinned, status,
2138
+ supersedes_id, current_revision, subject_key, content_hash, created_at, updated_at)
2139
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'active', ?, 1, ?, ?, ?, ?)
2140
+ `).run(id, event.projectID, candidate.kind, candidate.title, candidate.summary, candidate.content, sizeClass, supersedesID, subjectKey, contentHash, now, now);
2141
+ this.recordRevision(event, id, now);
2142
+ return id;
2143
+ }
2144
+ recordRevision(event, noteID, now) {
2145
+ const note = this.db.query("SELECT * FROM notes WHERE project_id = ? AND id = ?").get(event.projectID, noteID);
2146
+ const provenanceID = randomUUID6();
2147
+ this.db.query(`
2148
+ INSERT INTO note_provenance
2149
+ (id, project_id, note_id, source_type, capture_event_id, source_session_id,
2150
+ source_message_id, source_ordinal, source_tool_call_id, redaction_version,
2151
+ extractor_version, confidence, created_at)
2152
+ VALUES (?, ?, ?, 'opencode-capture', ?, ?, ?, ?, ?, ?, ?, ?, ?)
2153
+ `).run(provenanceID, event.projectID, noteID, event.idempotencyKey, event.source.sessionID, event.source.messageID ?? null, event.source.ordinal ?? null, event.source.toolCallID ?? null, REDACTION_POLICY_VERSION, EXTRACTOR_VERSION, event.candidate?.confidence ?? null, now);
2154
+ this.db.query(`
2155
+ INSERT INTO note_revisions
2156
+ (project_id, note_id, revision, kind, title, summary, content, size_class,
2157
+ pinned, status, supersedes_id, subject_key, content_hash, provenance_id, created_at)
2158
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2159
+ `).run(note.project_id, note.id, note.current_revision, note.kind, note.title, note.summary, note.content, note.size_class, note.pinned, note.status, note.supersedes_id, note.subject_key, note.content_hash, provenanceID, now);
2160
+ }
2161
+ enqueueOutbox(backend, operation, projectID2, noteID, revision, contentHash, now) {
2162
+ this.db.query(`
2163
+ INSERT OR IGNORE INTO index_outbox
2164
+ (backend, operation, project_id, note_id, revision, content_hash,
2165
+ state, attempt_count, available_at, created_at)
2166
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)
2167
+ `).run(backend, operation, projectID2, noteID, revision, contentHash, now, now);
2168
+ }
2169
+ finishEvent(idempotencyKey, state, noteID, now) {
2170
+ this.db.query(`
2171
+ UPDATE capture_events
2172
+ SET state = ?, note_id = ?, updated_at = ?, processed_at = ?
2173
+ WHERE idempotency_key = ?
2174
+ `).run(state, noteID, now, now, idempotencyKey);
2175
+ }
2176
+ binding(bindingKey, projectID2) {
2177
+ return this.db.query("SELECT * FROM project_bindings WHERE binding_key = ? AND project_id = ?").get(bindingKey, projectID2);
2178
+ }
2179
+ }
2180
+ function prepareForPersistence(event, denylist) {
2181
+ const copy = structuredClone(event);
2182
+ let replacements = 0;
2183
+ let truncated = copy.redaction.truncated;
2184
+ let quarantined = event.redaction.policyVersion.endsWith("/quarantined");
2185
+ if (copy.candidate) {
2186
+ for (const field of ["title", "summary", "content", "subjectKey"]) {
2187
+ const value = copy.candidate[field];
2188
+ if (typeof value !== "string")
2189
+ continue;
2190
+ const maximum = field === "title" || field === "subjectKey" ? 240 : field === "summary" ? 1200 : 4800;
2191
+ const result = redactText(value, { maxCharacters: maximum, denylist });
2192
+ copy.candidate[field] = result.text;
2193
+ replacements += result.replacements;
2194
+ truncated ||= result.truncated;
2195
+ quarantined ||= result.quarantined;
2196
+ }
2197
+ }
2198
+ if (copy.signal) {
2199
+ for (const field of ["tool", "errorType"]) {
2200
+ const value = copy.signal[field];
2201
+ if (typeof value !== "string")
2202
+ continue;
2203
+ const result = redactText(value, { maxCharacters: 160, denylist });
2204
+ copy.signal[field] = result.text;
2205
+ replacements += result.replacements;
2206
+ truncated ||= result.truncated;
2207
+ quarantined ||= result.quarantined;
2208
+ }
2209
+ }
2210
+ copy.redaction = {
2211
+ policyVersion: REDACTION_POLICY_VERSION,
2212
+ replacements: copy.redaction.replacements + replacements,
2213
+ truncated
2214
+ };
2215
+ const validated = parseCaptureEvent(copy);
2216
+ const payload = quarantined ? null : JSON.stringify(validated);
2217
+ return {
2218
+ event: validated,
2219
+ payload,
2220
+ payloadHash: payload ? sha256(payload) : null,
2221
+ quarantined,
2222
+ additionalReplacements: replacements
2223
+ };
2224
+ }
2225
+ function sha256(value) {
2226
+ return createHash6("sha256").update(value, "utf8").digest("hex");
2227
+ }
2228
+ function derivedHash(note) {
2229
+ return deriveDocument({
2230
+ projectID: note.project_id,
2231
+ noteID: note.id,
2232
+ revision: note.current_revision,
2233
+ kind: note.kind,
2234
+ title: note.title,
2235
+ summary: note.summary,
2236
+ content: note.content
2237
+ })?.contentHash ?? null;
2238
+ }
2239
+
1759
2240
  // src/index.ts
1760
2241
  function main() {
1761
2242
  const { databasePath } = resolveConfig();
@@ -1764,6 +2245,16 @@ function main() {
1764
2245
  mkdirSync3(directory, { recursive: true });
1765
2246
  const opened = openMemoryDatabase(databasePath);
1766
2247
  const store = new MemoryStore(opened.db);
2248
+ const capture = new CaptureStore(opened.db);
2249
+ capture.runRetentionBacklog();
2250
+ const retentionTimer = setInterval(() => {
2251
+ try {
2252
+ capture.runRetentionBacklog();
2253
+ } catch (error) {
2254
+ console.error(`[agz-memory] retention failed: ${error instanceof Error ? error.message : String(error)}`);
2255
+ }
2256
+ }, 60 * 60000);
2257
+ retentionTimer.unref();
1767
2258
  const handle = serveStdio(() => createMemoryServer(store), {
1768
2259
  onerror: (error) => console.error(`[agz-memory] ${error.message}`)
1769
2260
  });
@@ -1772,6 +2263,7 @@ function main() {
1772
2263
  if (closing)
1773
2264
  return;
1774
2265
  closing = true;
2266
+ clearInterval(retentionTimer);
1775
2267
  try {
1776
2268
  await handle.close();
1777
2269
  } finally {