dsh-recall-plugin 2.3.1 → 2.3.3
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 +48 -0
- package/README.en.md +4 -4
- package/README.md +8 -9
- package/lib/client.js +1689 -1484
- 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/snapshots.js
CHANGED
|
@@ -1,588 +1,400 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
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
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (!
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
async function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
async function
|
|
337
|
-
const
|
|
338
|
-
if (!
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (count > 0) return
|
|
402
|
-
}
|
|
403
|
-
const store = state.stores.get(root)
|
|
404
|
-
const gitExe = await rt.resolveGit()
|
|
405
|
-
if (!store || !gitExe) return
|
|
406
|
-
try {
|
|
407
|
-
// 带时间戳清单(listTagsWithTimeScript):重建条目从 tag 的
|
|
408
|
-
// creatordate 恢复 time——此前只列 tag 名,重建条目一律 time=0,
|
|
409
|
-
// 管理列表时间前缀缺失、retention/limits 按「最旧」误清真实快照。
|
|
410
|
-
const listing = S.stripBom(await rt.runShell(S.listTagsWithTimeScript(store, gitExe), { stdoutMaxBytes: 4194304 })).trim()
|
|
411
|
-
if (!listing) return
|
|
412
|
-
for (const { name, time } of parseTagsWithTime(listing)) {
|
|
413
|
-
const id = name.replace(/^snap-/, '')
|
|
414
|
-
// F-G1:安全 tag(snap-pre-rollback-<ts>)只作救援锚点,不进索引——
|
|
415
|
-
// 否则被 rebuild 成 time=0 条目后会进快照管理列表、占
|
|
416
|
-
// maxSnapshotsPerWorkspace 配额、被 retention/limits 当「最旧」优先
|
|
417
|
-
// 清掉,H1 的救援点在重度使用下会随 purge 消失(与 routes-core.js
|
|
418
|
-
// execute「不进 index.json、列表不展示」的设计承诺对齐)。
|
|
419
|
-
if (!id || isSafetySnapshotId(id) || state.snapshots.has(id)) continue
|
|
420
|
-
state.snapshots.set(id, { root, time: time || 0, sessionId })
|
|
421
|
-
}
|
|
422
|
-
await saveIndex(root, sessionId)
|
|
423
|
-
} catch (error) {
|
|
424
|
-
rt.recordError('recall rebuildOrphans failed: ' + String(error))
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function captureSnapshot(sessionId, messageId, time) {
|
|
429
|
-
const root = await rt.resolveRoot(sessionId)
|
|
430
|
-
if (!root) return
|
|
431
|
-
// 熔断检查要在任何解析/建仓动作之前:冷却期内连 resolveStore 都
|
|
432
|
-
// 不必跑,把失败重试的开销也一并止住
|
|
433
|
-
const fused = snapFailures.get(root)
|
|
434
|
-
if (fused && Date.now() < fused.skipUntil) return
|
|
435
|
-
let store = await rt.resolveStore(root)
|
|
436
|
-
store = await rt.tryUpgradeToHome(root)
|
|
437
|
-
// ensureGit 失败(issue #11 主线缺口):原先静默 return,不进
|
|
438
|
-
// snapFeedback,客户端空轮询 20 次后放弃、用户零感知。现在走与
|
|
439
|
-
// snapshotScript 失败相同的反馈通道——buildFeedbackError 把原始
|
|
440
|
-
// stderr 分类成可行动提示(锁冲突/磁盘满等),客户端轮询到 failed
|
|
441
|
-
// 即弹「快照失败:<提示>」并停止轮询。不接熔断:环境类失败常可自愈
|
|
442
|
-
// (清磁盘/退锁后下一条消息即恢复),保持按消息重试,刷屏由
|
|
443
|
-
// recordError 尾部去重与 toast 10min 节流缓解。
|
|
444
|
-
const g = await rt.ensureGit(root, store)
|
|
445
|
-
if (!g.ok) {
|
|
446
|
-
setFeedback(messageId, { failed: true, ...buildFeedbackError(g.error || '未知原因') })
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
await loadIndex(root, sessionId)
|
|
450
|
-
try {
|
|
451
|
-
const out = await rt.runShell(S.snapshotScript(root, store, state.gitExe, messageId, BASE()), { timeoutMs: 600000, stdoutMaxBytes: 65536 })
|
|
452
|
-
snapFailures.delete(root)
|
|
453
|
-
state.snapshots.set(String(messageId), { root, time: time || Date.now(), sessionId })
|
|
454
|
-
await saveIndex(root, sessionId)
|
|
455
|
-
setFeedback(messageId, { skipped: parseSkipped(out) })
|
|
456
|
-
} catch (error) {
|
|
457
|
-
rt.recordError('recall snapshot failed: ' + String(error))
|
|
458
|
-
// 分类后的提示替代原始 stderr 直传(M1-D4):识别为环境错误时给
|
|
459
|
-
// 可行动文案,未识别时 buildFeedbackError 内部回落原文截断(保现状)
|
|
460
|
-
setFeedback(messageId, { failed: true, ...buildFeedbackError(String(error)) })
|
|
461
|
-
await handleSnapshotFailure(root, store)
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// 逐消息反馈写入(issue #7 失败可见性):成功无跳过 → 清除(重试成功
|
|
466
|
-
// 自愈);失败/有跳过 → 记录。上限防泄漏:交替成功失败的长会话可以无限
|
|
467
|
-
// 积累,Map 保插入序做 FIFO 淘汰。
|
|
468
|
-
function setFeedback(messageId, rec) {
|
|
469
|
-
const id = String(messageId)
|
|
470
|
-
const keep = rec && ((rec.failed) || (Array.isArray(rec.skipped) && rec.skipped.length))
|
|
471
|
-
if (keep) state.snapFeedback.set(id, rec)
|
|
472
|
-
else state.snapFeedback.delete(id)
|
|
473
|
-
if (state.snapFeedback.size > 200) state.snapFeedback.delete(state.snapFeedback.keys().next().value)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// snapshot-info 端点的反馈查询:优先逐消息记录;无记录但该 root 熔断中
|
|
477
|
-
// 时反馈熔断状态(冷却期内的消息快照被静默跳过,客户端需要知道「不是
|
|
478
|
-
// 还没好,是暂停了」)。客户端只对近 5 分钟的消息弹提示,历史消息查询
|
|
479
|
-
// 不受影响。
|
|
480
|
-
async function feedbackFor(sessionId, messageId) {
|
|
481
|
-
const rec = state.snapFeedback.get(String(messageId || ''))
|
|
482
|
-
if (rec) return rec
|
|
483
|
-
if (!sessionId) return {}
|
|
484
|
-
const root = await rt.resolveRoot(sessionId)
|
|
485
|
-
if (root) {
|
|
486
|
-
const f = snapFailures.get(root)
|
|
487
|
-
if (f && Date.now() < f.skipUntil) {
|
|
488
|
-
return { failed: true, error: '快照连续失败已暂停(熔断),约 ' + Math.ceil((f.skipUntil - Date.now()) / 60000) + ' 分钟后自动重试,详情见设置 · 插件配置 · 最近错误' }
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
return {}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// 失败善后 = 清残骸 + 推进熔断。captureSnapshot 的调用方就是串行队列
|
|
495
|
-
// (见 index.js 事件接线),这两个动作留在 catch 里顺势排队执行,
|
|
496
|
-
// 与下一次快照天然互斥,无 git 锁竞态;自身整体 best-effort,
|
|
497
|
-
// 清理失败不该让队列任务以异常收场。
|
|
498
|
-
async function handleSnapshotFailure(root, store) {
|
|
499
|
-
if (store && state.gitExe) {
|
|
500
|
-
try {
|
|
501
|
-
await rt.runShell(S.pruneScript(store, state.gitExe), { timeoutMs: 600000, stdoutMaxBytes: 4096 })
|
|
502
|
-
} catch (error) {
|
|
503
|
-
rt.recordError('recall prune after snapshot failure failed: ' + String(error))
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
const f = snapFailures.get(root) || { count: 0, skipUntil: 0 }
|
|
507
|
-
f.count++
|
|
508
|
-
if (f.count >= FUSE_AFTER) {
|
|
509
|
-
const backoff = Math.min(FUSE_BACKOFF_BASE_MS * 2 ** (f.count - FUSE_AFTER), FUSE_BACKOFF_CAP_MS)
|
|
510
|
-
const wasFused = Date.now() < f.skipUntil
|
|
511
|
-
f.skipUntil = Date.now() + backoff
|
|
512
|
-
// 只在「未熔断→熔断」的跳变沿记录:熔断期间每条消息都会走上面的
|
|
513
|
-
// 静默跳过分支,逐条记录会把最近错误环形缓冲刷成同一条目
|
|
514
|
-
if (!wasFused) rt.recordError('recall snapshot fused after ' + f.count + ' consecutive failures, backoff ' + Math.round(backoff / 60000) + 'min for: ' + root)
|
|
515
|
-
}
|
|
516
|
-
snapFailures.set(root, f)
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// 变更清单截断上限:防止超大工作区(几千个文件)把 DOM 与 JSON
|
|
520
|
-
// 双双撑爆。清单对用户的价值集中在前若干条,其余以 truncated 计数
|
|
521
|
-
// 汇总展示;total 保留完整计数让面板文案仍准确。
|
|
522
|
-
const MAX_CHANGES = 500
|
|
523
|
-
|
|
524
|
-
async function diffFor(messageId) {
|
|
525
|
-
const snap = state.snapshots.get(String(messageId))
|
|
526
|
-
if (!snap) return null
|
|
527
|
-
const store = state.stores.get(snap.root)
|
|
528
|
-
if (!store) return null
|
|
529
|
-
// 8MB 上限:按平均每条 60 字节估算可容纳十余万条,正常项目远够;
|
|
530
|
-
// 真超限时报错文案与「JSON 半截解析失败」的真实原因脱节,需显式检测
|
|
531
|
-
const text = S.stripBom(await rt.runShell(S.diffScript(snap.root, store, state.gitExe, 'snap-' + messageId, BASE(), MAX_CHANGES), { timeoutMs: 600000, stdoutMaxBytes: 8388608 }))
|
|
532
|
-
const trimmed = text.trim()
|
|
533
|
-
if (!trimmed) return { changes: [], total: 0, truncated: false, treeId: null }
|
|
534
|
-
// PF-1:解析下沉到 parseDiffOutput(标记行剥离 + 截断 + 树指纹),纯函数单测钉住
|
|
535
|
-
return parseDiffOutput(trimmed, rt.isWin, MAX_CHANGES)
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async function rollbackFor(messageId) {
|
|
539
|
-
const snap = state.snapshots.get(String(messageId))
|
|
540
|
-
if (!snap) return { ok: false, error: '该消息没有可用的项目快照' }
|
|
541
|
-
const store = state.stores.get(snap.root)
|
|
542
|
-
if (!store) return { ok: false, error: '快照存储不可用' }
|
|
543
|
-
try {
|
|
544
|
-
const text = S.stripBom(await rt.runShell(S.rollbackScript(snap.root, store, state.gitExe, 'snap-' + messageId, BASE()), { timeoutMs: 600000, stdoutMaxBytes: 65536 }))
|
|
545
|
-
const m = text.trim().match(/^ROLLBACK_OK\s+(\d+)\s+(\d+)/)
|
|
546
|
-
if (!m) {
|
|
547
|
-
// 无 ROLLBACK_OK 哨兵:脚本在输出哨兵前终止,工作区状态不可知——
|
|
548
|
-
// 一律按「可能半回退」处理,交给 execute 侧救援(H1)。
|
|
549
|
-
return { ok: false, partial: true, error: '回退脚本未正常完成(工作区可能处于半回退状态):' + text.slice(0, 300) }
|
|
550
|
-
}
|
|
551
|
-
const deleted = parseInt(m[1], 10)
|
|
552
|
-
const restored = parseInt(m[2], 10)
|
|
553
|
-
return { ok: true, count: (Number.isNaN(deleted) ? 0 : deleted) + (Number.isNaN(restored) ? 0 : restored) }
|
|
554
|
-
} catch (error) {
|
|
555
|
-
// runShell 抛错(脚本异常终止):工作区同样可能半回退,交 execute 救援。
|
|
556
|
-
const msg = String(error && error.message ? error.message : error)
|
|
557
|
-
return { ok: false, partial: true, error: msg }
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// 解析“整段回退”的会话切点:优先读 live 会话的内存事件(零 IO、毫秒级),
|
|
562
|
-
// 冷会话回退到 sessionQuery.readSession;结果按 (会话, 消息) 缓存——
|
|
563
|
-
// 消息一旦入日志,其之前的 turn/end 永不变化,缓存终身有效。
|
|
564
|
-
async function resolveCutSeq(sessionId, messageId) {
|
|
565
|
-
if (!sessionId || !messageId) return null
|
|
566
|
-
const cacheKey = String(sessionId) + '\u0000' + String(messageId)
|
|
567
|
-
if (state.cutSeqCache.has(cacheKey)) return state.cutSeqCache.get(cacheKey)
|
|
568
|
-
let result = null
|
|
569
|
-
const live = sessions.get(sessionId)
|
|
570
|
-
if (live && Array.isArray(live.events)) {
|
|
571
|
-
result = scanCutSeq(live.events, messageId)
|
|
572
|
-
} else {
|
|
573
|
-
const query = ctx.get('sessionQuery')
|
|
574
|
-
if (query) {
|
|
575
|
-
try {
|
|
576
|
-
const log = await query.readSession(sessionId)
|
|
577
|
-
result = scanCutSeq(Array.isArray(log && log.events) ? log.events : [], messageId)
|
|
578
|
-
} catch (error) {
|
|
579
|
-
result = null
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
state.cutSeqCache.set(cacheKey, result)
|
|
584
|
-
return result
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
return { saveIndex, loadIndex, readExclude, writeExclude, rebuildOrphans, captureSnapshot, diffFor, rollbackFor, resolveCutSeq, feedbackFor, loadLineage, recordLineage }
|
|
588
|
-
}
|
|
1
|
+
import * as E from "./errors.js";
|
|
2
|
+
import { buildFeedbackError } from "./diagnostics.js";
|
|
3
|
+
function parseSkipped(out) {
|
|
4
|
+
const skipped = [];
|
|
5
|
+
for (const line of String(out || "").split(/\r?\n/)) {
|
|
6
|
+
if (line.indexOf("SNAP_SKIP ") === 0) skipped.push(line.slice("SNAP_SKIP ".length));
|
|
7
|
+
}
|
|
8
|
+
return skipped;
|
|
9
|
+
}
|
|
10
|
+
function parseChanges(text, isWin) {
|
|
11
|
+
if (isWin) {
|
|
12
|
+
const parsed = JSON.parse(text);
|
|
13
|
+
if (Array.isArray(parsed)) return parsed;
|
|
14
|
+
if (parsed && typeof parsed === "object") return [parsed];
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const line of text.split(/\r?\n/)) {
|
|
19
|
+
if (!line) continue;
|
|
20
|
+
const tab = line.indexOf(" ");
|
|
21
|
+
if (tab < 0) continue;
|
|
22
|
+
out.push({ kind: line.slice(0, tab), rel: line.slice(tab + 1) });
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function parseTreeId(out) {
|
|
27
|
+
for (const line of String(out || "").split(/\r?\n/)) {
|
|
28
|
+
if (line.indexOf("TREE ") === 0) return line.slice(5).trim() || null;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function parseDiffOutput(text, isWin, maxChanges) {
|
|
33
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
34
|
+
let total = null;
|
|
35
|
+
let treeId = null;
|
|
36
|
+
const body = [];
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (line.indexOf("TREE ") === 0) {
|
|
39
|
+
treeId = line.slice(5).trim() || null;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (line.indexOf("TOTAL ") === 0) {
|
|
43
|
+
const n = parseInt(line.slice(6), 10);
|
|
44
|
+
if (Number.isFinite(n) && n >= 0) total = n;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
body.push(line);
|
|
48
|
+
}
|
|
49
|
+
const raw = body.join("\n").trim();
|
|
50
|
+
const all = raw ? parseChanges(raw, isWin) : [];
|
|
51
|
+
const finalTotal = total !== null ? total : all.length;
|
|
52
|
+
const changes = all.slice(0, maxChanges);
|
|
53
|
+
return { changes, total: finalTotal, truncated: finalTotal > changes.length, treeId };
|
|
54
|
+
}
|
|
55
|
+
function scanCutSeq(events, messageId) {
|
|
56
|
+
let anchor = -1;
|
|
57
|
+
for (let i = 0; i < events.length; i++) {
|
|
58
|
+
const e = events[i];
|
|
59
|
+
if (e && e.type === "user/message" && e.data && String(e.data.id) === String(messageId)) {
|
|
60
|
+
anchor = i;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (anchor < 0) return null;
|
|
65
|
+
for (let i = anchor - 1; i >= 0; i--) {
|
|
66
|
+
const e = events[i];
|
|
67
|
+
if (e && e.type === "turn/end" && typeof e.seq === "number") return e.seq;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
async function rescueRollback(deps, opts) {
|
|
72
|
+
const { root, store, safetyId, safetyOk, rollbackError } = opts;
|
|
73
|
+
const reason = String(rollbackError || "\u672A\u77E5\u539F\u56E0");
|
|
74
|
+
if (!safetyOk) {
|
|
75
|
+
deps.recordError("recall rollback failed, no rescue snapshot: " + reason);
|
|
76
|
+
return { ok: false, code: E.RECALL_ROLLBACK_FAILED, message: "\u56DE\u9000\u5931\u8D25\uFF1A" + reason + "\uFF08\u65E0\u53EF\u7528\u5B89\u5168\u5FEB\u7167\uFF0C\u5DE5\u4F5C\u533A\u53EF\u80FD\u5904\u4E8E\u534A\u56DE\u9000\u72B6\u6001\uFF09" };
|
|
77
|
+
}
|
|
78
|
+
const tag = "snap-" + safetyId;
|
|
79
|
+
const manual = 'git --git-dir="' + store.git + '" --work-tree="' + root + '" reset --hard ' + tag;
|
|
80
|
+
try {
|
|
81
|
+
const out = await deps.runShell(deps.scripts.rescueScript(root, store, deps.gitExe, tag), { timeoutMs: 6e5, stdoutMaxBytes: 65536 });
|
|
82
|
+
if (String(out || "").indexOf("RESCUE_OK") < 0) throw new Error("rescue \u811A\u672C\u672A\u8F93\u51FA RESCUE_OK \u54E8\u5175");
|
|
83
|
+
deps.recordError("recall rollback failed, rescued to safety tag: " + tag + " \u2014 " + reason);
|
|
84
|
+
return { ok: false, code: E.RECALL_ROLLBACK_FAILED, message: "\u56DE\u9000\u5931\u8D25\uFF1A" + reason + "\uFF1B\u5DF2\u81EA\u52A8\u6062\u590D\u5230\u56DE\u9000\u524D\u7684\u5B89\u5168\u5FEB\u7167\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u540E\u91CD\u8BD5" };
|
|
85
|
+
} catch (rescueError) {
|
|
86
|
+
const re = rescueError;
|
|
87
|
+
const rescueReason = String(re && re.message ? re.message : rescueError);
|
|
88
|
+
deps.recordError("recall rollback failed and rescue failed: " + tag + " \u2014 " + reason + " | rescue: " + rescueReason);
|
|
89
|
+
return { ok: false, code: E.RECALL_ROLLBACK_FAILED, message: "\u56DE\u9000\u5931\u8D25\uFF1A" + reason + "\uFF1B\u81EA\u52A8\u6062\u590D\u4E5F\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C\uFF1A" + manual };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function isSafetySnapshotId(id) {
|
|
93
|
+
return typeof id === "string" && id.indexOf("pre-rollback-") === 0;
|
|
94
|
+
}
|
|
95
|
+
function parseTagsWithTime(text) {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
98
|
+
const t = line.trim();
|
|
99
|
+
if (!t) continue;
|
|
100
|
+
const sp = t.lastIndexOf(" ");
|
|
101
|
+
const name = sp > 0 ? t.slice(0, sp) : t;
|
|
102
|
+
const ts = sp > 0 ? parseInt(t.slice(sp + 1), 10) : NaN;
|
|
103
|
+
out.push({ name, time: Number.isFinite(ts) && ts > 0 ? ts * 1e3 : null });
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function createSnapshots(ctx, rt, config) {
|
|
108
|
+
const sessions = ctx.sessions;
|
|
109
|
+
const state = rt.state;
|
|
110
|
+
const S = rt.scripts;
|
|
111
|
+
const BASE = () => config.baseExcludes;
|
|
112
|
+
const FUSE_AFTER = 3;
|
|
113
|
+
const FUSE_BACKOFF_BASE_MS = 5 * 60 * 1e3;
|
|
114
|
+
const FUSE_BACKOFF_CAP_MS = 60 * 60 * 1e3;
|
|
115
|
+
const snapFailures = /* @__PURE__ */ new Map();
|
|
116
|
+
async function saveIndex(root, sessionId) {
|
|
117
|
+
const store = state.stores.get(root);
|
|
118
|
+
if (!store) return;
|
|
119
|
+
const entries = Array.from(state.snapshots.entries()).filter(([, s]) => s.root === root).map(([id, s]) => {
|
|
120
|
+
const rec = { id, time: s.time, root: s.root, sessionId: s.sessionId };
|
|
121
|
+
const fb = state.snapFeedback.get(id);
|
|
122
|
+
if (fb && (fb.failed || Array.isArray(fb.skipped) && fb.skipped.length)) rec.feedback = fb;
|
|
123
|
+
return rec;
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
await rt.writeTextViaShell(store.dir + (rt.isWin ? "\\" : "/") + "index.json", JSON.stringify(entries));
|
|
127
|
+
} catch (error) {
|
|
128
|
+
rt.recordError("recall saveIndex failed: " + String(error));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function loadIndex(root, sessionId) {
|
|
132
|
+
if (state.indexLoaded.has(root)) return;
|
|
133
|
+
const store = state.stores.get(root);
|
|
134
|
+
if (!store) return;
|
|
135
|
+
let raw = "";
|
|
136
|
+
let truncated = false;
|
|
137
|
+
try {
|
|
138
|
+
const meta = await rt.runShellMeta(S.indexReadCmd(store.dir), { stdoutMaxBytes: 4194304 });
|
|
139
|
+
raw = S.stripBom(meta.text).trim();
|
|
140
|
+
truncated = Boolean(meta.truncated);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (truncated) {
|
|
145
|
+
rt.recordError("recall index read truncated: " + root + " \u7684 index.json \u8D85\u8FC7\u8BFB\u53D6\u4E0A\u9650\uFF0C\u6309\u7A7A\u7D22\u5F15\u7EE7\u7EED\uFF08\u539F\u6587\u4EF6\u672A\u6539\u52A8\uFF0C\u4E0B\u6B21\u5199\u7D22\u5F15\u81EA\u7136\u8986\u76D6\uFF09");
|
|
146
|
+
state.indexLoaded.add(root);
|
|
147
|
+
state.indexTruncated.add(root);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!raw) {
|
|
151
|
+
state.indexLoaded.add(root);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let entries = null;
|
|
155
|
+
try {
|
|
156
|
+
entries = JSON.parse(raw);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (await quarantineCorruptIndex(store)) state.indexLoaded.add(root);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!Array.isArray(entries)) {
|
|
162
|
+
if (await quarantineCorruptIndex(store)) state.indexLoaded.add(root);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let invalid = 0;
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || !entry.id) {
|
|
168
|
+
invalid++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
state.snapshots.set(entry.id, {
|
|
172
|
+
root,
|
|
173
|
+
time: typeof entry.time === "number" ? entry.time : Date.now(),
|
|
174
|
+
sessionId: entry.sessionId || sessionId || ""
|
|
175
|
+
});
|
|
176
|
+
const fb = entry.feedback;
|
|
177
|
+
if (fb && typeof fb === "object") {
|
|
178
|
+
const rec = {};
|
|
179
|
+
if (fb.failed) {
|
|
180
|
+
rec.failed = true;
|
|
181
|
+
if (typeof fb.error === "string") rec.error = fb.error;
|
|
182
|
+
if (typeof fb.kind === "string") rec.kind = fb.kind;
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(fb.skipped)) rec.skipped = fb.skipped.filter((p) => typeof p === "string");
|
|
185
|
+
if (rec.failed || Array.isArray(rec.skipped) && rec.skipped.length) setFeedback(entry.id, rec);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (invalid > 0) rt.recordError("recall index has " + invalid + " invalid entries for: " + root);
|
|
189
|
+
state.indexLoaded.add(root);
|
|
190
|
+
state.indexHealthy.add(root);
|
|
191
|
+
}
|
|
192
|
+
const quarantineThrottle = /* @__PURE__ */ new Map();
|
|
193
|
+
function quarantineErrorThrottled(store, text) {
|
|
194
|
+
const last = quarantineThrottle.get(store.dir) || 0;
|
|
195
|
+
if (Date.now() - last < 5 * 60 * 1e3) return;
|
|
196
|
+
quarantineThrottle.set(store.dir, Date.now());
|
|
197
|
+
rt.recordError(text);
|
|
198
|
+
}
|
|
199
|
+
async function quarantineCorruptIndex(store) {
|
|
200
|
+
const sep = rt.isWin ? "\\" : "/";
|
|
201
|
+
const corrupt = store.dir + sep + "index.json.corrupt-" + Date.now();
|
|
202
|
+
try {
|
|
203
|
+
await rt.runShell(S.renameFileCmd(store.dir + sep + "index.json", corrupt), { stdoutMaxBytes: 4096 });
|
|
204
|
+
rt.recordError("recall index corrupt: \u5DF2\u6309\u7A7A\u7D22\u5F15\u7EE7\u7EED\uFF0C\u574F\u6587\u4EF6\u4FDD\u7559\u4E3A " + corrupt);
|
|
205
|
+
return true;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
quarantineErrorThrottled(store, "recall index quarantine failed: " + String(error));
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function loadLineage(root) {
|
|
212
|
+
const store = state.stores.get(root);
|
|
213
|
+
if (!store) return [];
|
|
214
|
+
try {
|
|
215
|
+
const raw = S.stripBom(await rt.runShell(S.lineageReadCmd(store.dir), { stdoutMaxBytes: 1048576 })).trim();
|
|
216
|
+
if (!raw) return [];
|
|
217
|
+
const arr = JSON.parse(raw);
|
|
218
|
+
return Array.isArray(arr) ? arr.filter((e) => e && typeof e.childId === "string" && typeof e.parentId === "string") : [];
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function recordLineage(root, childId, parentId) {
|
|
224
|
+
const store = state.stores.get(root);
|
|
225
|
+
if (!store) return;
|
|
226
|
+
const sep = rt.isWin ? "\\" : "/";
|
|
227
|
+
const existing = await loadLineage(root);
|
|
228
|
+
if (!existing.some((e) => e.childId === childId && e.parentId === parentId)) {
|
|
229
|
+
existing.push({ childId, parentId, time: Date.now() });
|
|
230
|
+
try {
|
|
231
|
+
await rt.writeTextViaShell(store.dir + sep + "lineage.json", JSON.stringify(existing));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
rt.recordError("recall recordLineage failed: " + String(error));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function readExclude(store) {
|
|
238
|
+
return S.stripBom(await rt.runShell(S.excludeReadCmd(store.excludeFile), { stdoutMaxBytes: 1048576 }));
|
|
239
|
+
}
|
|
240
|
+
async function writeExclude(store, text) {
|
|
241
|
+
const body = String(text == null ? "" : text);
|
|
242
|
+
const sep = rt.isWin ? "\\" : "/";
|
|
243
|
+
const parent = store.excludeFile.slice(0, store.excludeFile.lastIndexOf(sep));
|
|
244
|
+
await rt.runShell(S.mkdirScript(parent), { stdoutMaxBytes: 4096 });
|
|
245
|
+
await rt.writeTextViaShell(store.excludeFile, body);
|
|
246
|
+
}
|
|
247
|
+
async function rebuildOrphans(root, sessionId) {
|
|
248
|
+
if (state.indexTruncated.has(root)) return;
|
|
249
|
+
if (state.indexHealthy.has(root)) {
|
|
250
|
+
let count = 0;
|
|
251
|
+
for (const s of state.snapshots.values()) {
|
|
252
|
+
if (s && s.root === root) count++;
|
|
253
|
+
}
|
|
254
|
+
if (count > 0) return;
|
|
255
|
+
}
|
|
256
|
+
const store = state.stores.get(root);
|
|
257
|
+
const gitExe = await rt.resolveGit();
|
|
258
|
+
if (!store || !gitExe) return;
|
|
259
|
+
try {
|
|
260
|
+
const listing = S.stripBom(await rt.runShell(S.listTagsWithTimeScript(store, gitExe), { stdoutMaxBytes: 4194304 })).trim();
|
|
261
|
+
if (!listing) return;
|
|
262
|
+
for (const { name, time } of parseTagsWithTime(listing)) {
|
|
263
|
+
const id = name.replace(/^snap-/, "");
|
|
264
|
+
if (!id || isSafetySnapshotId(id) || state.snapshots.has(id)) continue;
|
|
265
|
+
state.snapshots.set(id, { root, time: time || 0, sessionId });
|
|
266
|
+
}
|
|
267
|
+
await saveIndex(root, sessionId);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
rt.recordError("recall rebuildOrphans failed: " + String(error));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function captureSnapshot(sessionId, messageId, time) {
|
|
273
|
+
const root = await rt.resolveRoot(sessionId);
|
|
274
|
+
if (!root) return;
|
|
275
|
+
const fused = snapFailures.get(root);
|
|
276
|
+
if (fused && Date.now() < fused.skipUntil) return;
|
|
277
|
+
let store = await rt.resolveStore(root);
|
|
278
|
+
store = await rt.tryUpgradeToHome(root);
|
|
279
|
+
const g = await rt.ensureGit(root, store);
|
|
280
|
+
if (!g.ok) {
|
|
281
|
+
setFeedback(messageId, { failed: true, ...buildFeedbackError(g.error || "\u672A\u77E5\u539F\u56E0") });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await loadIndex(root, sessionId);
|
|
285
|
+
try {
|
|
286
|
+
const out = await rt.runShell(S.snapshotScript(root, store, state.gitExe || "", messageId, BASE()), { timeoutMs: 6e5, stdoutMaxBytes: 65536 });
|
|
287
|
+
snapFailures.delete(root);
|
|
288
|
+
state.snapshots.set(String(messageId), { root, time: time || Date.now(), sessionId });
|
|
289
|
+
await saveIndex(root, sessionId);
|
|
290
|
+
setFeedback(messageId, { skipped: parseSkipped(out) });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
rt.recordError("recall snapshot failed: " + String(error));
|
|
293
|
+
setFeedback(messageId, { failed: true, ...buildFeedbackError(String(error)) });
|
|
294
|
+
await handleSnapshotFailure(root, store);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function setFeedback(messageId, rec) {
|
|
298
|
+
const id = String(messageId);
|
|
299
|
+
const keep = rec && (rec.failed || Array.isArray(rec.skipped) && rec.skipped.length);
|
|
300
|
+
if (keep) state.snapFeedback.set(id, rec);
|
|
301
|
+
else state.snapFeedback.delete(id);
|
|
302
|
+
if (state.snapFeedback.size > 200) state.snapFeedback.delete(state.snapFeedback.keys().next().value);
|
|
303
|
+
}
|
|
304
|
+
async function feedbackFor(sessionId, messageId) {
|
|
305
|
+
const rec = state.snapFeedback.get(String(messageId || ""));
|
|
306
|
+
if (rec) return rec;
|
|
307
|
+
if (!sessionId) return {};
|
|
308
|
+
const root = await rt.resolveRoot(sessionId);
|
|
309
|
+
if (root) {
|
|
310
|
+
const f = snapFailures.get(root);
|
|
311
|
+
if (f && Date.now() < f.skipUntil) {
|
|
312
|
+
return { failed: true, error: "\u5FEB\u7167\u8FDE\u7EED\u5931\u8D25\u5DF2\u6682\u505C\uFF08\u7194\u65AD\uFF09\uFF0C\u7EA6 " + Math.ceil((f.skipUntil - Date.now()) / 6e4) + " \u5206\u949F\u540E\u81EA\u52A8\u91CD\u8BD5\uFF0C\u8BE6\u60C5\u89C1\u8BBE\u7F6E \xB7 \u63D2\u4EF6\u914D\u7F6E \xB7 \u6700\u8FD1\u9519\u8BEF" };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
async function handleSnapshotFailure(root, store) {
|
|
318
|
+
if (store && state.gitExe) {
|
|
319
|
+
try {
|
|
320
|
+
await rt.runShell(S.pruneScript(store, state.gitExe), { timeoutMs: 6e5, stdoutMaxBytes: 4096 });
|
|
321
|
+
} catch (error) {
|
|
322
|
+
rt.recordError("recall prune after snapshot failure failed: " + String(error));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const f = snapFailures.get(root) || { count: 0, skipUntil: 0 };
|
|
326
|
+
f.count++;
|
|
327
|
+
if (f.count >= FUSE_AFTER) {
|
|
328
|
+
const backoff = Math.min(FUSE_BACKOFF_BASE_MS * 2 ** (f.count - FUSE_AFTER), FUSE_BACKOFF_CAP_MS);
|
|
329
|
+
const wasFused = Date.now() < f.skipUntil;
|
|
330
|
+
f.skipUntil = Date.now() + backoff;
|
|
331
|
+
if (!wasFused) rt.recordError("recall snapshot fused after " + f.count + " consecutive failures, backoff " + Math.round(backoff / 6e4) + "min for: " + root);
|
|
332
|
+
}
|
|
333
|
+
snapFailures.set(root, f);
|
|
334
|
+
}
|
|
335
|
+
const MAX_CHANGES = 500;
|
|
336
|
+
async function diffFor(messageId) {
|
|
337
|
+
const snap = state.snapshots.get(String(messageId));
|
|
338
|
+
if (!snap) return null;
|
|
339
|
+
const store = state.stores.get(snap.root);
|
|
340
|
+
if (!store) return null;
|
|
341
|
+
const text = S.stripBom(await rt.runShell(S.diffScript(snap.root, store, state.gitExe || "", "snap-" + messageId, BASE(), MAX_CHANGES), { timeoutMs: 6e5, stdoutMaxBytes: 8388608 }));
|
|
342
|
+
const trimmed = text.trim();
|
|
343
|
+
if (!trimmed) return { changes: [], total: 0, truncated: false, treeId: null };
|
|
344
|
+
return parseDiffOutput(trimmed, rt.isWin, MAX_CHANGES);
|
|
345
|
+
}
|
|
346
|
+
async function rollbackFor(messageId) {
|
|
347
|
+
const snap = state.snapshots.get(String(messageId));
|
|
348
|
+
if (!snap) return { ok: false, error: "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167" };
|
|
349
|
+
const store = state.stores.get(snap.root);
|
|
350
|
+
if (!store) return { ok: false, error: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528" };
|
|
351
|
+
try {
|
|
352
|
+
const text = S.stripBom(await rt.runShell(S.rollbackScript(snap.root, store, state.gitExe || "", "snap-" + messageId, BASE()), { timeoutMs: 6e5, stdoutMaxBytes: 65536 }));
|
|
353
|
+
const m = text.trim().match(/^ROLLBACK_OK\s+(\d+)\s+(\d+)/);
|
|
354
|
+
if (!m) {
|
|
355
|
+
return { ok: false, partial: true, error: "\u56DE\u9000\u811A\u672C\u672A\u6B63\u5E38\u5B8C\u6210\uFF08\u5DE5\u4F5C\u533A\u53EF\u80FD\u5904\u4E8E\u534A\u56DE\u9000\u72B6\u6001\uFF09\uFF1A" + text.slice(0, 300) };
|
|
356
|
+
}
|
|
357
|
+
const deleted = parseInt(m[1], 10);
|
|
358
|
+
const restored = parseInt(m[2], 10);
|
|
359
|
+
return { ok: true, count: (Number.isNaN(deleted) ? 0 : deleted) + (Number.isNaN(restored) ? 0 : restored) };
|
|
360
|
+
} catch (error) {
|
|
361
|
+
const re = error;
|
|
362
|
+
const msg = String(re && re.message ? re.message : error);
|
|
363
|
+
return { ok: false, partial: true, error: msg };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function resolveCutSeq(sessionId, messageId) {
|
|
367
|
+
if (!sessionId || !messageId) return null;
|
|
368
|
+
const cacheKey = String(sessionId) + "\0" + String(messageId);
|
|
369
|
+
if (state.cutSeqCache.has(cacheKey)) return state.cutSeqCache.get(cacheKey) || null;
|
|
370
|
+
let result = null;
|
|
371
|
+
const live = sessions.get(sessionId);
|
|
372
|
+
if (live && Array.isArray(live.events)) {
|
|
373
|
+
result = scanCutSeq(live.events, messageId);
|
|
374
|
+
} else {
|
|
375
|
+
const query = ctx.get("sessionQuery");
|
|
376
|
+
if (query) {
|
|
377
|
+
try {
|
|
378
|
+
const log = await query.readSession(sessionId);
|
|
379
|
+
result = scanCutSeq(Array.isArray(log && log.events) ? log.events : [], messageId);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
result = null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
state.cutSeqCache.set(cacheKey, result);
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
return { saveIndex, loadIndex, readExclude, writeExclude, rebuildOrphans, captureSnapshot, diffFor, rollbackFor, resolveCutSeq, feedbackFor, loadLineage, recordLineage };
|
|
389
|
+
}
|
|
390
|
+
export {
|
|
391
|
+
createSnapshots,
|
|
392
|
+
isSafetySnapshotId,
|
|
393
|
+
parseChanges,
|
|
394
|
+
parseDiffOutput,
|
|
395
|
+
parseSkipped,
|
|
396
|
+
parseTagsWithTime,
|
|
397
|
+
parseTreeId,
|
|
398
|
+
rescueRollback,
|
|
399
|
+
scanCutSeq
|
|
400
|
+
};
|