dsh-tdai-memory 0.2.13 → 0.2.14
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 -21
- package/THIRD-PARTY-NOTICES.md +20 -20
- package/client.js +8 -4
- package/cordis.patch.yml +8 -8
- package/index.js +414 -414
- package/package.json +1 -1
- package/recall-inject.js +86 -86
- package/vendor/tdai/adapters/index.js +15 -15
- package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
- package/vendor/tdai/adapters/openclaw/index.js +5 -5
- package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
- package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
- package/vendor/tdai/adapters/standalone/index.js +5 -5
- package/vendor/tdai/adapters/standalone/llm-runner.js +246 -246
- package/vendor/tdai/config.js +363 -363
- package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
- package/vendor/tdai/core/hooks/auto-capture.js +236 -236
- package/vendor/tdai/core/hooks/auto-recall.js +653 -653
- package/vendor/tdai/core/index.js +8 -8
- package/vendor/tdai/core/persona/persona-generator.js +184 -184
- package/vendor/tdai/core/persona/persona-trigger.js +94 -94
- package/vendor/tdai/core/profile/profile-sync.js +200 -200
- package/vendor/tdai/core/prompts/l1-dedup.js +136 -136
- package/vendor/tdai/core/prompts/l1-extraction.js +132 -132
- package/vendor/tdai/core/prompts/persona-generation.js +160 -160
- package/vendor/tdai/core/prompts/scene-extraction.js +269 -269
- package/vendor/tdai/core/record/l1-dedup.js +318 -318
- package/vendor/tdai/core/record/l1-extractor.js +379 -379
- package/vendor/tdai/core/record/l1-reader.js +178 -178
- package/vendor/tdai/core/record/l1-writer.js +141 -141
- package/vendor/tdai/core/report/reporter.js +86 -86
- package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
- package/vendor/tdai/core/scene/scene-extractor.js +410 -410
- package/vendor/tdai/core/scene/scene-format.js +53 -53
- package/vendor/tdai/core/scene/scene-index.js +82 -82
- package/vendor/tdai/core/scene/scene-navigation.js +60 -60
- package/vendor/tdai/core/seed/input.js +404 -404
- package/vendor/tdai/core/seed/seed-runtime.js +314 -314
- package/vendor/tdai/core/seed/types.js +9 -9
- package/vendor/tdai/core/store/bm25-client.js +117 -117
- package/vendor/tdai/core/store/bm25-local.js +68 -68
- package/vendor/tdai/core/store/embedding.js +632 -632
- package/vendor/tdai/core/store/factory.js +95 -95
- package/vendor/tdai/core/store/search-utils.js +54 -54
- package/vendor/tdai/core/store/sqlite.js +1822 -1822
- package/vendor/tdai/core/store/tcvdb-client.js +221 -221
- package/vendor/tdai/core/store/tcvdb.js +1047 -1047
- package/vendor/tdai/core/store/types.js +17 -17
- package/vendor/tdai/core/tdai-core.js +446 -446
- package/vendor/tdai/core/tools/conversation-search.js +199 -199
- package/vendor/tdai/core/tools/memory-search.js +207 -207
- package/vendor/tdai/core/types.js +12 -12
- package/vendor/tdai/utils/backup.js +189 -189
- package/vendor/tdai/utils/checkpoint.js +360 -360
- package/vendor/tdai/utils/clean-context-runner.js +472 -472
- package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
- package/vendor/tdai/utils/env.js +13 -13
- package/vendor/tdai/utils/managed-timer.js +127 -127
- package/vendor/tdai/utils/manifest.js +91 -91
- package/vendor/tdai/utils/memory-cleaner.js +300 -300
- package/vendor/tdai/utils/no-think-fetch.js +109 -109
- package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
- package/vendor/tdai/utils/pipeline-factory.js +517 -517
- package/vendor/tdai/utils/pipeline-manager.js +892 -892
- package/vendor/tdai/utils/sanitize.js +360 -360
- package/vendor/tdai/utils/serial-queue.js +103 -103
- package/vendor/tdai/utils/session-filter.js +88 -88
- package/vendor/tdai/utils/text-utils.js +29 -29
- package/vendor/tdai/utils/time.js +252 -252
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { readSceneIndex, syncSceneIndex } from "../scene/scene-index.js";
|
|
5
|
-
import { generateSceneNavigation, stripSceneNavigation } from "../scene/scene-navigation.js";
|
|
6
|
-
const PROFILE_SCOPE = "global";
|
|
7
|
-
/** Check if an error is a rename race condition (another concurrent pull won). */
|
|
8
|
-
function isRenameRaceError(err) {
|
|
9
|
-
const code = err?.code;
|
|
10
|
-
return code === "ENOTEMPTY" || code === "EEXIST";
|
|
11
|
-
}
|
|
12
|
-
export function buildProfileStableId(scope, type, filename) {
|
|
13
|
-
const hash = createHash("sha256")
|
|
14
|
-
.update(`${scope}\u0000${type}\u0000${filename}`)
|
|
15
|
-
.digest("hex");
|
|
16
|
-
return `profile:v1:${hash}`;
|
|
17
|
-
}
|
|
18
|
-
function md5(text) {
|
|
19
|
-
return createHash("md5").update(text).digest("hex");
|
|
20
|
-
}
|
|
21
|
-
async function statTimes(filePath) {
|
|
22
|
-
try {
|
|
23
|
-
const stat = await fs.stat(filePath);
|
|
24
|
-
return {
|
|
25
|
-
createdAtMs: Math.floor(stat.birthtimeMs || stat.ctimeMs || Date.now()),
|
|
26
|
-
updatedAtMs: Math.floor(stat.mtimeMs || Date.now()),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
const now = Date.now();
|
|
31
|
-
return { createdAtMs: now, updatedAtMs: now };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
async function refreshPersonaNavigation(dataDir) {
|
|
35
|
-
const personaPath = path.join(dataDir, "persona.md");
|
|
36
|
-
let body;
|
|
37
|
-
try {
|
|
38
|
-
body = stripSceneNavigation(await fs.readFile(personaPath, "utf-8")).trim();
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
if (!body)
|
|
44
|
-
return;
|
|
45
|
-
const index = await readSceneIndex(dataDir);
|
|
46
|
-
const nav = generateSceneNavigation(index);
|
|
47
|
-
const finalContent = nav ? `${body}\n\n${nav}\n` : `${body}\n`;
|
|
48
|
-
await fs.writeFile(personaPath, finalContent, "utf-8");
|
|
49
|
-
}
|
|
50
|
-
export async function listLocalProfiles(dataDir) {
|
|
51
|
-
const profiles = [];
|
|
52
|
-
const blocksDir = path.join(dataDir, "scene_blocks");
|
|
53
|
-
try {
|
|
54
|
-
const files = (await fs.readdir(blocksDir)).filter((file) => file.endsWith(".md")).sort();
|
|
55
|
-
for (const filename of files) {
|
|
56
|
-
const filePath = path.join(blocksDir, filename);
|
|
57
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
58
|
-
const { createdAtMs, updatedAtMs } = await statTimes(filePath);
|
|
59
|
-
profiles.push({
|
|
60
|
-
id: buildProfileStableId(PROFILE_SCOPE, "l2", filename),
|
|
61
|
-
type: "l2",
|
|
62
|
-
filename,
|
|
63
|
-
content,
|
|
64
|
-
contentMd5: md5(content),
|
|
65
|
-
version: 0,
|
|
66
|
-
createdAtMs,
|
|
67
|
-
updatedAtMs,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// ignore missing scene_blocks directory
|
|
73
|
-
}
|
|
74
|
-
const personaPath = path.join(dataDir, "persona.md");
|
|
75
|
-
try {
|
|
76
|
-
const rawPersona = await fs.readFile(personaPath, "utf-8");
|
|
77
|
-
const body = stripSceneNavigation(rawPersona).trim();
|
|
78
|
-
if (body) {
|
|
79
|
-
const { createdAtMs, updatedAtMs } = await statTimes(personaPath);
|
|
80
|
-
profiles.push({
|
|
81
|
-
id: buildProfileStableId(PROFILE_SCOPE, "l3", "persona.md"),
|
|
82
|
-
type: "l3",
|
|
83
|
-
filename: "persona.md",
|
|
84
|
-
content: body,
|
|
85
|
-
contentMd5: md5(body),
|
|
86
|
-
version: 0,
|
|
87
|
-
createdAtMs,
|
|
88
|
-
updatedAtMs,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// ignore missing persona file
|
|
94
|
-
}
|
|
95
|
-
return profiles;
|
|
96
|
-
}
|
|
97
|
-
export async function pullProfilesToLocal(dataDir, store, logger) {
|
|
98
|
-
if (!store.pullProfiles)
|
|
99
|
-
return new Map();
|
|
100
|
-
const records = await store.pullProfiles();
|
|
101
|
-
const baseline = new Map();
|
|
102
|
-
const tempDir = await fs.mkdtemp(path.join(dataDir, ".profiles-pull-"));
|
|
103
|
-
const tempBlocksDir = path.join(tempDir, "scene_blocks");
|
|
104
|
-
await fs.mkdir(tempBlocksDir, { recursive: true });
|
|
105
|
-
try {
|
|
106
|
-
for (const record of records) {
|
|
107
|
-
baseline.set(record.id, {
|
|
108
|
-
version: record.version,
|
|
109
|
-
contentMd5: record.contentMd5,
|
|
110
|
-
createdAtMs: record.createdAtMs,
|
|
111
|
-
});
|
|
112
|
-
if (record.type === "l2") {
|
|
113
|
-
const target = path.join(tempBlocksDir, record.filename);
|
|
114
|
-
await fs.writeFile(target, record.content, "utf-8");
|
|
115
|
-
if (md5(record.content) !== record.contentMd5) {
|
|
116
|
-
await fs.rm(target, { force: true });
|
|
117
|
-
logger.debug?.(`[memory-tdai][profile-sync] MD5 mismatch for ${record.filename} (will re-pull on next sync)`);
|
|
118
|
-
}
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (record.type === "l3") {
|
|
122
|
-
const body = stripSceneNavigation(record.content).trim();
|
|
123
|
-
await fs.writeFile(path.join(tempDir, "persona.md"), body, "utf-8");
|
|
124
|
-
if (md5(body) !== record.contentMd5) {
|
|
125
|
-
await fs.rm(path.join(tempDir, "persona.md"), { force: true });
|
|
126
|
-
logger.debug?.(`[memory-tdai][profile-sync] MD5 mismatch for ${record.filename} (will re-pull on next sync)`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const localBlocksDir = path.join(dataDir, "scene_blocks");
|
|
131
|
-
await fs.rm(localBlocksDir, { recursive: true, force: true });
|
|
132
|
-
await fs.mkdir(path.dirname(localBlocksDir), { recursive: true });
|
|
133
|
-
try {
|
|
134
|
-
await fs.rename(tempBlocksDir, localBlocksDir);
|
|
135
|
-
}
|
|
136
|
-
catch (err) {
|
|
137
|
-
if (isRenameRaceError(err)) {
|
|
138
|
-
// Another concurrent pull already wrote scene_blocks — ours is redundant.
|
|
139
|
-
// Both pulls fetched the same remote snapshot, so the other result is equivalent.
|
|
140
|
-
logger.debug?.(`[memory-tdai][profile-sync] scene_blocks rename lost race (${err.code}), using existing`);
|
|
141
|
-
return baseline;
|
|
142
|
-
}
|
|
143
|
-
throw err;
|
|
144
|
-
}
|
|
145
|
-
const tempPersonaPath = path.join(tempDir, "persona.md");
|
|
146
|
-
const localPersonaPath = path.join(dataDir, "persona.md");
|
|
147
|
-
try {
|
|
148
|
-
await fs.access(tempPersonaPath);
|
|
149
|
-
await fs.rm(localPersonaPath, { force: true });
|
|
150
|
-
try {
|
|
151
|
-
await fs.rename(tempPersonaPath, localPersonaPath);
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
if (!isRenameRaceError(err))
|
|
155
|
-
throw err;
|
|
156
|
-
logger.debug?.(`[memory-tdai][profile-sync] persona.md rename lost race, using existing`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
// No temp persona file → remove local persona (remote has none)
|
|
161
|
-
if (err.code === "ENOENT") {
|
|
162
|
-
await fs.rm(localPersonaPath, { force: true });
|
|
163
|
-
}
|
|
164
|
-
else if (!isRenameRaceError(err)) {
|
|
165
|
-
throw err;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
await syncSceneIndex(dataDir);
|
|
169
|
-
await refreshPersonaNavigation(dataDir);
|
|
170
|
-
logger.debug?.(`[memory-tdai][profile-sync] Pulled ${records.length} profile(s) to local cache`);
|
|
171
|
-
return baseline;
|
|
172
|
-
}
|
|
173
|
-
finally {
|
|
174
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
export async function syncLocalProfilesToStore(dataDir, store, baselineMap, logger) {
|
|
178
|
-
const localProfiles = await listLocalProfiles(dataDir);
|
|
179
|
-
const localIds = new Set(localProfiles.map((profile) => profile.id));
|
|
180
|
-
const syncRecords = localProfiles
|
|
181
|
-
.filter((profile) => baselineMap.get(profile.id)?.contentMd5 !== profile.contentMd5 || !baselineMap.has(profile.id))
|
|
182
|
-
.map((profile) => ({
|
|
183
|
-
...profile,
|
|
184
|
-
baselineVersion: baselineMap.get(profile.id)?.version,
|
|
185
|
-
}));
|
|
186
|
-
if (syncRecords.length > 0 && store.syncProfiles) {
|
|
187
|
-
await store.syncProfiles(syncRecords);
|
|
188
|
-
logger.info(`[memory-tdai][profile-sync] Synced ${syncRecords.length} changed profile(s)`);
|
|
189
|
-
}
|
|
190
|
-
const deletedIds = [...baselineMap.keys()].filter((id) => !localIds.has(id));
|
|
191
|
-
if (deletedIds.length > 0 && store.deleteProfiles) {
|
|
192
|
-
await store.deleteProfiles(deletedIds);
|
|
193
|
-
logger.info(`[memory-tdai][profile-sync] Deleted ${deletedIds.length} stale profile(s)`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
export async function ensureL2L3Local(dataDir, store, logger) {
|
|
197
|
-
if (!store.pullProfiles)
|
|
198
|
-
return new Map();
|
|
199
|
-
return pullProfilesToLocal(dataDir, store, logger);
|
|
200
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readSceneIndex, syncSceneIndex } from "../scene/scene-index.js";
|
|
5
|
+
import { generateSceneNavigation, stripSceneNavigation } from "../scene/scene-navigation.js";
|
|
6
|
+
const PROFILE_SCOPE = "global";
|
|
7
|
+
/** Check if an error is a rename race condition (another concurrent pull won). */
|
|
8
|
+
function isRenameRaceError(err) {
|
|
9
|
+
const code = err?.code;
|
|
10
|
+
return code === "ENOTEMPTY" || code === "EEXIST";
|
|
11
|
+
}
|
|
12
|
+
export function buildProfileStableId(scope, type, filename) {
|
|
13
|
+
const hash = createHash("sha256")
|
|
14
|
+
.update(`${scope}\u0000${type}\u0000${filename}`)
|
|
15
|
+
.digest("hex");
|
|
16
|
+
return `profile:v1:${hash}`;
|
|
17
|
+
}
|
|
18
|
+
function md5(text) {
|
|
19
|
+
return createHash("md5").update(text).digest("hex");
|
|
20
|
+
}
|
|
21
|
+
async function statTimes(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
const stat = await fs.stat(filePath);
|
|
24
|
+
return {
|
|
25
|
+
createdAtMs: Math.floor(stat.birthtimeMs || stat.ctimeMs || Date.now()),
|
|
26
|
+
updatedAtMs: Math.floor(stat.mtimeMs || Date.now()),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
return { createdAtMs: now, updatedAtMs: now };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function refreshPersonaNavigation(dataDir) {
|
|
35
|
+
const personaPath = path.join(dataDir, "persona.md");
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = stripSceneNavigation(await fs.readFile(personaPath, "utf-8")).trim();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!body)
|
|
44
|
+
return;
|
|
45
|
+
const index = await readSceneIndex(dataDir);
|
|
46
|
+
const nav = generateSceneNavigation(index);
|
|
47
|
+
const finalContent = nav ? `${body}\n\n${nav}\n` : `${body}\n`;
|
|
48
|
+
await fs.writeFile(personaPath, finalContent, "utf-8");
|
|
49
|
+
}
|
|
50
|
+
export async function listLocalProfiles(dataDir) {
|
|
51
|
+
const profiles = [];
|
|
52
|
+
const blocksDir = path.join(dataDir, "scene_blocks");
|
|
53
|
+
try {
|
|
54
|
+
const files = (await fs.readdir(blocksDir)).filter((file) => file.endsWith(".md")).sort();
|
|
55
|
+
for (const filename of files) {
|
|
56
|
+
const filePath = path.join(blocksDir, filename);
|
|
57
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
58
|
+
const { createdAtMs, updatedAtMs } = await statTimes(filePath);
|
|
59
|
+
profiles.push({
|
|
60
|
+
id: buildProfileStableId(PROFILE_SCOPE, "l2", filename),
|
|
61
|
+
type: "l2",
|
|
62
|
+
filename,
|
|
63
|
+
content,
|
|
64
|
+
contentMd5: md5(content),
|
|
65
|
+
version: 0,
|
|
66
|
+
createdAtMs,
|
|
67
|
+
updatedAtMs,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// ignore missing scene_blocks directory
|
|
73
|
+
}
|
|
74
|
+
const personaPath = path.join(dataDir, "persona.md");
|
|
75
|
+
try {
|
|
76
|
+
const rawPersona = await fs.readFile(personaPath, "utf-8");
|
|
77
|
+
const body = stripSceneNavigation(rawPersona).trim();
|
|
78
|
+
if (body) {
|
|
79
|
+
const { createdAtMs, updatedAtMs } = await statTimes(personaPath);
|
|
80
|
+
profiles.push({
|
|
81
|
+
id: buildProfileStableId(PROFILE_SCOPE, "l3", "persona.md"),
|
|
82
|
+
type: "l3",
|
|
83
|
+
filename: "persona.md",
|
|
84
|
+
content: body,
|
|
85
|
+
contentMd5: md5(body),
|
|
86
|
+
version: 0,
|
|
87
|
+
createdAtMs,
|
|
88
|
+
updatedAtMs,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// ignore missing persona file
|
|
94
|
+
}
|
|
95
|
+
return profiles;
|
|
96
|
+
}
|
|
97
|
+
export async function pullProfilesToLocal(dataDir, store, logger) {
|
|
98
|
+
if (!store.pullProfiles)
|
|
99
|
+
return new Map();
|
|
100
|
+
const records = await store.pullProfiles();
|
|
101
|
+
const baseline = new Map();
|
|
102
|
+
const tempDir = await fs.mkdtemp(path.join(dataDir, ".profiles-pull-"));
|
|
103
|
+
const tempBlocksDir = path.join(tempDir, "scene_blocks");
|
|
104
|
+
await fs.mkdir(tempBlocksDir, { recursive: true });
|
|
105
|
+
try {
|
|
106
|
+
for (const record of records) {
|
|
107
|
+
baseline.set(record.id, {
|
|
108
|
+
version: record.version,
|
|
109
|
+
contentMd5: record.contentMd5,
|
|
110
|
+
createdAtMs: record.createdAtMs,
|
|
111
|
+
});
|
|
112
|
+
if (record.type === "l2") {
|
|
113
|
+
const target = path.join(tempBlocksDir, record.filename);
|
|
114
|
+
await fs.writeFile(target, record.content, "utf-8");
|
|
115
|
+
if (md5(record.content) !== record.contentMd5) {
|
|
116
|
+
await fs.rm(target, { force: true });
|
|
117
|
+
logger.debug?.(`[memory-tdai][profile-sync] MD5 mismatch for ${record.filename} (will re-pull on next sync)`);
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (record.type === "l3") {
|
|
122
|
+
const body = stripSceneNavigation(record.content).trim();
|
|
123
|
+
await fs.writeFile(path.join(tempDir, "persona.md"), body, "utf-8");
|
|
124
|
+
if (md5(body) !== record.contentMd5) {
|
|
125
|
+
await fs.rm(path.join(tempDir, "persona.md"), { force: true });
|
|
126
|
+
logger.debug?.(`[memory-tdai][profile-sync] MD5 mismatch for ${record.filename} (will re-pull on next sync)`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const localBlocksDir = path.join(dataDir, "scene_blocks");
|
|
131
|
+
await fs.rm(localBlocksDir, { recursive: true, force: true });
|
|
132
|
+
await fs.mkdir(path.dirname(localBlocksDir), { recursive: true });
|
|
133
|
+
try {
|
|
134
|
+
await fs.rename(tempBlocksDir, localBlocksDir);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (isRenameRaceError(err)) {
|
|
138
|
+
// Another concurrent pull already wrote scene_blocks — ours is redundant.
|
|
139
|
+
// Both pulls fetched the same remote snapshot, so the other result is equivalent.
|
|
140
|
+
logger.debug?.(`[memory-tdai][profile-sync] scene_blocks rename lost race (${err.code}), using existing`);
|
|
141
|
+
return baseline;
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
const tempPersonaPath = path.join(tempDir, "persona.md");
|
|
146
|
+
const localPersonaPath = path.join(dataDir, "persona.md");
|
|
147
|
+
try {
|
|
148
|
+
await fs.access(tempPersonaPath);
|
|
149
|
+
await fs.rm(localPersonaPath, { force: true });
|
|
150
|
+
try {
|
|
151
|
+
await fs.rename(tempPersonaPath, localPersonaPath);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (!isRenameRaceError(err))
|
|
155
|
+
throw err;
|
|
156
|
+
logger.debug?.(`[memory-tdai][profile-sync] persona.md rename lost race, using existing`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// No temp persona file → remove local persona (remote has none)
|
|
161
|
+
if (err.code === "ENOENT") {
|
|
162
|
+
await fs.rm(localPersonaPath, { force: true });
|
|
163
|
+
}
|
|
164
|
+
else if (!isRenameRaceError(err)) {
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
await syncSceneIndex(dataDir);
|
|
169
|
+
await refreshPersonaNavigation(dataDir);
|
|
170
|
+
logger.debug?.(`[memory-tdai][profile-sync] Pulled ${records.length} profile(s) to local cache`);
|
|
171
|
+
return baseline;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function syncLocalProfilesToStore(dataDir, store, baselineMap, logger) {
|
|
178
|
+
const localProfiles = await listLocalProfiles(dataDir);
|
|
179
|
+
const localIds = new Set(localProfiles.map((profile) => profile.id));
|
|
180
|
+
const syncRecords = localProfiles
|
|
181
|
+
.filter((profile) => baselineMap.get(profile.id)?.contentMd5 !== profile.contentMd5 || !baselineMap.has(profile.id))
|
|
182
|
+
.map((profile) => ({
|
|
183
|
+
...profile,
|
|
184
|
+
baselineVersion: baselineMap.get(profile.id)?.version,
|
|
185
|
+
}));
|
|
186
|
+
if (syncRecords.length > 0 && store.syncProfiles) {
|
|
187
|
+
await store.syncProfiles(syncRecords);
|
|
188
|
+
logger.info(`[memory-tdai][profile-sync] Synced ${syncRecords.length} changed profile(s)`);
|
|
189
|
+
}
|
|
190
|
+
const deletedIds = [...baselineMap.keys()].filter((id) => !localIds.has(id));
|
|
191
|
+
if (deletedIds.length > 0 && store.deleteProfiles) {
|
|
192
|
+
await store.deleteProfiles(deletedIds);
|
|
193
|
+
logger.info(`[memory-tdai][profile-sync] Deleted ${deletedIds.length} stale profile(s)`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export async function ensureL2L3Local(dataDir, store, logger) {
|
|
197
|
+
if (!store.pullProfiles)
|
|
198
|
+
return new Map();
|
|
199
|
+
return pullProfilesToLocal(dataDir, store, logger);
|
|
200
|
+
}
|