@shadowforge0/aquifer-memory 1.0.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -29
- package/consumers/claude-code.js +117 -0
- package/consumers/cli.js +28 -1
- package/consumers/default/daily-entries.js +196 -0
- package/consumers/default/index.js +282 -0
- package/consumers/default/prompts/summary.js +153 -0
- package/consumers/mcp.js +3 -23
- package/consumers/miranda/context-inject.js +119 -0
- package/consumers/miranda/daily-entries.js +224 -0
- package/consumers/miranda/index.js +353 -0
- package/consumers/miranda/instance.js +55 -0
- package/consumers/miranda/llm.js +99 -0
- package/consumers/miranda/profile.json +145 -0
- package/consumers/miranda/prompts/summary.js +303 -0
- package/consumers/miranda/recall-format.js +74 -0
- package/consumers/miranda/render-daily-md.js +186 -0
- package/consumers/miranda/workspace-files.js +91 -0
- package/consumers/openclaw-ext/index.js +38 -0
- package/consumers/openclaw-ext/openclaw.plugin.json +9 -0
- package/consumers/openclaw-ext/package.json +10 -0
- package/consumers/openclaw-plugin.js +66 -74
- package/consumers/opencode.js +21 -24
- package/consumers/shared/autodetect.js +64 -0
- package/consumers/shared/entity-parser.js +119 -0
- package/consumers/shared/ingest.js +148 -0
- package/consumers/shared/llm-autodetect.js +137 -0
- package/consumers/shared/normalize.js +129 -0
- package/consumers/shared/recall-format.js +110 -0
- package/core/aquifer.js +209 -71
- package/core/artifacts.js +174 -0
- package/core/bundles.js +400 -0
- package/core/consolidation.js +340 -0
- package/core/decisions.js +164 -0
- package/core/entity.js +1 -3
- package/core/errors.js +97 -0
- package/core/handoff.js +153 -0
- package/core/mcp-manifest.js +131 -0
- package/core/narratives.js +212 -0
- package/core/profiles.js +171 -0
- package/core/state.js +163 -0
- package/core/storage.js +86 -28
- package/core/timeline.js +152 -0
- package/docs/postprocess-contract.md +132 -0
- package/index.js +23 -1
- package/package.json +23 -2
- package/pipeline/_http.js +1 -1
- package/pipeline/consolidation/apply.js +176 -0
- package/pipeline/consolidation/index.js +21 -0
- package/pipeline/extract-entities.js +2 -2
- package/pipeline/rerank.js +1 -1
- package/pipeline/summarize.js +4 -1
- package/schema/001-base.sql +61 -24
- package/schema/002-entities.sql +17 -3
- package/schema/004-completion.sql +375 -0
- package/schema/004-facts.sql +67 -0
- package/scripts/diagnose-fts-zh.js +168 -134
- package/scripts/diagnose-vector.js +188 -0
- package/scripts/install-openclaw.sh +59 -0
- package/scripts/smoke.mjs +2 -2
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
-- 004-completion.sql — cross-session completion schema (P1 foundation)
|
|
2
|
+
--
|
|
3
|
+
-- Adds the minimal DDL needed for the aquifer-completion capability surface:
|
|
4
|
+
-- * shared set_updated_at() trigger function (reused by narratives, consumer_profiles,
|
|
5
|
+
-- and future completion tables)
|
|
6
|
+
-- * sessions.consolidation_phases JSONB (per-phase state map; see consolidation
|
|
7
|
+
-- orchestration spec)
|
|
8
|
+
-- * narratives table — cross-session state snapshot with supersede chain
|
|
9
|
+
-- * consumer_profiles table — consumer schema registry with composite primary key
|
|
10
|
+
-- (tenant_id, consumer_id, version) for future multi-tenant safety
|
|
11
|
+
--
|
|
12
|
+
-- All identifiers stay parameterised on ${schema} so P4 schema rename
|
|
13
|
+
-- (miranda → aquifer) is a one-line config change rather than a DDL rewrite.
|
|
14
|
+
|
|
15
|
+
-- Ensure pg_trgm available (used by existing migrations; re-declared for independent
|
|
16
|
+
-- run safety).
|
|
17
|
+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
18
|
+
|
|
19
|
+
-- Shared trigger: bump updated_at on row modification.
|
|
20
|
+
CREATE OR REPLACE FUNCTION ${schema}.set_updated_at()
|
|
21
|
+
RETURNS trigger
|
|
22
|
+
LANGUAGE plpgsql
|
|
23
|
+
AS $$
|
|
24
|
+
BEGIN
|
|
25
|
+
NEW.updated_at := now();
|
|
26
|
+
RETURN NEW;
|
|
27
|
+
END;
|
|
28
|
+
$$;
|
|
29
|
+
|
|
30
|
+
-- sessions.consolidation_phases: per-phase state map keyed by phase name.
|
|
31
|
+
-- Shape (documented in spec, enforced at application layer):
|
|
32
|
+
-- {
|
|
33
|
+
-- "<phase>": {
|
|
34
|
+
-- "status": "pending|claimed|running|succeeded|failed|skipped",
|
|
35
|
+
-- "attempts": int,
|
|
36
|
+
-- "idempotencyKey": string?, "claimToken": string?, "workerId": string?,
|
|
37
|
+
-- "startedAt": iso?, "finishedAt": iso?, "retryAfter": iso?,
|
|
38
|
+
-- "errorCode": string?, "errorMessage": string?,
|
|
39
|
+
-- "outputRef": { ... }?
|
|
40
|
+
-- }
|
|
41
|
+
-- }
|
|
42
|
+
ALTER TABLE ${schema}.sessions
|
|
43
|
+
ADD COLUMN IF NOT EXISTS consolidation_phases JSONB NOT NULL DEFAULT '{}'::jsonb;
|
|
44
|
+
|
|
45
|
+
-- narratives: cross-session state snapshots with scope-based addressing and
|
|
46
|
+
-- supersede chain. Only one 'active' row per (tenant, agent, scope, scope_key).
|
|
47
|
+
CREATE TABLE IF NOT EXISTS ${schema}.narratives (
|
|
48
|
+
id BIGSERIAL PRIMARY KEY,
|
|
49
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
50
|
+
session_row_id BIGINT REFERENCES ${schema}.sessions(id) ON DELETE SET NULL,
|
|
51
|
+
source_session_id TEXT,
|
|
52
|
+
agent_id TEXT NOT NULL DEFAULT 'main',
|
|
53
|
+
consumer_profile_id TEXT NOT NULL,
|
|
54
|
+
consumer_profile_version INT NOT NULL,
|
|
55
|
+
consumer_schema_hash TEXT NOT NULL,
|
|
56
|
+
idempotency_key TEXT UNIQUE,
|
|
57
|
+
scope TEXT NOT NULL DEFAULT 'agent'
|
|
58
|
+
CHECK (scope IN ('agent', 'workspace', 'project', 'custom')),
|
|
59
|
+
scope_key TEXT NOT NULL,
|
|
60
|
+
text TEXT NOT NULL,
|
|
61
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
62
|
+
CHECK (status IN ('active', 'archived', 'superseded')),
|
|
63
|
+
based_on_fact_ids BIGINT[] NOT NULL DEFAULT '{}',
|
|
64
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
65
|
+
superseded_by_narrative_id BIGINT REFERENCES ${schema}.narratives(id) ON DELETE SET NULL,
|
|
66
|
+
effective_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
67
|
+
search_tsv TSVECTOR,
|
|
68
|
+
search_text TEXT,
|
|
69
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
70
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
-- Only one active narrative per (tenant, agent, scope, scope_key).
|
|
74
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_narratives_active_scope
|
|
75
|
+
ON ${schema}.narratives (tenant_id, agent_id, scope, scope_key)
|
|
76
|
+
WHERE status = 'active';
|
|
77
|
+
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_narratives_effective_at
|
|
79
|
+
ON ${schema}.narratives (tenant_id, agent_id, effective_at DESC);
|
|
80
|
+
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_narratives_search_tsv
|
|
82
|
+
ON ${schema}.narratives USING GIN (search_tsv);
|
|
83
|
+
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_narratives_search_text_trgm
|
|
85
|
+
ON ${schema}.narratives USING GIN (search_text gin_trgm_ops);
|
|
86
|
+
|
|
87
|
+
CREATE OR REPLACE FUNCTION ${schema}.narratives_search_tsv_update()
|
|
88
|
+
RETURNS trigger
|
|
89
|
+
LANGUAGE plpgsql
|
|
90
|
+
AS $$
|
|
91
|
+
BEGIN
|
|
92
|
+
NEW.search_text := COALESCE(NEW.text, '') || ' ' || COALESCE(NEW.metadata::text, '');
|
|
93
|
+
NEW.search_tsv := setweight(to_tsvector('simple', COALESCE(NEW.text, '')), 'A');
|
|
94
|
+
RETURN NEW;
|
|
95
|
+
END;
|
|
96
|
+
$$;
|
|
97
|
+
|
|
98
|
+
DROP TRIGGER IF EXISTS trg_narratives_search_tsv ON ${schema}.narratives;
|
|
99
|
+
CREATE TRIGGER trg_narratives_search_tsv
|
|
100
|
+
BEFORE INSERT OR UPDATE OF text, metadata
|
|
101
|
+
ON ${schema}.narratives
|
|
102
|
+
FOR EACH ROW
|
|
103
|
+
EXECUTE FUNCTION ${schema}.narratives_search_tsv_update();
|
|
104
|
+
|
|
105
|
+
DROP TRIGGER IF EXISTS trg_narratives_updated_at ON ${schema}.narratives;
|
|
106
|
+
CREATE TRIGGER trg_narratives_updated_at
|
|
107
|
+
BEFORE UPDATE ON ${schema}.narratives
|
|
108
|
+
FOR EACH ROW
|
|
109
|
+
EXECUTE FUNCTION ${schema}.set_updated_at();
|
|
110
|
+
|
|
111
|
+
-- consumer_profiles: registry for consumer output contracts.
|
|
112
|
+
-- Composite primary key (tenant_id, consumer_id, version) future-proofs multi-tenant.
|
|
113
|
+
-- profile_hash UNIQUE per (consumer_id, version) catches accidental hash drift within
|
|
114
|
+
-- a consumer version.
|
|
115
|
+
CREATE TABLE IF NOT EXISTS ${schema}.consumer_profiles (
|
|
116
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
117
|
+
consumer_id TEXT NOT NULL,
|
|
118
|
+
version INT NOT NULL,
|
|
119
|
+
profile_hash TEXT NOT NULL,
|
|
120
|
+
profile_json JSONB NOT NULL,
|
|
121
|
+
loaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
122
|
+
deprecated_at TIMESTAMPTZ,
|
|
123
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
124
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
125
|
+
PRIMARY KEY (tenant_id, consumer_id, version),
|
|
126
|
+
UNIQUE (consumer_id, version, profile_hash)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_consumer_profiles_active
|
|
130
|
+
ON ${schema}.consumer_profiles (tenant_id, consumer_id, version DESC)
|
|
131
|
+
WHERE deprecated_at IS NULL;
|
|
132
|
+
|
|
133
|
+
DROP TRIGGER IF EXISTS trg_consumer_profiles_updated_at ON ${schema}.consumer_profiles;
|
|
134
|
+
CREATE TRIGGER trg_consumer_profiles_updated_at
|
|
135
|
+
BEFORE UPDATE ON ${schema}.consumer_profiles
|
|
136
|
+
FOR EACH ROW
|
|
137
|
+
EXECUTE FUNCTION ${schema}.set_updated_at();
|
|
138
|
+
|
|
139
|
+
-- timeline_events: append-only event log keyed by (tenant, agent, occurred_at).
|
|
140
|
+
-- category vocabulary is consumer-owned (focus/todo/mood/handoff/narrative/cli
|
|
141
|
+
-- for Miranda default), event shape is strict core. idempotency_key UNIQUE
|
|
142
|
+
-- across the table to make caller-driven dedupe safe.
|
|
143
|
+
CREATE TABLE IF NOT EXISTS ${schema}.timeline_events (
|
|
144
|
+
id BIGSERIAL PRIMARY KEY,
|
|
145
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
146
|
+
session_row_id BIGINT REFERENCES ${schema}.sessions(id) ON DELETE SET NULL,
|
|
147
|
+
source_session_id TEXT,
|
|
148
|
+
agent_id TEXT NOT NULL DEFAULT 'main',
|
|
149
|
+
consumer_profile_id TEXT NOT NULL,
|
|
150
|
+
consumer_profile_version INT NOT NULL,
|
|
151
|
+
consumer_schema_hash TEXT NOT NULL,
|
|
152
|
+
idempotency_key TEXT UNIQUE,
|
|
153
|
+
occurred_at TIMESTAMPTZ NOT NULL,
|
|
154
|
+
source TEXT NOT NULL,
|
|
155
|
+
session_ref TEXT,
|
|
156
|
+
category TEXT NOT NULL,
|
|
157
|
+
text TEXT NOT NULL,
|
|
158
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
159
|
+
search_tsv TSVECTOR,
|
|
160
|
+
search_text TEXT,
|
|
161
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
162
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_timeline_events_occurred_at
|
|
166
|
+
ON ${schema}.timeline_events (tenant_id, agent_id, occurred_at DESC);
|
|
167
|
+
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_timeline_events_category
|
|
169
|
+
ON ${schema}.timeline_events (tenant_id, agent_id, category, occurred_at DESC);
|
|
170
|
+
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_timeline_events_search_tsv
|
|
172
|
+
ON ${schema}.timeline_events USING GIN (search_tsv);
|
|
173
|
+
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_timeline_events_search_text_trgm
|
|
175
|
+
ON ${schema}.timeline_events USING GIN (search_text gin_trgm_ops);
|
|
176
|
+
|
|
177
|
+
CREATE OR REPLACE FUNCTION ${schema}.timeline_events_search_tsv_update()
|
|
178
|
+
RETURNS trigger
|
|
179
|
+
LANGUAGE plpgsql
|
|
180
|
+
AS $$
|
|
181
|
+
BEGIN
|
|
182
|
+
NEW.search_text :=
|
|
183
|
+
COALESCE(NEW.category, '') || ' ' ||
|
|
184
|
+
COALESCE(NEW.text, '') || ' ' ||
|
|
185
|
+
COALESCE(NEW.metadata::text, '');
|
|
186
|
+
|
|
187
|
+
NEW.search_tsv :=
|
|
188
|
+
setweight(to_tsvector('simple', COALESCE(NEW.category, '')), 'B') ||
|
|
189
|
+
setweight(to_tsvector('simple', COALESCE(NEW.text, '')), 'A');
|
|
190
|
+
|
|
191
|
+
RETURN NEW;
|
|
192
|
+
END;
|
|
193
|
+
$$;
|
|
194
|
+
|
|
195
|
+
DROP TRIGGER IF EXISTS trg_timeline_events_search_tsv ON ${schema}.timeline_events;
|
|
196
|
+
CREATE TRIGGER trg_timeline_events_search_tsv
|
|
197
|
+
BEFORE INSERT OR UPDATE OF category, text, metadata
|
|
198
|
+
ON ${schema}.timeline_events
|
|
199
|
+
FOR EACH ROW
|
|
200
|
+
EXECUTE FUNCTION ${schema}.timeline_events_search_tsv_update();
|
|
201
|
+
|
|
202
|
+
DROP TRIGGER IF EXISTS trg_timeline_events_updated_at ON ${schema}.timeline_events;
|
|
203
|
+
CREATE TRIGGER trg_timeline_events_updated_at
|
|
204
|
+
BEFORE UPDATE ON ${schema}.timeline_events
|
|
205
|
+
FOR EACH ROW
|
|
206
|
+
EXECUTE FUNCTION ${schema}.set_updated_at();
|
|
207
|
+
|
|
208
|
+
-- session_states: latest-snapshot-per-scope with supersede chain.
|
|
209
|
+
-- is_latest + partial unique index enforces at-most-one latest per
|
|
210
|
+
-- (tenant, agent, scope_key); writer supersedes prior latest atomically.
|
|
211
|
+
CREATE TABLE IF NOT EXISTS ${schema}.session_states (
|
|
212
|
+
id BIGSERIAL PRIMARY KEY,
|
|
213
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
214
|
+
session_row_id BIGINT REFERENCES ${schema}.sessions(id) ON DELETE SET NULL,
|
|
215
|
+
source_session_id TEXT,
|
|
216
|
+
agent_id TEXT NOT NULL DEFAULT 'main',
|
|
217
|
+
scope_key TEXT NOT NULL,
|
|
218
|
+
consumer_profile_id TEXT NOT NULL,
|
|
219
|
+
consumer_profile_version INT NOT NULL,
|
|
220
|
+
consumer_schema_hash TEXT NOT NULL,
|
|
221
|
+
idempotency_key TEXT UNIQUE,
|
|
222
|
+
goal TEXT,
|
|
223
|
+
active_work JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
224
|
+
blockers JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
225
|
+
affect JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
226
|
+
payload JSONB NOT NULL,
|
|
227
|
+
is_latest BOOLEAN NOT NULL DEFAULT true,
|
|
228
|
+
supersedes_state_id BIGINT REFERENCES ${schema}.session_states(id) ON DELETE SET NULL,
|
|
229
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_session_states_latest
|
|
233
|
+
ON ${schema}.session_states (tenant_id, agent_id, scope_key)
|
|
234
|
+
WHERE is_latest = true;
|
|
235
|
+
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_session_states_agent
|
|
237
|
+
ON ${schema}.session_states (tenant_id, agent_id, created_at DESC);
|
|
238
|
+
|
|
239
|
+
-- session_handoffs: append-only handoff log. getLatest by (agent) or (agent, session).
|
|
240
|
+
-- No latest-enforcement — every write is a row; retrieval sorts by created_at DESC.
|
|
241
|
+
CREATE TABLE IF NOT EXISTS ${schema}.session_handoffs (
|
|
242
|
+
id BIGSERIAL PRIMARY KEY,
|
|
243
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
244
|
+
session_row_id BIGINT REFERENCES ${schema}.sessions(id) ON DELETE SET NULL,
|
|
245
|
+
source_session_id TEXT NOT NULL,
|
|
246
|
+
agent_id TEXT NOT NULL DEFAULT 'main',
|
|
247
|
+
consumer_profile_id TEXT NOT NULL,
|
|
248
|
+
consumer_profile_version INT NOT NULL,
|
|
249
|
+
consumer_schema_hash TEXT NOT NULL,
|
|
250
|
+
idempotency_key TEXT UNIQUE,
|
|
251
|
+
status TEXT NOT NULL,
|
|
252
|
+
last_step TEXT,
|
|
253
|
+
next_step TEXT,
|
|
254
|
+
blockers JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
255
|
+
decided JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
256
|
+
open_loops JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
257
|
+
payload JSONB NOT NULL,
|
|
258
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_session_handoffs_agent
|
|
262
|
+
ON ${schema}.session_handoffs (tenant_id, agent_id, created_at DESC);
|
|
263
|
+
|
|
264
|
+
CREATE INDEX IF NOT EXISTS idx_session_handoffs_session
|
|
265
|
+
ON ${schema}.session_handoffs (tenant_id, source_session_id, created_at DESC);
|
|
266
|
+
|
|
267
|
+
-- decisions: append-only decision log. status vocabulary
|
|
268
|
+
-- (proposed/committed/reversed) lives in a CHECK constraint so bad writes
|
|
269
|
+
-- fail at DB boundary. reversed_by_decision_id forms a supersede chain.
|
|
270
|
+
CREATE TABLE IF NOT EXISTS ${schema}.decisions (
|
|
271
|
+
id BIGSERIAL PRIMARY KEY,
|
|
272
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
273
|
+
session_row_id BIGINT REFERENCES ${schema}.sessions(id) ON DELETE SET NULL,
|
|
274
|
+
source_session_id TEXT,
|
|
275
|
+
agent_id TEXT NOT NULL DEFAULT 'main',
|
|
276
|
+
consumer_profile_id TEXT NOT NULL,
|
|
277
|
+
consumer_profile_version INT NOT NULL,
|
|
278
|
+
consumer_schema_hash TEXT NOT NULL,
|
|
279
|
+
idempotency_key TEXT UNIQUE,
|
|
280
|
+
payload JSONB NOT NULL,
|
|
281
|
+
status TEXT NOT NULL
|
|
282
|
+
CHECK (status IN ('proposed', 'committed', 'reversed')),
|
|
283
|
+
decision_text TEXT NOT NULL,
|
|
284
|
+
reason_text TEXT,
|
|
285
|
+
decided_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
286
|
+
reversed_by_decision_id BIGINT REFERENCES ${schema}.decisions(id) ON DELETE SET NULL,
|
|
287
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
288
|
+
search_tsv TSVECTOR,
|
|
289
|
+
search_text TEXT,
|
|
290
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
291
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_status
|
|
295
|
+
ON ${schema}.decisions (tenant_id, agent_id, status, decided_at DESC);
|
|
296
|
+
|
|
297
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_session
|
|
298
|
+
ON ${schema}.decisions (tenant_id, source_session_id);
|
|
299
|
+
|
|
300
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_search_tsv
|
|
301
|
+
ON ${schema}.decisions USING GIN (search_tsv);
|
|
302
|
+
|
|
303
|
+
CREATE OR REPLACE FUNCTION ${schema}.decisions_search_tsv_update()
|
|
304
|
+
RETURNS trigger
|
|
305
|
+
LANGUAGE plpgsql
|
|
306
|
+
AS $$
|
|
307
|
+
BEGIN
|
|
308
|
+
NEW.search_text :=
|
|
309
|
+
COALESCE(NEW.decision_text, '') || ' ' ||
|
|
310
|
+
COALESCE(NEW.reason_text, '') || ' ' ||
|
|
311
|
+
COALESCE(NEW.metadata::text, '');
|
|
312
|
+
|
|
313
|
+
NEW.search_tsv :=
|
|
314
|
+
setweight(to_tsvector('simple', COALESCE(NEW.decision_text, '')), 'A') ||
|
|
315
|
+
setweight(to_tsvector('simple', COALESCE(NEW.reason_text, '')), 'B');
|
|
316
|
+
|
|
317
|
+
RETURN NEW;
|
|
318
|
+
END;
|
|
319
|
+
$$;
|
|
320
|
+
|
|
321
|
+
DROP TRIGGER IF EXISTS trg_decisions_search_tsv ON ${schema}.decisions;
|
|
322
|
+
CREATE TRIGGER trg_decisions_search_tsv
|
|
323
|
+
BEFORE INSERT OR UPDATE OF decision_text, reason_text, metadata
|
|
324
|
+
ON ${schema}.decisions
|
|
325
|
+
FOR EACH ROW
|
|
326
|
+
EXECUTE FUNCTION ${schema}.decisions_search_tsv_update();
|
|
327
|
+
|
|
328
|
+
DROP TRIGGER IF EXISTS trg_decisions_updated_at ON ${schema}.decisions;
|
|
329
|
+
CREATE TRIGGER trg_decisions_updated_at
|
|
330
|
+
BEFORE UPDATE ON ${schema}.decisions
|
|
331
|
+
FOR EACH ROW
|
|
332
|
+
EXECUTE FUNCTION ${schema}.set_updated_at();
|
|
333
|
+
|
|
334
|
+
-- artifacts: records producer-declared outputs (daily md, render, export).
|
|
335
|
+
-- Aquifer doesn't interpret payload — producers own shape. status lifecycle
|
|
336
|
+
-- pending → produced|failed|discarded.
|
|
337
|
+
CREATE TABLE IF NOT EXISTS ${schema}.artifacts (
|
|
338
|
+
id BIGSERIAL PRIMARY KEY,
|
|
339
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
340
|
+
session_row_id BIGINT REFERENCES ${schema}.sessions(id) ON DELETE SET NULL,
|
|
341
|
+
source_session_id TEXT,
|
|
342
|
+
agent_id TEXT NOT NULL DEFAULT 'main',
|
|
343
|
+
consumer_profile_id TEXT NOT NULL,
|
|
344
|
+
consumer_profile_version INT NOT NULL,
|
|
345
|
+
consumer_schema_hash TEXT NOT NULL,
|
|
346
|
+
idempotency_key TEXT UNIQUE,
|
|
347
|
+
producer_id TEXT NOT NULL,
|
|
348
|
+
artifact_type TEXT NOT NULL,
|
|
349
|
+
trigger_phase TEXT,
|
|
350
|
+
format TEXT NOT NULL,
|
|
351
|
+
destination TEXT NOT NULL,
|
|
352
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
353
|
+
CHECK (status IN ('pending', 'produced', 'failed', 'discarded')),
|
|
354
|
+
content_ref TEXT,
|
|
355
|
+
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
356
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
357
|
+
produced_at TIMESTAMPTZ,
|
|
358
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
359
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_lookup
|
|
363
|
+
ON ${schema}.artifacts (tenant_id, agent_id, producer_id, created_at DESC);
|
|
364
|
+
|
|
365
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_session
|
|
366
|
+
ON ${schema}.artifacts (tenant_id, source_session_id, created_at DESC);
|
|
367
|
+
|
|
368
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_status
|
|
369
|
+
ON ${schema}.artifacts (tenant_id, status, created_at DESC);
|
|
370
|
+
|
|
371
|
+
DROP TRIGGER IF EXISTS trg_artifacts_updated_at ON ${schema}.artifacts;
|
|
372
|
+
CREATE TRIGGER trg_artifacts_updated_at
|
|
373
|
+
BEFORE UPDATE ON ${schema}.artifacts
|
|
374
|
+
FOR EACH ROW
|
|
375
|
+
EXECUTE FUNCTION ${schema}.set_updated_at();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
-- Aquifer facts / consolidation extension
|
|
2
|
+
-- Requires: 001-base.sql applied first
|
|
3
|
+
-- Usage: replace ${schema} with actual schema name
|
|
4
|
+
--
|
|
5
|
+
-- Facts store long-lived subject/statement pairs with a lifecycle:
|
|
6
|
+
-- candidate → active → (stale | archived | superseded)
|
|
7
|
+
-- Consumers write candidates during enrich (via writeFactCandidates).
|
|
8
|
+
-- consolidate() then promotes / updates / confirms / archives them.
|
|
9
|
+
|
|
10
|
+
-- =========================================================================
|
|
11
|
+
-- Facts: long-lived current-state statements per (subject, agent)
|
|
12
|
+
-- =========================================================================
|
|
13
|
+
CREATE TABLE IF NOT EXISTS ${schema}.facts (
|
|
14
|
+
id BIGSERIAL PRIMARY KEY,
|
|
15
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
16
|
+
subject_key TEXT NOT NULL,
|
|
17
|
+
subject_label TEXT NOT NULL,
|
|
18
|
+
statement TEXT NOT NULL,
|
|
19
|
+
status TEXT NOT NULL DEFAULT 'candidate'
|
|
20
|
+
CHECK (status IN ('candidate','active','stale','archived','superseded')),
|
|
21
|
+
importance SMALLINT NOT NULL DEFAULT 5,
|
|
22
|
+
source_session_id TEXT,
|
|
23
|
+
agent_id TEXT NOT NULL DEFAULT 'main',
|
|
24
|
+
evidence JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
25
|
+
superseded_by BIGINT REFERENCES ${schema}.facts(id) ON DELETE SET NULL,
|
|
26
|
+
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
27
|
+
last_confirmed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- Migration: add tenant_id if upgrading from a legacy facts table (no tenant column).
|
|
31
|
+
ALTER TABLE ${schema}.facts ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
|
|
32
|
+
|
|
33
|
+
-- At most one active fact per (tenant, subject, agent)
|
|
34
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_active_subject
|
|
35
|
+
ON ${schema}.facts (tenant_id, subject_key, agent_id)
|
|
36
|
+
WHERE status = 'active';
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_facts_active_agent
|
|
39
|
+
ON ${schema}.facts (tenant_id, agent_id, importance DESC, last_confirmed_at DESC)
|
|
40
|
+
WHERE status = 'active';
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_facts_status_created
|
|
43
|
+
ON ${schema}.facts (tenant_id, status, first_seen_at DESC);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_facts_subject
|
|
46
|
+
ON ${schema}.facts (tenant_id, subject_key);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_facts_source_session
|
|
49
|
+
ON ${schema}.facts (source_session_id)
|
|
50
|
+
WHERE source_session_id IS NOT NULL;
|
|
51
|
+
|
|
52
|
+
COMMENT ON TABLE ${schema}.facts IS 'Fact candidates and active facts per (tenant, subject, agent) with consolidation lifecycle';
|
|
53
|
+
|
|
54
|
+
-- =========================================================================
|
|
55
|
+
-- Fact ↔ Entity join (optional, only when entities enabled)
|
|
56
|
+
-- =========================================================================
|
|
57
|
+
CREATE TABLE IF NOT EXISTS ${schema}.fact_entities (
|
|
58
|
+
id BIGSERIAL PRIMARY KEY,
|
|
59
|
+
fact_id BIGINT NOT NULL REFERENCES ${schema}.facts(id) ON DELETE CASCADE,
|
|
60
|
+
entity_id BIGINT NOT NULL,
|
|
61
|
+
UNIQUE (fact_id, entity_id)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_fact_entities_entity_id
|
|
65
|
+
ON ${schema}.fact_entities (entity_id);
|
|
66
|
+
|
|
67
|
+
COMMENT ON TABLE ${schema}.fact_entities IS 'Join table linking facts to entities (FK to entities is soft — entities table is optional)';
|