dsh-sessions-manager 3.5.4 → 3.6.1
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/README.en.md +50 -10
- package/README.md +51 -10
- package/assets/screenshot-session-empty.png +0 -0
- package/assets/screenshot-session-multimove.png +0 -0
- package/assets/screenshot-session-settings.png +0 -0
- package/assets/screenshot-session-subagent.png +0 -0
- package/lib/client.js +890 -79
- package/lib/client.js.map +3 -3
- package/lib/index.js +887 -221
- package/lib/index.js.map +4 -4
- package/package.json +3 -2
- package/src/client/index.jsx +887 -60
- package/src/client/logic.js +79 -0
- package/src/compat/persistence.js +16 -2
- package/src/handle-era-ops.js +178 -18
- package/src/handle-era-paths.js +22 -9
- package/src/index.js +558 -125
- package/src/lineage.js +78 -0
- package/src/pending-moves.js +126 -0
- package/src/session-meta-cache.js +18 -0
- package/src/state-temp-sweep.js +42 -0
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { mkdir as
|
|
3
|
-
import { basename as
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { homedir as
|
|
2
|
+
import { mkdir as mkdir6, readFile as readFile3, readdir as readdir4, realpath, rename as rename6, rm as rm3, stat as stat4, unlink, writeFile as writeFile6 } from "node:fs/promises";
|
|
3
|
+
import { basename as basename3, dirname as dirname3, isAbsolute, join as join8 } from "node:path";
|
|
4
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
5
|
+
import { homedir as homedir4 } from "node:os";
|
|
6
6
|
|
|
7
7
|
// src/zstd-frame.js
|
|
8
8
|
import zlib from "node:zlib";
|
|
@@ -303,6 +303,138 @@ function createStarIndex(options = {}) {
|
|
|
303
303
|
return { read, write, mutate, setStarred, removeIds, indexPath, dir };
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// src/pending-moves.js
|
|
307
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
308
|
+
import { mkdir as mkdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
309
|
+
import { homedir as homedir2 } from "node:os";
|
|
310
|
+
import { join as join2 } from "node:path";
|
|
311
|
+
var SCHEMA_VERSION = 1;
|
|
312
|
+
var DEFAULT_DIR = process.env.DSH_SESSIONS_MANAGER_PENDING_DIR || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join2(homedir2(), ".dsh", "sessions-manager");
|
|
313
|
+
var MAX_ITEMS = 50;
|
|
314
|
+
var MAX_ATTEMPTS = 5;
|
|
315
|
+
function isSafeId(value) {
|
|
316
|
+
return typeof value === "string" && value.length > 0 && value.length <= 200;
|
|
317
|
+
}
|
|
318
|
+
function normalizeStore(raw) {
|
|
319
|
+
const items = [];
|
|
320
|
+
const seen = /* @__PURE__ */ new Set();
|
|
321
|
+
const source = raw && Array.isArray(raw.items) ? raw.items : [];
|
|
322
|
+
for (const item of source) {
|
|
323
|
+
if (!item || !isSafeId(item.sessionId) || typeof item.targetPath !== "string" || item.targetPath.length === 0) continue;
|
|
324
|
+
if (seen.has(item.sessionId)) continue;
|
|
325
|
+
seen.add(item.sessionId);
|
|
326
|
+
items.push({
|
|
327
|
+
sessionId: String(item.sessionId),
|
|
328
|
+
targetPath: String(item.targetPath),
|
|
329
|
+
queuedAt: Number.isFinite(item.queuedAt) ? Number(item.queuedAt) : Date.now(),
|
|
330
|
+
attempts: Number.isSafeInteger(item.attempts) && item.attempts >= 0 ? Number(item.attempts) : 0
|
|
331
|
+
});
|
|
332
|
+
if (items.length >= MAX_ITEMS) break;
|
|
333
|
+
}
|
|
334
|
+
return { schemaVersion: SCHEMA_VERSION, items };
|
|
335
|
+
}
|
|
336
|
+
function createPendingMoveStore(options = {}) {
|
|
337
|
+
const dir = options.dir || DEFAULT_DIR;
|
|
338
|
+
const indexPath = options.indexPath || join2(dir, "pending-moves.json");
|
|
339
|
+
let mutation = Promise.resolve();
|
|
340
|
+
async function read() {
|
|
341
|
+
try {
|
|
342
|
+
return normalizeStore(JSON.parse(readFileSync2(indexPath, "utf8")));
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return normalizeStore(null);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function write(store) {
|
|
348
|
+
await mkdir2(dir, { recursive: true });
|
|
349
|
+
const tmp = join2(dir, `.pending-moves-${process.pid}-${Date.now()}.tmp`);
|
|
350
|
+
await writeFile2(tmp, JSON.stringify(normalizeStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
351
|
+
await rename2(tmp, indexPath);
|
|
352
|
+
}
|
|
353
|
+
function mutate(mutator) {
|
|
354
|
+
const operation = mutation.then(async () => {
|
|
355
|
+
const store = await read();
|
|
356
|
+
const result = await mutator(store);
|
|
357
|
+
await write(store);
|
|
358
|
+
return result;
|
|
359
|
+
});
|
|
360
|
+
mutation = operation.catch(() => {
|
|
361
|
+
});
|
|
362
|
+
return operation;
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
indexPath,
|
|
366
|
+
dir,
|
|
367
|
+
list: async () => (await read()).items,
|
|
368
|
+
has: async (sessionId) => (await read()).items.some((item) => item.sessionId === String(sessionId)),
|
|
369
|
+
queue: (sessionId, targetPath) => mutate((store) => {
|
|
370
|
+
if (!isSafeId(sessionId) || typeof targetPath !== "string" || targetPath.length === 0) return null;
|
|
371
|
+
const existing = store.items.find((item2) => item2.sessionId === String(sessionId));
|
|
372
|
+
if (existing) {
|
|
373
|
+
existing.targetPath = String(targetPath);
|
|
374
|
+
existing.attempts = 0;
|
|
375
|
+
return existing;
|
|
376
|
+
}
|
|
377
|
+
const item = { sessionId: String(sessionId), targetPath: String(targetPath), queuedAt: Date.now(), attempts: 0 };
|
|
378
|
+
store.items.push(item);
|
|
379
|
+
if (store.items.length > MAX_ITEMS) store.items.splice(0, store.items.length - MAX_ITEMS);
|
|
380
|
+
return item;
|
|
381
|
+
}),
|
|
382
|
+
remove: (sessionIds) => mutate((store) => {
|
|
383
|
+
const drop = new Set((Array.isArray(sessionIds) ? sessionIds : [sessionIds]).filter(isSafeId).map(String));
|
|
384
|
+
const before = store.items.length;
|
|
385
|
+
store.items = store.items.filter((item) => !drop.has(item.sessionId));
|
|
386
|
+
return before - store.items.length;
|
|
387
|
+
}),
|
|
388
|
+
bumpAttempts: (sessionId) => mutate((store) => {
|
|
389
|
+
const item = store.items.find((entry) => entry.sessionId === String(sessionId));
|
|
390
|
+
if (!item) return null;
|
|
391
|
+
item.attempts += 1;
|
|
392
|
+
if (item.attempts >= MAX_ATTEMPTS) {
|
|
393
|
+
store.items = store.items.filter((entry) => entry.sessionId !== item.sessionId);
|
|
394
|
+
return { dropped: true, attempts: item.attempts };
|
|
395
|
+
}
|
|
396
|
+
return { dropped: false, attempts: item.attempts };
|
|
397
|
+
})
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/state-temp-sweep.js
|
|
402
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
403
|
+
import { join as join3 } from "node:path";
|
|
404
|
+
var TEMP_NAME_RE = /^\..+-\d+-\d+\.tmp$/;
|
|
405
|
+
async function sweepStaleStateTemps(dirs, options = {}) {
|
|
406
|
+
const maxAgeMs = Number.isSafeInteger(options.maxAgeMs) && options.maxAgeMs >= 0 ? options.maxAgeMs : 36e5;
|
|
407
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
408
|
+
let removed = 0;
|
|
409
|
+
for (const dir of Array.isArray(dirs) ? dirs : []) {
|
|
410
|
+
if (typeof dir !== "string" || dir.length === 0) continue;
|
|
411
|
+
let entries;
|
|
412
|
+
try {
|
|
413
|
+
entries = await readdir(dir);
|
|
414
|
+
} catch (e) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
for (const name2 of entries) {
|
|
418
|
+
if (!TEMP_NAME_RE.test(name2)) continue;
|
|
419
|
+
const path = join3(dir, name2);
|
|
420
|
+
let info;
|
|
421
|
+
try {
|
|
422
|
+
info = await stat(path);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (!info.isFile()) continue;
|
|
427
|
+
if (now - info.mtimeMs < maxAgeMs) continue;
|
|
428
|
+
try {
|
|
429
|
+
await rm(path, { force: true });
|
|
430
|
+
removed++;
|
|
431
|
+
} catch (e) {
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return removed;
|
|
436
|
+
}
|
|
437
|
+
|
|
306
438
|
// src/storage-stats.js
|
|
307
439
|
var UNGROUPED_KEY = "__ungrouped__";
|
|
308
440
|
function isFiniteSize(value) {
|
|
@@ -355,15 +487,15 @@ function aggregateStorage(items, options = {}) {
|
|
|
355
487
|
}
|
|
356
488
|
|
|
357
489
|
// src/auto-archive.js
|
|
358
|
-
import { mkdir as
|
|
359
|
-
import { readFileSync as
|
|
360
|
-
import { homedir as
|
|
361
|
-
import { join as
|
|
490
|
+
import { mkdir as mkdir3, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
|
|
491
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
492
|
+
import { homedir as homedir3 } from "node:os";
|
|
493
|
+
import { join as join4 } from "node:path";
|
|
362
494
|
var AUTO_ARCHIVE_SCHEMA_VERSION = 4;
|
|
363
495
|
var INACTIVE_DAY_OPTIONS = Object.freeze([0, 30, 60, 90]);
|
|
364
496
|
var DAY_MS = 864e5;
|
|
365
497
|
var RUN_INTERVAL_MS = DAY_MS;
|
|
366
|
-
var
|
|
498
|
+
var DEFAULT_DIR2 = join4(homedir3(), ".dsh", "sessions-manager");
|
|
367
499
|
function normalizeAutoArchiveStore(raw) {
|
|
368
500
|
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
369
501
|
const settings = source.settings && typeof source.settings === "object" ? source.settings : {};
|
|
@@ -407,21 +539,21 @@ function pickInactiveCandidates(items, options = {}) {
|
|
|
407
539
|
return out;
|
|
408
540
|
}
|
|
409
541
|
function createAutoArchiveStore(options = {}) {
|
|
410
|
-
const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR ||
|
|
411
|
-
const indexPath = options.indexPath ||
|
|
542
|
+
const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR || DEFAULT_DIR2;
|
|
543
|
+
const indexPath = options.indexPath || join4(dir, "auto-archive.json");
|
|
412
544
|
let mutation = Promise.resolve();
|
|
413
545
|
async function read() {
|
|
414
546
|
try {
|
|
415
|
-
return normalizeAutoArchiveStore(JSON.parse(
|
|
547
|
+
return normalizeAutoArchiveStore(JSON.parse(readFileSync3(indexPath, "utf8")));
|
|
416
548
|
} catch {
|
|
417
549
|
return normalizeAutoArchiveStore(null);
|
|
418
550
|
}
|
|
419
551
|
}
|
|
420
552
|
async function write(store) {
|
|
421
|
-
await
|
|
422
|
-
const tmp =
|
|
423
|
-
await
|
|
424
|
-
await
|
|
553
|
+
await mkdir3(dir, { recursive: true });
|
|
554
|
+
const tmp = join4(dir, `.auto-archive-${process.pid}-${Date.now()}.tmp`);
|
|
555
|
+
await writeFile3(tmp, JSON.stringify(normalizeAutoArchiveStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
556
|
+
await rename3(tmp, indexPath);
|
|
425
557
|
}
|
|
426
558
|
function mutate(mutator) {
|
|
427
559
|
const operation = mutation.then(async () => {
|
|
@@ -468,13 +600,13 @@ function createAutoArchiveStore(options = {}) {
|
|
|
468
600
|
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
469
601
|
var DEFAULT_MAX = 4e3;
|
|
470
602
|
var REVISION_PREFIX = "rev:";
|
|
471
|
-
function fingerprintOf(
|
|
472
|
-
if (!
|
|
473
|
-
if (typeof
|
|
474
|
-
return REVISION_PREFIX +
|
|
603
|
+
function fingerprintOf(stat5) {
|
|
604
|
+
if (!stat5 || typeof stat5 !== "object") return null;
|
|
605
|
+
if (typeof stat5.revision === "string" && stat5.revision.length > 0) {
|
|
606
|
+
return REVISION_PREFIX + stat5.revision;
|
|
475
607
|
}
|
|
476
|
-
const mtimeMs =
|
|
477
|
-
const size =
|
|
608
|
+
const mtimeMs = stat5.mtimeMs;
|
|
609
|
+
const size = stat5.size;
|
|
478
610
|
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs) || mtimeMs <= 0) return null;
|
|
479
611
|
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
480
612
|
return `${Math.floor(mtimeMs)}:${size}`;
|
|
@@ -482,9 +614,15 @@ function fingerprintOf(stat3) {
|
|
|
482
614
|
function isPersistableFingerprint(fingerprint) {
|
|
483
615
|
return typeof fingerprint === "string" && fingerprint !== "" && !fingerprint.startsWith(REVISION_PREFIX);
|
|
484
616
|
}
|
|
485
|
-
function
|
|
617
|
+
function persistFingerprintOf(stat5) {
|
|
618
|
+
const fp = fingerprintOf(stat5);
|
|
619
|
+
if (fp && isPersistableFingerprint(fp)) return fp;
|
|
620
|
+
if (stat5 && Number.isFinite(stat5.sizeBytes) && stat5.sizeBytes >= 0) return `sz:${stat5.sizeBytes}`;
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
function isFresh(entry, stat5, now, ttlMs = DEFAULT_TTL_MS) {
|
|
486
624
|
if (!entry) return false;
|
|
487
|
-
const fp = fingerprintOf(
|
|
625
|
+
const fp = fingerprintOf(stat5);
|
|
488
626
|
if (!fp) return false;
|
|
489
627
|
if (entry.fingerprint !== fp) return false;
|
|
490
628
|
if (typeof entry.at !== "number") return false;
|
|
@@ -496,8 +634,8 @@ function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAU
|
|
|
496
634
|
const missing = [];
|
|
497
635
|
for (const id of ids) {
|
|
498
636
|
const entry = cache && cache.get(String(id));
|
|
499
|
-
const
|
|
500
|
-
if (isFresh(entry,
|
|
637
|
+
const stat5 = statsById && statsById.get(String(id));
|
|
638
|
+
if (isFresh(entry, stat5, now, ttlMs) && entry && entry.meta) {
|
|
501
639
|
cached.set(String(id), entry.meta);
|
|
502
640
|
} else {
|
|
503
641
|
missing.push(String(id));
|
|
@@ -513,10 +651,10 @@ function createSessionMetaCache(opts = {}) {
|
|
|
513
651
|
let misses = 0;
|
|
514
652
|
return {
|
|
515
653
|
// 命中返回 meta,未命中/无法校验返回 null。
|
|
516
|
-
get(id,
|
|
654
|
+
get(id, stat5) {
|
|
517
655
|
const key = String(id);
|
|
518
656
|
const entry = map.get(key);
|
|
519
|
-
if (isFresh(entry,
|
|
657
|
+
if (isFresh(entry, stat5, Date.now(), ttlMs)) {
|
|
520
658
|
hits++;
|
|
521
659
|
map.delete(key);
|
|
522
660
|
map.set(key, entry);
|
|
@@ -525,9 +663,9 @@ function createSessionMetaCache(opts = {}) {
|
|
|
525
663
|
misses++;
|
|
526
664
|
return null;
|
|
527
665
|
},
|
|
528
|
-
set(id,
|
|
666
|
+
set(id, stat5, meta) {
|
|
529
667
|
if (!meta) return null;
|
|
530
|
-
const fp = fingerprintOf(
|
|
668
|
+
const fp = fingerprintOf(stat5);
|
|
531
669
|
if (!fp) return null;
|
|
532
670
|
const key = String(id);
|
|
533
671
|
map.delete(key);
|
|
@@ -562,8 +700,8 @@ function createSessionMetaCache(opts = {}) {
|
|
|
562
700
|
}
|
|
563
701
|
|
|
564
702
|
// src/title-persist-index.js
|
|
565
|
-
import { mkdir as
|
|
566
|
-
import { dirname, join as
|
|
703
|
+
import { mkdir as mkdir4, readFile, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
|
|
704
|
+
import { dirname, join as join5 } from "node:path";
|
|
567
705
|
var TITLE_INDEX_SCHEMA_VERSION = 1;
|
|
568
706
|
var MAX_ENTRIES = 2e4;
|
|
569
707
|
function normalizeEntry(raw) {
|
|
@@ -601,7 +739,7 @@ function mergeEntries(left, right) {
|
|
|
601
739
|
function createTitleIndexStore({ dir, file }) {
|
|
602
740
|
let cache = null;
|
|
603
741
|
let chain = Promise.resolve();
|
|
604
|
-
const path = file ||
|
|
742
|
+
const path = file || join5(dir, "title-index.json");
|
|
605
743
|
async function readRaw() {
|
|
606
744
|
try {
|
|
607
745
|
return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
|
|
@@ -636,10 +774,10 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
636
774
|
if (!Object.keys(right).length) return false;
|
|
637
775
|
await enqueue(async (store) => {
|
|
638
776
|
const next = mergeEntries(store, right);
|
|
639
|
-
await
|
|
640
|
-
const tmp =
|
|
641
|
-
await
|
|
642
|
-
await
|
|
777
|
+
await mkdir4(dirname(path), { recursive: true });
|
|
778
|
+
const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
|
|
779
|
+
await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
|
|
780
|
+
await rename4(tmp, path);
|
|
643
781
|
cache = next;
|
|
644
782
|
});
|
|
645
783
|
return true;
|
|
@@ -656,10 +794,10 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
656
794
|
}
|
|
657
795
|
}
|
|
658
796
|
if (!changed) return;
|
|
659
|
-
await
|
|
660
|
-
const tmp =
|
|
661
|
-
await
|
|
662
|
-
await
|
|
797
|
+
await mkdir4(dirname(path), { recursive: true });
|
|
798
|
+
const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
|
|
799
|
+
await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
|
|
800
|
+
await rename4(tmp, path);
|
|
663
801
|
});
|
|
664
802
|
return true;
|
|
665
803
|
}
|
|
@@ -667,8 +805,8 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
667
805
|
}
|
|
668
806
|
|
|
669
807
|
// src/handle-era-paths.js
|
|
670
|
-
import { readdir, stat } from "node:fs/promises";
|
|
671
|
-
import { basename, join as
|
|
808
|
+
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
809
|
+
import { basename, join as join6 } from "node:path";
|
|
672
810
|
|
|
673
811
|
// src/path-guard.js
|
|
674
812
|
function splitSegments(target) {
|
|
@@ -700,7 +838,7 @@ function pathOwnsSession(target, sid) {
|
|
|
700
838
|
}
|
|
701
839
|
|
|
702
840
|
// src/handle-era-paths.js
|
|
703
|
-
var GENERATION_LOG_RE = /^session\.v\d
|
|
841
|
+
var GENERATION_LOG_RE = /^session(\.v\d+)?\.jsonl(\.zst(d)?)?$/;
|
|
704
842
|
function isSafeChar(ch) {
|
|
705
843
|
return ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch);
|
|
706
844
|
}
|
|
@@ -741,8 +879,11 @@ function resolveSessionRoot(sp) {
|
|
|
741
879
|
return typeof root === "string" && root.length > 0 ? root : null;
|
|
742
880
|
}
|
|
743
881
|
function deriveSessionDir(root, cwd, id) {
|
|
744
|
-
const project = cwd === void 0 || cwd === null || cwd === "" ?
|
|
745
|
-
return
|
|
882
|
+
const project = cwd === void 0 || cwd === null || cwd === "" ? join6(root, "_no-cwd") : join6(root, projectKeyFor(cwd));
|
|
883
|
+
return join6(project, encodeSegmentFor(id));
|
|
884
|
+
}
|
|
885
|
+
function generationVersionOf(name2) {
|
|
886
|
+
return Number((String(name2).match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
746
887
|
}
|
|
747
888
|
async function locateSessionArtifacts(sp, header) {
|
|
748
889
|
const root = resolveSessionRoot(sp);
|
|
@@ -757,27 +898,26 @@ async function locateSessionArtifacts(sp, header) {
|
|
|
757
898
|
if (basename(sessionDir) !== encodeSegmentFor(sid)) return null;
|
|
758
899
|
let entries;
|
|
759
900
|
try {
|
|
760
|
-
const st = await
|
|
901
|
+
const st = await stat2(sessionDir);
|
|
761
902
|
if (!st.isDirectory()) return null;
|
|
762
|
-
entries = await
|
|
903
|
+
entries = await readdir2(sessionDir);
|
|
763
904
|
} catch (e) {
|
|
764
905
|
return null;
|
|
765
906
|
}
|
|
766
907
|
const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
|
|
767
908
|
if (generationFiles.length === 0) return null;
|
|
768
|
-
generationFiles.sort((a, b) =>
|
|
769
|
-
|
|
770
|
-
const vb = Number((b.match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
771
|
-
return vb - va;
|
|
772
|
-
});
|
|
773
|
-
const logPath = join4(sessionDir, generationFiles[0]);
|
|
909
|
+
generationFiles.sort((a, b) => generationVersionOf(b) - generationVersionOf(a));
|
|
910
|
+
const logPath = join6(sessionDir, generationFiles[0]);
|
|
774
911
|
if (!pathOwnsSession(logPath, sid)) return null;
|
|
775
912
|
return {
|
|
776
913
|
root,
|
|
777
|
-
projectDir:
|
|
914
|
+
projectDir: join6(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
|
|
778
915
|
sessionDir,
|
|
779
916
|
logPath,
|
|
780
|
-
generationFiles
|
|
917
|
+
generationFiles,
|
|
918
|
+
// 最高代版本号:0 = legacy `session.jsonl[.zstd]`,≥1 = `session.vN.jsonl[.zstd]`。
|
|
919
|
+
// 上层用它判断「另一个目录里的同 id 副本是不是被取代的旧代」。
|
|
920
|
+
generationVersion: generationVersionOf(generationFiles[0])
|
|
781
921
|
};
|
|
782
922
|
}
|
|
783
923
|
|
|
@@ -785,9 +925,15 @@ async function locateSessionArtifacts(sp, header) {
|
|
|
785
925
|
var DEFAULT_CHUNK = 400;
|
|
786
926
|
var MAX_CHUNKS = 2e4;
|
|
787
927
|
function normalizeReadResult(events) {
|
|
928
|
+
if (events === void 0 || events === null) return [];
|
|
788
929
|
if (Array.isArray(events)) return events;
|
|
789
|
-
if (
|
|
790
|
-
return [];
|
|
930
|
+
if (typeof events === "object" && Array.isArray(events.events)) return events.events;
|
|
931
|
+
if (typeof events[Symbol.iterator] === "function") return [...events];
|
|
932
|
+
const error = new Error(
|
|
933
|
+
"DSH \u4F1A\u8BDD\u8BFB\u53D6\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u7ED3\u6784\uFF08runtime \u63A5\u53E3\u53EF\u80FD\u5DF2\u53D8\u66F4\uFF09\uFF0C\u5DF2\u505C\u6B62\u89E3\u6790\u4EE5\u514D\u8BEF\u5224\u4E3A\u7A7A\uFF1A" + Object.prototype.toString.call(events)
|
|
934
|
+
);
|
|
935
|
+
error.code = "DSM_READ_SHAPE_UNKNOWN";
|
|
936
|
+
throw error;
|
|
791
937
|
}
|
|
792
938
|
async function closeQuietly(handle) {
|
|
793
939
|
try {
|
|
@@ -975,9 +1121,80 @@ function requireCapability(capabilities, name2) {
|
|
|
975
1121
|
}
|
|
976
1122
|
|
|
977
1123
|
// src/handle-era-ops.js
|
|
978
|
-
import { mkdir as
|
|
979
|
-
import { join as
|
|
1124
|
+
import { mkdir as mkdir5, readFile as readFile2, readdir as readdir3, rename as rename5, rm as rm2, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
|
|
1125
|
+
import { basename as basename2, dirname as dirname2, join as join7 } from "node:path";
|
|
980
1126
|
var MOVE_BATCH = 400;
|
|
1127
|
+
var REAL_LOG_RE = /^session.*\.jsonl(\.zst(d)?)?$/;
|
|
1128
|
+
var MOVE_TEMP_RE = /\.move-(backup|stage)-/;
|
|
1129
|
+
var SESSION_LOCAL_JUNK_RE = /^(\.?session\.lock.*|\.lock.*|.*\.lock|.*\.tmp|.*\.stage|.*\.bak|\.DS_Store)$/;
|
|
1130
|
+
async function cleanSiblingCopies(root, sid, keepDir, keepVersion) {
|
|
1131
|
+
let projects;
|
|
1132
|
+
try {
|
|
1133
|
+
projects = await readdir3(root, { withFileTypes: true });
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
return { removed: 0, superseded: [], real: [] };
|
|
1136
|
+
}
|
|
1137
|
+
let segment;
|
|
1138
|
+
try {
|
|
1139
|
+
segment = encodeSegmentFor(sid);
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
return { removed: 0, superseded: [], real: [] };
|
|
1142
|
+
}
|
|
1143
|
+
let removed = 0;
|
|
1144
|
+
const superseded = [];
|
|
1145
|
+
const real = [];
|
|
1146
|
+
for (const ent of projects) {
|
|
1147
|
+
if (!ent.isDirectory()) continue;
|
|
1148
|
+
const candidate = join7(root, ent.name, segment);
|
|
1149
|
+
if (keepDir && candidate === keepDir) continue;
|
|
1150
|
+
let entries;
|
|
1151
|
+
try {
|
|
1152
|
+
const st = await stat3(candidate);
|
|
1153
|
+
if (!st.isDirectory()) continue;
|
|
1154
|
+
entries = await readdir3(candidate);
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const logs = entries.filter((name2) => REAL_LOG_RE.test(name2) && !MOVE_TEMP_RE.test(name2));
|
|
1159
|
+
if (logs.length === 0) {
|
|
1160
|
+
try {
|
|
1161
|
+
await rm2(candidate, { recursive: true, force: true });
|
|
1162
|
+
removed++;
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
}
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
const highest = Math.max(...logs.map(generationVersionOf));
|
|
1168
|
+
const onlyLogsAndLocalJunk = entries.every((name2) => REAL_LOG_RE.test(name2) && !MOVE_TEMP_RE.test(name2) || SESSION_LOCAL_JUNK_RE.test(name2));
|
|
1169
|
+
if (Number.isSafeInteger(keepVersion) && highest < keepVersion && onlyLogsAndLocalJunk) {
|
|
1170
|
+
try {
|
|
1171
|
+
await rm2(candidate, { recursive: true, force: true });
|
|
1172
|
+
superseded.push(candidate);
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
real.push(candidate);
|
|
1175
|
+
}
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
real.push(candidate);
|
|
1179
|
+
}
|
|
1180
|
+
return { removed, superseded, real };
|
|
1181
|
+
}
|
|
1182
|
+
async function removeSourceDir(sessionDir) {
|
|
1183
|
+
let entries;
|
|
1184
|
+
try {
|
|
1185
|
+
entries = await readdir3(sessionDir);
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
return { cleaned: true, leftover: [] };
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
await rm2(sessionDir, { recursive: true, force: true });
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
return { cleaned: false, leftover: entries };
|
|
1193
|
+
}
|
|
1194
|
+
return { cleaned: true, leftover: [] };
|
|
1195
|
+
}
|
|
1196
|
+
var BUSY_MOVE_MESSAGE = "\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF0C\u6682\u65F6\u65E0\u6CD5\u79FB\u52A8\uFF1B\u8BF7\u91CD\u542F DSH \u540E\u518D\u8BD5\u3002";
|
|
1197
|
+
var BUSY_PURGE_MESSAGE = "\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF0C\u6682\u65F6\u65E0\u6CD5\u5F7B\u5E95\u5220\u9664\uFF1B\u8BF7\u91CD\u542F DSH \u540E\u518D\u8BD5\u3002";
|
|
981
1198
|
function conflictError(message) {
|
|
982
1199
|
const error = new Error(message);
|
|
983
1200
|
error.status = 409;
|
|
@@ -1004,7 +1221,7 @@ async function ensureNoActiveWriter(sp, sid) {
|
|
|
1004
1221
|
try {
|
|
1005
1222
|
handle = await sp.open(sid, "write");
|
|
1006
1223
|
} catch (e) {
|
|
1007
|
-
if (isAlreadyOwned(e)) throw conflictError(
|
|
1224
|
+
if (isAlreadyOwned(e)) throw conflictError(BUSY_MOVE_MESSAGE);
|
|
1008
1225
|
throw e;
|
|
1009
1226
|
}
|
|
1010
1227
|
await closeQuietly2(handle);
|
|
@@ -1020,13 +1237,13 @@ async function purgeSessionArtifacts(sp, sid, header) {
|
|
|
1020
1237
|
try {
|
|
1021
1238
|
writer = await sp.open(sid, "write");
|
|
1022
1239
|
} catch (e) {
|
|
1023
|
-
if (isAlreadyOwned(e)) throw conflictError(
|
|
1240
|
+
if (isAlreadyOwned(e)) throw conflictError(BUSY_PURGE_MESSAGE);
|
|
1024
1241
|
throw e;
|
|
1025
1242
|
}
|
|
1026
1243
|
await closeQuietly2(writer);
|
|
1027
1244
|
writer = null;
|
|
1028
1245
|
try {
|
|
1029
|
-
await
|
|
1246
|
+
await rm2(artifacts.sessionDir, { recursive: true, force: true });
|
|
1030
1247
|
} catch (e) {
|
|
1031
1248
|
const error = new Error("\u5220\u9664\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1032
1249
|
error.status = 500;
|
|
@@ -1042,8 +1259,8 @@ async function purgeSessionArtifacts(sp, sid, header) {
|
|
|
1042
1259
|
}
|
|
1043
1260
|
return artifacts;
|
|
1044
1261
|
}
|
|
1045
|
-
async function relocateRewrittenBackup({ sid, canonical,
|
|
1046
|
-
const original = await readFile2(
|
|
1262
|
+
async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts }) {
|
|
1263
|
+
const original = await readFile2(backupLogPath);
|
|
1047
1264
|
const frames = scanZstdFrames(original).frames;
|
|
1048
1265
|
if (frames.length === 0) throw new Error("\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574 zstd \u5E27");
|
|
1049
1266
|
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
@@ -1053,26 +1270,66 @@ async function relocateRewrittenBackup({ sid, canonical, backupPath, artifacts }
|
|
|
1053
1270
|
throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
1054
1271
|
}
|
|
1055
1272
|
const targetDir = deriveSessionDir(artifacts.root, canonical, sid);
|
|
1056
|
-
await
|
|
1057
|
-
const staged =
|
|
1058
|
-
await
|
|
1059
|
-
await
|
|
1273
|
+
await mkdir5(targetDir, { recursive: true });
|
|
1274
|
+
const staged = join7(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
|
|
1275
|
+
await writeFile5(staged, rewritten, { mode: 384 });
|
|
1276
|
+
await rename5(staged, join7(targetDir, artifacts.generationFiles[0]));
|
|
1060
1277
|
return targetDir;
|
|
1061
1278
|
}
|
|
1062
1279
|
async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
|
|
1280
|
+
let probeError = null;
|
|
1281
|
+
try {
|
|
1282
|
+
await ensureNoActiveWriter(sp, sid);
|
|
1283
|
+
} catch (e) {
|
|
1284
|
+
if (e && e.status === 409) throw e;
|
|
1285
|
+
probeError = e;
|
|
1286
|
+
}
|
|
1063
1287
|
const artifacts = await locateSessionArtifacts(sp, header);
|
|
1064
1288
|
if (!artifacts) {
|
|
1065
1289
|
const error = new Error("\u65E0\u6CD5\u5B9A\u4F4D\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8");
|
|
1066
1290
|
error.status = 409;
|
|
1067
1291
|
throw error;
|
|
1068
1292
|
}
|
|
1069
|
-
|
|
1293
|
+
if (probeError) {
|
|
1294
|
+
const error = new Error("\u65E0\u6CD5\u786E\u8BA4\u8BE5\u4F1A\u8BDD\u662F\u5426\u6B63\u88AB\u5199\u5165\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8\uFF1A" + String(probeError && probeError.message || probeError));
|
|
1295
|
+
error.status = 409;
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
const HEADER_ONLY_MAX = 4096;
|
|
1299
|
+
if (events.length === 0) {
|
|
1300
|
+
let sourceBytes = null;
|
|
1301
|
+
try {
|
|
1302
|
+
sourceBytes = (await stat3(artifacts.logPath)).size;
|
|
1303
|
+
} catch (e) {
|
|
1304
|
+
sourceBytes = null;
|
|
1305
|
+
}
|
|
1306
|
+
if (sourceBytes !== null && sourceBytes > HEADER_ONLY_MAX) {
|
|
1307
|
+
const error = new Error(
|
|
1308
|
+
`\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u6E90\u65E5\u5FD7\u6709 ${sourceBytes} \u5B57\u8282\u4F46\u8BFB\u5230 0 \u6761\u4E8B\u4EF6\uFF08\u8BFB\u53D6\u94FE\u8DEF\u5F02\u5E38\uFF09\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8\u4EE5\u514D\u4E22\u5931\u4F1A\u8BDD\u5185\u5BB9\u3002`
|
|
1309
|
+
);
|
|
1310
|
+
error.status = 409;
|
|
1311
|
+
error.code = "DSM_MOVE_EMPTY_READ";
|
|
1312
|
+
throw error;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const siblings = await cleanSiblingCopies(artifacts.root, sid, artifacts.sessionDir, artifacts.generationVersion);
|
|
1316
|
+
if (siblings.real.length > 0) {
|
|
1317
|
+
const error = new Error(
|
|
1318
|
+
"\u65E0\u6CD5\u79FB\u52A8\uFF1A\u5728\u5176\u4ED6\u5DE5\u4F5C\u533A\u76EE\u5F55\u53D1\u73B0\u540C\u4E00\u4F1A\u8BDD\u7684\u65E5\u5FD7\u526F\u672C\uFF0C\u8BF7\u5148\u786E\u8BA4\u4FDD\u7559\u54EA\u4E00\u4EFD\uFF1A\n" + siblings.real.map((p) => "\xB7 " + p).join("\n")
|
|
1319
|
+
);
|
|
1320
|
+
error.status = 409;
|
|
1321
|
+
error.code = "DSM_SESSION_DUP_LOG";
|
|
1322
|
+
throw error;
|
|
1323
|
+
}
|
|
1070
1324
|
const newHeader = Object.assign({}, header, { cwd: canonical });
|
|
1071
1325
|
const firstSeq = events.length ? Number(events[0].seq) : 0;
|
|
1072
1326
|
const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
|
|
1073
1327
|
const createOptions = header.isSeeded && Number.isSafeInteger(inheritedEventCount) && inheritedEventCount > 0 ? { inheritedEventCount } : void 0;
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1328
|
+
const backupRoot = join7(dirname2(artifacts.root), "sessions-manager-move-backup");
|
|
1329
|
+
const backupDir = join7(backupRoot, `${encodeSegmentFor(sid)}-${process.pid}-${Date.now()}`);
|
|
1330
|
+
await mkdir5(backupRoot, { recursive: true });
|
|
1331
|
+
const backupLogPath = join7(backupDir, basename2(artifacts.logPath));
|
|
1332
|
+
await rename5(artifacts.sessionDir, backupDir);
|
|
1076
1333
|
let writer = null;
|
|
1077
1334
|
try {
|
|
1078
1335
|
try {
|
|
@@ -1085,7 +1342,7 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1085
1342
|
writer = null;
|
|
1086
1343
|
} catch (e) {
|
|
1087
1344
|
if (isAlreadyExists(e)) {
|
|
1088
|
-
await relocateRewrittenBackup({ sid, canonical,
|
|
1345
|
+
await relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts });
|
|
1089
1346
|
} else {
|
|
1090
1347
|
throw e;
|
|
1091
1348
|
}
|
|
@@ -1101,11 +1358,15 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1101
1358
|
} catch (e) {
|
|
1102
1359
|
await closeQuietly2(writer);
|
|
1103
1360
|
try {
|
|
1104
|
-
await
|
|
1361
|
+
await rm2(deriveSessionDir(artifacts.root, canonical, sid), { recursive: true, force: true });
|
|
1362
|
+
} catch (_) {
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
await mkdir5(dirname2(artifacts.sessionDir), { recursive: true });
|
|
1105
1366
|
} catch (_) {
|
|
1106
1367
|
}
|
|
1107
1368
|
try {
|
|
1108
|
-
await
|
|
1369
|
+
await rename5(backupDir, artifacts.sessionDir);
|
|
1109
1370
|
} catch (_) {
|
|
1110
1371
|
}
|
|
1111
1372
|
if (e && e.status) throw e;
|
|
@@ -1113,19 +1374,53 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1113
1374
|
error.status = 500;
|
|
1114
1375
|
throw error;
|
|
1115
1376
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1377
|
+
const sourceCleanup = await removeSourceDir(backupDir);
|
|
1378
|
+
return {
|
|
1379
|
+
sessionDir: deriveSessionDir(artifacts.root, canonical, sid),
|
|
1380
|
+
sourceCleanup,
|
|
1381
|
+
reclaimedSiblings: siblings.superseded
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/lineage.js
|
|
1386
|
+
var EMPTY_BASE = 190;
|
|
1387
|
+
function isEmptyLogSize(sizeBytes, cwd) {
|
|
1388
|
+
if (!Number.isFinite(sizeBytes) || sizeBytes < 0) return false;
|
|
1389
|
+
const cwdLen = typeof cwd === "string" ? cwd.length : 0;
|
|
1390
|
+
return sizeBytes <= EMPTY_BASE + cwdLen;
|
|
1391
|
+
}
|
|
1392
|
+
var EMPTY_DECODE_LIMIT = 8192;
|
|
1393
|
+
var LIFECYCLE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1394
|
+
"session",
|
|
1395
|
+
"permission/preset",
|
|
1396
|
+
"sandbox/mode",
|
|
1397
|
+
"approval/policy"
|
|
1398
|
+
]);
|
|
1399
|
+
function isEmptyEventTypes(types) {
|
|
1400
|
+
if (!Array.isArray(types)) return false;
|
|
1401
|
+
for (const t of types) {
|
|
1402
|
+
if (typeof t !== "string" || !LIFECYCLE_EVENT_TYPES.has(t)) return false;
|
|
1403
|
+
}
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1406
|
+
function classifyLineage(header, sizeBytes) {
|
|
1407
|
+
if (!header || typeof header !== "object") return null;
|
|
1408
|
+
const origin = header.origin === "subagent" ? "subagent" : null;
|
|
1409
|
+
const parentSession = typeof header.parentSession === "string" && header.parentSession ? header.parentSession : null;
|
|
1410
|
+
const delegationDepth = Number.isSafeInteger(header.delegationDepth) && header.delegationDepth > 0 ? header.delegationDepth : 0;
|
|
1411
|
+
const empty = isEmptyLogSize(sizeBytes, header.cwd);
|
|
1412
|
+
if (!origin && !parentSession && !empty) return null;
|
|
1413
|
+
return { origin, parentSession, delegationDepth, empty };
|
|
1121
1414
|
}
|
|
1122
1415
|
|
|
1123
1416
|
// src/index.js
|
|
1417
|
+
var BUILD_STAMP = true ? "3.6.1+f0814a9e" : "dev";
|
|
1124
1418
|
var name = "dsh-sessions-manager";
|
|
1125
1419
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
1126
1420
|
var MAX_TITLE = 80;
|
|
1127
|
-
var
|
|
1128
|
-
var
|
|
1421
|
+
var STATE_DIR = process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join8(homedir4(), ".dsh", "sessions-manager");
|
|
1422
|
+
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join8(homedir4(), ".dsh", "sessions-manager-trash");
|
|
1423
|
+
var TRASH_INDEX = join8(TRASH_DIR, "index.json");
|
|
1129
1424
|
var TRASH_SCHEMA_VERSION = 2;
|
|
1130
1425
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
1131
1426
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
@@ -1227,7 +1522,7 @@ function apply(ctx) {
|
|
|
1227
1522
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
1228
1523
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
1229
1524
|
const metaCache = createSessionMetaCache();
|
|
1230
|
-
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file:
|
|
1525
|
+
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join8(TRASH_DIR, "title-index.json") });
|
|
1231
1526
|
async function hydrateFromPersist(ids, statsById) {
|
|
1232
1527
|
const hits = /* @__PURE__ */ new Map();
|
|
1233
1528
|
if (!ids || !ids.length) return hits;
|
|
@@ -1238,29 +1533,31 @@ function apply(ctx) {
|
|
|
1238
1533
|
return hits;
|
|
1239
1534
|
}
|
|
1240
1535
|
for (const id of ids) {
|
|
1241
|
-
const
|
|
1536
|
+
const stat5 = statsById.get(id);
|
|
1242
1537
|
const entry = store && store[id];
|
|
1243
|
-
if (!
|
|
1244
|
-
const fp =
|
|
1245
|
-
if (!
|
|
1538
|
+
if (!stat5 || !entry) continue;
|
|
1539
|
+
const fp = persistFingerprintOf(stat5);
|
|
1540
|
+
if (!fp) continue;
|
|
1246
1541
|
if (fp && entry.fingerprint === fp) {
|
|
1247
1542
|
hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
|
|
1248
1543
|
}
|
|
1249
1544
|
}
|
|
1250
1545
|
return hits;
|
|
1251
1546
|
}
|
|
1252
|
-
function persistDecoded(decoded, statsById) {
|
|
1547
|
+
async function persistDecoded(decoded, statsById) {
|
|
1253
1548
|
if (!decoded || !decoded.size) return;
|
|
1254
1549
|
const batch = {};
|
|
1255
1550
|
const now = Date.now();
|
|
1256
1551
|
for (const [id, meta] of decoded) {
|
|
1257
|
-
const fp =
|
|
1258
|
-
if (!
|
|
1552
|
+
const fp = persistFingerprintOf(statsById.get(id));
|
|
1553
|
+
if (!fp) continue;
|
|
1259
1554
|
batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
|
|
1260
1555
|
}
|
|
1261
1556
|
if (!Object.keys(batch).length) return;
|
|
1262
|
-
|
|
1263
|
-
|
|
1557
|
+
try {
|
|
1558
|
+
await titleIndex.merge(batch);
|
|
1559
|
+
} catch (e) {
|
|
1560
|
+
}
|
|
1264
1561
|
}
|
|
1265
1562
|
function metaFromSnapshot(o) {
|
|
1266
1563
|
let title = null, createdAt = null, cwd = null;
|
|
@@ -1332,50 +1629,63 @@ function apply(ctx) {
|
|
|
1332
1629
|
}
|
|
1333
1630
|
return base;
|
|
1334
1631
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1632
|
+
const EMPTY_META = { title: null, cwd: null, createdAt: null };
|
|
1633
|
+
const WARM_CHUNK = 4;
|
|
1634
|
+
const warmQueue = /* @__PURE__ */ new Map();
|
|
1635
|
+
let warmRunning = false;
|
|
1636
|
+
let warmKickTimer = null;
|
|
1637
|
+
function scheduleWarm() {
|
|
1638
|
+
if (warmKickTimer || warmRunning) return;
|
|
1639
|
+
warmKickTimer = setTimeout(() => {
|
|
1640
|
+
warmKickTimer = null;
|
|
1641
|
+
runWarm();
|
|
1642
|
+
}, 25);
|
|
1643
|
+
if (typeof warmKickTimer.unref === "function") warmKickTimer.unref();
|
|
1644
|
+
}
|
|
1645
|
+
function enqueueWarm(ids, statsById, entryById) {
|
|
1646
|
+
if (!ids || !ids.length) return;
|
|
1647
|
+
for (const id of ids) {
|
|
1648
|
+
const key = String(id);
|
|
1649
|
+
const entry = entryById ? entryById.get(key) : null;
|
|
1650
|
+
warmQueue.set(key, {
|
|
1651
|
+
stat: statsById ? statsById.get(key) || null : null,
|
|
1652
|
+
header: entry && entry.header || null
|
|
1653
|
+
});
|
|
1358
1654
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1655
|
+
scheduleWarm();
|
|
1656
|
+
}
|
|
1657
|
+
async function runWarm() {
|
|
1658
|
+
if (warmRunning) return;
|
|
1659
|
+
warmRunning = true;
|
|
1660
|
+
try {
|
|
1661
|
+
while (warmQueue.size) {
|
|
1662
|
+
const batch = [...warmQueue.entries()].slice(0, WARM_CHUNK);
|
|
1663
|
+
for (const [id] of batch) warmQueue.delete(id);
|
|
1664
|
+
try {
|
|
1665
|
+
const ids = batch.map(([id]) => id);
|
|
1666
|
+
const snapshotById = await projectTitles(ids);
|
|
1667
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
1668
|
+
const statsById = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const [id, desc] of batch) {
|
|
1670
|
+
const snapshot = snapshotById.has(id) ? snapshotById.get(id) : null;
|
|
1671
|
+
const meta = metaFromSnapshot(snapshot);
|
|
1672
|
+
if (desc.header) {
|
|
1673
|
+
if (typeof desc.header.cwd === "string") meta.cwd = desc.header.cwd;
|
|
1674
|
+
if (desc.header.createdAt != null) meta.createdAt = desc.header.createdAt;
|
|
1675
|
+
}
|
|
1676
|
+
metaCache.set(id, desc.stat, meta);
|
|
1677
|
+
statsById.set(id, desc.stat);
|
|
1678
|
+
if (desc.stat) decoded.set(id, meta);
|
|
1366
1679
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
if (!meta.cwd) meta.cwd = summary.meta.cwd || null;
|
|
1370
|
-
if (!meta.createdAt) meta.createdAt = summary.meta.createdAt || null;
|
|
1680
|
+
await persistDecoded(decoded, statsById);
|
|
1681
|
+
} catch (e) {
|
|
1371
1682
|
}
|
|
1372
|
-
|
|
1373
|
-
} catch (e2) {
|
|
1683
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1374
1684
|
}
|
|
1685
|
+
} finally {
|
|
1686
|
+
warmRunning = false;
|
|
1687
|
+
if (warmQueue.size) scheduleWarm();
|
|
1375
1688
|
}
|
|
1376
|
-
metaCache.set(key, statInfo, meta);
|
|
1377
|
-
if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
|
|
1378
|
-
return buildItem(key, meta, usage, opts.exposeUsage);
|
|
1379
1689
|
}
|
|
1380
1690
|
async function collectUsage(preloadedEntries) {
|
|
1381
1691
|
const sizeById = /* @__PURE__ */ new Map();
|
|
@@ -1399,14 +1709,14 @@ function apply(ctx) {
|
|
|
1399
1709
|
if (!id) return;
|
|
1400
1710
|
if (entry && typeof entry.revision === "string" && entry.revision) {
|
|
1401
1711
|
if (Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
|
|
1402
|
-
statsById.set(id, { revision: entry.revision });
|
|
1712
|
+
statsById.set(id, Number.isFinite(entry.sizeBytes) ? { revision: entry.revision, sizeBytes: Number(entry.sizeBytes) } : { revision: entry.revision });
|
|
1403
1713
|
return;
|
|
1404
1714
|
}
|
|
1405
1715
|
if (entry && Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
|
|
1406
1716
|
try {
|
|
1407
1717
|
const loc = persistence.locate(header);
|
|
1408
1718
|
if (!loc || typeof loc.path !== "string" || !loc.path) return;
|
|
1409
|
-
const st = await
|
|
1719
|
+
const st = await stat4(loc.path);
|
|
1410
1720
|
if (!st) return;
|
|
1411
1721
|
if (typeof st.size === "number") sizeById.set(id, st.size);
|
|
1412
1722
|
if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) {
|
|
@@ -1437,7 +1747,7 @@ function apply(ctx) {
|
|
|
1437
1747
|
}
|
|
1438
1748
|
async function readTrashStore() {
|
|
1439
1749
|
try {
|
|
1440
|
-
return normalizeTrashStore(JSON.parse(
|
|
1750
|
+
return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
|
|
1441
1751
|
} catch (e) {
|
|
1442
1752
|
return normalizeTrashStore(null);
|
|
1443
1753
|
}
|
|
@@ -1446,10 +1756,10 @@ function apply(ctx) {
|
|
|
1446
1756
|
return (await readTrashStore()).items;
|
|
1447
1757
|
}
|
|
1448
1758
|
async function writeTrashStore(store) {
|
|
1449
|
-
await
|
|
1450
|
-
const tmp =
|
|
1451
|
-
await
|
|
1452
|
-
await
|
|
1759
|
+
await mkdir6(TRASH_DIR, { recursive: true });
|
|
1760
|
+
const tmp = join8(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
|
|
1761
|
+
await writeFile6(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
1762
|
+
await rename6(tmp, TRASH_INDEX);
|
|
1453
1763
|
}
|
|
1454
1764
|
function mutateTrash(mutator) {
|
|
1455
1765
|
const operation = trashMutation.then(async () => {
|
|
@@ -1463,7 +1773,10 @@ function apply(ctx) {
|
|
|
1463
1773
|
return operation;
|
|
1464
1774
|
}
|
|
1465
1775
|
const stars = createStarIndex();
|
|
1776
|
+
const pendingMoves = createPendingMoveStore();
|
|
1466
1777
|
const autoArchive = createAutoArchiveStore();
|
|
1778
|
+
sweepStaleStateTemps([STATE_DIR, TRASH_DIR], {}).catch(() => {
|
|
1779
|
+
});
|
|
1467
1780
|
async function gcStars(validIds) {
|
|
1468
1781
|
try {
|
|
1469
1782
|
const store = await stars.read();
|
|
@@ -1550,10 +1863,10 @@ function apply(ctx) {
|
|
|
1550
1863
|
let exists = false;
|
|
1551
1864
|
let header = null;
|
|
1552
1865
|
try {
|
|
1553
|
-
const
|
|
1554
|
-
if (
|
|
1866
|
+
const stat5 = await persistence.statSession(sid);
|
|
1867
|
+
if (stat5) {
|
|
1555
1868
|
exists = true;
|
|
1556
|
-
header =
|
|
1869
|
+
header = stat5.header;
|
|
1557
1870
|
}
|
|
1558
1871
|
} catch (e) {
|
|
1559
1872
|
}
|
|
@@ -1584,7 +1897,7 @@ function apply(ctx) {
|
|
|
1584
1897
|
if (loc && typeof loc.path === "string") originalPath = loc.path;
|
|
1585
1898
|
}
|
|
1586
1899
|
if (originalPath) {
|
|
1587
|
-
const existsOnDisk = await
|
|
1900
|
+
const existsOnDisk = await stat4(originalPath).then(() => true).catch(() => false);
|
|
1588
1901
|
if (!existsOnDisk) {
|
|
1589
1902
|
return { ok: false, status: 409, code: "DSM_SESSION_LOG_MISSING", message: "\u56DE\u6536\u7AD9\u7D22\u5F15\u4ECD\u8BB0\u5F55\u8BE5\u4F1A\u8BDD\uFF0C\u4F46\u5176\u65E5\u5FD7\u6587\u4EF6\u5DF2\u6D88\u5931\uFF08\u53EF\u80FD\u88AB\u5916\u90E8\u79FB\u52A8\u6216\u5220\u9664\uFF09" };
|
|
1590
1903
|
}
|
|
@@ -1697,7 +2010,7 @@ function apply(ctx) {
|
|
|
1697
2010
|
await purgeSessionArtifacts(sp, sid, locatedHeader);
|
|
1698
2011
|
} else if (target) {
|
|
1699
2012
|
try {
|
|
1700
|
-
await
|
|
2013
|
+
await unlink(target);
|
|
1701
2014
|
} catch (e) {
|
|
1702
2015
|
if (e && e.code !== "ENOENT") throw new Error("\u5220\u9664\u6587\u4EF6\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1703
2016
|
}
|
|
@@ -1730,8 +2043,10 @@ function apply(ctx) {
|
|
|
1730
2043
|
purged = true;
|
|
1731
2044
|
});
|
|
1732
2045
|
if (!purged) throw new Error("\u5F7B\u5E95\u5220\u9664\u5931\u8D25");
|
|
1733
|
-
|
|
1734
|
-
|
|
2046
|
+
try {
|
|
2047
|
+
await stars.removeIds([sid]);
|
|
2048
|
+
} catch (e) {
|
|
2049
|
+
}
|
|
1735
2050
|
return { ok: true, purged: true };
|
|
1736
2051
|
}
|
|
1737
2052
|
async function trashSettings(next) {
|
|
@@ -1767,8 +2082,8 @@ function apply(ctx) {
|
|
|
1767
2082
|
async function moveTargetWorkspace(rawPath) {
|
|
1768
2083
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
1769
2084
|
let p = String(rawPath).trim();
|
|
1770
|
-
if (p.startsWith("~/")) p =
|
|
1771
|
-
if (!isAbsolute(p)) p =
|
|
2085
|
+
if (p.startsWith("~/")) p = join8(homedir4(), p.slice(2));
|
|
2086
|
+
if (!isAbsolute(p)) p = join8(homedir4(), p);
|
|
1772
2087
|
let canonical = null;
|
|
1773
2088
|
try {
|
|
1774
2089
|
canonical = await realpath(p);
|
|
@@ -1776,16 +2091,19 @@ function apply(ctx) {
|
|
|
1776
2091
|
canonical = null;
|
|
1777
2092
|
}
|
|
1778
2093
|
if (canonical === null) {
|
|
1779
|
-
await
|
|
2094
|
+
await mkdir6(p, { recursive: true });
|
|
1780
2095
|
canonical = await realpath(p);
|
|
1781
2096
|
}
|
|
1782
|
-
return { canonical, entity: await w.create(canonical,
|
|
2097
|
+
return { canonical, entity: await w.create(canonical, basename3(canonical) || "workspace") };
|
|
1783
2098
|
}
|
|
1784
2099
|
async function moveOne(sid, targetPath) {
|
|
1785
2100
|
requireCapability(capabilities, "move");
|
|
1786
2101
|
const activeId = getActiveSessionId(ctx);
|
|
1787
2102
|
if (activeId != null && String(activeId) === String(sid)) {
|
|
1788
|
-
|
|
2103
|
+
const error = new Error("\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF0C\u6682\u65F6\u65E0\u6CD5\u79FB\u52A8\uFF1B\u8BF7\u91CD\u542F DSH \u540E\u518D\u8BD5\u3002");
|
|
2104
|
+
error.status = 409;
|
|
2105
|
+
error.code = "DSM_SESSION_BUSY";
|
|
2106
|
+
throw error;
|
|
1789
2107
|
}
|
|
1790
2108
|
const r = await persistence.readSession(sid, 0);
|
|
1791
2109
|
if (!r || !r.meta) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
|
|
@@ -1805,6 +2123,7 @@ function apply(ctx) {
|
|
|
1805
2123
|
}
|
|
1806
2124
|
}
|
|
1807
2125
|
const newHeader = Object.assign({}, meta, { cwd: canonical });
|
|
2126
|
+
const moveNotes = [];
|
|
1808
2127
|
const live = ctx.get("sessions");
|
|
1809
2128
|
const liveObj = live && live.get && live.get(sid);
|
|
1810
2129
|
const isOpen = !!liveObj;
|
|
@@ -1817,14 +2136,14 @@ function apply(ctx) {
|
|
|
1817
2136
|
const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
|
|
1818
2137
|
let destinationInstalled = false;
|
|
1819
2138
|
try {
|
|
1820
|
-
await
|
|
2139
|
+
await mkdir6(dirname3(newPath), { recursive: true });
|
|
1821
2140
|
try {
|
|
1822
|
-
await
|
|
2141
|
+
await stat4(newPath);
|
|
1823
2142
|
throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1824
2143
|
} catch (e) {
|
|
1825
2144
|
if (e && e.code !== "ENOENT") throw e;
|
|
1826
2145
|
}
|
|
1827
|
-
await
|
|
2146
|
+
await rename6(oldPath, backupPath);
|
|
1828
2147
|
const original = await readFile3(backupPath);
|
|
1829
2148
|
const originalFrames = scanZstdFrames(original).frames;
|
|
1830
2149
|
if (originalFrames.length === 0) throw new Error("\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574 zstd \u5E27");
|
|
@@ -1834,23 +2153,25 @@ function apply(ctx) {
|
|
|
1834
2153
|
const originalTail = original.subarray(originalFrames[0].end);
|
|
1835
2154
|
const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
|
|
1836
2155
|
if (!originalTail.equals(rewrittenTail)) throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
1837
|
-
await
|
|
1838
|
-
await
|
|
2156
|
+
await writeFile6(stagedPath, rewritten, { mode: 384 });
|
|
2157
|
+
await rename6(stagedPath, newPath);
|
|
1839
2158
|
destinationInstalled = true;
|
|
1840
|
-
await
|
|
2159
|
+
await unlink(backupPath);
|
|
2160
|
+
const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
|
|
2161
|
+
if (keptInSource.length > 0) moveNotes.push(`\u6E90\u76EE\u5F55\u672A\u80FD\u6E05\u7406\u5E72\u51C0\uFF08\u6B8B\u7559 ${keptInSource.join("\u3001")}\uFF09\uFF0C\u82E5\u540E\u7EED\u79FB\u52A8\u62A5\u201Cduplicate\u201D\u8BF7\u624B\u52A8\u6E05\u7A7A\u8BE5\u76EE\u5F55\u3002`);
|
|
1841
2162
|
} catch (e) {
|
|
1842
2163
|
try {
|
|
1843
|
-
await
|
|
2164
|
+
await unlink(stagedPath);
|
|
1844
2165
|
} catch (_) {
|
|
1845
2166
|
}
|
|
1846
2167
|
if (destinationInstalled) {
|
|
1847
2168
|
try {
|
|
1848
|
-
await
|
|
2169
|
+
await unlink(newPath);
|
|
1849
2170
|
} catch (_) {
|
|
1850
2171
|
}
|
|
1851
2172
|
}
|
|
1852
2173
|
try {
|
|
1853
|
-
await
|
|
2174
|
+
await rename6(backupPath, oldPath);
|
|
1854
2175
|
} catch (_) {
|
|
1855
2176
|
}
|
|
1856
2177
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -1874,6 +2195,20 @@ function apply(ctx) {
|
|
|
1874
2195
|
}
|
|
1875
2196
|
return null;
|
|
1876
2197
|
};
|
|
2198
|
+
const cleanupMovedSourceDir = async (dir) => {
|
|
2199
|
+
let entries = null;
|
|
2200
|
+
try {
|
|
2201
|
+
entries = await readdir4(dir);
|
|
2202
|
+
} catch (e) {
|
|
2203
|
+
return [];
|
|
2204
|
+
}
|
|
2205
|
+
try {
|
|
2206
|
+
await rm3(dir, { recursive: true, force: true });
|
|
2207
|
+
} catch (e) {
|
|
2208
|
+
return entries;
|
|
2209
|
+
}
|
|
2210
|
+
return [];
|
|
2211
|
+
};
|
|
1877
2212
|
if (persistence.kind === "session-handle") {
|
|
1878
2213
|
const stat1 = await persistence.statSession(sid);
|
|
1879
2214
|
if (stat1 && stat1.revision) {
|
|
@@ -1883,7 +2218,13 @@ function apply(ctx) {
|
|
|
1883
2218
|
}
|
|
1884
2219
|
}
|
|
1885
2220
|
try {
|
|
1886
|
-
await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
|
|
2221
|
+
const movedHandle = await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
|
|
2222
|
+
if (movedHandle && Array.isArray(movedHandle.reclaimedSiblings) && movedHandle.reclaimedSiblings.length > 0) {
|
|
2223
|
+
moveNotes.push(`\u5DF2\u987A\u5E26\u56DE\u6536 ${movedHandle.reclaimedSiblings.length} \u5904\u88AB\u53D6\u4EE3\u7684\u65E7\u4EE3\u65E5\u5FD7\u526F\u672C\uFF08\u5386\u53F2\u79FB\u52A8\u6B8B\u7559\uFF09\uFF0C\u907F\u514D\u540C\u4E00\u4F1A\u8BDD\u5728\u591A\u76EE\u5F55\u91CD\u590D\u3002`);
|
|
2224
|
+
}
|
|
2225
|
+
if (movedHandle && movedHandle.sourceCleanup && movedHandle.sourceCleanup.cleaned === false) {
|
|
2226
|
+
moveNotes.push(`\u6E90\u76EE\u5F55\u672A\u80FD\u6E05\u7406\u5E72\u51C0\uFF08\u6B8B\u7559 ${movedHandle.sourceCleanup.leftover.join("\u3001")}\uFF09\uFF0C\u82E5\u540E\u7EED\u79FB\u52A8\u62A5\u201Cduplicate\u201D\u8BF7\u624B\u52A8\u6E05\u7A7A\u8BE5\u76EE\u5F55\u3002`);
|
|
2227
|
+
}
|
|
1887
2228
|
} catch (e) {
|
|
1888
2229
|
if (e && e.status) throw e;
|
|
1889
2230
|
throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
@@ -1910,7 +2251,7 @@ function apply(ctx) {
|
|
|
1910
2251
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
1911
2252
|
if (backupPath) {
|
|
1912
2253
|
try {
|
|
1913
|
-
await
|
|
2254
|
+
await rename6(oldPath, backupPath);
|
|
1914
2255
|
} catch (e) {
|
|
1915
2256
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1916
2257
|
}
|
|
@@ -1918,7 +2259,7 @@ function apply(ctx) {
|
|
|
1918
2259
|
const restore = async () => {
|
|
1919
2260
|
if (backupPath) {
|
|
1920
2261
|
try {
|
|
1921
|
-
await
|
|
2262
|
+
await rename6(backupPath, oldPath);
|
|
1922
2263
|
} catch (_) {
|
|
1923
2264
|
}
|
|
1924
2265
|
}
|
|
@@ -1932,10 +2273,14 @@ function apply(ctx) {
|
|
|
1932
2273
|
}
|
|
1933
2274
|
if (backupPath) {
|
|
1934
2275
|
try {
|
|
1935
|
-
await
|
|
2276
|
+
await unlink(backupPath);
|
|
1936
2277
|
} catch (e) {
|
|
1937
2278
|
}
|
|
1938
2279
|
}
|
|
2280
|
+
if (oldPath) {
|
|
2281
|
+
const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
|
|
2282
|
+
if (keptInSource.length > 0) moveNotes.push(`\u6E90\u76EE\u5F55\u672A\u80FD\u6E05\u7406\u5E72\u51C0\uFF08\u6B8B\u7559 ${keptInSource.join("\u3001")}\uFF09\uFF0C\u82E5\u540E\u7EED\u79FB\u52A8\u62A5\u201Cduplicate\u201D\u8BF7\u624B\u52A8\u6E05\u7A7A\u8BE5\u76EE\u5F55\u3002`);
|
|
2283
|
+
}
|
|
1939
2284
|
} catch (e) {
|
|
1940
2285
|
if (ALREADY_EXISTS_RE.test(String(e && e.message || e))) {
|
|
1941
2286
|
await restore();
|
|
@@ -1979,9 +2324,107 @@ function apply(ctx) {
|
|
|
1979
2324
|
moved: true,
|
|
1980
2325
|
workspaceId: target.id,
|
|
1981
2326
|
workspaceTitle: target.title,
|
|
1982
|
-
workspacePath: canonical
|
|
2327
|
+
workspacePath: canonical,
|
|
2328
|
+
...moveNotes.length > 0 ? { notes: moveNotes } : {}
|
|
1983
2329
|
};
|
|
1984
2330
|
}
|
|
2331
|
+
const QUEUE_NOTE = "\u5DF2\u6392\u961F\uFF1A\u8BE5\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF08\u5199\u6240\u6709\u6743\u53EA\u5728 DSH \u9000\u51FA\u65F6\u91CA\u653E\uFF09\u3002\u91CD\u542F DSH \u65F6\u4F1A\u81EA\u52A8\u5B8C\u6210\uFF0C\u8BF7\u5148\u522B\u6253\u5F00\u5B83\u3002";
|
|
2332
|
+
function isBusyError(e) {
|
|
2333
|
+
if (!e) return false;
|
|
2334
|
+
if (e.code === "DSM_SESSION_BUSY") return true;
|
|
2335
|
+
const text = String(e && e.message || e);
|
|
2336
|
+
return /正被 DSH 打开|already owned/i.test(text);
|
|
2337
|
+
}
|
|
2338
|
+
async function moveOrQueue(sid, targetPath) {
|
|
2339
|
+
try {
|
|
2340
|
+
return await moveOne(sid, targetPath);
|
|
2341
|
+
} catch (e) {
|
|
2342
|
+
if (!isBusyError(e)) throw e;
|
|
2343
|
+
await pendingMoves.queue(sid, targetPath);
|
|
2344
|
+
return { ok: true, moved: false, queued: true, notes: [QUEUE_NOTE] };
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
const BOOT_WARMUP_MS = 3e4;
|
|
2348
|
+
const bootedAt = Date.now();
|
|
2349
|
+
let queueRunning = false;
|
|
2350
|
+
async function runPendingMoves(reason) {
|
|
2351
|
+
if (queueRunning) return { ran: false, moved: 0, kept: 0 };
|
|
2352
|
+
queueRunning = true;
|
|
2353
|
+
let moved = 0;
|
|
2354
|
+
const notes = [];
|
|
2355
|
+
try {
|
|
2356
|
+
const items = await pendingMoves.list();
|
|
2357
|
+
for (const item of items) {
|
|
2358
|
+
try {
|
|
2359
|
+
await moveOne(item.sessionId, item.targetPath);
|
|
2360
|
+
await pendingMoves.remove([item.sessionId]);
|
|
2361
|
+
metaCache.invalidate(item.sessionId);
|
|
2362
|
+
moved++;
|
|
2363
|
+
notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u5DF2\u5B8C\u6210\uFF1A${String(item.sessionId).slice(0, 18)}\u2026`);
|
|
2364
|
+
} catch (e) {
|
|
2365
|
+
if (isBusyError(e)) continue;
|
|
2366
|
+
if (Date.now() - bootedAt < BOOT_WARMUP_MS) continue;
|
|
2367
|
+
const bumped = await pendingMoves.bumpAttempts(item.sessionId);
|
|
2368
|
+
if (bumped && bumped.dropped) notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u591A\u6B21\u5931\u8D25\u5DF2\u653E\u5F03\uFF1A${String(item.sessionId).slice(0, 18)}\u2026\uFF08${String(e && e.message || e)}\uFF09`);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (moved > 0) {
|
|
2372
|
+
try {
|
|
2373
|
+
await reindexRegistry();
|
|
2374
|
+
} catch (e) {
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
} finally {
|
|
2378
|
+
queueRunning = false;
|
|
2379
|
+
}
|
|
2380
|
+
if (notes.length > 0) {
|
|
2381
|
+
try {
|
|
2382
|
+
for (const n of notes) console.warn("[dsh-sessions-manager] " + n);
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
void reason;
|
|
2387
|
+
return { ran: true, moved, kept: (await pendingMoves.list()).length };
|
|
2388
|
+
}
|
|
2389
|
+
const queueEffect = typeof ctx.effect === "function" ? ctx.effect.bind(ctx) : ((fn) => {
|
|
2390
|
+
fn();
|
|
2391
|
+
});
|
|
2392
|
+
if (typeof ctx.on === "function") {
|
|
2393
|
+
queueEffect(() => ctx.on("session/disposed", (session) => {
|
|
2394
|
+
const id = session && session.id != null ? String(session.id) : null;
|
|
2395
|
+
if (!id) return;
|
|
2396
|
+
pendingMoves.has(id).then((queued) => {
|
|
2397
|
+
if (!queued) return;
|
|
2398
|
+
const t = setTimeout(() => {
|
|
2399
|
+
runPendingMoves("session-disposed").catch(() => {
|
|
2400
|
+
});
|
|
2401
|
+
}, 500);
|
|
2402
|
+
if (t && typeof t.unref === "function") t.unref();
|
|
2403
|
+
}).catch(() => {
|
|
2404
|
+
});
|
|
2405
|
+
}));
|
|
2406
|
+
}
|
|
2407
|
+
queueEffect(() => {
|
|
2408
|
+
const timers = [0, 1e3, 3e3, 6e3, 12e3, 3e4].map((ms) => {
|
|
2409
|
+
const t = setTimeout(() => {
|
|
2410
|
+
runPendingMoves(ms === 0 ? "startup" : `startup+${ms}`).catch(() => {
|
|
2411
|
+
});
|
|
2412
|
+
}, ms);
|
|
2413
|
+
if (t && typeof t.unref === "function") t.unref();
|
|
2414
|
+
return t;
|
|
2415
|
+
});
|
|
2416
|
+
const tickTimer = setInterval(() => {
|
|
2417
|
+
pendingMoves.list().then((items) => {
|
|
2418
|
+
if (items.length > 0) return runPendingMoves("tick");
|
|
2419
|
+
}).catch(() => {
|
|
2420
|
+
});
|
|
2421
|
+
}, 12e4);
|
|
2422
|
+
if (tickTimer && typeof tickTimer.unref === "function") tickTimer.unref();
|
|
2423
|
+
return () => {
|
|
2424
|
+
for (const t of timers) clearTimeout(t);
|
|
2425
|
+
clearInterval(tickTimer);
|
|
2426
|
+
};
|
|
2427
|
+
});
|
|
1985
2428
|
async function reindexRegistry() {
|
|
1986
2429
|
const reg = w;
|
|
1987
2430
|
if (!reg || typeof reg.replaceHeaderIndex !== "function") return false;
|
|
@@ -2067,35 +2510,48 @@ function apply(ctx) {
|
|
|
2067
2510
|
wsByPath = {};
|
|
2068
2511
|
}
|
|
2069
2512
|
const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
|
|
2070
|
-
const items = [];
|
|
2071
2513
|
const usage = await collectUsage(entries);
|
|
2072
2514
|
const statsById = new Map(visibleIds.map((id) => [
|
|
2073
2515
|
id,
|
|
2074
2516
|
usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
|
|
2075
2517
|
]));
|
|
2076
|
-
const { missing } = metaCache.partition(visibleIds, statsById);
|
|
2518
|
+
const { cached, missing } = metaCache.partition(visibleIds, statsById);
|
|
2077
2519
|
const persisted = await hydrateFromPersist(missing, statsById);
|
|
2078
|
-
for (const [id, meta] of persisted)
|
|
2520
|
+
for (const [id, meta] of persisted) {
|
|
2521
|
+
const entry = entryById.get(id);
|
|
2522
|
+
const header = entry && entry.header ? entry.header : null;
|
|
2523
|
+
metaCache.set(id, statsById.get(id), {
|
|
2524
|
+
title: meta.title,
|
|
2525
|
+
cwd: header && typeof header.cwd === "string" && header.cwd ? header.cwd : meta.cwd,
|
|
2526
|
+
createdAt: header && header.createdAt != null ? header.createdAt : meta.createdAt
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2079
2529
|
const stillMissing = missing.filter((id) => !persisted.has(id));
|
|
2080
|
-
|
|
2081
|
-
const
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
for (let i = 0; i < visibleIds.length; i += CHUNK) {
|
|
2087
|
-
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => {
|
|
2530
|
+
if (stillMissing.length) enqueueWarm(stillMissing, statsById, entryById);
|
|
2531
|
+
const items = [];
|
|
2532
|
+
for (const id of visibleIds) {
|
|
2533
|
+
let meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || null;
|
|
2534
|
+
if (!meta) {
|
|
2535
|
+
meta = { title: null, cwd: null, createdAt: null };
|
|
2088
2536
|
const entry = entryById.get(id);
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2537
|
+
const header = entry && entry.header ? entry.header : null;
|
|
2538
|
+
if (header) {
|
|
2539
|
+
if (typeof header.cwd === "string") meta.cwd = header.cwd;
|
|
2540
|
+
if (header.createdAt != null) meta.createdAt = header.createdAt;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
const it = buildItem(id, meta, usage, !!(opts && opts.usage));
|
|
2544
|
+
items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
2545
|
+
}
|
|
2546
|
+
if (opts && opts.onlyArchived) {
|
|
2547
|
+
const archivedItems = [];
|
|
2548
|
+
for (const it of items) {
|
|
2549
|
+
if (!it.archived) continue;
|
|
2550
|
+
const { archived: _drop, ...rest } = it;
|
|
2551
|
+
archivedItems.push(rest);
|
|
2552
|
+
}
|
|
2553
|
+
return { items: archivedItems, usage };
|
|
2097
2554
|
}
|
|
2098
|
-
persistDecoded(decoded, statsById);
|
|
2099
2555
|
let starredSet = /* @__PURE__ */ new Set();
|
|
2100
2556
|
try {
|
|
2101
2557
|
starredSet = new Set((await stars.read()).starredSessionIds);
|
|
@@ -2173,43 +2629,82 @@ function apply(ctx) {
|
|
|
2173
2629
|
const activeTombstones = store.purgedSessionIds.map(String).filter((id) => !present.has(id));
|
|
2174
2630
|
if (ids.length) {
|
|
2175
2631
|
const usage = await collectUsage(entries);
|
|
2632
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
2176
2633
|
const statsById = new Map(ids.map((id) => [
|
|
2177
2634
|
id,
|
|
2178
2635
|
usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
|
|
2179
2636
|
]));
|
|
2180
2637
|
const { cached, missing } = metaCache.partition(ids, statsById);
|
|
2181
2638
|
const persisted = await hydrateFromPersist(missing, statsById);
|
|
2182
|
-
for (const [id, meta] of persisted)
|
|
2639
|
+
for (const [id, meta] of persisted) {
|
|
2640
|
+
const entry = entryById.get(id);
|
|
2641
|
+
const header = entry && entry.header ? entry.header : null;
|
|
2642
|
+
metaCache.set(id, statsById.get(id), {
|
|
2643
|
+
title: meta.title,
|
|
2644
|
+
cwd: header && typeof header.cwd === "string" && header.cwd ? header.cwd : meta.cwd,
|
|
2645
|
+
createdAt: header && header.createdAt != null ? header.createdAt : meta.createdAt
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2183
2648
|
const rest = missing.filter((id) => !persisted.has(id));
|
|
2184
|
-
|
|
2185
|
-
const decoded = /* @__PURE__ */ new Map();
|
|
2186
|
-
const collectDecoded = (id, meta) => {
|
|
2187
|
-
decoded.set(id, meta);
|
|
2188
|
-
};
|
|
2649
|
+
if (rest.length) enqueueWarm(rest, statsById, entryById);
|
|
2189
2650
|
for (const id of ids) {
|
|
2190
|
-
|
|
2191
|
-
if (!meta) {
|
|
2192
|
-
const entry = entries.find((e) => e.id === id);
|
|
2193
|
-
const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
|
|
2194
|
-
const next = metaFromSnapshot(snapshot);
|
|
2195
|
-
if (entry && entry.header) {
|
|
2196
|
-
if (!next.cwd && typeof entry.header.cwd === "string") next.cwd = entry.header.cwd;
|
|
2197
|
-
if (!next.createdAt && entry.header.createdAt != null) next.createdAt = entry.header.createdAt;
|
|
2198
|
-
}
|
|
2199
|
-
metaCache.set(id, statsById.get(id), next);
|
|
2200
|
-
if (statsById.get(id)) collectDecoded(id, next);
|
|
2201
|
-
meta = next;
|
|
2202
|
-
}
|
|
2651
|
+
const meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || null;
|
|
2203
2652
|
if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
|
|
2204
2653
|
}
|
|
2205
|
-
persistDecoded(decoded, statsById);
|
|
2206
2654
|
}
|
|
2655
|
+
const lineage = {};
|
|
2656
|
+
for (const entry of entries) {
|
|
2657
|
+
const info = classifyLineage(entry.header, entry.sizeBytes);
|
|
2658
|
+
if (info) lineage[entry.id] = info;
|
|
2659
|
+
}
|
|
2660
|
+
await refineEmptyLineage(lineage, entries);
|
|
2207
2661
|
return {
|
|
2208
2662
|
titles: Object.fromEntries(authorityTitleCache),
|
|
2209
2663
|
trashedSessionIds: store.items.map((item) => String(item.sessionId)),
|
|
2210
|
-
purgedSessionIds: activeTombstones
|
|
2664
|
+
purgedSessionIds: activeTombstones,
|
|
2665
|
+
lineage
|
|
2211
2666
|
};
|
|
2212
2667
|
}
|
|
2668
|
+
const emptyScanCache = /* @__PURE__ */ new Map();
|
|
2669
|
+
async function refineEmptyLineage(lineage, entries) {
|
|
2670
|
+
if (!persistence || typeof persistence.inspectSession !== "function") return;
|
|
2671
|
+
for (const entry of entries) {
|
|
2672
|
+
const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
|
|
2673
|
+
if (size === null || size > EMPTY_DECODE_LIMIT) continue;
|
|
2674
|
+
const id = String(entry.id);
|
|
2675
|
+
let scanned = emptyScanCache.get(id);
|
|
2676
|
+
if (!scanned || scanned.sizeBytes !== size) {
|
|
2677
|
+
let isEmpty = false;
|
|
2678
|
+
try {
|
|
2679
|
+
const types = [];
|
|
2680
|
+
await persistence.inspectSession(id, { onEvents: (batch) => {
|
|
2681
|
+
for (const ev of batch || []) if (types.length < 64) types.push(ev && ev.type);
|
|
2682
|
+
} });
|
|
2683
|
+
isEmpty = isEmptyEventTypes(types);
|
|
2684
|
+
} catch (e) {
|
|
2685
|
+
isEmpty = false;
|
|
2686
|
+
}
|
|
2687
|
+
scanned = { sizeBytes: size, empty: isEmpty };
|
|
2688
|
+
emptyScanCache.set(id, scanned);
|
|
2689
|
+
}
|
|
2690
|
+
if (scanned.empty) {
|
|
2691
|
+
const info = lineage[id] || (lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: false });
|
|
2692
|
+
info.empty = true;
|
|
2693
|
+
} else if (lineage[id]) {
|
|
2694
|
+
const info = lineage[id];
|
|
2695
|
+
info.empty = false;
|
|
2696
|
+
if (!info.origin && !info.parentSession) delete lineage[id];
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
const live = /* @__PURE__ */ new Set();
|
|
2700
|
+
for (const entry of entries) {
|
|
2701
|
+
const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
|
|
2702
|
+
if (size !== null && size <= EMPTY_DECODE_LIMIT) live.add(String(entry.id));
|
|
2703
|
+
}
|
|
2704
|
+
for (const key of emptyScanCache.keys()) {
|
|
2705
|
+
if (!live.has(key)) emptyScanCache.delete(key);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2213
2708
|
async function buildDetails(sid, signal) {
|
|
2214
2709
|
const sessions = ctx.get("sessions");
|
|
2215
2710
|
const live = sessions && sessions.get(sid);
|
|
@@ -2294,8 +2789,8 @@ function apply(ctx) {
|
|
|
2294
2789
|
let sizeBytes = null;
|
|
2295
2790
|
if (live === void 0) {
|
|
2296
2791
|
try {
|
|
2297
|
-
const
|
|
2298
|
-
if (
|
|
2792
|
+
const stat5 = await persistence.statSession(sid);
|
|
2793
|
+
if (stat5 && Number.isFinite(stat5.sizeBytes)) sizeBytes = stat5.sizeBytes;
|
|
2299
2794
|
} catch (e) {
|
|
2300
2795
|
}
|
|
2301
2796
|
}
|
|
@@ -2303,7 +2798,7 @@ function apply(ctx) {
|
|
|
2303
2798
|
try {
|
|
2304
2799
|
const loc = persistence.locate(meta);
|
|
2305
2800
|
if (loc && typeof loc.path === "string" && loc.path) {
|
|
2306
|
-
const st = await
|
|
2801
|
+
const st = await stat4(loc.path);
|
|
2307
2802
|
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
2308
2803
|
}
|
|
2309
2804
|
} catch (e) {
|
|
@@ -2312,7 +2807,7 @@ function apply(ctx) {
|
|
|
2312
2807
|
}
|
|
2313
2808
|
if (stats.fetches.length > MAX_FETCHES) stats.fetches = stats.fetches.slice(0, MAX_FETCHES);
|
|
2314
2809
|
const fileEntries = [...fileSet.entries()].slice(0, MAX_FILES * 2);
|
|
2315
|
-
const exists = await Promise.all(fileEntries.map(([p]) =>
|
|
2810
|
+
const exists = await Promise.all(fileEntries.map(([p]) => stat4(p).then(() => true).catch(() => false)));
|
|
2316
2811
|
const files = fileEntries.filter((_, i) => exists[i]).map(([path, tool]) => ({ path, tool })).slice(0, MAX_FILES);
|
|
2317
2812
|
const lineage = {
|
|
2318
2813
|
parentSessionId: meta && typeof meta.parentSession === "string" ? meta.parentSession : null,
|
|
@@ -2364,7 +2859,7 @@ function apply(ctx) {
|
|
|
2364
2859
|
disposers.push(ctx.webServer.register({
|
|
2365
2860
|
kind: "exact",
|
|
2366
2861
|
path: "/archived-sessions/capabilities",
|
|
2367
|
-
handler: async (req, res) => json(res, capabilities)
|
|
2862
|
+
handler: async (req, res) => json(res, { ...capabilities, buildStamp: BUILD_STAMP })
|
|
2368
2863
|
}));
|
|
2369
2864
|
disposers.push(ctx.webServer.register({
|
|
2370
2865
|
kind: "exact",
|
|
@@ -2395,12 +2890,10 @@ function apply(ctx) {
|
|
|
2395
2890
|
} catch (e) {
|
|
2396
2891
|
wsByPath = {};
|
|
2397
2892
|
}
|
|
2398
|
-
const
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
items.push.apply(items, res2);
|
|
2403
|
-
}
|
|
2893
|
+
const wanted = new Set(idStrs);
|
|
2894
|
+
const detailed = await allSessionItemsDetailed();
|
|
2895
|
+
const items = detailed.items.filter((it) => wanted.has(String(it.sessionId)) && it.archived);
|
|
2896
|
+
for (const it of items) delete it.archived;
|
|
2404
2897
|
json(res, { items });
|
|
2405
2898
|
} catch (e) {
|
|
2406
2899
|
json(res, { error: String(e && e.message || e) }, 500);
|
|
@@ -2520,7 +3013,7 @@ function apply(ctx) {
|
|
|
2520
3013
|
if (typeof item.originalPath !== "string" || !item.originalPath) {
|
|
2521
3014
|
return { sessionId: item.sessionId, status: "unverified", originalPath: null };
|
|
2522
3015
|
}
|
|
2523
|
-
const exists = await
|
|
3016
|
+
const exists = await stat4(item.originalPath).then(() => true).catch(() => false);
|
|
2524
3017
|
return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
|
|
2525
3018
|
}));
|
|
2526
3019
|
json(res, {
|
|
@@ -2561,8 +3054,10 @@ function apply(ctx) {
|
|
|
2561
3054
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
2562
3055
|
const out = await purgeFromTrash(sid);
|
|
2563
3056
|
metaCache.invalidate(sid);
|
|
2564
|
-
|
|
2565
|
-
|
|
3057
|
+
try {
|
|
3058
|
+
await titleIndex.remove([sid]);
|
|
3059
|
+
} catch (e) {
|
|
3060
|
+
}
|
|
2566
3061
|
json(res, out);
|
|
2567
3062
|
} catch (e) {
|
|
2568
3063
|
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
@@ -2582,7 +3077,7 @@ function apply(ctx) {
|
|
|
2582
3077
|
try {
|
|
2583
3078
|
results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
|
|
2584
3079
|
metaCache.invalidate(sid);
|
|
2585
|
-
titleIndex.remove([sid]).catch(() => {
|
|
3080
|
+
await titleIndex.remove([sid]).catch(() => {
|
|
2586
3081
|
});
|
|
2587
3082
|
} catch (e) {
|
|
2588
3083
|
results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
|
|
@@ -2672,6 +3167,108 @@ function apply(ctx) {
|
|
|
2672
3167
|
}
|
|
2673
3168
|
}
|
|
2674
3169
|
}));
|
|
3170
|
+
disposers.push(ctx.webServer.register({
|
|
3171
|
+
kind: "exact",
|
|
3172
|
+
path: "/archived-sessions/lineage-tree",
|
|
3173
|
+
handler: async (req, res) => {
|
|
3174
|
+
try {
|
|
3175
|
+
const body = await readJsonBody(req);
|
|
3176
|
+
const rootId = body && typeof body.sessionId === "string" ? body.sessionId : "";
|
|
3177
|
+
if (!rootId || !isSafeSessionId2(rootId)) return json(res, { error: "missing sessionId" }, 400);
|
|
3178
|
+
const headerById = /* @__PURE__ */ new Map();
|
|
3179
|
+
const sizeById = /* @__PURE__ */ new Map();
|
|
3180
|
+
const trashedIds = /* @__PURE__ */ new Set();
|
|
3181
|
+
let purgedIds = [];
|
|
3182
|
+
const persistedIds = /* @__PURE__ */ new Set();
|
|
3183
|
+
try {
|
|
3184
|
+
const store = await readTrashStore();
|
|
3185
|
+
for (const item of store.items) trashedIds.add(String(item.sessionId));
|
|
3186
|
+
purgedIds = store.purgedSessionIds.map(String);
|
|
3187
|
+
} catch (e) {
|
|
3188
|
+
}
|
|
3189
|
+
const addHeader = (h, sizeBytes) => {
|
|
3190
|
+
if (!h || h.id == null) return;
|
|
3191
|
+
const id = String(h.id);
|
|
3192
|
+
if (headerById.has(id) || trashedIds.has(id)) return;
|
|
3193
|
+
headerById.set(id, {
|
|
3194
|
+
parentSession: h.parentSession != null ? String(h.parentSession) : null,
|
|
3195
|
+
subagent: h.origin === "subagent",
|
|
3196
|
+
delegationDepth: Number.isFinite(h.delegationDepth) ? h.delegationDepth : null,
|
|
3197
|
+
createdAt: typeof h.createdAt === "number" ? h.createdAt : null
|
|
3198
|
+
});
|
|
3199
|
+
if (Number.isFinite(sizeBytes)) sizeById.set(id, sizeBytes);
|
|
3200
|
+
};
|
|
3201
|
+
try {
|
|
3202
|
+
for (const entry of await persistence.listEntries()) {
|
|
3203
|
+
addHeader(entry.header, entry.sizeBytes);
|
|
3204
|
+
if (entry && entry.id != null) persistedIds.add(String(entry.id));
|
|
3205
|
+
}
|
|
3206
|
+
} catch (e) {
|
|
3207
|
+
}
|
|
3208
|
+
const liveIds = /* @__PURE__ */ new Set();
|
|
3209
|
+
const live = ctx.get("sessions");
|
|
3210
|
+
try {
|
|
3211
|
+
if (live && typeof live.list === "function") {
|
|
3212
|
+
live.list().forEach((s) => {
|
|
3213
|
+
liveIds.add(String(s.id));
|
|
3214
|
+
addHeader(s.header, void 0);
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
} catch (e) {
|
|
3218
|
+
}
|
|
3219
|
+
for (const id of purgedIds) {
|
|
3220
|
+
if (!persistedIds.has(id)) headerById.delete(id);
|
|
3221
|
+
}
|
|
3222
|
+
const kidsOf = /* @__PURE__ */ new Map();
|
|
3223
|
+
for (const [id, h] of headerById) {
|
|
3224
|
+
if (!h.subagent || !h.parentSession) continue;
|
|
3225
|
+
if (!kidsOf.has(h.parentSession)) kidsOf.set(h.parentSession, []);
|
|
3226
|
+
kidsOf.get(h.parentSession).push(id);
|
|
3227
|
+
}
|
|
3228
|
+
const MAX_NODES = 500;
|
|
3229
|
+
let nodeCount = 0;
|
|
3230
|
+
const build = (id, depth, visited) => {
|
|
3231
|
+
nodeCount++;
|
|
3232
|
+
const h = headerById.get(id) || {};
|
|
3233
|
+
const childVisited = new Set(visited);
|
|
3234
|
+
childVisited.add(id);
|
|
3235
|
+
const kidIds = nodeCount >= MAX_NODES ? [] : (kidsOf.get(id) || []).filter((k) => !childVisited.has(k));
|
|
3236
|
+
return {
|
|
3237
|
+
sessionId: id,
|
|
3238
|
+
parentSession: h.parentSession || null,
|
|
3239
|
+
delegationDepth: h.delegationDepth,
|
|
3240
|
+
depth,
|
|
3241
|
+
createdAt: h.createdAt || null,
|
|
3242
|
+
sizeBytes: sizeById.has(id) ? sizeById.get(id) : null,
|
|
3243
|
+
live: liveIds.has(id),
|
|
3244
|
+
title: null,
|
|
3245
|
+
children: kidIds.map((k) => build(k, depth + 1, childVisited))
|
|
3246
|
+
};
|
|
3247
|
+
};
|
|
3248
|
+
const nodes = (kidsOf.get(rootId) || []).map((k) => build(k, 1, /* @__PURE__ */ new Set([rootId])));
|
|
3249
|
+
const allIds = [rootId];
|
|
3250
|
+
const walk = (n) => {
|
|
3251
|
+
allIds.push(n.sessionId);
|
|
3252
|
+
n.children.forEach(walk);
|
|
3253
|
+
};
|
|
3254
|
+
nodes.forEach(walk);
|
|
3255
|
+
const titles = await projectTitles(allIds);
|
|
3256
|
+
const titleOf = (id) => {
|
|
3257
|
+
const snap = titles.get(id);
|
|
3258
|
+
const m = snap ? metaFromSnapshot(snap) : null;
|
|
3259
|
+
return m && m.title || null;
|
|
3260
|
+
};
|
|
3261
|
+
const fill = (n) => {
|
|
3262
|
+
n.title = titleOf(n.sessionId);
|
|
3263
|
+
n.children.forEach(fill);
|
|
3264
|
+
};
|
|
3265
|
+
nodes.forEach(fill);
|
|
3266
|
+
json(res, { parent: { sessionId: rootId, title: titleOf(rootId) }, nodes });
|
|
3267
|
+
} catch (e) {
|
|
3268
|
+
json(res, { error: String(e && e.message || e) }, errorStatus(e));
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
}));
|
|
2675
3272
|
disposers.push(ctx.webServer.register({
|
|
2676
3273
|
kind: "exact",
|
|
2677
3274
|
path: "/archived-sessions/workspaces",
|
|
@@ -2693,7 +3290,8 @@ function apply(ctx) {
|
|
|
2693
3290
|
const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
|
|
2694
3291
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
2695
3292
|
if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
|
|
2696
|
-
const moved = await
|
|
3293
|
+
const moved = await moveOrQueue(sid, target);
|
|
3294
|
+
if (moved.queued) return json(res, { sessionId: sid, ...moved });
|
|
2697
3295
|
metaCache.invalidate(sid);
|
|
2698
3296
|
try {
|
|
2699
3297
|
await reindexRegistry();
|
|
@@ -2705,6 +3303,74 @@ function apply(ctx) {
|
|
|
2705
3303
|
}
|
|
2706
3304
|
}
|
|
2707
3305
|
}));
|
|
3306
|
+
disposers.push(ctx.webServer.register({
|
|
3307
|
+
kind: "exact",
|
|
3308
|
+
path: "/archived-sessions/move-many",
|
|
3309
|
+
handler: async (req, res) => {
|
|
3310
|
+
try {
|
|
3311
|
+
const body = await readJsonBody(req);
|
|
3312
|
+
const ids = Array.isArray(body && body.sessionIds) ? [...new Set(body.sessionIds.filter((x) => typeof x === "string" && x))] : [];
|
|
3313
|
+
const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
|
|
3314
|
+
if (!ids.length) return json(res, { ok: false, error: "missing sessionIds" }, 400);
|
|
3315
|
+
if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
|
|
3316
|
+
let moved = 0;
|
|
3317
|
+
const failed = [];
|
|
3318
|
+
const queued = [];
|
|
3319
|
+
for (const sid of ids) {
|
|
3320
|
+
try {
|
|
3321
|
+
const r = await moveOrQueue(sid, target);
|
|
3322
|
+
if (r && r.queued) {
|
|
3323
|
+
queued.push(sid);
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
metaCache.invalidate(sid);
|
|
3327
|
+
moved++;
|
|
3328
|
+
} catch (e) {
|
|
3329
|
+
const err = e && e.code ? { sessionId: sid, error: String(e && e.message || e), code: e.code } : { sessionId: sid, error: String(e && e.message || e) };
|
|
3330
|
+
failed.push(err);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
try {
|
|
3334
|
+
await reindexRegistry();
|
|
3335
|
+
} catch (e) {
|
|
3336
|
+
}
|
|
3337
|
+
json(res, {
|
|
3338
|
+
moved,
|
|
3339
|
+
failed,
|
|
3340
|
+
...queued.length > 0 ? { queued, notes: [`${queued.length} \u4E2A\u4F1A\u8BDD\u5DF2\u6392\u961F\uFF08\u6B63\u88AB DSH \u6253\u5F00\uFF09\uFF0C\u91CD\u542F DSH \u540E\u4F1A\u81EA\u52A8\u5B8C\u6210\uFF1B\u91CD\u542F\u540E\u8BF7\u5148\u522B\u6253\u5F00\u5B83\u4EEC\u3002`] } : {}
|
|
3341
|
+
});
|
|
3342
|
+
} catch (e) {
|
|
3343
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}));
|
|
3347
|
+
disposers.push(ctx.webServer.register({
|
|
3348
|
+
kind: "exact",
|
|
3349
|
+
path: "/archived-sessions/pending-moves",
|
|
3350
|
+
handler: async (req, res) => {
|
|
3351
|
+
try {
|
|
3352
|
+
const items = await pendingMoves.list();
|
|
3353
|
+
json(res, { items: items.map((i) => ({ sessionId: i.sessionId, targetPath: i.targetPath, queuedAt: i.queuedAt, attempts: i.attempts })) });
|
|
3354
|
+
} catch (e) {
|
|
3355
|
+
json(res, { error: String(e && e.message || e) }, 500);
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
}));
|
|
3359
|
+
disposers.push(ctx.webServer.register({
|
|
3360
|
+
kind: "exact",
|
|
3361
|
+
path: "/archived-sessions/pending-moves/cancel",
|
|
3362
|
+
handler: async (req, res) => {
|
|
3363
|
+
try {
|
|
3364
|
+
const body = await readJsonBody(req);
|
|
3365
|
+
const ids = parseIds(body);
|
|
3366
|
+
if (!ids || ids.length === 0) return json(res, { ok: false, error: "missing sessionIds" }, 400);
|
|
3367
|
+
const removed = await pendingMoves.remove(ids);
|
|
3368
|
+
json(res, { ok: true, removed });
|
|
3369
|
+
} catch (e) {
|
|
3370
|
+
json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
}));
|
|
2708
3374
|
disposers.push(ctx.webServer.register({
|
|
2709
3375
|
kind: "exact",
|
|
2710
3376
|
path: "/archived-sessions/archive",
|