dsh-recall-plugin 2.3.1 → 2.3.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.
- package/CHANGELOG.md +30 -0
- package/README.en.md +4 -4
- package/README.md +8 -9
- package/lib/client.js +625 -476
- package/lib/config.js +53 -96
- package/lib/diagnostics.js +27 -52
- package/lib/dump-parse.js +79 -61
- package/lib/errors.js +41 -55
- package/lib/index.js +345 -446
- package/lib/maintenance.js +204 -275
- package/lib/routes-core.js +89 -140
- package/lib/routes-manage.js +571 -624
- package/lib/scripts.posix.js +452 -638
- package/lib/scripts.pwsh.js +537 -778
- package/lib/session-info.js +42 -57
- package/lib/snapshots.js +400 -588
- package/lib/store.js +364 -563
- package/package.json +7 -3
package/lib/index.js
CHANGED
|
@@ -1,446 +1,345 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
async function
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
for (const [
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
add(e.id, baseRoot || (typeof e.root === 'string' && e.root) || null, e.sessionId, e.time)
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return records
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ---- 端点表组装:核心路由 + 管理路由,合并进单一 endpoints 对象供
|
|
353
|
-
// webServer 前缀路由分发(端点名是 path 第一段,故无跨域命名冲突)。
|
|
354
|
-
const deps = {
|
|
355
|
-
ctx, rt, snaps, maint, state, cfg, supported,
|
|
356
|
-
enqueue, agentBusy, runLimited, readJsonBody, sendJson, errBody,
|
|
357
|
-
listExcludeFiles, dumpStores, locateSnapshotOnDisk, collectAllSnapshotRecords,
|
|
358
|
-
listCache, excludeCache, usageCache, sessionInfo, titleFromEvents, messageTextFromEvents,
|
|
359
|
-
// readSettings 传活绑定而非当前引用(A1):dsh-settings 服务晚挂载时
|
|
360
|
-
// setSource 会重绑定 readSettings——按值捕获的副本停在旧闭包(入口
|
|
361
|
-
// config),config-reset 会按旧值「恢复默认」。活绑定让消费者每次调用
|
|
362
|
-
// 都取到当前闭包。
|
|
363
|
-
applyResolvedConfig, readSettings: () => readSettings(), DEFAULTS, rescueRollback, E,
|
|
364
|
-
}
|
|
365
|
-
const endpoints = {
|
|
366
|
-
...createRoutesCore(deps),
|
|
367
|
-
...createRoutesManage(deps),
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
ctx.effect(() => webServer.register({
|
|
371
|
-
kind: 'prefix',
|
|
372
|
-
path: '/api/recall',
|
|
373
|
-
handler: async (req, res) => {
|
|
374
|
-
const path = (req.url || '').split('?')[0]
|
|
375
|
-
const name = path.replace(/^\/api\/recall\/?/, '').split('/')[0]
|
|
376
|
-
const endpoint = endpoints[name]
|
|
377
|
-
if (!endpoint) {
|
|
378
|
-
sendJson(res, 404, { ok: false, code: E.RECALL_UNKNOWN_ENDPOINT, message: 'unknown endpoint: ' + name })
|
|
379
|
-
return
|
|
380
|
-
}
|
|
381
|
-
try {
|
|
382
|
-
const args = await readJsonBody(req)
|
|
383
|
-
sendJson(res, 200, await endpoint(args))
|
|
384
|
-
} catch (error) {
|
|
385
|
-
sendJson(res, 200, errBody(error))
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}))
|
|
389
|
-
|
|
390
|
-
// 快照事件与启动预热仅在受支持平台注册(见上方 supported 短路说明)
|
|
391
|
-
if (!supported) return
|
|
392
|
-
|
|
393
|
-
// 每条用户消息触发快照(子代理会话跳过);快照完成后串行接一次维护
|
|
394
|
-
// (定期 gc / 会话清理)——排在同一条队列里,与快照天然互斥,无 git 锁竞态
|
|
395
|
-
ctx.on('session/event', (session, event) => {
|
|
396
|
-
if (!event || event.type !== 'user/message') return
|
|
397
|
-
const data = event.data
|
|
398
|
-
if (!data || typeof data.id !== 'string' || !data.id) return
|
|
399
|
-
const source = data.source
|
|
400
|
-
if (!source || source.kind !== 'user') return
|
|
401
|
-
if (session && session.header && session.header.origin === 'subagent') return
|
|
402
|
-
const messageId = data.id
|
|
403
|
-
const time = event.time
|
|
404
|
-
state.queue = state.queue
|
|
405
|
-
// 快照总开关:cfg 按调用时读取,设置页热更即时生效。关闭时只冻结新建,
|
|
406
|
-
// maybeMaintain 照常跑——已停增的存储仍需被 gc/清理治理。
|
|
407
|
-
.then(() => (cfg.snapshotEnabled ? snaps.captureSnapshot(session.id, messageId, time) : null))
|
|
408
|
-
.then(() => maint.maybeMaintain(session.id))
|
|
409
|
-
// PF-6:不清 items 只标 stale——列表先按旧数据应答、后台补新(见 listCache 注释)
|
|
410
|
-
.then(() => { listCache.stale = true })
|
|
411
|
-
.catch((error) => rt.recordError('recall snapshot error: ' + String(error)))
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
// 启动预热:所有已存在工作区解析存储、重建索引与孤儿快照,并清理旧版
|
|
415
|
-
// 项目内 blobs 目录(home 可用时)。不触发维护(开机预热应尽量轻)。
|
|
416
|
-
;(async () => {
|
|
417
|
-
const warmupRoots = new Map()
|
|
418
|
-
for (const session of ctx.sessions.list()) {
|
|
419
|
-
const cwd = session && session.header && session.header.cwd
|
|
420
|
-
if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, session.id)
|
|
421
|
-
}
|
|
422
|
-
const querySvc = ctx.get('sessionQuery')
|
|
423
|
-
if (querySvc && typeof querySvc.listSessions === 'function') {
|
|
424
|
-
try {
|
|
425
|
-
const records = await querySvc.listSessions()
|
|
426
|
-
for (const record of records || []) {
|
|
427
|
-
// listSessions 记录形如 {header, live, persisted},会话 id 在
|
|
428
|
-
// header.id——此前误用顶层 record.id(恒 undefined),预热重建的
|
|
429
|
-
// 孤儿快照 sessionId 记为空,树形管理里会落进「已删除会话」。
|
|
430
|
-
const id = record && record.header && record.header.id ? record.header.id : null
|
|
431
|
-
const cwd = record && record.header && record.header.cwd
|
|
432
|
-
if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, id)
|
|
433
|
-
}
|
|
434
|
-
} catch (error) { /* 冷元数据不可用则退回 live 注册表 */ }
|
|
435
|
-
}
|
|
436
|
-
for (const [cwd, sessionId] of warmupRoots) {
|
|
437
|
-
Promise.resolve(rt.resolveStore(cwd))
|
|
438
|
-
.then(() => rt.tryUpgradeToHome(cwd))
|
|
439
|
-
.then((store) => rt.ensureGit(cwd, store))
|
|
440
|
-
.then(() => snaps.loadIndex(cwd, sessionId))
|
|
441
|
-
.then(() => snaps.rebuildOrphans(cwd, sessionId))
|
|
442
|
-
.then(() => rt.cleanupLegacy(cwd))
|
|
443
|
-
.catch(() => {})
|
|
444
|
-
}
|
|
445
|
-
})()
|
|
446
|
-
}
|
|
1
|
+
import { createConfig, Config, DEFAULTS } from "./config.js";
|
|
2
|
+
import { parseStoresDump, parseExcludeDump } from "./dump-parse.js";
|
|
3
|
+
import { createRuntime } from "./store.js";
|
|
4
|
+
import { createSnapshots, rescueRollback } from "./snapshots.js";
|
|
5
|
+
import { createMaintenance } from "./maintenance.js";
|
|
6
|
+
import { createSessionInfo, titleFromEvents, messageTextFromEvents } from "./session-info.js";
|
|
7
|
+
import { createRoutesCore } from "./routes-core.js";
|
|
8
|
+
import { createRoutesManage } from "./routes-manage.js";
|
|
9
|
+
import * as dshSettings from "@deepseek-ai/dsh-settings";
|
|
10
|
+
import * as E from "./errors.js";
|
|
11
|
+
const name = "dsh-recall-plugin";
|
|
12
|
+
const inject = ["shell", "sessions", "webServer", "agents"];
|
|
13
|
+
function apply(ctx, config) {
|
|
14
|
+
const webServer = ctx.webServer;
|
|
15
|
+
const cfg = createConfig(config);
|
|
16
|
+
const rt = createRuntime(ctx, cfg);
|
|
17
|
+
const snaps = createSnapshots(ctx, rt, cfg);
|
|
18
|
+
const maint = createMaintenance(ctx, rt, snaps, cfg);
|
|
19
|
+
const state = rt.state;
|
|
20
|
+
let readSettings = () => config;
|
|
21
|
+
function applyResolvedConfig(resolved) {
|
|
22
|
+
Object.assign(cfg, createConfig(resolved && typeof resolved === "object" ? resolved : {}));
|
|
23
|
+
}
|
|
24
|
+
const settingsHooks = {
|
|
25
|
+
setSource: (fn) => {
|
|
26
|
+
readSettings = fn;
|
|
27
|
+
},
|
|
28
|
+
onChange: () => applyResolvedConfig(readSettings())
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
if (typeof dshSettings.installSettingsSection === "function") {
|
|
32
|
+
dshSettings.installSettingsSection(ctx, "dsh-recall", Config, config, settingsHooks);
|
|
33
|
+
} else if (typeof ctx.inject === "function") {
|
|
34
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
35
|
+
const settingsService = settingsCtx.settings;
|
|
36
|
+
if (typeof settingsService.installSection === "function") {
|
|
37
|
+
settingsService.installSection(ctx, "dsh-recall", Config, config, settingsHooks);
|
|
38
|
+
} else if (typeof settingsService.register === "function") {
|
|
39
|
+
const scope = settingsService.register("dsh-recall", Config, { base: config });
|
|
40
|
+
settingsHooks.setSource(() => scope.get());
|
|
41
|
+
settingsHooks.onChange();
|
|
42
|
+
scope.watch(() => settingsHooks.onChange());
|
|
43
|
+
settingsCtx.effect(() => () => {
|
|
44
|
+
settingsHooks.setSource(() => config);
|
|
45
|
+
settingsHooks.onChange();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
rt.recordError("recall settings namespace skipped: " + String(error));
|
|
52
|
+
}
|
|
53
|
+
const supported = process.platform === "win32" || process.platform === "linux" || process.platform === "darwin";
|
|
54
|
+
const MAX_BODY_BYTES = 1048576;
|
|
55
|
+
const listCache = { at: 0, items: null, stale: false, refreshing: null };
|
|
56
|
+
const excludeCache = { at: 0, payload: null };
|
|
57
|
+
const usageCache = { at: 0, payload: null };
|
|
58
|
+
const sessionInfo = createSessionInfo(ctx);
|
|
59
|
+
async function readJsonBody(req) {
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
for await (const chunk of req) {
|
|
63
|
+
size += chunk.length;
|
|
64
|
+
if (size > MAX_BODY_BYTES) throw new Error(E.RECALL_BODY_TOO_LARGE);
|
|
65
|
+
chunks.push(chunk);
|
|
66
|
+
}
|
|
67
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
68
|
+
if (!text.trim()) return {};
|
|
69
|
+
return JSON.parse(text);
|
|
70
|
+
}
|
|
71
|
+
function sendJson(res, status, body) {
|
|
72
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
73
|
+
res.end(JSON.stringify(body));
|
|
74
|
+
}
|
|
75
|
+
function errBody(error) {
|
|
76
|
+
const e = error;
|
|
77
|
+
const text = String(e && e.message ? e.message : error);
|
|
78
|
+
if (text === E.RECALL_BODY_TOO_LARGE) return { ok: false, code: E.RECALL_BODY_TOO_LARGE, message: "\u8BF7\u6C42\u4F53\u8D85\u8FC7 1MB \u4E0A\u9650" };
|
|
79
|
+
return { ok: false, code: E.RECALL_ERROR, message: text };
|
|
80
|
+
}
|
|
81
|
+
function enqueue(task) {
|
|
82
|
+
const run = state.queue.then(task);
|
|
83
|
+
state.queue = run.catch(() => {
|
|
84
|
+
});
|
|
85
|
+
return run;
|
|
86
|
+
}
|
|
87
|
+
async function runLimited(tasks, concurrency) {
|
|
88
|
+
const limit = concurrency > 0 ? concurrency : 4;
|
|
89
|
+
let index = 0;
|
|
90
|
+
const workers = Array.from({ length: Math.min(limit, tasks.length) }, async () => {
|
|
91
|
+
while (index < tasks.length) {
|
|
92
|
+
const task = tasks[index++];
|
|
93
|
+
await task();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
await Promise.all(workers);
|
|
97
|
+
}
|
|
98
|
+
function normalizeWorkdir(path) {
|
|
99
|
+
if (!path) return "";
|
|
100
|
+
let p = String(path);
|
|
101
|
+
return (process.platform === "win32" ? p.toLowerCase() : p).replace(/[\\/]+$/, "");
|
|
102
|
+
}
|
|
103
|
+
function agentBusy(sessionId, root) {
|
|
104
|
+
let reg = null;
|
|
105
|
+
try {
|
|
106
|
+
reg = ctx.agents;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (!reg) return false;
|
|
111
|
+
try {
|
|
112
|
+
if (typeof reg.list === "function") {
|
|
113
|
+
for (const agent of reg.list()) {
|
|
114
|
+
if (!agent || agent.status !== "running") continue;
|
|
115
|
+
if (sessionId && String(agent.id) === String(sessionId)) return true;
|
|
116
|
+
const cwd = agent.session && agent.session.header && agent.session.header.cwd;
|
|
117
|
+
if (root && cwd && normalizeWorkdir(cwd) === normalizeWorkdir(root)) return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (sessionId && typeof reg.get === "function") {
|
|
122
|
+
const agent = reg.get(sessionId);
|
|
123
|
+
return Boolean(agent && agent.status === "running");
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
async function listExcludeFiles() {
|
|
130
|
+
const roots = new Set(state.stores.keys());
|
|
131
|
+
for (const session of ctx.sessions.list()) {
|
|
132
|
+
const cwd = session && session.header && session.header.cwd;
|
|
133
|
+
if (cwd) roots.add(cwd);
|
|
134
|
+
}
|
|
135
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
136
|
+
await Promise.all(Array.from(roots).map(async (root) => {
|
|
137
|
+
try {
|
|
138
|
+
const store = await rt.resolveStore(root);
|
|
139
|
+
if (store && !byFile.has(store.excludeFile)) byFile.set(store.excludeFile, { store, roots: [] });
|
|
140
|
+
byFile.get(store.excludeFile).roots.push(root);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
try {
|
|
145
|
+
const container = await rt.resolveHomeContainer();
|
|
146
|
+
if (container) {
|
|
147
|
+
const probe = rt.scripts.stripBom(await rt.runShell(rt.scripts.dirExistsScript(container), { stdoutMaxBytes: 4096 })).trim();
|
|
148
|
+
if (probe === "YES") {
|
|
149
|
+
const excludeFile = container + (rt.isWin ? "\\" : "/") + "exclude.txt";
|
|
150
|
+
if (!byFile.has(excludeFile)) {
|
|
151
|
+
byFile.set(excludeFile, { store: { dir: container, home: true, excludeFile }, roots: [] });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
}
|
|
157
|
+
return byFile;
|
|
158
|
+
}
|
|
159
|
+
async function collectCwds() {
|
|
160
|
+
const cwds = /* @__PURE__ */ new Set();
|
|
161
|
+
for (const session of ctx.sessions.list()) {
|
|
162
|
+
const cwd = session && session.header && session.header.cwd;
|
|
163
|
+
if (cwd) cwds.add(cwd);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const querySvc = ctx.get("sessionQuery");
|
|
167
|
+
if (querySvc && typeof querySvc.listSessions === "function") {
|
|
168
|
+
for (const record of await querySvc.listSessions()) {
|
|
169
|
+
const cwd = record && record.header && record.header.cwd;
|
|
170
|
+
if (cwd) cwds.add(cwd);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
}
|
|
175
|
+
return cwds;
|
|
176
|
+
}
|
|
177
|
+
async function dumpStores() {
|
|
178
|
+
const container = await rt.resolveHomeContainer();
|
|
179
|
+
const extras = Array.from(await collectCwds()).map((cwd) => cwd + (rt.isWin ? "\\" : "/") + ".dsh-recall-snapshots");
|
|
180
|
+
try {
|
|
181
|
+
const text = rt.scripts.stripBom(await rt.runShell(rt.scripts.storesDumpScript(container || "", extras), { timeoutMs: 12e4, stdoutMaxBytes: 8388608 }));
|
|
182
|
+
return parseStoresDump(text);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("recall stores dump failed:", String(error && error.stack || error));
|
|
185
|
+
return /* @__PURE__ */ new Map();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function locateSnapshotOnDisk(id) {
|
|
189
|
+
if (!id) return null;
|
|
190
|
+
const dump = await dumpStores();
|
|
191
|
+
const hints = /* @__PURE__ */ new Map();
|
|
192
|
+
for (const [root, st] of state.stores.entries()) {
|
|
193
|
+
if (st && st.dir) hints.set(st.dir, root);
|
|
194
|
+
}
|
|
195
|
+
for (const [dir, info] of dump) {
|
|
196
|
+
const hit = (info.entries || []).find((e) => e && e.id === id);
|
|
197
|
+
if (!hit) continue;
|
|
198
|
+
const root = info.root || hints.get(dir) || typeof hit.root === "string" && hit.root || null;
|
|
199
|
+
if (!root) continue;
|
|
200
|
+
try {
|
|
201
|
+
const store = await rt.resolveStore(root);
|
|
202
|
+
if (store) return { store, root };
|
|
203
|
+
} catch (error) {
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
async function collectAllSnapshotRecords() {
|
|
209
|
+
const records = /* @__PURE__ */ new Map();
|
|
210
|
+
function add(id, root, sessionId, time) {
|
|
211
|
+
if (!id || typeof id !== "string") return;
|
|
212
|
+
const old = records.get(id);
|
|
213
|
+
if (!old) {
|
|
214
|
+
records.set(id, {
|
|
215
|
+
id,
|
|
216
|
+
root: root || null,
|
|
217
|
+
sessionId: sessionId || null,
|
|
218
|
+
time: typeof time === "number" ? time : 0
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!old.root && root) old.root = root;
|
|
223
|
+
if (!old.sessionId && sessionId) old.sessionId = sessionId;
|
|
224
|
+
if (!old.time && time) old.time = time;
|
|
225
|
+
}
|
|
226
|
+
for (const [id, s] of state.snapshots.entries()) {
|
|
227
|
+
if (s) add(id, s.root, s.sessionId, s.time);
|
|
228
|
+
}
|
|
229
|
+
const dump = await dumpStores();
|
|
230
|
+
const hints = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const [root, st] of state.stores.entries()) {
|
|
232
|
+
if (st && st.dir) hints.set(st.dir, root);
|
|
233
|
+
}
|
|
234
|
+
for (const [dir, info] of dump) {
|
|
235
|
+
const baseRoot = info.root || hints.get(dir) || null;
|
|
236
|
+
for (const e of info.entries || []) {
|
|
237
|
+
if (!e || typeof e.id !== "string") continue;
|
|
238
|
+
add(e.id, baseRoot || typeof e.root === "string" && e.root || null, e.sessionId, e.time);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return records;
|
|
242
|
+
}
|
|
243
|
+
const deps = {
|
|
244
|
+
ctx,
|
|
245
|
+
rt,
|
|
246
|
+
snaps,
|
|
247
|
+
maint,
|
|
248
|
+
state,
|
|
249
|
+
cfg,
|
|
250
|
+
supported,
|
|
251
|
+
enqueue,
|
|
252
|
+
agentBusy,
|
|
253
|
+
runLimited,
|
|
254
|
+
readJsonBody,
|
|
255
|
+
sendJson,
|
|
256
|
+
errBody,
|
|
257
|
+
listExcludeFiles,
|
|
258
|
+
dumpStores,
|
|
259
|
+
locateSnapshotOnDisk,
|
|
260
|
+
collectAllSnapshotRecords,
|
|
261
|
+
listCache,
|
|
262
|
+
excludeCache,
|
|
263
|
+
usageCache,
|
|
264
|
+
sessionInfo,
|
|
265
|
+
titleFromEvents,
|
|
266
|
+
messageTextFromEvents,
|
|
267
|
+
// readSettings 传活绑定而非当前引用(A1):dsh-settings 服务晚挂载时
|
|
268
|
+
// setSource 会重绑定 readSettings——按值捕获的副本停在旧闭包(入口
|
|
269
|
+
// config),config-reset 会按旧值「恢复默认」。活绑定让消费者每次调用
|
|
270
|
+
// 都取到当前闭包。
|
|
271
|
+
applyResolvedConfig,
|
|
272
|
+
readSettings: () => readSettings(),
|
|
273
|
+
DEFAULTS,
|
|
274
|
+
rescueRollback,
|
|
275
|
+
E
|
|
276
|
+
};
|
|
277
|
+
const endpoints = {
|
|
278
|
+
...createRoutesCore(deps),
|
|
279
|
+
...createRoutesManage(deps)
|
|
280
|
+
};
|
|
281
|
+
ctx.effect(() => webServer.register({
|
|
282
|
+
kind: "prefix",
|
|
283
|
+
path: "/api/recall",
|
|
284
|
+
handler: async (req, res) => {
|
|
285
|
+
const path = (req.url || "").split("?")[0];
|
|
286
|
+
const name2 = path.replace(/^\/api\/recall\/?/, "").split("/")[0];
|
|
287
|
+
const endpoint = endpoints[name2];
|
|
288
|
+
if (!endpoint) {
|
|
289
|
+
sendJson(res, 404, { ok: false, code: E.RECALL_UNKNOWN_ENDPOINT, message: "unknown endpoint: " + name2 });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const args = await readJsonBody(req);
|
|
294
|
+
sendJson(res, 200, await endpoint(args));
|
|
295
|
+
} catch (error) {
|
|
296
|
+
sendJson(res, 200, errBody(error));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}));
|
|
300
|
+
if (!supported) return;
|
|
301
|
+
ctx.on("session/event", (session, event) => {
|
|
302
|
+
if (!event || event.type !== "user/message") return;
|
|
303
|
+
const data = event.data;
|
|
304
|
+
if (!data || typeof data.id !== "string" || !data.id) return;
|
|
305
|
+
const source = data.source;
|
|
306
|
+
if (!source || source.kind !== "user") return;
|
|
307
|
+
if (session && session.header && session.header.origin === "subagent") return;
|
|
308
|
+
const messageId = data.id;
|
|
309
|
+
const time = event.time;
|
|
310
|
+
state.queue = state.queue.then(() => cfg.snapshotEnabled ? snaps.captureSnapshot(session.id, messageId, time) : null).then(() => maint.maybeMaintain(session.id)).then(() => {
|
|
311
|
+
listCache.stale = true;
|
|
312
|
+
}).catch((error) => rt.recordError("recall snapshot error: " + String(error)));
|
|
313
|
+
});
|
|
314
|
+
(async () => {
|
|
315
|
+
const warmupRoots = /* @__PURE__ */ new Map();
|
|
316
|
+
for (const session of ctx.sessions.list()) {
|
|
317
|
+
const cwd = session && session.header && session.header.cwd;
|
|
318
|
+
if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, session.id);
|
|
319
|
+
}
|
|
320
|
+
const querySvc = ctx.get("sessionQuery");
|
|
321
|
+
if (querySvc && typeof querySvc.listSessions === "function") {
|
|
322
|
+
try {
|
|
323
|
+
const records = await querySvc.listSessions();
|
|
324
|
+
for (const record of records || []) {
|
|
325
|
+
const id = record && record.header && record.header.id ? record.header.id : null;
|
|
326
|
+
const cwd = record && record.header && record.header.cwd;
|
|
327
|
+
if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, id);
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const [cwd, sessionId] of warmupRoots) {
|
|
333
|
+
Promise.resolve(rt.resolveStore(cwd)).then(() => rt.tryUpgradeToHome(cwd)).then((store) => rt.ensureGit(cwd, store)).then(() => snaps.loadIndex(cwd, sessionId)).then(() => snaps.rebuildOrphans(cwd, sessionId)).then(() => rt.cleanupLegacy(cwd)).catch(() => {
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
})();
|
|
337
|
+
}
|
|
338
|
+
export {
|
|
339
|
+
Config,
|
|
340
|
+
apply,
|
|
341
|
+
inject,
|
|
342
|
+
name,
|
|
343
|
+
parseExcludeDump,
|
|
344
|
+
parseStoresDump
|
|
345
|
+
};
|