@yandy0725/memory-core 2.0.1 → 2.1.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/dist/config.d.ts +66 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +69 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +28 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -5
- package/dist/index.js.map +1 -1
- package/dist/injection.d.ts.map +1 -1
- package/dist/injection.js +1 -1
- package/dist/injection.js.map +1 -1
- package/dist/orchestration/conversation.d.ts.map +1 -1
- package/dist/orchestration/conversation.js +1 -2
- package/dist/orchestration/conversation.js.map +1 -1
- package/dist/orchestration/ports.d.ts.map +1 -1
- package/dist/orchestration/task.d.ts.map +1 -1
- package/dist/orchestration/task.js +1 -1
- package/dist/orchestration/task.js.map +1 -1
- package/dist/store/filename.d.ts +43 -0
- package/dist/store/filename.d.ts.map +1 -1
- package/dist/store/filename.js +72 -0
- package/dist/store/filename.js.map +1 -1
- package/dist/store/fs-lock.d.ts.map +1 -1
- package/dist/store/fs-lock.js +1 -3
- package/dist/store/fs-lock.js.map +1 -1
- package/dist/store/memory-store.d.ts +37 -0
- package/dist/store/memory-store.d.ts.map +1 -1
- package/dist/store/memory-store.js +169 -17
- package/dist/store/memory-store.js.map +1 -1
- package/dist/store/paths.d.ts.map +1 -1
- package/dist/store/paths.js +8 -2
- package/dist/store/paths.js.map +1 -1
- package/dist/store/process-lock.d.ts.map +1 -1
- package/dist/store/process-lock.js.map +1 -1
- package/dist/sync/backend-git.d.ts +27 -0
- package/dist/sync/backend-git.d.ts.map +1 -0
- package/dist/sync/backend-git.js +815 -0
- package/dist/sync/backend-git.js.map +1 -0
- package/dist/sync/backend.d.ts +213 -0
- package/dist/sync/backend.d.ts.map +1 -0
- package/dist/sync/backend.js +31 -0
- package/dist/sync/backend.js.map +1 -0
- package/dist/sync/conflict.d.ts +74 -0
- package/dist/sync/conflict.d.ts.map +1 -0
- package/dist/sync/conflict.js +164 -0
- package/dist/sync/conflict.js.map +1 -0
- package/dist/sync/device.d.ts +45 -0
- package/dist/sync/device.d.ts.map +1 -0
- package/dist/sync/device.js +227 -0
- package/dist/sync/device.js.map +1 -0
- package/dist/sync/diff.d.ts +78 -0
- package/dist/sync/diff.d.ts.map +1 -0
- package/dist/sync/diff.js +115 -0
- package/dist/sync/diff.js.map +1 -0
- package/dist/sync/engine.d.ts +142 -0
- package/dist/sync/engine.d.ts.map +1 -0
- package/dist/sync/engine.js +960 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/hash.d.ts +11 -0
- package/dist/sync/hash.d.ts.map +1 -0
- package/dist/sync/hash.js +14 -0
- package/dist/sync/hash.js.map +1 -0
- package/dist/sync/manifest.d.ts +63 -0
- package/dist/sync/manifest.d.ts.map +1 -0
- package/dist/sync/manifest.js +117 -0
- package/dist/sync/manifest.js.map +1 -0
- package/dist/sync/paths.d.ts +70 -0
- package/dist/sync/paths.d.ts.map +1 -0
- package/dist/sync/paths.js +111 -0
- package/dist/sync/paths.js.map +1 -0
- package/dist/sync/scan.d.ts +44 -0
- package/dist/sync/scan.d.ts.map +1 -0
- package/dist/sync/scan.js +102 -0
- package/dist/sync/scan.js.map +1 -0
- package/dist/sync/scheduler.d.ts +81 -0
- package/dist/sync/scheduler.d.ts.map +1 -0
- package/dist/sync/scheduler.js +183 -0
- package/dist/sync/scheduler.js.map +1 -0
- package/dist/sync/types.d.ts +21 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +8 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/tool/handler.d.ts.map +1 -1
- package/dist/tool/handler.js +1 -3
- package/dist/tool/handler.js.map +1 -1
- package/package.json +13 -3
- package/src/config.ts +121 -0
- package/src/index.ts +101 -5
- package/src/injection.ts +1 -3
- package/src/orchestration/conversation.ts +1 -2
- package/src/orchestration/dream.ts +0 -1
- package/src/orchestration/extract.ts +0 -1
- package/src/orchestration/ports.ts +1 -5
- package/src/orchestration/prompts.ts +0 -1
- package/src/orchestration/surfacing.ts +0 -1
- package/src/orchestration/task.ts +2 -10
- package/src/store/filename.ts +71 -0
- package/src/store/fs-lock.ts +2 -6
- package/src/store/memory-store.ts +311 -134
- package/src/store/paths.ts +12 -3
- package/src/store/process-lock.ts +4 -1
- package/src/sync/backend-git.ts +902 -0
- package/src/sync/backend.ts +220 -0
- package/src/sync/conflict.ts +247 -0
- package/src/sync/device.ts +266 -0
- package/src/sync/diff.ts +152 -0
- package/src/sync/engine.ts +1258 -0
- package/src/sync/hash.ts +14 -0
- package/src/sync/manifest.ts +148 -0
- package/src/sync/paths.ts +117 -0
- package/src/sync/scan.ts +115 -0
- package/src/sync/scheduler.ts +241 -0
- package/src/sync/types.ts +22 -0
- package/src/tool/handler.ts +1 -3
package/src/config.ts
CHANGED
|
@@ -52,6 +52,51 @@ export interface ExtractMemoriesConfig {
|
|
|
52
52
|
maxAssistantChars: number;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 跨设备同步的配置子树。
|
|
57
|
+
*
|
|
58
|
+
* 这里**只**收两个宿主共有的语义(core 的配置契约不收单宿主策略键,同 `dream` 的触发策略归宿主):
|
|
59
|
+
* 凡是只有一侧能可靠挂上的接线细节(比如退出时再推一次)就**不设键**,而不是设一个「配了不生效」的键。
|
|
60
|
+
*
|
|
61
|
+
* 本节不触碰文件系统:core 只给默认值与合并语义,配置由适配层从自己的配置源读出后交给 `mergeConfig`。
|
|
62
|
+
*/
|
|
63
|
+
export interface SyncConfig {
|
|
64
|
+
/**
|
|
65
|
+
* opt-in:自动往远端写数据必须由用户显式承担(同 `extractMemories`)。
|
|
66
|
+
*
|
|
67
|
+
* **必须是布尔值**,而且消费方一律按 `=== true` 判定:这是一道**同意边界**,不是类型洁癖。
|
|
68
|
+
* 配置从宿主的 JSON 直接合并进来(`mergeConfig` 不校验类型),而 `"false"` 这种字符串在 JS 里
|
|
69
|
+
* 是真值 —— 按真值判定就会把用户明写的「关」读成「开」,记忆继续往远端推,而校验出口还报「没问题」。
|
|
70
|
+
* 不为 `true` 时其余键一律不校验(见 `syncConfigErrors`)。
|
|
71
|
+
*/
|
|
72
|
+
enabled: boolean;
|
|
73
|
+
/** git 远端 URL:只供首次初始化用;初始化之后的真相来源是暂存 clone 的 origin。 */
|
|
74
|
+
remote: string;
|
|
75
|
+
/** 远端的跟踪分支名。 */
|
|
76
|
+
branch: string;
|
|
77
|
+
/**
|
|
78
|
+
* 会话开始时自动收敛远端变更并补推(一轮 push 同时包含 pull 与发布)。
|
|
79
|
+
* 键名保留既有配置契约,不因为启动入口是 push 就改名;同样是 `=== true` 才算开(见 `enabled`)。
|
|
80
|
+
*/
|
|
81
|
+
pullOnStart: boolean;
|
|
82
|
+
/** 写入后去抖自动发布。同样是 `=== true` 才算开(见 `enabled`)。 */
|
|
83
|
+
pushOnWrite: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* 写入触发的静默期:静默期内的多条写入合并成一次推送(一轮 dream 写十几个文件 → 一次推送)。
|
|
86
|
+
* 它是**两个宿主共有**的语义(两边的写触发都按它去抖),所以属于 core;退出兜底那种只有一侧
|
|
87
|
+
* 能可靠挂上的接线细节则不设键。
|
|
88
|
+
*/
|
|
89
|
+
pushDebounceMs: number;
|
|
90
|
+
/**
|
|
91
|
+
* 自动路径(会话开始的拉取、退出兜底的推)每次 git 子进程的时间上限。
|
|
92
|
+
* 与 `timeoutMs` 分两档是刻意的:自动路径被 await 在会话启动路径上,离线 / 连不上远端时若按
|
|
93
|
+
* 手动档计时,每开一次会话都要白等满 20s(一轮至少两次子进程)。自动档取短。
|
|
94
|
+
*/
|
|
95
|
+
autoTimeoutMs: number;
|
|
96
|
+
/** 手动命令(用户正在等结果)每次 git 子进程的时间上限,比自动档宽。 */
|
|
97
|
+
timeoutMs: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
export interface MemoryConfig {
|
|
56
101
|
/** Shared defaults for model and sessionPersistence. Per-task configs override. */
|
|
57
102
|
defaults?: DefaultsConfig;
|
|
@@ -92,6 +137,11 @@ export interface MemoryConfig {
|
|
|
92
137
|
extractMemories: ExtractMemoriesConfig & {
|
|
93
138
|
sessionPersistence?: SessionPersistenceConfig;
|
|
94
139
|
};
|
|
140
|
+
/**
|
|
141
|
+
* 跨设备同步。校验出口是 `syncConfigErrors`,**非** `modelConfigErrors`:后者被消费成
|
|
142
|
+
* 「整个记忆层不可用」,少写一个远端 URL 不该有这个代价(同设计里的降级铁律)。
|
|
143
|
+
*/
|
|
144
|
+
sync: SyncConfig;
|
|
95
145
|
}
|
|
96
146
|
|
|
97
147
|
export const DEFAULT_CONFIG: MemoryConfig = {
|
|
@@ -123,6 +173,20 @@ export const DEFAULT_CONFIG: MemoryConfig = {
|
|
|
123
173
|
maxToolResultChars: 500,
|
|
124
174
|
maxAssistantChars: 2000,
|
|
125
175
|
},
|
|
176
|
+
sync: {
|
|
177
|
+
// 自动往远端写数据必须由用户显式承担:默认关闭(同 extractMemories)。
|
|
178
|
+
enabled: false,
|
|
179
|
+
// 空 remote 只在 enabled 为真时才算错(见 syncConfigErrors):默认关闭时它只是占位。
|
|
180
|
+
remote: "",
|
|
181
|
+
branch: "main",
|
|
182
|
+
pullOnStart: true,
|
|
183
|
+
pushOnWrite: true,
|
|
184
|
+
// 一轮 dream 写十几个文件 → 去抖后只推送一次。
|
|
185
|
+
pushDebounceMs: 5000,
|
|
186
|
+
// 自动档 5s / 手动档 20s:自动路径在会话启动上被 await,不能让离线把每次启动拖满。
|
|
187
|
+
autoTimeoutMs: 5000,
|
|
188
|
+
timeoutMs: 20000,
|
|
189
|
+
},
|
|
126
190
|
};
|
|
127
191
|
|
|
128
192
|
/** 需要显式模型的子任务。顺序固定:dream → extractMemories → autoSurfacing(校验信息按此顺序输出)。 */
|
|
@@ -186,6 +250,63 @@ export function modelConfigErrors(
|
|
|
186
250
|
return errors;
|
|
187
251
|
}
|
|
188
252
|
|
|
253
|
+
/**
|
|
254
|
+
* 同步配置的校验出口,**非致命**(形状仿 `modelConfigErrors`,但刻意是另一个函数)。
|
|
255
|
+
*
|
|
256
|
+
* 为什么不并进 `modelConfigErrors`:那条路的唯一消费者会把配置错误当成「整个记忆层不可用」——
|
|
257
|
+
* 不解析目录、不建 store、不注册工具、没有索引段。少写一个远端 URL 不该让用户丢掉全部记忆功能,
|
|
258
|
+
* 所以收到非空列表的适配层只需把同步当成未启用并报出来,其余记忆功能照常。
|
|
259
|
+
*
|
|
260
|
+
* 三个布尔键**无条件**校验类型(它们就是开关本身,包括 `enabled`);其余键只在 `enabled === true`
|
|
261
|
+
* 时才检查:关掉的开关不该逼用户先去修它管辖的键。
|
|
262
|
+
* 顺序固定:enabled → pullOnStart → pushOnWrite → remote → branch → pushDebounceMs → autoTimeoutMs
|
|
263
|
+
* → timeoutMs;每条消息都点名键,便于直接改配置。
|
|
264
|
+
* `options.initialized === true` 时本地 origin 已是远端真相来源,因此只放宽 remote 为空这一条,
|
|
265
|
+
* 其余键与同意边界的校验完全不变。
|
|
266
|
+
*/
|
|
267
|
+
export function syncConfigErrors(cfg: MemoryConfig, options: { initialized?: boolean } = {}): string[] {
|
|
268
|
+
const sync = cfg.sync;
|
|
269
|
+
// 读不懂的子树(`deepMerge` 会把用户写的 `"sync": null`、甚至一个字符串原样带进来)按「未启用」
|
|
270
|
+
// 处理,一条都不报:没有键可校验,也没有开关可开。不抛 TypeError 是底线。
|
|
271
|
+
if (typeof sync !== "object" || sync === null) return [];
|
|
272
|
+
|
|
273
|
+
const errors: string[] = [];
|
|
274
|
+
// 三个布尔键按**类型**校验,不按真值:`"enabled": "false"` 在 JS 里是真值,
|
|
275
|
+
// 只判真值的话用户明写的「关」会被当成「开」,而这个函数还返回空数组(= 配置没问题)。
|
|
276
|
+
// 自动往远端推数据必须由用户显式承担,所以「不是明确的 true」一律按关处理并报出来。
|
|
277
|
+
for (const key of ["enabled", "pullOnStart", "pushOnWrite"] as const) {
|
|
278
|
+
const value: unknown = sync[key];
|
|
279
|
+
if (typeof value !== "boolean") {
|
|
280
|
+
const shown = typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
281
|
+
errors.push(`sync is disabled because "sync.${key}" must be true or false (got ${shown})`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// 不是明确的 true 就到此为止:开关没开,它管辖的键不必逼用户先修(与消费方 `=== true` 同口径)。
|
|
285
|
+
if (sync.enabled !== true) return errors;
|
|
286
|
+
|
|
287
|
+
// 非字符串是类型错误,不是「空」;initialized 只豁免空字符串。先查类型,避免 `.trim()` 把校验变成崩溃。
|
|
288
|
+
if (typeof sync.remote !== "string") {
|
|
289
|
+
errors.push(
|
|
290
|
+
`sync is enabled but "sync.remote" must be a string (got ${String(sync.remote)}) — set a remote URL or unset "sync.enabled"`,
|
|
291
|
+
);
|
|
292
|
+
} else if (options.initialized !== true && sync.remote.trim() === "") {
|
|
293
|
+
errors.push('sync is enabled but "sync.remote" is empty — set "sync.remote" in memory.json or unset "sync.enabled"');
|
|
294
|
+
}
|
|
295
|
+
if (typeof sync.branch !== "string" || sync.branch.trim() === "") {
|
|
296
|
+
errors.push('sync is enabled but "sync.branch" is empty — set a branch name or unset "sync.enabled"');
|
|
297
|
+
}
|
|
298
|
+
for (const key of ["pushDebounceMs", "autoTimeoutMs", "timeoutMs"] as const) {
|
|
299
|
+
const value: unknown = sync[key];
|
|
300
|
+
// 类型也在这里查:`mergeConfig` 不校验类型,手写的配置里 `"5000"` 会一路传到定时器 /
|
|
301
|
+
// 子进程的 budget 上,那时是一句看不懂的失败而不是一条可读错误。
|
|
302
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
303
|
+
const shown = typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
304
|
+
errors.push(`sync is enabled but "sync.${key}" must be a positive number of milliseconds (got ${shown})`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return errors;
|
|
308
|
+
}
|
|
309
|
+
|
|
189
310
|
/** 已通过启动校验的模型值;缺失时抛错(真正的守卫在适配层的会话启动路径上,这里是防御)。 */
|
|
190
311
|
export function requiredModel(cfg: MemoryConfig, task: ModelTask): string {
|
|
191
312
|
const value = taskModel(cfg, task);
|
package/src/index.ts
CHANGED
|
@@ -23,11 +23,25 @@ export {
|
|
|
23
23
|
} from "./store/entry-index.js";
|
|
24
24
|
export type { IndexCapacity, IndexLineEntry, ParsedEntryIndex } from "./store/entry-index.js";
|
|
25
25
|
|
|
26
|
-
export {
|
|
26
|
+
export {
|
|
27
|
+
ENTRY_EXT,
|
|
28
|
+
entryFileName,
|
|
29
|
+
foldFileName,
|
|
30
|
+
isEntryFileNameForRead,
|
|
31
|
+
isEntryFileNameForWrite,
|
|
32
|
+
resolveUniqueFileName,
|
|
33
|
+
} from "./store/filename.js";
|
|
27
34
|
|
|
28
35
|
// ── store/ 读写门面 ────────────────────────────────────────────────────────────
|
|
29
|
-
export { BACKUP_DIR, LOCK_FILE, MemoryStore } from "./store/memory-store.js";
|
|
30
|
-
export type {
|
|
36
|
+
export { BACKUP_DIR, INDEX_FILE, LOCK_FILE, MemoryStore } from "./store/memory-store.js";
|
|
37
|
+
export type {
|
|
38
|
+
ApplyChange,
|
|
39
|
+
ApplyResult,
|
|
40
|
+
Entry,
|
|
41
|
+
EntrySummary,
|
|
42
|
+
StoreConfig,
|
|
43
|
+
WriteOptions,
|
|
44
|
+
} from "./store/memory-store.js";
|
|
31
45
|
|
|
32
46
|
// ── store/ 并发与容错 ──────────────────────────────────────────────────────────
|
|
33
47
|
export { MemoryLockedError, readLockStatus, tryWithLock, withLock } from "./store/fs-lock.js";
|
|
@@ -36,7 +50,12 @@ export type { LockInfo, LockOptions } from "./store/fs-lock.js";
|
|
|
36
50
|
export { isTransientFsError, withFsRetry } from "./store/fs-retry.js";
|
|
37
51
|
export type { FsRetryOptions } from "./store/fs-retry.js";
|
|
38
52
|
|
|
39
|
-
export {
|
|
53
|
+
export {
|
|
54
|
+
ProcessLockTimeoutError,
|
|
55
|
+
isProcessLockActive,
|
|
56
|
+
tryWithProcessLock,
|
|
57
|
+
withProcessLock,
|
|
58
|
+
} from "./store/process-lock.js";
|
|
40
59
|
|
|
41
60
|
export { createSnapshot, pruneSnapshots, snapshotStamp } from "./store/snapshot.js";
|
|
42
61
|
export type { SnapshotOptions } from "./store/snapshot.js";
|
|
@@ -44,7 +63,13 @@ export type { SnapshotOptions } from "./store/snapshot.js";
|
|
|
44
63
|
export { withFileMutationQueue } from "./store/mutation-queue.js";
|
|
45
64
|
|
|
46
65
|
// ── store/ 路径与安全 ──────────────────────────────────────────────────────────
|
|
47
|
-
export {
|
|
66
|
+
export {
|
|
67
|
+
assertInsideRoot,
|
|
68
|
+
normalizeRemoteUrl,
|
|
69
|
+
projectDirName,
|
|
70
|
+
projectIdentity,
|
|
71
|
+
resolveMemoryDir,
|
|
72
|
+
} from "./store/paths.js";
|
|
48
73
|
export type { NamingOptions, ProjectIdentity, ProjectKind } from "./store/paths.js";
|
|
49
74
|
|
|
50
75
|
export { sanitizeForInjection, stripInvisibleChars } from "./store/sanitize.js";
|
|
@@ -61,6 +86,7 @@ export {
|
|
|
61
86
|
modelConfigErrors,
|
|
62
87
|
requiredModel,
|
|
63
88
|
requiredModels,
|
|
89
|
+
syncConfigErrors,
|
|
64
90
|
taskModel,
|
|
65
91
|
} from "./config.js";
|
|
66
92
|
export type {
|
|
@@ -71,6 +97,7 @@ export type {
|
|
|
71
97
|
MemoryConfig,
|
|
72
98
|
ModelTask,
|
|
73
99
|
SessionPersistenceConfig,
|
|
100
|
+
SyncConfig,
|
|
74
101
|
ThinkLevel,
|
|
75
102
|
} from "./config.js";
|
|
76
103
|
|
|
@@ -144,3 +171,72 @@ export type {
|
|
|
144
171
|
MemoryHandlerDeps,
|
|
145
172
|
MemoryHandlerOptions,
|
|
146
173
|
} from "./tool/handler.js";
|
|
174
|
+
|
|
175
|
+
// ── sync/ 跨设备同步 ──────────────────────────────────────────────────────────
|
|
176
|
+
// 适配层只从这里拿同步能力:装引擎(`createSyncEngine`)与写触发调度器、选或实现传输端口
|
|
177
|
+
// (`SyncBackend` / `createGitSyncBackend`)、以及读同步状态要用的远端布局与 manifest 助手。
|
|
178
|
+
// 文件名口径(读/写两个方向的 `isEntryFileNameFor*` 与 `foldFileName`)与 `applyEntryFiles` 的入参类型
|
|
179
|
+
// (`ApplyChange` / `ApplyResult`)在 store 分区、配置子树(`SyncConfig` / `syncConfigErrors`)
|
|
180
|
+
// 在配置分区:一个符号只导一次,分区跟拥有它的模块走。
|
|
181
|
+
export { NonFastForwardError } from "./sync/backend.js";
|
|
182
|
+
export type {
|
|
183
|
+
RemoteView,
|
|
184
|
+
SyncBackend,
|
|
185
|
+
SyncInitResult,
|
|
186
|
+
SyncPublishChanges,
|
|
187
|
+
SyncRunOptions,
|
|
188
|
+
SyncScope,
|
|
189
|
+
} from "./sync/backend.js";
|
|
190
|
+
|
|
191
|
+
export { createGitSyncBackend, gitAvailable } from "./sync/backend-git.js";
|
|
192
|
+
export type { GitSyncBackendOptions } from "./sync/backend-git.js";
|
|
193
|
+
|
|
194
|
+
export { conflictSuffix, resolveConflict } from "./sync/conflict.js";
|
|
195
|
+
export type { ConflictInput, ConflictOutcome, ConflictSide } from "./sync/conflict.js";
|
|
196
|
+
|
|
197
|
+
export { devicePath, loadOrCreateDevice } from "./sync/device.js";
|
|
198
|
+
export type { DeviceIdentity } from "./sync/device.js";
|
|
199
|
+
|
|
200
|
+
export { diffThreeWay, isEmptyDiff } from "./sync/diff.js";
|
|
201
|
+
export type { SyncConflict, SyncDiff } from "./sync/diff.js";
|
|
202
|
+
|
|
203
|
+
export { createSyncEngine } from "./sync/engine.js";
|
|
204
|
+
export type { SyncEngine, SyncEngineOptions, SyncRoundResult, SyncStatusReport } from "./sync/engine.js";
|
|
205
|
+
|
|
206
|
+
export { sha256Hex } from "./sync/hash.js";
|
|
207
|
+
|
|
208
|
+
export {
|
|
209
|
+
MANIFEST_VERSION,
|
|
210
|
+
emptyManifest,
|
|
211
|
+
manifestMatchesTransport,
|
|
212
|
+
manifestPath,
|
|
213
|
+
readManifest,
|
|
214
|
+
syncTransport,
|
|
215
|
+
writeManifest,
|
|
216
|
+
} from "./sync/manifest.js";
|
|
217
|
+
export type { SyncManifest, SyncTransport } from "./sync/manifest.js";
|
|
218
|
+
|
|
219
|
+
// 布局的单一来源:适配层渲染「这条记忆在远端哪里」一律调这几个,不要就地拼 `git/` 字面量;
|
|
220
|
+
// 本地状态(`.sync` 目录与根锁文件)同理 —— `syncLockPath` 必须与引擎用的是同一条派生,
|
|
221
|
+
// 否则「清除遗弃根锁」删的是一个没人用的文件。`remotePrefix` 收**身份 key**(在这里派生成项目 key),
|
|
222
|
+
// `remotePrefixFromKey` 收已经派生好的**项目 key**(适配层算 status 时手上往往就是它);
|
|
223
|
+
// 拿错一个是二次转义,不报错,只是远端多一个谁也不认的目录。
|
|
224
|
+
export {
|
|
225
|
+
SYNC_DIR_NAME,
|
|
226
|
+
remoteEntryPath,
|
|
227
|
+
remotePrefix,
|
|
228
|
+
remotePrefixFromKey,
|
|
229
|
+
remoteProjectKey,
|
|
230
|
+
sameSyncRemote,
|
|
231
|
+
syncLockPath,
|
|
232
|
+
syncRootPath,
|
|
233
|
+
} from "./sync/paths.js";
|
|
234
|
+
|
|
235
|
+
export { scanLocalEntries } from "./sync/scan.js";
|
|
236
|
+
export type { LocalScan } from "./sync/scan.js";
|
|
237
|
+
|
|
238
|
+
// 调度器是泛型:`T` 是「一轮推送的结果」,引擎那一侧代入 `SyncRoundResult` 就不必在公开边界上转型。
|
|
239
|
+
export { createSyncScheduler } from "./sync/scheduler.js";
|
|
240
|
+
export type { SyncScheduler, SyncSchedulerOptions } from "./sync/scheduler.js";
|
|
241
|
+
|
|
242
|
+
export type { SyncFileInfo, SyncFileSet } from "./sync/types.js";
|
package/src/injection.ts
CHANGED
|
@@ -211,9 +211,7 @@ export async function scanEntries(store: MemoryStore): Promise<EntryManifest[]>
|
|
|
211
211
|
const summaries = await store.listEntries();
|
|
212
212
|
return summaries
|
|
213
213
|
.map((s) => ({ file: s.file, name: s.name, description: s.description, type: s.type, modified: s.modified }))
|
|
214
|
-
.sort((a, b) =>
|
|
215
|
-
a.modified === b.modified ? a.file.localeCompare(b.file) : b.modified.localeCompare(a.modified),
|
|
216
|
-
)
|
|
214
|
+
.sort((a, b) => (a.modified === b.modified ? a.file.localeCompare(b.file) : b.modified.localeCompare(a.modified)))
|
|
217
215
|
.slice(0, SIDE_QUERY_MAX_ENTRIES);
|
|
218
216
|
}
|
|
219
217
|
|
|
@@ -63,8 +63,7 @@ function clipMiddle(text: string, maxChars: number, alreadyOmitted = 0, markerAt
|
|
|
63
63
|
if (label !== "") {
|
|
64
64
|
body =
|
|
65
65
|
markerAt >= 0 && text.startsWith(label, markerAt)
|
|
66
|
-
? text.slice(0, markerAt) +
|
|
67
|
-
text.slice(markerAt + label.length + (text[markerAt + label.length] === "\n" ? 1 : 0))
|
|
66
|
+
? text.slice(0, markerAt) + text.slice(markerAt + label.length + (text[markerAt + label.length] === "\n" ? 1 : 0))
|
|
68
67
|
: removeLastOccurrence(text, label);
|
|
69
68
|
}
|
|
70
69
|
// 给标记文本预留位置(按一个六位数省略量估算),避免「裁减之后反而更长」。
|
|
@@ -98,11 +98,7 @@ export interface ExtractMessage {
|
|
|
98
98
|
/** 宿主的会话历史:既供 `search scope=sessions`,也供 extract 的对话渲染。 */
|
|
99
99
|
export interface HostTranscriptPort {
|
|
100
100
|
/** 搜历史会话,返回已格式化好的文本(宿主决定格式与截断)。 */
|
|
101
|
-
searchSessions(
|
|
102
|
-
cwd: string,
|
|
103
|
-
query: string,
|
|
104
|
-
cfg: { maxSessions: number; maxMatches: number },
|
|
105
|
-
): Promise<string>;
|
|
101
|
+
searchSessions(cwd: string, query: string, cfg: { maxSessions: number; maxMatches: number }): Promise<string>;
|
|
106
102
|
/**
|
|
107
103
|
* 把宿主的原始消息序列转成中立的 `ExtractMessage`。
|
|
108
104
|
* 形状不认识的消息按「尽量保留文本」处理;**未知 role 绝不当作 user**(user 文本不截断,
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
requiredModel,
|
|
5
|
-
type MemoryConfig,
|
|
6
|
-
type ModelTask,
|
|
7
|
-
type SessionPersistenceConfig,
|
|
8
|
-
} from "../config.js";
|
|
3
|
+
import { requiredModel, type MemoryConfig, type ModelTask, type SessionPersistenceConfig } from "../config.js";
|
|
9
4
|
import { BACKUP_DIR, type MemoryStore } from "../store/memory-store.js";
|
|
10
5
|
import { createSnapshot } from "../store/snapshot.js";
|
|
11
6
|
import { isReservedWindowsName } from "../store/windows-names.js";
|
|
@@ -62,10 +57,7 @@ export interface RequestHost {
|
|
|
62
57
|
export type MemoryToolOnWrite = (info: { action: MemoryAction; name?: string }) => void;
|
|
63
58
|
|
|
64
59
|
/** `notCompleted` 收的 `why`:core 自己的判定,或端口给的非 completed 结果。 */
|
|
65
|
-
export type Incomplete =
|
|
66
|
-
| { status: "no-input" }
|
|
67
|
-
| { status: "lock-busy" }
|
|
68
|
-
| IncompleteOutcome;
|
|
60
|
+
export type Incomplete = { status: "no-input" } | { status: "lock-busy" } | IncompleteOutcome;
|
|
69
61
|
|
|
70
62
|
export interface MemoryTask<I, R> {
|
|
71
63
|
readonly id: ModelTask;
|
package/src/store/filename.ts
CHANGED
|
@@ -12,6 +12,77 @@ const STEM_MAX_BYTES = 100;
|
|
|
12
12
|
|
|
13
13
|
export const ENTRY_EXT = ".md";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 索引文件的文件名。定义在这里而不是 `memory-store.ts`:两个方向的文件名谓词都要排除它,
|
|
17
|
+
* 而从 store 那边 import 会造成循环依赖。`memory-store.ts` 的公开出口 `INDEX_FILE` 是它的别名,
|
|
18
|
+
* 名字/类型/值都不变。
|
|
19
|
+
*/
|
|
20
|
+
export const ENTRY_INDEX_FILE = "MEMORY.md";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 把文件名折叠成本地文件系统眼里的「同一个名字」:先 NFC 归一,再在 win32 / darwin 上去掉大小写。
|
|
24
|
+
*
|
|
25
|
+
* 为什么需要:`Foo.md` 与 `foo.md` 在 Linux 上是两条合法记忆,在 APFS / NTFS 上是同一个 inode ——
|
|
26
|
+
* 按名逐字直写会静默写穿一条(后写的那份盖掉先写的那份,两边都不报错)。NFC 归一同理:
|
|
27
|
+
* APFS / HFS+ 对 NFD 与 NFC 的两个名字做同一次查找。
|
|
28
|
+
*
|
|
29
|
+
* **跟平台**是它的核心语义,不是可选修饰:Linux 上把大小写也折叠掉,会让两条本来合法的
|
|
30
|
+
* 记忆被当成碰撞而丢掉一条。折叠只用于「是否同一个名字」的比较,绝不用于生成落盘的文件名。
|
|
31
|
+
*/
|
|
32
|
+
export function foldFileName(name: string, platform: NodeJS.Platform = process.platform): string {
|
|
33
|
+
const normalized = name.normalize("NFC");
|
|
34
|
+
return platform === "win32" || platform === "darwin" ? normalized.toLowerCase() : normalized;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 两个方向共用的**形状**判据:单层(不含路径分隔符)、`.md` 结尾、不是索引文件本身、
|
|
39
|
+
* 不以 `.` 开头(`.lock`、`.backups`、以及用户自己藏的临时文件都不是记忆)。
|
|
40
|
+
*
|
|
41
|
+
* 它刻意不导出:外面只该看见两个带方向的谓词,一个没有方向的「是不是条目名」正是
|
|
42
|
+
* 曾经让读口径与写口径互相串味的那个形状(见下)。
|
|
43
|
+
*/
|
|
44
|
+
function isEntryFileNameShape(name: string): boolean {
|
|
45
|
+
if (!name.endsWith(ENTRY_EXT)) return false;
|
|
46
|
+
if (name === ENTRY_INDEX_FILE) return false;
|
|
47
|
+
if (name.startsWith(".")) return false;
|
|
48
|
+
if (name.includes("/") || name.includes("\\")) return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* **读方向**:盘上(或索引里)已有的这个名字,算不算一条看得见的记忆。
|
|
54
|
+
*
|
|
55
|
+
* 消费者:`MemoryStore.#entryFiles()` 与同步的本地扫描 —— 两处必须共用这一份,口径一旦漂移
|
|
56
|
+
* 就会出现「store 看得到、同步看不到」(或反过来)的记忆,那是静默丢记忆。
|
|
57
|
+
*
|
|
58
|
+
* 保留设备名**只在 win32 上**剔除:按名打开 `con.md` 在 win32 上会命中控制台(`readFile` 阻塞而
|
|
59
|
+
* 不是返回内容),把它从清单里剔除的最坏表现是「该条记忆在本机不可见」;而在 Linux / macOS 上
|
|
60
|
+
* `con.md` 是一个普通文件、一条**合法**记忆,全平台剔除等于把它从 `listEntries()` 里静默抹掉。
|
|
61
|
+
*
|
|
62
|
+
* **绝不用它判「能不能写」**:写路径要的是全平台口径,见 `isEntryFileNameForWrite`。
|
|
63
|
+
*/
|
|
64
|
+
export function isEntryFileNameForRead(name: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
65
|
+
if (!isEntryFileNameShape(name)) return false;
|
|
66
|
+
return platform !== "win32" || !isReservedWindowsName(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* **写方向**:这个名字能不能被**写下去**(本地落盘,或发布到远端让另一台设备落盘)。
|
|
71
|
+
*
|
|
72
|
+
* 口径恒等于 win32(最严的那一个),**没有 platform 参数** —— 这不是疏忽而是它存在的全部理由:
|
|
73
|
+
* 写下去的文件名会成为跨设备的数据,只要有任何一台设备读不出来,它就是一个黑洞。在 Linux 上
|
|
74
|
+
* 写 `con.md` 完全合法,可它一旦同步到 Windows 那台机器就再也打不开(按名打开命中控制台),
|
|
75
|
+
* 而同步轮会在落盘前整批抛错 —— 一个名字让两台设备永久不同步。
|
|
76
|
+
*
|
|
77
|
+
* 消费者:`MemoryStore.applyEntryFiles` 的整批路径断言,以及**一切从网络进来的名字**
|
|
78
|
+
*(引擎在把远端清单交给差集之前先过它)。凡是「这个名字的归宿是某个文件系统」的判断都用它;
|
|
79
|
+
* 用读方向去判远端名字,就是把「本机读得懂」当成「每台设备都写得下」,那正是整批抛错的成因。
|
|
80
|
+
*/
|
|
81
|
+
export function isEntryFileNameForWrite(name: string): boolean {
|
|
82
|
+
if (!isEntryFileNameShape(name)) return false;
|
|
83
|
+
return !isReservedWindowsName(name);
|
|
84
|
+
}
|
|
85
|
+
|
|
15
86
|
function shortHash(input: string): string {
|
|
16
87
|
return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 8);
|
|
17
88
|
}
|
package/src/store/fs-lock.ts
CHANGED
|
@@ -41,9 +41,7 @@ export class MemoryLockedError extends Error {
|
|
|
41
41
|
/** 记录指向一个已死的本机进程,或本身无法解释 —— 没有任何人会释放它,只能人工清除。 */
|
|
42
42
|
readonly abandoned: boolean,
|
|
43
43
|
) {
|
|
44
|
-
const described = holder
|
|
45
|
-
? `${holder.op} (pid ${holder.pid}, started ${holder.startedAt})`
|
|
46
|
-
: "an unreadable record";
|
|
44
|
+
const described = holder ? `${holder.op} (pid ${holder.pid}, started ${holder.startedAt})` : "an unreadable record";
|
|
47
45
|
super(
|
|
48
46
|
abandoned
|
|
49
47
|
? `Memory lock at ${lockPath} is abandoned by ${described} — delete the file to clear it`
|
|
@@ -134,9 +132,7 @@ export async function readLockStatus(
|
|
|
134
132
|
return state.kind === "empty" ? { kind: "unreadable" } : state;
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
type AcquireOutcome =
|
|
138
|
-
| { acquired: true }
|
|
139
|
-
| { acquired: false; holder: LockInfo | null; abandoned: boolean };
|
|
135
|
+
type AcquireOutcome = { acquired: true } | { acquired: false; holder: LockInfo | null; abandoned: boolean };
|
|
140
136
|
|
|
141
137
|
/**
|
|
142
138
|
* 尝试一次获取。路径为空时不当作失败 —— 那是「刚好被释放」,直接再试一次 `open(wx)`(CAS 会自然地
|