dsh-archived 0.3.0
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/LICENSE +21 -0
- package/README.md +173 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +931 -0
- package/lib/host/metadata.js +144 -0
- package/lib/host/paths.js +117 -0
- package/lib/host/quarantine.js +185 -0
- package/lib/host/trust.js +68 -0
- package/lib/index.js +410 -0
- package/package.json +106 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// dsh-archived host half.
|
|
2
|
+
//
|
|
3
|
+
// Adds permanent deletion for archived sessions to the DSH web server:
|
|
4
|
+
//
|
|
5
|
+
// GET /api/dsh-archived/state archived rows + on-disk truth
|
|
6
|
+
// POST /api/dsh-archived/delete { sessionId, mode }
|
|
7
|
+
// POST /api/dsh-archived/delete-all { mode, ids? }
|
|
8
|
+
// POST /api/dsh-archived/restore { sessionId }
|
|
9
|
+
// POST /api/dsh-archived/empty-quarantine purge everything parked
|
|
10
|
+
// POST /api/dsh-archived/reveal { sessionId } -> file manager
|
|
11
|
+
//
|
|
12
|
+
// One archived session owns three pieces of state, and a permanent delete must
|
|
13
|
+
// clear all three or the archive list keeps lying to the user:
|
|
14
|
+
//
|
|
15
|
+
// 1. the artifact directory $DSH_HOME/sessions/<encoded-workspace>/session-<id>/
|
|
16
|
+
// 2. the projection cache doc $DSH_HOME/storages/session_projcache/sessions/<id>.json
|
|
17
|
+
// 3. the archive index entry workspace registry `archivedSessionIds`
|
|
18
|
+
//
|
|
19
|
+
// (3) goes through the official registry write, so every connected client
|
|
20
|
+
// refreshes its archive set through the normal broadcast. The registry's
|
|
21
|
+
// unarchiveSession deliberately runs no existence check, which is exactly what
|
|
22
|
+
// lets a leftover index entry whose session is already gone be cleaned too.
|
|
23
|
+
//
|
|
24
|
+
// Deleting is a MOVE by default: see lib/host/quarantine.js. `mode: "forever"`
|
|
25
|
+
// is the only irreversible path, and it is never the default.
|
|
26
|
+
|
|
27
|
+
import { spawn } from "node:child_process";
|
|
28
|
+
import { existsSync } from "node:fs";
|
|
29
|
+
import { rm } from "node:fs/promises";
|
|
30
|
+
import { dirname } from "node:path";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
SESSION_ID,
|
|
34
|
+
directorySize,
|
|
35
|
+
dshHome,
|
|
36
|
+
legacyAggregateFile,
|
|
37
|
+
locateArtifacts,
|
|
38
|
+
projectionCacheFile,
|
|
39
|
+
quarantineArtifactDir,
|
|
40
|
+
} from "./host/paths.js";
|
|
41
|
+
import {
|
|
42
|
+
ancestorsOf,
|
|
43
|
+
isRunning,
|
|
44
|
+
legacyAggregateRows,
|
|
45
|
+
liveChildrenOf,
|
|
46
|
+
readCacheDocument,
|
|
47
|
+
runningDescendants,
|
|
48
|
+
} from "./host/metadata.js";
|
|
49
|
+
import {
|
|
50
|
+
expireQuarantine,
|
|
51
|
+
listQuarantine,
|
|
52
|
+
purgeQuarantineEntry,
|
|
53
|
+
quarantineEntryFor,
|
|
54
|
+
quarantineSession,
|
|
55
|
+
restoreSession,
|
|
56
|
+
} from "./host/quarantine.js";
|
|
57
|
+
import { failureDetail, isTrustedRequest, json, readJsonBody } from "./host/trust.js";
|
|
58
|
+
|
|
59
|
+
export const name = "dsh-archived";
|
|
60
|
+
export const inject = ["webServer", "workspaceRegistry", "agents", "sessions"];
|
|
61
|
+
|
|
62
|
+
const BASE = "/api/dsh-archived";
|
|
63
|
+
const STATE_PATH = BASE + "/state";
|
|
64
|
+
const DELETE_PATH = BASE + "/delete";
|
|
65
|
+
const DELETE_ALL_PATH = BASE + "/delete-all";
|
|
66
|
+
const RESTORE_PATH = BASE + "/restore";
|
|
67
|
+
const EMPTY_PATH = BASE + "/empty-quarantine";
|
|
68
|
+
const REVEAL_PATH = BASE + "/reveal";
|
|
69
|
+
|
|
70
|
+
/** Wire version of the route contract, so the panel can detect a half-upgraded host. */
|
|
71
|
+
export const API_VERSION = 2;
|
|
72
|
+
|
|
73
|
+
/** How long a quarantined session stays recoverable before it expires. */
|
|
74
|
+
const QUARANTINE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
75
|
+
|
|
76
|
+
/** Post-delete sweep delays (ms): a lingering idle agent can rewrite its cache once more. */
|
|
77
|
+
const SWEEP_DELAYS = [3_000, 15_000, 60_000];
|
|
78
|
+
|
|
79
|
+
/** Live sweep timers, cleared when the plugin unloads. */
|
|
80
|
+
const sweeps = new Set();
|
|
81
|
+
|
|
82
|
+
/** Session ids with a delete in flight, so two tabs cannot interleave on one row. */
|
|
83
|
+
const inFlight = new Set();
|
|
84
|
+
|
|
85
|
+
/** The archive set's owner. `ctx.get` is the documented safe read; the property stays as a fallback. */
|
|
86
|
+
function registryOf(ctx) {
|
|
87
|
+
try {
|
|
88
|
+
const resolved = ctx.get?.("workspaceRegistry");
|
|
89
|
+
if (resolved !== undefined && resolved !== null) return resolved;
|
|
90
|
+
} catch {
|
|
91
|
+
// An unresolvable service read falls through to the property form.
|
|
92
|
+
}
|
|
93
|
+
return ctx.workspaceRegistry;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Host-owned archive set; a missing registry degrades to "nothing archived". */
|
|
97
|
+
function archivedIds(ctx) {
|
|
98
|
+
const ids = registryOf(ctx)?.archivedSessionIds;
|
|
99
|
+
return Array.isArray(ids) ? [...ids] : [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether this host still offers what a destructive route needs. A host whose
|
|
104
|
+
* registry lost `unarchiveSession` cannot clear the archive index, and a delete
|
|
105
|
+
* that leaves the index behind is the exact "fake success" this plugin exists
|
|
106
|
+
* to prevent — so the routes refuse instead of half-working.
|
|
107
|
+
*/
|
|
108
|
+
function hostSupportsDelete(ctx) {
|
|
109
|
+
return typeof registryOf(ctx)?.unarchiveSession === "function";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Refusal codes; the panel localizes every one of them. */
|
|
113
|
+
function refuse(code, extra) {
|
|
114
|
+
return { ok: false, error: code, ...(extra ?? {}) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sumBytes(entries) {
|
|
118
|
+
let total = 0;
|
|
119
|
+
for (const entry of entries) total += entry.bytes ?? 0;
|
|
120
|
+
return total;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Archived rows with everything the panel needs to draw them, plus quarantine. */
|
|
124
|
+
async function stateOf(ctx) {
|
|
125
|
+
const ids = archivedIds(ctx);
|
|
126
|
+
const legacyRows = await legacyAggregateRows();
|
|
127
|
+
const items = [];
|
|
128
|
+
for (const id of ids) {
|
|
129
|
+
const dirs = await locateArtifacts(id);
|
|
130
|
+
let bytes = 0;
|
|
131
|
+
for (const dir of dirs) bytes += await directorySize(dir);
|
|
132
|
+
const cacheFile = projectionCacheFile(id);
|
|
133
|
+
const cache = existsSync(cacheFile);
|
|
134
|
+
const document = cache ? await readCacheDocument(id) : null;
|
|
135
|
+
items.push({
|
|
136
|
+
id,
|
|
137
|
+
archived: true,
|
|
138
|
+
title: document?.title ?? null,
|
|
139
|
+
cwd: document?.cwd ?? null,
|
|
140
|
+
createdAt: document?.createdAt ?? null,
|
|
141
|
+
artifact: { present: dirs.length > 0, bytes, path: dirs.length > 0 ? dirs[0] : null },
|
|
142
|
+
cache,
|
|
143
|
+
indexOnly: dirs.length === 0 && cache === false,
|
|
144
|
+
cacheOnly: dirs.length === 0 && cache === true,
|
|
145
|
+
running: isRunning(ctx, id),
|
|
146
|
+
ancestors: ancestorsOf(ctx, id),
|
|
147
|
+
children: liveChildrenOf(ctx, id),
|
|
148
|
+
legacyRow: legacyRows !== null && legacyRows.has(id),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const parked = await listQuarantine();
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
apiVersion: API_VERSION,
|
|
155
|
+
home: dshHome(),
|
|
156
|
+
items,
|
|
157
|
+
quarantine: { count: parked.length, bytes: sumBytes(parked), items: parked },
|
|
158
|
+
legacy: {
|
|
159
|
+
present: existsSync(legacyAggregateFile()),
|
|
160
|
+
rows: legacyRows === null ? null : legacyRows.size,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Delay a delete that already happened in full. A session archived while its
|
|
167
|
+
* agent was still open keeps that idle agent in memory, and DSH's projection
|
|
168
|
+
* service can rewrite the cache JSON (never the log) from that state seconds
|
|
169
|
+
* later, so every delete schedules bounded best-effort sweeps that re-remove
|
|
170
|
+
* such residue.
|
|
171
|
+
*/
|
|
172
|
+
function scheduleSweep(ctx, sessionId) {
|
|
173
|
+
if (sweeps.size >= 192) return;
|
|
174
|
+
for (const delay of SWEEP_DELAYS) {
|
|
175
|
+
const timer = setTimeout(() => {
|
|
176
|
+
sweeps.delete(timer);
|
|
177
|
+
sweepResidue(ctx, sessionId).catch((error) => {
|
|
178
|
+
ctx.logger?.debug?.(`[dsh-archived] sweep ${sessionId} failed:`, error);
|
|
179
|
+
});
|
|
180
|
+
}, delay);
|
|
181
|
+
timer.unref?.();
|
|
182
|
+
sweeps.add(timer);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Re-remove the cache document a lingering idle agent can rewrite after a
|
|
188
|
+
* delete. Only that: the log directory never resurrects, and a directory back
|
|
189
|
+
* on disk means the session itself came back (a restore whose re-archive
|
|
190
|
+
* failed), which the sweep must not touch.
|
|
191
|
+
*/
|
|
192
|
+
async function sweepResidue(ctx, sessionId) {
|
|
193
|
+
if (archivedIds(ctx).includes(sessionId)) return;
|
|
194
|
+
if (isRunning(ctx, sessionId)) return;
|
|
195
|
+
if ((await locateArtifacts(sessionId)).length > 0) return;
|
|
196
|
+
const cacheFile = projectionCacheFile(sessionId);
|
|
197
|
+
if (existsSync(cacheFile)) await rm(cacheFile, { force: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Serialize destructive work per session so two tabs cannot interleave. */
|
|
201
|
+
async function withLock(sessionId, work) {
|
|
202
|
+
if (inFlight.has(sessionId)) return refuse("busy", { sessionId });
|
|
203
|
+
inFlight.add(sessionId);
|
|
204
|
+
try {
|
|
205
|
+
return await work();
|
|
206
|
+
} finally {
|
|
207
|
+
inFlight.delete(sessionId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Permanently remove (or park) one archived session: artifacts, projection
|
|
213
|
+
* cache, then the archive index entry. Refusals are explicit codes.
|
|
214
|
+
*/
|
|
215
|
+
async function deleteOne(ctx, sessionId, mode) {
|
|
216
|
+
if (typeof sessionId !== "string" || !SESSION_ID.test(sessionId)) return refuse("invalid-session-id", { sessionId });
|
|
217
|
+
if (!hostSupportsDelete(ctx)) return refuse("no-registry", { sessionId });
|
|
218
|
+
if (!archivedIds(ctx).includes(sessionId)) return refuse("not-archived", { sessionId });
|
|
219
|
+
if (isRunning(ctx, sessionId)) return refuse("session-running", { sessionId });
|
|
220
|
+
const descendants = runningDescendants(ctx, sessionId);
|
|
221
|
+
if (descendants.length > 0) return refuse("subagent-running", { sessionId, descendants });
|
|
222
|
+
|
|
223
|
+
return withLock(sessionId, async () => {
|
|
224
|
+
const dirs = await locateArtifacts(sessionId);
|
|
225
|
+
const cacheFile = projectionCacheFile(sessionId);
|
|
226
|
+
let bytes = 0;
|
|
227
|
+
for (const dir of dirs) bytes += await directorySize(dir);
|
|
228
|
+
|
|
229
|
+
let removed;
|
|
230
|
+
if (mode === "forever") {
|
|
231
|
+
for (const dir of dirs) await rm(dir, { recursive: true, force: true });
|
|
232
|
+
let cache = false;
|
|
233
|
+
if (existsSync(cacheFile)) {
|
|
234
|
+
await rm(cacheFile, { force: true });
|
|
235
|
+
cache = true;
|
|
236
|
+
}
|
|
237
|
+
removed = { directories: dirs.length, bytes, cache };
|
|
238
|
+
} else if (dirs.length === 0 && !existsSync(cacheFile)) {
|
|
239
|
+
// An index-only entry has no payload to park. Creating an empty recycle-bin
|
|
240
|
+
// entry for it would put something in the bin that can never be restored,
|
|
241
|
+
// and that the panel would not even show.
|
|
242
|
+
removed = { directories: 0, bytes: 0, cache: false, parked: false };
|
|
243
|
+
} else {
|
|
244
|
+
const document = existsSync(cacheFile) ? await readCacheDocument(sessionId) : null;
|
|
245
|
+
removed = await quarantineSession(sessionId, {
|
|
246
|
+
dirs,
|
|
247
|
+
cacheFile,
|
|
248
|
+
meta: {
|
|
249
|
+
deletedAt: Date.now(),
|
|
250
|
+
title: document?.title ?? null,
|
|
251
|
+
cwd: document?.cwd ?? null,
|
|
252
|
+
workspaceDir: dirs.length > 0 ? dirname(dirs[0]) : null,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
removed.parked = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await registryOf(ctx).unarchiveSession(sessionId);
|
|
259
|
+
scheduleSweep(ctx, sessionId);
|
|
260
|
+
ctx.logger?.info?.(
|
|
261
|
+
`[dsh-archived] delete ${sessionId} (${mode}): ${removed.directories} dir(s), ${removed.bytes} bytes`,
|
|
262
|
+
);
|
|
263
|
+
return { ok: true, sessionId, mode, removed, archivedSessionIds: archivedIds(ctx) };
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function deleteMany(ctx, ids, mode) {
|
|
268
|
+
const results = [];
|
|
269
|
+
for (const id of ids) {
|
|
270
|
+
try {
|
|
271
|
+
results.push(await deleteOne(ctx, id, mode));
|
|
272
|
+
} catch (error) {
|
|
273
|
+
results.push(refuse("error", { sessionId: id, message: failureDetail(error) }));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { ok: results.every((result) => result.ok), results, archivedSessionIds: archivedIds(ctx) };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Put a parked session back where it came from and re-file it in the archive set. */
|
|
280
|
+
async function restoreOne(ctx, sessionId) {
|
|
281
|
+
if (typeof sessionId !== "string" || !SESSION_ID.test(sessionId)) return refuse("invalid-session-id", { sessionId });
|
|
282
|
+
const entry = await quarantineEntryFor(sessionId);
|
|
283
|
+
if (entry === null) return refuse("not-quarantined", { sessionId });
|
|
284
|
+
return withLock(sessionId, async () => {
|
|
285
|
+
const restored = await restoreSession(ctx, sessionId, entry);
|
|
286
|
+
ctx.logger?.info?.(`[dsh-archived] restore ${sessionId}`);
|
|
287
|
+
return { ok: true, sessionId, restored, archivedSessionIds: archivedIds(ctx) };
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Open one session's directory in the platform file manager.
|
|
293
|
+
*
|
|
294
|
+
* `DSH_ARCHIVED_OPENER=none` answers the path without launching
|
|
295
|
+
* anything: a headless host has no file manager to launch, and a test run
|
|
296
|
+
* should not pop windows on the operator's desktop.
|
|
297
|
+
*
|
|
298
|
+
* @returns the opener actually used.
|
|
299
|
+
*/
|
|
300
|
+
function revealPath(path) {
|
|
301
|
+
if (process.env.DSH_ARCHIVED_OPENER === "none") return "none";
|
|
302
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
303
|
+
const child = spawn(command, [path], { detached: true, stdio: "ignore" });
|
|
304
|
+
child.on("error", () => {});
|
|
305
|
+
child.unref();
|
|
306
|
+
return command;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function revealOne(ctx, sessionId) {
|
|
310
|
+
if (typeof sessionId !== "string" || !SESSION_ID.test(sessionId)) return refuse("invalid-session-id", { sessionId });
|
|
311
|
+
const dirs = await locateArtifacts(sessionId);
|
|
312
|
+
if (dirs.length > 0) {
|
|
313
|
+
return { ok: true, sessionId, opened: dirs[0], where: "sessions", opener: revealPath(dirs[0]) };
|
|
314
|
+
}
|
|
315
|
+
const parked = quarantineArtifactDir(sessionId);
|
|
316
|
+
if (existsSync(parked)) {
|
|
317
|
+
return { ok: true, sessionId, opened: parked, where: "quarantine", opener: revealPath(parked) };
|
|
318
|
+
}
|
|
319
|
+
return refuse("no-artifact", { sessionId });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** One route: trust gate, method gate, handler, uniform failure reporting. */
|
|
323
|
+
function route(ctx, { path, method, handle }) {
|
|
324
|
+
return ctx.effect(
|
|
325
|
+
() =>
|
|
326
|
+
ctx.webServer.register({
|
|
327
|
+
kind: "exact",
|
|
328
|
+
path,
|
|
329
|
+
handler: async (request, response) => {
|
|
330
|
+
if (request.method !== method) {
|
|
331
|
+
json(response, 405, { error: "method-not-allowed" });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (!isTrustedRequest(request)) {
|
|
335
|
+
json(response, 403, { error: "forbidden" });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const body = method === "POST" ? await readJsonBody(request) : {};
|
|
340
|
+
const result = await handle(body);
|
|
341
|
+
json(response, result?.ok === false && result.error === "busy" ? 409 : 200, result);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
ctx.logger?.warn?.(`[dsh-archived] ${path} failed:`, error);
|
|
344
|
+
json(response, 500, { ok: false, error: "internal", detail: failureDetail(error) });
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
}),
|
|
348
|
+
`dsh-archived: ${path}`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function apply(ctx) {
|
|
353
|
+
route(ctx, { path: STATE_PATH, method: "GET", handle: () => stateOf(ctx) });
|
|
354
|
+
|
|
355
|
+
route(ctx, {
|
|
356
|
+
path: DELETE_PATH,
|
|
357
|
+
method: "POST",
|
|
358
|
+
handle: (body) => deleteOne(ctx, body?.sessionId, body?.mode === "forever" ? "forever" : "quarantine"),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
route(ctx, {
|
|
362
|
+
path: DELETE_ALL_PATH,
|
|
363
|
+
method: "POST",
|
|
364
|
+
handle: (body) => {
|
|
365
|
+
const mode = body?.mode === "forever" ? "forever" : "quarantine";
|
|
366
|
+
const ids = Array.isArray(body?.ids) ? body.ids : archivedIds(ctx);
|
|
367
|
+
return deleteMany(ctx, ids, mode);
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
route(ctx, { path: RESTORE_PATH, method: "POST", handle: (body) => restoreOne(ctx, body?.sessionId) });
|
|
372
|
+
|
|
373
|
+
route(ctx, {
|
|
374
|
+
path: EMPTY_PATH,
|
|
375
|
+
method: "POST",
|
|
376
|
+
handle: async () => {
|
|
377
|
+
const parked = await listQuarantine();
|
|
378
|
+
for (const entry of parked) await purgeQuarantineEntry(entry.id);
|
|
379
|
+
return { ok: true, purged: parked.length, bytes: sumBytes(parked) };
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
route(ctx, { path: REVEAL_PATH, method: "POST", handle: (body) => revealOne(ctx, body?.sessionId) });
|
|
384
|
+
|
|
385
|
+
// A parked session is recoverable for a bounded window, then it expires on
|
|
386
|
+
// its own so the quarantine can never become a second archive nobody drains.
|
|
387
|
+
const expire = () =>
|
|
388
|
+
expireQuarantine(QUARANTINE_MAX_AGE_MS).then((dropped) => {
|
|
389
|
+
if (dropped.length > 0) ctx.logger?.info?.(`[dsh-archived] quarantine expired: ${dropped.length}`);
|
|
390
|
+
});
|
|
391
|
+
expire().catch(() => {});
|
|
392
|
+
const daily = setInterval(() => expire().catch(() => {}), 24 * 60 * 60 * 1000);
|
|
393
|
+
daily.unref?.();
|
|
394
|
+
ctx.effect(() => () => clearInterval(daily), "dsh-archived: quarantine expiry");
|
|
395
|
+
|
|
396
|
+
ctx.effect(
|
|
397
|
+
() => () => {
|
|
398
|
+
for (const timer of sweeps) clearTimeout(timer);
|
|
399
|
+
sweeps.clear();
|
|
400
|
+
},
|
|
401
|
+
"dsh-archived: sweep timers",
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
if (!hostSupportsDelete(ctx)) {
|
|
405
|
+
ctx.logger?.warn?.(
|
|
406
|
+
"[dsh-archived] workspaceRegistry.unarchiveSession is missing — destructive routes will refuse",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
ctx.logger?.info?.("[dsh-archived] host loaded");
|
|
410
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-archived",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Archived-sessions page for DeepSeek Harness: per-row and batch deletion that clears the session log, the projection cache and the archive index together, with a 30-day recycle bin, subagent-aware refusals and a per-row disk footprint.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node tools/host-smoke.mjs && node tools/client-smoke.mjs && node tools/compat-check.mjs"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dsh": {
|
|
26
|
+
"bundle": {
|
|
27
|
+
"patch": "./cordis.patch.yml"
|
|
28
|
+
},
|
|
29
|
+
"client": {
|
|
30
|
+
"platform": "web",
|
|
31
|
+
"inject": [
|
|
32
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
33
|
+
"@deepseek-ai/dsh-client-locale",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
41
|
+
"@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
|
|
42
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-workspace": "^0.2.0-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-session": "^0.2.0-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-workspace": "^0.2.0-rc.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@deepseek-ai/cordis": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@deepseek-ai/dsh-agent": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-settings": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-workspace": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@deepseek-ai/dsh-session": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@deepseek-ai/dsh-workspace": {
|
|
76
|
+
"optional": true
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"keywords": [
|
|
80
|
+
"dsh",
|
|
81
|
+
"dsh-plugin",
|
|
82
|
+
"deepseek",
|
|
83
|
+
"deepseek-harness",
|
|
84
|
+
"cordis",
|
|
85
|
+
"sessions",
|
|
86
|
+
"archived-sessions",
|
|
87
|
+
"delete",
|
|
88
|
+
"recycle-bin",
|
|
89
|
+
"settings",
|
|
90
|
+
"web"
|
|
91
|
+
],
|
|
92
|
+
"author": "Haotian Lu (https://github.com/haotian-lu-prog)",
|
|
93
|
+
"repository": {
|
|
94
|
+
"type": "git",
|
|
95
|
+
"url": "git+https://github.com/haotian-lu-prog/dsh-archived.git"
|
|
96
|
+
},
|
|
97
|
+
"homepage": "https://github.com/haotian-lu-prog/dsh-archived#readme",
|
|
98
|
+
"bugs": {
|
|
99
|
+
"url": "https://github.com/haotian-lu-prog/dsh-archived/issues"
|
|
100
|
+
},
|
|
101
|
+
"publishConfig": {
|
|
102
|
+
"registry": "https://registry.npmjs.org",
|
|
103
|
+
"access": "public"
|
|
104
|
+
},
|
|
105
|
+
"license": "MIT"
|
|
106
|
+
}
|