@shadowforge0/aquifer-memory 1.5.9 → 1.6.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 (65) hide show
  1. package/.env.example +23 -0
  2. package/README.md +96 -73
  3. package/README_CN.md +659 -0
  4. package/README_TW.md +680 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +374 -39
  8. package/consumers/codex-handoff.js +152 -0
  9. package/consumers/codex.js +1549 -0
  10. package/consumers/default/daily-entries.js +23 -4
  11. package/consumers/default/index.js +2 -2
  12. package/consumers/default/prompts/summary.js +6 -6
  13. package/consumers/mcp.js +131 -7
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +44 -4
  16. package/consumers/shared/config.js +28 -0
  17. package/consumers/shared/factory.js +2 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +53 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +384 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/insights.js +210 -58
  25. package/core/mcp-manifest.js +69 -2
  26. package/core/memory-bootstrap.js +188 -0
  27. package/core/memory-consolidation.js +1236 -0
  28. package/core/memory-promotion.js +544 -0
  29. package/core/memory-recall.js +247 -0
  30. package/core/memory-records.js +581 -0
  31. package/core/memory-safety-gate.js +224 -0
  32. package/core/session-finalization.js +350 -0
  33. package/core/storage.js +456 -2
  34. package/docs/getting-started.md +99 -0
  35. package/docs/postprocess-contract.md +2 -2
  36. package/docs/setup.md +51 -2
  37. package/package.json +31 -9
  38. package/pipeline/normalize/adapters/codex.js +106 -0
  39. package/pipeline/normalize/detect.js +3 -2
  40. package/schema/001-base.sql +3 -0
  41. package/schema/007-v1-foundation.sql +273 -0
  42. package/schema/008-session-finalizations.sql +50 -0
  43. package/schema/009-v1-assertion-plane.sql +193 -0
  44. package/schema/010-v1-finalization-review.sql +160 -0
  45. package/schema/011-v1-compaction-claim.sql +46 -0
  46. package/schema/012-v1-compaction-lease.sql +39 -0
  47. package/schema/013-v1-compaction-lineage.sql +193 -0
  48. package/scripts/backfill-canonical-key.js +250 -0
  49. package/scripts/codex-recovery.js +532 -0
  50. package/consumers/miranda/context-inject.js +0 -119
  51. package/consumers/miranda/daily-entries.js +0 -224
  52. package/consumers/miranda/index.js +0 -364
  53. package/consumers/miranda/instance.js +0 -55
  54. package/consumers/miranda/llm.js +0 -99
  55. package/consumers/miranda/profile.json +0 -145
  56. package/consumers/miranda/prompts/summary.js +0 -303
  57. package/consumers/miranda/recall-format.js +0 -76
  58. package/consumers/miranda/render-daily-md.js +0 -186
  59. package/consumers/miranda/workspace-files.js +0 -91
  60. package/scripts/drop-entity-state-history.sql +0 -17
  61. package/scripts/drop-insights.sql +0 -12
  62. package/scripts/install-openclaw.sh +0 -59
  63. package/scripts/queries.json +0 -45
  64. package/scripts/retro-recall-bench.js +0 -409
  65. package/scripts/sample-bench-queries.sql +0 -75
@@ -0,0 +1,193 @@
1
+ -- Aquifer v1 structured assertion plane
2
+ -- Requires: 001-base.sql, 007-v1-foundation.sql, and 008-session-finalizations.sql
3
+ -- Usage: replace ${schema} with actual schema name
4
+ --
5
+ -- This migration is additive. It introduces a new v1 structured assertion
6
+ -- table and related integrity guards without repurposing legacy 004-facts rows.
7
+
8
+ -- =========================================================================
9
+ -- Scope tenant safety: prevent future cross-tenant ancestry leakage
10
+ -- =========================================================================
11
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_scopes_tenant_row
12
+ ON ${schema}.scopes (tenant_id, id);
13
+
14
+ CREATE OR REPLACE FUNCTION ${schema}.scope_parent_tenant_guard()
15
+ RETURNS trigger
16
+ LANGUAGE plpgsql
17
+ AS $$
18
+ DECLARE
19
+ parent_tenant TEXT;
20
+ BEGIN
21
+ IF NEW.parent_scope_id IS NULL THEN
22
+ RETURN NEW;
23
+ END IF;
24
+
25
+ SELECT tenant_id
26
+ INTO parent_tenant
27
+ FROM ${schema}.scopes
28
+ WHERE id = NEW.parent_scope_id;
29
+
30
+ IF parent_tenant IS NOT NULL AND parent_tenant <> NEW.tenant_id THEN
31
+ RAISE EXCEPTION
32
+ 'scope parent tenant mismatch: child tenant=% parent scope id=% parent tenant=%',
33
+ NEW.tenant_id, NEW.parent_scope_id, parent_tenant
34
+ USING ERRCODE = '23514';
35
+ END IF;
36
+
37
+ RETURN NEW;
38
+ END;
39
+ $$;
40
+
41
+ DROP TRIGGER IF EXISTS trg_scopes_parent_tenant_guard
42
+ ON ${schema}.scopes;
43
+
44
+ CREATE TRIGGER trg_scopes_parent_tenant_guard
45
+ BEFORE INSERT OR UPDATE OF tenant_id, parent_scope_id
46
+ ON ${schema}.scopes
47
+ FOR EACH ROW
48
+ EXECUTE FUNCTION ${schema}.scope_parent_tenant_guard();
49
+
50
+ -- =========================================================================
51
+ -- fact_assertions_v1: structured assertion plane for curated memory
52
+ -- =========================================================================
53
+ CREATE TABLE IF NOT EXISTS ${schema}.fact_assertions_v1 (
54
+ id BIGSERIAL PRIMARY KEY,
55
+ tenant_id TEXT NOT NULL DEFAULT 'default',
56
+ canonical_key TEXT NOT NULL CHECK (btrim(canonical_key) <> ''),
57
+ scope_id BIGINT NOT NULL REFERENCES ${schema}.scopes(id) ON DELETE RESTRICT,
58
+ subject_entity_id BIGINT,
59
+ predicate TEXT NOT NULL CHECK (btrim(predicate) <> ''),
60
+ object_kind TEXT NOT NULL
61
+ CHECK (object_kind IN ('entity','value','none')),
62
+ object_entity_id BIGINT,
63
+ object_value_json JSONB,
64
+ qualifiers_json JSONB NOT NULL DEFAULT '{}'::jsonb,
65
+ valid_from TIMESTAMPTZ,
66
+ valid_to TIMESTAMPTZ,
67
+ observed_at TIMESTAMPTZ,
68
+ stale_after TIMESTAMPTZ,
69
+ accepted_at TIMESTAMPTZ,
70
+ revoked_at TIMESTAMPTZ,
71
+ superseded_at TIMESTAMPTZ,
72
+ status TEXT NOT NULL DEFAULT 'candidate'
73
+ CHECK (status IN (
74
+ 'candidate','active','stale','superseded','revoked',
75
+ 'tombstoned','quarantined','archived'
76
+ )),
77
+ authority TEXT NOT NULL DEFAULT 'llm_inference'
78
+ CHECK (authority IN (
79
+ 'user_explicit','executable_evidence','verified_summary',
80
+ 'llm_inference','raw_transcript','manual','system'
81
+ )),
82
+ assertion_hash TEXT NOT NULL CHECK (btrim(assertion_hash) <> ''),
83
+ superseded_by BIGINT REFERENCES ${schema}.fact_assertions_v1(id) ON DELETE SET NULL,
84
+ version_id BIGINT REFERENCES ${schema}.versions(id) ON DELETE SET NULL,
85
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
86
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
87
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
88
+ CHECK (valid_to IS NULL OR valid_from IS NULL OR valid_to > valid_from),
89
+ CHECK (revoked_at IS NULL OR accepted_at IS NULL OR revoked_at >= accepted_at),
90
+ CHECK (superseded_at IS NULL OR accepted_at IS NULL OR superseded_at >= accepted_at),
91
+ CHECK (NOT (revoked_at IS NOT NULL AND superseded_at IS NOT NULL)),
92
+ CHECK (
93
+ (object_kind = 'entity' AND object_entity_id IS NOT NULL AND object_value_json IS NULL)
94
+ OR (object_kind = 'value' AND object_entity_id IS NULL AND object_value_json IS NOT NULL)
95
+ OR (object_kind = 'none' AND object_entity_id IS NULL AND object_value_json IS NULL)
96
+ )
97
+ );
98
+
99
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fact_assertions_v1_tenant_row
100
+ ON ${schema}.fact_assertions_v1 (tenant_id, id);
101
+
102
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_fact_assertions_v1_active_canonical
103
+ ON ${schema}.fact_assertions_v1 (tenant_id, canonical_key)
104
+ WHERE status = 'active';
105
+
106
+ CREATE INDEX IF NOT EXISTS idx_fact_assertions_v1_scope_status
107
+ ON ${schema}.fact_assertions_v1 (tenant_id, scope_id, status, observed_at DESC, id);
108
+
109
+ CREATE INDEX IF NOT EXISTS idx_fact_assertions_v1_hash
110
+ ON ${schema}.fact_assertions_v1 (tenant_id, assertion_hash);
111
+
112
+ CREATE INDEX IF NOT EXISTS idx_fact_assertions_v1_superseded_by
113
+ ON ${schema}.fact_assertions_v1 (superseded_by)
114
+ WHERE superseded_by IS NOT NULL;
115
+
116
+ DO $$
117
+ BEGIN
118
+ IF NOT EXISTS (
119
+ SELECT 1
120
+ FROM pg_constraint
121
+ WHERE conrelid = '${schema}.fact_assertions_v1'::regclass
122
+ AND conname = 'fact_assertions_v1_scope_tenant_fk'
123
+ ) THEN
124
+ ALTER TABLE ${schema}.fact_assertions_v1
125
+ ADD CONSTRAINT fact_assertions_v1_scope_tenant_fk
126
+ FOREIGN KEY (tenant_id, scope_id)
127
+ REFERENCES ${schema}.scopes (tenant_id, id)
128
+ ON DELETE RESTRICT
129
+ NOT VALID;
130
+ END IF;
131
+ END;
132
+ $$;
133
+
134
+ COMMENT ON TABLE ${schema}.fact_assertions_v1 IS
135
+ 'v1 structured assertion plane. New table; does not reuse legacy 004-facts rows.';
136
+
137
+ -- =========================================================================
138
+ -- memory_records: structured-assertion linkage + system-time lifecycle fields
139
+ -- =========================================================================
140
+ ALTER TABLE ${schema}.memory_records
141
+ ADD COLUMN IF NOT EXISTS backing_fact_id BIGINT,
142
+ ADD COLUMN IF NOT EXISTS observed_at TIMESTAMPTZ,
143
+ ADD COLUMN IF NOT EXISTS revoked_at TIMESTAMPTZ,
144
+ ADD COLUMN IF NOT EXISTS superseded_at TIMESTAMPTZ;
145
+
146
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_records_tenant_row
147
+ ON ${schema}.memory_records (tenant_id, id);
148
+
149
+ CREATE INDEX IF NOT EXISTS idx_memory_records_backing_fact
150
+ ON ${schema}.memory_records (tenant_id, backing_fact_id)
151
+ WHERE backing_fact_id IS NOT NULL;
152
+
153
+ DO $$
154
+ BEGIN
155
+ IF NOT EXISTS (
156
+ SELECT 1
157
+ FROM pg_constraint
158
+ WHERE conrelid = '${schema}.memory_records'::regclass
159
+ AND conname = 'memory_records_scope_tenant_fk'
160
+ ) THEN
161
+ ALTER TABLE ${schema}.memory_records
162
+ ADD CONSTRAINT memory_records_scope_tenant_fk
163
+ FOREIGN KEY (tenant_id, scope_id)
164
+ REFERENCES ${schema}.scopes (tenant_id, id)
165
+ ON DELETE RESTRICT
166
+ NOT VALID;
167
+ END IF;
168
+ END;
169
+ $$;
170
+
171
+ DO $$
172
+ BEGIN
173
+ IF NOT EXISTS (
174
+ SELECT 1
175
+ FROM pg_constraint
176
+ WHERE conrelid = '${schema}.memory_records'::regclass
177
+ AND conname = 'memory_records_backing_fact_tenant_fk'
178
+ ) THEN
179
+ ALTER TABLE ${schema}.memory_records
180
+ ADD CONSTRAINT memory_records_backing_fact_tenant_fk
181
+ FOREIGN KEY (tenant_id, backing_fact_id)
182
+ REFERENCES ${schema}.fact_assertions_v1 (tenant_id, id)
183
+ NOT VALID;
184
+ END IF;
185
+ END;
186
+ $$;
187
+
188
+ -- =========================================================================
189
+ -- compaction_runs: source/output coverage fields for auditability
190
+ -- =========================================================================
191
+ ALTER TABLE ${schema}.compaction_runs
192
+ ADD COLUMN IF NOT EXISTS source_coverage JSONB NOT NULL DEFAULT '{}'::jsonb,
193
+ ADD COLUMN IF NOT EXISTS output_coverage JSONB NOT NULL DEFAULT '{}'::jsonb;
@@ -0,0 +1,160 @@
1
+ -- Aquifer v1 finalization review and lineage
2
+ -- Requires: 001-base.sql, 007-v1-foundation.sql, 008-session-finalizations.sql, 009-v1-assertion-plane.sql
3
+ -- Usage: replace ${schema} with actual schema name
4
+ --
5
+ -- Finalization must leave a human-reviewable artifact and row-level lineage.
6
+ -- DB storage alone is not treated as usable runtime memory.
7
+
8
+ -- =========================================================================
9
+ -- Finalization ledger: explicit human review and minimal SessionStart text
10
+ -- =========================================================================
11
+ ALTER TABLE ${schema}.session_finalizations
12
+ ADD COLUMN IF NOT EXISTS summary_text TEXT,
13
+ ADD COLUMN IF NOT EXISTS structured_summary JSONB NOT NULL DEFAULT '{}'::jsonb,
14
+ ADD COLUMN IF NOT EXISTS human_review_text TEXT,
15
+ ADD COLUMN IF NOT EXISTS session_start_text TEXT;
16
+
17
+ COMMENT ON COLUMN ${schema}.session_finalizations.human_review_text IS
18
+ 'Human-facing finalization review: concise summary of what was made active, left open, quarantined, and excluded.';
19
+
20
+ COMMENT ON COLUMN ${schema}.session_finalizations.session_start_text IS
21
+ 'Minimal active curated context eligible for SessionStart injection; excludes raw transcript/debug/audit fields.';
22
+
23
+ -- =========================================================================
24
+ -- Lifecycle semantics: incorrect is an explicit non-serving state
25
+ -- =========================================================================
26
+ ALTER TABLE ${schema}.memory_records
27
+ DROP CONSTRAINT IF EXISTS memory_records_status_check;
28
+
29
+ ALTER TABLE ${schema}.memory_records
30
+ ADD CONSTRAINT memory_records_status_check
31
+ CHECK (status IN (
32
+ 'candidate','active','stale','superseded','revoked',
33
+ 'tombstoned','quarantined','archived','incorrect'
34
+ ));
35
+
36
+ ALTER TABLE ${schema}.fact_assertions_v1
37
+ DROP CONSTRAINT IF EXISTS fact_assertions_v1_status_check;
38
+
39
+ ALTER TABLE ${schema}.fact_assertions_v1
40
+ ADD CONSTRAINT fact_assertions_v1_status_check
41
+ CHECK (status IN (
42
+ 'candidate','active','stale','superseded','revoked',
43
+ 'tombstoned','quarantined','archived','incorrect'
44
+ ));
45
+
46
+ -- =========================================================================
47
+ -- Row-level finalization lineage
48
+ -- =========================================================================
49
+ ALTER TABLE ${schema}.memory_records
50
+ ADD COLUMN IF NOT EXISTS created_by_finalization_id BIGINT;
51
+
52
+ ALTER TABLE ${schema}.fact_assertions_v1
53
+ ADD COLUMN IF NOT EXISTS created_by_finalization_id BIGINT;
54
+
55
+ ALTER TABLE ${schema}.evidence_refs
56
+ ADD COLUMN IF NOT EXISTS created_by_finalization_id BIGINT;
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_memory_records_created_by_finalization
59
+ ON ${schema}.memory_records (tenant_id, created_by_finalization_id)
60
+ WHERE created_by_finalization_id IS NOT NULL;
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_fact_assertions_created_by_finalization
63
+ ON ${schema}.fact_assertions_v1 (tenant_id, created_by_finalization_id)
64
+ WHERE created_by_finalization_id IS NOT NULL;
65
+
66
+ CREATE INDEX IF NOT EXISTS idx_evidence_refs_created_by_finalization
67
+ ON ${schema}.evidence_refs (tenant_id, created_by_finalization_id)
68
+ WHERE created_by_finalization_id IS NOT NULL;
69
+
70
+ DO $$
71
+ BEGIN
72
+ IF NOT EXISTS (
73
+ SELECT 1
74
+ FROM pg_constraint
75
+ WHERE conrelid = '${schema}.memory_records'::regclass
76
+ AND conname = 'memory_records_created_by_finalization_fk'
77
+ ) THEN
78
+ ALTER TABLE ${schema}.memory_records
79
+ ADD CONSTRAINT memory_records_created_by_finalization_fk
80
+ FOREIGN KEY (created_by_finalization_id)
81
+ REFERENCES ${schema}.session_finalizations (id)
82
+ ON DELETE SET NULL
83
+ NOT VALID;
84
+ END IF;
85
+ END;
86
+ $$;
87
+
88
+ DO $$
89
+ BEGIN
90
+ IF NOT EXISTS (
91
+ SELECT 1
92
+ FROM pg_constraint
93
+ WHERE conrelid = '${schema}.fact_assertions_v1'::regclass
94
+ AND conname = 'fact_assertions_created_by_finalization_fk'
95
+ ) THEN
96
+ ALTER TABLE ${schema}.fact_assertions_v1
97
+ ADD CONSTRAINT fact_assertions_created_by_finalization_fk
98
+ FOREIGN KEY (created_by_finalization_id)
99
+ REFERENCES ${schema}.session_finalizations (id)
100
+ ON DELETE SET NULL
101
+ NOT VALID;
102
+ END IF;
103
+ END;
104
+ $$;
105
+
106
+ DO $$
107
+ BEGIN
108
+ IF NOT EXISTS (
109
+ SELECT 1
110
+ FROM pg_constraint
111
+ WHERE conrelid = '${schema}.evidence_refs'::regclass
112
+ AND conname = 'evidence_refs_created_by_finalization_fk'
113
+ ) THEN
114
+ ALTER TABLE ${schema}.evidence_refs
115
+ ADD CONSTRAINT evidence_refs_created_by_finalization_fk
116
+ FOREIGN KEY (created_by_finalization_id)
117
+ REFERENCES ${schema}.session_finalizations (id)
118
+ ON DELETE SET NULL
119
+ NOT VALID;
120
+ END IF;
121
+ END;
122
+ $$;
123
+
124
+ -- =========================================================================
125
+ -- Candidate ledger: every extracted item has an action and reason
126
+ -- =========================================================================
127
+ CREATE TABLE IF NOT EXISTS ${schema}.finalization_candidates (
128
+ id BIGSERIAL PRIMARY KEY,
129
+ tenant_id TEXT NOT NULL DEFAULT 'default',
130
+ finalization_id BIGINT NOT NULL REFERENCES ${schema}.session_finalizations(id) ON DELETE CASCADE,
131
+ session_id TEXT,
132
+ candidate_index INTEGER NOT NULL,
133
+ action TEXT NOT NULL
134
+ CHECK (action IN (
135
+ 'promote','quarantine','skip','skipped','supersede','error'
136
+ )),
137
+ reason TEXT,
138
+ memory_type TEXT,
139
+ canonical_key TEXT,
140
+ summary TEXT,
141
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
142
+ provenance JSONB NOT NULL DEFAULT '{}'::jsonb,
143
+ memory_record_id BIGINT,
144
+ fact_assertion_id BIGINT,
145
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
146
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
147
+ );
148
+
149
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_finalization_candidates_position
150
+ ON ${schema}.finalization_candidates (tenant_id, finalization_id, candidate_index);
151
+
152
+ CREATE INDEX IF NOT EXISTS idx_finalization_candidates_action
153
+ ON ${schema}.finalization_candidates (tenant_id, action, created_at DESC);
154
+
155
+ CREATE INDEX IF NOT EXISTS idx_finalization_candidates_memory
156
+ ON ${schema}.finalization_candidates (tenant_id, memory_record_id)
157
+ WHERE memory_record_id IS NOT NULL;
158
+
159
+ COMMENT ON TABLE ${schema}.finalization_candidates IS
160
+ 'Per-finalization candidate ledger. Promoted, skipped, quarantined, and incorrect candidates remain auditable without becoming runtime memory.';
@@ -0,0 +1,46 @@
1
+ -- Aquifer v1 compaction claim/apply guard
2
+ -- Requires: 007-v1-foundation.sql and 009-v1-assertion-plane.sql
3
+ -- Usage: replace ${schema} with actual schema name
4
+ --
5
+ -- Adds the minimum DB contract needed before compaction_runs can coordinate
6
+ -- daily/weekly/monthly apply workers. This remains a ledger/claim guard; it
7
+ -- does not create or promote aggregate memory.
8
+
9
+ ALTER TABLE ${schema}.compaction_runs
10
+ ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
11
+ ADD COLUMN IF NOT EXISTS worker_id TEXT,
12
+ ADD COLUMN IF NOT EXISTS apply_token TEXT;
13
+
14
+ ALTER TABLE ${schema}.compaction_runs
15
+ DROP CONSTRAINT IF EXISTS compaction_runs_status_check;
16
+
17
+ ALTER TABLE ${schema}.compaction_runs
18
+ ADD CONSTRAINT compaction_runs_status_check
19
+ CHECK (status IN ('planned','applying','applied','failed','skipped'));
20
+
21
+ ALTER TABLE ${schema}.compaction_runs
22
+ DROP CONSTRAINT IF EXISTS compaction_runs_applying_claim_check;
23
+
24
+ ALTER TABLE ${schema}.compaction_runs
25
+ ADD CONSTRAINT compaction_runs_applying_claim_check
26
+ CHECK (
27
+ status <> 'applying'
28
+ OR (
29
+ claimed_at IS NOT NULL
30
+ AND worker_id IS NOT NULL
31
+ AND btrim(worker_id) <> ''
32
+ AND apply_token IS NOT NULL
33
+ AND btrim(apply_token) <> ''
34
+ )
35
+ );
36
+
37
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_compaction_runs_one_live_apply
38
+ ON ${schema}.compaction_runs (tenant_id, cadence, period_start, period_end, policy_version)
39
+ WHERE status = 'applying';
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_compaction_runs_claims
42
+ ON ${schema}.compaction_runs (tenant_id, cadence, status, claimed_at DESC)
43
+ WHERE status = 'applying';
44
+
45
+ COMMENT ON COLUMN ${schema}.compaction_runs.apply_token IS
46
+ 'Opaque per-claim token. Apply/finalize code must present the claimed token before mutating memory.';
@@ -0,0 +1,39 @@
1
+ -- Aquifer v1 compaction claim lease guard
2
+ -- Requires: 011-v1-compaction-claim.sql
3
+ -- Usage: replace ${schema} with actual schema name
4
+ --
5
+ -- Adds row-level lease expiry for compaction claims. Lease expiry is a DB
6
+ -- timestamp fact, not a caller-clock interpretation of claimed_at.
7
+
8
+ ALTER TABLE ${schema}.compaction_runs
9
+ ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ,
10
+ ADD COLUMN IF NOT EXISTS reclaimed_at TIMESTAMPTZ,
11
+ ADD COLUMN IF NOT EXISTS reclaimed_by_worker_id TEXT;
12
+
13
+ UPDATE ${schema}.compaction_runs
14
+ SET lease_expires_at = claimed_at + interval '600 seconds'
15
+ WHERE status = 'applying'
16
+ AND claimed_at IS NOT NULL
17
+ AND lease_expires_at IS NULL;
18
+
19
+ ALTER TABLE ${schema}.compaction_runs
20
+ DROP CONSTRAINT IF EXISTS compaction_runs_applying_lease_check;
21
+
22
+ ALTER TABLE ${schema}.compaction_runs
23
+ ADD CONSTRAINT compaction_runs_applying_lease_check
24
+ CHECK (
25
+ status <> 'applying'
26
+ OR (
27
+ lease_expires_at IS NOT NULL
28
+ AND lease_expires_at > claimed_at
29
+ )
30
+ );
31
+
32
+ CREATE INDEX IF NOT EXISTS idx_compaction_runs_claim_lease
33
+ ON ${schema}.compaction_runs (
34
+ tenant_id, cadence, period_start, period_end, policy_version, lease_expires_at
35
+ )
36
+ WHERE status = 'applying';
37
+
38
+ COMMENT ON COLUMN ${schema}.compaction_runs.lease_expires_at IS
39
+ 'DB-time expiry for the current apply claim. Competing workers may reclaim only after this timestamp.';
@@ -0,0 +1,193 @@
1
+ -- Aquifer v1 compaction lineage and candidate ledger
2
+ -- Requires: 007-v1-foundation.sql, 009-v1-assertion-plane.sql, and 012-v1-compaction-lease.sql
3
+ -- Usage: replace ${schema} with actual schema name
4
+ --
5
+ -- Aggregate compaction candidates must remain auditable before promotion and
6
+ -- every promoted row must point back to the compaction run that created it.
7
+
8
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_compaction_runs_tenant_row
9
+ ON ${schema}.compaction_runs (tenant_id, id);
10
+
11
+ ALTER TABLE ${schema}.memory_records
12
+ ADD COLUMN IF NOT EXISTS created_by_compaction_run_id BIGINT;
13
+
14
+ ALTER TABLE ${schema}.fact_assertions_v1
15
+ ADD COLUMN IF NOT EXISTS created_by_compaction_run_id BIGINT;
16
+
17
+ ALTER TABLE ${schema}.evidence_refs
18
+ ADD COLUMN IF NOT EXISTS created_by_compaction_run_id BIGINT;
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_memory_records_created_by_compaction_run
21
+ ON ${schema}.memory_records (tenant_id, created_by_compaction_run_id)
22
+ WHERE created_by_compaction_run_id IS NOT NULL;
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_fact_assertions_created_by_compaction_run
25
+ ON ${schema}.fact_assertions_v1 (tenant_id, created_by_compaction_run_id)
26
+ WHERE created_by_compaction_run_id IS NOT NULL;
27
+
28
+ CREATE INDEX IF NOT EXISTS idx_evidence_refs_created_by_compaction_run
29
+ ON ${schema}.evidence_refs (tenant_id, created_by_compaction_run_id)
30
+ WHERE created_by_compaction_run_id IS NOT NULL;
31
+
32
+ DO $$
33
+ BEGIN
34
+ IF NOT EXISTS (
35
+ SELECT 1
36
+ FROM pg_constraint
37
+ WHERE conrelid = '${schema}.memory_records'::regclass
38
+ AND conname = 'memory_records_created_by_compaction_run_fk'
39
+ ) THEN
40
+ ALTER TABLE ${schema}.memory_records
41
+ ADD CONSTRAINT memory_records_created_by_compaction_run_fk
42
+ FOREIGN KEY (tenant_id, created_by_compaction_run_id)
43
+ REFERENCES ${schema}.compaction_runs (tenant_id, id)
44
+ ON DELETE SET NULL
45
+ NOT VALID;
46
+ END IF;
47
+ END;
48
+ $$;
49
+
50
+ DO $$
51
+ BEGIN
52
+ IF NOT EXISTS (
53
+ SELECT 1
54
+ FROM pg_constraint
55
+ WHERE conrelid = '${schema}.fact_assertions_v1'::regclass
56
+ AND conname = 'fact_assertions_created_by_compaction_run_fk'
57
+ ) THEN
58
+ ALTER TABLE ${schema}.fact_assertions_v1
59
+ ADD CONSTRAINT fact_assertions_created_by_compaction_run_fk
60
+ FOREIGN KEY (tenant_id, created_by_compaction_run_id)
61
+ REFERENCES ${schema}.compaction_runs (tenant_id, id)
62
+ ON DELETE SET NULL
63
+ NOT VALID;
64
+ END IF;
65
+ END;
66
+ $$;
67
+
68
+ DO $$
69
+ BEGIN
70
+ IF NOT EXISTS (
71
+ SELECT 1
72
+ FROM pg_constraint
73
+ WHERE conrelid = '${schema}.evidence_refs'::regclass
74
+ AND conname = 'evidence_refs_created_by_compaction_run_fk'
75
+ ) THEN
76
+ ALTER TABLE ${schema}.evidence_refs
77
+ ADD CONSTRAINT evidence_refs_created_by_compaction_run_fk
78
+ FOREIGN KEY (tenant_id, created_by_compaction_run_id)
79
+ REFERENCES ${schema}.compaction_runs (tenant_id, id)
80
+ ON DELETE SET NULL
81
+ NOT VALID;
82
+ END IF;
83
+ END;
84
+ $$;
85
+
86
+ CREATE TABLE IF NOT EXISTS ${schema}.compaction_candidates (
87
+ id BIGSERIAL PRIMARY KEY,
88
+ tenant_id TEXT NOT NULL DEFAULT 'default',
89
+ compaction_run_id BIGINT NOT NULL,
90
+ candidate_index INTEGER NOT NULL,
91
+ candidate_hash TEXT NOT NULL CHECK (btrim(candidate_hash) <> ''),
92
+ action TEXT NOT NULL DEFAULT 'planned'
93
+ CHECK (action IN (
94
+ 'planned','promote','quarantine','skip','skipped','error'
95
+ )),
96
+ reason TEXT,
97
+ memory_type TEXT,
98
+ canonical_key TEXT,
99
+ scope_kind TEXT,
100
+ scope_key TEXT,
101
+ context_key TEXT,
102
+ topic_key TEXT,
103
+ summary TEXT,
104
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
105
+ source_memory_ids BIGINT[] NOT NULL DEFAULT ARRAY[]::BIGINT[],
106
+ source_canonical_keys JSONB NOT NULL DEFAULT '[]'::jsonb,
107
+ memory_record_id BIGINT,
108
+ fact_assertion_id BIGINT,
109
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
110
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
111
+ CHECK (jsonb_typeof(source_canonical_keys) = 'array'),
112
+ CHECK (jsonb_array_length(source_canonical_keys) = cardinality(source_memory_ids))
113
+ );
114
+
115
+ DO $$
116
+ BEGIN
117
+ IF NOT EXISTS (
118
+ SELECT 1
119
+ FROM pg_constraint
120
+ WHERE conrelid = '${schema}.compaction_candidates'::regclass
121
+ AND conname = 'compaction_candidates_run_fk'
122
+ ) THEN
123
+ ALTER TABLE ${schema}.compaction_candidates
124
+ ADD CONSTRAINT compaction_candidates_run_fk
125
+ FOREIGN KEY (tenant_id, compaction_run_id)
126
+ REFERENCES ${schema}.compaction_runs (tenant_id, id)
127
+ ON DELETE CASCADE
128
+ NOT VALID;
129
+ END IF;
130
+ END;
131
+ $$;
132
+
133
+ DO $$
134
+ BEGIN
135
+ IF NOT EXISTS (
136
+ SELECT 1
137
+ FROM pg_constraint
138
+ WHERE conrelid = '${schema}.compaction_candidates'::regclass
139
+ AND conname = 'compaction_candidates_memory_fk'
140
+ ) THEN
141
+ ALTER TABLE ${schema}.compaction_candidates
142
+ ADD CONSTRAINT compaction_candidates_memory_fk
143
+ FOREIGN KEY (tenant_id, memory_record_id)
144
+ REFERENCES ${schema}.memory_records (tenant_id, id)
145
+ ON DELETE SET NULL
146
+ NOT VALID;
147
+ END IF;
148
+ END;
149
+ $$;
150
+
151
+ DO $$
152
+ BEGIN
153
+ IF NOT EXISTS (
154
+ SELECT 1
155
+ FROM pg_constraint
156
+ WHERE conrelid = '${schema}.compaction_candidates'::regclass
157
+ AND conname = 'compaction_candidates_fact_fk'
158
+ ) THEN
159
+ ALTER TABLE ${schema}.compaction_candidates
160
+ ADD CONSTRAINT compaction_candidates_fact_fk
161
+ FOREIGN KEY (tenant_id, fact_assertion_id)
162
+ REFERENCES ${schema}.fact_assertions_v1 (tenant_id, id)
163
+ ON DELETE SET NULL
164
+ NOT VALID;
165
+ END IF;
166
+ END;
167
+ $$;
168
+
169
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_compaction_candidates_position
170
+ ON ${schema}.compaction_candidates (tenant_id, compaction_run_id, candidate_index);
171
+
172
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_compaction_candidates_hash
173
+ ON ${schema}.compaction_candidates (tenant_id, compaction_run_id, candidate_hash);
174
+
175
+ CREATE INDEX IF NOT EXISTS idx_compaction_candidates_action
176
+ ON ${schema}.compaction_candidates (tenant_id, action, created_at DESC);
177
+
178
+ CREATE INDEX IF NOT EXISTS idx_compaction_candidates_memory
179
+ ON ${schema}.compaction_candidates (tenant_id, memory_record_id)
180
+ WHERE memory_record_id IS NOT NULL;
181
+
182
+ CREATE INDEX IF NOT EXISTS idx_compaction_candidates_fact
183
+ ON ${schema}.compaction_candidates (tenant_id, fact_assertion_id)
184
+ WHERE fact_assertion_id IS NOT NULL;
185
+
186
+ CREATE INDEX IF NOT EXISTS idx_compaction_candidates_sources
187
+ ON ${schema}.compaction_candidates USING GIN (source_memory_ids);
188
+
189
+ COMMENT ON TABLE ${schema}.compaction_candidates IS
190
+ 'Per-compaction-run aggregate candidate ledger. Planned, promoted, skipped, quarantined, and errored candidates remain auditable without bypassing promotion.';
191
+
192
+ COMMENT ON COLUMN ${schema}.memory_records.created_by_compaction_run_id IS
193
+ 'Compaction run that formally promoted this memory, if any. Planner-only candidates do not set this column.';