dsh-archived 0.3.1 → 0.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/lib/client.js +57 -31
- package/lib/host/paths.js +114 -5
- package/lib/host/quarantine.js +8 -2
- package/lib/index.js +41 -6
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -80,7 +80,15 @@ window.__ModuleLoader__.load({
|
|
|
80
80
|
document.head.appendChild(tag);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Same-origin call to this plugin's host routes.
|
|
85
|
+
*
|
|
86
|
+
* A JSON error body is returned for the caller to localize, but a transport
|
|
87
|
+
* failure must reject: a 403 (refused by the trust gate) or a 500 used to
|
|
88
|
+
* resolve into whatever JSON it carried, and every caller then read a field
|
|
89
|
+
* that wasn't there — the panel ended up showing a generic failure that hid
|
|
90
|
+
* the real reason.
|
|
91
|
+
*/
|
|
84
92
|
function request(path, options) {
|
|
85
93
|
var init = {
|
|
86
94
|
method: options && options.method ? options.method : "GET",
|
|
@@ -92,9 +100,22 @@ window.__ModuleLoader__.load({
|
|
|
92
100
|
init.body = JSON.stringify(options.body);
|
|
93
101
|
}
|
|
94
102
|
return fetch(path, init).then(function (response) {
|
|
95
|
-
return response
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
return response
|
|
104
|
+
.json()
|
|
105
|
+
.catch(function () {
|
|
106
|
+
return { error: "internal" };
|
|
107
|
+
})
|
|
108
|
+
.then(function (payload) {
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
// Localizing needs the panel's `t`, so carry the host's error code
|
|
111
|
+
// and status out and let the caller translate them.
|
|
112
|
+
var error = new Error("HTTP " + response.status);
|
|
113
|
+
error.code = payload && payload.error ? String(payload.error) : "internal";
|
|
114
|
+
error.status = response.status;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
return payload;
|
|
118
|
+
});
|
|
98
119
|
});
|
|
99
120
|
}
|
|
100
121
|
|
|
@@ -276,7 +297,10 @@ window.__ModuleLoader__.load({
|
|
|
276
297
|
return payload;
|
|
277
298
|
})
|
|
278
299
|
.catch(function (error) {
|
|
279
|
-
|
|
300
|
+
// A transport failure carries the host's error code; a payload-level
|
|
301
|
+
// business error already carries a localized message.
|
|
302
|
+
var text = error && error.code ? describeError(error.code) : String(error && error.message ? error.message : error);
|
|
303
|
+
setNotice({ kind: "error", text: text });
|
|
280
304
|
})
|
|
281
305
|
.then(function () {
|
|
282
306
|
setBusy(null);
|
|
@@ -297,24 +321,39 @@ window.__ModuleLoader__.load({
|
|
|
297
321
|
});
|
|
298
322
|
}
|
|
299
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Report one batch reply. The host's `delete-all` answers with a per-row
|
|
326
|
+
* `results` array; reading it unguarded turned any other reply into a
|
|
327
|
+
* TypeError that replaced the real reason on screen.
|
|
328
|
+
*/
|
|
329
|
+
function reportBatch(payload) {
|
|
330
|
+
var results = payload && Array.isArray(payload.results) ? payload.results : [];
|
|
331
|
+
if (results.length === 0) {
|
|
332
|
+
setNotice({ kind: "error", text: t("error.generic") });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
var ok = 0;
|
|
336
|
+
var bytes = 0;
|
|
337
|
+
for (var i = 0; i < results.length; i += 1) {
|
|
338
|
+
if (results[i] && results[i].ok === true) {
|
|
339
|
+
ok += 1;
|
|
340
|
+
if (results[i].removed) bytes += results[i].removed.bytes || 0;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
setNotice(
|
|
344
|
+
ok === results.length
|
|
345
|
+
? { kind: "ok", text: t("deletedMany", { n: ok, size: formatBytes(bytes) }) }
|
|
346
|
+
: { kind: "error", text: t("deletedPartial", { ok: ok, failed: results.length - ok }) },
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
300
350
|
function deleteSelected(mode) {
|
|
301
351
|
var ids = selectedIds;
|
|
302
352
|
if (ids.length === 0) return;
|
|
303
353
|
setBusy("__selected__");
|
|
304
354
|
run(DELETE_ALL_PATH, { ids: ids, mode: mode }, function (payload) {
|
|
305
|
-
var ok = payload.results.filter(function (result) {
|
|
306
|
-
return result.ok === true;
|
|
307
|
-
}).length;
|
|
308
|
-
var bytes = 0;
|
|
309
|
-
for (var i = 0; i < payload.results.length; i += 1) {
|
|
310
|
-
if (payload.results[i].ok && payload.results[i].removed) bytes += payload.results[i].removed.bytes || 0;
|
|
311
|
-
}
|
|
312
355
|
setSelected({});
|
|
313
|
-
|
|
314
|
-
ok === payload.results.length
|
|
315
|
-
? { kind: "ok", text: t("deletedMany", { n: ok, size: formatBytes(bytes) }) }
|
|
316
|
-
: { kind: "error", text: t("deletedPartial", { ok: ok, failed: payload.results.length - ok }) },
|
|
317
|
-
);
|
|
356
|
+
reportBatch(payload);
|
|
318
357
|
});
|
|
319
358
|
}
|
|
320
359
|
|
|
@@ -323,20 +362,7 @@ window.__ModuleLoader__.load({
|
|
|
323
362
|
return row.id;
|
|
324
363
|
});
|
|
325
364
|
setBusy("__all__");
|
|
326
|
-
run(DELETE_ALL_PATH, { ids: ids, mode: mode },
|
|
327
|
-
var ok = payload.results.filter(function (result) {
|
|
328
|
-
return result.ok === true;
|
|
329
|
-
}).length;
|
|
330
|
-
var bytes = 0;
|
|
331
|
-
for (var i = 0; i < payload.results.length; i += 1) {
|
|
332
|
-
if (payload.results[i].ok && payload.results[i].removed) bytes += payload.results[i].removed.bytes || 0;
|
|
333
|
-
}
|
|
334
|
-
setNotice(
|
|
335
|
-
ok === payload.results.length
|
|
336
|
-
? { kind: "ok", text: t("deletedMany", { n: ok, size: formatBytes(bytes) }) }
|
|
337
|
-
: { kind: "error", text: t("deletedPartial", { ok: ok, failed: payload.results.length - ok }) },
|
|
338
|
-
);
|
|
339
|
-
});
|
|
365
|
+
run(DELETE_ALL_PATH, { ids: ids, mode: mode }, reportBatch);
|
|
340
366
|
}
|
|
341
367
|
|
|
342
368
|
function restoreRow(entry) {
|
package/lib/host/paths.js
CHANGED
|
@@ -18,8 +18,87 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
import { join, resolve } from "node:path";
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Ids the *store* and the *host API* accept. A session is `session-<uuid>`, but
|
|
23
|
+
* a session whose header carries no prefix at all is a bare uuid (imported
|
|
24
|
+
* transcripts and delegate imports both produce those).
|
|
25
|
+
*/
|
|
26
|
+
const UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
27
|
+
const IMPORT_PREFIXES = ["import-session-", "import-"];
|
|
28
|
+
const CANONICAL_PREFIX = "session-";
|
|
29
|
+
export const HOST_ID = new RegExp(`^(?:${CANONICAL_PREFIX})?${UUID}$`, "i");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ids an archived *row* may be filed under.
|
|
33
|
+
*
|
|
34
|
+
* DSH's own sessions are `session-<uuid>`, but the import feature namespaces the
|
|
35
|
+
* id it files in the archive index: `import-session-<uuid>` for an imported
|
|
36
|
+
* transcript and `import-<uuid>` for a delegate-imported session. Every one of
|
|
37
|
+
* those is a real archived row the panel draws, so all of them must be accepted.
|
|
38
|
+
* Refusing the import namespaces was a silent, total failure: the rows rendered
|
|
39
|
+
* fine and every delete came back `invalid-session-id`.
|
|
40
|
+
*
|
|
41
|
+
* The path-safety argument is unchanged — a bare uuid is the only shape any form
|
|
42
|
+
* may carry, so no form can introduce a separator, a `..`, or a NUL.
|
|
43
|
+
*/
|
|
44
|
+
export const SESSION_ID = new RegExp(`^(?:import-session-|import-|${CANONICAL_PREFIX})?${UUID}$`, "i");
|
|
45
|
+
|
|
46
|
+
/** The raw id a registry entry names, with its import namespace peeled off. */
|
|
47
|
+
export function canonicalSessionId(raw) {
|
|
48
|
+
const text = String(raw);
|
|
49
|
+
for (const prefix of IMPORT_PREFIXES) {
|
|
50
|
+
if (text.startsWith(prefix)) return text.slice(prefix.length);
|
|
51
|
+
}
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ordered ids the *filesystem* may know one archived row by.
|
|
57
|
+
*
|
|
58
|
+
* The archive index is namespaced; the session store is not. `import-<uuid>` and
|
|
59
|
+
* `import-session-<uuid>` both name a session whose directory (and whose log
|
|
60
|
+
* header `id`) is the bare uuid or `session-<uuid>`. `locateArtifacts` only ever
|
|
61
|
+
* accepts a directory whose decoded name is one of these, so a namespace
|
|
62
|
+
* collision cannot make the plugin delete a different session's log.
|
|
63
|
+
*/
|
|
64
|
+
export function artifactIdCandidates(raw) {
|
|
65
|
+
const text = String(raw);
|
|
66
|
+
const out = [text];
|
|
67
|
+
const add = (value) => {
|
|
68
|
+
if (value.length > 0 && !out.includes(value)) out.push(value);
|
|
69
|
+
};
|
|
70
|
+
// The import namespace wraps the store shape, so peeling it off is the first
|
|
71
|
+
// and usually only step. The other store form is added as a fallback, because
|
|
72
|
+
// each import form has been seen wrapping each store shape. Peeling `import-`
|
|
73
|
+
// off `import-session-<uuid>` would leave `session-session-<uuid>`, a name
|
|
74
|
+
// nothing on disk carries.
|
|
75
|
+
for (const prefix of IMPORT_PREFIXES) {
|
|
76
|
+
if (!text.startsWith(prefix)) continue;
|
|
77
|
+
const rest = text.slice(prefix.length);
|
|
78
|
+
add(rest);
|
|
79
|
+
add(CANONICAL_PREFIX + rest);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Inverse of DSH's `encodeSegment`: safe units stay literal and every other code
|
|
86
|
+
* unit is `~XXXX`. Lets a directory name be compared against the candidate ids
|
|
87
|
+
* instead of guessing at string prefixes.
|
|
88
|
+
*/
|
|
89
|
+
export function decodeSegment(name) {
|
|
90
|
+
let out = "";
|
|
91
|
+
for (let i = 0; i < name.length; i += 1) {
|
|
92
|
+
const ch = name[i];
|
|
93
|
+
if (ch !== "~") {
|
|
94
|
+
out += ch;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
out += String.fromCharCode(parseInt(name.slice(i + 1, i + 5), 16));
|
|
98
|
+
i += 4;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
23
102
|
|
|
24
103
|
export function dshHome() {
|
|
25
104
|
const configured = process.env.DSH_HOME;
|
|
@@ -78,8 +157,20 @@ export function quarantineMetaFile(sessionId) {
|
|
|
78
157
|
return join(quarantineEntry(sessionId), "meta.json");
|
|
79
158
|
}
|
|
80
159
|
|
|
81
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Every on-disk artifact directory of one session (normally zero or one).
|
|
162
|
+
*
|
|
163
|
+
* The directory is named after the id inside the session log's header, which is
|
|
164
|
+
* NOT always the id the archive index files the row under: an imported row is
|
|
165
|
+
* indexed as `import-session-<uuid>` / `import-<uuid>` while its directory is
|
|
166
|
+
* `session-<uuid>` or the bare uuid. Looking up the raw registry id therefore
|
|
167
|
+
* found nothing, so a delete freed the projection cache and the index entry and
|
|
168
|
+
* silently left the session log on disk — the exact "fake success" this plugin
|
|
169
|
+
* exists to prevent. Match on the decoded directory name instead.
|
|
170
|
+
*/
|
|
82
171
|
export async function locateArtifacts(sessionId) {
|
|
172
|
+
const candidates = artifactIdCandidates(sessionId);
|
|
173
|
+
const wanted = new Set(candidates.map((id) => id.toLowerCase()));
|
|
83
174
|
const found = [];
|
|
84
175
|
let workspaces;
|
|
85
176
|
try {
|
|
@@ -89,8 +180,26 @@ export async function locateArtifacts(sessionId) {
|
|
|
89
180
|
}
|
|
90
181
|
for (const workspace of workspaces) {
|
|
91
182
|
if (!workspace.isDirectory()) continue;
|
|
92
|
-
const
|
|
93
|
-
|
|
183
|
+
const bucket = join(sessionsRoot(), workspace.name);
|
|
184
|
+
// The pre-encoded name is the common case; only fall back to decoding the
|
|
185
|
+
// bucket's entries when it is absent (an id that needed `encodeSegment`).
|
|
186
|
+
const direct = candidates.map((id) => join(bucket, id)).find((path) => existsSync(path));
|
|
187
|
+
if (direct !== undefined) {
|
|
188
|
+
found.push(direct);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let entries;
|
|
192
|
+
try {
|
|
193
|
+
entries = await readdir(bucket, { withFileTypes: true });
|
|
194
|
+
} catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
if (!entry.isDirectory()) continue;
|
|
199
|
+
if (!wanted.has(decodeSegment(entry.name).toLowerCase())) continue;
|
|
200
|
+
found.push(join(bucket, entry.name));
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
94
203
|
}
|
|
95
204
|
return found;
|
|
96
205
|
}
|
package/lib/host/quarantine.js
CHANGED
|
@@ -14,6 +14,7 @@ import { constants } from "node:fs";
|
|
|
14
14
|
import { access, cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import {
|
|
17
|
+
canonicalSessionId,
|
|
17
18
|
directorySize,
|
|
18
19
|
projectionCacheFile,
|
|
19
20
|
quarantineArtifactDir,
|
|
@@ -133,8 +134,13 @@ export async function restoreSession(ctx, sessionId, entry) {
|
|
|
133
134
|
const artifact = quarantineArtifactDir(sessionId);
|
|
134
135
|
const cache = quarantineCacheFile(sessionId);
|
|
135
136
|
const restored = { directories: 0, cache: false, rearchived: false };
|
|
137
|
+
// The registry id may be namespaced (`import-…`), while the log directory and
|
|
138
|
+
// the projection-cache document are named after the raw session id. Restoring
|
|
139
|
+
// under the registry id would put the directory back under a name nothing
|
|
140
|
+
// looks for again.
|
|
141
|
+
const rawId = canonicalSessionId(sessionId);
|
|
136
142
|
|
|
137
|
-
const target = entry.workspaceDir !== null ? join(entry.workspaceDir,
|
|
143
|
+
const target = entry.workspaceDir !== null ? join(entry.workspaceDir, rawId) : null;
|
|
138
144
|
if (await exists(artifact)) {
|
|
139
145
|
if (target === null) throw new Error("quarantine metadata has no workspaceDir");
|
|
140
146
|
await mkdir(dirname(target), { recursive: true });
|
|
@@ -142,7 +148,7 @@ export async function restoreSession(ctx, sessionId, entry) {
|
|
|
142
148
|
restored.directories = 1;
|
|
143
149
|
}
|
|
144
150
|
if (await exists(cache)) {
|
|
145
|
-
await movePath(cache, projectionCacheFile(
|
|
151
|
+
await movePath(cache, projectionCacheFile(rawId));
|
|
146
152
|
restored.cache = true;
|
|
147
153
|
}
|
|
148
154
|
await rm(root, { recursive: true, force: true });
|
package/lib/index.js
CHANGED
|
@@ -31,6 +31,8 @@ import { dirname } from "node:path";
|
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
33
|
SESSION_ID,
|
|
34
|
+
artifactIdCandidates,
|
|
35
|
+
canonicalSessionId,
|
|
34
36
|
directorySize,
|
|
35
37
|
dshHome,
|
|
36
38
|
legacyAggregateFile,
|
|
@@ -130,6 +132,33 @@ function archivedIds(ctx) {
|
|
|
130
132
|
return Array.isArray(ids) ? [...ids] : [];
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Ids other than the archived row that could name the *live agent* of the same
|
|
137
|
+
* session. An imported row is indexed under a namespaced id
|
|
138
|
+
* (`import-session-<uuid>` / `import-<uuid>`) while the agent table, the session
|
|
139
|
+
* store and the on-disk directory all know the bare uuid, so a running-turn
|
|
140
|
+
* check done only against the archived id would miss a live agent and delete a
|
|
141
|
+
* session out from under it.
|
|
142
|
+
*/
|
|
143
|
+
function agentIdCandidates(sessionId) {
|
|
144
|
+
return artifactIdCandidates(sessionId).filter((id) => id !== sessionId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Is any candidate id for this row a live agent running a turn? The archived id
|
|
149
|
+
* is checked by the caller (it is the form the host may broadcast), so only the
|
|
150
|
+
* remaining candidates are consulted here.
|
|
151
|
+
*/
|
|
152
|
+
function runningAlias(ctx, sessionId) {
|
|
153
|
+
return agentIdCandidates(sessionId).find((id) => isRunning(ctx, id)) ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The raw ids a delete had to look under, for an operator-facing log line. */
|
|
157
|
+
function artifactHints(sessionId) {
|
|
158
|
+
const hints = artifactIdCandidates(sessionId).filter((id) => id !== sessionId);
|
|
159
|
+
return hints.length > 0 ? `, looked up as ${hints.join(" | ")}` : "";
|
|
160
|
+
}
|
|
161
|
+
|
|
133
162
|
/**
|
|
134
163
|
* Whether this host still offers what a destructive route needs. A host whose
|
|
135
164
|
* registry lost `unarchiveSession` cannot clear the archive index, and a delete
|
|
@@ -160,9 +189,10 @@ async function stateOf(ctx) {
|
|
|
160
189
|
const dirs = await locateArtifacts(id);
|
|
161
190
|
let bytes = 0;
|
|
162
191
|
for (const dir of dirs) bytes += await directorySize(dir);
|
|
163
|
-
const
|
|
192
|
+
const rawId = canonicalSessionId(id);
|
|
193
|
+
const cacheFile = projectionCacheFile(rawId);
|
|
164
194
|
const cache = existsSync(cacheFile);
|
|
165
|
-
const document = cache ? await readCacheDocument(
|
|
195
|
+
const document = cache ? await readCacheDocument(rawId) : null;
|
|
166
196
|
items.push({
|
|
167
197
|
id,
|
|
168
198
|
archived: true,
|
|
@@ -225,7 +255,7 @@ async function sweepResidue(ctx, sessionId) {
|
|
|
225
255
|
if (archivedIds(ctx).includes(sessionId)) return;
|
|
226
256
|
if (isRunning(ctx, sessionId)) return;
|
|
227
257
|
if ((await locateArtifacts(sessionId)).length > 0) return;
|
|
228
|
-
const cacheFile = projectionCacheFile(sessionId);
|
|
258
|
+
const cacheFile = projectionCacheFile(canonicalSessionId(sessionId));
|
|
229
259
|
if (existsSync(cacheFile)) await rm(cacheFile, { force: true });
|
|
230
260
|
}
|
|
231
261
|
|
|
@@ -249,12 +279,17 @@ async function deleteOne(ctx, sessionId, mode) {
|
|
|
249
279
|
if (!hostSupportsDelete(ctx)) return refuse("no-registry", { sessionId });
|
|
250
280
|
if (!archivedIds(ctx).includes(sessionId)) return refuse("not-archived", { sessionId });
|
|
251
281
|
if (isRunning(ctx, sessionId)) return refuse("session-running", { sessionId });
|
|
282
|
+
const alias = runningAlias(ctx, sessionId);
|
|
283
|
+
if (alias !== null) return refuse("session-running", { sessionId, runningAs: alias });
|
|
252
284
|
const descendants = runningDescendants(ctx, sessionId);
|
|
253
285
|
if (descendants.length > 0) return refuse("subagent-running", { sessionId, descendants });
|
|
254
286
|
|
|
255
287
|
return withLock(sessionId, async () => {
|
|
288
|
+
// The archive index is namespaced; the log directory, the projection cache
|
|
289
|
+
// document and the host's own session store all use the raw id.
|
|
290
|
+
const rawId = canonicalSessionId(sessionId);
|
|
256
291
|
const dirs = await locateArtifacts(sessionId);
|
|
257
|
-
const cacheFile = projectionCacheFile(
|
|
292
|
+
const cacheFile = projectionCacheFile(rawId);
|
|
258
293
|
let bytes = 0;
|
|
259
294
|
for (const dir of dirs) bytes += await directorySize(dir);
|
|
260
295
|
|
|
@@ -273,7 +308,7 @@ async function deleteOne(ctx, sessionId, mode) {
|
|
|
273
308
|
// and that the panel would not even show.
|
|
274
309
|
removed = { directories: 0, bytes: 0, cache: false, parked: false };
|
|
275
310
|
} else {
|
|
276
|
-
const document = existsSync(cacheFile) ? await readCacheDocument(
|
|
311
|
+
const document = existsSync(cacheFile) ? await readCacheDocument(rawId) : null;
|
|
277
312
|
removed = await quarantineSession(sessionId, {
|
|
278
313
|
dirs,
|
|
279
314
|
cacheFile,
|
|
@@ -290,7 +325,7 @@ async function deleteOne(ctx, sessionId, mode) {
|
|
|
290
325
|
await registryOf(ctx).unarchiveSession(sessionId);
|
|
291
326
|
scheduleSweep(ctx, sessionId);
|
|
292
327
|
ctx.logger?.info?.(
|
|
293
|
-
`[dsh-archived] delete ${sessionId} (${mode}): ${removed.directories} dir(s), ${removed.bytes} bytes`,
|
|
328
|
+
`[dsh-archived] delete ${sessionId} (${mode}): ${removed.directories} dir(s), ${removed.bytes} bytes${artifactHints(sessionId)}`,
|
|
294
329
|
);
|
|
295
330
|
return { ok: true, sessionId, mode, removed, archivedSessionIds: archivedIds(ctx) };
|
|
296
331
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-archived",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|