memory-braid 0.2.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 +300 -0
- package/openclaw.plugin.json +129 -0
- package/package.json +46 -0
- package/src/bootstrap.ts +82 -0
- package/src/chunking.ts +280 -0
- package/src/config.ts +271 -0
- package/src/dedupe.ts +96 -0
- package/src/extract.ts +351 -0
- package/src/index.ts +489 -0
- package/src/local-memory.ts +128 -0
- package/src/logger.ts +128 -0
- package/src/mem0-client.ts +605 -0
- package/src/merge.ts +56 -0
- package/src/reconcile.ts +278 -0
- package/src/state.ts +130 -0
- package/src/types.ts +96 -0
package/src/reconcile.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { BootstrapState, CaptureDedupeState, ReconcileState } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BOOTSTRAP: BootstrapState = {
|
|
6
|
+
version: 1,
|
|
7
|
+
completed: false,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const DEFAULT_RECONCILE: ReconcileState = {
|
|
11
|
+
version: 1,
|
|
12
|
+
entries: {},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CAPTURE_DEDUPE: CaptureDedupeState = {
|
|
16
|
+
version: 1,
|
|
17
|
+
seen: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type StatePaths = {
|
|
21
|
+
rootDir: string;
|
|
22
|
+
bootstrapFile: string;
|
|
23
|
+
reconcileFile: string;
|
|
24
|
+
captureDedupeFile: string;
|
|
25
|
+
stateLockFile: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function createStatePaths(stateDir: string): StatePaths {
|
|
29
|
+
const rootDir = path.join(stateDir, "memory-braid");
|
|
30
|
+
return {
|
|
31
|
+
rootDir,
|
|
32
|
+
bootstrapFile: path.join(rootDir, "bootstrap-checkpoint.v1.json"),
|
|
33
|
+
reconcileFile: path.join(rootDir, "reconcile-state.v1.json"),
|
|
34
|
+
captureDedupeFile: path.join(rootDir, "capture-dedupe.v1.json"),
|
|
35
|
+
stateLockFile: path.join(rootDir, "state.v1.lock"),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function ensureStateDir(paths: StatePaths): Promise<void> {
|
|
40
|
+
await fs.mkdir(paths.rootDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readJsonFile<T>(filePath: string, fallback: T): Promise<T> {
|
|
44
|
+
try {
|
|
45
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
46
|
+
const parsed = JSON.parse(raw) as T;
|
|
47
|
+
return parsed;
|
|
48
|
+
} catch {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
54
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
55
|
+
const tmpPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
56
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
57
|
+
await fs.rename(tmpPath, filePath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function readBootstrapState(paths: StatePaths): Promise<BootstrapState> {
|
|
61
|
+
const value = await readJsonFile(paths.bootstrapFile, DEFAULT_BOOTSTRAP);
|
|
62
|
+
return { ...DEFAULT_BOOTSTRAP, ...value };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function writeBootstrapState(paths: StatePaths, state: BootstrapState): Promise<void> {
|
|
66
|
+
await writeJsonFile(paths.bootstrapFile, state);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function readReconcileState(paths: StatePaths): Promise<ReconcileState> {
|
|
70
|
+
const value = await readJsonFile(paths.reconcileFile, DEFAULT_RECONCILE);
|
|
71
|
+
return {
|
|
72
|
+
version: 1,
|
|
73
|
+
entries: value.entries ?? {},
|
|
74
|
+
lastRunAt: value.lastRunAt,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function writeReconcileState(paths: StatePaths, state: ReconcileState): Promise<void> {
|
|
79
|
+
await writeJsonFile(paths.reconcileFile, state);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function readCaptureDedupeState(paths: StatePaths): Promise<CaptureDedupeState> {
|
|
83
|
+
const value = await readJsonFile(paths.captureDedupeFile, DEFAULT_CAPTURE_DEDUPE);
|
|
84
|
+
return {
|
|
85
|
+
version: 1,
|
|
86
|
+
seen: value.seen ?? {},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function writeCaptureDedupeState(
|
|
91
|
+
paths: StatePaths,
|
|
92
|
+
state: CaptureDedupeState,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
await writeJsonFile(paths.captureDedupeFile, state);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function withStateLock<T>(
|
|
98
|
+
lockFilePath: string,
|
|
99
|
+
fn: () => Promise<T>,
|
|
100
|
+
options?: { retries?: number; retryDelayMs?: number },
|
|
101
|
+
): Promise<T> {
|
|
102
|
+
const retries = options?.retries ?? 12;
|
|
103
|
+
const retryDelayMs = options?.retryDelayMs ?? 150;
|
|
104
|
+
await fs.mkdir(path.dirname(lockFilePath), { recursive: true });
|
|
105
|
+
|
|
106
|
+
let handle: fs.FileHandle | null = null;
|
|
107
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
108
|
+
try {
|
|
109
|
+
handle = await fs.open(lockFilePath, "wx");
|
|
110
|
+
break;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const code = (err as { code?: string }).code;
|
|
113
|
+
if (code !== "EEXIST" || attempt >= retries) {
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!handle) {
|
|
121
|
+
throw new Error(`Failed to acquire lock for ${lockFilePath}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
return await fn();
|
|
126
|
+
} finally {
|
|
127
|
+
await handle.close().catch(() => undefined);
|
|
128
|
+
await fs.unlink(lockFilePath).catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type MemoryBraidSource = "local" | "mem0";
|
|
2
|
+
|
|
3
|
+
export type ManagedSourceType = "markdown" | "session";
|
|
4
|
+
|
|
5
|
+
export type PersistedSourceType = ManagedSourceType | "capture";
|
|
6
|
+
|
|
7
|
+
export type ScopeKey = {
|
|
8
|
+
workspaceHash: string;
|
|
9
|
+
agentId: string;
|
|
10
|
+
sessionKey?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type TargetWorkspace = {
|
|
14
|
+
workspaceDir: string;
|
|
15
|
+
stateDir: string;
|
|
16
|
+
agentId: string;
|
|
17
|
+
workspaceHash: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MemoryBraidResult = {
|
|
21
|
+
id?: string;
|
|
22
|
+
source: MemoryBraidSource;
|
|
23
|
+
path?: string;
|
|
24
|
+
startLine?: number;
|
|
25
|
+
endLine?: number;
|
|
26
|
+
snippet: string;
|
|
27
|
+
score: number;
|
|
28
|
+
mergedScore?: number;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
chunkKey?: string;
|
|
31
|
+
contentHash?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
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;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type IndexedEntry = {
|
|
46
|
+
chunkKey: string;
|
|
47
|
+
id?: string;
|
|
48
|
+
contentHash: string;
|
|
49
|
+
sourceType: PersistedSourceType;
|
|
50
|
+
path?: string;
|
|
51
|
+
workspaceHash: string;
|
|
52
|
+
agentId: string;
|
|
53
|
+
updatedAt: number;
|
|
54
|
+
missingCount?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ReconcileState = {
|
|
58
|
+
version: 1;
|
|
59
|
+
entries: Record<string, IndexedEntry>;
|
|
60
|
+
lastRunAt?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
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
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type CaptureDedupeState = {
|
|
79
|
+
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;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type ExtractedCandidate = {
|
|
92
|
+
text: string;
|
|
93
|
+
category: "preference" | "decision" | "fact" | "task" | "other";
|
|
94
|
+
score: number;
|
|
95
|
+
source: "heuristic" | "ml";
|
|
96
|
+
};
|