dsh-recall-plugin 2.3.0 → 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 +45 -0
- package/README.en.md +4 -4
- package/README.md +14 -18
- package/lib/client.js +641 -484
- 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 +8 -3
package/lib/maintenance.js
CHANGED
|
@@ -1,275 +1,204 @@
|
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
async function
|
|
143
|
-
const
|
|
144
|
-
if (!
|
|
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
|
-
return
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const now = Date.now()
|
|
206
|
-
const last = state.gcLastAt.get(store.git) || 0
|
|
207
|
-
const count = (state.gcCount.get(store.git) || 0) + 1
|
|
208
|
-
state.gcCount.set(store.git, count)
|
|
209
|
-
if (!force && count < config.gcSnaps && now - last < config.gcHours * 3600000) return false
|
|
210
|
-
state.gcCount.set(store.git, 0)
|
|
211
|
-
try {
|
|
212
|
-
await sweepDeletedSessions()
|
|
213
|
-
// P1-3:总量上限清理(sweep 之后、gc 之前)——与快照在同一条串行
|
|
214
|
-
// 队列里,删除 tag 与 gc 互斥,无 git 锁竞态;best-effort,
|
|
215
|
-
// 自身失败不进 catch 的主错误路径(enforceLimits 内部已兜)。
|
|
216
|
-
await enforceLimits()
|
|
217
|
-
// S2-3:按时间保留清理(与条数上限维度各自独立,同条串行队列)
|
|
218
|
-
await enforceRetention()
|
|
219
|
-
await rt.runShell(S.gcScript(store, state.gitExe), { timeoutMs: 600000, stdoutMaxBytes: 4096 })
|
|
220
|
-
} catch (error) {
|
|
221
|
-
rt.recordError('recall maintenance failed: ' + String(error))
|
|
222
|
-
}
|
|
223
|
-
state.gcLastAt.set(store.git, Date.now())
|
|
224
|
-
return true
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// 全局 gc(设置卡片没有会话上下文):清理扫描一次 + 逐 store gc。
|
|
228
|
-
// store 全集取内存缓存(启动预热与历次操作会填齐已知工作区);逐个
|
|
229
|
-
// best-effort,单个失败记错误继续。调用方(manage 端点)把它排进同一条
|
|
230
|
-
// 串行队列,与快照天然互斥,无 git 锁竞态。
|
|
231
|
-
async function runGcAll() {
|
|
232
|
-
const stores = Array.from(new Set(Array.from(state.stores.values()).filter(Boolean)))
|
|
233
|
-
if (!stores.length || !state.gitExe) return false
|
|
234
|
-
try {
|
|
235
|
-
await sweepDeletedSessions()
|
|
236
|
-
} catch (error) {
|
|
237
|
-
rt.recordError('recall sweep failed: ' + String(error))
|
|
238
|
-
}
|
|
239
|
-
try {
|
|
240
|
-
// P1-3:全局清理一次(runGcAll 无会话上下文,enforceLimits 自身按
|
|
241
|
-
// root 遍历内存快照,天然覆盖全部已知工作区)
|
|
242
|
-
await enforceLimits()
|
|
243
|
-
} catch (error) {
|
|
244
|
-
rt.recordError('recall enforceLimits failed: ' + String(error))
|
|
245
|
-
}
|
|
246
|
-
try {
|
|
247
|
-
// S2-3:按时间保留全局清理一次
|
|
248
|
-
await enforceRetention()
|
|
249
|
-
} catch (error) {
|
|
250
|
-
rt.recordError('recall enforceRetention failed: ' + String(error))
|
|
251
|
-
}
|
|
252
|
-
let done = 0
|
|
253
|
-
for (const store of stores) {
|
|
254
|
-
try {
|
|
255
|
-
await rt.runShell(S.gcScript(store, state.gitExe), { timeoutMs: 600000, stdoutMaxBytes: 4096 })
|
|
256
|
-
done++
|
|
257
|
-
} catch (error) {
|
|
258
|
-
rt.recordError('recall gc failed for ' + (store && store.git) + ': ' + String(error))
|
|
259
|
-
}
|
|
260
|
-
state.gcLastAt.set(store.git, Date.now())
|
|
261
|
-
state.gcCount.set(store.git, 0)
|
|
262
|
-
}
|
|
263
|
-
return true
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 每条消息快照后串行调用(见 index.js 事件接线)
|
|
267
|
-
async function maybeMaintain(sessionId) {
|
|
268
|
-
await runGc(sessionId, false)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// 模块收敛:runGc/runGcAll 之外的内部步骤不对外暴露面;enforceLimits /
|
|
272
|
-
// sweepDeletedSessions 保留导出供单测以工厂形态驱动(注入假 rt/ctx 钉
|
|
273
|
-
// 执行链路;PF-7 sweep 判定矩阵依赖导出)。
|
|
274
|
-
return { maybeMaintain, runGc, runGcAll, enforceLimits, enforceRetention, sweepDeletedSessions }
|
|
275
|
-
}
|
|
1
|
+
function selectOverLimitVictims(snapshots, limit) {
|
|
2
|
+
if (!limit || limit <= 0) return /* @__PURE__ */ new Map();
|
|
3
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
4
|
+
for (const [id, s] of snapshots.entries()) {
|
|
5
|
+
if (!s || !s.root) continue;
|
|
6
|
+
if (!byRoot.has(s.root)) byRoot.set(s.root, []);
|
|
7
|
+
byRoot.get(s.root).push({ id, time: s.time });
|
|
8
|
+
}
|
|
9
|
+
const victims = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const [root, list] of byRoot) {
|
|
11
|
+
if (list.length <= limit) continue;
|
|
12
|
+
const excess = list.length - limit;
|
|
13
|
+
list.sort((a, b) => (a.time || 0) - (b.time || 0));
|
|
14
|
+
victims.set(root, list.slice(0, excess));
|
|
15
|
+
}
|
|
16
|
+
return victims;
|
|
17
|
+
}
|
|
18
|
+
function selectExpiredVictims(snapshots, retentionDays, now) {
|
|
19
|
+
if (!retentionDays || retentionDays <= 0) return /* @__PURE__ */ new Map();
|
|
20
|
+
const cutoff = (typeof now === "number" ? now : Date.now()) - retentionDays * 864e5;
|
|
21
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const [id, s] of snapshots.entries()) {
|
|
23
|
+
if (!s || !s.root) continue;
|
|
24
|
+
if (s.time > 0 && s.time >= cutoff) continue;
|
|
25
|
+
if (!byRoot.has(s.root)) byRoot.set(s.root, []);
|
|
26
|
+
byRoot.get(s.root).push({ id, time: s.time });
|
|
27
|
+
}
|
|
28
|
+
return byRoot;
|
|
29
|
+
}
|
|
30
|
+
function createMaintenance(ctx, rt, snaps, config) {
|
|
31
|
+
const sessions = ctx.sessions;
|
|
32
|
+
const state = rt.state;
|
|
33
|
+
const S = rt.scripts;
|
|
34
|
+
async function purgeSession(sessionId) {
|
|
35
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const [id, s] of state.snapshots.entries()) {
|
|
37
|
+
if (!s || s.sessionId !== sessionId) continue;
|
|
38
|
+
if (!byRoot.has(s.root)) byRoot.set(s.root, []);
|
|
39
|
+
byRoot.get(s.root).push(id);
|
|
40
|
+
}
|
|
41
|
+
let purged = 0;
|
|
42
|
+
for (const [root, ids] of byRoot) {
|
|
43
|
+
let store = state.stores.get(root) || null;
|
|
44
|
+
if (!store) {
|
|
45
|
+
try {
|
|
46
|
+
store = await rt.resolveStore(root);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
store = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!store || !state.gitExe) continue;
|
|
52
|
+
try {
|
|
53
|
+
for (let i = 0; i < ids.length; i += 100) {
|
|
54
|
+
await rt.runShell(S.purgeTagsScript(store, state.gitExe, ids.slice(i, i + 100).map((id) => "snap-" + id)), { timeoutMs: 12e4, stdoutMaxBytes: 4096 });
|
|
55
|
+
}
|
|
56
|
+
for (const id of ids) state.snapshots.delete(id);
|
|
57
|
+
await snaps.saveIndex(root, sessionId);
|
|
58
|
+
purged += ids.length;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
rt.recordError("recall purge session failed: " + String(error));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (purged > 0) console.error("recall purged snapshots of deleted session:", sessionId, purged);
|
|
64
|
+
return purged;
|
|
65
|
+
}
|
|
66
|
+
async function sweepDeletedSessions() {
|
|
67
|
+
const ids = /* @__PURE__ */ new Set();
|
|
68
|
+
for (const s of state.snapshots.values()) {
|
|
69
|
+
if (s && s.sessionId) ids.add(s.sessionId);
|
|
70
|
+
}
|
|
71
|
+
if (!ids.size) return;
|
|
72
|
+
const query = ctx.get("sessionQuery");
|
|
73
|
+
if (!query || typeof query.listSessions !== "function") return;
|
|
74
|
+
let diskIds;
|
|
75
|
+
try {
|
|
76
|
+
diskIds = new Set((await query.listSessions() || []).map((r) => r && r.header && r.header.id).filter((v) => Boolean(v)));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const id of ids) {
|
|
81
|
+
if (sessions.get(id)) continue;
|
|
82
|
+
if (diskIds.has(id)) continue;
|
|
83
|
+
await purgeSession(id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function enforceLimits() {
|
|
87
|
+
const victimsMap = selectOverLimitVictims(state.snapshots, config.maxSnapshotsPerWorkspace);
|
|
88
|
+
if (!victimsMap.size) return 0;
|
|
89
|
+
let dropped = 0;
|
|
90
|
+
for (const [root, victims] of victimsMap) {
|
|
91
|
+
let store = state.stores.get(root) || null;
|
|
92
|
+
if (!store) {
|
|
93
|
+
try {
|
|
94
|
+
store = await rt.resolveStore(root);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
store = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!store || !state.gitExe) continue;
|
|
100
|
+
try {
|
|
101
|
+
for (let i = 0; i < victims.length; i += 100) {
|
|
102
|
+
await rt.runShell(S.purgeTagsScript(store, state.gitExe, victims.slice(i, i + 100).map((v) => "snap-" + v.id)), { timeoutMs: 12e4, stdoutMaxBytes: 4096 });
|
|
103
|
+
}
|
|
104
|
+
for (const v of victims) state.snapshots.delete(v.id);
|
|
105
|
+
await snaps.saveIndex(root, null);
|
|
106
|
+
dropped += victims.length;
|
|
107
|
+
console.error("recall enforceLimits dropped " + victims.length + " oldest snapshots for: " + root + " (max " + config.maxSnapshotsPerWorkspace + ")");
|
|
108
|
+
} catch (error) {
|
|
109
|
+
rt.recordError("recall enforceLimits failed for " + root + ": " + String(error));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return dropped;
|
|
113
|
+
}
|
|
114
|
+
async function enforceRetention() {
|
|
115
|
+
const victimsMap = selectExpiredVictims(state.snapshots, config.retentionDays, Date.now());
|
|
116
|
+
if (!victimsMap.size) return 0;
|
|
117
|
+
let dropped = 0;
|
|
118
|
+
for (const [root, victims] of victimsMap) {
|
|
119
|
+
let store = state.stores.get(root) || null;
|
|
120
|
+
if (!store) {
|
|
121
|
+
try {
|
|
122
|
+
store = await rt.resolveStore(root);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
store = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!store || !state.gitExe) continue;
|
|
128
|
+
try {
|
|
129
|
+
for (let i = 0; i < victims.length; i += 100) {
|
|
130
|
+
await rt.runShell(S.purgeTagsScript(store, state.gitExe, victims.slice(i, i + 100).map((v) => "snap-" + v.id)), { timeoutMs: 12e4, stdoutMaxBytes: 4096 });
|
|
131
|
+
}
|
|
132
|
+
for (const v of victims) state.snapshots.delete(v.id);
|
|
133
|
+
await snaps.saveIndex(root, null);
|
|
134
|
+
dropped += victims.length;
|
|
135
|
+
console.error("recall enforceRetention dropped " + victims.length + " expired snapshots for: " + root + " (retention " + config.retentionDays + "d)");
|
|
136
|
+
} catch (error) {
|
|
137
|
+
rt.recordError("recall enforceRetention failed for " + root + ": " + String(error));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return dropped;
|
|
141
|
+
}
|
|
142
|
+
async function runGc(sessionId, force) {
|
|
143
|
+
const root = await rt.resolveRoot(sessionId);
|
|
144
|
+
if (!root) return false;
|
|
145
|
+
const store = state.stores.get(root);
|
|
146
|
+
if (!store || !state.gitExe) return false;
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
const last = state.gcLastAt.get(store.git) || 0;
|
|
149
|
+
const count = (state.gcCount.get(store.git) || 0) + 1;
|
|
150
|
+
state.gcCount.set(store.git, count);
|
|
151
|
+
if (!force && count < config.gcSnaps && now - last < config.gcHours * 36e5) return false;
|
|
152
|
+
state.gcCount.set(store.git, 0);
|
|
153
|
+
try {
|
|
154
|
+
await sweepDeletedSessions();
|
|
155
|
+
await enforceLimits();
|
|
156
|
+
await enforceRetention();
|
|
157
|
+
await rt.runShell(S.gcScript(store, state.gitExe), { timeoutMs: 6e5, stdoutMaxBytes: 4096 });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
rt.recordError("recall maintenance failed: " + String(error));
|
|
160
|
+
}
|
|
161
|
+
state.gcLastAt.set(store.git, Date.now());
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
async function runGcAll() {
|
|
165
|
+
const stores = Array.from(new Set(Array.from(state.stores.values()).filter((s) => Boolean(s))));
|
|
166
|
+
if (!stores.length || !state.gitExe) return false;
|
|
167
|
+
try {
|
|
168
|
+
await sweepDeletedSessions();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
rt.recordError("recall sweep failed: " + String(error));
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
await enforceLimits();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
rt.recordError("recall enforceLimits failed: " + String(error));
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
await enforceRetention();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
rt.recordError("recall enforceRetention failed: " + String(error));
|
|
181
|
+
}
|
|
182
|
+
let done = 0;
|
|
183
|
+
for (const store of stores) {
|
|
184
|
+
try {
|
|
185
|
+
await rt.runShell(S.gcScript(store, state.gitExe), { timeoutMs: 6e5, stdoutMaxBytes: 4096 });
|
|
186
|
+
done++;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
rt.recordError("recall gc failed for " + (store && store.git) + ": " + String(error));
|
|
189
|
+
}
|
|
190
|
+
state.gcLastAt.set(store.git, Date.now());
|
|
191
|
+
state.gcCount.set(store.git, 0);
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
async function maybeMaintain(sessionId) {
|
|
196
|
+
await runGc(sessionId, false);
|
|
197
|
+
}
|
|
198
|
+
return { maybeMaintain, runGc, runGcAll, enforceLimits, enforceRetention, sweepDeletedSessions };
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
createMaintenance,
|
|
202
|
+
selectExpiredVictims,
|
|
203
|
+
selectOverLimitVictims
|
|
204
|
+
};
|