@ai-agent-forge/agent-forge 0.88.1 → 0.88.2

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bundled-plugins/agent-loop-policy/agent-forge.json +1 -1
  3. package/bundled-plugins/agent-loop-policy/plugin.json +1 -1
  4. package/bundled-plugins/coding-tools/agent-forge.json +1 -1
  5. package/bundled-plugins/coding-tools/plugin.json +1 -1
  6. package/bundled-plugins/compaction-policy/agent-forge.json +1 -1
  7. package/bundled-plugins/compaction-policy/plugin.json +1 -1
  8. package/bundled-plugins/overflow-policy/agent-forge.json +1 -1
  9. package/bundled-plugins/overflow-policy/plugin.json +1 -1
  10. package/bundled-plugins/provider-failover/agent-forge.json +1 -1
  11. package/bundled-plugins/provider-failover/plugin.json +1 -1
  12. package/bundled-plugins/retry-policy/agent-forge.json +1 -1
  13. package/bundled-plugins/retry-policy/plugin.json +1 -1
  14. package/dist/bundle/.build-manifest.json +68 -58
  15. package/dist/bundle/chunks/{chunk-D3ARGZFR.js → chunk-44I47ACN.js} +3 -3
  16. package/dist/bundle/chunks/{chunk-Z72JWUZ4.js → chunk-6F5JWGKE.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-LZOEQITZ.js → chunk-HTC5K5SM.js} +56 -45
  18. package/dist/bundle/chunks/{chunk-AQLNTW2B.js → chunk-LSP24WNH.js} +8 -7
  19. package/dist/bundle/chunks/image-resize-worker.js +3 -3
  20. package/dist/bundle/chunks/{virtual-modules-WF6WRHP7.js → virtual-modules-MRXD3RWG.js} +1 -1
  21. package/dist/bundle/cli.js +1 -1
  22. package/dist/bundle/client.js +1 -1
  23. package/dist/bundle/index.js +1 -1
  24. package/dist/bundle/rpc-entry.js +1 -1
  25. package/dist/cli/plugin-command.d.ts.map +1 -1
  26. package/dist/cli/plugin-command.js +8 -2
  27. package/dist/cli/plugin-command.js.map +1 -1
  28. package/dist/core/agent-session-internal.d.ts +2 -0
  29. package/dist/core/agent-session-internal.d.ts.map +1 -1
  30. package/dist/core/agent-session-internal.js.map +1 -1
  31. package/dist/core/agent-session-services.d.ts +2 -0
  32. package/dist/core/agent-session-services.d.ts.map +1 -1
  33. package/dist/core/agent-session-services.js +1 -0
  34. package/dist/core/agent-session-services.js.map +1 -1
  35. package/dist/core/agent-session-store.d.ts.map +1 -1
  36. package/dist/core/agent-session-store.js +6 -1
  37. package/dist/core/agent-session-store.js.map +1 -1
  38. package/dist/core/agent-session-tools.d.ts.map +1 -1
  39. package/dist/core/agent-session-tools.js +13 -0
  40. package/dist/core/agent-session-tools.js.map +1 -1
  41. package/dist/core/agent-session.d.ts +24 -0
  42. package/dist/core/agent-session.d.ts.map +1 -1
  43. package/dist/core/agent-session.js +58 -4
  44. package/dist/core/agent-session.js.map +1 -1
  45. package/dist/core/i18n/locales/zh-cn/cli.d.ts.map +1 -1
  46. package/dist/core/i18n/locales/zh-cn/cli.js +2 -0
  47. package/dist/core/i18n/locales/zh-cn/cli.js.map +1 -1
  48. package/dist/core/i18n/locales/zh-cn/index.d.ts.map +1 -1
  49. package/dist/core/i18n/locales/zh-cn/index.js +2 -0
  50. package/dist/core/i18n/locales/zh-cn/index.js.map +1 -1
  51. package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts.map +1 -1
  52. package/dist/core/i18n/locales/zh-cn/interactive-mode.js +5 -0
  53. package/dist/core/i18n/locales/zh-cn/interactive-mode.js.map +1 -1
  54. package/dist/core/i18n/locales/zh-cn/session-cleanup.d.ts +7 -0
  55. package/dist/core/i18n/locales/zh-cn/session-cleanup.d.ts.map +1 -0
  56. package/dist/core/i18n/locales/zh-cn/session-cleanup.js +15 -0
  57. package/dist/core/i18n/locales/zh-cn/session-cleanup.js.map +1 -0
  58. package/dist/core/i18n/locales/zh-cn/slash-commands.d.ts.map +1 -1
  59. package/dist/core/i18n/locales/zh-cn/slash-commands.js +1 -0
  60. package/dist/core/i18n/locales/zh-cn/slash-commands.js.map +1 -1
  61. package/dist/core/sdk.d.ts.map +1 -1
  62. package/dist/core/sdk.js +24 -1
  63. package/dist/core/sdk.js.map +1 -1
  64. package/dist/core/session-cleanup.d.ts +49 -0
  65. package/dist/core/session-cleanup.d.ts.map +1 -0
  66. package/dist/core/session-cleanup.js +159 -0
  67. package/dist/core/session-cleanup.js.map +1 -0
  68. package/dist/core/session-index.d.ts +75 -0
  69. package/dist/core/session-index.d.ts.map +1 -0
  70. package/dist/core/session-index.js +455 -0
  71. package/dist/core/session-index.js.map +1 -0
  72. package/dist/core/session-manager.d.ts +9 -0
  73. package/dist/core/session-manager.d.ts.map +1 -1
  74. package/dist/core/session-manager.js +202 -47
  75. package/dist/core/session-manager.js.map +1 -1
  76. package/dist/core/slash-commands.d.ts.map +1 -1
  77. package/dist/core/slash-commands.js +5 -0
  78. package/dist/core/slash-commands.js.map +1 -1
  79. package/dist/core/system-prompt.d.ts.map +1 -1
  80. package/dist/core/system-prompt.js +12 -1
  81. package/dist/core/system-prompt.js.map +1 -1
  82. package/dist/core/tools/capability-mount.d.ts +40 -0
  83. package/dist/core/tools/capability-mount.d.ts.map +1 -0
  84. package/dist/core/tools/capability-mount.js +112 -0
  85. package/dist/core/tools/capability-mount.js.map +1 -0
  86. package/dist/main.d.ts.map +1 -1
  87. package/dist/main.js +1 -0
  88. package/dist/main.js.map +1 -1
  89. package/dist/modes/interactive/components/assistant-message.d.ts +3 -0
  90. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  91. package/dist/modes/interactive/components/assistant-message.js +27 -5
  92. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  93. package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/session-selector-search.js +5 -1
  95. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  96. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  97. package/dist/modes/interactive/components/session-selector.js +9 -0
  98. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  99. package/dist/modes/interactive/components/tool-execution.d.ts +17 -0
  100. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  101. package/dist/modes/interactive/components/tool-execution.js +43 -0
  102. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  103. package/dist/modes/interactive/components/tool-run-group.d.ts +44 -0
  104. package/dist/modes/interactive/components/tool-run-group.d.ts.map +1 -0
  105. package/dist/modes/interactive/components/tool-run-group.js +121 -0
  106. package/dist/modes/interactive/components/tool-run-group.js.map +1 -0
  107. package/dist/modes/interactive/interactive-commands.d.ts +2 -0
  108. package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
  109. package/dist/modes/interactive/interactive-commands.js +7 -0
  110. package/dist/modes/interactive/interactive-commands.js.map +1 -1
  111. package/dist/modes/interactive/interactive-mode.d.ts +18 -0
  112. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  113. package/dist/modes/interactive/interactive-mode.js +85 -3
  114. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  115. package/dist/plugins/ecosystem/cli.d.ts.map +1 -1
  116. package/dist/plugins/ecosystem/cli.js +6 -3
  117. package/dist/plugins/ecosystem/cli.js.map +1 -1
  118. package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts +7 -1
  119. package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts.map +1 -1
  120. package/dist/plugins/ecosystem/node-plugin-bootstrap.js +50 -3
  121. package/dist/plugins/ecosystem/node-plugin-bootstrap.js.map +1 -1
  122. package/dist/plugins/ecosystem/plugin-bootstrap.d.ts +28 -16
  123. package/dist/plugins/ecosystem/plugin-bootstrap.d.ts.map +1 -1
  124. package/dist/plugins/ecosystem/plugin-bootstrap.js +35 -33
  125. package/dist/plugins/ecosystem/plugin-bootstrap.js.map +1 -1
  126. package/dist/profile/plugin-profiles.d.ts +1 -1
  127. package/dist/profile/plugin-profiles.d.ts.map +1 -1
  128. package/dist/profile/plugin-profiles.js +18 -0
  129. package/dist/profile/plugin-profiles.js.map +1 -1
  130. package/dist/profiles/builtin-suites.d.ts +14 -4
  131. package/dist/profiles/builtin-suites.d.ts.map +1 -1
  132. package/dist/profiles/builtin-suites.js +20 -5
  133. package/dist/profiles/builtin-suites.js.map +1 -1
  134. package/docs/settings.md +354 -354
  135. package/docs/usage.md +292 -292
  136. package/examples/extensions/with-deps/package-lock.json +2 -2
  137. package/examples/extensions/with-deps/package.json +1 -1
  138. package/examples/extensions/with-deps/plugin.json +1 -1
  139. package/package.json +1 -1
  140. package/examples/extensions/mcp-stdio/index.d.ts +0 -64
  141. package/examples/extensions/mcp-stdio/index.d.ts.map +0 -1
  142. package/examples/extensions/mcp-stdio/index.js +0 -292
  143. package/examples/extensions/mcp-stdio/index.js.map +0 -1
@@ -0,0 +1,455 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { readdir, stat } from "node:fs/promises";
4
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
5
+ import { getAgentDir, getSessionsDir } from "../config.js";
6
+ import { AGENT_SESSIONS_DIR_NAME } from "./agent-session-paths.js";
7
+ /**
8
+ * 会话元数据索引(session-index.json),设计契约见 docs/design/会话索引.md。
9
+ *
10
+ * 索引是文件系统的投影、永远不是事实源;条目内部分两层:关系(file/dir/id/cwd)
11
+ * 由 readdir 对账保证,内容缓存由 file_mtime_ms 令牌保证(mtime 未变才可信,
12
+ * 变了就重读刷新)。写钩子是预热优化、不承担正确性(设计 §4);读路径
13
+ * (listSessionsViaIndex)负责对账自愈与全量重建。不加跨进程锁——丢失的更新
14
+ * 会被令牌机制限定为无害,下次读取对账时按文件系统纠正(设计 §6)。
15
+ */
16
+ const SESSION_INDEX_VERSION = 1;
17
+ const SESSION_INDEX_FILE_NAME = "session-index.json";
18
+ /** 节流落盘窗口(设计 §4:每轮消息追加标脏,10s 落盘一次)。 */
19
+ const FLUSH_THROTTLE_MS = 10_000;
20
+ /** 索引文件路径(与 plugin-state.json 同级;getAgentDir() 尊重 AGENT_FORGE_DIR)。 */
21
+ export function getSessionIndexPath() {
22
+ return join(getAgentDir(), SESSION_INDEX_FILE_NAME);
23
+ }
24
+ let pendingFlush;
25
+ let flushTimer;
26
+ let exitHookRegistered = false;
27
+ function clearPending() {
28
+ pendingFlush = undefined;
29
+ if (flushTimer !== undefined) {
30
+ clearTimeout(flushTimer);
31
+ flushTimer = undefined;
32
+ }
33
+ }
34
+ function ensureExitFlushHook() {
35
+ if (exitHookRegistered)
36
+ return;
37
+ exitHookRegistered = true;
38
+ process.once("exit", () => {
39
+ flushSessionIndexNow();
40
+ });
41
+ }
42
+ function armDirty(indexPath, state) {
43
+ pendingFlush = { indexPath, state };
44
+ ensureExitFlushHook();
45
+ if (flushTimer === undefined) {
46
+ flushTimer = setTimeout(() => {
47
+ flushTimer = undefined;
48
+ flushSessionIndexNow();
49
+ }, FLUSH_THROTTLE_MS);
50
+ flushTimer.unref();
51
+ }
52
+ }
53
+ /** 立即落盘脏状态(测试与 exit 钩子共用);无脏状态时为 no-op。 */
54
+ export function flushSessionIndexNow() {
55
+ const pending = pendingFlush;
56
+ if (!pending)
57
+ return;
58
+ clearPending();
59
+ try {
60
+ writeStateSync(pending.indexPath, pending.state);
61
+ }
62
+ catch {
63
+ // 失败留给对账自愈(设计 §6),不向会话写入路径传播。
64
+ }
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // 读写原语
68
+ // ---------------------------------------------------------------------------
69
+ function emptyState() {
70
+ return { version: SESSION_INDEX_VERSION, updated_at_ms: 0, projects: [], sessions: [] };
71
+ }
72
+ function safeMs(value) {
73
+ return Number.isFinite(value) ? value : 0;
74
+ }
75
+ function isSessionFileName(file) {
76
+ return file.endsWith(".jsonl") && !file.endsWith(".trace.jsonl");
77
+ }
78
+ function isFiniteNumber(value) {
79
+ return typeof value === "number" && Number.isFinite(value);
80
+ }
81
+ /**
82
+ * 条目级最小形状校验:必填字段类型/finite 不符的条目直接丢弃(视同缺失,交给
83
+ * 对账/重建自愈,覆盖设计 §2 目标 3 的字段级损坏);可选字段仅在为 string 时
84
+ * 保留,否则按键缺席处理。
85
+ */
86
+ function coerceSessionEntry(value) {
87
+ if (typeof value !== "object" || value === null)
88
+ return null;
89
+ const raw = value;
90
+ if (typeof raw.file !== "string" || typeof raw.dir !== "string" || typeof raw.id !== "string")
91
+ return null;
92
+ if (typeof raw.cwd !== "string")
93
+ return null;
94
+ if (!isFiniteNumber(raw.created_ms) || !isFiniteNumber(raw.modified_ms) || !isFiniteNumber(raw.message_count)) {
95
+ return null;
96
+ }
97
+ if (typeof raw.first_message !== "string")
98
+ return null;
99
+ return {
100
+ file: raw.file,
101
+ dir: raw.dir,
102
+ id: raw.id,
103
+ cwd: raw.cwd,
104
+ ...(isFiniteNumber(raw.file_mtime_ms) ? { file_mtime_ms: raw.file_mtime_ms } : {}),
105
+ ...(typeof raw.name === "string" ? { name: raw.name } : {}),
106
+ ...(typeof raw.parent_session_path === "string" ? { parent_session_path: raw.parent_session_path } : {}),
107
+ ...(typeof raw.profile_id === "string" ? { profile_id: raw.profile_id } : {}),
108
+ ...(typeof raw.profile_version === "string" ? { profile_version: raw.profile_version } : {}),
109
+ created_ms: raw.created_ms,
110
+ modified_ms: raw.modified_ms,
111
+ message_count: raw.message_count,
112
+ first_message: raw.first_message,
113
+ };
114
+ }
115
+ function coerceProjectEntry(value) {
116
+ if (typeof value !== "object" || value === null)
117
+ return null;
118
+ const raw = value;
119
+ // 项目条目只有 dir/cwd 关系;last_active 由成员条目 modified_ms 服务时派生
120
+ // (设计 §3),入库时的任何附加键一律忽略。
121
+ if (typeof raw.dir !== "string" || typeof raw.cwd !== "string")
122
+ return null;
123
+ return { dir: raw.dir, cwd: raw.cwd };
124
+ }
125
+ /** 读取并校验索引文件;不存在/JSON 损坏/版本不符返回 null(由调用方重建或忽略)。 */
126
+ function readIndexFile(indexPath) {
127
+ let content;
128
+ try {
129
+ content = readFileSync(indexPath, "utf8");
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ try {
135
+ const parsed = JSON.parse(content);
136
+ if (parsed.version !== SESSION_INDEX_VERSION)
137
+ return null;
138
+ if (!Array.isArray(parsed.projects) || !Array.isArray(parsed.sessions))
139
+ return null;
140
+ const projects = parsed.projects.map(coerceProjectEntry).filter((entry) => {
141
+ return entry !== null;
142
+ });
143
+ const sessions = parsed.sessions.map(coerceSessionEntry).filter((entry) => {
144
+ return entry !== null;
145
+ });
146
+ return { version: SESSION_INDEX_VERSION, updated_at_ms: 0, projects, sessions };
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ /** 原子写(temp + rename,同 ecosystem/state.ts 模式);同步 API 供 exit 钩子共用。 */
153
+ function writeStateSync(indexPath, state) {
154
+ const parent = dirname(indexPath);
155
+ mkdirSync(parent, { recursive: true });
156
+ const temporaryPath = join(parent, `.${basename(indexPath)}.${randomUUID()}.tmp`);
157
+ try {
158
+ state.updated_at_ms = Date.now();
159
+ writeFileSync(temporaryPath, `${JSON.stringify(state)}\n`, "utf8");
160
+ renameSync(temporaryPath, indexPath);
161
+ }
162
+ finally {
163
+ try {
164
+ unlinkSync(temporaryPath);
165
+ }
166
+ catch {
167
+ // rename 成功后 tmp 已不存在;清理失败不掩盖写入结果。
168
+ }
169
+ }
170
+ }
171
+ /** 可变状态:优先续用未落盘的脏快照,避免节流窗口内的读-改-写互相覆盖。 */
172
+ function loadMutableState(indexPath) {
173
+ if (pendingFlush?.indexPath === indexPath)
174
+ return pendingFlush.state;
175
+ return readIndexFile(indexPath) ?? emptyState();
176
+ }
177
+ /**
178
+ * 解析会话路径在索引中的位置:dir = 相对 sessions 根的父目录名(不从 cwd 重推导,
179
+ * 规避 Windows 盘符大小写陷阱,设计 §3),file = basename。
180
+ * sessions 根之外、根一级散落文件、非 `*.jsonl` 与 `*.trace.jsonl` 一律返回 null。
181
+ */
182
+ function locationInRoot(sessionsRoot, sessionPath) {
183
+ const rel = relative(sessionsRoot, sessionPath);
184
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
185
+ return null;
186
+ const dir = dirname(rel);
187
+ if (dir === "." || dir === "..")
188
+ return null;
189
+ const file = basename(rel);
190
+ if (!isSessionFileName(file))
191
+ return null;
192
+ return { dir, file };
193
+ }
194
+ function findSessionEntry(state, dir, file) {
195
+ return state.sessions.find((entry) => entry.dir === dir && entry.file === file);
196
+ }
197
+ function touchProject(state, dir, cwd) {
198
+ if (state.projects.some((project) => project.dir === dir))
199
+ return;
200
+ state.projects.push({ dir, cwd });
201
+ }
202
+ function addSessionFromInfo(state, dir, file, info, mtimeMs) {
203
+ state.sessions = state.sessions.filter((entry) => !(entry.dir === dir && entry.file === file));
204
+ state.sessions.push({
205
+ file,
206
+ dir,
207
+ id: info.id,
208
+ cwd: info.cwd,
209
+ ...(info.name === undefined ? {} : { name: info.name }),
210
+ ...(info.parentSessionPath === undefined ? {} : { parent_session_path: info.parentSessionPath }),
211
+ ...(info.profileId === undefined ? {} : { profile_id: info.profileId }),
212
+ ...(info.profileVersion === undefined ? {} : { profile_version: info.profileVersion }),
213
+ ...(mtimeMs === undefined ? {} : { file_mtime_ms: mtimeMs }),
214
+ created_ms: safeMs(info.created.getTime()),
215
+ modified_ms: safeMs(info.modified.getTime()),
216
+ message_count: info.messageCount,
217
+ first_message: info.firstMessage,
218
+ });
219
+ touchProject(state, dir, info.cwd);
220
+ }
221
+ // ---------------------------------------------------------------------------
222
+ // 写路径(设计 §4)
223
+ // ---------------------------------------------------------------------------
224
+ /**
225
+ * upsert 单会话条目并按 immediate 决定落盘时机。预热优化(设计 §4):条目缺失
226
+ * 或过期由读路径的 mtime 令牌对账刷新,不承担正确性。内部错误一律吞掉
227
+ * (fail-open):索引是投影,写入失败由下次读取对账自愈,绝不影响会话持久化。
228
+ */
229
+ export function recordSessionUpdate(input) {
230
+ try {
231
+ const sessionsRoot = getSessionsDir();
232
+ const location = locationInRoot(sessionsRoot, input.sessionPath);
233
+ if (!location)
234
+ return;
235
+ // 缓存有效期令牌 = 当前文件 mtime;stat 失败则条目不带令牌(视为过期,无害)。
236
+ let mtimeMs;
237
+ try {
238
+ mtimeMs = statSync(input.sessionPath).mtimeMs;
239
+ }
240
+ catch {
241
+ mtimeMs = undefined;
242
+ }
243
+ const indexPath = getSessionIndexPath();
244
+ const state = loadMutableState(indexPath);
245
+ state.sessions = state.sessions.filter((entry) => !(entry.dir === location.dir && entry.file === location.file));
246
+ const modifiedMs = safeMs(input.modifiedMs);
247
+ state.sessions.push({
248
+ file: location.file,
249
+ dir: location.dir,
250
+ id: input.sessionId,
251
+ cwd: input.cwd,
252
+ ...(input.name === undefined ? {} : { name: input.name }),
253
+ ...(input.parentSessionPath === undefined ? {} : { parent_session_path: input.parentSessionPath }),
254
+ ...(input.profileId === undefined ? {} : { profile_id: input.profileId }),
255
+ ...(input.profileVersion === undefined ? {} : { profile_version: input.profileVersion }),
256
+ ...(mtimeMs === undefined ? {} : { file_mtime_ms: mtimeMs }),
257
+ created_ms: safeMs(input.createdMs),
258
+ modified_ms: modifiedMs,
259
+ message_count: input.messageCount,
260
+ first_message: input.firstMessage,
261
+ });
262
+ touchProject(state, location.dir, input.cwd);
263
+ if (input.immediate) {
264
+ clearPending();
265
+ writeStateSync(indexPath, state);
266
+ }
267
+ else {
268
+ armDirty(indexPath, state);
269
+ }
270
+ }
271
+ catch {
272
+ // 索引维护绝不影响会话写入(设计 §4/§6)。
273
+ }
274
+ }
275
+ /**
276
+ * 按路径移除条目并立即原子落盘;索引不存在/损坏/路径不在 sessions 根时静默 no-op
277
+ * (不抛错、不重建,设计 §6)。
278
+ */
279
+ export function removeSessionFromIndex(sessionPath) {
280
+ try {
281
+ const location = locationInRoot(getSessionsDir(), sessionPath);
282
+ if (!location)
283
+ return;
284
+ const indexPath = getSessionIndexPath();
285
+ const state = loadMutableState(indexPath);
286
+ const before = state.sessions.length;
287
+ state.sessions = state.sessions.filter((entry) => !(entry.dir === location.dir && entry.file === location.file));
288
+ if (state.sessions.length === before)
289
+ return;
290
+ clearPending();
291
+ writeStateSync(indexPath, state);
292
+ }
293
+ catch {
294
+ // 对账会在下次读取自愈(设计 §6)。
295
+ }
296
+ }
297
+ // ---------------------------------------------------------------------------
298
+ // 读路径与对账(设计 §5)
299
+ // ---------------------------------------------------------------------------
300
+ function sessionInfoFromEntry(sessionsRoot, entry) {
301
+ return {
302
+ path: join(sessionsRoot, entry.dir, entry.file),
303
+ id: entry.id,
304
+ cwd: entry.cwd,
305
+ ...(entry.name === undefined ? {} : { name: entry.name }),
306
+ ...(entry.parent_session_path === undefined ? {} : { parentSessionPath: entry.parent_session_path }),
307
+ ...(entry.profile_id === undefined ? {} : { profileId: entry.profile_id }),
308
+ ...(entry.profile_version === undefined ? {} : { profileVersion: entry.profile_version }),
309
+ created: new Date(entry.created_ms),
310
+ modified: new Date(entry.modified_ms),
311
+ messageCount: entry.message_count,
312
+ firstMessage: entry.first_message,
313
+ allMessagesText: "",
314
+ };
315
+ }
316
+ function sortedSessions(sessionsRoot, state) {
317
+ const ordered = [...state.sessions].sort((a, b) => b.modified_ms - a.modified_ms || b.file.localeCompare(a.file));
318
+ return ordered.map((entry) => sessionInfoFromEntry(sessionsRoot, entry));
319
+ }
320
+ /** 重建路径:由全量扫描结果构建索引状态,逐文件 stat 写入 mtime 令牌。 */
321
+ async function stateFromSessions(sessionsRoot, sessions) {
322
+ const state = emptyState();
323
+ for (const info of sessions) {
324
+ const location = locationInRoot(sessionsRoot, info.path);
325
+ if (!location)
326
+ continue;
327
+ let mtimeMs;
328
+ try {
329
+ mtimeMs = (await stat(info.path)).mtimeMs;
330
+ }
331
+ catch {
332
+ mtimeMs = undefined;
333
+ }
334
+ addSessionFromInfo(state, location.dir, location.file, info, mtimeMs);
335
+ }
336
+ return state;
337
+ }
338
+ /**
339
+ * 对账:目录名集合 diff + 目录内文件名集合 diff,逐文件 stat 与条目的
340
+ * file_mtime_ms 令牌比对(设计 §5)——条目缺失 → buildInfo 补;mtime 与令牌
341
+ * 一致 → 缓存可信直接沿用;不一致或令牌缺失 → buildInfo 重读刷新缓存与新令牌
342
+ * (重读失败 = 文件已不再是会话 → 剔除陈旧条目);索引有、磁盘无 → 剔除;
343
+ * 消失目录连同其 project 行剔除。返回是否有变更。
344
+ */
345
+ async function reconcileIndex(sessionsRoot, state, buildInfo, signal) {
346
+ // 磁盘清单:目录名 → (文件名 → stat mtime)。stat 失败(readdir 与 stat 之间
347
+ // 被删/不可读)视同本轮缺席,下轮对账收敛。
348
+ const diskDirs = new Map();
349
+ let rootEntries;
350
+ try {
351
+ rootEntries = await readdir(sessionsRoot, { withFileTypes: true });
352
+ }
353
+ catch {
354
+ // sessions 根不可读视同空盘:剔除全部条目(索引是投影,根恢复后重建)。
355
+ rootEntries = [];
356
+ }
357
+ for (const entry of rootEntries) {
358
+ if (entry.name === AGENT_SESSIONS_DIR_NAME)
359
+ continue;
360
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
361
+ continue;
362
+ let fileNames;
363
+ try {
364
+ fileNames = await readdir(join(sessionsRoot, entry.name));
365
+ }
366
+ catch {
367
+ // 目录不可读视同已清空,其索引条目被剔除(悬挂容忍,设计 §6)。
368
+ fileNames = [];
369
+ }
370
+ const files = new Map();
371
+ for (const file of fileNames) {
372
+ if (!isSessionFileName(file))
373
+ continue;
374
+ signal?.throwIfAborted();
375
+ try {
376
+ files.set(file, (await stat(join(sessionsRoot, entry.name, file))).mtimeMs);
377
+ }
378
+ catch {
379
+ // 本轮视同缺席。
380
+ }
381
+ }
382
+ diskDirs.set(entry.name, files);
383
+ }
384
+ let changed = false;
385
+ for (const [dir, files] of diskDirs) {
386
+ for (const [file, mtimeMs] of files) {
387
+ const existing = findSessionEntry(state, dir, file);
388
+ if (existing && existing.file_mtime_ms !== undefined && existing.file_mtime_ms === mtimeMs)
389
+ continue;
390
+ signal?.throwIfAborted();
391
+ const info = await buildInfo(join(sessionsRoot, dir, file));
392
+ if (!info) {
393
+ if (existing) {
394
+ // 文件已不再解析为会话(损坏/改写):剔除陈旧条目。
395
+ state.sessions = state.sessions.filter((candidate) => candidate !== existing);
396
+ changed = true;
397
+ }
398
+ continue;
399
+ }
400
+ addSessionFromInfo(state, dir, file, info, mtimeMs);
401
+ changed = true;
402
+ }
403
+ }
404
+ const keptSessions = state.sessions.filter((entry) => diskDirs.get(entry.dir)?.has(entry.file));
405
+ if (keptSessions.length !== state.sessions.length) {
406
+ state.sessions = keptSessions;
407
+ changed = true;
408
+ }
409
+ const keptProjects = state.projects.filter((project) => diskDirs.has(project.dir));
410
+ if (keptProjects.length !== state.projects.length) {
411
+ state.projects = keptProjects;
412
+ changed = true;
413
+ }
414
+ return changed;
415
+ }
416
+ /**
417
+ * listAll 默认布局分支的索引读路径。返回 undefined 表示索引层意外失败(IO 等),
418
+ * 调用方应回退到旧全量扫描(fail-open,设计 §5);中止按惯例抛 AbortError。
419
+ */
420
+ export async function listSessionsViaIndex(input) {
421
+ const { sessionsRoot, buildInfo, scanAll, onHit, signal } = input;
422
+ try {
423
+ signal?.throwIfAborted();
424
+ const indexPath = getSessionIndexPath();
425
+ let state;
426
+ if (pendingFlush?.indexPath === indexPath) {
427
+ // 节流窗口内已积累的更新是对账基准:对账落盘不得把窗口内更新回退覆盖。
428
+ state = pendingFlush.state;
429
+ }
430
+ else {
431
+ const fromDisk = readIndexFile(indexPath);
432
+ if (fromDisk === null) {
433
+ // 缺失/损坏/版本不符 → 旧扫描全量重建并落盘(与旧行为同价,设计 §5.1)。
434
+ const sessions = await scanAll();
435
+ writeStateSync(indexPath, await stateFromSessions(sessionsRoot, sessions));
436
+ return sessions;
437
+ }
438
+ state = fromDisk;
439
+ }
440
+ const changed = await reconcileIndex(sessionsRoot, state, buildInfo, signal);
441
+ if (changed) {
442
+ if (pendingFlush?.indexPath === indexPath)
443
+ clearPending();
444
+ writeStateSync(indexPath, state);
445
+ }
446
+ const sessions = sortedSessions(sessionsRoot, state);
447
+ onHit?.(sessions);
448
+ return sessions;
449
+ }
450
+ catch {
451
+ signal?.throwIfAborted();
452
+ return undefined;
453
+ }
454
+ }
455
+ //# sourceMappingURL=session-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-index.js","sourceRoot":"","sources":["../../src/core/session-index.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { type Dirent, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, join, relative } from \"node:path\";\nimport { getAgentDir, getSessionsDir } from \"../config.ts\";\nimport { AGENT_SESSIONS_DIR_NAME } from \"./agent-session-paths.ts\";\nimport type { SessionInfo } from \"./session-manager.ts\";\n\n/**\n * 会话元数据索引(session-index.json),设计契约见 docs/design/会话索引.md。\n *\n * 索引是文件系统的投影、永远不是事实源;条目内部分两层:关系(file/dir/id/cwd)\n * 由 readdir 对账保证,内容缓存由 file_mtime_ms 令牌保证(mtime 未变才可信,\n * 变了就重读刷新)。写钩子是预热优化、不承担正确性(设计 §4);读路径\n * (listSessionsViaIndex)负责对账自愈与全量重建。不加跨进程锁——丢失的更新\n * 会被令牌机制限定为无害,下次读取对账时按文件系统纠正(设计 §6)。\n */\n\nconst SESSION_INDEX_VERSION = 1;\nconst SESSION_INDEX_FILE_NAME = \"session-index.json\";\n/** 节流落盘窗口(设计 §4:每轮消息追加标脏,10s 落盘一次)。 */\nconst FLUSH_THROTTLE_MS = 10_000;\n\nexport interface SessionIndexProjectEntry {\n\t/** 项目目录名(getDefaultSessionDirPath(cwd) 的编码目录名,不从 cwd 重推导)。 */\n\tdir: string;\n\tcwd: string;\n}\n\nexport interface SessionIndexSessionEntry {\n\t/** 会话文件 basename;键 = (dir, file)。 */\n\tfile: string;\n\tdir: string;\n\tid: string;\n\tcwd: string;\n\t/**\n\t * 缓存有效期令牌:条目写入时该会话文件的 stat mtime。缺失或与磁盘不一致 =\n\t * 缓存过期,对账时重读刷新(设计 §3/§5)。\n\t */\n\tfile_mtime_ms?: number;\n\tname?: string;\n\tparent_session_path?: string;\n\t/** 创建时锁定的 profile(可选字段,向后可读;与 SessionHeader 对应)。 */\n\tprofile_id?: string;\n\tprofile_version?: string;\n\tcreated_ms: number;\n\tmodified_ms: number;\n\tmessage_count: number;\n\tfirst_message: string;\n}\n\ninterface SessionIndexFile {\n\tversion: number;\n\tupdated_at_ms: number;\n\tprojects: SessionIndexProjectEntry[];\n\tsessions: SessionIndexSessionEntry[];\n}\n\nexport interface SessionIndexUpdateInput {\n\t/** 绝对会话文件路径(默认布局下位于 sessions 根之内)。 */\n\tsessionPath: string;\n\tsessionId: string;\n\tcwd: string;\n\tname?: string;\n\tparentSessionPath?: string;\n\tprofileId?: string;\n\tprofileVersion?: string;\n\tcreatedMs: number;\n\tmodifiedMs: number;\n\tmessageCount: number;\n\tfirstMessage: string;\n\t/** true = 同步原子落盘;false = 标脏,节流窗口内落盘。 */\n\timmediate: boolean;\n}\n\nexport interface ListSessionsViaIndexInput {\n\t/** sessions 根目录(…/sessions)绝对路径。 */\n\tsessionsRoot: string;\n\t/** 完整 SessionInfo 构建器(session-manager 的 buildSessionInfo);返回 null = 非会话文件。 */\n\tbuildInfo: (path: string) => Promise<SessionInfo | null>;\n\t/** 旧全量扫描委托:索引缺失/损坏/版本不符时用于重建索引(与旧 listAll 同价)。 */\n\tscanAll: () => Promise<SessionInfo[]>;\n\t/** 索引命中时的单次完整已排序快照回调(重建路径的进度由 scanAll 按旧语义发布)。 */\n\tonHit?: (sessions: SessionInfo[]) => void;\n\tsignal?: AbortSignal;\n}\n\n/** 索引文件路径(与 plugin-state.json 同级;getAgentDir() 尊重 AGENT_FORGE_DIR)。 */\nexport function getSessionIndexPath(): string {\n\treturn join(getAgentDir(), SESSION_INDEX_FILE_NAME);\n}\n\n// ---------------------------------------------------------------------------\n// 模块级脏状态:pendingFlush 捕获写入目标与快照,进程 exit 时同步落盘。\n// 按路径比较,测试间 ENV_AGENT_DIR 切换不会串写。\n// ---------------------------------------------------------------------------\n\ninterface PendingFlush {\n\tindexPath: string;\n\tstate: SessionIndexFile;\n}\n\nlet pendingFlush: PendingFlush | undefined;\nlet flushTimer: ReturnType<typeof setTimeout> | undefined;\nlet exitHookRegistered = false;\n\nfunction clearPending(): void {\n\tpendingFlush = undefined;\n\tif (flushTimer !== undefined) {\n\t\tclearTimeout(flushTimer);\n\t\tflushTimer = undefined;\n\t}\n}\n\nfunction ensureExitFlushHook(): void {\n\tif (exitHookRegistered) return;\n\texitHookRegistered = true;\n\tprocess.once(\"exit\", () => {\n\t\tflushSessionIndexNow();\n\t});\n}\n\nfunction armDirty(indexPath: string, state: SessionIndexFile): void {\n\tpendingFlush = { indexPath, state };\n\tensureExitFlushHook();\n\tif (flushTimer === undefined) {\n\t\tflushTimer = setTimeout(() => {\n\t\t\tflushTimer = undefined;\n\t\t\tflushSessionIndexNow();\n\t\t}, FLUSH_THROTTLE_MS);\n\t\tflushTimer.unref();\n\t}\n}\n\n/** 立即落盘脏状态(测试与 exit 钩子共用);无脏状态时为 no-op。 */\nexport function flushSessionIndexNow(): void {\n\tconst pending = pendingFlush;\n\tif (!pending) return;\n\tclearPending();\n\ttry {\n\t\twriteStateSync(pending.indexPath, pending.state);\n\t} catch {\n\t\t// 失败留给对账自愈(设计 §6),不向会话写入路径传播。\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// 读写原语\n// ---------------------------------------------------------------------------\n\nfunction emptyState(): SessionIndexFile {\n\treturn { version: SESSION_INDEX_VERSION, updated_at_ms: 0, projects: [], sessions: [] };\n}\n\nfunction safeMs(value: number): number {\n\treturn Number.isFinite(value) ? value : 0;\n}\n\nfunction isSessionFileName(file: string): boolean {\n\treturn file.endsWith(\".jsonl\") && !file.endsWith(\".trace.jsonl\");\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value);\n}\n\n/**\n * 条目级最小形状校验:必填字段类型/finite 不符的条目直接丢弃(视同缺失,交给\n * 对账/重建自愈,覆盖设计 §2 目标 3 的字段级损坏);可选字段仅在为 string 时\n * 保留,否则按键缺席处理。\n */\nfunction coerceSessionEntry(value: unknown): SessionIndexSessionEntry | null {\n\tif (typeof value !== \"object\" || value === null) return null;\n\tconst raw = value as Record<string, unknown>;\n\tif (typeof raw.file !== \"string\" || typeof raw.dir !== \"string\" || typeof raw.id !== \"string\") return null;\n\tif (typeof raw.cwd !== \"string\") return null;\n\tif (!isFiniteNumber(raw.created_ms) || !isFiniteNumber(raw.modified_ms) || !isFiniteNumber(raw.message_count)) {\n\t\treturn null;\n\t}\n\tif (typeof raw.first_message !== \"string\") return null;\n\treturn {\n\t\tfile: raw.file,\n\t\tdir: raw.dir,\n\t\tid: raw.id,\n\t\tcwd: raw.cwd,\n\t\t...(isFiniteNumber(raw.file_mtime_ms) ? { file_mtime_ms: raw.file_mtime_ms } : {}),\n\t\t...(typeof raw.name === \"string\" ? { name: raw.name } : {}),\n\t\t...(typeof raw.parent_session_path === \"string\" ? { parent_session_path: raw.parent_session_path } : {}),\n\t\t...(typeof raw.profile_id === \"string\" ? { profile_id: raw.profile_id } : {}),\n\t\t...(typeof raw.profile_version === \"string\" ? { profile_version: raw.profile_version } : {}),\n\t\tcreated_ms: raw.created_ms,\n\t\tmodified_ms: raw.modified_ms,\n\t\tmessage_count: raw.message_count,\n\t\tfirst_message: raw.first_message,\n\t};\n}\n\nfunction coerceProjectEntry(value: unknown): SessionIndexProjectEntry | null {\n\tif (typeof value !== \"object\" || value === null) return null;\n\tconst raw = value as Record<string, unknown>;\n\t// 项目条目只有 dir/cwd 关系;last_active 由成员条目 modified_ms 服务时派生\n\t// (设计 §3),入库时的任何附加键一律忽略。\n\tif (typeof raw.dir !== \"string\" || typeof raw.cwd !== \"string\") return null;\n\treturn { dir: raw.dir, cwd: raw.cwd };\n}\n\n/** 读取并校验索引文件;不存在/JSON 损坏/版本不符返回 null(由调用方重建或忽略)。 */\nfunction readIndexFile(indexPath: string): SessionIndexFile | null {\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(indexPath, \"utf8\");\n\t} catch {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(content) as { version?: unknown; projects?: unknown; sessions?: unknown };\n\t\tif (parsed.version !== SESSION_INDEX_VERSION) return null;\n\t\tif (!Array.isArray(parsed.projects) || !Array.isArray(parsed.sessions)) return null;\n\t\tconst projects = parsed.projects.map(coerceProjectEntry).filter((entry): entry is SessionIndexProjectEntry => {\n\t\t\treturn entry !== null;\n\t\t});\n\t\tconst sessions = parsed.sessions.map(coerceSessionEntry).filter((entry): entry is SessionIndexSessionEntry => {\n\t\t\treturn entry !== null;\n\t\t});\n\t\treturn { version: SESSION_INDEX_VERSION, updated_at_ms: 0, projects, sessions };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/** 原子写(temp + rename,同 ecosystem/state.ts 模式);同步 API 供 exit 钩子共用。 */\nfunction writeStateSync(indexPath: string, state: SessionIndexFile): void {\n\tconst parent = dirname(indexPath);\n\tmkdirSync(parent, { recursive: true });\n\tconst temporaryPath = join(parent, `.${basename(indexPath)}.${randomUUID()}.tmp`);\n\ttry {\n\t\tstate.updated_at_ms = Date.now();\n\t\twriteFileSync(temporaryPath, `${JSON.stringify(state)}\\n`, \"utf8\");\n\t\trenameSync(temporaryPath, indexPath);\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(temporaryPath);\n\t\t} catch {\n\t\t\t// rename 成功后 tmp 已不存在;清理失败不掩盖写入结果。\n\t\t}\n\t}\n}\n\n/** 可变状态:优先续用未落盘的脏快照,避免节流窗口内的读-改-写互相覆盖。 */\nfunction loadMutableState(indexPath: string): SessionIndexFile {\n\tif (pendingFlush?.indexPath === indexPath) return pendingFlush.state;\n\treturn readIndexFile(indexPath) ?? emptyState();\n}\n\n/**\n * 解析会话路径在索引中的位置:dir = 相对 sessions 根的父目录名(不从 cwd 重推导,\n * 规避 Windows 盘符大小写陷阱,设计 §3),file = basename。\n * sessions 根之外、根一级散落文件、非 `*.jsonl` 与 `*.trace.jsonl` 一律返回 null。\n */\nfunction locationInRoot(sessionsRoot: string, sessionPath: string): { dir: string; file: string } | null {\n\tconst rel = relative(sessionsRoot, sessionPath);\n\tif (rel === \"\" || rel.startsWith(\"..\") || isAbsolute(rel)) return null;\n\tconst dir = dirname(rel);\n\tif (dir === \".\" || dir === \"..\") return null;\n\tconst file = basename(rel);\n\tif (!isSessionFileName(file)) return null;\n\treturn { dir, file };\n}\n\nfunction findSessionEntry(state: SessionIndexFile, dir: string, file: string): SessionIndexSessionEntry | undefined {\n\treturn state.sessions.find((entry) => entry.dir === dir && entry.file === file);\n}\n\nfunction touchProject(state: SessionIndexFile, dir: string, cwd: string): void {\n\tif (state.projects.some((project) => project.dir === dir)) return;\n\tstate.projects.push({ dir, cwd });\n}\n\nfunction addSessionFromInfo(\n\tstate: SessionIndexFile,\n\tdir: string,\n\tfile: string,\n\tinfo: SessionInfo,\n\tmtimeMs?: number,\n): void {\n\tstate.sessions = state.sessions.filter((entry) => !(entry.dir === dir && entry.file === file));\n\tstate.sessions.push({\n\t\tfile,\n\t\tdir,\n\t\tid: info.id,\n\t\tcwd: info.cwd,\n\t\t...(info.name === undefined ? {} : { name: info.name }),\n\t\t...(info.parentSessionPath === undefined ? {} : { parent_session_path: info.parentSessionPath }),\n\t\t...(info.profileId === undefined ? {} : { profile_id: info.profileId }),\n\t\t...(info.profileVersion === undefined ? {} : { profile_version: info.profileVersion }),\n\t\t...(mtimeMs === undefined ? {} : { file_mtime_ms: mtimeMs }),\n\t\tcreated_ms: safeMs(info.created.getTime()),\n\t\tmodified_ms: safeMs(info.modified.getTime()),\n\t\tmessage_count: info.messageCount,\n\t\tfirst_message: info.firstMessage,\n\t});\n\ttouchProject(state, dir, info.cwd);\n}\n\n// ---------------------------------------------------------------------------\n// 写路径(设计 §4)\n// ---------------------------------------------------------------------------\n\n/**\n * upsert 单会话条目并按 immediate 决定落盘时机。预热优化(设计 §4):条目缺失\n * 或过期由读路径的 mtime 令牌对账刷新,不承担正确性。内部错误一律吞掉\n * (fail-open):索引是投影,写入失败由下次读取对账自愈,绝不影响会话持久化。\n */\nexport function recordSessionUpdate(input: SessionIndexUpdateInput): void {\n\ttry {\n\t\tconst sessionsRoot = getSessionsDir();\n\t\tconst location = locationInRoot(sessionsRoot, input.sessionPath);\n\t\tif (!location) return;\n\t\t// 缓存有效期令牌 = 当前文件 mtime;stat 失败则条目不带令牌(视为过期,无害)。\n\t\tlet mtimeMs: number | undefined;\n\t\ttry {\n\t\t\tmtimeMs = statSync(input.sessionPath).mtimeMs;\n\t\t} catch {\n\t\t\tmtimeMs = undefined;\n\t\t}\n\t\tconst indexPath = getSessionIndexPath();\n\t\tconst state = loadMutableState(indexPath);\n\t\tstate.sessions = state.sessions.filter((entry) => !(entry.dir === location.dir && entry.file === location.file));\n\t\tconst modifiedMs = safeMs(input.modifiedMs);\n\t\tstate.sessions.push({\n\t\t\tfile: location.file,\n\t\t\tdir: location.dir,\n\t\t\tid: input.sessionId,\n\t\t\tcwd: input.cwd,\n\t\t\t...(input.name === undefined ? {} : { name: input.name }),\n\t\t\t...(input.parentSessionPath === undefined ? {} : { parent_session_path: input.parentSessionPath }),\n\t\t\t...(input.profileId === undefined ? {} : { profile_id: input.profileId }),\n\t\t\t...(input.profileVersion === undefined ? {} : { profile_version: input.profileVersion }),\n\t\t\t...(mtimeMs === undefined ? {} : { file_mtime_ms: mtimeMs }),\n\t\t\tcreated_ms: safeMs(input.createdMs),\n\t\t\tmodified_ms: modifiedMs,\n\t\t\tmessage_count: input.messageCount,\n\t\t\tfirst_message: input.firstMessage,\n\t\t});\n\t\ttouchProject(state, location.dir, input.cwd);\n\t\tif (input.immediate) {\n\t\t\tclearPending();\n\t\t\twriteStateSync(indexPath, state);\n\t\t} else {\n\t\t\tarmDirty(indexPath, state);\n\t\t}\n\t} catch {\n\t\t// 索引维护绝不影响会话写入(设计 §4/§6)。\n\t}\n}\n\n/**\n * 按路径移除条目并立即原子落盘;索引不存在/损坏/路径不在 sessions 根时静默 no-op\n * (不抛错、不重建,设计 §6)。\n */\nexport function removeSessionFromIndex(sessionPath: string): void {\n\ttry {\n\t\tconst location = locationInRoot(getSessionsDir(), sessionPath);\n\t\tif (!location) return;\n\t\tconst indexPath = getSessionIndexPath();\n\t\tconst state = loadMutableState(indexPath);\n\t\tconst before = state.sessions.length;\n\t\tstate.sessions = state.sessions.filter((entry) => !(entry.dir === location.dir && entry.file === location.file));\n\t\tif (state.sessions.length === before) return;\n\t\tclearPending();\n\t\twriteStateSync(indexPath, state);\n\t} catch {\n\t\t// 对账会在下次读取自愈(设计 §6)。\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// 读路径与对账(设计 §5)\n// ---------------------------------------------------------------------------\n\nfunction sessionInfoFromEntry(sessionsRoot: string, entry: SessionIndexSessionEntry): SessionInfo {\n\treturn {\n\t\tpath: join(sessionsRoot, entry.dir, entry.file),\n\t\tid: entry.id,\n\t\tcwd: entry.cwd,\n\t\t...(entry.name === undefined ? {} : { name: entry.name }),\n\t\t...(entry.parent_session_path === undefined ? {} : { parentSessionPath: entry.parent_session_path }),\n\t\t...(entry.profile_id === undefined ? {} : { profileId: entry.profile_id }),\n\t\t...(entry.profile_version === undefined ? {} : { profileVersion: entry.profile_version }),\n\t\tcreated: new Date(entry.created_ms),\n\t\tmodified: new Date(entry.modified_ms),\n\t\tmessageCount: entry.message_count,\n\t\tfirstMessage: entry.first_message,\n\t\tallMessagesText: \"\",\n\t};\n}\n\nfunction sortedSessions(sessionsRoot: string, state: SessionIndexFile): SessionInfo[] {\n\tconst ordered = [...state.sessions].sort((a, b) => b.modified_ms - a.modified_ms || b.file.localeCompare(a.file));\n\treturn ordered.map((entry) => sessionInfoFromEntry(sessionsRoot, entry));\n}\n\n/** 重建路径:由全量扫描结果构建索引状态,逐文件 stat 写入 mtime 令牌。 */\nasync function stateFromSessions(sessionsRoot: string, sessions: SessionInfo[]): Promise<SessionIndexFile> {\n\tconst state = emptyState();\n\tfor (const info of sessions) {\n\t\tconst location = locationInRoot(sessionsRoot, info.path);\n\t\tif (!location) continue;\n\t\tlet mtimeMs: number | undefined;\n\t\ttry {\n\t\t\tmtimeMs = (await stat(info.path)).mtimeMs;\n\t\t} catch {\n\t\t\tmtimeMs = undefined;\n\t\t}\n\t\taddSessionFromInfo(state, location.dir, location.file, info, mtimeMs);\n\t}\n\treturn state;\n}\n\n/**\n * 对账:目录名集合 diff + 目录内文件名集合 diff,逐文件 stat 与条目的\n * file_mtime_ms 令牌比对(设计 §5)——条目缺失 → buildInfo 补;mtime 与令牌\n * 一致 → 缓存可信直接沿用;不一致或令牌缺失 → buildInfo 重读刷新缓存与新令牌\n * (重读失败 = 文件已不再是会话 → 剔除陈旧条目);索引有、磁盘无 → 剔除;\n * 消失目录连同其 project 行剔除。返回是否有变更。\n */\nasync function reconcileIndex(\n\tsessionsRoot: string,\n\tstate: SessionIndexFile,\n\tbuildInfo: (path: string) => Promise<SessionInfo | null>,\n\tsignal?: AbortSignal,\n): Promise<boolean> {\n\t// 磁盘清单:目录名 → (文件名 → stat mtime)。stat 失败(readdir 与 stat 之间\n\t// 被删/不可读)视同本轮缺席,下轮对账收敛。\n\tconst diskDirs = new Map<string, Map<string, number>>();\n\tlet rootEntries: Dirent[];\n\ttry {\n\t\trootEntries = await readdir(sessionsRoot, { withFileTypes: true });\n\t} catch {\n\t\t// sessions 根不可读视同空盘:剔除全部条目(索引是投影,根恢复后重建)。\n\t\trootEntries = [];\n\t}\n\tfor (const entry of rootEntries) {\n\t\tif (entry.name === AGENT_SESSIONS_DIR_NAME) continue;\n\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\t\tlet fileNames: string[];\n\t\ttry {\n\t\t\tfileNames = await readdir(join(sessionsRoot, entry.name));\n\t\t} catch {\n\t\t\t// 目录不可读视同已清空,其索引条目被剔除(悬挂容忍,设计 §6)。\n\t\t\tfileNames = [];\n\t\t}\n\t\tconst files = new Map<string, number>();\n\t\tfor (const file of fileNames) {\n\t\t\tif (!isSessionFileName(file)) continue;\n\t\t\tsignal?.throwIfAborted();\n\t\t\ttry {\n\t\t\t\tfiles.set(file, (await stat(join(sessionsRoot, entry.name, file))).mtimeMs);\n\t\t\t} catch {\n\t\t\t\t// 本轮视同缺席。\n\t\t\t}\n\t\t}\n\t\tdiskDirs.set(entry.name, files);\n\t}\n\n\tlet changed = false;\n\n\tfor (const [dir, files] of diskDirs) {\n\t\tfor (const [file, mtimeMs] of files) {\n\t\t\tconst existing = findSessionEntry(state, dir, file);\n\t\t\tif (existing && existing.file_mtime_ms !== undefined && existing.file_mtime_ms === mtimeMs) continue;\n\t\t\tsignal?.throwIfAborted();\n\t\t\tconst info = await buildInfo(join(sessionsRoot, dir, file));\n\t\t\tif (!info) {\n\t\t\t\tif (existing) {\n\t\t\t\t\t// 文件已不再解析为会话(损坏/改写):剔除陈旧条目。\n\t\t\t\t\tstate.sessions = state.sessions.filter((candidate) => candidate !== existing);\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\taddSessionFromInfo(state, dir, file, info, mtimeMs);\n\t\t\tchanged = true;\n\t\t}\n\t}\n\n\tconst keptSessions = state.sessions.filter((entry) => diskDirs.get(entry.dir)?.has(entry.file));\n\tif (keptSessions.length !== state.sessions.length) {\n\t\tstate.sessions = keptSessions;\n\t\tchanged = true;\n\t}\n\n\tconst keptProjects = state.projects.filter((project) => diskDirs.has(project.dir));\n\tif (keptProjects.length !== state.projects.length) {\n\t\tstate.projects = keptProjects;\n\t\tchanged = true;\n\t}\n\n\treturn changed;\n}\n\n/**\n * listAll 默认布局分支的索引读路径。返回 undefined 表示索引层意外失败(IO 等),\n * 调用方应回退到旧全量扫描(fail-open,设计 §5);中止按惯例抛 AbortError。\n */\nexport async function listSessionsViaIndex(input: ListSessionsViaIndexInput): Promise<SessionInfo[] | undefined> {\n\tconst { sessionsRoot, buildInfo, scanAll, onHit, signal } = input;\n\ttry {\n\t\tsignal?.throwIfAborted();\n\t\tconst indexPath = getSessionIndexPath();\n\t\tlet state: SessionIndexFile;\n\t\tif (pendingFlush?.indexPath === indexPath) {\n\t\t\t// 节流窗口内已积累的更新是对账基准:对账落盘不得把窗口内更新回退覆盖。\n\t\t\tstate = pendingFlush.state;\n\t\t} else {\n\t\t\tconst fromDisk = readIndexFile(indexPath);\n\t\t\tif (fromDisk === null) {\n\t\t\t\t// 缺失/损坏/版本不符 → 旧扫描全量重建并落盘(与旧行为同价,设计 §5.1)。\n\t\t\t\tconst sessions = await scanAll();\n\t\t\t\twriteStateSync(indexPath, await stateFromSessions(sessionsRoot, sessions));\n\t\t\t\treturn sessions;\n\t\t\t}\n\t\t\tstate = fromDisk;\n\t\t}\n\t\tconst changed = await reconcileIndex(sessionsRoot, state, buildInfo, signal);\n\t\tif (changed) {\n\t\t\tif (pendingFlush?.indexPath === indexPath) clearPending();\n\t\t\twriteStateSync(indexPath, state);\n\t\t}\n\t\tconst sessions = sortedSessions(sessionsRoot, state);\n\t\tonHit?.(sessions);\n\t\treturn sessions;\n\t} catch {\n\t\tsignal?.throwIfAborted();\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -297,6 +297,15 @@ export declare class SessionManager {
297
297
  */
298
298
  private _hasConversation;
299
299
  _persist(entry: SessionEntry): void;
300
+ /**
301
+ * 会话索引写钩子(设计 docs/design/会话索引.md §4)。定位是预热优化,不承担
302
+ * 正确性:条目缺失或过期由读路径的 mtime 令牌对账刷新。仅在默认布局且已落盘
303
+ * (flushed = 会话文件已存在于磁盘)时生效;自定义 --session-dir 与 inMemory
304
+ * 会话不进索引。字段从内存 fileEntries 派生(deriveSessionIndexFields,与
305
+ * buildSessionInfo 逐语义对齐)。索引维护的任何失败都在 recordSessionUpdate
306
+ * 内部吞掉,绝不影响会话持久化。
307
+ */
308
+ private notifySessionIndex;
300
309
  private _appendEntry;
301
310
  /** Append a message as child of current leaf, then advance leaf. Returns entry id.
302
311
  * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.