memory-braid 0.3.6 → 0.4.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.
@@ -112,6 +112,64 @@ function asNonEmptyString(value: unknown): string | undefined {
112
112
  return trimmed ? trimmed : undefined;
113
113
  }
114
114
 
115
+ function cloneConfigValue(value: unknown): unknown {
116
+ if (Array.isArray(value)) {
117
+ return value.map((entry) => cloneConfigValue(entry));
118
+ }
119
+ if (isObjectLike(value)) {
120
+ return cloneConfigRecord(value);
121
+ }
122
+ return value;
123
+ }
124
+
125
+ function cloneConfigRecord(value: Record<string, unknown>): Record<string, unknown> {
126
+ const out: Record<string, unknown> = {};
127
+ for (const [key, entry] of Object.entries(value)) {
128
+ out[key] = cloneConfigValue(entry);
129
+ }
130
+ return out;
131
+ }
132
+
133
+ function shouldReplaceProviderSection(
134
+ baseValue: Record<string, unknown>,
135
+ overrideValue: Record<string, unknown>,
136
+ ): boolean {
137
+ const baseProvider = asNonEmptyString(baseValue.provider)?.toLowerCase();
138
+ const overrideProvider = asNonEmptyString(overrideValue.provider)?.toLowerCase();
139
+ return Boolean(baseProvider && overrideProvider && baseProvider !== overrideProvider);
140
+ }
141
+
142
+ function deepMergeConfigRecords(
143
+ baseValue: Record<string, unknown>,
144
+ overrideValue: Record<string, unknown>,
145
+ ): Record<string, unknown> {
146
+ const merged = cloneConfigRecord(baseValue);
147
+ for (const [key, overrideEntry] of Object.entries(overrideValue)) {
148
+ if (typeof overrideEntry === "undefined") {
149
+ continue;
150
+ }
151
+ const currentBase = merged[key];
152
+ if (isObjectLike(currentBase) && isObjectLike(overrideEntry)) {
153
+ merged[key] = shouldReplaceProviderSection(currentBase, overrideEntry)
154
+ ? cloneConfigRecord(overrideEntry)
155
+ : deepMergeConfigRecords(currentBase, overrideEntry);
156
+ continue;
157
+ }
158
+ merged[key] = cloneConfigValue(overrideEntry);
159
+ }
160
+ return merged;
161
+ }
162
+
163
+ export function mergeOssConfigWithDefaults(
164
+ defaults: Record<string, unknown>,
165
+ override: Record<string, unknown>,
166
+ ): Record<string, unknown> {
167
+ if (!isLikelyOssConfig(override)) {
168
+ return cloneConfigRecord(defaults);
169
+ }
170
+ return deepMergeConfigRecords(defaults, override);
171
+ }
172
+
115
173
  function asOssMemoryCtor(value: unknown): OssMemoryCtor | undefined {
116
174
  if (typeof value !== "function") {
117
175
  return undefined;
@@ -157,6 +215,20 @@ function isSqliteBindingsError(error: unknown): boolean {
157
215
  return /Could not locate the bindings file/i.test(message) || /node_sqlite3\.node/i.test(message);
158
216
  }
159
217
 
218
+ export function isMem0DeleteNotFoundError(error: unknown): boolean {
219
+ const message = asErrorMessage(error).toLowerCase();
220
+ if (!message) {
221
+ return false;
222
+ }
223
+
224
+ return (
225
+ /\bmemory\b.*\bnot found\b/.test(message) ||
226
+ /\bnot found\b.*\bmemory\b/.test(message) ||
227
+ /\bmemory\b.*\bdoes not exist\b/.test(message) ||
228
+ /\bno such memory\b/.test(message)
229
+ );
230
+ }
231
+
160
232
  function createLocalRequire(): NodeJS.Require {
161
233
  return createRequire(import.meta.url);
162
234
  }
@@ -410,12 +482,11 @@ export class Mem0Adapter {
410
482
  );
411
483
  }
412
484
 
413
- const providedConfig = this.cfg.mem0.ossConfig;
485
+ const providedConfig = asRecord(this.cfg.mem0.ossConfig);
414
486
  const hasCustomConfig = isLikelyOssConfig(providedConfig);
415
- const baseConfig = hasCustomConfig
416
- ? { ...providedConfig }
417
- : buildDefaultOssConfig(this.cfg, this.stateDir);
418
- const configToUse = applyOssStorageDefaults(baseConfig, this.stateDir);
487
+ const defaultConfig = buildDefaultOssConfig(this.cfg, this.stateDir);
488
+ const mergedConfig = mergeOssConfigWithDefaults(defaultConfig, providedConfig);
489
+ const configToUse = applyOssStorageDefaults(mergedConfig, this.stateDir);
419
490
  await ensureSqliteParentDirs(configToUse);
420
491
 
421
492
  this.ossClient = new Memory(configToUse);
@@ -738,6 +809,21 @@ export class Mem0Adapter {
738
809
  });
739
810
  return true;
740
811
  } catch (err) {
812
+ const missingRemote = isMem0DeleteNotFoundError(err);
813
+ if (missingRemote) {
814
+ this.log.debug("memory_braid.mem0.response", {
815
+ runId: params.runId,
816
+ action: "delete",
817
+ mode: prepared.mode,
818
+ workspaceHash: params.scope.workspaceHash,
819
+ agentId: params.scope.agentId,
820
+ memoryId: params.memoryId,
821
+ durMs: Date.now() - startedAt,
822
+ alreadyMissing: true,
823
+ });
824
+ return true;
825
+ }
826
+
741
827
  this.log.warn("memory_braid.mem0.error", {
742
828
  runId: params.runId,
743
829
  action: "delete",
package/src/state.ts CHANGED
@@ -1,27 +1,43 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import type { BootstrapState, CaptureDedupeState, ReconcileState } from "./types.js";
3
+ import type {
4
+ CaptureDedupeState,
5
+ LifecycleState,
6
+ PluginStatsState,
7
+ } from "./types.js";
4
8
 
5
- const DEFAULT_BOOTSTRAP: BootstrapState = {
9
+ const DEFAULT_CAPTURE_DEDUPE: CaptureDedupeState = {
6
10
  version: 1,
7
- completed: false,
11
+ seen: {},
8
12
  };
9
13
 
10
- const DEFAULT_RECONCILE: ReconcileState = {
14
+ const DEFAULT_LIFECYCLE: LifecycleState = {
11
15
  version: 1,
12
16
  entries: {},
13
17
  };
14
18
 
15
- const DEFAULT_CAPTURE_DEDUPE: CaptureDedupeState = {
19
+ const DEFAULT_STATS: PluginStatsState = {
16
20
  version: 1,
17
- seen: {},
21
+ capture: {
22
+ runs: 0,
23
+ runsWithCandidates: 0,
24
+ runsNoCandidates: 0,
25
+ candidates: 0,
26
+ dedupeSkipped: 0,
27
+ persisted: 0,
28
+ mem0AddAttempts: 0,
29
+ mem0AddWithId: 0,
30
+ mem0AddWithoutId: 0,
31
+ entityAnnotatedCandidates: 0,
32
+ totalEntitiesAttached: 0,
33
+ },
18
34
  };
19
35
 
20
36
  export type StatePaths = {
21
37
  rootDir: string;
22
- bootstrapFile: string;
23
- reconcileFile: string;
24
38
  captureDedupeFile: string;
39
+ lifecycleFile: string;
40
+ statsFile: string;
25
41
  stateLockFile: string;
26
42
  };
27
43
 
@@ -29,9 +45,9 @@ export function createStatePaths(stateDir: string): StatePaths {
29
45
  const rootDir = path.join(stateDir, "memory-braid");
30
46
  return {
31
47
  rootDir,
32
- bootstrapFile: path.join(rootDir, "bootstrap-checkpoint.v1.json"),
33
- reconcileFile: path.join(rootDir, "reconcile-state.v1.json"),
34
48
  captureDedupeFile: path.join(rootDir, "capture-dedupe.v1.json"),
49
+ lifecycleFile: path.join(rootDir, "lifecycle.v1.json"),
50
+ statsFile: path.join(rootDir, "stats.v1.json"),
35
51
  stateLockFile: path.join(rootDir, "state.v1.lock"),
36
52
  };
37
53
  }
@@ -57,41 +73,52 @@ async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
57
73
  await fs.rename(tmpPath, filePath);
58
74
  }
59
75
 
60
- export async function readBootstrapState(paths: StatePaths): Promise<BootstrapState> {
61
- const value = await readJsonFile(paths.bootstrapFile, DEFAULT_BOOTSTRAP);
62
- return { ...DEFAULT_BOOTSTRAP, ...value };
76
+ export async function readCaptureDedupeState(paths: StatePaths): Promise<CaptureDedupeState> {
77
+ const value = await readJsonFile(paths.captureDedupeFile, DEFAULT_CAPTURE_DEDUPE);
78
+ return {
79
+ version: 1,
80
+ seen: value.seen ?? {},
81
+ };
63
82
  }
64
83
 
65
- export async function writeBootstrapState(paths: StatePaths, state: BootstrapState): Promise<void> {
66
- await writeJsonFile(paths.bootstrapFile, state);
84
+ export async function writeCaptureDedupeState(
85
+ paths: StatePaths,
86
+ state: CaptureDedupeState,
87
+ ): Promise<void> {
88
+ await writeJsonFile(paths.captureDedupeFile, state);
67
89
  }
68
90
 
69
- export async function readReconcileState(paths: StatePaths): Promise<ReconcileState> {
70
- const value = await readJsonFile(paths.reconcileFile, DEFAULT_RECONCILE);
91
+ export async function readLifecycleState(paths: StatePaths): Promise<LifecycleState> {
92
+ const value = await readJsonFile(paths.lifecycleFile, DEFAULT_LIFECYCLE);
71
93
  return {
72
94
  version: 1,
73
95
  entries: value.entries ?? {},
74
- lastRunAt: value.lastRunAt,
96
+ lastCleanupAt: value.lastCleanupAt,
97
+ lastCleanupReason: value.lastCleanupReason,
98
+ lastCleanupScanned: value.lastCleanupScanned,
99
+ lastCleanupExpired: value.lastCleanupExpired,
100
+ lastCleanupDeleted: value.lastCleanupDeleted,
101
+ lastCleanupFailed: value.lastCleanupFailed,
75
102
  };
76
103
  }
77
104
 
78
- export async function writeReconcileState(paths: StatePaths, state: ReconcileState): Promise<void> {
79
- await writeJsonFile(paths.reconcileFile, state);
105
+ export async function writeLifecycleState(paths: StatePaths, state: LifecycleState): Promise<void> {
106
+ await writeJsonFile(paths.lifecycleFile, state);
80
107
  }
81
108
 
82
- export async function readCaptureDedupeState(paths: StatePaths): Promise<CaptureDedupeState> {
83
- const value = await readJsonFile(paths.captureDedupeFile, DEFAULT_CAPTURE_DEDUPE);
109
+ export async function readStatsState(paths: StatePaths): Promise<PluginStatsState> {
110
+ const value = await readJsonFile(paths.statsFile, DEFAULT_STATS);
84
111
  return {
85
112
  version: 1,
86
- seen: value.seen ?? {},
113
+ capture: {
114
+ ...DEFAULT_STATS.capture,
115
+ ...(value.capture ?? {}),
116
+ },
87
117
  };
88
118
  }
89
119
 
90
- export async function writeCaptureDedupeState(
91
- paths: StatePaths,
92
- state: CaptureDedupeState,
93
- ): Promise<void> {
94
- await writeJsonFile(paths.captureDedupeFile, state);
120
+ export async function writeStatsState(paths: StatePaths, state: PluginStatsState): Promise<void> {
121
+ await writeJsonFile(paths.statsFile, state);
95
122
  }
96
123
 
97
124
  export async function withStateLock<T>(
package/src/types.ts CHANGED
@@ -1,22 +1,11 @@
1
1
  export type MemoryBraidSource = "local" | "mem0";
2
2
 
3
- export type ManagedSourceType = "markdown" | "session";
4
-
5
- export type PersistedSourceType = ManagedSourceType | "capture";
6
-
7
3
  export type ScopeKey = {
8
4
  workspaceHash: string;
9
5
  agentId: string;
10
6
  sessionKey?: string;
11
7
  };
12
8
 
13
- export type TargetWorkspace = {
14
- workspaceDir: string;
15
- stateDir: string;
16
- agentId: string;
17
- workspaceHash: string;
18
- };
19
-
20
9
  export type MemoryBraidResult = {
21
10
  id?: string;
22
11
  source: MemoryBraidSource;
@@ -31,61 +20,54 @@ export type MemoryBraidResult = {
31
20
  contentHash?: string;
32
21
  };
33
22
 
34
- export type ManifestChunk = {
35
- chunkKey: string;
36
- contentHash: string;
37
- sourceType: ManagedSourceType;
38
- text: string;
39
- path: string;
40
- workspaceHash: string;
41
- agentId: string;
42
- updatedAt: number;
23
+ export type CaptureDedupeState = {
24
+ version: 1;
25
+ seen: Record<string, number>;
43
26
  };
44
27
 
45
- export type IndexedEntry = {
46
- chunkKey: string;
47
- id?: string;
28
+ export type LifecycleEntry = {
29
+ memoryId: string;
48
30
  contentHash: string;
49
- sourceType: PersistedSourceType;
50
- path?: string;
51
31
  workspaceHash: string;
52
32
  agentId: string;
33
+ sessionKey?: string;
34
+ category?: string;
35
+ createdAt: number;
36
+ lastCapturedAt: number;
37
+ lastRecalledAt?: number;
38
+ recallCount: number;
53
39
  updatedAt: number;
54
- missingCount?: number;
55
40
  };
56
41
 
57
- export type ReconcileState = {
42
+ export type LifecycleState = {
58
43
  version: 1;
59
- entries: Record<string, IndexedEntry>;
60
- lastRunAt?: string;
44
+ entries: Record<string, LifecycleEntry>;
45
+ lastCleanupAt?: string;
46
+ lastCleanupReason?: "startup" | "interval" | "command";
47
+ lastCleanupScanned?: number;
48
+ lastCleanupExpired?: number;
49
+ lastCleanupDeleted?: number;
50
+ lastCleanupFailed?: number;
61
51
  };
62
52
 
63
- export type BootstrapState = {
64
- version: 1;
65
- completed: boolean;
66
- startedAt?: string;
67
- completedAt?: string;
68
- lastError?: string;
69
- summary?: {
70
- reason: string;
71
- total: number;
72
- upserted: number;
73
- deleted: number;
74
- unchanged: number;
75
- };
53
+ export type CaptureStats = {
54
+ runs: number;
55
+ runsWithCandidates: number;
56
+ runsNoCandidates: number;
57
+ candidates: number;
58
+ dedupeSkipped: number;
59
+ persisted: number;
60
+ mem0AddAttempts: number;
61
+ mem0AddWithId: number;
62
+ mem0AddWithoutId: number;
63
+ entityAnnotatedCandidates: number;
64
+ totalEntitiesAttached: number;
65
+ lastRunAt?: string;
76
66
  };
77
67
 
78
- export type CaptureDedupeState = {
68
+ export type PluginStatsState = {
79
69
  version: 1;
80
- seen: Record<string, number>;
81
- };
82
-
83
- export type ReconcileSummary = {
84
- reason: string;
85
- total: number;
86
- upserted: number;
87
- deleted: number;
88
- unchanged: number;
70
+ capture: CaptureStats;
89
71
  };
90
72
 
91
73
  export type ExtractedCandidate = {
package/src/bootstrap.ts DELETED
@@ -1,82 +0,0 @@
1
- import type { MemoryBraidConfig } from "./config.js";
2
- import { MemoryBraidLogger } from "./logger.js";
3
- import { Mem0Adapter } from "./mem0-client.js";
4
- import {
5
- readBootstrapState,
6
- type StatePaths,
7
- writeBootstrapState,
8
- } from "./state.js";
9
- import type { TargetWorkspace } from "./types.js";
10
- import { runReconcileOnce } from "./reconcile.js";
11
-
12
- export async function runBootstrapIfNeeded(params: {
13
- cfg: MemoryBraidConfig;
14
- mem0: Mem0Adapter;
15
- statePaths: StatePaths;
16
- log: MemoryBraidLogger;
17
- targets: TargetWorkspace[];
18
- runId?: string;
19
- }): Promise<{ started: boolean; completed: boolean }> {
20
- const runId = params.runId ?? params.log.newRunId();
21
-
22
- if (!params.cfg.bootstrap.enabled) {
23
- return { started: false, completed: false };
24
- }
25
-
26
- const existing = await readBootstrapState(params.statePaths);
27
- if (existing.completed) {
28
- return { started: false, completed: true };
29
- }
30
-
31
- params.log.debug("memory_braid.bootstrap.begin", {
32
- runId,
33
- targets: params.targets.length,
34
- }, true);
35
-
36
- await writeBootstrapState(params.statePaths, {
37
- version: 1,
38
- completed: false,
39
- startedAt: new Date().toISOString(),
40
- lastError: undefined,
41
- });
42
-
43
- try {
44
- const summary = await runReconcileOnce({
45
- cfg: params.cfg,
46
- mem0: params.mem0,
47
- statePaths: params.statePaths,
48
- log: params.log,
49
- targets: params.targets,
50
- reason: "bootstrap",
51
- runId,
52
- deleteStale: false,
53
- });
54
-
55
- await writeBootstrapState(params.statePaths, {
56
- version: 1,
57
- completed: true,
58
- startedAt: existing.startedAt ?? new Date().toISOString(),
59
- completedAt: new Date().toISOString(),
60
- summary,
61
- });
62
-
63
- params.log.debug("memory_braid.bootstrap.complete", {
64
- runId,
65
- ...summary,
66
- }, true);
67
- return { started: true, completed: true };
68
- } catch (err) {
69
- const error = err instanceof Error ? err.message : String(err);
70
- await writeBootstrapState(params.statePaths, {
71
- version: 1,
72
- completed: false,
73
- startedAt: existing.startedAt ?? new Date().toISOString(),
74
- lastError: error,
75
- });
76
- params.log.warn("memory_braid.bootstrap.error", {
77
- runId,
78
- error,
79
- });
80
- return { started: true, completed: false };
81
- }
82
- }