@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/admin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // src/admin/index.ts
5
- import { createHash as createHash5 } from "crypto";
5
+ import { createHash as createHash6 } from "crypto";
6
6
  import { Database as Database3 } from "bun:sqlite";
7
7
  import {
8
8
  existsSync as existsSync4,
@@ -41,7 +41,8 @@ function normalizeProjectName(value) {
41
41
  }
42
42
 
43
43
  // src/types.ts
44
- var SCHEMA_VERSION = 9;
44
+ var SCHEMA_VERSION = 10;
45
+ var KINDS = ["decision", "fact", "procedure", "context", "research", "preference", "task"];
45
46
  var PREDICATES = ["SUPPORTS", "DERIVED_FROM", "PART_OF", "ABOUT", "PRECEDES", "SUPERSEDES"];
46
47
 
47
48
  // src/db/backup.ts
@@ -103,7 +104,7 @@ function hasTable(db, table) {
103
104
  }
104
105
 
105
106
  // src/db/backup.ts
106
- var BACKUP_FORMAT = "opencode2-memory-backup/1";
107
+ var BACKUP_FORMAT = "agz-memory-backup/1";
107
108
  function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, productVersion) {
108
109
  const sourceHealth = assertHealthyDatabase(db);
109
110
  const checkpoint = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
@@ -467,8 +468,77 @@ function processIsAlive(pid) {
467
468
  // src/db/migrations/v009.ts
468
469
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
469
470
 
471
+ // src/capture/contract.ts
472
+ import * as z from "zod/v4";
473
+ var CAPTURE_SCHEMA = "agz-memory.capture/1";
474
+ var SUPPORTED_OPENCODE_VERSION = "0.0.0-beta-18743";
475
+ var CAPTURE_EVENT_MAX_BYTES = 16 * 1024;
476
+ var CAPTURE_CONTENT_MAX_CHARACTERS = 4800;
477
+ var CAPTURE_SUMMARY_MAX_CHARACTERS = 1200;
478
+ var CAPTURE_SUBJECT_MAX_CHARACTERS = 240;
479
+ var candidateSchema = z.object({
480
+ kind: z.enum(KINDS),
481
+ title: z.string().min(1).max(240),
482
+ summary: z.string().min(1).max(CAPTURE_SUMMARY_MAX_CHARACTERS),
483
+ content: z.string().min(1).max(CAPTURE_CONTENT_MAX_CHARACTERS),
484
+ subjectKey: z.string().min(1).max(CAPTURE_SUBJECT_MAX_CHARACTERS).optional(),
485
+ intent: z.enum(["create", "supersede", "ignore", "review"]),
486
+ targetNoteID: z.string().min(1).max(240).optional(),
487
+ confidence: z.number().finite().min(0).max(1),
488
+ evidence: z.enum(["explicit-user", "verified-outcome", "session-summary"])
489
+ }).strict();
490
+ var signalSchema = z.object({
491
+ tool: z.string().min(1).max(160),
492
+ status: z.enum(["completed", "error"]),
493
+ errorType: z.string().min(1).max(160).optional()
494
+ }).strict();
495
+ var captureEventSchema = z.object({
496
+ schema: z.literal(CAPTURE_SCHEMA),
497
+ idempotencyKey: z.string().regex(/^[0-9a-f]{64}$/),
498
+ projectID: z.uuid(),
499
+ bindingKey: z.string().regex(/^[0-9a-f]{64}$/),
500
+ kind: z.enum(["user-candidate", "assistant-candidate", "session-summary", "tool-signal"]),
501
+ source: z.object({
502
+ system: z.literal("opencode-v2"),
503
+ opencodeVersion: z.literal(SUPPORTED_OPENCODE_VERSION),
504
+ pluginVersion: z.string().min(1).max(80),
505
+ sessionID: z.string().min(1).max(240),
506
+ messageID: z.string().min(1).max(240).optional(),
507
+ ordinal: z.number().int().nonnegative().optional(),
508
+ toolCallID: z.string().min(1).max(240).optional(),
509
+ observedAt: z.number().int().nonnegative()
510
+ }).strict(),
511
+ candidate: candidateSchema.optional(),
512
+ signal: signalSchema.optional(),
513
+ redaction: z.object({
514
+ policyVersion: z.string().min(1).max(80),
515
+ replacements: z.number().int().nonnegative(),
516
+ truncated: z.boolean()
517
+ }).strict()
518
+ }).strict().superRefine((event, context) => {
519
+ if (event.kind === "tool-signal" && !event.signal) {
520
+ context.addIssue({ code: "custom", message: "tool-signal requires signal" });
521
+ }
522
+ if (event.kind !== "tool-signal" && !event.candidate) {
523
+ context.addIssue({ code: "custom", message: `${event.kind} requires candidate` });
524
+ }
525
+ if (event.kind === "tool-signal" && event.candidate) {
526
+ context.addIssue({ code: "custom", message: "tool-signal cannot carry candidate" });
527
+ }
528
+ if (event.kind !== "tool-signal" && event.signal) {
529
+ context.addIssue({ code: "custom", message: `${event.kind} cannot carry signal` });
530
+ }
531
+ });
532
+ function parseCaptureEvent(value) {
533
+ const event = captureEventSchema.parse(value);
534
+ if (Buffer.byteLength(JSON.stringify(event), "utf8") > CAPTURE_EVENT_MAX_BYTES) {
535
+ throw new Error(`capture event exceeds ${CAPTURE_EVENT_MAX_BYTES} UTF-8 bytes`);
536
+ }
537
+ return event;
538
+ }
539
+
470
540
  // src/db/schema.ts
471
- var SCHEMA_V9_TABLES = `
541
+ var SCHEMA_TABLES = `
472
542
  CREATE TABLE IF NOT EXISTS projects (
473
543
  id TEXT PRIMARY KEY,
474
544
  name TEXT NOT NULL,
@@ -538,28 +608,7 @@ CREATE TABLE IF NOT EXISTS capture_checkpoints (
538
608
  );
539
609
  CREATE INDEX IF NOT EXISTS capture_checkpoints_due_idx
540
610
  ON capture_checkpoints(state, next_reconcile_at);
541
- CREATE TABLE IF NOT EXISTS capture_events (
542
- idempotency_key TEXT PRIMARY KEY,
543
- contract TEXT NOT NULL CHECK (contract = 'opencode2-memory.capture/1'),
544
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
545
- binding_key TEXT NOT NULL REFERENCES project_bindings(binding_key) ON DELETE CASCADE,
546
- event_kind TEXT NOT NULL CHECK (event_kind IN ('user-candidate','assistant-candidate','session-summary','tool-signal')),
547
- source_session_id TEXT NOT NULL,
548
- source_message_id TEXT,
549
- source_ordinal INTEGER CHECK (source_ordinal IS NULL OR source_ordinal >= 0),
550
- source_tool_call_id TEXT,
551
- payload_json TEXT CHECK (payload_json IS NULL OR json_valid(payload_json)),
552
- payload_hash TEXT,
553
- redaction_version TEXT NOT NULL,
554
- state TEXT NOT NULL CHECK (state IN ('pending','shadowed','review','materialized','duplicate','ignored','rejected','quarantined','failed','dead')),
555
- attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
556
- note_id TEXT,
557
- last_error_code TEXT,
558
- generation INTEGER NOT NULL DEFAULT 0 CHECK (generation >= 0),
559
- created_at INTEGER NOT NULL,
560
- updated_at INTEGER NOT NULL,
561
- processed_at INTEGER
562
- );
611
+ ${captureEventsTable()}
563
612
  CREATE INDEX IF NOT EXISTS capture_events_state_idx ON capture_events(state, updated_at);
564
613
  CREATE INDEX IF NOT EXISTS capture_events_session_idx ON capture_events(project_id, source_session_id);
565
614
  CREATE INDEX IF NOT EXISTS capture_events_note_idx ON capture_events(project_id, note_id);
@@ -643,15 +692,39 @@ CREATE TRIGGER IF NOT EXISTS notes_fts_au AFTER UPDATE OF title, summary, conten
643
692
  VALUES (new.rowid, new.title, new.summary, new.content);
644
693
  END;
645
694
  `;
646
- function createSchemaV9(db) {
647
- db.exec(SCHEMA_V9_TABLES);
695
+ function createSchema(db) {
696
+ db.exec(SCHEMA_TABLES);
648
697
  db.exec(FTS_V9);
649
698
  db.query("DELETE FROM schema_state").run();
650
- db.query("INSERT INTO schema_state(version) VALUES (9)").run();
699
+ db.query("INSERT INTO schema_state(version) VALUES (10)").run();
651
700
  }
652
701
  function rebuildFts(db) {
653
702
  db.query("INSERT INTO notes_fts(notes_fts) VALUES ('rebuild')").run();
654
703
  }
704
+ function captureEventsTable(table = "capture_events") {
705
+ return `CREATE TABLE IF NOT EXISTS ${table} (
706
+ idempotency_key TEXT PRIMARY KEY,
707
+ contract TEXT NOT NULL CHECK (contract = '${CAPTURE_SCHEMA}'),
708
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
709
+ binding_key TEXT NOT NULL REFERENCES project_bindings(binding_key) ON DELETE CASCADE,
710
+ event_kind TEXT NOT NULL CHECK (event_kind IN ('user-candidate','assistant-candidate','session-summary','tool-signal')),
711
+ source_session_id TEXT NOT NULL,
712
+ source_message_id TEXT,
713
+ source_ordinal INTEGER CHECK (source_ordinal IS NULL OR source_ordinal >= 0),
714
+ source_tool_call_id TEXT,
715
+ payload_json TEXT CHECK (payload_json IS NULL OR json_valid(payload_json)),
716
+ payload_hash TEXT,
717
+ redaction_version TEXT NOT NULL,
718
+ state TEXT NOT NULL CHECK (state IN ('pending','shadowed','review','materialized','duplicate','ignored','rejected','quarantined','failed','dead')),
719
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
720
+ note_id TEXT,
721
+ last_error_code TEXT,
722
+ generation INTEGER NOT NULL DEFAULT 0 CHECK (generation >= 0),
723
+ created_at INTEGER NOT NULL,
724
+ updated_at INTEGER NOT NULL,
725
+ processed_at INTEGER
726
+ );`;
727
+ }
655
728
 
656
729
  // src/db/migrations/v009.ts
657
730
  function migrateV8ToV9(db) {
@@ -720,7 +793,7 @@ function migrateV8ToV9(db) {
720
793
  ALTER TABLE notes_v9 RENAME TO notes;
721
794
  ALTER TABLE note_edges_v9 RENAME TO note_edges;
722
795
  `);
723
- db.exec(SCHEMA_V9_TABLES);
796
+ db.exec(SCHEMA_TABLES);
724
797
  for (const note of notes) {
725
798
  const provenanceID = randomUUID3();
726
799
  db.query(`
@@ -744,6 +817,57 @@ function noteContentHash(kind, title, summary, content) {
744
817
  return createHash3("sha256").update(`${kind}\x00${title}\x00${summary}\x00${content}`, "utf8").digest("hex");
745
818
  }
746
819
 
820
+ // src/db/migrations/v010.ts
821
+ import { createHash as createHash4 } from "crypto";
822
+ function migrateV9ToV10(db) {
823
+ const payloads = db.query("SELECT idempotency_key, payload_json FROM capture_events WHERE payload_json IS NOT NULL").all();
824
+ const migratedPayloads = payloads.map((row) => {
825
+ const event = JSON.parse(row.payload_json);
826
+ if (!event || typeof event !== "object" || Array.isArray(event)) {
827
+ throw new Error(`capture event ${row.idempotency_key} payload is not an object`);
828
+ }
829
+ event.schema = CAPTURE_SCHEMA;
830
+ const payload = JSON.stringify(parseCaptureEvent(event));
831
+ return {
832
+ idempotencyKey: row.idempotency_key,
833
+ payload,
834
+ payloadHash: createHash4("sha256").update(payload, "utf8").digest("hex")
835
+ };
836
+ });
837
+ db.exec("DROP TABLE IF EXISTS capture_events_v10");
838
+ db.exec(captureEventsTable("capture_events_v10"));
839
+ db.query(`
840
+ INSERT INTO capture_events_v10
841
+ (idempotency_key, contract, project_id, binding_key, event_kind,
842
+ source_session_id, source_message_id, source_ordinal, source_tool_call_id,
843
+ payload_json, payload_hash, redaction_version, state, attempt_count,
844
+ note_id, last_error_code, generation, created_at, updated_at, processed_at)
845
+ SELECT idempotency_key, ?, project_id, binding_key, event_kind,
846
+ source_session_id, source_message_id, source_ordinal, source_tool_call_id,
847
+ payload_json, payload_hash, redaction_version, state, attempt_count,
848
+ note_id, last_error_code, generation, created_at, updated_at, processed_at
849
+ FROM capture_events
850
+ `).run(CAPTURE_SCHEMA);
851
+ const updatePayload = db.query(`
852
+ UPDATE capture_events_v10
853
+ SET payload_json = ?, payload_hash = ?
854
+ WHERE idempotency_key = ?
855
+ `);
856
+ for (const row of migratedPayloads) {
857
+ updatePayload.run(row.payload, row.payloadHash, row.idempotencyKey);
858
+ }
859
+ db.exec(`
860
+ DROP TABLE capture_events;
861
+ ALTER TABLE capture_events_v10 RENAME TO capture_events;
862
+ `);
863
+ db.exec(SCHEMA_TABLES);
864
+ db.query("DELETE FROM schema_state").run();
865
+ db.query("INSERT INTO schema_state(version) VALUES (10)").run();
866
+ }
867
+
868
+ // src/version.ts
869
+ var PRODUCT_VERSION = "0.4.0";
870
+
747
871
  // src/db.ts
748
872
  var DDL = `
749
873
  CREATE TABLE IF NOT EXISTS projects (
@@ -799,13 +923,13 @@ function openMemoryDatabase(path) {
799
923
  const hasExistingData = hasLegacyV2(db) || hasTable2(db, "notes") || Boolean(existingVersion);
800
924
  if (!hasExistingData) {
801
925
  db.exec("PRAGMA foreign_keys=ON");
802
- db.transaction(() => createSchemaV9(db))();
926
+ db.transaction(() => createSchema(db))();
803
927
  assertHealthyDatabase(db);
804
928
  return { db, close: () => db.close() };
805
929
  }
806
930
  if ((existingVersion?.version ?? 0) < SCHEMA_VERSION) {
807
931
  lock = acquireMigrationLock(path, SCHEMA_VERSION);
808
- backup = createVerifiedBackup(db, path, existingVersion?.version ?? 2, SCHEMA_VERSION, "0.4.0-beta.1");
932
+ backup = createVerifiedBackup(db, path, existingVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
809
933
  db.exec("PRAGMA foreign_keys=OFF");
810
934
  if (!existingVersion && hasLegacyV2(db)) {
811
935
  db.exec(DDL);
@@ -824,9 +948,13 @@ function openMemoryDatabase(path) {
824
948
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
825
949
  migrateToV8(db);
826
950
  }
827
- const version = getSchemaVersion(db)?.version ?? 8;
828
- if (version < 9)
951
+ let version = getSchemaVersion(db)?.version ?? 8;
952
+ if (version < 9) {
829
953
  db.transaction(() => migrateV8ToV9(db))();
954
+ version = 9;
955
+ }
956
+ if (version < 10)
957
+ db.transaction(() => migrateV9ToV10(db))();
830
958
  db.exec("PRAGMA foreign_keys=ON");
831
959
  if (db.query("PRAGMA foreign_keys").get().foreign_keys !== 1) {
832
960
  throw new Error("failed to enable database foreign keys");
@@ -838,7 +966,7 @@ function openMemoryDatabase(path) {
838
966
  return { db, close: () => db.close() };
839
967
  }
840
968
  db.exec("PRAGMA foreign_keys=ON");
841
- db.exec(SCHEMA_V9_TABLES);
969
+ db.exec(SCHEMA_TABLES);
842
970
  db.exec(FTS_V9);
843
971
  assertHealthyDatabase(db);
844
972
  db.exec("PRAGMA foreign_keys=ON");
@@ -1159,7 +1287,7 @@ function count(db, sql, ...bindings) {
1159
1287
  }
1160
1288
 
1161
1289
  // src/retrieval/derived.ts
1162
- import { createHash as createHash4 } from "crypto";
1290
+ import { createHash as createHash5 } from "crypto";
1163
1291
 
1164
1292
  // src/capture/redact.ts
1165
1293
  var RULES = [
@@ -1190,15 +1318,15 @@ function redactText(value, options = {}) {
1190
1318
  let replacements = 0;
1191
1319
  let highRisk = 0;
1192
1320
  const classes = {};
1193
- for (const literal of options.denylist ?? []) {
1194
- if (!literal)
1321
+ for (const literal2 of options.denylist ?? []) {
1322
+ if (!literal2)
1195
1323
  continue;
1196
- const count2 = text.split(literal).length - 1;
1324
+ const count2 = text.split(literal2).length - 1;
1197
1325
  if (count2 === 0)
1198
1326
  continue;
1199
1327
  replacements += count2;
1200
1328
  classes.denylist = (classes.denylist ?? 0) + count2;
1201
- text = text.replaceAll(literal, "[REDACTED:denylist]");
1329
+ text = text.replaceAll(literal2, "[REDACTED:denylist]");
1202
1330
  }
1203
1331
  for (const rule of RULES) {
1204
1332
  text = text.replace(rule.pattern, () => {
@@ -1248,7 +1376,7 @@ function deriveDocument(source) {
1248
1376
  const content = redactText(source.content);
1249
1377
  if (title.quarantined || summary.quarantined || content.quarantined)
1250
1378
  return;
1251
- const contentHash = createHash4("sha256").update(`${source.kind}\x00${title.text}\x00${summary.text}\x00${content.text}`, "utf8").digest("hex");
1379
+ const contentHash = createHash5("sha256").update(`${source.kind}\x00${title.text}\x00${summary.text}\x00${content.text}`, "utf8").digest("hex");
1252
1380
  return {
1253
1381
  projectID: source.projectID,
1254
1382
  noteID: source.noteID,
@@ -1282,7 +1410,7 @@ async function runAdmin(argv = process.argv.slice(2)) {
1282
1410
  const db = new Database3(databasePath);
1283
1411
  try {
1284
1412
  const version = schemaVersion(db);
1285
- return createVerifiedBackup(db, databasePath, version, version, "0.4.0-beta.1");
1413
+ return createVerifiedBackup(db, databasePath, version, version, PRODUCT_VERSION);
1286
1414
  } finally {
1287
1415
  db.close();
1288
1416
  lock.release();
@@ -1390,7 +1518,7 @@ async function runAdmin(argv = process.argv.slice(2)) {
1390
1518
  if (command === "backup" && subcommand === "prune") {
1391
1519
  const entries = backupEntries(databasePath);
1392
1520
  const root = resolve2(`${databasePath}.backup`);
1393
- const digest = createHash5("sha256").update(`${resolve2(databasePath)}\x00${root}
1521
+ const digest = createHash6("sha256").update(`${resolve2(databasePath)}\x00${root}
1394
1522
  ${entries.map((entry) => `${basename2(entry.manifest)}\x00${basename2(entry.database)}\x00${entry.sha256}\x00${entry.size}\x00${entry.manifestHash}`).join(`
1395
1523
  `)}`).digest("hex");
1396
1524
  if (option(argv, "--confirm") !== "DELETE_VERIFIED_BACKUPS") {
@@ -1472,7 +1600,7 @@ function verifiedBackupEntry(root, manifest) {
1472
1600
  database: verified.databasePath,
1473
1601
  sha256: verified.manifest.sha256,
1474
1602
  size: verified.manifest.size,
1475
- manifestHash: createHash5("sha256").update(bytes).digest("hex")
1603
+ manifestHash: createHash6("sha256").update(bytes).digest("hex")
1476
1604
  };
1477
1605
  }
1478
1606
  if (import.meta.main) {