@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.
Files changed (59) hide show
  1. package/README.md +37 -29
  2. package/consumers/claude-code.js +117 -0
  3. package/consumers/cli.js +28 -1
  4. package/consumers/default/daily-entries.js +196 -0
  5. package/consumers/default/index.js +282 -0
  6. package/consumers/default/prompts/summary.js +153 -0
  7. package/consumers/mcp.js +3 -23
  8. package/consumers/miranda/context-inject.js +119 -0
  9. package/consumers/miranda/daily-entries.js +224 -0
  10. package/consumers/miranda/index.js +353 -0
  11. package/consumers/miranda/instance.js +55 -0
  12. package/consumers/miranda/llm.js +99 -0
  13. package/consumers/miranda/profile.json +145 -0
  14. package/consumers/miranda/prompts/summary.js +303 -0
  15. package/consumers/miranda/recall-format.js +74 -0
  16. package/consumers/miranda/render-daily-md.js +186 -0
  17. package/consumers/miranda/workspace-files.js +91 -0
  18. package/consumers/openclaw-ext/index.js +38 -0
  19. package/consumers/openclaw-ext/openclaw.plugin.json +9 -0
  20. package/consumers/openclaw-ext/package.json +10 -0
  21. package/consumers/openclaw-plugin.js +66 -74
  22. package/consumers/opencode.js +21 -24
  23. package/consumers/shared/autodetect.js +64 -0
  24. package/consumers/shared/entity-parser.js +119 -0
  25. package/consumers/shared/ingest.js +148 -0
  26. package/consumers/shared/llm-autodetect.js +137 -0
  27. package/consumers/shared/normalize.js +129 -0
  28. package/consumers/shared/recall-format.js +110 -0
  29. package/core/aquifer.js +209 -71
  30. package/core/artifacts.js +174 -0
  31. package/core/bundles.js +400 -0
  32. package/core/consolidation.js +340 -0
  33. package/core/decisions.js +164 -0
  34. package/core/entity.js +1 -3
  35. package/core/errors.js +97 -0
  36. package/core/handoff.js +153 -0
  37. package/core/mcp-manifest.js +131 -0
  38. package/core/narratives.js +212 -0
  39. package/core/profiles.js +171 -0
  40. package/core/state.js +163 -0
  41. package/core/storage.js +86 -28
  42. package/core/timeline.js +152 -0
  43. package/docs/postprocess-contract.md +132 -0
  44. package/index.js +23 -1
  45. package/package.json +23 -2
  46. package/pipeline/_http.js +1 -1
  47. package/pipeline/consolidation/apply.js +176 -0
  48. package/pipeline/consolidation/index.js +21 -0
  49. package/pipeline/extract-entities.js +2 -2
  50. package/pipeline/rerank.js +1 -1
  51. package/pipeline/summarize.js +4 -1
  52. package/schema/001-base.sql +61 -24
  53. package/schema/002-entities.sql +17 -3
  54. package/schema/004-completion.sql +375 -0
  55. package/schema/004-facts.sql +67 -0
  56. package/scripts/diagnose-fts-zh.js +168 -134
  57. package/scripts/diagnose-vector.js +188 -0
  58. package/scripts/install-openclaw.sh +59 -0
  59. package/scripts/smoke.mjs +2 -2
@@ -0,0 +1,340 @@
1
+ 'use strict';
2
+
3
+ // aq.consolidation.* — session-level multi-phase orchestration.
4
+ //
5
+ // Spec: aquifer-completion §3 consolidationOrchestration. State lives in
6
+ // sessions.consolidation_phases JSONB keyed by phase name. 10 phases cover
7
+ // the post-session pipeline: summary_extract, entity_extract, fact_extract,
8
+ // fact_consolidation, narrative_refresh, decision_write, handoff_write,
9
+ // session_state_write, timeline_write, artifact_dispatch.
10
+ //
11
+ // Status vocabulary: pending|claimed|running|succeeded|failed|skipped.
12
+ // State transitions (enforced by transitionPhase):
13
+ // pending → claimed
14
+ // claimed → running|failed|skipped|claimed (stale reclaim only)
15
+ // running → succeeded|failed|claimed (stale reclaim only)
16
+ // failed → claimed (retry)
17
+ // succeeded|skipped → non-terminal requires forceReplay=true
18
+ //
19
+ // Advisory lock (pg_advisory_xact_lock on session_row_id) wraps the
20
+ // read-modify-write in a transaction so two workers can't claim the same
21
+ // session phase simultaneously.
22
+
23
+ const crypto = require('crypto');
24
+ const { AqError, ok, err } = require('./errors');
25
+
26
+ const PHASES = Object.freeze([
27
+ 'summary_extract',
28
+ 'entity_extract',
29
+ 'fact_extract',
30
+ 'fact_consolidation',
31
+ 'narrative_refresh',
32
+ 'decision_write',
33
+ 'handoff_write',
34
+ 'session_state_write',
35
+ 'timeline_write',
36
+ 'artifact_dispatch',
37
+ ]);
38
+ const PHASE_SET = new Set(PHASES);
39
+
40
+ const STATUSES = Object.freeze([
41
+ 'pending', 'claimed', 'running', 'succeeded', 'failed', 'skipped',
42
+ ]);
43
+ const STATUS_SET = new Set(STATUSES);
44
+
45
+ const TERMINAL = new Set(['succeeded', 'skipped']);
46
+
47
+ // Valid transitions. Caller must specify fromStatus to guard against races.
48
+ const VALID_TRANSITIONS = {
49
+ pending: new Set(['claimed']),
50
+ claimed: new Set(['running', 'failed', 'skipped', 'claimed']),
51
+ running: new Set(['succeeded', 'failed', 'claimed']),
52
+ failed: new Set(['claimed']),
53
+ succeeded: new Set([]),
54
+ skipped: new Set([]),
55
+ };
56
+
57
+ function toNumber(v) {
58
+ if (v === null || v === undefined) return null;
59
+ const n = Number(v);
60
+ return Number.isFinite(n) ? n : null;
61
+ }
62
+
63
+ function emptyPhaseState() {
64
+ return { status: 'pending', attempts: 0 };
65
+ }
66
+
67
+ function fillDefaults(phasesJson) {
68
+ const out = { ...(phasesJson || {}) };
69
+ for (const phase of PHASES) {
70
+ if (!out[phase]) out[phase] = emptyPhaseState();
71
+ }
72
+ return out;
73
+ }
74
+
75
+ function isStale(phaseState, staleAfterSeconds) {
76
+ if (phaseState.status !== 'claimed' && phaseState.status !== 'running') return false;
77
+ const startedAt = phaseState.startedAt;
78
+ if (!startedAt) return true;
79
+ const ageMs = Date.now() - new Date(startedAt).getTime();
80
+ return ageMs > staleAfterSeconds * 1000;
81
+ }
82
+
83
+ function newClaimToken() {
84
+ return crypto.randomBytes(12).toString('hex');
85
+ }
86
+
87
+ function advisoryLockKey(sessionRowId) {
88
+ // Map bigint-ish id to signed int4 range for pg_advisory_xact_lock.
89
+ const id = Number(sessionRowId);
90
+ return (id ^ 0x9e3779b9) & 0x7fffffff;
91
+ }
92
+
93
+ function createConsolidation({ pool, schema, defaultTenantId }) {
94
+
95
+ async function claimNext(input = {}) {
96
+ try {
97
+ if (!input.workerId) return err('AQ_INVALID_INPUT', 'workerId is required');
98
+ const tenantId = input.tenantId || defaultTenantId || 'default';
99
+ const phases = Array.isArray(input.phases) && input.phases.length > 0
100
+ ? input.phases.filter(p => PHASE_SET.has(p))
101
+ : PHASES;
102
+ if (phases.length === 0) {
103
+ return err('AQ_INVALID_INPUT', 'phases filter produced empty list');
104
+ }
105
+ const staleAfterSeconds = Number.isFinite(input.staleAfterSeconds)
106
+ ? Math.max(10, input.staleAfterSeconds)
107
+ : 600;
108
+
109
+ // Look at candidate sessions with at least one non-terminal phase,
110
+ // then iterate under advisory lock to claim atomically.
111
+ const candidates = await pool.query(
112
+ `SELECT id AS session_row_id, session_id, agent_id, processing_status,
113
+ consolidation_phases
114
+ FROM ${schema}.sessions
115
+ WHERE tenant_id = $1
116
+ ORDER BY id ASC
117
+ LIMIT 200`,
118
+ [tenantId],
119
+ );
120
+
121
+ for (const row of candidates.rows) {
122
+ const current = fillDefaults(row.consolidation_phases);
123
+ let targetPhase = null;
124
+ for (const p of phases) {
125
+ const st = current[p];
126
+ if (st.status === 'pending' || st.status === 'failed') {
127
+ targetPhase = p; break;
128
+ }
129
+ if ((st.status === 'claimed' || st.status === 'running')
130
+ && isStale(st, staleAfterSeconds)) {
131
+ targetPhase = p; break;
132
+ }
133
+ }
134
+ if (!targetPhase) continue;
135
+
136
+ const client = await pool.connect();
137
+ try {
138
+ await client.query('BEGIN');
139
+ await client.query('SELECT pg_advisory_xact_lock($1)',
140
+ [advisoryLockKey(row.session_row_id)]);
141
+
142
+ // Re-read under lock — another worker may have claimed in between.
143
+ const { rows: freshRows } = await client.query(
144
+ `SELECT consolidation_phases FROM ${schema}.sessions WHERE id = $1`,
145
+ [row.session_row_id],
146
+ );
147
+ const fresh = fillDefaults(freshRows[0] && freshRows[0].consolidation_phases);
148
+ const freshState = fresh[targetPhase];
149
+ const eligible =
150
+ freshState.status === 'pending'
151
+ || freshState.status === 'failed'
152
+ || ((freshState.status === 'claimed' || freshState.status === 'running')
153
+ && isStale(freshState, staleAfterSeconds));
154
+ if (!eligible) {
155
+ await client.query('ROLLBACK');
156
+ client.release();
157
+ continue;
158
+ }
159
+
160
+ const claimToken = newClaimToken();
161
+ const attempts = (freshState.attempts || 0) + 1;
162
+ fresh[targetPhase] = {
163
+ ...freshState,
164
+ status: 'claimed',
165
+ claimToken,
166
+ workerId: input.workerId,
167
+ startedAt: new Date().toISOString(),
168
+ finishedAt: null,
169
+ attempts,
170
+ errorCode: null,
171
+ errorMessage: null,
172
+ };
173
+
174
+ await client.query(
175
+ `UPDATE ${schema}.sessions SET consolidation_phases = $1
176
+ WHERE id = $2`,
177
+ [JSON.stringify(fresh), row.session_row_id],
178
+ );
179
+ await client.query('COMMIT');
180
+ client.release();
181
+
182
+ return ok({
183
+ session: {
184
+ sessionRowId: toNumber(row.session_row_id),
185
+ sessionId: row.session_id,
186
+ agentId: row.agent_id,
187
+ processingStatus: row.processing_status,
188
+ phases: fresh,
189
+ },
190
+ claimToken,
191
+ claimedPhase: targetPhase,
192
+ });
193
+ } catch (e) {
194
+ await client.query('ROLLBACK').catch(() => {});
195
+ client.release();
196
+ throw e;
197
+ }
198
+ }
199
+
200
+ return ok({ session: null, claimToken: null, claimedPhase: null });
201
+ } catch (e) {
202
+ if (e instanceof AqError) return err(e);
203
+ return err('AQ_INTERNAL', e.message, { cause: e });
204
+ }
205
+ }
206
+
207
+ async function transitionPhase(input = {}) {
208
+ try {
209
+ if (!input.sessionId) return err('AQ_INVALID_INPUT', 'sessionId is required');
210
+ if (!input.phase || !PHASE_SET.has(input.phase)) {
211
+ return err('AQ_INVALID_INPUT', `phase must be one of ${PHASES.join(', ')}`);
212
+ }
213
+ if (!input.fromStatus || !STATUS_SET.has(input.fromStatus)) {
214
+ return err('AQ_INVALID_INPUT', 'valid fromStatus is required');
215
+ }
216
+ if (!input.toStatus || !STATUS_SET.has(input.toStatus)) {
217
+ return err('AQ_INVALID_INPUT', 'valid toStatus is required');
218
+ }
219
+ const tenantId = input.tenantId || defaultTenantId || 'default';
220
+
221
+ const sessionRow = await pool.query(
222
+ `SELECT id FROM ${schema}.sessions WHERE tenant_id = $1 AND session_id = $2`,
223
+ [tenantId, input.sessionId],
224
+ );
225
+ if (sessionRow.rowCount === 0) {
226
+ return err('AQ_NOT_FOUND', `session ${input.sessionId} not found`);
227
+ }
228
+ const sessionRowId = sessionRow.rows[0].id;
229
+
230
+ const client = await pool.connect();
231
+ try {
232
+ await client.query('BEGIN');
233
+ await client.query('SELECT pg_advisory_xact_lock($1)',
234
+ [advisoryLockKey(sessionRowId)]);
235
+
236
+ const { rows } = await client.query(
237
+ `SELECT consolidation_phases FROM ${schema}.sessions WHERE id = $1`,
238
+ [sessionRowId],
239
+ );
240
+ const phases = fillDefaults(rows[0] && rows[0].consolidation_phases);
241
+ const current = phases[input.phase];
242
+
243
+ // Guard: fromStatus must match current.
244
+ if (current.status !== input.fromStatus) {
245
+ await client.query('ROLLBACK');
246
+ return err('AQ_PHASE_CLAIM_CONFLICT',
247
+ `phase ${input.phase} currently ${current.status}, not ${input.fromStatus}`);
248
+ }
249
+
250
+ // Guard: claimToken must match when transitioning from claimed/running.
251
+ if ((input.fromStatus === 'claimed' || input.fromStatus === 'running')
252
+ && input.claimToken && current.claimToken !== input.claimToken) {
253
+ await client.query('ROLLBACK');
254
+ return err('AQ_PHASE_CLAIM_CONFLICT',
255
+ `claimToken mismatch for phase ${input.phase}`);
256
+ }
257
+
258
+ // Validate transition.
259
+ const allowed = VALID_TRANSITIONS[input.fromStatus] || new Set();
260
+ if (!allowed.has(input.toStatus)) {
261
+ // Terminal → non-terminal requires forceReplay.
262
+ const leavingTerminal = TERMINAL.has(input.fromStatus);
263
+ if (!(leavingTerminal && input.forceReplay === true)) {
264
+ await client.query('ROLLBACK');
265
+ return err('AQ_PHASE_TRANSITION_INVALID',
266
+ `cannot transition ${input.phase} from ${input.fromStatus} to ${input.toStatus}`);
267
+ }
268
+ }
269
+
270
+ const next = { ...current, status: input.toStatus };
271
+ if (input.toStatus === 'running') {
272
+ next.startedAt = current.startedAt || new Date().toISOString();
273
+ }
274
+ if (input.toStatus === 'succeeded' || input.toStatus === 'failed'
275
+ || input.toStatus === 'skipped') {
276
+ next.finishedAt = new Date().toISOString();
277
+ if (input.toStatus === 'succeeded' || input.toStatus === 'skipped') {
278
+ next.errorCode = null;
279
+ next.errorMessage = null;
280
+ }
281
+ }
282
+ if (input.toStatus === 'failed' && input.error) {
283
+ next.errorCode = input.error.code || 'AQ_INTERNAL';
284
+ next.errorMessage = input.error.message || '';
285
+ }
286
+ if (input.retryAfter) next.retryAfter = input.retryAfter;
287
+ if (input.idempotencyKey) next.idempotencyKey = input.idempotencyKey;
288
+ if (input.outputRef) next.outputRef = { ...(current.outputRef || {}), ...input.outputRef };
289
+
290
+ phases[input.phase] = next;
291
+
292
+ await client.query(
293
+ `UPDATE ${schema}.sessions SET consolidation_phases = $1 WHERE id = $2`,
294
+ [JSON.stringify(phases), sessionRowId],
295
+ );
296
+ await client.query('COMMIT');
297
+
298
+ return ok({
299
+ sessionId: input.sessionId,
300
+ phase: input.phase,
301
+ state: next,
302
+ });
303
+ } catch (e) {
304
+ await client.query('ROLLBACK').catch(() => {});
305
+ throw e;
306
+ } finally {
307
+ client.release();
308
+ }
309
+ } catch (e) {
310
+ if (e instanceof AqError) return err(e);
311
+ return err('AQ_INTERNAL', e.message, { cause: e });
312
+ }
313
+ }
314
+
315
+ async function getState(input = {}) {
316
+ try {
317
+ if (!input.sessionId) return err('AQ_INVALID_INPUT', 'sessionId is required');
318
+ const tenantId = input.tenantId || defaultTenantId || 'default';
319
+ const { rows } = await pool.query(
320
+ `SELECT processing_status, consolidation_phases
321
+ FROM ${schema}.sessions
322
+ WHERE tenant_id = $1 AND session_id = $2`,
323
+ [tenantId, input.sessionId],
324
+ );
325
+ if (rows.length === 0) {
326
+ return err('AQ_NOT_FOUND', `session ${input.sessionId} not found`);
327
+ }
328
+ return ok({
329
+ processingStatus: rows[0].processing_status,
330
+ phases: fillDefaults(rows[0].consolidation_phases),
331
+ });
332
+ } catch (e) {
333
+ return err('AQ_INTERNAL', e.message, { cause: e });
334
+ }
335
+ }
336
+
337
+ return { claimNext, transitionPhase, getState, PHASES, STATUSES };
338
+ }
339
+
340
+ module.exports = { createConsolidation, PHASES, STATUSES };
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ // aq.decisions.* — append-only decision log capability.
4
+ //
5
+ // Spec: aquifer-completion §9 decisionLog. status vocabulary
6
+ // (proposed/committed/reversed) enforced both at API layer (fast reject)
7
+ // and by DB CHECK constraint (defense in depth). reversal is implemented
8
+ // by appending a new 'reversed' decision and optionally pointing
9
+ // reversed_by_decision_id; Aquifer doesn't auto-compute the chain.
10
+
11
+ const crypto = require('crypto');
12
+ const { AqError, ok, err } = require('./errors');
13
+
14
+ const DEFAULT_PROFILE = Object.freeze({
15
+ id: 'anon',
16
+ version: 0,
17
+ schemaHash: 'pending',
18
+ });
19
+
20
+ const VALID_STATUSES = new Set(['proposed', 'committed', 'reversed']);
21
+
22
+ function resolveProfile(profile) {
23
+ if (!profile) return DEFAULT_PROFILE;
24
+ return {
25
+ id: profile.id || DEFAULT_PROFILE.id,
26
+ version: Number.isInteger(profile.version) ? profile.version : DEFAULT_PROFILE.version,
27
+ schemaHash: profile.schemaHash || DEFAULT_PROFILE.schemaHash,
28
+ };
29
+ }
30
+
31
+ function toNumber(v) {
32
+ if (v === null || v === undefined) return null;
33
+ const n = Number(v);
34
+ return Number.isFinite(n) ? n : null;
35
+ }
36
+
37
+ function defaultIdempotencyKey({ tenantId, agentId, sessionId, payload }) {
38
+ return crypto.createHash('sha256')
39
+ .update(`${tenantId}:${agentId}:${sessionId}:${JSON.stringify(payload)}`)
40
+ .digest('hex');
41
+ }
42
+
43
+ function mapRow(row) {
44
+ if (!row) return null;
45
+ return {
46
+ decisionId: toNumber(row.id),
47
+ sessionId: row.source_session_id,
48
+ agentId: row.agent_id,
49
+ status: row.status,
50
+ decisionText: row.decision_text,
51
+ reasonText: row.reason_text,
52
+ payload: row.payload || {},
53
+ metadata: row.metadata || {},
54
+ decidedAt: row.decided_at,
55
+ reversedByDecisionId: toNumber(row.reversed_by_decision_id),
56
+ createdAt: row.created_at,
57
+ };
58
+ }
59
+
60
+ function createDecisions({ pool, schema, defaultTenantId }) {
61
+ async function append(input) {
62
+ try {
63
+ if (!input || typeof input !== 'object') {
64
+ return err('AQ_INVALID_INPUT', 'append requires an input object');
65
+ }
66
+ if (!input.agentId) return err('AQ_INVALID_INPUT', 'agentId is required');
67
+ if (!input.sessionId) return err('AQ_INVALID_INPUT', 'sessionId is required');
68
+ if (!input.payload || typeof input.payload !== 'object') {
69
+ return err('AQ_INVALID_INPUT', 'payload is required');
70
+ }
71
+ const tenantId = input.tenantId || defaultTenantId || 'default';
72
+ const agentId = input.agentId;
73
+ const sessionId = input.sessionId;
74
+ const payload = input.payload;
75
+ const profile = resolveProfile(input.profile);
76
+
77
+ const status = payload.status || 'committed';
78
+ if (!VALID_STATUSES.has(status)) {
79
+ return err('AQ_INVALID_INPUT',
80
+ `status must be one of ${Array.from(VALID_STATUSES).join(', ')}`);
81
+ }
82
+ const decisionText = typeof payload.decision === 'string'
83
+ ? payload.decision
84
+ : (typeof payload.decision_text === 'string' ? payload.decision_text : null);
85
+ if (!decisionText) {
86
+ return err('AQ_INVALID_INPUT', 'payload.decision (or decision_text) is required');
87
+ }
88
+ const reasonText = typeof payload.reason === 'string'
89
+ ? payload.reason
90
+ : (typeof payload.reason_text === 'string' ? payload.reason_text : null);
91
+
92
+ const idempotencyKey = input.idempotencyKey
93
+ || defaultIdempotencyKey({ tenantId, agentId, sessionId, payload });
94
+
95
+ const insertResult = await pool.query(
96
+ `INSERT INTO ${schema}.decisions (
97
+ tenant_id, agent_id, source_session_id,
98
+ consumer_profile_id, consumer_profile_version, consumer_schema_hash,
99
+ idempotency_key, payload, status, decision_text, reason_text,
100
+ decided_at, metadata
101
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
102
+ COALESCE($12::timestamptz, now()), $13)
103
+ ON CONFLICT (idempotency_key) DO NOTHING
104
+ RETURNING *`,
105
+ [
106
+ tenantId, agentId, sessionId,
107
+ profile.id, profile.version, profile.schemaHash,
108
+ idempotencyKey, JSON.stringify(payload), status,
109
+ decisionText, reasonText,
110
+ input.decidedAt || null,
111
+ JSON.stringify(payload.metadata || {}),
112
+ ],
113
+ );
114
+ let row = insertResult.rows[0];
115
+ if (!row) {
116
+ const existing = await pool.query(
117
+ `SELECT * FROM ${schema}.decisions WHERE idempotency_key = $1`,
118
+ [idempotencyKey],
119
+ );
120
+ row = existing.rows[0];
121
+ }
122
+ const mapped = mapRow(row);
123
+ return ok({ decisionId: mapped.decisionId, payload: mapped.payload });
124
+ } catch (e) {
125
+ if (e instanceof AqError) return err(e);
126
+ return err('AQ_INTERNAL', e.message, { cause: e });
127
+ }
128
+ }
129
+
130
+ async function list(input = {}) {
131
+ try {
132
+ if (!input.agentId) return err('AQ_INVALID_INPUT', 'agentId is required');
133
+ const tenantId = input.tenantId || defaultTenantId || 'default';
134
+ const limit = Math.min(Math.max(input.limit || 50, 1), 500);
135
+
136
+ const params = [tenantId, input.agentId];
137
+ let where = 'tenant_id = $1 AND agent_id = $2';
138
+ if (Array.isArray(input.statuses) && input.statuses.length > 0) {
139
+ params.push(input.statuses);
140
+ where += ` AND status = ANY($${params.length})`;
141
+ }
142
+ if (input.sessionId) {
143
+ params.push(input.sessionId);
144
+ where += ` AND source_session_id = $${params.length}`;
145
+ }
146
+ params.push(limit);
147
+
148
+ const { rows } = await pool.query(
149
+ `SELECT * FROM ${schema}.decisions
150
+ WHERE ${where}
151
+ ORDER BY decided_at DESC, id DESC
152
+ LIMIT $${params.length}`,
153
+ params,
154
+ );
155
+ return ok({ rows: rows.map(mapRow) });
156
+ } catch (e) {
157
+ return err('AQ_INTERNAL', e.message, { cause: e });
158
+ }
159
+ }
160
+
161
+ return { append, list };
162
+ }
163
+
164
+ module.exports = { createDecisions };
package/core/entity.js CHANGED
@@ -236,7 +236,6 @@ async function upsertEntityRelations(pool, {
236
236
  if (validPairs.length === 0) return { upserted: 0 };
237
237
 
238
238
  // Batch insert: multi-row VALUES
239
- const COLS_PER_ROW = 3;
240
239
  const valueClauses = [];
241
240
  const params = [];
242
241
 
@@ -387,8 +386,7 @@ async function resolveEntities(pool, {
387
386
  if (!normQ || seen.has(normQ)) continue;
388
387
  seen.set(normQ, true);
389
388
 
390
- const escaped = _escapeIlike(normQ);
391
- const result = await pool.query(
389
+ const result = await pool.query(
392
390
  `SELECT id, name, normalized_name
393
391
  FROM ${qi(schema)}.entities
394
392
  WHERE status = 'active'
package/core/errors.js ADDED
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ // AqError / AqResult — canonical error and result envelope for the
4
+ // completion-capability API surface (P1 foundation).
5
+ //
6
+ // Scope: NEW capability methods only (aq.narratives.*, aq.facts.*,
7
+ // aq.consolidation.*, aq.profiles.*, aq.timeline.*, etc.).
8
+ // Legacy APIs (commit/enrich/recall/migrate) keep throw semantics
9
+ // until a 2.0 major. See aquifer-completion define §audit.
10
+ //
11
+ // Shape mirrors the spec:
12
+ // type AqResult<T> = { ok: true, data: T } | { ok: false, error: AqError };
13
+ //
14
+ // AqError is a plain subclass of Error that carries a stable `code`,
15
+ // an optional `details` bag, and a `retryable` flag so transport-layer
16
+ // retries (cc-afterburn, gateway afterburn) can make routing decisions
17
+ // without string-matching messages.
18
+
19
+ const KNOWN_CODES = new Set([
20
+ // Generic
21
+ 'AQ_INVALID_INPUT',
22
+ 'AQ_NOT_FOUND',
23
+ 'AQ_CONFLICT',
24
+ 'AQ_INTERNAL',
25
+ 'AQ_DEPENDENCY',
26
+ // Consolidation orchestration
27
+ 'AQ_PHASE_CLAIM_CONFLICT',
28
+ 'AQ_PHASE_TRANSITION_INVALID',
29
+ // Schema registry / profile
30
+ 'AQ_PROFILE_NOT_FOUND',
31
+ 'AQ_PROFILE_MARKER_MISMATCH',
32
+ // Bundle
33
+ 'AQ_IMPORT_CONFLICT',
34
+ // Facts / narratives lifecycle
35
+ 'AQ_FACT_SUPERSEDED',
36
+ 'AQ_NARRATIVE_SUPERSEDED',
37
+ ]);
38
+
39
+ class AqError extends Error {
40
+ constructor(code, message, opts = {}) {
41
+ super(message);
42
+ this.name = 'AqError';
43
+ this.code = code;
44
+ this.details = opts.details || null;
45
+ this.retryable = opts.retryable === true;
46
+ if (opts.cause) this.cause = opts.cause;
47
+ }
48
+
49
+ toJSON() {
50
+ return {
51
+ name: this.name,
52
+ code: this.code,
53
+ message: this.message,
54
+ details: this.details,
55
+ retryable: this.retryable,
56
+ };
57
+ }
58
+ }
59
+
60
+ function ok(data) {
61
+ return { ok: true, data };
62
+ }
63
+
64
+ function err(code, message, opts = {}) {
65
+ const error = code instanceof AqError
66
+ ? code
67
+ : new AqError(code, message, opts);
68
+ return { ok: false, error };
69
+ }
70
+
71
+ // Wraps an async function so any thrown error becomes an AQ_INTERNAL AqError.
72
+ // Use at capability method boundaries; inside, code should prefer explicit
73
+ // ok()/err() returns for known failure modes.
74
+ function asResult(asyncFn) {
75
+ return async (...args) => {
76
+ try {
77
+ const data = await asyncFn(...args);
78
+ return ok(data);
79
+ } catch (e) {
80
+ if (e instanceof AqError) return err(e);
81
+ return err('AQ_INTERNAL', e.message, { cause: e });
82
+ }
83
+ };
84
+ }
85
+
86
+ function isKnownCode(code) {
87
+ return KNOWN_CODES.has(code);
88
+ }
89
+
90
+ module.exports = {
91
+ AqError,
92
+ ok,
93
+ err,
94
+ asResult,
95
+ isKnownCode,
96
+ KNOWN_CODES,
97
+ };