dsh-sessions-manager 3.5.4 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +49 -9
- package/README.md +50 -9
- 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 +778 -134
- 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 +406 -14
- package/src/lineage.js +78 -0
- package/src/pending-moves.js +126 -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,9 @@ function fingerprintOf(stat3) {
|
|
|
482
614
|
function isPersistableFingerprint(fingerprint) {
|
|
483
615
|
return typeof fingerprint === "string" && fingerprint !== "" && !fingerprint.startsWith(REVISION_PREFIX);
|
|
484
616
|
}
|
|
485
|
-
function isFresh(entry,
|
|
617
|
+
function isFresh(entry, stat5, now, ttlMs = DEFAULT_TTL_MS) {
|
|
486
618
|
if (!entry) return false;
|
|
487
|
-
const fp = fingerprintOf(
|
|
619
|
+
const fp = fingerprintOf(stat5);
|
|
488
620
|
if (!fp) return false;
|
|
489
621
|
if (entry.fingerprint !== fp) return false;
|
|
490
622
|
if (typeof entry.at !== "number") return false;
|
|
@@ -496,8 +628,8 @@ function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAU
|
|
|
496
628
|
const missing = [];
|
|
497
629
|
for (const id of ids) {
|
|
498
630
|
const entry = cache && cache.get(String(id));
|
|
499
|
-
const
|
|
500
|
-
if (isFresh(entry,
|
|
631
|
+
const stat5 = statsById && statsById.get(String(id));
|
|
632
|
+
if (isFresh(entry, stat5, now, ttlMs) && entry && entry.meta) {
|
|
501
633
|
cached.set(String(id), entry.meta);
|
|
502
634
|
} else {
|
|
503
635
|
missing.push(String(id));
|
|
@@ -513,10 +645,10 @@ function createSessionMetaCache(opts = {}) {
|
|
|
513
645
|
let misses = 0;
|
|
514
646
|
return {
|
|
515
647
|
// 命中返回 meta,未命中/无法校验返回 null。
|
|
516
|
-
get(id,
|
|
648
|
+
get(id, stat5) {
|
|
517
649
|
const key = String(id);
|
|
518
650
|
const entry = map.get(key);
|
|
519
|
-
if (isFresh(entry,
|
|
651
|
+
if (isFresh(entry, stat5, Date.now(), ttlMs)) {
|
|
520
652
|
hits++;
|
|
521
653
|
map.delete(key);
|
|
522
654
|
map.set(key, entry);
|
|
@@ -525,9 +657,9 @@ function createSessionMetaCache(opts = {}) {
|
|
|
525
657
|
misses++;
|
|
526
658
|
return null;
|
|
527
659
|
},
|
|
528
|
-
set(id,
|
|
660
|
+
set(id, stat5, meta) {
|
|
529
661
|
if (!meta) return null;
|
|
530
|
-
const fp = fingerprintOf(
|
|
662
|
+
const fp = fingerprintOf(stat5);
|
|
531
663
|
if (!fp) return null;
|
|
532
664
|
const key = String(id);
|
|
533
665
|
map.delete(key);
|
|
@@ -562,8 +694,8 @@ function createSessionMetaCache(opts = {}) {
|
|
|
562
694
|
}
|
|
563
695
|
|
|
564
696
|
// src/title-persist-index.js
|
|
565
|
-
import { mkdir as
|
|
566
|
-
import { dirname, join as
|
|
697
|
+
import { mkdir as mkdir4, readFile, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
|
|
698
|
+
import { dirname, join as join5 } from "node:path";
|
|
567
699
|
var TITLE_INDEX_SCHEMA_VERSION = 1;
|
|
568
700
|
var MAX_ENTRIES = 2e4;
|
|
569
701
|
function normalizeEntry(raw) {
|
|
@@ -601,7 +733,7 @@ function mergeEntries(left, right) {
|
|
|
601
733
|
function createTitleIndexStore({ dir, file }) {
|
|
602
734
|
let cache = null;
|
|
603
735
|
let chain = Promise.resolve();
|
|
604
|
-
const path = file ||
|
|
736
|
+
const path = file || join5(dir, "title-index.json");
|
|
605
737
|
async function readRaw() {
|
|
606
738
|
try {
|
|
607
739
|
return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
|
|
@@ -636,10 +768,10 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
636
768
|
if (!Object.keys(right).length) return false;
|
|
637
769
|
await enqueue(async (store) => {
|
|
638
770
|
const next = mergeEntries(store, right);
|
|
639
|
-
await
|
|
640
|
-
const tmp =
|
|
641
|
-
await
|
|
642
|
-
await
|
|
771
|
+
await mkdir4(dirname(path), { recursive: true });
|
|
772
|
+
const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
|
|
773
|
+
await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
|
|
774
|
+
await rename4(tmp, path);
|
|
643
775
|
cache = next;
|
|
644
776
|
});
|
|
645
777
|
return true;
|
|
@@ -656,10 +788,10 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
656
788
|
}
|
|
657
789
|
}
|
|
658
790
|
if (!changed) return;
|
|
659
|
-
await
|
|
660
|
-
const tmp =
|
|
661
|
-
await
|
|
662
|
-
await
|
|
791
|
+
await mkdir4(dirname(path), { recursive: true });
|
|
792
|
+
const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
|
|
793
|
+
await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
|
|
794
|
+
await rename4(tmp, path);
|
|
663
795
|
});
|
|
664
796
|
return true;
|
|
665
797
|
}
|
|
@@ -667,8 +799,8 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
667
799
|
}
|
|
668
800
|
|
|
669
801
|
// src/handle-era-paths.js
|
|
670
|
-
import { readdir, stat } from "node:fs/promises";
|
|
671
|
-
import { basename, join as
|
|
802
|
+
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
803
|
+
import { basename, join as join6 } from "node:path";
|
|
672
804
|
|
|
673
805
|
// src/path-guard.js
|
|
674
806
|
function splitSegments(target) {
|
|
@@ -700,7 +832,7 @@ function pathOwnsSession(target, sid) {
|
|
|
700
832
|
}
|
|
701
833
|
|
|
702
834
|
// src/handle-era-paths.js
|
|
703
|
-
var GENERATION_LOG_RE = /^session\.v\d
|
|
835
|
+
var GENERATION_LOG_RE = /^session(\.v\d+)?\.jsonl(\.zst(d)?)?$/;
|
|
704
836
|
function isSafeChar(ch) {
|
|
705
837
|
return ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch);
|
|
706
838
|
}
|
|
@@ -741,8 +873,11 @@ function resolveSessionRoot(sp) {
|
|
|
741
873
|
return typeof root === "string" && root.length > 0 ? root : null;
|
|
742
874
|
}
|
|
743
875
|
function deriveSessionDir(root, cwd, id) {
|
|
744
|
-
const project = cwd === void 0 || cwd === null || cwd === "" ?
|
|
745
|
-
return
|
|
876
|
+
const project = cwd === void 0 || cwd === null || cwd === "" ? join6(root, "_no-cwd") : join6(root, projectKeyFor(cwd));
|
|
877
|
+
return join6(project, encodeSegmentFor(id));
|
|
878
|
+
}
|
|
879
|
+
function generationVersionOf(name2) {
|
|
880
|
+
return Number((String(name2).match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
746
881
|
}
|
|
747
882
|
async function locateSessionArtifacts(sp, header) {
|
|
748
883
|
const root = resolveSessionRoot(sp);
|
|
@@ -757,27 +892,26 @@ async function locateSessionArtifacts(sp, header) {
|
|
|
757
892
|
if (basename(sessionDir) !== encodeSegmentFor(sid)) return null;
|
|
758
893
|
let entries;
|
|
759
894
|
try {
|
|
760
|
-
const st = await
|
|
895
|
+
const st = await stat2(sessionDir);
|
|
761
896
|
if (!st.isDirectory()) return null;
|
|
762
|
-
entries = await
|
|
897
|
+
entries = await readdir2(sessionDir);
|
|
763
898
|
} catch (e) {
|
|
764
899
|
return null;
|
|
765
900
|
}
|
|
766
901
|
const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
|
|
767
902
|
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]);
|
|
903
|
+
generationFiles.sort((a, b) => generationVersionOf(b) - generationVersionOf(a));
|
|
904
|
+
const logPath = join6(sessionDir, generationFiles[0]);
|
|
774
905
|
if (!pathOwnsSession(logPath, sid)) return null;
|
|
775
906
|
return {
|
|
776
907
|
root,
|
|
777
|
-
projectDir:
|
|
908
|
+
projectDir: join6(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
|
|
778
909
|
sessionDir,
|
|
779
910
|
logPath,
|
|
780
|
-
generationFiles
|
|
911
|
+
generationFiles,
|
|
912
|
+
// 最高代版本号:0 = legacy `session.jsonl[.zstd]`,≥1 = `session.vN.jsonl[.zstd]`。
|
|
913
|
+
// 上层用它判断「另一个目录里的同 id 副本是不是被取代的旧代」。
|
|
914
|
+
generationVersion: generationVersionOf(generationFiles[0])
|
|
781
915
|
};
|
|
782
916
|
}
|
|
783
917
|
|
|
@@ -785,9 +919,15 @@ async function locateSessionArtifacts(sp, header) {
|
|
|
785
919
|
var DEFAULT_CHUNK = 400;
|
|
786
920
|
var MAX_CHUNKS = 2e4;
|
|
787
921
|
function normalizeReadResult(events) {
|
|
922
|
+
if (events === void 0 || events === null) return [];
|
|
788
923
|
if (Array.isArray(events)) return events;
|
|
789
|
-
if (
|
|
790
|
-
return [];
|
|
924
|
+
if (typeof events === "object" && Array.isArray(events.events)) return events.events;
|
|
925
|
+
if (typeof events[Symbol.iterator] === "function") return [...events];
|
|
926
|
+
const error = new Error(
|
|
927
|
+
"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)
|
|
928
|
+
);
|
|
929
|
+
error.code = "DSM_READ_SHAPE_UNKNOWN";
|
|
930
|
+
throw error;
|
|
791
931
|
}
|
|
792
932
|
async function closeQuietly(handle) {
|
|
793
933
|
try {
|
|
@@ -975,9 +1115,80 @@ function requireCapability(capabilities, name2) {
|
|
|
975
1115
|
}
|
|
976
1116
|
|
|
977
1117
|
// src/handle-era-ops.js
|
|
978
|
-
import { mkdir as
|
|
979
|
-
import { join as
|
|
1118
|
+
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";
|
|
1119
|
+
import { basename as basename2, dirname as dirname2, join as join7 } from "node:path";
|
|
980
1120
|
var MOVE_BATCH = 400;
|
|
1121
|
+
var REAL_LOG_RE = /^session.*\.jsonl(\.zst(d)?)?$/;
|
|
1122
|
+
var MOVE_TEMP_RE = /\.move-(backup|stage)-/;
|
|
1123
|
+
var SESSION_LOCAL_JUNK_RE = /^(\.?session\.lock.*|\.lock.*|.*\.lock|.*\.tmp|.*\.stage|.*\.bak|\.DS_Store)$/;
|
|
1124
|
+
async function cleanSiblingCopies(root, sid, keepDir, keepVersion) {
|
|
1125
|
+
let projects;
|
|
1126
|
+
try {
|
|
1127
|
+
projects = await readdir3(root, { withFileTypes: true });
|
|
1128
|
+
} catch (e) {
|
|
1129
|
+
return { removed: 0, superseded: [], real: [] };
|
|
1130
|
+
}
|
|
1131
|
+
let segment;
|
|
1132
|
+
try {
|
|
1133
|
+
segment = encodeSegmentFor(sid);
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
return { removed: 0, superseded: [], real: [] };
|
|
1136
|
+
}
|
|
1137
|
+
let removed = 0;
|
|
1138
|
+
const superseded = [];
|
|
1139
|
+
const real = [];
|
|
1140
|
+
for (const ent of projects) {
|
|
1141
|
+
if (!ent.isDirectory()) continue;
|
|
1142
|
+
const candidate = join7(root, ent.name, segment);
|
|
1143
|
+
if (keepDir && candidate === keepDir) continue;
|
|
1144
|
+
let entries;
|
|
1145
|
+
try {
|
|
1146
|
+
const st = await stat3(candidate);
|
|
1147
|
+
if (!st.isDirectory()) continue;
|
|
1148
|
+
entries = await readdir3(candidate);
|
|
1149
|
+
} catch (e) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
const logs = entries.filter((name2) => REAL_LOG_RE.test(name2) && !MOVE_TEMP_RE.test(name2));
|
|
1153
|
+
if (logs.length === 0) {
|
|
1154
|
+
try {
|
|
1155
|
+
await rm2(candidate, { recursive: true, force: true });
|
|
1156
|
+
removed++;
|
|
1157
|
+
} catch (e) {
|
|
1158
|
+
}
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const highest = Math.max(...logs.map(generationVersionOf));
|
|
1162
|
+
const onlyLogsAndLocalJunk = entries.every((name2) => REAL_LOG_RE.test(name2) && !MOVE_TEMP_RE.test(name2) || SESSION_LOCAL_JUNK_RE.test(name2));
|
|
1163
|
+
if (Number.isSafeInteger(keepVersion) && highest < keepVersion && onlyLogsAndLocalJunk) {
|
|
1164
|
+
try {
|
|
1165
|
+
await rm2(candidate, { recursive: true, force: true });
|
|
1166
|
+
superseded.push(candidate);
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
real.push(candidate);
|
|
1169
|
+
}
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
real.push(candidate);
|
|
1173
|
+
}
|
|
1174
|
+
return { removed, superseded, real };
|
|
1175
|
+
}
|
|
1176
|
+
async function removeSourceDir(sessionDir) {
|
|
1177
|
+
let entries;
|
|
1178
|
+
try {
|
|
1179
|
+
entries = await readdir3(sessionDir);
|
|
1180
|
+
} catch (e) {
|
|
1181
|
+
return { cleaned: true, leftover: [] };
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
await rm2(sessionDir, { recursive: true, force: true });
|
|
1185
|
+
} catch (e) {
|
|
1186
|
+
return { cleaned: false, leftover: entries };
|
|
1187
|
+
}
|
|
1188
|
+
return { cleaned: true, leftover: [] };
|
|
1189
|
+
}
|
|
1190
|
+
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";
|
|
1191
|
+
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
1192
|
function conflictError(message) {
|
|
982
1193
|
const error = new Error(message);
|
|
983
1194
|
error.status = 409;
|
|
@@ -1004,7 +1215,7 @@ async function ensureNoActiveWriter(sp, sid) {
|
|
|
1004
1215
|
try {
|
|
1005
1216
|
handle = await sp.open(sid, "write");
|
|
1006
1217
|
} catch (e) {
|
|
1007
|
-
if (isAlreadyOwned(e)) throw conflictError(
|
|
1218
|
+
if (isAlreadyOwned(e)) throw conflictError(BUSY_MOVE_MESSAGE);
|
|
1008
1219
|
throw e;
|
|
1009
1220
|
}
|
|
1010
1221
|
await closeQuietly2(handle);
|
|
@@ -1020,13 +1231,13 @@ async function purgeSessionArtifacts(sp, sid, header) {
|
|
|
1020
1231
|
try {
|
|
1021
1232
|
writer = await sp.open(sid, "write");
|
|
1022
1233
|
} catch (e) {
|
|
1023
|
-
if (isAlreadyOwned(e)) throw conflictError(
|
|
1234
|
+
if (isAlreadyOwned(e)) throw conflictError(BUSY_PURGE_MESSAGE);
|
|
1024
1235
|
throw e;
|
|
1025
1236
|
}
|
|
1026
1237
|
await closeQuietly2(writer);
|
|
1027
1238
|
writer = null;
|
|
1028
1239
|
try {
|
|
1029
|
-
await
|
|
1240
|
+
await rm2(artifacts.sessionDir, { recursive: true, force: true });
|
|
1030
1241
|
} catch (e) {
|
|
1031
1242
|
const error = new Error("\u5220\u9664\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1032
1243
|
error.status = 500;
|
|
@@ -1042,8 +1253,8 @@ async function purgeSessionArtifacts(sp, sid, header) {
|
|
|
1042
1253
|
}
|
|
1043
1254
|
return artifacts;
|
|
1044
1255
|
}
|
|
1045
|
-
async function relocateRewrittenBackup({ sid, canonical,
|
|
1046
|
-
const original = await readFile2(
|
|
1256
|
+
async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts }) {
|
|
1257
|
+
const original = await readFile2(backupLogPath);
|
|
1047
1258
|
const frames = scanZstdFrames(original).frames;
|
|
1048
1259
|
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
1260
|
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
@@ -1053,26 +1264,66 @@ async function relocateRewrittenBackup({ sid, canonical, backupPath, artifacts }
|
|
|
1053
1264
|
throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
1054
1265
|
}
|
|
1055
1266
|
const targetDir = deriveSessionDir(artifacts.root, canonical, sid);
|
|
1056
|
-
await
|
|
1057
|
-
const staged =
|
|
1058
|
-
await
|
|
1059
|
-
await
|
|
1267
|
+
await mkdir5(targetDir, { recursive: true });
|
|
1268
|
+
const staged = join7(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
|
|
1269
|
+
await writeFile5(staged, rewritten, { mode: 384 });
|
|
1270
|
+
await rename5(staged, join7(targetDir, artifacts.generationFiles[0]));
|
|
1060
1271
|
return targetDir;
|
|
1061
1272
|
}
|
|
1062
1273
|
async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
|
|
1274
|
+
let probeError = null;
|
|
1275
|
+
try {
|
|
1276
|
+
await ensureNoActiveWriter(sp, sid);
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
if (e && e.status === 409) throw e;
|
|
1279
|
+
probeError = e;
|
|
1280
|
+
}
|
|
1063
1281
|
const artifacts = await locateSessionArtifacts(sp, header);
|
|
1064
1282
|
if (!artifacts) {
|
|
1065
1283
|
const error = new Error("\u65E0\u6CD5\u5B9A\u4F4D\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8");
|
|
1066
1284
|
error.status = 409;
|
|
1067
1285
|
throw error;
|
|
1068
1286
|
}
|
|
1069
|
-
|
|
1287
|
+
if (probeError) {
|
|
1288
|
+
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));
|
|
1289
|
+
error.status = 409;
|
|
1290
|
+
throw error;
|
|
1291
|
+
}
|
|
1292
|
+
const HEADER_ONLY_MAX = 4096;
|
|
1293
|
+
if (events.length === 0) {
|
|
1294
|
+
let sourceBytes = null;
|
|
1295
|
+
try {
|
|
1296
|
+
sourceBytes = (await stat3(artifacts.logPath)).size;
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
sourceBytes = null;
|
|
1299
|
+
}
|
|
1300
|
+
if (sourceBytes !== null && sourceBytes > HEADER_ONLY_MAX) {
|
|
1301
|
+
const error = new Error(
|
|
1302
|
+
`\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`
|
|
1303
|
+
);
|
|
1304
|
+
error.status = 409;
|
|
1305
|
+
error.code = "DSM_MOVE_EMPTY_READ";
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
const siblings = await cleanSiblingCopies(artifacts.root, sid, artifacts.sessionDir, artifacts.generationVersion);
|
|
1310
|
+
if (siblings.real.length > 0) {
|
|
1311
|
+
const error = new Error(
|
|
1312
|
+
"\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")
|
|
1313
|
+
);
|
|
1314
|
+
error.status = 409;
|
|
1315
|
+
error.code = "DSM_SESSION_DUP_LOG";
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1070
1318
|
const newHeader = Object.assign({}, header, { cwd: canonical });
|
|
1071
1319
|
const firstSeq = events.length ? Number(events[0].seq) : 0;
|
|
1072
1320
|
const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
|
|
1073
1321
|
const createOptions = header.isSeeded && Number.isSafeInteger(inheritedEventCount) && inheritedEventCount > 0 ? { inheritedEventCount } : void 0;
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1322
|
+
const backupRoot = join7(dirname2(artifacts.root), "sessions-manager-move-backup");
|
|
1323
|
+
const backupDir = join7(backupRoot, `${encodeSegmentFor(sid)}-${process.pid}-${Date.now()}`);
|
|
1324
|
+
await mkdir5(backupRoot, { recursive: true });
|
|
1325
|
+
const backupLogPath = join7(backupDir, basename2(artifacts.logPath));
|
|
1326
|
+
await rename5(artifacts.sessionDir, backupDir);
|
|
1076
1327
|
let writer = null;
|
|
1077
1328
|
try {
|
|
1078
1329
|
try {
|
|
@@ -1085,7 +1336,7 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1085
1336
|
writer = null;
|
|
1086
1337
|
} catch (e) {
|
|
1087
1338
|
if (isAlreadyExists(e)) {
|
|
1088
|
-
await relocateRewrittenBackup({ sid, canonical,
|
|
1339
|
+
await relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts });
|
|
1089
1340
|
} else {
|
|
1090
1341
|
throw e;
|
|
1091
1342
|
}
|
|
@@ -1101,11 +1352,15 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1101
1352
|
} catch (e) {
|
|
1102
1353
|
await closeQuietly2(writer);
|
|
1103
1354
|
try {
|
|
1104
|
-
await
|
|
1355
|
+
await rm2(deriveSessionDir(artifacts.root, canonical, sid), { recursive: true, force: true });
|
|
1105
1356
|
} catch (_) {
|
|
1106
1357
|
}
|
|
1107
1358
|
try {
|
|
1108
|
-
await
|
|
1359
|
+
await mkdir5(dirname2(artifacts.sessionDir), { recursive: true });
|
|
1360
|
+
} catch (_) {
|
|
1361
|
+
}
|
|
1362
|
+
try {
|
|
1363
|
+
await rename5(backupDir, artifacts.sessionDir);
|
|
1109
1364
|
} catch (_) {
|
|
1110
1365
|
}
|
|
1111
1366
|
if (e && e.status) throw e;
|
|
@@ -1113,19 +1368,53 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1113
1368
|
error.status = 500;
|
|
1114
1369
|
throw error;
|
|
1115
1370
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1371
|
+
const sourceCleanup = await removeSourceDir(backupDir);
|
|
1372
|
+
return {
|
|
1373
|
+
sessionDir: deriveSessionDir(artifacts.root, canonical, sid),
|
|
1374
|
+
sourceCleanup,
|
|
1375
|
+
reclaimedSiblings: siblings.superseded
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/lineage.js
|
|
1380
|
+
var EMPTY_BASE = 190;
|
|
1381
|
+
function isEmptyLogSize(sizeBytes, cwd) {
|
|
1382
|
+
if (!Number.isFinite(sizeBytes) || sizeBytes < 0) return false;
|
|
1383
|
+
const cwdLen = typeof cwd === "string" ? cwd.length : 0;
|
|
1384
|
+
return sizeBytes <= EMPTY_BASE + cwdLen;
|
|
1385
|
+
}
|
|
1386
|
+
var EMPTY_DECODE_LIMIT = 8192;
|
|
1387
|
+
var LIFECYCLE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1388
|
+
"session",
|
|
1389
|
+
"permission/preset",
|
|
1390
|
+
"sandbox/mode",
|
|
1391
|
+
"approval/policy"
|
|
1392
|
+
]);
|
|
1393
|
+
function isEmptyEventTypes(types) {
|
|
1394
|
+
if (!Array.isArray(types)) return false;
|
|
1395
|
+
for (const t of types) {
|
|
1396
|
+
if (typeof t !== "string" || !LIFECYCLE_EVENT_TYPES.has(t)) return false;
|
|
1397
|
+
}
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
function classifyLineage(header, sizeBytes) {
|
|
1401
|
+
if (!header || typeof header !== "object") return null;
|
|
1402
|
+
const origin = header.origin === "subagent" ? "subagent" : null;
|
|
1403
|
+
const parentSession = typeof header.parentSession === "string" && header.parentSession ? header.parentSession : null;
|
|
1404
|
+
const delegationDepth = Number.isSafeInteger(header.delegationDepth) && header.delegationDepth > 0 ? header.delegationDepth : 0;
|
|
1405
|
+
const empty = isEmptyLogSize(sizeBytes, header.cwd);
|
|
1406
|
+
if (!origin && !parentSession && !empty) return null;
|
|
1407
|
+
return { origin, parentSession, delegationDepth, empty };
|
|
1121
1408
|
}
|
|
1122
1409
|
|
|
1123
1410
|
// src/index.js
|
|
1411
|
+
var BUILD_STAMP = true ? "3.6.0 built 2026-09-10T09:47:49.428Z" : "dev";
|
|
1124
1412
|
var name = "dsh-sessions-manager";
|
|
1125
1413
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
1126
1414
|
var MAX_TITLE = 80;
|
|
1127
|
-
var
|
|
1128
|
-
var
|
|
1415
|
+
var STATE_DIR = process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join8(homedir4(), ".dsh", "sessions-manager");
|
|
1416
|
+
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join8(homedir4(), ".dsh", "sessions-manager-trash");
|
|
1417
|
+
var TRASH_INDEX = join8(TRASH_DIR, "index.json");
|
|
1129
1418
|
var TRASH_SCHEMA_VERSION = 2;
|
|
1130
1419
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
1131
1420
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
@@ -1227,7 +1516,7 @@ function apply(ctx) {
|
|
|
1227
1516
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
1228
1517
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
1229
1518
|
const metaCache = createSessionMetaCache();
|
|
1230
|
-
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file:
|
|
1519
|
+
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join8(TRASH_DIR, "title-index.json") });
|
|
1231
1520
|
async function hydrateFromPersist(ids, statsById) {
|
|
1232
1521
|
const hits = /* @__PURE__ */ new Map();
|
|
1233
1522
|
if (!ids || !ids.length) return hits;
|
|
@@ -1238,10 +1527,10 @@ function apply(ctx) {
|
|
|
1238
1527
|
return hits;
|
|
1239
1528
|
}
|
|
1240
1529
|
for (const id of ids) {
|
|
1241
|
-
const
|
|
1530
|
+
const stat5 = statsById.get(id);
|
|
1242
1531
|
const entry = store && store[id];
|
|
1243
|
-
if (!
|
|
1244
|
-
const fp = fingerprintOf(
|
|
1532
|
+
if (!stat5 || !entry) continue;
|
|
1533
|
+
const fp = fingerprintOf(stat5);
|
|
1245
1534
|
if (!isPersistableFingerprint(fp)) continue;
|
|
1246
1535
|
if (fp && entry.fingerprint === fp) {
|
|
1247
1536
|
hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
|
|
@@ -1249,7 +1538,7 @@ function apply(ctx) {
|
|
|
1249
1538
|
}
|
|
1250
1539
|
return hits;
|
|
1251
1540
|
}
|
|
1252
|
-
function persistDecoded(decoded, statsById) {
|
|
1541
|
+
async function persistDecoded(decoded, statsById) {
|
|
1253
1542
|
if (!decoded || !decoded.size) return;
|
|
1254
1543
|
const batch = {};
|
|
1255
1544
|
const now = Date.now();
|
|
@@ -1259,8 +1548,10 @@ function apply(ctx) {
|
|
|
1259
1548
|
batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
|
|
1260
1549
|
}
|
|
1261
1550
|
if (!Object.keys(batch).length) return;
|
|
1262
|
-
|
|
1263
|
-
|
|
1551
|
+
try {
|
|
1552
|
+
await titleIndex.merge(batch);
|
|
1553
|
+
} catch (e) {
|
|
1554
|
+
}
|
|
1264
1555
|
}
|
|
1265
1556
|
function metaFromSnapshot(o) {
|
|
1266
1557
|
let title = null, createdAt = null, cwd = null;
|
|
@@ -1406,7 +1697,7 @@ function apply(ctx) {
|
|
|
1406
1697
|
try {
|
|
1407
1698
|
const loc = persistence.locate(header);
|
|
1408
1699
|
if (!loc || typeof loc.path !== "string" || !loc.path) return;
|
|
1409
|
-
const st = await
|
|
1700
|
+
const st = await stat4(loc.path);
|
|
1410
1701
|
if (!st) return;
|
|
1411
1702
|
if (typeof st.size === "number") sizeById.set(id, st.size);
|
|
1412
1703
|
if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) {
|
|
@@ -1437,7 +1728,7 @@ function apply(ctx) {
|
|
|
1437
1728
|
}
|
|
1438
1729
|
async function readTrashStore() {
|
|
1439
1730
|
try {
|
|
1440
|
-
return normalizeTrashStore(JSON.parse(
|
|
1731
|
+
return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
|
|
1441
1732
|
} catch (e) {
|
|
1442
1733
|
return normalizeTrashStore(null);
|
|
1443
1734
|
}
|
|
@@ -1446,10 +1737,10 @@ function apply(ctx) {
|
|
|
1446
1737
|
return (await readTrashStore()).items;
|
|
1447
1738
|
}
|
|
1448
1739
|
async function writeTrashStore(store) {
|
|
1449
|
-
await
|
|
1450
|
-
const tmp =
|
|
1451
|
-
await
|
|
1452
|
-
await
|
|
1740
|
+
await mkdir6(TRASH_DIR, { recursive: true });
|
|
1741
|
+
const tmp = join8(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
|
|
1742
|
+
await writeFile6(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
1743
|
+
await rename6(tmp, TRASH_INDEX);
|
|
1453
1744
|
}
|
|
1454
1745
|
function mutateTrash(mutator) {
|
|
1455
1746
|
const operation = trashMutation.then(async () => {
|
|
@@ -1463,7 +1754,10 @@ function apply(ctx) {
|
|
|
1463
1754
|
return operation;
|
|
1464
1755
|
}
|
|
1465
1756
|
const stars = createStarIndex();
|
|
1757
|
+
const pendingMoves = createPendingMoveStore();
|
|
1466
1758
|
const autoArchive = createAutoArchiveStore();
|
|
1759
|
+
sweepStaleStateTemps([STATE_DIR, TRASH_DIR], {}).catch(() => {
|
|
1760
|
+
});
|
|
1467
1761
|
async function gcStars(validIds) {
|
|
1468
1762
|
try {
|
|
1469
1763
|
const store = await stars.read();
|
|
@@ -1550,10 +1844,10 @@ function apply(ctx) {
|
|
|
1550
1844
|
let exists = false;
|
|
1551
1845
|
let header = null;
|
|
1552
1846
|
try {
|
|
1553
|
-
const
|
|
1554
|
-
if (
|
|
1847
|
+
const stat5 = await persistence.statSession(sid);
|
|
1848
|
+
if (stat5) {
|
|
1555
1849
|
exists = true;
|
|
1556
|
-
header =
|
|
1850
|
+
header = stat5.header;
|
|
1557
1851
|
}
|
|
1558
1852
|
} catch (e) {
|
|
1559
1853
|
}
|
|
@@ -1584,7 +1878,7 @@ function apply(ctx) {
|
|
|
1584
1878
|
if (loc && typeof loc.path === "string") originalPath = loc.path;
|
|
1585
1879
|
}
|
|
1586
1880
|
if (originalPath) {
|
|
1587
|
-
const existsOnDisk = await
|
|
1881
|
+
const existsOnDisk = await stat4(originalPath).then(() => true).catch(() => false);
|
|
1588
1882
|
if (!existsOnDisk) {
|
|
1589
1883
|
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
1884
|
}
|
|
@@ -1697,7 +1991,7 @@ function apply(ctx) {
|
|
|
1697
1991
|
await purgeSessionArtifacts(sp, sid, locatedHeader);
|
|
1698
1992
|
} else if (target) {
|
|
1699
1993
|
try {
|
|
1700
|
-
await
|
|
1994
|
+
await unlink(target);
|
|
1701
1995
|
} catch (e) {
|
|
1702
1996
|
if (e && e.code !== "ENOENT") throw new Error("\u5220\u9664\u6587\u4EF6\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1703
1997
|
}
|
|
@@ -1730,8 +2024,10 @@ function apply(ctx) {
|
|
|
1730
2024
|
purged = true;
|
|
1731
2025
|
});
|
|
1732
2026
|
if (!purged) throw new Error("\u5F7B\u5E95\u5220\u9664\u5931\u8D25");
|
|
1733
|
-
|
|
1734
|
-
|
|
2027
|
+
try {
|
|
2028
|
+
await stars.removeIds([sid]);
|
|
2029
|
+
} catch (e) {
|
|
2030
|
+
}
|
|
1735
2031
|
return { ok: true, purged: true };
|
|
1736
2032
|
}
|
|
1737
2033
|
async function trashSettings(next) {
|
|
@@ -1767,8 +2063,8 @@ function apply(ctx) {
|
|
|
1767
2063
|
async function moveTargetWorkspace(rawPath) {
|
|
1768
2064
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
1769
2065
|
let p = String(rawPath).trim();
|
|
1770
|
-
if (p.startsWith("~/")) p =
|
|
1771
|
-
if (!isAbsolute(p)) p =
|
|
2066
|
+
if (p.startsWith("~/")) p = join8(homedir4(), p.slice(2));
|
|
2067
|
+
if (!isAbsolute(p)) p = join8(homedir4(), p);
|
|
1772
2068
|
let canonical = null;
|
|
1773
2069
|
try {
|
|
1774
2070
|
canonical = await realpath(p);
|
|
@@ -1776,16 +2072,19 @@ function apply(ctx) {
|
|
|
1776
2072
|
canonical = null;
|
|
1777
2073
|
}
|
|
1778
2074
|
if (canonical === null) {
|
|
1779
|
-
await
|
|
2075
|
+
await mkdir6(p, { recursive: true });
|
|
1780
2076
|
canonical = await realpath(p);
|
|
1781
2077
|
}
|
|
1782
|
-
return { canonical, entity: await w.create(canonical,
|
|
2078
|
+
return { canonical, entity: await w.create(canonical, basename3(canonical) || "workspace") };
|
|
1783
2079
|
}
|
|
1784
2080
|
async function moveOne(sid, targetPath) {
|
|
1785
2081
|
requireCapability(capabilities, "move");
|
|
1786
2082
|
const activeId = getActiveSessionId(ctx);
|
|
1787
2083
|
if (activeId != null && String(activeId) === String(sid)) {
|
|
1788
|
-
|
|
2084
|
+
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");
|
|
2085
|
+
error.status = 409;
|
|
2086
|
+
error.code = "DSM_SESSION_BUSY";
|
|
2087
|
+
throw error;
|
|
1789
2088
|
}
|
|
1790
2089
|
const r = await persistence.readSession(sid, 0);
|
|
1791
2090
|
if (!r || !r.meta) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
|
|
@@ -1805,6 +2104,7 @@ function apply(ctx) {
|
|
|
1805
2104
|
}
|
|
1806
2105
|
}
|
|
1807
2106
|
const newHeader = Object.assign({}, meta, { cwd: canonical });
|
|
2107
|
+
const moveNotes = [];
|
|
1808
2108
|
const live = ctx.get("sessions");
|
|
1809
2109
|
const liveObj = live && live.get && live.get(sid);
|
|
1810
2110
|
const isOpen = !!liveObj;
|
|
@@ -1817,14 +2117,14 @@ function apply(ctx) {
|
|
|
1817
2117
|
const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
|
|
1818
2118
|
let destinationInstalled = false;
|
|
1819
2119
|
try {
|
|
1820
|
-
await
|
|
2120
|
+
await mkdir6(dirname3(newPath), { recursive: true });
|
|
1821
2121
|
try {
|
|
1822
|
-
await
|
|
2122
|
+
await stat4(newPath);
|
|
1823
2123
|
throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1824
2124
|
} catch (e) {
|
|
1825
2125
|
if (e && e.code !== "ENOENT") throw e;
|
|
1826
2126
|
}
|
|
1827
|
-
await
|
|
2127
|
+
await rename6(oldPath, backupPath);
|
|
1828
2128
|
const original = await readFile3(backupPath);
|
|
1829
2129
|
const originalFrames = scanZstdFrames(original).frames;
|
|
1830
2130
|
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 +2134,25 @@ function apply(ctx) {
|
|
|
1834
2134
|
const originalTail = original.subarray(originalFrames[0].end);
|
|
1835
2135
|
const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
|
|
1836
2136
|
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
|
|
2137
|
+
await writeFile6(stagedPath, rewritten, { mode: 384 });
|
|
2138
|
+
await rename6(stagedPath, newPath);
|
|
1839
2139
|
destinationInstalled = true;
|
|
1840
|
-
await
|
|
2140
|
+
await unlink(backupPath);
|
|
2141
|
+
const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
|
|
2142
|
+
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
2143
|
} catch (e) {
|
|
1842
2144
|
try {
|
|
1843
|
-
await
|
|
2145
|
+
await unlink(stagedPath);
|
|
1844
2146
|
} catch (_) {
|
|
1845
2147
|
}
|
|
1846
2148
|
if (destinationInstalled) {
|
|
1847
2149
|
try {
|
|
1848
|
-
await
|
|
2150
|
+
await unlink(newPath);
|
|
1849
2151
|
} catch (_) {
|
|
1850
2152
|
}
|
|
1851
2153
|
}
|
|
1852
2154
|
try {
|
|
1853
|
-
await
|
|
2155
|
+
await rename6(backupPath, oldPath);
|
|
1854
2156
|
} catch (_) {
|
|
1855
2157
|
}
|
|
1856
2158
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -1874,6 +2176,20 @@ function apply(ctx) {
|
|
|
1874
2176
|
}
|
|
1875
2177
|
return null;
|
|
1876
2178
|
};
|
|
2179
|
+
const cleanupMovedSourceDir = async (dir) => {
|
|
2180
|
+
let entries = null;
|
|
2181
|
+
try {
|
|
2182
|
+
entries = await readdir4(dir);
|
|
2183
|
+
} catch (e) {
|
|
2184
|
+
return [];
|
|
2185
|
+
}
|
|
2186
|
+
try {
|
|
2187
|
+
await rm3(dir, { recursive: true, force: true });
|
|
2188
|
+
} catch (e) {
|
|
2189
|
+
return entries;
|
|
2190
|
+
}
|
|
2191
|
+
return [];
|
|
2192
|
+
};
|
|
1877
2193
|
if (persistence.kind === "session-handle") {
|
|
1878
2194
|
const stat1 = await persistence.statSession(sid);
|
|
1879
2195
|
if (stat1 && stat1.revision) {
|
|
@@ -1883,7 +2199,13 @@ function apply(ctx) {
|
|
|
1883
2199
|
}
|
|
1884
2200
|
}
|
|
1885
2201
|
try {
|
|
1886
|
-
await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
|
|
2202
|
+
const movedHandle = await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
|
|
2203
|
+
if (movedHandle && Array.isArray(movedHandle.reclaimedSiblings) && movedHandle.reclaimedSiblings.length > 0) {
|
|
2204
|
+
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`);
|
|
2205
|
+
}
|
|
2206
|
+
if (movedHandle && movedHandle.sourceCleanup && movedHandle.sourceCleanup.cleaned === false) {
|
|
2207
|
+
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`);
|
|
2208
|
+
}
|
|
1887
2209
|
} catch (e) {
|
|
1888
2210
|
if (e && e.status) throw e;
|
|
1889
2211
|
throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
@@ -1910,7 +2232,7 @@ function apply(ctx) {
|
|
|
1910
2232
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
1911
2233
|
if (backupPath) {
|
|
1912
2234
|
try {
|
|
1913
|
-
await
|
|
2235
|
+
await rename6(oldPath, backupPath);
|
|
1914
2236
|
} catch (e) {
|
|
1915
2237
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1916
2238
|
}
|
|
@@ -1918,7 +2240,7 @@ function apply(ctx) {
|
|
|
1918
2240
|
const restore = async () => {
|
|
1919
2241
|
if (backupPath) {
|
|
1920
2242
|
try {
|
|
1921
|
-
await
|
|
2243
|
+
await rename6(backupPath, oldPath);
|
|
1922
2244
|
} catch (_) {
|
|
1923
2245
|
}
|
|
1924
2246
|
}
|
|
@@ -1932,10 +2254,14 @@ function apply(ctx) {
|
|
|
1932
2254
|
}
|
|
1933
2255
|
if (backupPath) {
|
|
1934
2256
|
try {
|
|
1935
|
-
await
|
|
2257
|
+
await unlink(backupPath);
|
|
1936
2258
|
} catch (e) {
|
|
1937
2259
|
}
|
|
1938
2260
|
}
|
|
2261
|
+
if (oldPath) {
|
|
2262
|
+
const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
|
|
2263
|
+
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`);
|
|
2264
|
+
}
|
|
1939
2265
|
} catch (e) {
|
|
1940
2266
|
if (ALREADY_EXISTS_RE.test(String(e && e.message || e))) {
|
|
1941
2267
|
await restore();
|
|
@@ -1979,9 +2305,107 @@ function apply(ctx) {
|
|
|
1979
2305
|
moved: true,
|
|
1980
2306
|
workspaceId: target.id,
|
|
1981
2307
|
workspaceTitle: target.title,
|
|
1982
|
-
workspacePath: canonical
|
|
2308
|
+
workspacePath: canonical,
|
|
2309
|
+
...moveNotes.length > 0 ? { notes: moveNotes } : {}
|
|
1983
2310
|
};
|
|
1984
2311
|
}
|
|
2312
|
+
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";
|
|
2313
|
+
function isBusyError(e) {
|
|
2314
|
+
if (!e) return false;
|
|
2315
|
+
if (e.code === "DSM_SESSION_BUSY") return true;
|
|
2316
|
+
const text = String(e && e.message || e);
|
|
2317
|
+
return /正被 DSH 打开|already owned/i.test(text);
|
|
2318
|
+
}
|
|
2319
|
+
async function moveOrQueue(sid, targetPath) {
|
|
2320
|
+
try {
|
|
2321
|
+
return await moveOne(sid, targetPath);
|
|
2322
|
+
} catch (e) {
|
|
2323
|
+
if (!isBusyError(e)) throw e;
|
|
2324
|
+
await pendingMoves.queue(sid, targetPath);
|
|
2325
|
+
return { ok: true, moved: false, queued: true, notes: [QUEUE_NOTE] };
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
const BOOT_WARMUP_MS = 3e4;
|
|
2329
|
+
const bootedAt = Date.now();
|
|
2330
|
+
let queueRunning = false;
|
|
2331
|
+
async function runPendingMoves(reason) {
|
|
2332
|
+
if (queueRunning) return { ran: false, moved: 0, kept: 0 };
|
|
2333
|
+
queueRunning = true;
|
|
2334
|
+
let moved = 0;
|
|
2335
|
+
const notes = [];
|
|
2336
|
+
try {
|
|
2337
|
+
const items = await pendingMoves.list();
|
|
2338
|
+
for (const item of items) {
|
|
2339
|
+
try {
|
|
2340
|
+
await moveOne(item.sessionId, item.targetPath);
|
|
2341
|
+
await pendingMoves.remove([item.sessionId]);
|
|
2342
|
+
metaCache.invalidate(item.sessionId);
|
|
2343
|
+
moved++;
|
|
2344
|
+
notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u5DF2\u5B8C\u6210\uFF1A${String(item.sessionId).slice(0, 18)}\u2026`);
|
|
2345
|
+
} catch (e) {
|
|
2346
|
+
if (isBusyError(e)) continue;
|
|
2347
|
+
if (Date.now() - bootedAt < BOOT_WARMUP_MS) continue;
|
|
2348
|
+
const bumped = await pendingMoves.bumpAttempts(item.sessionId);
|
|
2349
|
+
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`);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
if (moved > 0) {
|
|
2353
|
+
try {
|
|
2354
|
+
await reindexRegistry();
|
|
2355
|
+
} catch (e) {
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
} finally {
|
|
2359
|
+
queueRunning = false;
|
|
2360
|
+
}
|
|
2361
|
+
if (notes.length > 0) {
|
|
2362
|
+
try {
|
|
2363
|
+
for (const n of notes) console.warn("[dsh-sessions-manager] " + n);
|
|
2364
|
+
} catch (e) {
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
void reason;
|
|
2368
|
+
return { ran: true, moved, kept: (await pendingMoves.list()).length };
|
|
2369
|
+
}
|
|
2370
|
+
const queueEffect = typeof ctx.effect === "function" ? ctx.effect.bind(ctx) : ((fn) => {
|
|
2371
|
+
fn();
|
|
2372
|
+
});
|
|
2373
|
+
if (typeof ctx.on === "function") {
|
|
2374
|
+
queueEffect(() => ctx.on("session/disposed", (session) => {
|
|
2375
|
+
const id = session && session.id != null ? String(session.id) : null;
|
|
2376
|
+
if (!id) return;
|
|
2377
|
+
pendingMoves.has(id).then((queued) => {
|
|
2378
|
+
if (!queued) return;
|
|
2379
|
+
const t = setTimeout(() => {
|
|
2380
|
+
runPendingMoves("session-disposed").catch(() => {
|
|
2381
|
+
});
|
|
2382
|
+
}, 500);
|
|
2383
|
+
if (t && typeof t.unref === "function") t.unref();
|
|
2384
|
+
}).catch(() => {
|
|
2385
|
+
});
|
|
2386
|
+
}));
|
|
2387
|
+
}
|
|
2388
|
+
queueEffect(() => {
|
|
2389
|
+
const timers = [0, 1e3, 3e3, 6e3, 12e3, 3e4].map((ms) => {
|
|
2390
|
+
const t = setTimeout(() => {
|
|
2391
|
+
runPendingMoves(ms === 0 ? "startup" : `startup+${ms}`).catch(() => {
|
|
2392
|
+
});
|
|
2393
|
+
}, ms);
|
|
2394
|
+
if (t && typeof t.unref === "function") t.unref();
|
|
2395
|
+
return t;
|
|
2396
|
+
});
|
|
2397
|
+
const tickTimer = setInterval(() => {
|
|
2398
|
+
pendingMoves.list().then((items) => {
|
|
2399
|
+
if (items.length > 0) return runPendingMoves("tick");
|
|
2400
|
+
}).catch(() => {
|
|
2401
|
+
});
|
|
2402
|
+
}, 12e4);
|
|
2403
|
+
if (tickTimer && typeof tickTimer.unref === "function") tickTimer.unref();
|
|
2404
|
+
return () => {
|
|
2405
|
+
for (const t of timers) clearTimeout(t);
|
|
2406
|
+
clearInterval(tickTimer);
|
|
2407
|
+
};
|
|
2408
|
+
});
|
|
1985
2409
|
async function reindexRegistry() {
|
|
1986
2410
|
const reg = w;
|
|
1987
2411
|
if (!reg || typeof reg.replaceHeaderIndex !== "function") return false;
|
|
@@ -2095,7 +2519,7 @@ function apply(ctx) {
|
|
|
2095
2519
|
}));
|
|
2096
2520
|
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
2097
2521
|
}
|
|
2098
|
-
persistDecoded(decoded, statsById);
|
|
2522
|
+
await persistDecoded(decoded, statsById);
|
|
2099
2523
|
let starredSet = /* @__PURE__ */ new Set();
|
|
2100
2524
|
try {
|
|
2101
2525
|
starredSet = new Set((await stars.read()).starredSessionIds);
|
|
@@ -2202,14 +2626,61 @@ function apply(ctx) {
|
|
|
2202
2626
|
}
|
|
2203
2627
|
if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
|
|
2204
2628
|
}
|
|
2205
|
-
persistDecoded(decoded, statsById);
|
|
2629
|
+
await persistDecoded(decoded, statsById);
|
|
2206
2630
|
}
|
|
2631
|
+
const lineage = {};
|
|
2632
|
+
for (const entry of entries) {
|
|
2633
|
+
const info = classifyLineage(entry.header, entry.sizeBytes);
|
|
2634
|
+
if (info) lineage[entry.id] = info;
|
|
2635
|
+
}
|
|
2636
|
+
await refineEmptyLineage(lineage, entries);
|
|
2207
2637
|
return {
|
|
2208
2638
|
titles: Object.fromEntries(authorityTitleCache),
|
|
2209
2639
|
trashedSessionIds: store.items.map((item) => String(item.sessionId)),
|
|
2210
|
-
purgedSessionIds: activeTombstones
|
|
2640
|
+
purgedSessionIds: activeTombstones,
|
|
2641
|
+
lineage
|
|
2211
2642
|
};
|
|
2212
2643
|
}
|
|
2644
|
+
const emptyScanCache = /* @__PURE__ */ new Map();
|
|
2645
|
+
async function refineEmptyLineage(lineage, entries) {
|
|
2646
|
+
if (!persistence || typeof persistence.inspectSession !== "function") return;
|
|
2647
|
+
for (const entry of entries) {
|
|
2648
|
+
const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
|
|
2649
|
+
if (size === null || size > EMPTY_DECODE_LIMIT) continue;
|
|
2650
|
+
const id = String(entry.id);
|
|
2651
|
+
let scanned = emptyScanCache.get(id);
|
|
2652
|
+
if (!scanned || scanned.sizeBytes !== size) {
|
|
2653
|
+
let isEmpty = false;
|
|
2654
|
+
try {
|
|
2655
|
+
const types = [];
|
|
2656
|
+
await persistence.inspectSession(id, { onEvents: (batch) => {
|
|
2657
|
+
for (const ev of batch || []) if (types.length < 64) types.push(ev && ev.type);
|
|
2658
|
+
} });
|
|
2659
|
+
isEmpty = isEmptyEventTypes(types);
|
|
2660
|
+
} catch (e) {
|
|
2661
|
+
isEmpty = false;
|
|
2662
|
+
}
|
|
2663
|
+
scanned = { sizeBytes: size, empty: isEmpty };
|
|
2664
|
+
emptyScanCache.set(id, scanned);
|
|
2665
|
+
}
|
|
2666
|
+
if (scanned.empty) {
|
|
2667
|
+
const info = lineage[id] || (lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: false });
|
|
2668
|
+
info.empty = true;
|
|
2669
|
+
} else if (lineage[id]) {
|
|
2670
|
+
const info = lineage[id];
|
|
2671
|
+
info.empty = false;
|
|
2672
|
+
if (!info.origin && !info.parentSession) delete lineage[id];
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
const live = /* @__PURE__ */ new Set();
|
|
2676
|
+
for (const entry of entries) {
|
|
2677
|
+
const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
|
|
2678
|
+
if (size !== null && size <= EMPTY_DECODE_LIMIT) live.add(String(entry.id));
|
|
2679
|
+
}
|
|
2680
|
+
for (const key of emptyScanCache.keys()) {
|
|
2681
|
+
if (!live.has(key)) emptyScanCache.delete(key);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2213
2684
|
async function buildDetails(sid, signal) {
|
|
2214
2685
|
const sessions = ctx.get("sessions");
|
|
2215
2686
|
const live = sessions && sessions.get(sid);
|
|
@@ -2294,8 +2765,8 @@ function apply(ctx) {
|
|
|
2294
2765
|
let sizeBytes = null;
|
|
2295
2766
|
if (live === void 0) {
|
|
2296
2767
|
try {
|
|
2297
|
-
const
|
|
2298
|
-
if (
|
|
2768
|
+
const stat5 = await persistence.statSession(sid);
|
|
2769
|
+
if (stat5 && Number.isFinite(stat5.sizeBytes)) sizeBytes = stat5.sizeBytes;
|
|
2299
2770
|
} catch (e) {
|
|
2300
2771
|
}
|
|
2301
2772
|
}
|
|
@@ -2303,7 +2774,7 @@ function apply(ctx) {
|
|
|
2303
2774
|
try {
|
|
2304
2775
|
const loc = persistence.locate(meta);
|
|
2305
2776
|
if (loc && typeof loc.path === "string" && loc.path) {
|
|
2306
|
-
const st = await
|
|
2777
|
+
const st = await stat4(loc.path);
|
|
2307
2778
|
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
2308
2779
|
}
|
|
2309
2780
|
} catch (e) {
|
|
@@ -2312,7 +2783,7 @@ function apply(ctx) {
|
|
|
2312
2783
|
}
|
|
2313
2784
|
if (stats.fetches.length > MAX_FETCHES) stats.fetches = stats.fetches.slice(0, MAX_FETCHES);
|
|
2314
2785
|
const fileEntries = [...fileSet.entries()].slice(0, MAX_FILES * 2);
|
|
2315
|
-
const exists = await Promise.all(fileEntries.map(([p]) =>
|
|
2786
|
+
const exists = await Promise.all(fileEntries.map(([p]) => stat4(p).then(() => true).catch(() => false)));
|
|
2316
2787
|
const files = fileEntries.filter((_, i) => exists[i]).map(([path, tool]) => ({ path, tool })).slice(0, MAX_FILES);
|
|
2317
2788
|
const lineage = {
|
|
2318
2789
|
parentSessionId: meta && typeof meta.parentSession === "string" ? meta.parentSession : null,
|
|
@@ -2364,7 +2835,7 @@ function apply(ctx) {
|
|
|
2364
2835
|
disposers.push(ctx.webServer.register({
|
|
2365
2836
|
kind: "exact",
|
|
2366
2837
|
path: "/archived-sessions/capabilities",
|
|
2367
|
-
handler: async (req, res) => json(res, capabilities)
|
|
2838
|
+
handler: async (req, res) => json(res, { ...capabilities, buildStamp: BUILD_STAMP })
|
|
2368
2839
|
}));
|
|
2369
2840
|
disposers.push(ctx.webServer.register({
|
|
2370
2841
|
kind: "exact",
|
|
@@ -2520,7 +2991,7 @@ function apply(ctx) {
|
|
|
2520
2991
|
if (typeof item.originalPath !== "string" || !item.originalPath) {
|
|
2521
2992
|
return { sessionId: item.sessionId, status: "unverified", originalPath: null };
|
|
2522
2993
|
}
|
|
2523
|
-
const exists = await
|
|
2994
|
+
const exists = await stat4(item.originalPath).then(() => true).catch(() => false);
|
|
2524
2995
|
return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
|
|
2525
2996
|
}));
|
|
2526
2997
|
json(res, {
|
|
@@ -2561,8 +3032,10 @@ function apply(ctx) {
|
|
|
2561
3032
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
2562
3033
|
const out = await purgeFromTrash(sid);
|
|
2563
3034
|
metaCache.invalidate(sid);
|
|
2564
|
-
|
|
2565
|
-
|
|
3035
|
+
try {
|
|
3036
|
+
await titleIndex.remove([sid]);
|
|
3037
|
+
} catch (e) {
|
|
3038
|
+
}
|
|
2566
3039
|
json(res, out);
|
|
2567
3040
|
} catch (e) {
|
|
2568
3041
|
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
@@ -2582,7 +3055,7 @@ function apply(ctx) {
|
|
|
2582
3055
|
try {
|
|
2583
3056
|
results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
|
|
2584
3057
|
metaCache.invalidate(sid);
|
|
2585
|
-
titleIndex.remove([sid]).catch(() => {
|
|
3058
|
+
await titleIndex.remove([sid]).catch(() => {
|
|
2586
3059
|
});
|
|
2587
3060
|
} catch (e) {
|
|
2588
3061
|
results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
|
|
@@ -2672,6 +3145,108 @@ function apply(ctx) {
|
|
|
2672
3145
|
}
|
|
2673
3146
|
}
|
|
2674
3147
|
}));
|
|
3148
|
+
disposers.push(ctx.webServer.register({
|
|
3149
|
+
kind: "exact",
|
|
3150
|
+
path: "/archived-sessions/lineage-tree",
|
|
3151
|
+
handler: async (req, res) => {
|
|
3152
|
+
try {
|
|
3153
|
+
const body = await readJsonBody(req);
|
|
3154
|
+
const rootId = body && typeof body.sessionId === "string" ? body.sessionId : "";
|
|
3155
|
+
if (!rootId || !isSafeSessionId2(rootId)) return json(res, { error: "missing sessionId" }, 400);
|
|
3156
|
+
const headerById = /* @__PURE__ */ new Map();
|
|
3157
|
+
const sizeById = /* @__PURE__ */ new Map();
|
|
3158
|
+
const trashedIds = /* @__PURE__ */ new Set();
|
|
3159
|
+
let purgedIds = [];
|
|
3160
|
+
const persistedIds = /* @__PURE__ */ new Set();
|
|
3161
|
+
try {
|
|
3162
|
+
const store = await readTrashStore();
|
|
3163
|
+
for (const item of store.items) trashedIds.add(String(item.sessionId));
|
|
3164
|
+
purgedIds = store.purgedSessionIds.map(String);
|
|
3165
|
+
} catch (e) {
|
|
3166
|
+
}
|
|
3167
|
+
const addHeader = (h, sizeBytes) => {
|
|
3168
|
+
if (!h || h.id == null) return;
|
|
3169
|
+
const id = String(h.id);
|
|
3170
|
+
if (headerById.has(id) || trashedIds.has(id)) return;
|
|
3171
|
+
headerById.set(id, {
|
|
3172
|
+
parentSession: h.parentSession != null ? String(h.parentSession) : null,
|
|
3173
|
+
subagent: h.origin === "subagent",
|
|
3174
|
+
delegationDepth: Number.isFinite(h.delegationDepth) ? h.delegationDepth : null,
|
|
3175
|
+
createdAt: typeof h.createdAt === "number" ? h.createdAt : null
|
|
3176
|
+
});
|
|
3177
|
+
if (Number.isFinite(sizeBytes)) sizeById.set(id, sizeBytes);
|
|
3178
|
+
};
|
|
3179
|
+
try {
|
|
3180
|
+
for (const entry of await persistence.listEntries()) {
|
|
3181
|
+
addHeader(entry.header, entry.sizeBytes);
|
|
3182
|
+
if (entry && entry.id != null) persistedIds.add(String(entry.id));
|
|
3183
|
+
}
|
|
3184
|
+
} catch (e) {
|
|
3185
|
+
}
|
|
3186
|
+
const liveIds = /* @__PURE__ */ new Set();
|
|
3187
|
+
const live = ctx.get("sessions");
|
|
3188
|
+
try {
|
|
3189
|
+
if (live && typeof live.list === "function") {
|
|
3190
|
+
live.list().forEach((s) => {
|
|
3191
|
+
liveIds.add(String(s.id));
|
|
3192
|
+
addHeader(s.header, void 0);
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
} catch (e) {
|
|
3196
|
+
}
|
|
3197
|
+
for (const id of purgedIds) {
|
|
3198
|
+
if (!persistedIds.has(id)) headerById.delete(id);
|
|
3199
|
+
}
|
|
3200
|
+
const kidsOf = /* @__PURE__ */ new Map();
|
|
3201
|
+
for (const [id, h] of headerById) {
|
|
3202
|
+
if (!h.subagent || !h.parentSession) continue;
|
|
3203
|
+
if (!kidsOf.has(h.parentSession)) kidsOf.set(h.parentSession, []);
|
|
3204
|
+
kidsOf.get(h.parentSession).push(id);
|
|
3205
|
+
}
|
|
3206
|
+
const MAX_NODES = 500;
|
|
3207
|
+
let nodeCount = 0;
|
|
3208
|
+
const build = (id, depth, visited) => {
|
|
3209
|
+
nodeCount++;
|
|
3210
|
+
const h = headerById.get(id) || {};
|
|
3211
|
+
const childVisited = new Set(visited);
|
|
3212
|
+
childVisited.add(id);
|
|
3213
|
+
const kidIds = nodeCount >= MAX_NODES ? [] : (kidsOf.get(id) || []).filter((k) => !childVisited.has(k));
|
|
3214
|
+
return {
|
|
3215
|
+
sessionId: id,
|
|
3216
|
+
parentSession: h.parentSession || null,
|
|
3217
|
+
delegationDepth: h.delegationDepth,
|
|
3218
|
+
depth,
|
|
3219
|
+
createdAt: h.createdAt || null,
|
|
3220
|
+
sizeBytes: sizeById.has(id) ? sizeById.get(id) : null,
|
|
3221
|
+
live: liveIds.has(id),
|
|
3222
|
+
title: null,
|
|
3223
|
+
children: kidIds.map((k) => build(k, depth + 1, childVisited))
|
|
3224
|
+
};
|
|
3225
|
+
};
|
|
3226
|
+
const nodes = (kidsOf.get(rootId) || []).map((k) => build(k, 1, /* @__PURE__ */ new Set([rootId])));
|
|
3227
|
+
const allIds = [rootId];
|
|
3228
|
+
const walk = (n) => {
|
|
3229
|
+
allIds.push(n.sessionId);
|
|
3230
|
+
n.children.forEach(walk);
|
|
3231
|
+
};
|
|
3232
|
+
nodes.forEach(walk);
|
|
3233
|
+
const titles = await projectTitles(allIds);
|
|
3234
|
+
const titleOf = (id) => {
|
|
3235
|
+
const snap = titles.get(id);
|
|
3236
|
+
const m = snap ? metaFromSnapshot(snap) : null;
|
|
3237
|
+
return m && m.title || null;
|
|
3238
|
+
};
|
|
3239
|
+
const fill = (n) => {
|
|
3240
|
+
n.title = titleOf(n.sessionId);
|
|
3241
|
+
n.children.forEach(fill);
|
|
3242
|
+
};
|
|
3243
|
+
nodes.forEach(fill);
|
|
3244
|
+
json(res, { parent: { sessionId: rootId, title: titleOf(rootId) }, nodes });
|
|
3245
|
+
} catch (e) {
|
|
3246
|
+
json(res, { error: String(e && e.message || e) }, errorStatus(e));
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}));
|
|
2675
3250
|
disposers.push(ctx.webServer.register({
|
|
2676
3251
|
kind: "exact",
|
|
2677
3252
|
path: "/archived-sessions/workspaces",
|
|
@@ -2693,7 +3268,8 @@ function apply(ctx) {
|
|
|
2693
3268
|
const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
|
|
2694
3269
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
2695
3270
|
if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
|
|
2696
|
-
const moved = await
|
|
3271
|
+
const moved = await moveOrQueue(sid, target);
|
|
3272
|
+
if (moved.queued) return json(res, { sessionId: sid, ...moved });
|
|
2697
3273
|
metaCache.invalidate(sid);
|
|
2698
3274
|
try {
|
|
2699
3275
|
await reindexRegistry();
|
|
@@ -2705,6 +3281,74 @@ function apply(ctx) {
|
|
|
2705
3281
|
}
|
|
2706
3282
|
}
|
|
2707
3283
|
}));
|
|
3284
|
+
disposers.push(ctx.webServer.register({
|
|
3285
|
+
kind: "exact",
|
|
3286
|
+
path: "/archived-sessions/move-many",
|
|
3287
|
+
handler: async (req, res) => {
|
|
3288
|
+
try {
|
|
3289
|
+
const body = await readJsonBody(req);
|
|
3290
|
+
const ids = Array.isArray(body && body.sessionIds) ? [...new Set(body.sessionIds.filter((x) => typeof x === "string" && x))] : [];
|
|
3291
|
+
const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
|
|
3292
|
+
if (!ids.length) return json(res, { ok: false, error: "missing sessionIds" }, 400);
|
|
3293
|
+
if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
|
|
3294
|
+
let moved = 0;
|
|
3295
|
+
const failed = [];
|
|
3296
|
+
const queued = [];
|
|
3297
|
+
for (const sid of ids) {
|
|
3298
|
+
try {
|
|
3299
|
+
const r = await moveOrQueue(sid, target);
|
|
3300
|
+
if (r && r.queued) {
|
|
3301
|
+
queued.push(sid);
|
|
3302
|
+
continue;
|
|
3303
|
+
}
|
|
3304
|
+
metaCache.invalidate(sid);
|
|
3305
|
+
moved++;
|
|
3306
|
+
} catch (e) {
|
|
3307
|
+
const err = e && e.code ? { sessionId: sid, error: String(e && e.message || e), code: e.code } : { sessionId: sid, error: String(e && e.message || e) };
|
|
3308
|
+
failed.push(err);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
try {
|
|
3312
|
+
await reindexRegistry();
|
|
3313
|
+
} catch (e) {
|
|
3314
|
+
}
|
|
3315
|
+
json(res, {
|
|
3316
|
+
moved,
|
|
3317
|
+
failed,
|
|
3318
|
+
...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`] } : {}
|
|
3319
|
+
});
|
|
3320
|
+
} catch (e) {
|
|
3321
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}));
|
|
3325
|
+
disposers.push(ctx.webServer.register({
|
|
3326
|
+
kind: "exact",
|
|
3327
|
+
path: "/archived-sessions/pending-moves",
|
|
3328
|
+
handler: async (req, res) => {
|
|
3329
|
+
try {
|
|
3330
|
+
const items = await pendingMoves.list();
|
|
3331
|
+
json(res, { items: items.map((i) => ({ sessionId: i.sessionId, targetPath: i.targetPath, queuedAt: i.queuedAt, attempts: i.attempts })) });
|
|
3332
|
+
} catch (e) {
|
|
3333
|
+
json(res, { error: String(e && e.message || e) }, 500);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
}));
|
|
3337
|
+
disposers.push(ctx.webServer.register({
|
|
3338
|
+
kind: "exact",
|
|
3339
|
+
path: "/archived-sessions/pending-moves/cancel",
|
|
3340
|
+
handler: async (req, res) => {
|
|
3341
|
+
try {
|
|
3342
|
+
const body = await readJsonBody(req);
|
|
3343
|
+
const ids = parseIds(body);
|
|
3344
|
+
if (!ids || ids.length === 0) return json(res, { ok: false, error: "missing sessionIds" }, 400);
|
|
3345
|
+
const removed = await pendingMoves.remove(ids);
|
|
3346
|
+
json(res, { ok: true, removed });
|
|
3347
|
+
} catch (e) {
|
|
3348
|
+
json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}));
|
|
2708
3352
|
disposers.push(ctx.webServer.register({
|
|
2709
3353
|
kind: "exact",
|
|
2710
3354
|
path: "/archived-sessions/archive",
|