@wangxiaocuo/dsh-session-trash 0.6.3 → 0.6.5
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 +36 -0
- package/README.en.md +25 -8
- package/README.md +25 -8
- package/client.js +117 -6
- package/index.js +2 -2
- package/lib/cleanup.js +23 -16
- package/lib/eligibility.js +5 -5
- package/lib/errors.js +9 -6
- package/lib/fsops.js +8 -6
- package/lib/http.js +45 -19
- package/lib/inventory.js +40 -16
- package/lib/members.js +6 -6
- package/lib/state.js +11 -9
- package/lib/store.js +45 -16
- package/lib/trash-service.js +103 -66
- package/package.json +10 -3
package/lib/trash-service.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
samePath,
|
|
15
15
|
} from './fsops.js'
|
|
16
16
|
import { mapLimit } from './concurrency.js'
|
|
17
|
-
import {
|
|
17
|
+
import { isEmptyObjectDir, memberPresence, rebuildIndex, reconcileIndex, scanObjects } from './inventory.js'
|
|
18
18
|
import { aggregateMetrics, memberList } from './members.js'
|
|
19
19
|
|
|
20
20
|
/** How many short retries confirm a moved or restored session is visible. */
|
|
@@ -45,7 +45,7 @@ function errorText(message) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* @param {
|
|
48
|
+
* @param {any} error arbitrary failure.
|
|
49
49
|
* @returns {string} a user-facing error message.
|
|
50
50
|
*/
|
|
51
51
|
function messageOf(error) {
|
|
@@ -53,9 +53,9 @@ function messageOf(error) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* @param {readonly
|
|
57
|
-
* @param {
|
|
58
|
-
* @returns {
|
|
56
|
+
* @param {readonly any[]} entries current index entries.
|
|
57
|
+
* @param {any} next replacement entry.
|
|
58
|
+
* @returns {any[]} a new list with the entry replaced or appended.
|
|
59
59
|
*/
|
|
60
60
|
function upsertEntry(entries, next) {
|
|
61
61
|
const copy = [...entries]
|
|
@@ -66,9 +66,9 @@ function upsertEntry(entries, next) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* @param {readonly
|
|
69
|
+
* @param {readonly any[]} entries current index entries.
|
|
70
70
|
* @param {string} sessionId session to drop.
|
|
71
|
-
* @returns {
|
|
71
|
+
* @returns {any[]} a new list without that session.
|
|
72
72
|
*/
|
|
73
73
|
function dropEntry(entries, sessionId) {
|
|
74
74
|
return entries.filter((entry) => entry.sessionId !== sessionId)
|
|
@@ -81,23 +81,23 @@ function dropEntry(entries, sessionId) {
|
|
|
81
81
|
* @param {{
|
|
82
82
|
* ctx: import('@deepseek-ai/cordis').Context,
|
|
83
83
|
* store: import('./store.js').TrashStore,
|
|
84
|
-
* eligibility: { evaluate: (sessionId: string) => Promise<
|
|
84
|
+
* eligibility: { evaluate: (sessionId: string) => Promise<any>, invalidate: (sessionId?: string) => void },
|
|
85
85
|
* config: { retentionDays: number, sweepIntervalHours: number, enableAutoCleanup: boolean },
|
|
86
86
|
* logger: { info: Function, warn: Function, error: Function, debug: Function },
|
|
87
87
|
* paths: { root: string, display: string },
|
|
88
88
|
* }} deps host services and resolved paths.
|
|
89
|
-
* @returns {
|
|
89
|
+
* @returns {any} the operation surface.
|
|
90
90
|
*/
|
|
91
91
|
export function createTrashService({ ctx, store, eligibility, config, logger, paths }) {
|
|
92
92
|
/**
|
|
93
|
-
* @returns {
|
|
93
|
+
* @returns {any | undefined} the optional title service.
|
|
94
94
|
*/
|
|
95
95
|
function sessionQuery() {
|
|
96
96
|
return ctx.get('sessionQuery')
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* @returns {
|
|
100
|
+
* @returns {any | undefined} the optional workspace registry.
|
|
101
101
|
*/
|
|
102
102
|
function workspaceRegistry() {
|
|
103
103
|
return ctx.get('workspaceRegistry')
|
|
@@ -105,7 +105,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* @param {string} sessionId session whose storage snapshot to read.
|
|
108
|
-
* @returns {Promise<
|
|
108
|
+
* @returns {Promise<any | undefined>} the persistence snapshot.
|
|
109
109
|
*/
|
|
110
110
|
async function stat(sessionId) {
|
|
111
111
|
return ctx.sessionPersistence.stat(sessionId)
|
|
@@ -113,7 +113,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* @param {string} sessionId session to check.
|
|
116
|
-
* @returns {
|
|
116
|
+
* @returns {boolean} whether the session is live in this process.
|
|
117
117
|
*/
|
|
118
118
|
function isLive(sessionId) {
|
|
119
119
|
const liveSession = typeof ctx.sessions?.get === 'function' ? ctx.sessions.get(sessionId) : undefined
|
|
@@ -122,7 +122,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* @param {
|
|
125
|
+
* @param {any} snapshot persistence snapshot.
|
|
126
126
|
* @returns {string} the session header's cwd, or an empty string when absent.
|
|
127
127
|
*/
|
|
128
128
|
function cwdOf(snapshot) {
|
|
@@ -134,7 +134,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
134
134
|
* query service.
|
|
135
135
|
*
|
|
136
136
|
* @param {string} sessionId session whose title to read.
|
|
137
|
-
* @param {
|
|
137
|
+
* @param {any} header persistence header.
|
|
138
138
|
* @returns {Promise<string>} the title, then cwd basename, then session id.
|
|
139
139
|
*/
|
|
140
140
|
async function resolveTitle(sessionId, header) {
|
|
@@ -158,7 +158,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
158
158
|
* Find the Workspace that currently accounts for a session.
|
|
159
159
|
*
|
|
160
160
|
* @param {string} sessionId session to locate.
|
|
161
|
-
* @returns {{ workspace:
|
|
161
|
+
* @returns {{ workspace: any, nextSessionId: string | null } | undefined} owning workspace and following sibling.
|
|
162
162
|
*/
|
|
163
163
|
function findOwningWorkspace(sessionId) {
|
|
164
164
|
const registry = workspaceRegistry()
|
|
@@ -177,7 +177,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
|
-
* @param {
|
|
180
|
+
* @param {any} target path to probe.
|
|
181
181
|
* @returns {Promise<string>} the entry kind, treating an absent path as missing.
|
|
182
182
|
*/
|
|
183
183
|
async function kindOf(target) {
|
|
@@ -185,15 +185,15 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
/**
|
|
188
|
-
* @param {
|
|
189
|
-
* @returns {
|
|
188
|
+
* @param {any} entry index entry.
|
|
189
|
+
* @returns {any[]} every owned directory, parent first.
|
|
190
190
|
*/
|
|
191
191
|
function membersOf(entry) {
|
|
192
192
|
return memberList(entry)
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* @param {
|
|
196
|
+
* @param {any} entry index entry.
|
|
197
197
|
* @param {number} index member position.
|
|
198
198
|
* @returns {string} this member's directory inside the trash object.
|
|
199
199
|
*/
|
|
@@ -202,7 +202,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
|
-
* @param {
|
|
205
|
+
* @param {any} entry trash index entry.
|
|
206
206
|
* @returns {Promise<void>} resolution once every member path stays inside `objects/`.
|
|
207
207
|
*/
|
|
208
208
|
async function assertSafeObject(entry) {
|
|
@@ -226,8 +226,8 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
226
226
|
* Probe every member directory in parallel: where the source copy and the
|
|
227
227
|
* trash copy currently live.
|
|
228
228
|
*
|
|
229
|
-
* @param {
|
|
230
|
-
* @returns {Promise<{ index: number, member:
|
|
229
|
+
* @param {any} entry index entry.
|
|
230
|
+
* @returns {Promise<{ index: number, member: any, source: boolean, trash: boolean }[]>} member positions.
|
|
231
231
|
*/
|
|
232
232
|
async function memberRows(entry) {
|
|
233
233
|
const members = membersOf(entry)
|
|
@@ -251,8 +251,8 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
251
251
|
* Probe only the original session directories of every member, in parallel.
|
|
252
252
|
* Used by recovery after the trash copies were already proven present.
|
|
253
253
|
*
|
|
254
|
-
* @param {
|
|
255
|
-
* @returns {Promise<{ index: number, member:
|
|
254
|
+
* @param {any} entry index entry.
|
|
255
|
+
* @returns {Promise<{ index: number, member: any, source: boolean }[]>} member source positions.
|
|
256
256
|
*/
|
|
257
257
|
async function sourceRows(entry) {
|
|
258
258
|
const members = membersOf(entry)
|
|
@@ -266,7 +266,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
/**
|
|
269
|
-
* @param {
|
|
269
|
+
* @param {any} entry index entry.
|
|
270
270
|
* @param {number} index member position.
|
|
271
271
|
* @returns {Promise<void>} resolution once the member directory moved into its object.
|
|
272
272
|
*/
|
|
@@ -276,7 +276,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
|
-
* @param {
|
|
279
|
+
* @param {any} entry index entry.
|
|
280
280
|
* @param {number} index member position.
|
|
281
281
|
* @returns {Promise<void>} resolution once the member directory moved back to its original location.
|
|
282
282
|
*/
|
|
@@ -286,7 +286,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
/**
|
|
289
|
-
* @param {
|
|
289
|
+
* @param {any} entry index entry.
|
|
290
290
|
* @param {readonly { index: number }[]} rows members to move into the object.
|
|
291
291
|
* @returns {Promise<{ index: number, error: unknown }[]>} per-member failures.
|
|
292
292
|
*/
|
|
@@ -303,7 +303,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
|
-
* @param {
|
|
306
|
+
* @param {any} entry index entry.
|
|
307
307
|
* @param {readonly { index: number }[]} rows members to move back to their original locations.
|
|
308
308
|
* @returns {Promise<{ index: number, error: unknown }[]>} per-member failures.
|
|
309
309
|
*/
|
|
@@ -323,7 +323,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
323
323
|
* Move every member directory into the trash object, children first and the
|
|
324
324
|
* visible parent last so the sidebar keeps the row until the move settles.
|
|
325
325
|
*
|
|
326
|
-
* @param {
|
|
326
|
+
* @param {any} entry index entry.
|
|
327
327
|
* @returns {Promise<void>} resolution once every member directory moved.
|
|
328
328
|
*/
|
|
329
329
|
async function moveMembersToTrash(entry) {
|
|
@@ -337,7 +337,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
/**
|
|
340
|
-
* @param {
|
|
340
|
+
* @param {any} entry index entry.
|
|
341
341
|
* @param {number} index member position.
|
|
342
342
|
* @returns {Promise<void>} resolution once the unusable trash copy is gone.
|
|
343
343
|
*/
|
|
@@ -351,7 +351,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
/**
|
|
354
|
-
* @param {
|
|
354
|
+
* @param {any} member planned member whose restored copy failed verification.
|
|
355
355
|
* @returns {Promise<void>} resolution once the unusable restored copy is gone.
|
|
356
356
|
*/
|
|
357
357
|
async function removeRestoredMember(member) {
|
|
@@ -363,7 +363,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
/**
|
|
366
|
-
* @param {
|
|
366
|
+
* @param {any} entry index entry.
|
|
367
367
|
* @returns {Promise<{ ok: boolean, index?: number, message?: string }>} whether every member landed with its recorded metrics.
|
|
368
368
|
*/
|
|
369
369
|
async function verifyTrashedMembers(entry) {
|
|
@@ -386,7 +386,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
/**
|
|
389
|
-
* @param {
|
|
389
|
+
* @param {any} entry index entry.
|
|
390
390
|
* @returns {Promise<{ ok: boolean, index?: number, message?: string }>} whether every member returned with its recorded metrics.
|
|
391
391
|
*/
|
|
392
392
|
async function verifyRestoredMembers(entry) {
|
|
@@ -406,7 +406,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
/**
|
|
409
|
-
* @param {
|
|
409
|
+
* @param {any} entry index entry.
|
|
410
410
|
* @returns {Promise<{ hidden: boolean, sessionId?: string }>} whether session storage stopped reporting every member.
|
|
411
411
|
*/
|
|
412
412
|
async function waitForMembersHidden(entry) {
|
|
@@ -421,7 +421,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
/**
|
|
424
|
-
* @param {
|
|
424
|
+
* @param {any} entry index entry.
|
|
425
425
|
* @returns {Promise<{ visible: boolean, sessionId?: string }>} whether session storage reports every member again.
|
|
426
426
|
*/
|
|
427
427
|
async function waitForMembersVisible(entry) {
|
|
@@ -440,7 +440,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
440
440
|
|
|
441
441
|
/**
|
|
442
442
|
* @param {string} sessionId session to probe.
|
|
443
|
-
* @param {(snapshot:
|
|
443
|
+
* @param {(snapshot: any | undefined) => boolean} predicate expected visibility state.
|
|
444
444
|
* @returns {Promise<boolean>} whether the persistence backend agreed within the retry budget.
|
|
445
445
|
*/
|
|
446
446
|
async function waitForVisibility(sessionId, predicate) {
|
|
@@ -479,8 +479,8 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
479
479
|
*
|
|
480
480
|
* @param {string} sessionId restored session identity.
|
|
481
481
|
* @param {string} title restored display title.
|
|
482
|
-
* @param {
|
|
483
|
-
* @returns {
|
|
482
|
+
* @param {any | undefined} snapshot persistence snapshot.
|
|
483
|
+
* @returns {any} a `SessionSummary`.
|
|
484
484
|
*/
|
|
485
485
|
function buildSummary(sessionId, title, snapshot) {
|
|
486
486
|
const header = snapshot?.header ?? {}
|
|
@@ -501,9 +501,9 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
501
501
|
/**
|
|
502
502
|
* Record a failure summary on one entry and commit it, best effort.
|
|
503
503
|
*
|
|
504
|
-
* @param {
|
|
504
|
+
* @param {any} entry entry to annotate.
|
|
505
505
|
* @param {string} message failure summary.
|
|
506
|
-
* @returns {Promise<
|
|
506
|
+
* @returns {Promise<any>} the annotated entry.
|
|
507
507
|
*/
|
|
508
508
|
async function markFailure(entry, message) {
|
|
509
509
|
const next = { ...entry, lastError: errorText(message) }
|
|
@@ -521,7 +521,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
521
521
|
/**
|
|
522
522
|
* Restore the workspace accounting after a normal restore.
|
|
523
523
|
*
|
|
524
|
-
* @param {
|
|
524
|
+
* @param {any} entry restored index entry.
|
|
525
525
|
* @returns {Promise<{ reattached: boolean, warning?: string }>} reattachment outcome.
|
|
526
526
|
*/
|
|
527
527
|
async function reattachWorkspace(entry) {
|
|
@@ -584,7 +584,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
584
584
|
* Remove a session from every workspace that still accounts for it. Used by
|
|
585
585
|
* orphan restore so the restored log appears under “Ungrouped”.
|
|
586
586
|
*
|
|
587
|
-
* @param {
|
|
587
|
+
* @param {any} entry restored index entry.
|
|
588
588
|
* @returns {Promise<{ detached: boolean, warning?: string }>} detach outcome.
|
|
589
589
|
*/
|
|
590
590
|
async function detachWorkspace(entry) {
|
|
@@ -625,7 +625,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
625
625
|
*
|
|
626
626
|
* @param {string} from current path.
|
|
627
627
|
* @param {string} to previous path.
|
|
628
|
-
* @param {
|
|
628
|
+
* @param {any | null | undefined} expected metrics for a cross-device copy.
|
|
629
629
|
* @returns {Promise<boolean>} whether the directory now lives at `to`.
|
|
630
630
|
*/
|
|
631
631
|
async function moveBack(from, to, expected) {
|
|
@@ -669,7 +669,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
669
669
|
* object or its original location. Never writes the index: the caller owns
|
|
670
670
|
* the commit so startup recovery can accumulate one write.
|
|
671
671
|
*
|
|
672
|
-
* @param {
|
|
672
|
+
* @param {any} entry `trashing` index entry.
|
|
673
673
|
* @returns {Promise<{ kind: 'ready' } | { kind: 'rolledBack' } | { kind: 'missing', entry: object }>} the settled position.
|
|
674
674
|
*/
|
|
675
675
|
async function resolveTrashing(entry) {
|
|
@@ -725,7 +725,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
725
725
|
* location, or back into the trash object when the restore cannot complete.
|
|
726
726
|
* Never writes the index: the caller owns the commit.
|
|
727
727
|
*
|
|
728
|
-
* @param {
|
|
728
|
+
* @param {any} entry `restoring` index entry.
|
|
729
729
|
* @returns {Promise<{ kind: 'ready' } | { kind: 'trashed', entry: object } | { kind: 'missing', entry: object }>} the settled position.
|
|
730
730
|
*/
|
|
731
731
|
async function resolveRestoring(entry) {
|
|
@@ -752,6 +752,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
752
752
|
row.source = false
|
|
753
753
|
}
|
|
754
754
|
}
|
|
755
|
+
/** @returns {{ kind: 'trashed', entry: any }} */
|
|
755
756
|
const rolledBack = () => ({
|
|
756
757
|
kind: 'trashed',
|
|
757
758
|
entry: {
|
|
@@ -787,9 +788,9 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
787
788
|
}
|
|
788
789
|
|
|
789
790
|
/**
|
|
790
|
-
* @param {
|
|
791
|
+
* @param {any} entry settled `trashing` entry.
|
|
791
792
|
* @param {{ emitRemoval?: boolean }} [options] whether to announce the removal to clients.
|
|
792
|
-
* @returns {Promise<
|
|
793
|
+
* @returns {Promise<any>} the committed `trashed` entry.
|
|
793
794
|
*/
|
|
794
795
|
async function commitTrashed(entry, options = {}) {
|
|
795
796
|
const next = { ...entry, state: 'trashed', trashedAt: Date.now(), lastError: null }
|
|
@@ -811,9 +812,9 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
811
812
|
* Finish a settled restore: wait for visibility, restore workspace
|
|
812
813
|
* accounting, drop the entry, and optionally announce the session.
|
|
813
814
|
*
|
|
814
|
-
* @param {
|
|
815
|
+
* @param {any} entry settled `restoring` entry.
|
|
815
816
|
* @param {{ mode?: 'normal' | 'orphan', emitAdded?: boolean, reattach?: boolean }} [options] finishing policy.
|
|
816
|
-
* @returns {Promise<
|
|
817
|
+
* @returns {Promise<any>} the workspace attachment outcome.
|
|
817
818
|
*/
|
|
818
819
|
async function completeRestore(entry, options = {}) {
|
|
819
820
|
const mode = options.mode ?? 'normal'
|
|
@@ -847,7 +848,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
847
848
|
}
|
|
848
849
|
|
|
849
850
|
/**
|
|
850
|
-
* @param {
|
|
851
|
+
* @param {any} entry index entry.
|
|
851
852
|
* @param {number} now current timestamp.
|
|
852
853
|
* @returns {boolean} whether the retention window has elapsed.
|
|
853
854
|
*/
|
|
@@ -864,8 +865,8 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
864
865
|
* recovery; successes are dropped and failures annotated in one final write.
|
|
865
866
|
* The caller owns the trash lock.
|
|
866
867
|
*
|
|
867
|
-
* @param {readonly
|
|
868
|
-
* @returns {Promise<{ purged: number, failures: { entry:
|
|
868
|
+
* @param {readonly any[]} targets `trashed` entries to purge.
|
|
869
|
+
* @returns {Promise<{ purged: number, failures: { entry: any, error: unknown, phase: string }[] }>} per-entry outcome.
|
|
869
870
|
*/
|
|
870
871
|
async function purgeManyInsideLock(targets) {
|
|
871
872
|
const failures = []
|
|
@@ -954,7 +955,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
954
955
|
* Permanently remove one object directory and its index entry. The caller
|
|
955
956
|
* owns the trash lock.
|
|
956
957
|
*
|
|
957
|
-
* @param {
|
|
958
|
+
* @param {any} entry `trashed` index entry to purge.
|
|
958
959
|
* @returns {Promise<void>} resolution once the object and entry are gone.
|
|
959
960
|
*/
|
|
960
961
|
async function purgeInsideLock(entry) {
|
|
@@ -967,7 +968,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
967
968
|
* their plugin-owned remnants. No session file is deleted. The caller owns
|
|
968
969
|
* the trash lock.
|
|
969
970
|
*
|
|
970
|
-
* @param {readonly
|
|
971
|
+
* @param {readonly any[]} entries `missing` index entries to forget.
|
|
971
972
|
* @returns {Promise<number>} how many entries were removed from the index.
|
|
972
973
|
*/
|
|
973
974
|
async function forgetManyInsideLock(entries) {
|
|
@@ -993,7 +994,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
993
994
|
* Drop one `missing` entry from the index and remove its plugin-owned
|
|
994
995
|
* remnants. No session file is deleted. The caller owns the trash lock.
|
|
995
996
|
*
|
|
996
|
-
* @param {
|
|
997
|
+
* @param {any} entry `missing` index entry to forget.
|
|
997
998
|
* @returns {Promise<{ sessionId: string, title: string }>} the removed identity.
|
|
998
999
|
*/
|
|
999
1000
|
async function forgetInsideLock(entry) {
|
|
@@ -1006,19 +1007,21 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
1006
1007
|
* `origin: 'subagent'` descendants moves together with them.
|
|
1007
1008
|
*
|
|
1008
1009
|
* @param {string} sessionId session to trash.
|
|
1009
|
-
* @returns {Promise<
|
|
1010
|
+
* @returns {Promise<any>} the committed `trashed` entry.
|
|
1010
1011
|
*/
|
|
1011
1012
|
async function trash(sessionId) {
|
|
1012
1013
|
const startedAt = Date.now()
|
|
1014
|
+
// Lineage listing and live checks are slow; resolve them before taking the
|
|
1015
|
+
// cross-process lock and keep only the directory re-checks inside it.
|
|
1016
|
+
eligibility.invalidate(sessionId)
|
|
1017
|
+
const verdict = await eligibility.evaluate(sessionId)
|
|
1018
|
+
if (verdict.eligible !== true) {
|
|
1019
|
+
throw new TrashError(verdict.code, verdict.message, verdict.details)
|
|
1020
|
+
}
|
|
1013
1021
|
return store.withLock(async () => {
|
|
1014
1022
|
if (store.isProtected()) {
|
|
1015
1023
|
throw new TrashError('INDEX_CORRUPT', 'The trash index is unreadable, so protection mode refuses new entries.')
|
|
1016
1024
|
}
|
|
1017
|
-
eligibility.invalidate(sessionId)
|
|
1018
|
-
const verdict = await eligibility.evaluate(sessionId)
|
|
1019
|
-
if (verdict.eligible !== true) {
|
|
1020
|
-
throw new TrashError(verdict.code, verdict.message, verdict.details)
|
|
1021
|
-
}
|
|
1022
1025
|
const existing = store.findEntry(sessionId)
|
|
1023
1026
|
if (existing !== undefined) {
|
|
1024
1027
|
if (existing.state === 'missing') {
|
|
@@ -1206,7 +1209,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
1206
1209
|
*
|
|
1207
1210
|
* @param {string} sessionId session to restore.
|
|
1208
1211
|
* @param {'normal' | 'orphan'} [mode] restore mode; orphan restores the log and detaches the Workspace.
|
|
1209
|
-
* @returns {Promise<
|
|
1212
|
+
* @returns {Promise<any>} the restore result.
|
|
1210
1213
|
*/
|
|
1211
1214
|
async function restore(sessionId, mode = 'normal') {
|
|
1212
1215
|
if (mode !== 'normal' && mode !== 'orphan') {
|
|
@@ -1471,7 +1474,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
1471
1474
|
* object directory vanished. The caller must hold the trash lock.
|
|
1472
1475
|
*
|
|
1473
1476
|
* @param {number} now current timestamp.
|
|
1474
|
-
* @returns {Promise<
|
|
1477
|
+
* @returns {Promise<any>} reconcile summary.
|
|
1475
1478
|
*/
|
|
1476
1479
|
async function reconcileInsideLock(now) {
|
|
1477
1480
|
try {
|
|
@@ -1493,7 +1496,7 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
1493
1496
|
* entries. Used by startup recovery, the scheduled sweep, and manual cleanup.
|
|
1494
1497
|
*
|
|
1495
1498
|
* @param {{ allowPurge?: boolean, now?: number, reason?: string }} [options] sweep policy.
|
|
1496
|
-
* @returns {Promise<
|
|
1499
|
+
* @returns {Promise<any>} the sweep summary.
|
|
1497
1500
|
*/
|
|
1498
1501
|
async function sweep({ allowPurge = true, now = Date.now(), reason = 'scheduled' } = {}) {
|
|
1499
1502
|
return store.withLock(async () => {
|
|
@@ -1519,6 +1522,40 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
1519
1522
|
})
|
|
1520
1523
|
}
|
|
1521
1524
|
|
|
1525
|
+
/**
|
|
1526
|
+
* Remove unindexed object directories that contain no session payload, only
|
|
1527
|
+
* plugin metadata. Directories that may still hold session logs are kept for
|
|
1528
|
+
* manual inspection.
|
|
1529
|
+
*
|
|
1530
|
+
* @returns {Promise<{ removed: number }>} how many empty object directories were removed.
|
|
1531
|
+
*/
|
|
1532
|
+
async function purgeEmptyOrphans() {
|
|
1533
|
+
return store.withLock(async () => {
|
|
1534
|
+
if (store.isProtected()) {
|
|
1535
|
+
throw new TrashError('INDEX_CORRUPT', 'The trash index is unreadable, so protection mode refuses changes.')
|
|
1536
|
+
}
|
|
1537
|
+
const { objects } = await scanObjects(store, logger)
|
|
1538
|
+
const indexed = new Set(store.entries().map((entry) => entry.trashId))
|
|
1539
|
+
let removed = 0
|
|
1540
|
+
for (const object of objects) {
|
|
1541
|
+
if (indexed.has(object.trashId)) continue
|
|
1542
|
+
if (!(await isEmptyObjectDir(store, object.trashId))) continue
|
|
1543
|
+
try {
|
|
1544
|
+
await store.removeObjectDir(object.trashId)
|
|
1545
|
+
removed += 1
|
|
1546
|
+
logger.info(`removed an empty unindexed trash object (${object.trashId})`)
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
logger.warn(`cannot remove empty unindexed trash object ${object.trashId}: ${messageOf(error)}`)
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (removed > 0) {
|
|
1552
|
+
const { unindexed } = await scanObjects(store, logger)
|
|
1553
|
+
store.unindexed = unindexed
|
|
1554
|
+
}
|
|
1555
|
+
return { removed }
|
|
1556
|
+
})
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1522
1559
|
/**
|
|
1523
1560
|
* Restore the index from object sidecars after the primary index and its
|
|
1524
1561
|
* backup were both rejected.
|
|
@@ -1740,5 +1777,5 @@ export function createTrashService({ ctx, store, eligibility, config, logger, pa
|
|
|
1740
1777
|
return summary
|
|
1741
1778
|
}
|
|
1742
1779
|
|
|
1743
|
-
return { trash, restore, recover, purge, purgeAll, forget, sweep, rebuild, reveal }
|
|
1780
|
+
return { trash, restore, recover, purge, purgeAll, forget, sweep, rebuild, reveal, purgeEmptyOrphans }
|
|
1744
1781
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wangxiaocuo/dsh-session-trash",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "DeepSeek Harness plugin: move cold sessions into a recoverable trash, restore them within the retention window, or delete their logs permanently.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wangxiaocuo",
|
|
@@ -47,14 +47,21 @@
|
|
|
47
47
|
"test": "node test/run-unit.mjs",
|
|
48
48
|
"test:coverage": "node test/run-unit.mjs --experimental-test-coverage",
|
|
49
49
|
"test:e2e": "node test/e2e-web.mjs",
|
|
50
|
-
"test:smoke": "node test/smoke-host.mjs"
|
|
50
|
+
"test:smoke": "node test/smoke-host.mjs",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
52
|
+
"test:dom": "node --test test/client-dom.test.js"
|
|
51
53
|
},
|
|
52
54
|
"dependencies": {
|
|
53
55
|
"@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
|
|
54
56
|
"@deepseek-ai/schemastery": "3.19.0"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
|
57
|
-
"@deepseek-ai/cordis": "4.1.0"
|
|
59
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
60
|
+
"@types/node": "20.19.43",
|
|
61
|
+
"jsdom": "27.4.0",
|
|
62
|
+
"react": "19.3.1",
|
|
63
|
+
"react-dom": "19.3.1",
|
|
64
|
+
"typescript": "5.9.3"
|
|
58
65
|
},
|
|
59
66
|
"dsh": {
|
|
60
67
|
"bundle": {
|