audrey 0.17.0 → 0.20.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 +129 -374
- package/dist/mcp-server/config.d.ts +20 -0
- package/dist/mcp-server/config.d.ts.map +1 -0
- package/dist/mcp-server/config.js +125 -0
- package/dist/mcp-server/config.js.map +1 -0
- package/dist/mcp-server/index.d.ts +100 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +1113 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/adaptive.d.ts +7 -0
- package/dist/src/adaptive.d.ts.map +1 -0
- package/dist/src/adaptive.js +49 -0
- package/dist/src/adaptive.js.map +1 -0
- package/dist/src/affect.d.ts +19 -0
- package/dist/src/affect.d.ts.map +1 -0
- package/dist/src/affect.js +72 -0
- package/dist/src/affect.js.map +1 -0
- package/dist/src/audrey.d.ts +140 -0
- package/dist/src/audrey.d.ts.map +1 -0
- package/dist/src/audrey.js +564 -0
- package/dist/src/audrey.js.map +1 -0
- package/dist/src/capsule.d.ts +68 -0
- package/dist/src/capsule.d.ts.map +1 -0
- package/dist/src/capsule.js +311 -0
- package/dist/src/capsule.js.map +1 -0
- package/dist/src/causal.d.ts +28 -0
- package/dist/src/causal.d.ts.map +1 -0
- package/dist/src/causal.js +65 -0
- package/dist/src/causal.js.map +1 -0
- package/dist/src/confidence.d.ts +12 -0
- package/dist/src/confidence.d.ts.map +1 -0
- package/dist/src/confidence.js +63 -0
- package/dist/src/confidence.js.map +1 -0
- package/dist/src/consolidate.d.ts +8 -0
- package/dist/src/consolidate.d.ts.map +1 -0
- package/dist/src/consolidate.js +218 -0
- package/dist/src/consolidate.js.map +1 -0
- package/dist/src/context.d.ts +3 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +19 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/db.d.ts +12 -0
- package/dist/src/db.d.ts.map +1 -0
- package/dist/src/db.js +380 -0
- package/dist/src/db.js.map +1 -0
- package/dist/src/decay.d.ts +7 -0
- package/dist/src/decay.d.ts.map +1 -0
- package/dist/src/decay.js +68 -0
- package/dist/src/decay.js.map +1 -0
- package/dist/src/embedding.d.ts +57 -0
- package/dist/src/embedding.d.ts.map +1 -0
- package/dist/src/embedding.js +254 -0
- package/dist/src/embedding.js.map +1 -0
- package/dist/src/encode.d.ts +15 -0
- package/dist/src/encode.d.ts.map +1 -0
- package/dist/src/encode.js +36 -0
- package/dist/src/encode.js.map +1 -0
- package/dist/src/events.d.ts +69 -0
- package/dist/src/events.d.ts.map +1 -0
- package/dist/src/events.js +149 -0
- package/dist/src/events.js.map +1 -0
- package/dist/src/export.d.ts +3 -0
- package/dist/src/export.d.ts.map +1 -0
- package/dist/src/export.js +46 -0
- package/dist/src/export.js.map +1 -0
- package/dist/src/forget.d.ts +11 -0
- package/dist/src/forget.d.ts.map +1 -0
- package/dist/src/forget.js +105 -0
- package/dist/src/forget.js.map +1 -0
- package/dist/src/fts.d.ts +34 -0
- package/dist/src/fts.d.ts.map +1 -0
- package/dist/src/fts.js +117 -0
- package/dist/src/fts.js.map +1 -0
- package/dist/src/hybrid-recall.d.ts +37 -0
- package/dist/src/hybrid-recall.d.ts.map +1 -0
- package/dist/src/hybrid-recall.js +213 -0
- package/dist/src/hybrid-recall.js.map +1 -0
- package/dist/src/import.d.ts +4 -0
- package/dist/src/import.d.ts.map +1 -0
- package/dist/src/import.js +127 -0
- package/dist/src/import.js.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -0
- package/{src → dist/src}/index.js +5 -13
- package/dist/src/index.js.map +1 -0
- package/dist/src/interference.d.ts +13 -0
- package/dist/src/interference.d.ts.map +1 -0
- package/dist/src/interference.js +45 -0
- package/dist/src/interference.js.map +1 -0
- package/dist/src/introspect.d.ts +4 -0
- package/dist/src/introspect.d.ts.map +1 -0
- package/dist/src/introspect.js +40 -0
- package/dist/src/introspect.js.map +1 -0
- package/dist/src/llm.d.ts +38 -0
- package/dist/src/llm.d.ts.map +1 -0
- package/dist/src/llm.js +167 -0
- package/dist/src/llm.js.map +1 -0
- package/dist/src/migrate.d.ts +6 -0
- package/dist/src/migrate.d.ts.map +1 -0
- package/dist/src/migrate.js +51 -0
- package/dist/src/migrate.js.map +1 -0
- package/dist/src/promote.d.ts +40 -0
- package/dist/src/promote.d.ts.map +1 -0
- package/dist/src/promote.js +200 -0
- package/dist/src/promote.js.map +1 -0
- package/dist/src/prompts.d.ts +16 -0
- package/dist/src/prompts.d.ts.map +1 -0
- package/{src → dist/src}/prompts.js +172 -203
- package/dist/src/prompts.js.map +1 -0
- package/dist/src/recall.d.ts +9 -0
- package/dist/src/recall.d.ts.map +1 -0
- package/dist/src/recall.js +432 -0
- package/dist/src/recall.js.map +1 -0
- package/dist/src/redact.d.ts +27 -0
- package/dist/src/redact.d.ts.map +1 -0
- package/dist/src/redact.js +228 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/rollback.d.ts +8 -0
- package/dist/src/rollback.d.ts.map +1 -0
- package/dist/src/rollback.js +33 -0
- package/dist/src/rollback.js.map +1 -0
- package/dist/src/routes.d.ts +7 -0
- package/dist/src/routes.d.ts.map +1 -0
- package/dist/src/routes.js +226 -0
- package/dist/src/routes.js.map +1 -0
- package/dist/src/rules-compiler.d.ts +20 -0
- package/dist/src/rules-compiler.d.ts.map +1 -0
- package/dist/src/rules-compiler.js +143 -0
- package/dist/src/rules-compiler.js.map +1 -0
- package/dist/src/server.d.ts +12 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +22 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/tool-trace.d.ts +37 -0
- package/dist/src/tool-trace.d.ts.map +1 -0
- package/dist/src/tool-trace.js +142 -0
- package/dist/src/tool-trace.js.map +1 -0
- package/dist/src/types.d.ts +446 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/ulid.d.ts +3 -0
- package/dist/src/ulid.d.ts.map +1 -0
- package/dist/src/ulid.js +11 -0
- package/dist/src/ulid.js.map +1 -0
- package/dist/src/utils.d.ts +10 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +41 -0
- package/dist/src/utils.js.map +1 -0
- package/dist/src/validate.d.ts +22 -0
- package/dist/src/validate.d.ts.map +1 -0
- package/dist/src/validate.js +109 -0
- package/dist/src/validate.js.map +1 -0
- package/docs/production-readiness.md +28 -0
- package/examples/fintech-ops-demo.js +1 -1
- package/examples/healthcare-ops-demo.js +1 -1
- package/examples/stripe-demo.js +1 -1
- package/package.json +34 -13
- package/benchmarks/baselines.js +0 -169
- package/benchmarks/cases.js +0 -421
- package/benchmarks/reference-results.js +0 -70
- package/benchmarks/report.js +0 -255
- package/benchmarks/run.js +0 -514
- package/mcp-server/config.js +0 -133
- package/mcp-server/index.js +0 -1265
- package/mcp-server/serve.js +0 -482
- package/src/adaptive.js +0 -53
- package/src/affect.js +0 -64
- package/src/audrey.js +0 -642
- package/src/causal.js +0 -95
- package/src/confidence.js +0 -120
- package/src/consolidate.js +0 -281
- package/src/context.js +0 -15
- package/src/db.js +0 -391
- package/src/decay.js +0 -84
- package/src/embedding.js +0 -260
- package/src/encode.js +0 -69
- package/src/export.js +0 -67
- package/src/forget.js +0 -111
- package/src/fts.js +0 -134
- package/src/import.js +0 -273
- package/src/interference.js +0 -51
- package/src/introspect.js +0 -48
- package/src/llm.js +0 -249
- package/src/migrate.js +0 -58
- package/src/recall.js +0 -573
- package/src/rollback.js +0 -42
- package/src/ulid.js +0 -18
- package/src/utils.js +0 -63
- package/src/validate.js +0 -172
- package/types/index.d.ts +0 -434
package/src/recall.js
DELETED
|
@@ -1,573 +0,0 @@
|
|
|
1
|
-
import { computeConfidence, DEFAULT_HALF_LIVES, salienceModifier, sourceReliability } from './confidence.js';
|
|
2
|
-
import { interferenceModifier } from './interference.js';
|
|
3
|
-
import { contextMatchRatio, contextModifier } from './context.js';
|
|
4
|
-
import { moodCongruenceModifier, affectSimilarity } from './affect.js';
|
|
5
|
-
import { daysBetween, safeJsonParse } from './utils.js';
|
|
6
|
-
import { hasFTSTables, searchFTSEpisodes, searchFTSSemantics, searchFTSProcedures, sanitizeFTSQuery } from './fts.js';
|
|
7
|
-
|
|
8
|
-
const STOPWORDS = new Set([
|
|
9
|
-
'a', 'an', 'and', 'are', 'at', 'be', 'by', 'did', 'do', 'does', 'for', 'from', 'had', 'has', 'have',
|
|
10
|
-
'how', 'i', 'in', 'is', 'it', 'me', 'my', 'now', 'of', 'on', 'or', 'our', 's', 'sam', 'she', 'that',
|
|
11
|
-
'the', 'their', 'them', 'there', 'they', 'this', 'to', 'was', 'we', 'were', 'what', 'when', 'where',
|
|
12
|
-
'which', 'who', 'why', 'with', 'would', 'you', 'your',
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
const IDENTIFIER_TERMS = new Set(['account', 'api', 'credential', 'id', 'identifier', 'key', 'number', 'password', 'secret', 'ssn', 'token']);
|
|
16
|
-
|
|
17
|
-
function tokenize(text) {
|
|
18
|
-
return String(text || '')
|
|
19
|
-
.toLowerCase()
|
|
20
|
-
.replace(/[^a-z0-9]+/g, ' ')
|
|
21
|
-
.trim()
|
|
22
|
-
.split(/\s+/)
|
|
23
|
-
.filter(Boolean);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function significantTokens(text) {
|
|
27
|
-
return tokenize(text).filter(token => !STOPWORDS.has(token));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function lexicalCoverage(query, content) {
|
|
31
|
-
const queryTokens = significantTokens(query);
|
|
32
|
-
if (queryTokens.length === 0) return 1;
|
|
33
|
-
const contentTokens = new Set(significantTokens(content));
|
|
34
|
-
let matched = 0;
|
|
35
|
-
for (const token of queryTokens) {
|
|
36
|
-
if (contentTokens.has(token)) matched++;
|
|
37
|
-
}
|
|
38
|
-
return matched / queryTokens.length;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function hasIdentifierIntent(query) {
|
|
42
|
-
const normalized = String(query || '').toLowerCase();
|
|
43
|
-
const asksForValue = /\b(find|give|lookup|show|tell|what|which)\b/.test(normalized);
|
|
44
|
-
const mentionsIdentifier = /\b(account number|api key|credential|id|identifier|key|number|passport number|password|secret|ssn|token)\b/.test(normalized);
|
|
45
|
-
return asksForValue && mentionsIdentifier;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function hasIdentifierEvidence(content) {
|
|
49
|
-
const tokens = significantTokens(content);
|
|
50
|
-
if (tokens.some(token => IDENTIFIER_TERMS.has(token))) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
return /(?:\b\d{4,}\b|sk-[a-z0-9_-]+)/i.test(content);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function adjustedScore(query, entry) {
|
|
57
|
-
const coverage = lexicalCoverage(query, entry.content);
|
|
58
|
-
let score = entry.score;
|
|
59
|
-
|
|
60
|
-
if (hasIdentifierIntent(query) && !hasIdentifierEvidence(entry.content)) {
|
|
61
|
-
score *= 0.02;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { score, coverage };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function overlapRatio(contentA, contentB) {
|
|
68
|
-
const tokensA = significantTokens(contentA);
|
|
69
|
-
const tokensB = significantTokens(contentB);
|
|
70
|
-
if (tokensA.length === 0 || tokensB.length === 0) return 0;
|
|
71
|
-
const setB = new Set(tokensB);
|
|
72
|
-
let matched = 0;
|
|
73
|
-
for (const token of tokensA) {
|
|
74
|
-
if (setB.has(token)) matched++;
|
|
75
|
-
}
|
|
76
|
-
return matched / Math.min(tokensA.length, tokensB.length);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function reliabilityForRecallSource(source) {
|
|
80
|
-
if (source === 'consolidation') {
|
|
81
|
-
return sourceReliability('tool-result');
|
|
82
|
-
}
|
|
83
|
-
return sourceReliability(source);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function shouldSuppressDuplicate(existing, candidate) {
|
|
87
|
-
const overlap = overlapRatio(existing.content, candidate.content);
|
|
88
|
-
if (overlap < 0.5) return false;
|
|
89
|
-
if (existing.type !== candidate.type) return false;
|
|
90
|
-
const existingReliability = reliabilityForRecallSource(existing.source);
|
|
91
|
-
const candidateReliability = reliabilityForRecallSource(candidate.source);
|
|
92
|
-
if (existingReliability < candidateReliability) return false;
|
|
93
|
-
if (existingReliability - candidateReliability < 0.2) return false;
|
|
94
|
-
return existing.score >= candidate.score * 0.95;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function applyResultGuards(query, results, limit) {
|
|
98
|
-
const identifierIntent = hasIdentifierIntent(query);
|
|
99
|
-
const rescored = results
|
|
100
|
-
.map(entry => {
|
|
101
|
-
const { score, coverage } = adjustedScore(query, entry);
|
|
102
|
-
return { ...entry, score, lexicalCoverage: coverage };
|
|
103
|
-
})
|
|
104
|
-
.filter(entry => !identifierIntent || entry.score > 0.05)
|
|
105
|
-
.sort((a, b) => b.score - a.score);
|
|
106
|
-
|
|
107
|
-
const accepted = [];
|
|
108
|
-
for (const candidate of rescored) {
|
|
109
|
-
if (accepted.some(existing => shouldSuppressDuplicate(existing, candidate))) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
accepted.push(candidate);
|
|
113
|
-
if (accepted.length >= limit) break;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return accepted;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function computeEpisodicConfidence(ep, now, confidenceConfig = {}) {
|
|
120
|
-
const ageDays = daysBetween(ep.created_at, now);
|
|
121
|
-
const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
|
|
122
|
-
let confidence = computeConfidence({
|
|
123
|
-
sourceType: ep.source,
|
|
124
|
-
supportingCount: 1,
|
|
125
|
-
contradictingCount: 0,
|
|
126
|
-
ageDays,
|
|
127
|
-
halfLifeDays: halfLives.episodic ?? DEFAULT_HALF_LIVES.episodic,
|
|
128
|
-
retrievalCount: 0,
|
|
129
|
-
daysSinceRetrieval: ageDays,
|
|
130
|
-
weights: confidenceConfig.weights,
|
|
131
|
-
customSourceReliability: confidenceConfig.sourceReliability,
|
|
132
|
-
});
|
|
133
|
-
confidence *= salienceModifier(ep.salience);
|
|
134
|
-
return Math.max(0, Math.min(1, confidence));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function computeSemanticConfidence(sem, now, confidenceConfig = {}) {
|
|
138
|
-
const ageDays = daysBetween(sem.created_at, now);
|
|
139
|
-
const daysSinceRetrieval = sem.last_reinforced_at
|
|
140
|
-
? daysBetween(sem.last_reinforced_at, now)
|
|
141
|
-
: ageDays;
|
|
142
|
-
const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
|
|
143
|
-
let confidence = computeConfidence({
|
|
144
|
-
sourceType: 'tool-result',
|
|
145
|
-
supportingCount: sem.supporting_count || 0,
|
|
146
|
-
contradictingCount: sem.contradicting_count || 0,
|
|
147
|
-
ageDays,
|
|
148
|
-
halfLifeDays: halfLives.semantic ?? DEFAULT_HALF_LIVES.semantic,
|
|
149
|
-
retrievalCount: sem.retrieval_count || 0,
|
|
150
|
-
daysSinceRetrieval,
|
|
151
|
-
weights: confidenceConfig.weights,
|
|
152
|
-
customSourceReliability: confidenceConfig.sourceReliability,
|
|
153
|
-
});
|
|
154
|
-
confidence *= interferenceModifier(sem.interference_count || 0, confidenceConfig.interferenceWeight);
|
|
155
|
-
confidence *= salienceModifier(sem.salience);
|
|
156
|
-
return Math.max(0, Math.min(1, confidence));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
|
|
160
|
-
const ageDays = daysBetween(proc.created_at, now);
|
|
161
|
-
const daysSinceRetrieval = proc.last_reinforced_at
|
|
162
|
-
? daysBetween(proc.last_reinforced_at, now)
|
|
163
|
-
: ageDays;
|
|
164
|
-
const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
|
|
165
|
-
let confidence = computeConfidence({
|
|
166
|
-
sourceType: 'tool-result',
|
|
167
|
-
supportingCount: proc.success_count || 0,
|
|
168
|
-
contradictingCount: proc.failure_count || 0,
|
|
169
|
-
ageDays,
|
|
170
|
-
halfLifeDays: halfLives.procedural ?? DEFAULT_HALF_LIVES.procedural,
|
|
171
|
-
retrievalCount: proc.retrieval_count || 0,
|
|
172
|
-
daysSinceRetrieval,
|
|
173
|
-
weights: confidenceConfig.weights,
|
|
174
|
-
customSourceReliability: confidenceConfig.sourceReliability,
|
|
175
|
-
});
|
|
176
|
-
confidence *= interferenceModifier(proc.interference_count || 0, confidenceConfig.interferenceWeight);
|
|
177
|
-
confidence *= salienceModifier(proc.salience);
|
|
178
|
-
return Math.max(0, Math.min(1, confidence));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMatch, moodCongruence) {
|
|
182
|
-
const entry = {
|
|
183
|
-
id: ep.id,
|
|
184
|
-
content: ep.content,
|
|
185
|
-
type: 'episodic',
|
|
186
|
-
confidence,
|
|
187
|
-
score,
|
|
188
|
-
source: ep.source,
|
|
189
|
-
createdAt: ep.created_at,
|
|
190
|
-
agent: ep.agent || 'default',
|
|
191
|
-
};
|
|
192
|
-
if (contextMatch !== undefined) {
|
|
193
|
-
entry.contextMatch = contextMatch;
|
|
194
|
-
}
|
|
195
|
-
if (moodCongruence !== undefined) {
|
|
196
|
-
entry.moodCongruence = moodCongruence;
|
|
197
|
-
}
|
|
198
|
-
if (includeProvenance) {
|
|
199
|
-
entry.provenance = {
|
|
200
|
-
source: ep.source,
|
|
201
|
-
sourceReliability: ep.source_reliability,
|
|
202
|
-
createdAt: ep.created_at,
|
|
203
|
-
supersedes: ep.supersedes || null,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return entry;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function buildSemanticEntry(sem, confidence, score, includeProvenance) {
|
|
210
|
-
const entry = {
|
|
211
|
-
id: sem.id,
|
|
212
|
-
content: sem.content,
|
|
213
|
-
type: 'semantic',
|
|
214
|
-
confidence,
|
|
215
|
-
score,
|
|
216
|
-
source: 'consolidation',
|
|
217
|
-
state: sem.state,
|
|
218
|
-
createdAt: sem.created_at,
|
|
219
|
-
agent: sem.agent || 'default',
|
|
220
|
-
};
|
|
221
|
-
if (includeProvenance) {
|
|
222
|
-
entry.provenance = {
|
|
223
|
-
evidenceEpisodeIds: safeJsonParse(sem.evidence_episode_ids, []),
|
|
224
|
-
evidenceCount: sem.evidence_count || 0,
|
|
225
|
-
supportingCount: sem.supporting_count || 0,
|
|
226
|
-
contradictingCount: sem.contradicting_count || 0,
|
|
227
|
-
consolidationCheckpoint: sem.consolidation_checkpoint || null,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
return entry;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function buildProceduralEntry(proc, confidence, score, includeProvenance) {
|
|
234
|
-
const entry = {
|
|
235
|
-
id: proc.id,
|
|
236
|
-
content: proc.content,
|
|
237
|
-
type: 'procedural',
|
|
238
|
-
confidence,
|
|
239
|
-
score,
|
|
240
|
-
source: 'consolidation',
|
|
241
|
-
state: proc.state,
|
|
242
|
-
createdAt: proc.created_at,
|
|
243
|
-
agent: proc.agent || 'default',
|
|
244
|
-
};
|
|
245
|
-
if (includeProvenance) {
|
|
246
|
-
entry.provenance = {
|
|
247
|
-
evidenceEpisodeIds: safeJsonParse(proc.evidence_episode_ids, []),
|
|
248
|
-
successCount: proc.success_count || 0,
|
|
249
|
-
failureCount: proc.failure_count || 0,
|
|
250
|
-
triggerConditions: proc.trigger_conditions || null,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
return entry;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function stateClause(includeDormant) {
|
|
257
|
-
return includeDormant
|
|
258
|
-
? "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')"
|
|
259
|
-
: "AND (v.state = 'active' OR v.state = 'context_dependent')";
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function matchesDateFilters(createdAt, filters) {
|
|
263
|
-
if (filters.after && createdAt <= filters.after) return false;
|
|
264
|
-
if (filters.before && createdAt >= filters.before) return false;
|
|
265
|
-
return true;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function safeKForTable(db, table, candidateK) {
|
|
269
|
-
const rowCount = db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get().c;
|
|
270
|
-
return rowCount > 0 ? Math.min(candidateK, rowCount) : 0;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters = {}, includePrivate = false, agentFilter = null) {
|
|
274
|
-
const safeK = safeKForTable(db, 'vec_episodes', candidateK);
|
|
275
|
-
if (safeK === 0) return [];
|
|
276
|
-
const privateClause = includePrivate ? '' : 'AND e."private" = 0';
|
|
277
|
-
const agentClause = agentFilter ? 'AND e.agent = ?' : '';
|
|
278
|
-
const params = agentFilter ? [queryBuffer, safeK, agentFilter] : [queryBuffer, safeK];
|
|
279
|
-
const rows = db.prepare(`
|
|
280
|
-
SELECT e.*, (1.0 - v.distance) AS similarity
|
|
281
|
-
FROM vec_episodes v
|
|
282
|
-
JOIN episodes e ON e.id = v.id
|
|
283
|
-
WHERE v.embedding MATCH ?
|
|
284
|
-
AND k = ?
|
|
285
|
-
AND e.superseded_by IS NULL
|
|
286
|
-
${privateClause}
|
|
287
|
-
${agentClause}
|
|
288
|
-
`).all(...params);
|
|
289
|
-
|
|
290
|
-
const results = [];
|
|
291
|
-
for (const row of rows) {
|
|
292
|
-
if (!matchesDateFilters(row.created_at, filters)) continue;
|
|
293
|
-
if (filters.tags?.length) {
|
|
294
|
-
const rowTags = safeJsonParse(row.tags, []);
|
|
295
|
-
if (!filters.tags.some(t => rowTags.includes(t))) continue;
|
|
296
|
-
}
|
|
297
|
-
if (filters.sources?.length && !filters.sources.includes(row.source)) continue;
|
|
298
|
-
let confidence = computeEpisodicConfidence(row, now, confidenceConfig);
|
|
299
|
-
|
|
300
|
-
let ctxMatch;
|
|
301
|
-
if (confidenceConfig?.retrievalContext) {
|
|
302
|
-
const encodingCtx = safeJsonParse(row.context, {});
|
|
303
|
-
ctxMatch = contextMatchRatio(encodingCtx, confidenceConfig.retrievalContext);
|
|
304
|
-
confidence *= contextModifier(encodingCtx, confidenceConfig.retrievalContext, confidenceConfig.contextWeight);
|
|
305
|
-
confidence = Math.max(0, Math.min(1, confidence));
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
let moodMatch;
|
|
309
|
-
if (confidenceConfig?.retrievalMood) {
|
|
310
|
-
const encodingAffect = safeJsonParse(row.affect, {});
|
|
311
|
-
moodMatch = affectSimilarity(encodingAffect, confidenceConfig.retrievalMood);
|
|
312
|
-
confidence *= moodCongruenceModifier(encodingAffect, confidenceConfig.retrievalMood, confidenceConfig.affectWeight);
|
|
313
|
-
confidence = Math.max(0, Math.min(1, confidence));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (confidence < minConfidence) continue;
|
|
317
|
-
const score = row.similarity * confidence;
|
|
318
|
-
results.push(buildEpisodicEntry(row, confidence, score, includeProvenance, ctxMatch, moodMatch));
|
|
319
|
-
}
|
|
320
|
-
return results;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters = {}, agentFilter = null) {
|
|
324
|
-
const safeK = safeKForTable(db, 'vec_semantics', candidateK);
|
|
325
|
-
if (safeK === 0) return { results: [], matchedIds: [] };
|
|
326
|
-
const agentClause = agentFilter ? 'AND s.agent = ?' : '';
|
|
327
|
-
const params = agentFilter ? [queryBuffer, safeK, agentFilter] : [queryBuffer, safeK];
|
|
328
|
-
const rows = db.prepare(`
|
|
329
|
-
SELECT s.*, (1.0 - v.distance) AS similarity
|
|
330
|
-
FROM vec_semantics v
|
|
331
|
-
JOIN semantics s ON s.id = v.id
|
|
332
|
-
WHERE v.embedding MATCH ?
|
|
333
|
-
AND k = ?
|
|
334
|
-
${stateClause(includeDormant)}
|
|
335
|
-
${agentClause}
|
|
336
|
-
`).all(...params);
|
|
337
|
-
|
|
338
|
-
const results = [];
|
|
339
|
-
const matchedIds = [];
|
|
340
|
-
for (const row of rows) {
|
|
341
|
-
if (!matchesDateFilters(row.created_at, filters)) continue;
|
|
342
|
-
const confidence = computeSemanticConfidence(row, now, confidenceConfig);
|
|
343
|
-
if (confidence < minConfidence) continue;
|
|
344
|
-
const score = row.similarity * confidence;
|
|
345
|
-
matchedIds.push(row.id);
|
|
346
|
-
results.push(buildSemanticEntry(row, confidence, score, includeProvenance));
|
|
347
|
-
}
|
|
348
|
-
return { results, matchedIds };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters = {}, agentFilter = null) {
|
|
352
|
-
const safeK = safeKForTable(db, 'vec_procedures', candidateK);
|
|
353
|
-
if (safeK === 0) return { results: [], matchedIds: [] };
|
|
354
|
-
const agentClause = agentFilter ? 'AND p.agent = ?' : '';
|
|
355
|
-
const params = agentFilter ? [queryBuffer, safeK, agentFilter] : [queryBuffer, safeK];
|
|
356
|
-
const rows = db.prepare(`
|
|
357
|
-
SELECT p.*, (1.0 - v.distance) AS similarity
|
|
358
|
-
FROM vec_procedures v
|
|
359
|
-
JOIN procedures p ON p.id = v.id
|
|
360
|
-
WHERE v.embedding MATCH ?
|
|
361
|
-
AND k = ?
|
|
362
|
-
${stateClause(includeDormant)}
|
|
363
|
-
${agentClause}
|
|
364
|
-
`).all(...params);
|
|
365
|
-
|
|
366
|
-
const results = [];
|
|
367
|
-
const matchedIds = [];
|
|
368
|
-
for (const row of rows) {
|
|
369
|
-
if (!matchesDateFilters(row.created_at, filters)) continue;
|
|
370
|
-
const confidence = computeProceduralConfidence(row, now, confidenceConfig);
|
|
371
|
-
if (confidence < minConfidence) continue;
|
|
372
|
-
const score = row.similarity * confidence;
|
|
373
|
-
matchedIds.push(row.id);
|
|
374
|
-
results.push(buildProceduralEntry(row, confidence, score, includeProvenance));
|
|
375
|
-
}
|
|
376
|
-
return { results, matchedIds };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async function runRecallQuery(db, embeddingProvider, query, options = {}) {
|
|
380
|
-
const {
|
|
381
|
-
minConfidence = 0,
|
|
382
|
-
types,
|
|
383
|
-
limit = 10,
|
|
384
|
-
includeProvenance = false,
|
|
385
|
-
includeDormant = false,
|
|
386
|
-
confidenceConfig,
|
|
387
|
-
tags,
|
|
388
|
-
sources,
|
|
389
|
-
after,
|
|
390
|
-
before,
|
|
391
|
-
includePrivate = false,
|
|
392
|
-
scope = 'shared',
|
|
393
|
-
agent,
|
|
394
|
-
retrieval = 'hybrid',
|
|
395
|
-
} = options;
|
|
396
|
-
|
|
397
|
-
const searchTypes = types || ['episodic', 'semantic', 'procedural'];
|
|
398
|
-
const now = new Date();
|
|
399
|
-
const agentFilter = scope === 'agent' && agent ? agent : null;
|
|
400
|
-
|
|
401
|
-
// Keyword-only mode: FTS5 search without vector embeddings
|
|
402
|
-
if (retrieval === 'keyword') {
|
|
403
|
-
const ftsAvailable = hasFTSTables(db);
|
|
404
|
-
if (!ftsAvailable) {
|
|
405
|
-
return { top: [], errors: [] };
|
|
406
|
-
}
|
|
407
|
-
const sanitized = sanitizeFTSQuery(query);
|
|
408
|
-
if (!sanitized) return { top: [], errors: [] };
|
|
409
|
-
|
|
410
|
-
const keywordResults = [];
|
|
411
|
-
try {
|
|
412
|
-
if (searchTypes.includes('episodic')) {
|
|
413
|
-
for (const row of searchFTSEpisodes(db, sanitized, limit * 3, agentFilter)) {
|
|
414
|
-
keywordResults.push({ id: row.id, content: row.content, type: 'episodic', score: -row.rank, agent: row.agent || 'default' });
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (searchTypes.includes('semantic')) {
|
|
418
|
-
for (const row of searchFTSSemantics(db, sanitized, limit * 3, agentFilter)) {
|
|
419
|
-
keywordResults.push({ id: row.id, content: row.content, type: 'semantic', score: -row.rank, agent: row.agent || 'default' });
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
if (searchTypes.includes('procedural')) {
|
|
423
|
-
for (const row of searchFTSProcedures(db, sanitized, limit * 3, agentFilter)) {
|
|
424
|
-
keywordResults.push({ id: row.id, content: row.content, type: 'procedural', score: -row.rank, agent: row.agent || 'default' });
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
} catch {
|
|
428
|
-
// FTS query syntax error — fall through with whatever we have
|
|
429
|
-
}
|
|
430
|
-
keywordResults.sort((a, b) => b.score - a.score);
|
|
431
|
-
const top = keywordResults.slice(0, limit).map(entry => ({
|
|
432
|
-
...entry,
|
|
433
|
-
confidence: 1,
|
|
434
|
-
source: 'keyword',
|
|
435
|
-
createdAt: now.toISOString(),
|
|
436
|
-
}));
|
|
437
|
-
return { top, errors: [] };
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const queryVector = await embeddingProvider.embed(query);
|
|
441
|
-
const queryBuffer = embeddingProvider.vectorToBuffer(queryVector);
|
|
442
|
-
const hasFilters = tags?.length || sources?.length || after || before;
|
|
443
|
-
const candidateK = hasFilters ? limit * 5 : limit * 3;
|
|
444
|
-
const filters = { tags, sources, after, before };
|
|
445
|
-
|
|
446
|
-
const allResults = [];
|
|
447
|
-
const errors = [];
|
|
448
|
-
|
|
449
|
-
if (searchTypes.includes('episodic')) {
|
|
450
|
-
try {
|
|
451
|
-
const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters, includePrivate, agentFilter);
|
|
452
|
-
allResults.push(...episodic);
|
|
453
|
-
} catch (err) {
|
|
454
|
-
errors.push({ type: 'episodic', message: err.message });
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (searchTypes.includes('semantic')) {
|
|
459
|
-
try {
|
|
460
|
-
const { results: semResults, matchedIds: semIds } =
|
|
461
|
-
knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters, agentFilter);
|
|
462
|
-
allResults.push(...semResults);
|
|
463
|
-
|
|
464
|
-
if (semIds.length > 0) {
|
|
465
|
-
const nowISO = now.toISOString();
|
|
466
|
-
const placeholders = semIds.map(() => '?').join(',');
|
|
467
|
-
db.prepare(
|
|
468
|
-
`UPDATE semantics SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id IN (${placeholders})`
|
|
469
|
-
).run(nowISO, ...semIds);
|
|
470
|
-
}
|
|
471
|
-
} catch (err) {
|
|
472
|
-
errors.push({ type: 'semantic', message: err.message });
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (searchTypes.includes('procedural')) {
|
|
477
|
-
try {
|
|
478
|
-
const { results: procResults, matchedIds: procIds } =
|
|
479
|
-
knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters, agentFilter);
|
|
480
|
-
allResults.push(...procResults);
|
|
481
|
-
|
|
482
|
-
if (procIds.length > 0) {
|
|
483
|
-
const nowISO = now.toISOString();
|
|
484
|
-
const placeholders = procIds.map(() => '?').join(',');
|
|
485
|
-
db.prepare(
|
|
486
|
-
`UPDATE procedures SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id IN (${placeholders})`
|
|
487
|
-
).run(nowISO, ...procIds);
|
|
488
|
-
}
|
|
489
|
-
} catch (err) {
|
|
490
|
-
errors.push({ type: 'procedural', message: err.message });
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Hybrid mode: merge vector results with FTS5 keyword results via RRF
|
|
495
|
-
if (retrieval === 'hybrid' && hasFTSTables(db)) {
|
|
496
|
-
const sanitized = sanitizeFTSQuery(query);
|
|
497
|
-
if (sanitized) {
|
|
498
|
-
const keywordHits = new Map();
|
|
499
|
-
try {
|
|
500
|
-
if (searchTypes.includes('episodic')) {
|
|
501
|
-
for (const row of searchFTSEpisodes(db, sanitized, limit * 3, agentFilter)) {
|
|
502
|
-
keywordHits.set(row.id, (keywordHits.get(row.id) || 0) + 1);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
if (searchTypes.includes('semantic')) {
|
|
506
|
-
for (const row of searchFTSSemantics(db, sanitized, limit * 3, agentFilter)) {
|
|
507
|
-
keywordHits.set(row.id, (keywordHits.get(row.id) || 0) + 1);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
if (searchTypes.includes('procedural')) {
|
|
511
|
-
for (const row of searchFTSProcedures(db, sanitized, limit * 3, agentFilter)) {
|
|
512
|
-
keywordHits.set(row.id, (keywordHits.get(row.id) || 0) + 1);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
} catch {
|
|
516
|
-
// FTS query error — continue with vector-only results
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// RRF boost: memories found by both vector AND keyword get a score bonus
|
|
520
|
-
const RRF_K = 60;
|
|
521
|
-
if (keywordHits.size > 0) {
|
|
522
|
-
// Rank keyword results by their BM25 order
|
|
523
|
-
const keywordRanks = new Map();
|
|
524
|
-
let rank = 1;
|
|
525
|
-
for (const id of keywordHits.keys()) {
|
|
526
|
-
keywordRanks.set(id, rank++);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
for (const result of allResults) {
|
|
530
|
-
if (keywordRanks.has(result.id)) {
|
|
531
|
-
// Boost score for results found by both vector AND keyword search
|
|
532
|
-
const kRank = keywordRanks.get(result.id);
|
|
533
|
-
const rrfBoost = 1 / (RRF_K + kRank);
|
|
534
|
-
result.score = result.score + rrfBoost;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const top = applyResultGuards(query, allResults, limit);
|
|
542
|
-
return { top, errors };
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* @param {import('better-sqlite3').Database} db
|
|
547
|
-
* @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
|
|
548
|
-
* @param {string} query
|
|
549
|
-
* @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean, tags?: string[], sources?: string[], after?: string, before?: string }} [options]
|
|
550
|
-
* @returns {AsyncGenerator<{ id: string, content: string, type: string, confidence: number, score: number, source: string, createdAt: string }>}
|
|
551
|
-
*/
|
|
552
|
-
export async function* recallStream(db, embeddingProvider, query, options = {}) {
|
|
553
|
-
const { top, errors } = await runRecallQuery(db, embeddingProvider, query, options);
|
|
554
|
-
for (const entry of top) {
|
|
555
|
-
if (errors.length > 0) entry._recallErrors = errors;
|
|
556
|
-
yield entry;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* @param {import('better-sqlite3').Database} db
|
|
562
|
-
* @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
|
|
563
|
-
* @param {string} query
|
|
564
|
-
* @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean, tags?: string[], sources?: string[], after?: string, before?: string }} [options]
|
|
565
|
-
* @returns {Promise<Array<{ id: string, content: string, type: string, confidence: number, score: number, source: string, createdAt: string }>>}
|
|
566
|
-
*/
|
|
567
|
-
export async function recall(db, embeddingProvider, query, options = {}) {
|
|
568
|
-
const { top, errors } = await runRecallQuery(db, embeddingProvider, query, options);
|
|
569
|
-
const results = [...top];
|
|
570
|
-
results.partialFailure = errors.length > 0;
|
|
571
|
-
results.errors = errors;
|
|
572
|
-
return results;
|
|
573
|
-
}
|
package/src/rollback.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { safeJsonParse } from './utils.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @param {import('better-sqlite3').Database} db
|
|
5
|
-
* @returns {Array<{ id: string, checkpoint_cursor: string|null, input_episode_ids: string, output_memory_ids: string, started_at: string, completed_at: string|null, status: string }>}
|
|
6
|
-
*/
|
|
7
|
-
export function getConsolidationHistory(db) {
|
|
8
|
-
return db.prepare(`
|
|
9
|
-
SELECT id, checkpoint_cursor, input_episode_ids, output_memory_ids,
|
|
10
|
-
started_at, completed_at, status
|
|
11
|
-
FROM consolidation_runs ORDER BY started_at DESC
|
|
12
|
-
`).all();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {import('better-sqlite3').Database} db
|
|
17
|
-
* @param {string} runId
|
|
18
|
-
* @returns {{ rolledBackMemories: number, restoredEpisodes: number }}
|
|
19
|
-
*/
|
|
20
|
-
export function rollbackConsolidation(db, runId) {
|
|
21
|
-
const run = db.prepare('SELECT * FROM consolidation_runs WHERE id = ?').get(runId);
|
|
22
|
-
if (!run) throw new Error(`Consolidation run not found: ${runId}`);
|
|
23
|
-
if (run.status === 'rolled_back') throw new Error(`Run already rolled back: ${runId}`);
|
|
24
|
-
|
|
25
|
-
const outputIds = safeJsonParse(run.output_memory_ids, []);
|
|
26
|
-
const inputIds = safeJsonParse(run.input_episode_ids, []);
|
|
27
|
-
|
|
28
|
-
const doRollback = db.transaction(() => {
|
|
29
|
-
const markSemantics = db.prepare('UPDATE semantics SET state = ? WHERE id = ?');
|
|
30
|
-
const markProcedures = db.prepare('UPDATE procedures SET state = ? WHERE id = ?');
|
|
31
|
-
for (const id of outputIds) {
|
|
32
|
-
markSemantics.run('rolled_back', id);
|
|
33
|
-
markProcedures.run('rolled_back', id);
|
|
34
|
-
}
|
|
35
|
-
const unmark = db.prepare('UPDATE episodes SET consolidated = 0 WHERE id = ?');
|
|
36
|
-
for (const id of inputIds) { unmark.run(id); }
|
|
37
|
-
db.prepare('UPDATE consolidation_runs SET status = ? WHERE id = ?').run('rolled_back', runId);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
doRollback();
|
|
41
|
-
return { rolledBackMemories: outputIds.length, restoredEpisodes: inputIds.length };
|
|
42
|
-
}
|
package/src/ulid.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { monotonicFactory } from 'ulid';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
|
|
4
|
-
const monotonic = monotonicFactory();
|
|
5
|
-
|
|
6
|
-
/** @returns {string} */
|
|
7
|
-
export function generateId() {
|
|
8
|
-
return monotonic();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @param {...*} parts
|
|
13
|
-
* @returns {string}
|
|
14
|
-
*/
|
|
15
|
-
export function generateDeterministicId(...parts) {
|
|
16
|
-
const input = JSON.stringify(parts);
|
|
17
|
-
return createHash('sha256').update(input).digest('hex').slice(0, 26);
|
|
18
|
-
}
|
package/src/utils.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @param {Buffer} bufA
|
|
3
|
-
* @param {Buffer} bufB
|
|
4
|
-
* @param {import('./embedding.js').EmbeddingProvider} provider
|
|
5
|
-
* @returns {number}
|
|
6
|
-
*/
|
|
7
|
-
export function cosineSimilarity(bufA, bufB, provider) {
|
|
8
|
-
const a = provider.bufferToVector(bufA);
|
|
9
|
-
const b = provider.bufferToVector(bufB);
|
|
10
|
-
let dot = 0, magA = 0, magB = 0;
|
|
11
|
-
for (let i = 0; i < a.length; i++) {
|
|
12
|
-
dot += a[i] * b[i];
|
|
13
|
-
magA += a[i] * a[i];
|
|
14
|
-
magB += b[i] * b[i];
|
|
15
|
-
}
|
|
16
|
-
const mag = Math.sqrt(magA) * Math.sqrt(magB);
|
|
17
|
-
return mag === 0 ? 0 : dot / mag;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {string} dateStr
|
|
22
|
-
* @param {Date} now
|
|
23
|
-
* @returns {number}
|
|
24
|
-
*/
|
|
25
|
-
export function daysBetween(dateStr, now) {
|
|
26
|
-
return Math.max(0, (now.getTime() - new Date(dateStr).getTime()) / (1000 * 60 * 60 * 24));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {string | null | undefined} str
|
|
31
|
-
* @param {*} [fallback=null]
|
|
32
|
-
* @returns {*}
|
|
33
|
-
*/
|
|
34
|
-
export function safeJsonParse(str, fallback = null) {
|
|
35
|
-
if (!str) return fallback;
|
|
36
|
-
try { return JSON.parse(str); }
|
|
37
|
-
catch { return fallback; }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string | undefined | null} apiKey
|
|
42
|
-
* @param {string} operation
|
|
43
|
-
* @param {string} envVar
|
|
44
|
-
* @returns {void}
|
|
45
|
-
*/
|
|
46
|
-
export function requireApiKey(apiKey, operation, envVar) {
|
|
47
|
-
if (typeof apiKey !== 'string' || apiKey.trim() === '') {
|
|
48
|
-
throw new Error(`${operation} requires ${envVar}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {{ status: number, text: () => Promise<string> }} response
|
|
54
|
-
* @returns {Promise<string>}
|
|
55
|
-
*/
|
|
56
|
-
export async function describeHttpError(response) {
|
|
57
|
-
if (typeof response.text !== 'function') {
|
|
58
|
-
return `${response.status}`;
|
|
59
|
-
}
|
|
60
|
-
const body = await response.text().catch(() => '');
|
|
61
|
-
const normalized = body.replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
62
|
-
return normalized ? `${response.status} ${normalized}` : `${response.status}`;
|
|
63
|
-
}
|