memory-braid 0.3.7 → 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.
- package/README.md +56 -27
- package/openclaw.plugin.json +24 -24
- package/package.json +2 -2
- package/src/chunking.ts +0 -267
- package/src/config.ts +45 -57
- package/src/dedupe.ts +4 -4
- package/src/extract.ts +10 -2
- package/src/index.ts +678 -162
- package/src/mem0-client.ts +62 -5
- package/src/state.ts +55 -28
- package/src/types.ts +34 -52
- package/src/bootstrap.ts +0 -82
- package/src/reconcile.ts +0 -278
package/src/mem0-client.ts
CHANGED
|
@@ -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;
|
|
@@ -424,12 +482,11 @@ export class Mem0Adapter {
|
|
|
424
482
|
);
|
|
425
483
|
}
|
|
426
484
|
|
|
427
|
-
const providedConfig = this.cfg.mem0.ossConfig;
|
|
485
|
+
const providedConfig = asRecord(this.cfg.mem0.ossConfig);
|
|
428
486
|
const hasCustomConfig = isLikelyOssConfig(providedConfig);
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
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);
|
|
433
490
|
await ensureSqliteParentDirs(configToUse);
|
|
434
491
|
|
|
435
492
|
this.ossClient = new Memory(configToUse);
|
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 {
|
|
3
|
+
import type {
|
|
4
|
+
CaptureDedupeState,
|
|
5
|
+
LifecycleState,
|
|
6
|
+
PluginStatsState,
|
|
7
|
+
} from "./types.js";
|
|
4
8
|
|
|
5
|
-
const
|
|
9
|
+
const DEFAULT_CAPTURE_DEDUPE: CaptureDedupeState = {
|
|
6
10
|
version: 1,
|
|
7
|
-
|
|
11
|
+
seen: {},
|
|
8
12
|
};
|
|
9
13
|
|
|
10
|
-
const
|
|
14
|
+
const DEFAULT_LIFECYCLE: LifecycleState = {
|
|
11
15
|
version: 1,
|
|
12
16
|
entries: {},
|
|
13
17
|
};
|
|
14
18
|
|
|
15
|
-
const
|
|
19
|
+
const DEFAULT_STATS: PluginStatsState = {
|
|
16
20
|
version: 1,
|
|
17
|
-
|
|
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
|
|
61
|
-
const value = await readJsonFile(paths.
|
|
62
|
-
return {
|
|
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
|
|
66
|
-
|
|
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
|
|
70
|
-
const value = await readJsonFile(paths.
|
|
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
|
-
|
|
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
|
|
79
|
-
await writeJsonFile(paths.
|
|
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
|
|
83
|
-
const value = await readJsonFile(paths.
|
|
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
|
-
|
|
113
|
+
capture: {
|
|
114
|
+
...DEFAULT_STATS.capture,
|
|
115
|
+
...(value.capture ?? {}),
|
|
116
|
+
},
|
|
87
117
|
};
|
|
88
118
|
}
|
|
89
119
|
|
|
90
|
-
export async function
|
|
91
|
-
paths
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
46
|
-
|
|
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
|
|
42
|
+
export type LifecycleState = {
|
|
58
43
|
version: 1;
|
|
59
|
-
entries: Record<string,
|
|
60
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
68
|
+
export type PluginStatsState = {
|
|
79
69
|
version: 1;
|
|
80
|
-
|
|
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
|
-
}
|
package/src/reconcile.ts
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { buildMarkdownChunks, buildSessionChunks, sha256 } from "./chunking.js";
|
|
4
|
-
import type { MemoryBraidConfig } from "./config.js";
|
|
5
|
-
import { MemoryBraidLogger } from "./logger.js";
|
|
6
|
-
import { Mem0Adapter } from "./mem0-client.js";
|
|
7
|
-
import {
|
|
8
|
-
readReconcileState,
|
|
9
|
-
type StatePaths,
|
|
10
|
-
withStateLock,
|
|
11
|
-
writeReconcileState,
|
|
12
|
-
} from "./state.js";
|
|
13
|
-
import type { IndexedEntry, ManifestChunk, ReconcileSummary, TargetWorkspace } from "./types.js";
|
|
14
|
-
|
|
15
|
-
type OpenClawConfigLike = {
|
|
16
|
-
agents?: {
|
|
17
|
-
defaults?: {
|
|
18
|
-
workspace?: string;
|
|
19
|
-
};
|
|
20
|
-
list?: Array<{
|
|
21
|
-
id?: string;
|
|
22
|
-
workspace?: string;
|
|
23
|
-
default?: boolean;
|
|
24
|
-
}>;
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function normalizeAgentId(value: string | undefined): string {
|
|
29
|
-
const trimmed = (value ?? "main").trim().toLowerCase();
|
|
30
|
-
return trimmed || "main";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolveWorkspacePath(input: string | undefined, fallback?: string): string | undefined {
|
|
34
|
-
const value = (input ?? "").trim() || (fallback ?? "").trim();
|
|
35
|
-
if (!value) {
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
return path.resolve(value);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function workspaceHashForDir(workspaceDir: string): Promise<string> {
|
|
42
|
-
try {
|
|
43
|
-
const real = await fs.realpath(workspaceDir);
|
|
44
|
-
return sha256(real.toLowerCase());
|
|
45
|
-
} catch {
|
|
46
|
-
return sha256(path.resolve(workspaceDir).toLowerCase());
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function resolveTargets(params: {
|
|
51
|
-
config?: OpenClawConfigLike;
|
|
52
|
-
stateDir: string;
|
|
53
|
-
fallbackWorkspaceDir?: string;
|
|
54
|
-
}): Promise<TargetWorkspace[]> {
|
|
55
|
-
const targets: TargetWorkspace[] = [];
|
|
56
|
-
const seen = new Set<string>();
|
|
57
|
-
|
|
58
|
-
const fallbackWorkspace = resolveWorkspacePath(
|
|
59
|
-
params.config?.agents?.defaults?.workspace,
|
|
60
|
-
params.fallbackWorkspaceDir,
|
|
61
|
-
);
|
|
62
|
-
if (fallbackWorkspace) {
|
|
63
|
-
const agentId = "main";
|
|
64
|
-
const key = `${agentId}|${fallbackWorkspace}`;
|
|
65
|
-
seen.add(key);
|
|
66
|
-
targets.push({
|
|
67
|
-
workspaceDir: fallbackWorkspace,
|
|
68
|
-
stateDir: params.stateDir,
|
|
69
|
-
agentId,
|
|
70
|
-
workspaceHash: await workspaceHashForDir(fallbackWorkspace),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (const entry of params.config?.agents?.list ?? []) {
|
|
75
|
-
const workspace = resolveWorkspacePath(entry.workspace, fallbackWorkspace);
|
|
76
|
-
if (!workspace) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const agentId = normalizeAgentId(entry.id);
|
|
80
|
-
const key = `${agentId}|${workspace}`;
|
|
81
|
-
if (seen.has(key)) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
seen.add(key);
|
|
85
|
-
targets.push({
|
|
86
|
-
workspaceDir: workspace,
|
|
87
|
-
stateDir: params.stateDir,
|
|
88
|
-
agentId,
|
|
89
|
-
workspaceHash: await workspaceHashForDir(workspace),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return targets;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export async function buildManagedManifest(params: {
|
|
97
|
-
targets: TargetWorkspace[];
|
|
98
|
-
cfg: MemoryBraidConfig;
|
|
99
|
-
}): Promise<ManifestChunk[]> {
|
|
100
|
-
const out: ManifestChunk[] = [];
|
|
101
|
-
for (const target of params.targets) {
|
|
102
|
-
if (params.cfg.bootstrap.includeMarkdown) {
|
|
103
|
-
out.push(...(await buildMarkdownChunks(target)));
|
|
104
|
-
}
|
|
105
|
-
if (params.cfg.bootstrap.includeSessions) {
|
|
106
|
-
out.push(...(await buildSessionChunks(target, params.cfg.bootstrap.sessionLookbackDays)));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return out;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export async function runReconcileOnce(params: {
|
|
113
|
-
cfg: MemoryBraidConfig;
|
|
114
|
-
mem0: Mem0Adapter;
|
|
115
|
-
statePaths: StatePaths;
|
|
116
|
-
log: MemoryBraidLogger;
|
|
117
|
-
targets: TargetWorkspace[];
|
|
118
|
-
reason: string;
|
|
119
|
-
runId?: string;
|
|
120
|
-
deleteStale?: boolean;
|
|
121
|
-
}): Promise<ReconcileSummary> {
|
|
122
|
-
const runId = params.runId ?? params.log.newRunId();
|
|
123
|
-
const startedAt = Date.now();
|
|
124
|
-
|
|
125
|
-
return withStateLock(params.statePaths.stateLockFile, async () => {
|
|
126
|
-
params.log.debug("memory_braid.reconcile.begin", {
|
|
127
|
-
runId,
|
|
128
|
-
reason: params.reason,
|
|
129
|
-
targets: params.targets.length,
|
|
130
|
-
}, true);
|
|
131
|
-
|
|
132
|
-
const state = await readReconcileState(params.statePaths);
|
|
133
|
-
const manifest = await buildManagedManifest({
|
|
134
|
-
targets: params.targets,
|
|
135
|
-
cfg: params.cfg,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const byKey = new Map<string, ManifestChunk>();
|
|
139
|
-
for (const chunk of manifest) {
|
|
140
|
-
byKey.set(chunk.chunkKey, chunk);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let upserted = 0;
|
|
144
|
-
let deleted = 0;
|
|
145
|
-
let unchanged = 0;
|
|
146
|
-
const now = Date.now();
|
|
147
|
-
|
|
148
|
-
// Mark managed entries that are no longer present.
|
|
149
|
-
for (const [chunkKey, entry] of Object.entries(state.entries)) {
|
|
150
|
-
if (entry.sourceType === "capture") {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (byKey.has(chunkKey)) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
const missingCount = (entry.missingCount ?? 0) + 1;
|
|
157
|
-
const shouldDelete =
|
|
158
|
-
(params.deleteStale ?? params.cfg.reconcile.deleteStale) && missingCount >= 2;
|
|
159
|
-
if (!shouldDelete) {
|
|
160
|
-
state.entries[chunkKey] = {
|
|
161
|
-
...entry,
|
|
162
|
-
missingCount,
|
|
163
|
-
updatedAt: now,
|
|
164
|
-
};
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const deletedRemote = await params.mem0.deleteMemory({
|
|
169
|
-
memoryId: entry.id,
|
|
170
|
-
scope: {
|
|
171
|
-
workspaceHash: entry.workspaceHash,
|
|
172
|
-
agentId: entry.agentId,
|
|
173
|
-
},
|
|
174
|
-
runId,
|
|
175
|
-
});
|
|
176
|
-
if (deletedRemote || !entry.id) {
|
|
177
|
-
delete state.entries[chunkKey];
|
|
178
|
-
deleted += 1;
|
|
179
|
-
} else {
|
|
180
|
-
state.entries[chunkKey] = {
|
|
181
|
-
...entry,
|
|
182
|
-
missingCount,
|
|
183
|
-
updatedAt: now,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const allChunks = Array.from(byKey.values());
|
|
189
|
-
const batchSize = Math.max(1, params.cfg.reconcile.batchSize);
|
|
190
|
-
for (let offset = 0; offset < allChunks.length; offset += batchSize) {
|
|
191
|
-
const batch = allChunks.slice(offset, offset + batchSize);
|
|
192
|
-
for (const chunk of batch) {
|
|
193
|
-
const existing = state.entries[chunk.chunkKey];
|
|
194
|
-
if (existing && existing.contentHash === chunk.contentHash && existing.sourceType !== "capture") {
|
|
195
|
-
unchanged += 1;
|
|
196
|
-
state.entries[chunk.chunkKey] = {
|
|
197
|
-
...existing,
|
|
198
|
-
missingCount: 0,
|
|
199
|
-
updatedAt: now,
|
|
200
|
-
};
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (existing?.id && existing.sourceType !== "capture") {
|
|
205
|
-
await params.mem0.deleteMemory({
|
|
206
|
-
memoryId: existing.id,
|
|
207
|
-
scope: {
|
|
208
|
-
workspaceHash: existing.workspaceHash,
|
|
209
|
-
agentId: existing.agentId,
|
|
210
|
-
},
|
|
211
|
-
runId,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const metadata = {
|
|
216
|
-
sourceType: chunk.sourceType,
|
|
217
|
-
path: chunk.path,
|
|
218
|
-
workspaceHash: chunk.workspaceHash,
|
|
219
|
-
agentId: chunk.agentId,
|
|
220
|
-
chunkKey: chunk.chunkKey,
|
|
221
|
-
contentHash: chunk.contentHash,
|
|
222
|
-
indexedAt: new Date(now).toISOString(),
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const addResult = await params.mem0.addMemory({
|
|
226
|
-
text: chunk.text,
|
|
227
|
-
scope: {
|
|
228
|
-
workspaceHash: chunk.workspaceHash,
|
|
229
|
-
agentId: chunk.agentId,
|
|
230
|
-
},
|
|
231
|
-
metadata,
|
|
232
|
-
runId,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const next: IndexedEntry = {
|
|
236
|
-
chunkKey: chunk.chunkKey,
|
|
237
|
-
id: addResult.id ?? existing?.id,
|
|
238
|
-
contentHash: chunk.contentHash,
|
|
239
|
-
sourceType: chunk.sourceType,
|
|
240
|
-
path: chunk.path,
|
|
241
|
-
workspaceHash: chunk.workspaceHash,
|
|
242
|
-
agentId: chunk.agentId,
|
|
243
|
-
updatedAt: now,
|
|
244
|
-
missingCount: 0,
|
|
245
|
-
};
|
|
246
|
-
state.entries[chunk.chunkKey] = next;
|
|
247
|
-
upserted += 1;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
params.log.debug("memory_braid.reconcile.progress", {
|
|
251
|
-
runId,
|
|
252
|
-
reason: params.reason,
|
|
253
|
-
processed: Math.min(offset + batch.length, allChunks.length),
|
|
254
|
-
total: allChunks.length,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const summary: ReconcileSummary = {
|
|
259
|
-
reason: params.reason,
|
|
260
|
-
total: allChunks.length,
|
|
261
|
-
upserted,
|
|
262
|
-
deleted,
|
|
263
|
-
unchanged,
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
state.lastRunAt = new Date(now).toISOString();
|
|
267
|
-
await writeReconcileState(params.statePaths, state);
|
|
268
|
-
|
|
269
|
-
params.log.debug("memory_braid.reconcile.complete", {
|
|
270
|
-
runId,
|
|
271
|
-
reason: params.reason,
|
|
272
|
-
...summary,
|
|
273
|
-
durMs: Date.now() - startedAt,
|
|
274
|
-
}, true);
|
|
275
|
-
|
|
276
|
-
return summary;
|
|
277
|
-
});
|
|
278
|
-
}
|