memory-braid 0.6.1 → 0.7.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.
@@ -0,0 +1,292 @@
1
+ import * as chrono from "chrono-node";
2
+ import { normalizeWhitespace } from "./chunking.js";
3
+ import { asRecord, asString } from "./memory-model.js";
4
+ import type { MemoryBraidResult } from "./types.js";
5
+
6
+ const SPANISH_MONTH_MAP: Record<string, string> = {
7
+ enero: "January",
8
+ febrero: "February",
9
+ marzo: "March",
10
+ abril: "April",
11
+ mayo: "May",
12
+ junio: "June",
13
+ julio: "July",
14
+ agosto: "August",
15
+ septiembre: "September",
16
+ setiembre: "September",
17
+ octubre: "October",
18
+ noviembre: "November",
19
+ diciembre: "December",
20
+ };
21
+
22
+ const MONTH_INDEX_BY_ENGLISH: Record<string, number> = {
23
+ january: 0,
24
+ february: 1,
25
+ march: 2,
26
+ april: 3,
27
+ may: 4,
28
+ june: 5,
29
+ july: 6,
30
+ august: 7,
31
+ september: 8,
32
+ october: 9,
33
+ november: 10,
34
+ december: 11,
35
+ };
36
+
37
+ export type TimeRange = {
38
+ startMs: number;
39
+ endMs: number;
40
+ startDate: string;
41
+ endDate: string;
42
+ label: string;
43
+ matchedText?: string;
44
+ };
45
+
46
+ type NormalizedQuery = {
47
+ normalizedMatchedText?: string;
48
+ matchedOriginalText?: string;
49
+ explicitYear: boolean;
50
+ monthLevel: boolean;
51
+ };
52
+
53
+ function monthRange(year: number, monthIndex: number): TimeRange {
54
+ const start = new Date(Date.UTC(year, monthIndex, 1, 0, 0, 0, 0));
55
+ const end = new Date(Date.UTC(year, monthIndex + 1, 0, 23, 59, 59, 999));
56
+ return {
57
+ startMs: start.getTime(),
58
+ endMs: end.getTime(),
59
+ startDate: start.toISOString().slice(0, 10),
60
+ endDate: end.toISOString().slice(0, 10),
61
+ label: `${start.toLocaleString("en-US", { month: "long", timeZone: "UTC" })} ${year}`,
62
+ };
63
+ }
64
+
65
+ function cleanQueryAfterTimeRemoval(query: string): string {
66
+ return normalizeWhitespace(query).replace(/\s+([?.!,;:])/g, "$1");
67
+ }
68
+
69
+ function explicitRange(start: string, end: string): TimeRange | undefined {
70
+ const startDate = Date.parse(`${start}T00:00:00.000Z`);
71
+ const endDate = Date.parse(`${end}T23:59:59.999Z`);
72
+ if (!Number.isFinite(startDate) || !Number.isFinite(endDate) || endDate < startDate) {
73
+ return undefined;
74
+ }
75
+ return {
76
+ startMs: startDate,
77
+ endMs: endDate,
78
+ startDate: new Date(startDate).toISOString().slice(0, 10),
79
+ endDate: new Date(endDate).toISOString().slice(0, 10),
80
+ label: `${start}..${end}`,
81
+ };
82
+ }
83
+
84
+ function normalizeSpanishMonthPhrase(query: string): NormalizedQuery | undefined {
85
+ const relativeMonth = /\b(?:este mes)\b/i.exec(query);
86
+ if (relativeMonth) {
87
+ return {
88
+ normalizedMatchedText: "this month",
89
+ matchedOriginalText: relativeMonth[0],
90
+ explicitYear: false,
91
+ monthLevel: true,
92
+ };
93
+ }
94
+
95
+ const lastMonth = /\b(?:el\s+mes\s+pasado|mes\s+pasado)\b/i.exec(query);
96
+ if (lastMonth) {
97
+ return {
98
+ normalizedMatchedText: "last month",
99
+ matchedOriginalText: lastMonth[0],
100
+ explicitYear: false,
101
+ monthLevel: true,
102
+ };
103
+ }
104
+
105
+ const byMonth = /\ben\s+(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|setiembre|octubre|noviembre|diciembre)(?:\s+(?:de|del))?(?:\s+(\d{4}))?\b/i.exec(
106
+ query,
107
+ );
108
+ if (!byMonth) {
109
+ return undefined;
110
+ }
111
+ const englishMonth = SPANISH_MONTH_MAP[byMonth[1].toLowerCase()];
112
+ if (!englishMonth) {
113
+ return undefined;
114
+ }
115
+ const replacement = byMonth[2] ? `in ${englishMonth} ${byMonth[2]}` : `in ${englishMonth}`;
116
+ return {
117
+ normalizedMatchedText: replacement,
118
+ matchedOriginalText: byMonth[0],
119
+ explicitYear: Boolean(byMonth[2]),
120
+ monthLevel: true,
121
+ };
122
+ }
123
+
124
+ function normalizeTimeQuery(query: string): NormalizedQuery {
125
+ const normalized = normalizeWhitespace(query);
126
+ const spanish = normalizeSpanishMonthPhrase(normalized);
127
+ if (spanish) {
128
+ return spanish;
129
+ }
130
+
131
+ const englishMonth = /\bin\s+(january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+(\d{4}))?\b/i.exec(
132
+ normalized,
133
+ );
134
+ if (englishMonth) {
135
+ return {
136
+ normalizedMatchedText: englishMonth[0],
137
+ matchedOriginalText: englishMonth[0],
138
+ explicitYear: Boolean(englishMonth[2]),
139
+ monthLevel: true,
140
+ };
141
+ }
142
+
143
+ const thisMonth = /\bthis month\b/i.exec(normalized);
144
+ if (thisMonth) {
145
+ return {
146
+ normalizedMatchedText: thisMonth[0],
147
+ matchedOriginalText: thisMonth[0],
148
+ explicitYear: false,
149
+ monthLevel: true,
150
+ };
151
+ }
152
+
153
+ const lastMonth = /\blast month\b/i.exec(normalized);
154
+ if (lastMonth) {
155
+ return {
156
+ normalizedMatchedText: lastMonth[0],
157
+ matchedOriginalText: lastMonth[0],
158
+ explicitYear: false,
159
+ monthLevel: true,
160
+ };
161
+ }
162
+
163
+ return {
164
+ explicitYear: false,
165
+ monthLevel: false,
166
+ };
167
+ }
168
+
169
+ function inferMonthRangeFromChrono(params: {
170
+ result: chrono.ParsingResult;
171
+ matchedText?: string;
172
+ detectionText?: string;
173
+ explicitYear: boolean;
174
+ now: Date;
175
+ }): TimeRange | undefined {
176
+ const parsedDate = params.result.start?.date();
177
+ if (!parsedDate) {
178
+ return undefined;
179
+ }
180
+ const detectionText = (params.detectionText ?? params.result.text ?? "").toLowerCase();
181
+ const relative = /\bthis month\b|\blast month\b/i.test(detectionText);
182
+ const containsMonth = Object.keys(MONTH_INDEX_BY_ENGLISH).some((month) => detectionText.includes(month));
183
+ if (!relative && !containsMonth) {
184
+ return undefined;
185
+ }
186
+
187
+ let year = parsedDate.getUTCFullYear();
188
+ const monthIndex = parsedDate.getUTCMonth();
189
+ if (!params.explicitYear && containsMonth && monthIndex > params.now.getUTCMonth()) {
190
+ year -= 1;
191
+ }
192
+ return {
193
+ ...monthRange(year, monthIndex),
194
+ matchedText: params.matchedText ?? params.result.text,
195
+ };
196
+ }
197
+
198
+ export function parseTimeRangeFromQuery(
199
+ query: string,
200
+ now = new Date(),
201
+ ): { range?: TimeRange; queryWithoutTime: string } {
202
+ const normalized = normalizeTimeQuery(query);
203
+ if (!normalized.normalizedMatchedText) {
204
+ return { queryWithoutTime: "" };
205
+ }
206
+
207
+ const results = chrono.en.parse(normalized.normalizedMatchedText, now, { forwardDate: true });
208
+ const best = results[0];
209
+ const range =
210
+ best && normalized.monthLevel
211
+ ? inferMonthRangeFromChrono({
212
+ result: best,
213
+ matchedText: normalized.matchedOriginalText,
214
+ detectionText: normalized.normalizedMatchedText,
215
+ explicitYear: normalized.explicitYear,
216
+ now,
217
+ })
218
+ : undefined;
219
+
220
+ return {
221
+ range,
222
+ queryWithoutTime: normalized.matchedOriginalText
223
+ ? cleanQueryAfterTimeRemoval(query.replace(normalized.matchedOriginalText, " "))
224
+ : cleanQueryAfterTimeRemoval(query),
225
+ };
226
+ }
227
+
228
+ export function buildTimeRange(params: {
229
+ query: string;
230
+ from?: string;
231
+ to?: string;
232
+ enabled?: boolean;
233
+ now?: Date;
234
+ }): { range?: TimeRange; queryWithoutTime: string } {
235
+ const parsed =
236
+ params.enabled === false
237
+ ? { queryWithoutTime: normalizeWhitespace(params.query) }
238
+ : parseTimeRangeFromQuery(params.query, params.now);
239
+ if (params.from || params.to) {
240
+ const start = params.from ?? params.to ?? "";
241
+ const end = params.to ?? params.from ?? "";
242
+ const explicit = explicitRange(start, end);
243
+ return {
244
+ range: explicit,
245
+ queryWithoutTime: parsed.queryWithoutTime,
246
+ };
247
+ }
248
+ return parsed;
249
+ }
250
+
251
+ export function resolveResultTimeMs(result: MemoryBraidResult): number | undefined {
252
+ const metadata = asRecord(result.metadata);
253
+ const fields = [metadata.eventAt, metadata.firstSeenAt, metadata.indexedAt, metadata.createdAt];
254
+ for (const value of fields) {
255
+ const text = asString(value);
256
+ if (!text) {
257
+ continue;
258
+ }
259
+ const parsed = Date.parse(text);
260
+ if (Number.isFinite(parsed)) {
261
+ return parsed;
262
+ }
263
+ }
264
+ return undefined;
265
+ }
266
+
267
+ export function isResultInTimeRange(result: MemoryBraidResult, range?: TimeRange): boolean {
268
+ if (!range) {
269
+ return true;
270
+ }
271
+ const ts = resolveResultTimeMs(result);
272
+ if (!ts) {
273
+ return false;
274
+ }
275
+ return ts >= range.startMs && ts <= range.endMs;
276
+ }
277
+
278
+ export function formatTimeRange(range?: TimeRange): string {
279
+ if (!range) {
280
+ return "n/a";
281
+ }
282
+ return `${range.startDate}..${range.endDate}`;
283
+ }
284
+
285
+ export function inferQuerySpecificity(text: string): "broad" | "specific" {
286
+ const normalized = normalizeWhitespace(text);
287
+ const tokenCount = (normalized.match(/[\p{L}\p{N}]+/gu) ?? []).length;
288
+ if (tokenCount >= 8) {
289
+ return "specific";
290
+ }
291
+ return "broad";
292
+ }
package/src/types.ts CHANGED
@@ -24,6 +24,19 @@ export type RecallTarget = "response" | "planning" | "both";
24
24
 
25
25
  export type Stability = "ephemeral" | "session" | "durable";
26
26
 
27
+ export type MemoryLayer = "episodic" | "semantic" | "procedural";
28
+
29
+ export type MemorySelectionDecision = "ignore" | "episodic" | "procedural" | "semantic";
30
+
31
+ export type TaxonomyBuckets = {
32
+ people: string[];
33
+ places: string[];
34
+ organizations: string[];
35
+ projects: string[];
36
+ tools: string[];
37
+ topics: string[];
38
+ };
39
+
27
40
  export type ScopeKey = {
28
41
  workspaceHash: string;
29
42
  agentId: string;
@@ -102,8 +115,19 @@ export type CaptureStats = {
102
115
  agentLearningAutoRejected: number;
103
116
  agentLearningInjected: number;
104
117
  agentLearningRecallHits: number;
118
+ selectionSkipped: number;
119
+ agentLearningRejectedSelection: number;
120
+ consolidationRuns: number;
121
+ consolidationCandidates: number;
122
+ clustersFormed: number;
123
+ semanticCreated: number;
124
+ semanticUpdated: number;
125
+ episodicMarkedConsolidated: number;
126
+ contradictionsDetected: number;
127
+ supersededMarked: number;
105
128
  lastRunAt?: string;
106
129
  lastRemediationAt?: string;
130
+ lastConsolidationAt?: string;
107
131
  };
108
132
 
109
133
  export type PluginStatsState = {
@@ -150,3 +174,19 @@ export type RemediationState = {
150
174
  }
151
175
  >;
152
176
  };
177
+
178
+ export type ConsolidationReason = "startup" | "interval" | "opportunistic" | "command";
179
+
180
+ export type ConsolidationState = {
181
+ version: 1;
182
+ lastConsolidationAt?: string;
183
+ lastConsolidationReason?: ConsolidationReason;
184
+ newEpisodicSinceLastRun: number;
185
+ semanticByCompendiumKey: Record<
186
+ string,
187
+ {
188
+ memoryId: string;
189
+ updatedAt: number;
190
+ }
191
+ >;
192
+ };