@b4run/memory 0.8.28
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/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/browse-budget.d.ts +40 -0
- package/dist/browse-budget.d.ts.map +1 -0
- package/dist/browse-budget.js +57 -0
- package/dist/browse-cursor.d.ts +22 -0
- package/dist/browse-cursor.d.ts.map +1 -0
- package/dist/browse-cursor.js +146 -0
- package/dist/browse-filter.d.ts +13 -0
- package/dist/browse-filter.d.ts.map +1 -0
- package/dist/browse-filter.js +16 -0
- package/dist/browse-order.d.ts +28 -0
- package/dist/browse-order.d.ts.map +1 -0
- package/dist/browse-order.js +37 -0
- package/dist/browse-range.d.ts +16 -0
- package/dist/browse-range.d.ts.map +1 -0
- package/dist/browse-range.js +52 -0
- package/dist/browse-validate.d.ts +31 -0
- package/dist/browse-validate.d.ts.map +1 -0
- package/dist/browse-validate.js +263 -0
- package/dist/browse.d.ts +15 -0
- package/dist/browse.d.ts.map +1 -0
- package/dist/browse.js +5 -0
- package/dist/distill.d.ts +81 -0
- package/dist/distill.d.ts.map +1 -0
- package/dist/distill.js +380 -0
- package/dist/hybrid.d.ts +26 -0
- package/dist/hybrid.d.ts.map +1 -0
- package/dist/hybrid.js +86 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/namespace.d.ts +18 -0
- package/dist/namespace.d.ts.map +1 -0
- package/dist/namespace.js +68 -0
- package/dist/reconcile.d.ts +51 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +110 -0
- package/dist/score.d.ts +54 -0
- package/dist/score.d.ts.map +1 -0
- package/dist/score.js +66 -0
- package/dist/sqlite-browse-sql.d.ts +30 -0
- package/dist/sqlite-browse-sql.d.ts.map +1 -0
- package/dist/sqlite-browse-sql.js +178 -0
- package/dist/sqlite-store.d.ts +10 -0
- package/dist/sqlite-store.d.ts.map +1 -0
- package/dist/sqlite-store.js +521 -0
- package/dist/tokenize.d.ts +3 -0
- package/dist/tokenize.d.ts.map +1 -0
- package/dist/tokenize.js +14 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +201 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/vector.d.ts +22 -0
- package/dist/vector.d.ts.map +1 -0
- package/dist/vector.js +42 -0
- package/package.json +67 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/** Largest `limit` the UNTRUSTED boundary accepts. Enforced only when a caller passes
|
|
2
|
+
* `maxLimit` — in-process callers (the CLI's 10 000-row consolidation scan) are
|
|
3
|
+
* trusted and exempt; the HTTP route is not. */
|
|
4
|
+
export const BROWSE_MAX_LIMIT = 1000;
|
|
5
|
+
/** Applied by the stores when `limit` is absent. */
|
|
6
|
+
export const BROWSE_DEFAULT_LIMIT = 50;
|
|
7
|
+
const MAX_STRING_BYTES = 1024;
|
|
8
|
+
const MAX_CURSOR_CHARS = 4096;
|
|
9
|
+
/** Never the deciding constraint: one filter per field over six fields already rejects a
|
|
10
|
+
* seventh. This is the fast-fail on an enormous array from an untrusted body, taken
|
|
11
|
+
* before the per-filter loop walks it. */
|
|
12
|
+
const MAX_FILTERS = 8;
|
|
13
|
+
const MAX_ORDER_BY = 3;
|
|
14
|
+
const ENCODER = new TextEncoder();
|
|
15
|
+
export const BROWSE_SORT_FIELDS = [
|
|
16
|
+
"updatedAt",
|
|
17
|
+
"createdAt",
|
|
18
|
+
"confidence",
|
|
19
|
+
"namespace",
|
|
20
|
+
"kind",
|
|
21
|
+
"status",
|
|
22
|
+
];
|
|
23
|
+
const STATUSES = ["candidate", "active", "superseded"];
|
|
24
|
+
const KINDS = [
|
|
25
|
+
"semantic",
|
|
26
|
+
"episodic",
|
|
27
|
+
"procedural",
|
|
28
|
+
"reflection",
|
|
29
|
+
];
|
|
30
|
+
const SOURCE_TYPES = [
|
|
31
|
+
"run",
|
|
32
|
+
"user",
|
|
33
|
+
"tool",
|
|
34
|
+
"eval",
|
|
35
|
+
"human",
|
|
36
|
+
];
|
|
37
|
+
const FILTER_FIELDS = [
|
|
38
|
+
"status",
|
|
39
|
+
"kind",
|
|
40
|
+
"content",
|
|
41
|
+
"namespace",
|
|
42
|
+
"confidence",
|
|
43
|
+
"updatedAt",
|
|
44
|
+
];
|
|
45
|
+
const _listsSpellOutTheirUnions = [true, true, true, true, true];
|
|
46
|
+
void _listsSpellOutTheirUnions;
|
|
47
|
+
const CONTENT_OPS = [
|
|
48
|
+
"contains",
|
|
49
|
+
"notContains",
|
|
50
|
+
"equals",
|
|
51
|
+
"notEquals",
|
|
52
|
+
"startsWith",
|
|
53
|
+
"endsWith",
|
|
54
|
+
];
|
|
55
|
+
const NAMESPACE_OPS = ["equals", "startsWith"];
|
|
56
|
+
const CONFIDENCE_OPS = ["eq", "neq", "gt", "gte", "lt", "lte", "between"];
|
|
57
|
+
const UPDATED_AT_OPS = ["onDay", "beforeDay", "afterDay", "betweenDays"];
|
|
58
|
+
const SET_OPS = ["in", "notIn"];
|
|
59
|
+
const ISO_Z = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
60
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
61
|
+
/** Every rejection this module raises. The Inspector maps it to 400 `{error}`; the
|
|
62
|
+
* stores let it propagate, so a bad query fails loudly instead of silently matching
|
|
63
|
+
* zero rows. */
|
|
64
|
+
export class BrowseQueryError extends Error {
|
|
65
|
+
code;
|
|
66
|
+
constructor(message, code = "invalid-query") {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = "BrowseQueryError";
|
|
69
|
+
this.code = code;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function fail(message) {
|
|
73
|
+
throw new BrowseQueryError(message);
|
|
74
|
+
}
|
|
75
|
+
function checkString(value, label) {
|
|
76
|
+
if (typeof value !== "string")
|
|
77
|
+
fail(`${label} must be a string`);
|
|
78
|
+
if (value.length === 0)
|
|
79
|
+
fail(`${label} must not be empty`);
|
|
80
|
+
// Every character costs at least one UTF-8 byte, so a too-long string is already
|
|
81
|
+
// too many bytes — decided without encoding a multi-megabyte untrusted body.
|
|
82
|
+
if (value.length > MAX_STRING_BYTES || ENCODER.encode(value).length > MAX_STRING_BYTES)
|
|
83
|
+
fail(`${label} must be at most ${MAX_STRING_BYTES} bytes`);
|
|
84
|
+
}
|
|
85
|
+
function checkFinite(value, label) {
|
|
86
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
87
|
+
fail(`${label} must be a finite number`);
|
|
88
|
+
}
|
|
89
|
+
function checkInstant(value, label) {
|
|
90
|
+
// Full-ISO-Z only: the stores compare these TEXT columns lexicographically, so a
|
|
91
|
+
// shorter or offset form silently windows wrong rather than failing.
|
|
92
|
+
if (typeof value !== "string" || !ISO_Z.test(value))
|
|
93
|
+
fail(`${label} must be a full ISO-8601 UTC instant ("YYYY-MM-DDTHH:MM:SS.sssZ")`);
|
|
94
|
+
// Out-of-range components ROLL OVER rather than fail: "2026-02-31T…" parses as March 3
|
|
95
|
+
// and "…T24:00:00.000Z" as the next day. Both spellings then sort against the stored
|
|
96
|
+
// text at the wrong place, so only a string that round-trips names the instant it reads as.
|
|
97
|
+
const parsed = Date.parse(value);
|
|
98
|
+
if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value)
|
|
99
|
+
fail(`${label} "${value}" is not a real UTC instant`);
|
|
100
|
+
}
|
|
101
|
+
function checkDay(value, label) {
|
|
102
|
+
if (typeof value !== "string" || !DAY.test(value))
|
|
103
|
+
fail(`${label} must be a "YYYY-MM-DD" UTC day`);
|
|
104
|
+
const parsed = Date.parse(`${value}T00:00:00.000Z`);
|
|
105
|
+
if (!Number.isFinite(parsed) || new Date(parsed).toISOString().slice(0, 10) !== value)
|
|
106
|
+
fail(`${label} "${value}" is not a real calendar day`);
|
|
107
|
+
}
|
|
108
|
+
function checkEnum(value, allowed, label) {
|
|
109
|
+
if (typeof value !== "string" || !allowed.includes(value))
|
|
110
|
+
fail(`invalid ${label} ${JSON.stringify(value)} (expected one of: ${allowed.join(", ")})`);
|
|
111
|
+
}
|
|
112
|
+
function checkEnumList(value, allowed, label) {
|
|
113
|
+
const values = typeof value === "string" ? [value] : value;
|
|
114
|
+
if (!Array.isArray(values))
|
|
115
|
+
fail(`${label} must be a value or an array of values`);
|
|
116
|
+
for (const entry of values)
|
|
117
|
+
checkEnum(entry, allowed, label);
|
|
118
|
+
}
|
|
119
|
+
function checkOp(op, allowed, field) {
|
|
120
|
+
if (typeof op !== "string" || !allowed.includes(op))
|
|
121
|
+
fail(`unknown op ${JSON.stringify(op)} for filter field "${field}" (expected one of: ${allowed.join(", ")})`);
|
|
122
|
+
return op;
|
|
123
|
+
}
|
|
124
|
+
function validateFilter(raw, seen) {
|
|
125
|
+
const filter = raw;
|
|
126
|
+
const field = filter?.field;
|
|
127
|
+
if (typeof field !== "string" || !FILTER_FIELDS.includes(field))
|
|
128
|
+
fail(`unknown filter field ${JSON.stringify(field)} (expected one of: ${FILTER_FIELDS.join(", ")})`);
|
|
129
|
+
if (seen.has(field))
|
|
130
|
+
fail(`at most one filter per field; "${field}" appears twice`);
|
|
131
|
+
seen.add(field);
|
|
132
|
+
switch (field) {
|
|
133
|
+
case "status":
|
|
134
|
+
case "kind": {
|
|
135
|
+
checkOp(filter.op, SET_OPS, field);
|
|
136
|
+
const values = filter.values;
|
|
137
|
+
if (!Array.isArray(values))
|
|
138
|
+
fail(`${field} values must be an array`);
|
|
139
|
+
// An empty list can only be a bug: an inactive filter is never sent.
|
|
140
|
+
if (values.length === 0)
|
|
141
|
+
fail(`${field} values must not be empty`);
|
|
142
|
+
checkEnumList(values, field === "status" ? STATUSES : KINDS, field);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
case "content": {
|
|
146
|
+
checkOp(filter.op, CONTENT_OPS, field);
|
|
147
|
+
checkString(filter.value, "content value");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
case "namespace": {
|
|
151
|
+
checkOp(filter.op, NAMESPACE_OPS, field);
|
|
152
|
+
checkString(filter.value, "namespace value");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
case "confidence": {
|
|
156
|
+
const op = checkOp(filter.op, CONFIDENCE_OPS, field);
|
|
157
|
+
if (op === "between") {
|
|
158
|
+
const { min, max } = filter;
|
|
159
|
+
checkFinite(min, "confidence min");
|
|
160
|
+
checkFinite(max, "confidence max");
|
|
161
|
+
if (min > max)
|
|
162
|
+
fail("confidence between requires min <= max");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
checkFinite(filter.value, "confidence value");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
default: {
|
|
169
|
+
const op = checkOp(filter.op, UPDATED_AT_OPS, field);
|
|
170
|
+
if (op === "betweenDays") {
|
|
171
|
+
const { fromDay, untilDay } = filter;
|
|
172
|
+
checkDay(fromDay, "updatedAt fromDay");
|
|
173
|
+
checkDay(untilDay, "updatedAt untilDay");
|
|
174
|
+
// "YYYY-MM-DD" is uniform-width ASCII, so lexicographic order IS chronological.
|
|
175
|
+
if (fromDay > untilDay)
|
|
176
|
+
fail("updatedAt betweenDays requires fromDay <= untilDay");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
checkDay(filter.day, "updatedAt day");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The single reading of "is this browse query legal". Runs at the Inspector HTTP
|
|
185
|
+
* boundary (mapped to 400) and defensively inside every store (thrown). Pass
|
|
186
|
+
* `maxLimit` at untrusted boundaries only — see BROWSE_MAX_LIMIT.
|
|
187
|
+
*
|
|
188
|
+
* The empty set is spelled two ways and they do NOT mean the same thing. The shorthand
|
|
189
|
+
* `status: []` / `kind: []` is legal and means "match nothing" (see `BrowseQuery`), while
|
|
190
|
+
* `filters: [{ field: "status", op: "in", values: [] }]` is rejected: a filter entry exists
|
|
191
|
+
* only because a caller constructed one, so an empty value list is a construction bug
|
|
192
|
+
* rather than a narrowing. A caller translating UI state must use the shorthand to say
|
|
193
|
+
* "narrowed to nothing".
|
|
194
|
+
*/
|
|
195
|
+
export function validateBrowseQuery(query, opts = {}) {
|
|
196
|
+
// The query may arrive from JSON, so every field is treated as unknown.
|
|
197
|
+
const q = query;
|
|
198
|
+
const limit = q.limit;
|
|
199
|
+
if (limit !== undefined) {
|
|
200
|
+
if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1)
|
|
201
|
+
fail("limit must be an integer >= 1");
|
|
202
|
+
if (opts.maxLimit !== undefined && limit > opts.maxLimit)
|
|
203
|
+
fail(`limit must be at most ${opts.maxLimit}`);
|
|
204
|
+
}
|
|
205
|
+
const offset = q.offset;
|
|
206
|
+
if (offset !== undefined) {
|
|
207
|
+
if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)
|
|
208
|
+
fail("offset must be an integer >= 0");
|
|
209
|
+
}
|
|
210
|
+
if (q.cursor !== undefined) {
|
|
211
|
+
if (typeof q.cursor !== "string" || q.cursor.length === 0)
|
|
212
|
+
fail("cursor must be a non-empty string");
|
|
213
|
+
if (q.cursor.length > MAX_CURSOR_CHARS)
|
|
214
|
+
fail(`cursor must be at most ${MAX_CURSOR_CHARS} characters`);
|
|
215
|
+
if (offset !== undefined && offset !== 0)
|
|
216
|
+
fail("cursor and a non-zero offset cannot be combined — a keyset continuation already carries the position");
|
|
217
|
+
}
|
|
218
|
+
if (q.namespace !== undefined)
|
|
219
|
+
checkString(q.namespace, "namespace");
|
|
220
|
+
if (q.namespacePrefix !== undefined)
|
|
221
|
+
checkString(q.namespacePrefix, "namespacePrefix");
|
|
222
|
+
if (q.since !== undefined)
|
|
223
|
+
checkInstant(q.since, "since");
|
|
224
|
+
if (q.until !== undefined)
|
|
225
|
+
checkInstant(q.until, "until");
|
|
226
|
+
if (q.now !== undefined)
|
|
227
|
+
checkInstant(q.now, "now");
|
|
228
|
+
if (q.status !== undefined)
|
|
229
|
+
checkEnumList(q.status, STATUSES, "status");
|
|
230
|
+
if (q.kind !== undefined)
|
|
231
|
+
checkEnumList(q.kind, KINDS, "kind");
|
|
232
|
+
// Scalar, unlike status/kind: every store binds it as ONE parameter, so an array
|
|
233
|
+
// reaches the driver as a bad bind rather than a set match.
|
|
234
|
+
if (q.sourceType !== undefined)
|
|
235
|
+
checkEnum(q.sourceType, SOURCE_TYPES, "sourceType");
|
|
236
|
+
if (q.filters !== undefined) {
|
|
237
|
+
if (!Array.isArray(q.filters))
|
|
238
|
+
fail("filters must be an array");
|
|
239
|
+
if (q.filters.length > MAX_FILTERS)
|
|
240
|
+
fail(`at most ${MAX_FILTERS} filters`);
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
for (const filter of q.filters)
|
|
243
|
+
validateFilter(filter, seen);
|
|
244
|
+
}
|
|
245
|
+
if (q.orderBy !== undefined) {
|
|
246
|
+
if (!Array.isArray(q.orderBy))
|
|
247
|
+
fail("orderBy must be an array");
|
|
248
|
+
if (q.orderBy.length > MAX_ORDER_BY)
|
|
249
|
+
fail(`at most ${MAX_ORDER_BY} orderBy entries`);
|
|
250
|
+
const seenFields = new Set();
|
|
251
|
+
for (const raw of q.orderBy) {
|
|
252
|
+
const entry = raw;
|
|
253
|
+
const field = entry?.field;
|
|
254
|
+
if (typeof field !== "string" || !BROWSE_SORT_FIELDS.includes(field))
|
|
255
|
+
fail(`unknown sort field ${JSON.stringify(field)} (expected one of: ${BROWSE_SORT_FIELDS.join(", ")})`);
|
|
256
|
+
if (entry.dir !== "asc" && entry.dir !== "desc")
|
|
257
|
+
fail(`sort direction must be "asc" or "desc", got ${JSON.stringify(entry.dir)}`);
|
|
258
|
+
if (seenFields.has(field))
|
|
259
|
+
fail(`orderBy repeats the field "${field}"`);
|
|
260
|
+
seenFields.add(field);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
package/dist/browse.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PURE browse contract: types, validation, the sort whitelist, range math and the
|
|
3
|
+
* cursor codec. Deliberately imports nothing from `sqlite-store.ts`, so importing
|
|
4
|
+
* `@b4run/memory/browse` never pulls `node:sqlite` — bundled server routes and
|
|
5
|
+
* browser code can both use it.
|
|
6
|
+
*/
|
|
7
|
+
export type { BrowseCursorPayload, BrowseCursorValue } from "./browse-cursor.js";
|
|
8
|
+
export { BROWSE_CURSOR_VERSION, browseCursorKey, browseQueryFingerprint, decodeBrowseCursor, encodeBrowseCursor, } from "./browse-cursor.js";
|
|
9
|
+
export { normalizeSetFilter } from "./browse-filter.js";
|
|
10
|
+
export type { ResolvedBrowseSort } from "./browse-order.js";
|
|
11
|
+
export { DEFAULT_BROWSE_ORDER, resolveBrowseOrder } from "./browse-order.js";
|
|
12
|
+
export { namespacePrefixUpperBound, utcDayAfter, utcDayStart } from "./browse-range.js";
|
|
13
|
+
export { BROWSE_DEFAULT_LIMIT, BROWSE_MAX_LIMIT, BROWSE_SORT_FIELDS, BrowseQueryError, validateBrowseQuery, } from "./browse-validate.js";
|
|
14
|
+
export type { BrowseFilter, BrowsePage, BrowseQuery, BrowseSortEntry, BrowseSortField, MemoryKind, MemoryRecord, MemorySource, MemoryStatus, } from "./types.js";
|
|
15
|
+
//# sourceMappingURL=browse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../src/browse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAChF,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC5E,OAAO,EAAE,yBAAyB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACvF,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACV,YAAY,EACZ,UAAU,EACV,WAAW,EACX,eAAe,EACf,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,YAAY,GACb,MAAM,YAAY,CAAA"}
|
package/dist/browse.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { BROWSE_CURSOR_VERSION, browseCursorKey, browseQueryFingerprint, decodeBrowseCursor, encodeBrowseCursor, } from "./browse-cursor.js";
|
|
2
|
+
export { normalizeSetFilter } from "./browse-filter.js";
|
|
3
|
+
export { DEFAULT_BROWSE_ORDER, resolveBrowseOrder } from "./browse-order.js";
|
|
4
|
+
export { namespacePrefixUpperBound, utcDayAfter, utcDayStart } from "./browse-range.js";
|
|
5
|
+
export { BROWSE_DEFAULT_LIMIT, BROWSE_MAX_LIMIT, BROWSE_SORT_FIELDS, BrowseQueryError, validateBrowseQuery, } from "./browse-validate.js";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { MemoryRecord } from "./types.js";
|
|
2
|
+
/** Event time for distillation ordering/grouping: when it happened, not when the row moved. */
|
|
3
|
+
export declare function eventTimeOf(record: MemoryRecord): string;
|
|
4
|
+
/** ISO-week key (UTC): "<isoYear>-W<isoWeek>", so a batch is one namespace-week. */
|
|
5
|
+
export declare function isoWeekKey(iso: string): string;
|
|
6
|
+
export interface ConsolidationBatch {
|
|
7
|
+
readonly namespace: string;
|
|
8
|
+
readonly period: {
|
|
9
|
+
readonly since: string;
|
|
10
|
+
readonly until: string;
|
|
11
|
+
};
|
|
12
|
+
readonly records: readonly MemoryRecord[];
|
|
13
|
+
}
|
|
14
|
+
/** Group active episodic records into per-(namespace, ISO week) batches, ordered by
|
|
15
|
+
* event time; groups below minBatchSize are dropped (summarizing 2 runs is noise),
|
|
16
|
+
* groups above maxBatchSize are chunked. Pure: the caller filters by age/status. */
|
|
17
|
+
export declare function selectConsolidationBatches(records: readonly MemoryRecord[], opts: {
|
|
18
|
+
readonly minBatchSize: number;
|
|
19
|
+
readonly maxBatchSize: number;
|
|
20
|
+
}): ConsolidationBatch[];
|
|
21
|
+
export interface ReflectionInput {
|
|
22
|
+
readonly namespace: string;
|
|
23
|
+
readonly records: readonly MemoryRecord[];
|
|
24
|
+
/** The newest event time covered — becomes the next pass's watermark. */
|
|
25
|
+
readonly coveredUntil: string;
|
|
26
|
+
}
|
|
27
|
+
/** Records strictly newer than the watermark, newest-capped then re-sorted ascending.
|
|
28
|
+
* Returns null below the threshold — that null is what makes `b4 memory reflect`
|
|
29
|
+
* a cheap no-op for cron. Callers pass records from ONE namespace. */
|
|
30
|
+
export declare function selectReflectionInput(records: readonly MemoryRecord[], opts: {
|
|
31
|
+
readonly minNewRecords: number;
|
|
32
|
+
readonly maxRecords: number;
|
|
33
|
+
readonly coveredUntil?: string;
|
|
34
|
+
}): ReflectionInput | null;
|
|
35
|
+
export declare function buildConsolidationPrompt(batch: ConsolidationBatch): string;
|
|
36
|
+
export declare function buildReflectionPrompt(input: ReflectionInput): string;
|
|
37
|
+
export declare function parseConsolidationOutput(raw: string): {
|
|
38
|
+
summary: string;
|
|
39
|
+
};
|
|
40
|
+
export interface ReflectionInsight {
|
|
41
|
+
readonly insight: string;
|
|
42
|
+
readonly confidence: number;
|
|
43
|
+
readonly tags: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/** Deliberate leniency asymmetry: a missing/garbage `confidence` falls back to
|
|
46
|
+
* 0.5 and non-string tags are dropped (cosmetic fields — don't fail a whole
|
|
47
|
+
* batch over them), while a missing `insight` throws (that IS the payload). */
|
|
48
|
+
export declare function parseReflectionOutput(raw: string): {
|
|
49
|
+
insights: ReflectionInsight[];
|
|
50
|
+
};
|
|
51
|
+
/** One summary per batch: the id is derived, not random, so re-consolidating the
|
|
52
|
+
* SAME batch overwrites its own summary instead of piling up duplicates — the
|
|
53
|
+
* idempotency the engine relies on.
|
|
54
|
+
* The source ids are part of the hash because (namespace, period) alone is NOT
|
|
55
|
+
* unique: when every record in a namespace-week shares an exactly equal event
|
|
56
|
+
* time (bulk import, backfill) and maxBatchSize splits them, each chunk derives
|
|
57
|
+
* the same since/until (t and t+1ms) — two distinct batches, one id, and the
|
|
58
|
+
* second summary would silently overwrite the first. Hashing the chunk's own
|
|
59
|
+
* record ids disambiguates them and still yields a stable id for an identical
|
|
60
|
+
* re-run. Covered by "gives same-period chunks distinct summary ids". */
|
|
61
|
+
export declare function buildSummaryRecord(batch: ConsolidationBatch, summary: string, now: string, opts?: {
|
|
62
|
+
readonly ttlMs?: number;
|
|
63
|
+
}): MemoryRecord;
|
|
64
|
+
/** A pass that legitimately yields NO durable insight still did the work, and the
|
|
65
|
+
* watermark is the only place that fact can live. Without this record the
|
|
66
|
+
* namespace is re-selected — and re-PAID for — on every subsequent cron run,
|
|
67
|
+
* forever, because `readWatermark` finds nothing to advance past.
|
|
68
|
+
* It is written `superseded` on purpose: `recall` sees only active/candidate
|
|
69
|
+
* rows, so the sentinel can never surface as a fake insight, while `browse`
|
|
70
|
+
* (which readWatermark uses, and which does not filter by status unless asked)
|
|
71
|
+
* still finds it. The id is derived from (namespace, coveredUntil) only — an
|
|
72
|
+
* identical re-run overwrites its own sentinel instead of piling up.
|
|
73
|
+
* Covered by "a zero-insight pass still advances the watermark" (distill-engine). */
|
|
74
|
+
export declare function buildReflectionWatermarkRecord(input: ReflectionInput, now: string): MemoryRecord;
|
|
75
|
+
/** The id hashes (namespace, coveredUntil, insight) — so the SAME insight text in
|
|
76
|
+
* the SAME pass is one record, not two (the engine's put dedupes it), while a
|
|
77
|
+
* later pass with a newer watermark restates it as a distinct record. */
|
|
78
|
+
export declare function buildReflectionRecords(input: ReflectionInput, insights: readonly ReflectionInsight[], now: string, opts: {
|
|
79
|
+
readonly status: "candidate" | "active";
|
|
80
|
+
}): MemoryRecord[];
|
|
81
|
+
//# sourceMappingURL=distill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"distill.d.ts","sourceRoot":"","sources":["../src/distill.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAExD;AAED,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAW9C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACnE,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;CAC1C;AAED;;qFAEqF;AACrF,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,IAAI,EAAE;IAAE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACrE,kBAAkB,EAAE,CAsCtB;AAOD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IACzC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC9B;AAED;;uEAEuE;AACvE,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,IAAI,EAAE;IACJ,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B,GACA,eAAe,GAAG,IAAI,CAWxB;AAiDD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAiB1E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAiBpE;AAyFD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAQzE;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CACjC;AAED;;gFAEgF;AAChF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAA;CAAE,CAwBpF;AAOD;;;;;;;;;0EAS0E;AAC1E,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC,YAAY,CAgCd;AAED;;;;;;;;;sFASsF;AACtF,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAkBhG;AAED;;0EAE0E;AAC1E,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,SAAS,iBAAiB,EAAE,EACtC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAA;CAAE,GAChD,YAAY,EAAE,CAoBhB"}
|