nlm-memory 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/cli/nlm.js +67 -1
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/dataset/build-dataset.d.ts +13 -0
- package/dist/core/dataset/build-dataset.js +31 -2
- package/dist/core/dataset/build-dataset.js.map +1 -1
- package/dist/core/integrity/check-invariants.d.ts +41 -0
- package/dist/core/integrity/check-invariants.js +349 -0
- package/dist/core/integrity/check-invariants.js.map +1 -0
- package/dist/core/scheduler/scan-once.d.ts +4 -1
- package/dist/core/scheduler/scan-once.js +53 -14
- package/dist/core/scheduler/scan-once.js.map +1 -1
- package/dist/core/scheduler/scheduler.js +64 -11
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/storage/live-status.d.ts +5 -3
- package/dist/core/storage/live-status.js +6 -2
- package/dist/core/storage/live-status.js.map +1 -1
- package/dist/core/storage/migrate.d.ts +5 -0
- package/dist/core/storage/migrate.js +24 -8
- package/dist/core/storage/migrate.js.map +1 -1
- package/dist/core/storage/pg-session-store.d.ts +2 -2
- package/dist/core/storage/pg-session-store.js +31 -6
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-fact-store.js +7 -15
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +14 -2
- package/dist/core/storage/sqlite-session-store.js +61 -24
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/http/app.js +8 -2
- package/dist/http/app.js.map +1 -1
- package/dist/shared/types.d.ts +24 -5
- package/dist/ui/assets/index-CJgVgxlM.js +11 -0
- package/dist/ui/assets/index-D5lKaksH.css +1 -0
- package/dist/ui/index.html +2 -2
- package/migrations/000_initial_schema.sql +174 -0
- package/migrations/001_entity_type_rename.sql +17 -0
- package/migrations/002_adapter_state_extend.sql +12 -0
- package/migrations/003_session_embeddings.sql +11 -0
- package/migrations/004_facts.sql +46 -0
- package/migrations/005_sources.sql +31 -0
- package/migrations/006_providers.sql +33 -0
- package/migrations/007_source_tokens.sql +17 -0
- package/migrations/008_fts_rebuild.sql +9 -0
- package/migrations/009_session_embedding_chunks.sql +46 -0
- package/migrations/010_sources_opencode.sql +30 -0
- package/migrations/011_sources_hermes_agent.sql +30 -0
- package/migrations/012_sources_aider.sql +30 -0
- package/migrations/013_adapter_state_failure_count.sql +12 -0
- package/migrations/014_sources_cursor.sql +30 -0
- package/migrations/015_sources_windsurf.sql +30 -0
- package/migrations/016_sources_codex.sql +30 -0
- package/migrations/017_signals.sql +31 -0
- package/migrations/018_repair_self_supersede.sql +21 -0
- package/migrations/019_split_replaces.sql +117 -0
- package/migrations/020_collapse_duplicate_active_facts.sql +45 -0
- package/migrations/pg/001_initial.sql +214 -0
- package/migrations/pg/018_repair_self_supersede.sql +16 -0
- package/migrations/pg/019_split_replaces.sql +32 -0
- package/migrations/pg/020_collapse_duplicate_active_facts.sql +46 -0
- package/package.json +2 -1
- package/dist/core/recall/useful-scan.d.ts +0 -52
- package/dist/core/recall/useful-scan.js +0 -300
- package/dist/core/recall/useful-scan.js.map +0 -1
- package/dist/hook/pre-compact-hook.d.ts +0 -26
- package/dist/hook/pre-compact-hook.js +0 -94
- package/dist/hook/pre-compact-hook.js.map +0 -1
- package/dist/hook/subagent-start-hook.d.ts +0 -30
- package/dist/hook/subagent-start-hook.js +0 -108
- package/dist/hook/subagent-start-hook.js.map +0 -1
- package/dist/ui/assets/index-BTj60bAH.js +0 -11
- package/dist/ui/assets/index-Baw2qIKc.css +0 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Migration 014: add 'cursor' to the sources.kind CHECK constraint.
|
|
2
|
+
--
|
|
3
|
+
-- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
|
|
4
|
+
-- Standard approach: rename → recreate → copy → drop old.
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = OFF;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE sources_new (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'hermes-agent', 'aider', 'cursor', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
|
|
11
|
+
name TEXT NOT NULL UNIQUE,
|
|
12
|
+
path_or_url TEXT,
|
|
13
|
+
runtime_label TEXT NOT NULL,
|
|
14
|
+
parse_config TEXT NOT NULL DEFAULT '{}',
|
|
15
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
16
|
+
token TEXT,
|
|
17
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
|
|
22
|
+
|
|
23
|
+
DROP TABLE sources;
|
|
24
|
+
ALTER TABLE sources_new RENAME TO sources;
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
|
|
27
|
+
|
|
28
|
+
PRAGMA foreign_keys = ON;
|
|
29
|
+
|
|
30
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (14, '014_sources_cursor');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Migration 015: add 'windsurf' to the sources.kind CHECK constraint.
|
|
2
|
+
--
|
|
3
|
+
-- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
|
|
4
|
+
-- Standard approach: rename → recreate → copy → drop old.
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = OFF;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE sources_new (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'hermes-agent', 'aider', 'cursor', 'windsurf', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
|
|
11
|
+
name TEXT NOT NULL UNIQUE,
|
|
12
|
+
path_or_url TEXT,
|
|
13
|
+
runtime_label TEXT NOT NULL,
|
|
14
|
+
parse_config TEXT NOT NULL DEFAULT '{}',
|
|
15
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
16
|
+
token TEXT,
|
|
17
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
|
|
22
|
+
|
|
23
|
+
DROP TABLE sources;
|
|
24
|
+
ALTER TABLE sources_new RENAME TO sources;
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
|
|
27
|
+
|
|
28
|
+
PRAGMA foreign_keys = ON;
|
|
29
|
+
|
|
30
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (15, '015_sources_windsurf');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Migration 016: add 'codex' to the sources.kind CHECK constraint.
|
|
2
|
+
--
|
|
3
|
+
-- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
|
|
4
|
+
-- Standard approach: rename → recreate → copy → drop old.
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = OFF;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE sources_new (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'codex', 'hermes', 'hermes-agent', 'aider', 'cursor', 'windsurf', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
|
|
11
|
+
name TEXT NOT NULL UNIQUE,
|
|
12
|
+
path_or_url TEXT,
|
|
13
|
+
runtime_label TEXT NOT NULL,
|
|
14
|
+
parse_config TEXT NOT NULL DEFAULT '{}',
|
|
15
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
16
|
+
token TEXT,
|
|
17
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
|
|
22
|
+
|
|
23
|
+
DROP TABLE sources;
|
|
24
|
+
ALTER TABLE sources_new RENAME TO sources;
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
|
|
27
|
+
|
|
28
|
+
PRAGMA foreign_keys = ON;
|
|
29
|
+
|
|
30
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (16, '016_sources_codex');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Migration 017: signals - agent self-improvement telemetry lane.
|
|
2
|
+
--
|
|
3
|
+
-- Distinct from facts: append-only, idempotent on a deterministic id, no
|
|
4
|
+
-- supersedence pointer, no embeddings. No FK to sessions; a signal can arrive
|
|
5
|
+
-- before or without a session row, session_id is a soft link.
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS signals (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
v INTEGER NOT NULL DEFAULT 1,
|
|
10
|
+
install_scope TEXT NOT NULL,
|
|
11
|
+
kind TEXT NOT NULL CHECK (kind IN ('gate', 'eval', 'review', 'test')),
|
|
12
|
+
producer TEXT NOT NULL,
|
|
13
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('pass', 'fail', 'fix', 'exhausted')),
|
|
14
|
+
model TEXT NOT NULL,
|
|
15
|
+
repo TEXT NOT NULL,
|
|
16
|
+
step TEXT,
|
|
17
|
+
detail TEXT,
|
|
18
|
+
session_id TEXT,
|
|
19
|
+
ts TEXT NOT NULL,
|
|
20
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- Aggregation hot path: failure-mode roll-up scoped to an install + repo/model.
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_signals_agg
|
|
25
|
+
ON signals(install_scope, repo, model, kind, step);
|
|
26
|
+
|
|
27
|
+
-- Retention prune + recency window scans.
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_signals_ts
|
|
29
|
+
ON signals(ts);
|
|
30
|
+
|
|
31
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (17, '017_signals');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Migration 018: repair self-supersedence damage.
|
|
2
|
+
--
|
|
3
|
+
-- The SQLite scanOnce path set supersedes = prior.session_id with no self-id
|
|
4
|
+
-- guard. Adapter session ids are deterministic, so a resumed (grown) transcript
|
|
5
|
+
-- re-ingested under the SAME id and insertSession wrote a self-loop edge
|
|
6
|
+
-- (id -> id, 'supersedes') then flipped that same row to status='superseded'.
|
|
7
|
+
-- Result: a session that supersedes itself and is marked superseded with no
|
|
8
|
+
-- real successor. The code fix guards both the scan path and insertSession;
|
|
9
|
+
-- this migration repairs rows already damaged.
|
|
10
|
+
|
|
11
|
+
DELETE FROM session_edges WHERE from_session = to_session AND kind = 'supersedes';
|
|
12
|
+
|
|
13
|
+
-- Restore sessions wrongly marked superseded: a legitimately superseded row
|
|
14
|
+
-- keeps a real incoming 'supersedes' edge (some other session -> it). Rows with
|
|
15
|
+
-- no remaining incoming edge were only self-superseded and should be 'closed'.
|
|
16
|
+
UPDATE sessions
|
|
17
|
+
SET status = 'closed', updated_at = datetime('now')
|
|
18
|
+
WHERE status = 'superseded'
|
|
19
|
+
AND id NOT IN (SELECT to_session FROM session_edges WHERE kind = 'supersedes');
|
|
20
|
+
|
|
21
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (18, '018_repair_self_supersede');
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
-- nlm:no-wrap
|
|
2
|
+
-- Migration 019: split the mechanical `replaces` relation out of `supersedes`.
|
|
3
|
+
--
|
|
4
|
+
-- Two relations were overloaded onto one edge kind ('supersedes') and one
|
|
5
|
+
-- status ('superseded'): mechanical re-ingest of a grown transcript
|
|
6
|
+
-- (supersede-on-resume) and operator-asserted epistemic overturn. This
|
|
7
|
+
-- migration introduces edge kind 'replaces' + status 'replaced' for the
|
|
8
|
+
-- mechanical case and reclassifies existing rows.
|
|
9
|
+
--
|
|
10
|
+
-- The mechanical signature is exact, no heuristics: an edge whose two sessions
|
|
11
|
+
-- share the same transcript_path is a re-parse (replaces); different paths is
|
|
12
|
+
-- operator supersedence (untouched). See
|
|
13
|
+
-- docs/plans/2026-06-10-supersedence-split.md.
|
|
14
|
+
--
|
|
15
|
+
-- Widening the CHECK constraints on sessions.status and session_edges.kind
|
|
16
|
+
-- requires a table rebuild (SQLite cannot ALTER a CHECK constraint, and
|
|
17
|
+
-- better-sqlite3 forbids writable_schema edits). The rebuild runs under
|
|
18
|
+
-- foreign_keys=OFF so dropping the old `sessions` table does not cascade-delete
|
|
19
|
+
-- its dependents; the pragma is a no-op inside a transaction, hence nlm:no-wrap.
|
|
20
|
+
|
|
21
|
+
PRAGMA foreign_keys = OFF;
|
|
22
|
+
|
|
23
|
+
BEGIN;
|
|
24
|
+
|
|
25
|
+
-- ── Rebuild sessions with the widened status CHECK ──────────────────────────
|
|
26
|
+
DROP TRIGGER IF EXISTS sessions_ai;
|
|
27
|
+
DROP TRIGGER IF EXISTS sessions_au;
|
|
28
|
+
DROP TRIGGER IF EXISTS sessions_ad;
|
|
29
|
+
|
|
30
|
+
CREATE TABLE sessions_new (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
runtime TEXT NOT NULL,
|
|
33
|
+
runtime_session_id TEXT,
|
|
34
|
+
started_at TEXT NOT NULL,
|
|
35
|
+
ended_at TEXT,
|
|
36
|
+
duration_min INTEGER,
|
|
37
|
+
label TEXT NOT NULL,
|
|
38
|
+
summary TEXT NOT NULL,
|
|
39
|
+
body TEXT,
|
|
40
|
+
status TEXT NOT NULL CHECK(status IN ('active','closed','superseded','replaced')),
|
|
41
|
+
transcript_kind TEXT,
|
|
42
|
+
transcript_path TEXT,
|
|
43
|
+
transcript_offset INTEGER,
|
|
44
|
+
transcript_length INTEGER,
|
|
45
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
46
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
INSERT INTO sessions_new
|
|
50
|
+
(id, runtime, runtime_session_id, started_at, ended_at, duration_min,
|
|
51
|
+
label, summary, body, status, transcript_kind, transcript_path,
|
|
52
|
+
transcript_offset, transcript_length, created_at, updated_at)
|
|
53
|
+
SELECT
|
|
54
|
+
id, runtime, runtime_session_id, started_at, ended_at, duration_min,
|
|
55
|
+
label, summary, body, status, transcript_kind, transcript_path,
|
|
56
|
+
transcript_offset, transcript_length, created_at, updated_at
|
|
57
|
+
FROM sessions;
|
|
58
|
+
|
|
59
|
+
DROP TABLE sessions;
|
|
60
|
+
ALTER TABLE sessions_new RENAME TO sessions;
|
|
61
|
+
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at DESC);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_runtime ON sessions(runtime);
|
|
65
|
+
|
|
66
|
+
CREATE TRIGGER sessions_ai AFTER INSERT ON sessions BEGIN
|
|
67
|
+
INSERT INTO sessions_fts(rowid, label, summary, body)
|
|
68
|
+
VALUES (new.rowid, new.label, new.summary, new.body);
|
|
69
|
+
END;
|
|
70
|
+
CREATE TRIGGER sessions_au AFTER UPDATE ON sessions BEGIN
|
|
71
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, label, summary, body)
|
|
72
|
+
VALUES('delete', old.rowid, old.label, old.summary, old.body);
|
|
73
|
+
INSERT INTO sessions_fts(rowid, label, summary, body)
|
|
74
|
+
VALUES (new.rowid, new.label, new.summary, new.body);
|
|
75
|
+
END;
|
|
76
|
+
CREATE TRIGGER sessions_ad AFTER DELETE ON sessions BEGIN
|
|
77
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, label, summary, body)
|
|
78
|
+
VALUES('delete', old.rowid, old.label, old.summary, old.body);
|
|
79
|
+
END;
|
|
80
|
+
|
|
81
|
+
-- The rebuild reassigned rowids; rebuild the external-content FTS index so its
|
|
82
|
+
-- rowid → session mapping matches the new sessions table.
|
|
83
|
+
INSERT INTO sessions_fts(sessions_fts) VALUES('rebuild');
|
|
84
|
+
|
|
85
|
+
-- ── Rebuild session_edges with the widened kind CHECK ───────────────────────
|
|
86
|
+
CREATE TABLE session_edges_new (
|
|
87
|
+
from_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
88
|
+
to_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
89
|
+
kind TEXT NOT NULL CHECK(kind IN ('supersedes','replaces','continues','branched_from','merged_from')),
|
|
90
|
+
PRIMARY KEY (from_session, to_session, kind)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
INSERT INTO session_edges_new (from_session, to_session, kind)
|
|
94
|
+
SELECT from_session, to_session, kind FROM session_edges;
|
|
95
|
+
|
|
96
|
+
DROP TABLE session_edges;
|
|
97
|
+
ALTER TABLE session_edges_new RENAME TO session_edges;
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_session_edges_from ON session_edges(from_session);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_session_edges_to ON session_edges(to_session);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_session_edges_kind ON session_edges(kind);
|
|
102
|
+
|
|
103
|
+
-- ── Reclassify mechanical edges + predecessor rows ──────────────────────────
|
|
104
|
+
UPDATE session_edges SET kind = 'replaces'
|
|
105
|
+
WHERE kind = 'supersedes'
|
|
106
|
+
AND (SELECT transcript_path FROM sessions WHERE id = from_session)
|
|
107
|
+
= (SELECT transcript_path FROM sessions WHERE id = to_session);
|
|
108
|
+
|
|
109
|
+
UPDATE sessions SET status = 'replaced', updated_at = datetime('now')
|
|
110
|
+
WHERE status = 'superseded'
|
|
111
|
+
AND id IN (SELECT to_session FROM session_edges WHERE kind = 'replaces');
|
|
112
|
+
|
|
113
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (19, '019_split_replaces');
|
|
114
|
+
|
|
115
|
+
COMMIT;
|
|
116
|
+
|
|
117
|
+
PRAGMA foreign_keys = ON;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- Migration 020: collapse duplicate active facts (NLM #301).
|
|
2
|
+
--
|
|
3
|
+
-- The SQLite supersedence loop superseded only the single most-recent active
|
|
4
|
+
-- prior per new fact (ORDER BY created_at DESC LIMIT 1). Once two facts for the
|
|
5
|
+
-- same (subject, predicate) were simultaneously active — a multi-pass backfill,
|
|
6
|
+
-- or an ON DELETE SET NULL un-supersede when a re-ingest deleted a chain head —
|
|
7
|
+
-- every later ingest cleared only one, so the duplicate persisted. The code fix
|
|
8
|
+
-- collapses ALL priors set-wise; this migration repairs the rows already stuck.
|
|
9
|
+
--
|
|
10
|
+
-- Within each conflicting (subject, predicate) group, the newest active fact
|
|
11
|
+
-- (created_at, then id as a deterministic tiebreak) stays active; the older
|
|
12
|
+
-- actives get superseded_by = that newest fact's id. Plain UPDATE — no table
|
|
13
|
+
-- rebuild, so the runner's BEGIN/COMMIT wrapper is fine (no -- nlm:no-wrap).
|
|
14
|
+
|
|
15
|
+
WITH winners AS (
|
|
16
|
+
SELECT subject, predicate, id AS winner_id
|
|
17
|
+
FROM (
|
|
18
|
+
SELECT
|
|
19
|
+
subject, predicate, id,
|
|
20
|
+
ROW_NUMBER() OVER (
|
|
21
|
+
PARTITION BY subject, predicate
|
|
22
|
+
ORDER BY created_at DESC, id DESC
|
|
23
|
+
) AS rn
|
|
24
|
+
FROM facts
|
|
25
|
+
WHERE superseded_by IS NULL
|
|
26
|
+
)
|
|
27
|
+
WHERE rn = 1
|
|
28
|
+
)
|
|
29
|
+
UPDATE facts
|
|
30
|
+
SET superseded_by = (
|
|
31
|
+
SELECT w.winner_id FROM winners w
|
|
32
|
+
WHERE w.subject = facts.subject AND w.predicate = facts.predicate
|
|
33
|
+
)
|
|
34
|
+
WHERE superseded_by IS NULL
|
|
35
|
+
AND id != (
|
|
36
|
+
SELECT w.winner_id FROM winners w
|
|
37
|
+
WHERE w.subject = facts.subject AND w.predicate = facts.predicate
|
|
38
|
+
)
|
|
39
|
+
AND EXISTS (
|
|
40
|
+
SELECT 1 FROM facts dup
|
|
41
|
+
WHERE dup.subject = facts.subject AND dup.predicate = facts.predicate
|
|
42
|
+
AND dup.superseded_by IS NULL AND dup.id != facts.id
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (20, '020_collapse_duplicate_active_facts');
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
-- NLM PostgreSQL schema v1.
|
|
2
|
+
-- Mirrors SQLite migrations/000–016 but uses PG idioms:
|
|
3
|
+
-- - SERIAL / TEXT for PKs (no AUTOINCREMENT)
|
|
4
|
+
-- - pgvector for embeddings instead of sqlite-vec
|
|
5
|
+
-- - tsvector generated column + GIN for FTS5 equivalent
|
|
6
|
+
-- - NOW() instead of datetime('now')
|
|
7
|
+
-- - ON CONFLICT DO NOTHING / DO UPDATE instead of INSERT OR IGNORE / REPLACE
|
|
8
|
+
|
|
9
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
10
|
+
|
|
11
|
+
-- ── Sessions ────────────────────────────────────────────────────────────────
|
|
12
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
runtime TEXT NOT NULL,
|
|
15
|
+
runtime_session_id TEXT,
|
|
16
|
+
started_at TEXT NOT NULL,
|
|
17
|
+
ended_at TEXT,
|
|
18
|
+
duration_min REAL,
|
|
19
|
+
label TEXT NOT NULL,
|
|
20
|
+
summary TEXT NOT NULL,
|
|
21
|
+
body TEXT,
|
|
22
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'closed', 'superseded')),
|
|
23
|
+
transcript_kind TEXT,
|
|
24
|
+
transcript_path TEXT,
|
|
25
|
+
transcript_offset BIGINT,
|
|
26
|
+
transcript_length BIGINT,
|
|
27
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
28
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
29
|
+
fts_vector TSVECTOR GENERATED ALWAYS AS (
|
|
30
|
+
setweight(to_tsvector('english', coalesce(label, '')), 'A') ||
|
|
31
|
+
setweight(to_tsvector('english', coalesce(summary, '')), 'B') ||
|
|
32
|
+
setweight(to_tsvector('english', coalesce(body, '')), 'C')
|
|
33
|
+
) STORED
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS sessions_fts_idx ON sessions USING GIN(fts_vector);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at);
|
|
38
|
+
|
|
39
|
+
-- ── Session embeddings (chunks + map, mirrors SQLite architecture) ──────────
|
|
40
|
+
CREATE TABLE IF NOT EXISTS session_embedding_chunks (
|
|
41
|
+
chunk_id SERIAL PRIMARY KEY,
|
|
42
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
43
|
+
chunk_idx INTEGER NOT NULL,
|
|
44
|
+
embedding vector(768) NOT NULL
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE INDEX IF NOT EXISTS session_chunks_embedding_idx
|
|
48
|
+
ON session_embedding_chunks USING ivfflat (embedding vector_l2_ops)
|
|
49
|
+
WITH (lists = 100);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS session_chunk_map (
|
|
52
|
+
chunk_id INTEGER NOT NULL REFERENCES session_embedding_chunks(chunk_id) ON DELETE CASCADE,
|
|
53
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
54
|
+
chunk_idx INTEGER NOT NULL,
|
|
55
|
+
PRIMARY KEY (chunk_id)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_session_chunk_map_session ON session_chunk_map(session_id);
|
|
59
|
+
|
|
60
|
+
-- ── Markers (decisions + open questions) ────────────────────────────────────
|
|
61
|
+
CREATE TABLE IF NOT EXISTS markers (
|
|
62
|
+
id SERIAL PRIMARY KEY,
|
|
63
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
64
|
+
kind TEXT NOT NULL CHECK (kind IN ('decision', 'open')),
|
|
65
|
+
text TEXT NOT NULL,
|
|
66
|
+
position INTEGER NOT NULL DEFAULT 0
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_markers_session ON markers(session_id);
|
|
70
|
+
|
|
71
|
+
-- ── Entities ────────────────────────────────────────────────────────────────
|
|
72
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
73
|
+
canonical TEXT PRIMARY KEY,
|
|
74
|
+
type TEXT NOT NULL DEFAULT 'candidate',
|
|
75
|
+
status TEXT NOT NULL DEFAULT 'candidate',
|
|
76
|
+
source TEXT NOT NULL DEFAULT 'auto-detected',
|
|
77
|
+
first_seen_session TEXT REFERENCES sessions(id) ON DELETE SET NULL,
|
|
78
|
+
last_seen_session TEXT REFERENCES sessions(id) ON DELETE SET NULL,
|
|
79
|
+
session_count INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
81
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS session_entities (
|
|
85
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
86
|
+
entity_canonical TEXT NOT NULL REFERENCES entities(canonical) ON DELETE CASCADE,
|
|
87
|
+
PRIMARY KEY (session_id, entity_canonical)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- ── Session edges ────────────────────────────────────────────────────────────
|
|
91
|
+
CREATE TABLE IF NOT EXISTS session_edges (
|
|
92
|
+
from_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
93
|
+
to_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
94
|
+
kind TEXT NOT NULL CHECK (kind IN ('supersedes', 'continues')),
|
|
95
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
96
|
+
PRIMARY KEY (from_session, to_session, kind)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
-- ── Facts ────────────────────────────────────────────────────────────────────
|
|
100
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
101
|
+
id TEXT PRIMARY KEY,
|
|
102
|
+
kind TEXT NOT NULL CHECK (kind IN ('decision', 'open', 'attribute')),
|
|
103
|
+
subject TEXT NOT NULL,
|
|
104
|
+
predicate TEXT NOT NULL,
|
|
105
|
+
value TEXT NOT NULL,
|
|
106
|
+
source_session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
107
|
+
source_quote TEXT,
|
|
108
|
+
created_at TEXT NOT NULL DEFAULT to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'),
|
|
109
|
+
superseded_by TEXT REFERENCES facts(id) ON DELETE SET NULL,
|
|
110
|
+
confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_facts_subject_predicate_current
|
|
114
|
+
ON facts(subject, predicate) WHERE superseded_by IS NULL;
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_facts_subject_current
|
|
116
|
+
ON facts(subject) WHERE superseded_by IS NULL;
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_facts_session
|
|
118
|
+
ON facts(source_session_id);
|
|
119
|
+
|
|
120
|
+
-- ── Fact embeddings ──────────────────────────────────────────────────────────
|
|
121
|
+
CREATE TABLE IF NOT EXISTS fact_embeddings (
|
|
122
|
+
fact_id TEXT PRIMARY KEY REFERENCES facts(id) ON DELETE CASCADE,
|
|
123
|
+
embedding vector(768) NOT NULL
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE INDEX IF NOT EXISTS fact_embeddings_idx
|
|
127
|
+
ON fact_embeddings USING ivfflat (embedding vector_l2_ops)
|
|
128
|
+
WITH (lists = 100);
|
|
129
|
+
|
|
130
|
+
-- ── Actions (event-sourced action log) ───────────────────────────────────────
|
|
131
|
+
CREATE TABLE IF NOT EXISTS actions (
|
|
132
|
+
id TEXT PRIMARY KEY,
|
|
133
|
+
timestamp TEXT NOT NULL,
|
|
134
|
+
kind TEXT NOT NULL,
|
|
135
|
+
subject_type TEXT NOT NULL,
|
|
136
|
+
subject_id TEXT NOT NULL,
|
|
137
|
+
payload TEXT,
|
|
138
|
+
actor TEXT NOT NULL DEFAULT 'user',
|
|
139
|
+
runtime TEXT,
|
|
140
|
+
reverted_by TEXT REFERENCES actions(id),
|
|
141
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_actions_subject ON actions(subject_type, subject_id);
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_actions_kind ON actions(kind);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_actions_timestamp ON actions(timestamp DESC);
|
|
147
|
+
|
|
148
|
+
-- ── Sources registry ─────────────────────────────────────────────────────────
|
|
149
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
150
|
+
id SERIAL PRIMARY KEY,
|
|
151
|
+
kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'codex', 'hermes', 'hermes-agent', 'aider', 'cursor', 'windsurf', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
|
|
152
|
+
name TEXT NOT NULL UNIQUE,
|
|
153
|
+
path_or_url TEXT,
|
|
154
|
+
runtime_label TEXT NOT NULL,
|
|
155
|
+
parse_config TEXT NOT NULL DEFAULT '{}',
|
|
156
|
+
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
157
|
+
token TEXT,
|
|
158
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
159
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
-- ── Providers registry ───────────────────────────────────────────────────────
|
|
163
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
164
|
+
id SERIAL PRIMARY KEY,
|
|
165
|
+
kind TEXT NOT NULL CHECK (kind IN ('deepseek', 'ollama', 'openai', 'anthropic', 'openrouter', 'openai-compatible')),
|
|
166
|
+
name TEXT NOT NULL UNIQUE,
|
|
167
|
+
base_url TEXT,
|
|
168
|
+
api_key TEXT,
|
|
169
|
+
default_model TEXT,
|
|
170
|
+
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
171
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
172
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
-- ── Adapter state (per-runtime offsets for resumability) ─────────────────────
|
|
176
|
+
CREATE TABLE IF NOT EXISTS adapter_state (
|
|
177
|
+
adapter_name TEXT NOT NULL,
|
|
178
|
+
source_path TEXT NOT NULL,
|
|
179
|
+
last_offset BIGINT NOT NULL DEFAULT 0,
|
|
180
|
+
last_processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
181
|
+
file_size BIGINT,
|
|
182
|
+
session_id TEXT,
|
|
183
|
+
failure_count INTEGER NOT NULL DEFAULT 0,
|
|
184
|
+
PRIMARY KEY (adapter_name, source_path)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
-- ── Schema migrations tracker ────────────────────────────────────────────────
|
|
188
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
189
|
+
version INTEGER PRIMARY KEY,
|
|
190
|
+
name TEXT NOT NULL,
|
|
191
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
INSERT INTO schema_migrations (version, name) VALUES (1, '001_initial')
|
|
195
|
+
ON CONFLICT DO NOTHING;
|
|
196
|
+
|
|
197
|
+
-- Signals - agent self-improvement telemetry lane (mirror of SQLite 017).
|
|
198
|
+
CREATE TABLE IF NOT EXISTS signals (
|
|
199
|
+
id TEXT PRIMARY KEY,
|
|
200
|
+
v INTEGER NOT NULL DEFAULT 1,
|
|
201
|
+
install_scope TEXT NOT NULL,
|
|
202
|
+
kind TEXT NOT NULL CHECK (kind IN ('gate', 'eval', 'review', 'test')),
|
|
203
|
+
producer TEXT NOT NULL,
|
|
204
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('pass', 'fail', 'fix', 'exhausted')),
|
|
205
|
+
model TEXT NOT NULL,
|
|
206
|
+
repo TEXT NOT NULL,
|
|
207
|
+
step TEXT,
|
|
208
|
+
detail TEXT,
|
|
209
|
+
session_id TEXT,
|
|
210
|
+
ts TEXT NOT NULL,
|
|
211
|
+
created_at TEXT NOT NULL DEFAULT (now()::text)
|
|
212
|
+
);
|
|
213
|
+
CREATE INDEX IF NOT EXISTS idx_signals_agg ON signals(install_scope, repo, model, kind, step);
|
|
214
|
+
CREATE INDEX IF NOT EXISTS idx_signals_ts ON signals(ts);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- PG parity for SQLite migration 018: repair self-supersedence damage.
|
|
2
|
+
--
|
|
3
|
+
-- One-shot repair, applied manually by an operator against a PG canonical
|
|
4
|
+
-- store (PgStorage.init only runs 001_initial.sql, there is no version-gated
|
|
5
|
+
-- runner on the PG side). Mirrors migrations/018_repair_self_supersede.sql.
|
|
6
|
+
--
|
|
7
|
+
-- Removes self-loop 'supersedes' edges (id -> id) written by the unguarded
|
|
8
|
+
-- scan path, then restores sessions wrongly marked superseded — those left
|
|
9
|
+
-- without any real incoming 'supersedes' edge.
|
|
10
|
+
|
|
11
|
+
DELETE FROM session_edges WHERE from_session = to_session AND kind = 'supersedes';
|
|
12
|
+
|
|
13
|
+
UPDATE sessions
|
|
14
|
+
SET status = 'closed', updated_at = NOW()
|
|
15
|
+
WHERE status = 'superseded'
|
|
16
|
+
AND id NOT IN (SELECT to_session FROM session_edges WHERE kind = 'supersedes');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- PG parity for SQLite migration 019: split the mechanical `replaces` relation
|
|
2
|
+
-- out of `supersedes`.
|
|
3
|
+
--
|
|
4
|
+
-- One-shot repair, applied manually by an operator against a PG canonical store
|
|
5
|
+
-- (PgStorage.init only runs 001_initial.sql; there is no version-gated runner on
|
|
6
|
+
-- the PG side). Mirrors migrations/019_split_replaces.sql.
|
|
7
|
+
--
|
|
8
|
+
-- Widens the CHECK constraints on sessions.status and session_edges.kind to
|
|
9
|
+
-- admit 'replaced' / 'replaces', then reclassifies existing mechanical edges:
|
|
10
|
+
-- an edge whose two sessions share the same transcript_path is a re-parse
|
|
11
|
+
-- (replaces); different paths is operator supersedence (untouched).
|
|
12
|
+
|
|
13
|
+
BEGIN;
|
|
14
|
+
|
|
15
|
+
ALTER TABLE sessions DROP CONSTRAINT IF EXISTS sessions_status_check;
|
|
16
|
+
ALTER TABLE sessions ADD CONSTRAINT sessions_status_check
|
|
17
|
+
CHECK (status IN ('active', 'closed', 'superseded', 'replaced'));
|
|
18
|
+
|
|
19
|
+
ALTER TABLE session_edges DROP CONSTRAINT IF EXISTS session_edges_kind_check;
|
|
20
|
+
ALTER TABLE session_edges ADD CONSTRAINT session_edges_kind_check
|
|
21
|
+
CHECK (kind IN ('supersedes', 'replaces', 'continues'));
|
|
22
|
+
|
|
23
|
+
UPDATE session_edges e SET kind = 'replaces'
|
|
24
|
+
WHERE e.kind = 'supersedes'
|
|
25
|
+
AND (SELECT transcript_path FROM sessions WHERE id = e.from_session)
|
|
26
|
+
= (SELECT transcript_path FROM sessions WHERE id = e.to_session);
|
|
27
|
+
|
|
28
|
+
UPDATE sessions SET status = 'replaced', updated_at = NOW()
|
|
29
|
+
WHERE status = 'superseded'
|
|
30
|
+
AND id IN (SELECT to_session FROM session_edges WHERE kind = 'replaces');
|
|
31
|
+
|
|
32
|
+
COMMIT;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- PG parity for SQLite migration 020: collapse duplicate active facts (#301).
|
|
2
|
+
--
|
|
3
|
+
-- One-shot repair, applied manually by an operator against a PG canonical store
|
|
4
|
+
-- (PgStorage.init only runs 001_initial.sql; there is no version-gated runner on
|
|
5
|
+
-- the PG side). Mirrors migrations/020_collapse_duplicate_active_facts.sql.
|
|
6
|
+
--
|
|
7
|
+
-- Within each conflicting (subject, predicate) group, the newest active fact
|
|
8
|
+
-- (created_at, then id) stays active; the older actives get superseded_by set
|
|
9
|
+
-- to that newest fact's id. The PG ingest path already collapsed all priors
|
|
10
|
+
-- set-wise, so this only repairs rows the SQLite-era single-prior loop stranded
|
|
11
|
+
-- in a corpus migrated to PG.
|
|
12
|
+
|
|
13
|
+
BEGIN;
|
|
14
|
+
|
|
15
|
+
WITH winners AS (
|
|
16
|
+
SELECT subject, predicate, id AS winner_id
|
|
17
|
+
FROM (
|
|
18
|
+
SELECT
|
|
19
|
+
subject, predicate, id,
|
|
20
|
+
ROW_NUMBER() OVER (
|
|
21
|
+
PARTITION BY subject, predicate
|
|
22
|
+
ORDER BY created_at DESC, id DESC
|
|
23
|
+
) AS rn
|
|
24
|
+
FROM facts
|
|
25
|
+
WHERE superseded_by IS NULL
|
|
26
|
+
) ranked
|
|
27
|
+
WHERE rn = 1
|
|
28
|
+
),
|
|
29
|
+
conflicts AS (
|
|
30
|
+
SELECT subject, predicate
|
|
31
|
+
FROM facts
|
|
32
|
+
WHERE superseded_by IS NULL
|
|
33
|
+
GROUP BY subject, predicate
|
|
34
|
+
HAVING COUNT(*) > 1
|
|
35
|
+
)
|
|
36
|
+
UPDATE facts f
|
|
37
|
+
SET superseded_by = w.winner_id
|
|
38
|
+
FROM winners w, conflicts c
|
|
39
|
+
WHERE f.subject = w.subject
|
|
40
|
+
AND f.predicate = w.predicate
|
|
41
|
+
AND f.subject = c.subject
|
|
42
|
+
AND f.predicate = c.predicate
|
|
43
|
+
AND f.superseded_by IS NULL
|
|
44
|
+
AND f.id != w.winner_id;
|
|
45
|
+
|
|
46
|
+
COMMIT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nlm-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Local-first non-linear memory operating system for AI operators.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
+
"migrations",
|
|
31
32
|
"plugin",
|
|
32
33
|
"plugin-hermes-agent",
|
|
33
34
|
"nlm",
|