@dzhechkov/memory 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +30 -0
  2. package/dist/backend.d.ts +45 -0
  3. package/dist/backend.d.ts.map +1 -0
  4. package/dist/backend.js +7 -0
  5. package/dist/backend.js.map +1 -0
  6. package/dist/bridge.d.ts +32 -0
  7. package/dist/bridge.d.ts.map +1 -0
  8. package/dist/bridge.js +79 -0
  9. package/dist/bridge.js.map +1 -0
  10. package/dist/cascade.d.ts +32 -0
  11. package/dist/cascade.d.ts.map +1 -0
  12. package/dist/cascade.js +30 -0
  13. package/dist/cascade.js.map +1 -0
  14. package/dist/dreaming.d.ts +37 -0
  15. package/dist/dreaming.d.ts.map +1 -0
  16. package/dist/dreaming.js +102 -0
  17. package/dist/dreaming.js.map +1 -0
  18. package/dist/index.d.ts +23 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +15 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/json-backend.d.ts +33 -0
  23. package/dist/json-backend.d.ts.map +1 -0
  24. package/dist/json-backend.js +88 -0
  25. package/dist/json-backend.js.map +1 -0
  26. package/dist/reflexion.d.ts +37 -0
  27. package/dist/reflexion.d.ts.map +1 -0
  28. package/dist/reflexion.js +62 -0
  29. package/dist/reflexion.js.map +1 -0
  30. package/dist/sqlite-backend.d.ts +44 -0
  31. package/dist/sqlite-backend.d.ts.map +1 -0
  32. package/dist/sqlite-backend.js +213 -0
  33. package/dist/sqlite-backend.js.map +1 -0
  34. package/dist/sqlite-probe.d.ts +27 -0
  35. package/dist/sqlite-probe.d.ts.map +1 -0
  36. package/dist/sqlite-probe.js +34 -0
  37. package/dist/sqlite-probe.js.map +1 -0
  38. package/package.json +55 -0
  39. package/src/backend.ts +47 -0
  40. package/src/bridge.ts +99 -0
  41. package/src/cascade.ts +52 -0
  42. package/src/dreaming.ts +127 -0
  43. package/src/index.ts +24 -0
  44. package/src/json-backend.ts +109 -0
  45. package/src/reflexion.ts +79 -0
  46. package/src/sqlite-backend.ts +263 -0
  47. package/src/sqlite-probe.ts +44 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `Reflexion` — the skill-outcome feedback loop.
3
+ *
4
+ * Records the outcome of using a skill (`record(skillId, outcome, score)`) and
5
+ * ranks skills by their most recent score. Reads are **monotonic**: the latest
6
+ * record for a skill supersedes older ones.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import type { MemoryBackend, MemoryRecord } from './backend.js';
11
+ /** Optional extras for {@link Reflexion.record}. */
12
+ export interface ReflexionInput {
13
+ /** Free text to store (defaults to `"<skillId> <outcome>"`). */
14
+ readonly text?: string;
15
+ /** String-keyed metadata. */
16
+ readonly metadata?: Record<string, string>;
17
+ }
18
+ /** Records skill outcomes against a {@link MemoryBackend} and ranks skills. */
19
+ export declare class Reflexion {
20
+ private readonly backend;
21
+ constructor(backend: MemoryBackend);
22
+ /**
23
+ * Record the outcome of using a skill.
24
+ *
25
+ * @param score reward score, must be within `[0, 1]`.
26
+ * @throws if `score` is outside `[0, 1]`.
27
+ */
28
+ record(skillId: string, outcome: string, score: number, input?: ReflexionInput): Promise<MemoryRecord>;
29
+ /** The most recently recorded score for a skill, or `undefined` if none. */
30
+ scoreOf(skillId: string): Promise<number | undefined>;
31
+ /** Every skill with a recorded outcome, ranked by most-recent score, descending. */
32
+ ranking(): Promise<{
33
+ skillId: string;
34
+ score: number;
35
+ }[]>;
36
+ }
37
+ //# sourceMappingURL=reflexion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reflexion.d.ts","sourceRoot":"","sources":["../src/reflexion.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKhE,oDAAoD;AACpD,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6BAA6B;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED,+EAA+E;AAC/E,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,aAAa;IAEnD;;;;;OAKG;IACG,MAAM,CACV,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,cAAmB,GACzB,OAAO,CAAC,YAAY,CAAC;IAkBxB,4EAA4E;IACtE,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQ3D,oFAAoF;IAC9E,OAAO,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAY/D"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `Reflexion` — the skill-outcome feedback loop.
3
+ *
4
+ * Records the outcome of using a skill (`record(skillId, outcome, score)`) and
5
+ * ranks skills by their most recent score. Reads are **monotonic**: the latest
6
+ * record for a skill supersedes older ones.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ /** Monotonic counter — keeps record ids unique within a process. */
11
+ let sequence = 0;
12
+ /** Records skill outcomes against a {@link MemoryBackend} and ranks skills. */
13
+ export class Reflexion {
14
+ backend;
15
+ constructor(backend) {
16
+ this.backend = backend;
17
+ }
18
+ /**
19
+ * Record the outcome of using a skill.
20
+ *
21
+ * @param score reward score, must be within `[0, 1]`.
22
+ * @throws if `score` is outside `[0, 1]`.
23
+ */
24
+ async record(skillId, outcome, score, input = {}) {
25
+ if (!Number.isFinite(score) || score < 0 || score > 1) {
26
+ throw new Error(`reflexion: score must be within [0, 1], got ${score}`);
27
+ }
28
+ sequence += 1;
29
+ const record = {
30
+ id: `reflexion:${skillId}:${Date.now()}:${sequence}`,
31
+ skillId,
32
+ text: input.text ?? `${skillId} ${outcome}`,
33
+ score,
34
+ outcome,
35
+ timestamp: new Date().toISOString(),
36
+ ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
37
+ };
38
+ await this.backend.put(record);
39
+ return record;
40
+ }
41
+ /** The most recently recorded score for a skill, or `undefined` if none. */
42
+ async scoreOf(skillId) {
43
+ const records = await this.backend.query({ skillId, limit: Number.MAX_SAFE_INTEGER });
44
+ if (records.length === 0)
45
+ return undefined;
46
+ return records.reduce((latest, record) => record.timestamp >= latest.timestamp ? record : latest).score;
47
+ }
48
+ /** Every skill with a recorded outcome, ranked by most-recent score, descending. */
49
+ async ranking() {
50
+ const latest = new Map();
51
+ for (const record of await this.backend.all()) {
52
+ const current = latest.get(record.skillId);
53
+ if (current === undefined || record.timestamp > current.timestamp) {
54
+ latest.set(record.skillId, record);
55
+ }
56
+ }
57
+ return [...latest.values()]
58
+ .map((record) => ({ skillId: record.skillId, score: record.score }))
59
+ .sort((a, b) => b.score - a.score);
60
+ }
61
+ }
62
+ //# sourceMappingURL=reflexion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reflexion.js","sourceRoot":"","sources":["../src/reflexion.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,oEAAoE;AACpE,IAAI,QAAQ,GAAG,CAAC,CAAC;AAUjB,+EAA+E;AAC/E,MAAM,OAAO,SAAS;IACS;IAA7B,YAA6B,OAAsB;QAAtB,YAAO,GAAP,OAAO,CAAe;IAAG,CAAC;IAEvD;;;;;OAKG;IACH,KAAK,CAAC,MAAM,CACV,OAAe,EACf,OAAe,EACf,KAAa,EACb,QAAwB,EAAE;QAE1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,KAAK,CAAC,+CAA+C,KAAK,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,QAAQ,IAAI,CAAC,CAAC;QACd,MAAM,MAAM,GAAiB;YAC3B,EAAE,EAAE,aAAa,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE;YACpD,OAAO;YACP,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO,IAAI,OAAO,EAAE;YAC3C,KAAK;YACL,OAAO;YACP,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtE,CAAC;QACF,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/B,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,4EAA4E;IAC5E,KAAK,CAAC,OAAO,CAAC,OAAe;QAC3B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC;QACtF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAC3C,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CACvC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CACvD,CAAC,KAAK,CAAC;IACV,CAAC;IAED,oFAAoF;IACpF,KAAK,CAAC,OAAO;QACX,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;QAC/C,KAAK,MAAM,MAAM,IAAI,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;YAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC3C,IAAI,OAAO,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;gBAClE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QACD,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;aACxB,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;aACnE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC;CACF"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `SqliteBackend` — production-scale memory backend using better-sqlite3.
3
+ *
4
+ * Write-through persistence (every `put` is durable), WAL mode for concurrency,
5
+ * and indexed columns for efficient queries. Handles 100k+ records where
6
+ * JsonFileBackend degrades.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import type { MemoryBackend, MemoryQuery, MemoryRecord } from './backend.js';
11
+ /** Options for SqliteBackend. */
12
+ export interface SqliteBackendOptions {
13
+ /** Path to the SQLite database file. */
14
+ readonly filePath: string;
15
+ }
16
+ /**
17
+ * SQLite-backed memory store. Write-through, WAL mode, indexed.
18
+ *
19
+ * Requires `better-sqlite3` at runtime — use via {@link SqliteProbe} in the
20
+ * cascade to gracefully fall back when the native module is unavailable.
21
+ */
22
+ export declare class SqliteBackend implements MemoryBackend {
23
+ readonly name = "sqlite";
24
+ private readonly db;
25
+ private readonly upsertStmt;
26
+ private readonly allStmt;
27
+ private readonly countStmt;
28
+ private readonly bySkillStmt;
29
+ private readonly ftsSearchStmt;
30
+ private readonly ftsSearchSkillStmt;
31
+ private readonly hasFts5;
32
+ constructor(db: any);
33
+ /** Open (or create) a SQLite database at the given path. */
34
+ static open(filePath: string): SqliteBackend;
35
+ put(record: MemoryRecord): Promise<void>;
36
+ query(query: MemoryQuery): Promise<MemoryRecord[]>;
37
+ all(): Promise<MemoryRecord[]>;
38
+ count(): Promise<number>;
39
+ /** Batch insert records within a transaction (for bulk loading). */
40
+ putMany(records: readonly MemoryRecord[]): void;
41
+ /** Close the database connection. */
42
+ close(): void;
43
+ }
44
+ //# sourceMappingURL=sqlite-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-backend.d.ts","sourceRoot":"","sources":["../src/sqlite-backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAoF7E,iCAAiC;AACjC,MAAM,WAAW,oBAAoB;IACnC,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,qBAAa,aAAc,YAAW,aAAa;IACjD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAM;IACzB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAM;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAM;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAM;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAM;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkB;IAChD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAkB;IACrD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,EAAE,EAAE,GAAG;IAwBnB,4DAA4D;IAC5D,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa;IAW5C,GAAG,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAaxC,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAgDlD,GAAG,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAI9B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAIxB,oEAAoE;IACpE,OAAO,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI;IAiB/C,qCAAqC;IACrC,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * `SqliteBackend` — production-scale memory backend using better-sqlite3.
3
+ *
4
+ * Write-through persistence (every `put` is durable), WAL mode for concurrency,
5
+ * and indexed columns for efficient queries. Handles 100k+ records where
6
+ * JsonFileBackend degrades.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import { createRequire } from 'node:module';
11
+ import { mkdirSync } from 'node:fs';
12
+ import { dirname } from 'node:path';
13
+ const require = createRequire(import.meta.url);
14
+ const DEFAULT_LIMIT = 20;
15
+ /** Split text into lowercase word tokens of length > 1. */
16
+ function tokenize(text) {
17
+ return text
18
+ .toLowerCase()
19
+ .split(/[^a-z0-9]+/)
20
+ .filter((token) => token.length > 1);
21
+ }
22
+ /** Count how many query terms appear in a record's text/skillId. */
23
+ function relevanceOf(record, terms) {
24
+ if (terms.length === 0)
25
+ return 0;
26
+ const haystack = new Set(tokenize(`${record.text} ${record.skillId}`));
27
+ let hits = 0;
28
+ for (const term of terms) {
29
+ if (haystack.has(term))
30
+ hits += 1;
31
+ }
32
+ return hits;
33
+ }
34
+ /** Schema version for future migrations. */
35
+ const SCHEMA_VERSION = 2;
36
+ const INIT_SQL = `
37
+ CREATE TABLE IF NOT EXISTS memory_records (
38
+ id TEXT PRIMARY KEY,
39
+ skill_id TEXT NOT NULL,
40
+ text TEXT NOT NULL,
41
+ score REAL NOT NULL,
42
+ outcome TEXT NOT NULL,
43
+ timestamp TEXT NOT NULL,
44
+ metadata TEXT
45
+ );
46
+ CREATE INDEX IF NOT EXISTS idx_skill ON memory_records(skill_id);
47
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON memory_records(timestamp);
48
+ PRAGMA user_version = ${SCHEMA_VERSION};
49
+ `;
50
+ /** FTS5 virtual table + triggers for automatic sync. */
51
+ const FTS5_SQL = `
52
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
53
+ text, skill_id, content=memory_records, content_rowid=rowid
54
+ );
55
+ CREATE TRIGGER IF NOT EXISTS memory_fts_insert AFTER INSERT ON memory_records BEGIN
56
+ INSERT INTO memory_fts(rowid, text, skill_id) VALUES (new.rowid, new.text, new.skill_id);
57
+ END;
58
+ CREATE TRIGGER IF NOT EXISTS memory_fts_delete AFTER DELETE ON memory_records BEGIN
59
+ INSERT INTO memory_fts(memory_fts, rowid, text, skill_id) VALUES ('delete', old.rowid, old.text, old.skill_id);
60
+ END;
61
+ CREATE TRIGGER IF NOT EXISTS memory_fts_update AFTER UPDATE ON memory_records BEGIN
62
+ INSERT INTO memory_fts(memory_fts, rowid, text, skill_id) VALUES ('delete', old.rowid, old.text, old.skill_id);
63
+ INSERT INTO memory_fts(rowid, text, skill_id) VALUES (new.rowid, new.text, new.skill_id);
64
+ END;
65
+ `;
66
+ /** FTS5 query — returns matching record ids ranked by relevance. */
67
+ const FTS5_SEARCH_SQL = `
68
+ SELECT mr.* FROM memory_fts fts
69
+ JOIN memory_records mr ON mr.rowid = fts.rowid
70
+ WHERE memory_fts MATCH ?
71
+ ORDER BY fts.rank
72
+ `;
73
+ const FTS5_SEARCH_SKILL_SQL = `
74
+ SELECT mr.* FROM memory_fts fts
75
+ JOIN memory_records mr ON mr.rowid = fts.rowid
76
+ WHERE memory_fts MATCH ? AND mr.skill_id = ?
77
+ ORDER BY fts.rank
78
+ `;
79
+ const UPSERT_SQL = `
80
+ INSERT OR REPLACE INTO memory_records (id, skill_id, text, score, outcome, timestamp, metadata)
81
+ VALUES (?, ?, ?, ?, ?, ?, ?)
82
+ `;
83
+ const ALL_SQL = 'SELECT * FROM memory_records';
84
+ const COUNT_SQL = 'SELECT COUNT(*) as cnt FROM memory_records';
85
+ const BY_SKILL_SQL = 'SELECT * FROM memory_records WHERE skill_id = ?';
86
+ /**
87
+ * SQLite-backed memory store. Write-through, WAL mode, indexed.
88
+ *
89
+ * Requires `better-sqlite3` at runtime — use via {@link SqliteProbe} in the
90
+ * cascade to gracefully fall back when the native module is unavailable.
91
+ */
92
+ export class SqliteBackend {
93
+ name = 'sqlite';
94
+ db; // better-sqlite3 Database instance
95
+ upsertStmt;
96
+ allStmt;
97
+ countStmt;
98
+ bySkillStmt;
99
+ ftsSearchStmt;
100
+ ftsSearchSkillStmt;
101
+ hasFts5;
102
+ constructor(db) {
103
+ this.db = db;
104
+ db.exec(INIT_SQL);
105
+ // Try to enable FTS5 — gracefully degrade if unavailable
106
+ let ftsOk = false;
107
+ try {
108
+ db.exec(FTS5_SQL);
109
+ // Rebuild FTS index from existing data (idempotent)
110
+ db.exec(`INSERT INTO memory_fts(memory_fts) VALUES ('rebuild')`);
111
+ this.ftsSearchStmt = db.prepare(FTS5_SEARCH_SQL);
112
+ this.ftsSearchSkillStmt = db.prepare(FTS5_SEARCH_SKILL_SQL);
113
+ ftsOk = true;
114
+ }
115
+ catch {
116
+ // FTS5 not compiled in — fall back to keyword overlap
117
+ }
118
+ this.hasFts5 = ftsOk;
119
+ this.upsertStmt = db.prepare(UPSERT_SQL);
120
+ this.allStmt = db.prepare(ALL_SQL);
121
+ this.countStmt = db.prepare(COUNT_SQL);
122
+ this.bySkillStmt = db.prepare(BY_SKILL_SQL);
123
+ }
124
+ /** Open (or create) a SQLite database at the given path. */
125
+ static open(filePath) {
126
+ mkdirSync(dirname(filePath), { recursive: true });
127
+ // Dynamic require — better-sqlite3 must be available at runtime
128
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
129
+ const Database = require('better-sqlite3');
130
+ const db = new Database(filePath);
131
+ db.pragma('journal_mode = WAL');
132
+ db.pragma('synchronous = NORMAL');
133
+ return new SqliteBackend(db);
134
+ }
135
+ put(record) {
136
+ this.upsertStmt.run(record.id, record.skillId, record.text, record.score, record.outcome, record.timestamp, record.metadata ? JSON.stringify(record.metadata) : null);
137
+ return Promise.resolve();
138
+ }
139
+ query(query) {
140
+ const limit = query.limit ?? DEFAULT_LIMIT;
141
+ // FTS5 path — use SQLite full-text search when available and text query provided
142
+ if (this.hasFts5 && query.text !== undefined && query.text.trim().length > 0) {
143
+ try {
144
+ // FTS5 query syntax: simple terms joined by spaces (implicit AND → OR with ranking)
145
+ const ftsQuery = tokenize(query.text).join(' OR ');
146
+ if (ftsQuery.length > 0) {
147
+ let rows;
148
+ if (query.skillId !== undefined) {
149
+ rows = this.ftsSearchSkillStmt.all(ftsQuery, query.skillId);
150
+ }
151
+ else {
152
+ rows = this.ftsSearchStmt.all(ftsQuery);
153
+ }
154
+ const records = rows.map(rowToRecord);
155
+ // FTS5 rank is already applied; tiebreak by score DESC, timestamp DESC
156
+ records.sort((a, b) => b.score - a.score || b.timestamp.localeCompare(a.timestamp));
157
+ return Promise.resolve(records.slice(0, limit));
158
+ }
159
+ }
160
+ catch {
161
+ // FTS5 query failed (e.g., special chars) — fall through to keyword approach
162
+ }
163
+ }
164
+ // Keyword overlap fallback
165
+ const terms = query.text !== undefined ? tokenize(query.text) : [];
166
+ let rows;
167
+ if (query.skillId !== undefined) {
168
+ rows = this.bySkillStmt.all(query.skillId);
169
+ }
170
+ else {
171
+ rows = this.allStmt.all();
172
+ }
173
+ const records = rows.map(rowToRecord);
174
+ const ranked = records
175
+ .map((record) => ({ record, relevance: relevanceOf(record, terms) }))
176
+ .sort((a, b) => b.relevance - a.relevance ||
177
+ b.record.score - a.record.score ||
178
+ b.record.timestamp.localeCompare(a.record.timestamp));
179
+ return Promise.resolve(ranked.slice(0, limit).map((entry) => entry.record));
180
+ }
181
+ all() {
182
+ return Promise.resolve(this.allStmt.all().map(rowToRecord));
183
+ }
184
+ count() {
185
+ return Promise.resolve(this.countStmt.get().cnt);
186
+ }
187
+ /** Batch insert records within a transaction (for bulk loading). */
188
+ putMany(records) {
189
+ const insertMany = this.db.transaction((items) => {
190
+ for (const record of items) {
191
+ this.upsertStmt.run(record.id, record.skillId, record.text, record.score, record.outcome, record.timestamp, record.metadata ? JSON.stringify(record.metadata) : null);
192
+ }
193
+ });
194
+ insertMany(records);
195
+ }
196
+ /** Close the database connection. */
197
+ close() {
198
+ this.db.close();
199
+ }
200
+ }
201
+ /** Convert a raw SQLite row to a MemoryRecord. */
202
+ function rowToRecord(row) {
203
+ return {
204
+ id: row.id,
205
+ skillId: row.skill_id,
206
+ text: row.text,
207
+ score: row.score,
208
+ outcome: row.outcome,
209
+ timestamp: row.timestamp,
210
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
211
+ };
212
+ }
213
+ //# sourceMappingURL=sqlite-backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-backend.js","sourceRoot":"","sources":["../src/sqlite-backend.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `SqliteProbe` — cascade probe for the SQLite backend.
3
+ *
4
+ * Attempts to load `better-sqlite3` and open the database. Returns `undefined`
5
+ * if the native module is unavailable (not installed, build failed, etc.).
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ import type { BackendProbe } from './cascade.js';
10
+ import type { MemoryBackend } from './backend.js';
11
+ /** Options for SqliteProbe. */
12
+ export interface SqliteProbeOptions {
13
+ /** Path to the SQLite database file. Default: `.dz/memory.sqlite` */
14
+ readonly filePath?: string;
15
+ }
16
+ /**
17
+ * A {@link BackendProbe} that tries to initialise a SQLite backend.
18
+ * Safe to construct unconditionally — if `better-sqlite3` is not installed,
19
+ * `create()` returns `undefined` and the cascade moves on.
20
+ */
21
+ export declare class SqliteProbe implements BackendProbe {
22
+ readonly name = "sqlite";
23
+ private readonly filePath;
24
+ constructor(options?: SqliteProbeOptions);
25
+ create(): Promise<MemoryBackend | undefined>;
26
+ }
27
+ //# sourceMappingURL=sqlite-probe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-probe.d.ts","sourceRoot":"","sources":["../src/sqlite-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,+BAA+B;AAC/B,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,qBAAa,WAAY,YAAW,YAAY;IAC9C,QAAQ,CAAC,IAAI,YAAY;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,OAAO,GAAE,kBAAuB;IAItC,MAAM,IAAI,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;CAYnD"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `SqliteProbe` — cascade probe for the SQLite backend.
3
+ *
4
+ * Attempts to load `better-sqlite3` and open the database. Returns `undefined`
5
+ * if the native module is unavailable (not installed, build failed, etc.).
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ /**
10
+ * A {@link BackendProbe} that tries to initialise a SQLite backend.
11
+ * Safe to construct unconditionally — if `better-sqlite3` is not installed,
12
+ * `create()` returns `undefined` and the cascade moves on.
13
+ */
14
+ export class SqliteProbe {
15
+ name = 'sqlite';
16
+ filePath;
17
+ constructor(options = {}) {
18
+ this.filePath = options.filePath ?? '.dz/memory.sqlite';
19
+ }
20
+ async create() {
21
+ try {
22
+ // Attempt to load better-sqlite3 via the SqliteBackend.
23
+ // If the native module is not installed, this throws and we return undefined.
24
+ const { SqliteBackend } = await import('./sqlite-backend.js');
25
+ const backend = SqliteBackend.open(this.filePath);
26
+ return backend;
27
+ }
28
+ catch {
29
+ // Module not available or build failed — fall through
30
+ return undefined;
31
+ }
32
+ }
33
+ }
34
+ //# sourceMappingURL=sqlite-probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-probe.js","sourceRoot":"","sources":["../src/sqlite-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH;;;;GAIG;AACH,MAAM,OAAO,WAAW;IACb,IAAI,GAAG,QAAQ,CAAC;IACR,QAAQ,CAAS;IAElC,YAAY,UAA8B,EAAE;QAC1C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,mBAAmB,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC;YACH,wDAAwD;YACxD,8EAA8E;YAC9E,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;YAC9D,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClD,OAAO,OAAO,CAAC;QACjB,CAAC;QAAC,MAAM,CAAC;YACP,sDAAsD;YACtD,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@dzhechkov/memory",
3
+ "version": "0.2.0",
4
+ "description": "Harness memory layer - backend cascade, Reflexion feedback, and the host-memory bridge.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "dzhechko",
8
+ "keywords": [
9
+ "agent-skills",
10
+ "harness",
11
+ "memory",
12
+ "reflexion"
13
+ ],
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "README.md"
26
+ ],
27
+ "optionalDependencies": {
28
+ "better-sqlite3": "^11.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/better-sqlite3": "^7.6.0",
32
+ "@types/node": "^25.6.0",
33
+ "typescript": "^5.7.0",
34
+ "vitest": "^3.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/djd1m/dz-harness-hub.git",
45
+ "directory": "packages/@dzhechkov/memory"
46
+ },
47
+ "homepage": "https://github.com/djd1m/dz-harness-hub/tree/main/packages/@dzhechkov/memory#readme",
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "lint": "tsc -p tsconfig.json --noEmit"
54
+ }
55
+ }
package/src/backend.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The memory storage contract — what every backend implements.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ /** A single memory entry. */
8
+ export interface MemoryRecord {
9
+ /** Unique record id. */
10
+ readonly id: string;
11
+ /** The skill this record is about. */
12
+ readonly skillId: string;
13
+ /** Free text — the content keyword queries match against. */
14
+ readonly text: string;
15
+ /** Reward / quality score, in `[0, 1]`. */
16
+ readonly score: number;
17
+ /** A short outcome label, e.g. `excellent`, `good`, `imported`. */
18
+ readonly outcome: string;
19
+ /** ISO-8601 creation time. */
20
+ readonly timestamp: string;
21
+ /** Optional string-keyed metadata. */
22
+ readonly metadata?: Record<string, string>;
23
+ }
24
+
25
+ /** A retrieval query. */
26
+ export interface MemoryQuery {
27
+ /** Keyword text to rank records by relevance. */
28
+ readonly text?: string;
29
+ /** Restrict to one skill. */
30
+ readonly skillId?: string;
31
+ /** Maximum results (default 20). */
32
+ readonly limit?: number;
33
+ }
34
+
35
+ /** A pluggable memory store. */
36
+ export interface MemoryBackend {
37
+ /** Stable backend name, e.g. `json-file`. */
38
+ readonly name: string;
39
+ /** Insert or replace a record (keyed by `record.id`). */
40
+ put(record: MemoryRecord): Promise<void>;
41
+ /** Retrieve records ranked by relevance to the query. */
42
+ query(query: MemoryQuery): Promise<MemoryRecord[]>;
43
+ /** Every stored record. */
44
+ all(): Promise<MemoryRecord[]>;
45
+ /** Number of stored records. */
46
+ count(): Promise<number>;
47
+ }
package/src/bridge.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `MemoryBridge` — import host memory files into canonical `MemoryRecord`s.
3
+ *
4
+ * The bridge is the anti-corruption layer between a host's memory format
5
+ * (a Claude Code `MEMORY.md`, a memory note, …) and this package's records.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { MemoryBackend, MemoryRecord } from './backend.js';
11
+
12
+ /** Monotonic counter for bridge-generated record ids. */
13
+ let sequence = 0;
14
+
15
+ /** Options for a bridge import. */
16
+ export interface BridgeOptions {
17
+ /** A label for where the memory came from, e.g. `claude-code:MEMORY.md`. */
18
+ readonly source: string;
19
+ /** Skill id to associate the imported records with. Default `imported`. */
20
+ readonly skillId?: string;
21
+ /** Score for imported records, in `[0, 1]`. Default `0.5`. */
22
+ readonly score?: number;
23
+ }
24
+
25
+ interface Section {
26
+ readonly heading: string;
27
+ readonly body: string;
28
+ }
29
+
30
+ function slug(text: string): string {
31
+ return (
32
+ text
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9]+/g, '-')
35
+ .replace(/^-+|-+$/g, '')
36
+ .slice(0, 48) || 'section'
37
+ );
38
+ }
39
+
40
+ /** Split markdown into sections on `##` headings (whole doc if there are none). */
41
+ function splitSections(markdown: string): Section[] {
42
+ const sections: Section[] = [];
43
+ let heading = 'document';
44
+ let body: string[] = [];
45
+ const flush = (): void => {
46
+ sections.push({ heading, body: body.join('\n') });
47
+ };
48
+ for (const line of markdown.split(/\r?\n/)) {
49
+ const match = /^##\s+(.+?)\s*$/.exec(line);
50
+ if (match) {
51
+ flush();
52
+ heading = match[1] ?? 'section';
53
+ body = [];
54
+ } else {
55
+ body.push(line);
56
+ }
57
+ }
58
+ flush();
59
+ return sections;
60
+ }
61
+
62
+ /**
63
+ * Parse a host memory markdown document into {@link MemoryRecord}s — one record
64
+ * per `##` section (or one for the whole document if it has no headings).
65
+ * Empty sections are dropped. Pure: it does not touch any backend.
66
+ */
67
+ export function importMemoryMarkdown(markdown: string, options: BridgeOptions): MemoryRecord[] {
68
+ const skillId = options.skillId ?? 'imported';
69
+ const score = options.score ?? 0.5;
70
+ const timestamp = new Date().toISOString();
71
+ return splitSections(markdown)
72
+ .map((section): MemoryRecord => {
73
+ sequence += 1;
74
+ return {
75
+ id: `bridge:${slug(options.source)}:${slug(section.heading)}:${sequence}`,
76
+ skillId,
77
+ text: section.body.trim(),
78
+ score,
79
+ outcome: 'imported',
80
+ timestamp,
81
+ metadata: { source: options.source, heading: section.heading },
82
+ };
83
+ })
84
+ .filter((record) => record.text.length > 0);
85
+ }
86
+
87
+ /** Imports host memory markdown into a {@link MemoryBackend}. */
88
+ export class MemoryBridge {
89
+ constructor(private readonly backend: MemoryBackend) {}
90
+
91
+ /** Parse `markdown` and store each resulting record; returns the records. */
92
+ async importMarkdown(markdown: string, options: BridgeOptions): Promise<MemoryRecord[]> {
93
+ const records = importMemoryMarkdown(markdown, options);
94
+ for (const record of records) {
95
+ await this.backend.put(record);
96
+ }
97
+ return records;
98
+ }
99
+ }