dsh-sessions-manager 3.6.1 → 3.7.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/lib/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/index.js
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";
2
+ import { mkdir as mkdir10, readFile as readFile4, readdir as readdir4, realpath, rename as rename9, rm as rm3, stat as stat4, unlink, writeFile as writeFile10 } from "node:fs/promises";
3
+ import { basename as basename3, dirname as dirname4, isAbsolute, join as join12 } from "node:path";
4
+ import { readFileSync as readFileSync7 } from "node:fs";
5
+ import { homedir as homedir7 } from "node:os";
6
6
 
7
7
  // src/zstd-frame.js
8
8
  import zlib from "node:zlib";
@@ -303,13 +303,358 @@ function createStarIndex(options = {}) {
303
303
  return { read, write, mutate, setStarred, removeIds, indexPath, dir };
304
304
  }
305
305
 
306
- // src/pending-moves.js
306
+ // src/tag-index.js
307
+ import { mkdir as mkdir2, rename as fsRename, writeFile as writeFile2 } from "node:fs/promises";
307
308
  import { readFileSync as readFileSync2 } from "node:fs";
308
- import { mkdir as mkdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
309
+ import { randomBytes } from "node:crypto";
309
310
  import { homedir as homedir2 } from "node:os";
310
311
  import { join as join2 } from "node:path";
312
+ var TAG_SCHEMA_VERSION = 4;
313
+ var MAX_TAGS = 200;
314
+ var MAX_TAGS_PER_SESSION = 10;
315
+ var MAX_TAG_NAME = 24;
316
+ var DEFAULT_TAG_DIR = join2(homedir2(), ".dsh", "sessions-manager");
317
+ var TAG_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
318
+ function randomSuffix() {
319
+ const bytes = randomBytes(8);
320
+ let out = "";
321
+ for (const b of bytes) out += TAG_ID_ALPHABET[b % TAG_ID_ALPHABET.length];
322
+ return out;
323
+ }
324
+ function isSafeSessionId2(value) {
325
+ return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
326
+ }
327
+ function isSafeTagId(value) {
328
+ return typeof value === "string" && value.length > 0 && value.length <= 64 && !/[\\/\0]/.test(value);
329
+ }
330
+ function tagError(message, status, code) {
331
+ const error = new Error(message);
332
+ error.status = status;
333
+ if (code) error.code = code;
334
+ throw error;
335
+ }
336
+ function normalizeTagName(value) {
337
+ if (typeof value !== "string") return null;
338
+ const name2 = value.trim();
339
+ if (!name2 || /[\\/\0]/.test(name2)) return null;
340
+ if (Array.from(name2).length > MAX_TAG_NAME) return null;
341
+ return name2;
342
+ }
343
+ function nameKey(name2) {
344
+ return name2.toLocaleLowerCase();
345
+ }
346
+ function normalizeTagStore(raw) {
347
+ const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
348
+ const tags = [];
349
+ const ids = /* @__PURE__ */ new Set();
350
+ const names = /* @__PURE__ */ new Set();
351
+ for (const item of source && Array.isArray(source.tags) ? source.tags : []) {
352
+ if (!item || typeof item !== "object") continue;
353
+ if (!isSafeTagId(item.id) || ids.has(item.id)) continue;
354
+ const name2 = normalizeTagName(item.name);
355
+ if (name2 === null) continue;
356
+ const key = nameKey(name2);
357
+ if (names.has(key)) continue;
358
+ ids.add(item.id);
359
+ names.add(key);
360
+ const createdAt = Number.isFinite(Number(item.createdAt)) ? Number(item.createdAt) : 0;
361
+ tags.push({ id: item.id, name: name2, createdAt });
362
+ if (tags.length >= MAX_TAGS) break;
363
+ }
364
+ const assignments = {};
365
+ const rawAssignments = source && source.assignments && typeof source.assignments === "object" && !Array.isArray(source.assignments) ? source.assignments : null;
366
+ if (rawAssignments) {
367
+ for (const [sid, value] of Object.entries(rawAssignments)) {
368
+ if (!isSafeSessionId2(sid) || !Array.isArray(value)) continue;
369
+ const kept = [];
370
+ const seen = /* @__PURE__ */ new Set();
371
+ for (const tagId of value) {
372
+ if (!ids.has(tagId) || seen.has(tagId)) continue;
373
+ seen.add(tagId);
374
+ kept.push(tagId);
375
+ if (kept.length >= MAX_TAGS_PER_SESSION) break;
376
+ }
377
+ if (kept.length) assignments[sid] = kept;
378
+ }
379
+ }
380
+ return { schemaVersion: TAG_SCHEMA_VERSION, tags, assignments };
381
+ }
382
+ function createTagIndex(options = {}) {
383
+ const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || DEFAULT_TAG_DIR;
384
+ const indexPath = options.indexPath || join2(dir, "tags.json");
385
+ let mutation = Promise.resolve();
386
+ async function read() {
387
+ try {
388
+ return normalizeTagStore(JSON.parse(readFileSync2(indexPath, "utf8")));
389
+ } catch {
390
+ return normalizeTagStore(null);
391
+ }
392
+ }
393
+ async function write(store) {
394
+ await mkdir2(dir, { recursive: true });
395
+ const tmp = join2(dir, `.tags-${process.pid}-${Date.now()}.tmp`);
396
+ await writeFile2(tmp, JSON.stringify(normalizeTagStore(store), null, 2), { encoding: "utf8", mode: 384 });
397
+ await fsRename(tmp, indexPath);
398
+ }
399
+ function mutate(mutator) {
400
+ const operation = mutation.then(async () => {
401
+ const store = await read();
402
+ const result = await mutator(store);
403
+ await write(store);
404
+ return result;
405
+ });
406
+ mutation = operation.catch(() => {
407
+ });
408
+ return operation;
409
+ }
410
+ function newTagId(store) {
411
+ for (let i = 0; i < 16; i++) {
412
+ const id = `t_${randomSuffix()}`;
413
+ if (!store.tags.some((t) => t.id === id)) return id;
414
+ }
415
+ return tagError("\u65E0\u6CD5\u751F\u6210\u6807\u7B7E id\uFF08\u968F\u673A\u78B0\u649E\u5F02\u5E38\uFF09", 500, "DSM_TAG_ID_COLLISION");
416
+ }
417
+ async function create(rawName) {
418
+ const name2 = normalizeTagName(rawName);
419
+ if (name2 === null) tagError(`\u6807\u7B7E\u540D\u65E0\u6548\uFF08\u975E\u7A7A\u3001\u4E0D\u542B\u659C\u6760\u3001\u4E0D\u8D85\u8FC7 ${MAX_TAG_NAME} \u4E2A\u5B57\u7B26\uFF09`, 400, "DSM_TAG_NAME_INVALID");
420
+ const key = nameKey(name2);
421
+ return mutate((store) => {
422
+ if (store.tags.some((t) => nameKey(t.name) === key)) tagError("\u540C\u540D\u6807\u7B7E\u5DF2\u5B58\u5728", 409, "DSM_TAG_EXISTS");
423
+ if (store.tags.length >= MAX_TAGS) tagError(`\u6807\u7B7E\u603B\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF08${MAX_TAGS}\uFF09`, 409, "DSM_TAG_LIMIT");
424
+ const tag = { id: newTagId(store), name: name2, createdAt: Date.now() };
425
+ store.tags.push(tag);
426
+ return tag;
427
+ });
428
+ }
429
+ async function rename10(id, rawName) {
430
+ const name2 = normalizeTagName(rawName);
431
+ if (name2 === null) tagError(`\u6807\u7B7E\u540D\u65E0\u6548\uFF08\u975E\u7A7A\u3001\u4E0D\u542B\u659C\u6760\u3001\u4E0D\u8D85\u8FC7 ${MAX_TAG_NAME} \u4E2A\u5B57\u7B26\uFF09`, 400, "DSM_TAG_NAME_INVALID");
432
+ const key = nameKey(name2);
433
+ return mutate((store) => {
434
+ const tag = store.tags.find((t) => t.id === id);
435
+ if (!tag) tagError("\u6807\u7B7E\u4E0D\u5B58\u5728", 404, "DSM_TAG_NOT_FOUND");
436
+ if (store.tags.some((t) => t.id !== tag.id && nameKey(t.name) === key)) tagError("\u540C\u540D\u6807\u7B7E\u5DF2\u5B58\u5728", 409, "DSM_TAG_EXISTS");
437
+ tag.name = name2;
438
+ return tag;
439
+ });
440
+ }
441
+ async function merge(fromId, toId) {
442
+ if (!isSafeTagId(fromId) || !isSafeTagId(toId) || fromId === toId) {
443
+ tagError("\u5408\u5E76\u7684\u6E90/\u76EE\u6807\u6807\u7B7E\u65E0\u6548\u6216\u76F8\u540C", 400, "DSM_TAG_INVALID");
444
+ }
445
+ return mutate((store) => {
446
+ if (!store.tags.some((t) => t.id === fromId) || !store.tags.some((t) => t.id === toId)) {
447
+ tagError("\u6807\u7B7E\u4E0D\u5B58\u5728", 404, "DSM_TAG_NOT_FOUND");
448
+ }
449
+ for (const [sid, list2] of Object.entries(store.assignments)) {
450
+ if (!list2.includes(fromId)) continue;
451
+ const merged = [...new Set(list2.filter((x) => x !== fromId).concat(toId))];
452
+ store.assignments[sid] = merged.slice(0, MAX_TAGS_PER_SESSION);
453
+ }
454
+ store.tags = store.tags.filter((t) => t.id !== fromId);
455
+ return { merged: true };
456
+ });
457
+ }
458
+ function removeTag(id) {
459
+ return mutate((store) => {
460
+ if (isSafeTagId(id)) {
461
+ store.tags = store.tags.filter((t) => t.id !== id);
462
+ for (const [sid, list2] of Object.entries(store.assignments)) {
463
+ const kept = list2.filter((x) => x !== id);
464
+ if (kept.length) store.assignments[sid] = kept;
465
+ else delete store.assignments[sid];
466
+ }
467
+ }
468
+ return { removed: true };
469
+ });
470
+ }
471
+ async function setTags(sessionId, tagIds) {
472
+ if (!isSafeSessionId2(sessionId)) tagError("\u65E0\u6548\u7684 sessionId", 400, "DSM_TAG_SESSION_INVALID");
473
+ if (!Array.isArray(tagIds)) tagError("tagIds \u5FC5\u987B\u662F\u6570\u7EC4", 400, "DSM_TAG_IDS_INVALID");
474
+ const seen = /* @__PURE__ */ new Set();
475
+ const wanted = [];
476
+ for (const v of tagIds) {
477
+ if (!isSafeTagId(v)) tagError("\u65E0\u6548\u7684\u6807\u7B7E id", 400, "DSM_TAG_UNKNOWN");
478
+ if (seen.has(v)) continue;
479
+ seen.add(v);
480
+ wanted.push(v);
481
+ }
482
+ if (wanted.length > MAX_TAGS_PER_SESSION) tagError(`\u5355\u4E2A\u4F1A\u8BDD\u6700\u591A ${MAX_TAGS_PER_SESSION} \u4E2A\u6807\u7B7E`, 409, "DSM_TAG_LIMIT");
483
+ return mutate((store) => {
484
+ const known = new Set(store.tags.map((t) => t.id));
485
+ const unknown = wanted.filter((id) => !known.has(id));
486
+ if (unknown.length) tagError(`\u672A\u77E5\u7684\u6807\u7B7E id\uFF1A${unknown.join("\u3001")}`, 400, "DSM_TAG_UNKNOWN");
487
+ if (wanted.length) store.assignments[sessionId] = [...wanted];
488
+ else delete store.assignments[sessionId];
489
+ return store.assignments;
490
+ });
491
+ }
492
+ function removeIds(ids) {
493
+ const wanted = (Array.isArray(ids) ? ids : []).filter(isSafeSessionId2);
494
+ return mutate((store) => {
495
+ for (const sid of wanted) delete store.assignments[sid];
496
+ return store.assignments;
497
+ });
498
+ }
499
+ async function list() {
500
+ const store = await read();
501
+ return { tags: store.tags, assignments: store.assignments };
502
+ }
503
+ return { read, write, mutate, list, create, rename: rename10, merge, removeTag, setTags, removeIds, indexPath, dir };
504
+ }
505
+
506
+ // src/saved-filters.js
507
+ import { mkdir as mkdir3, rename as rename2, writeFile as writeFile3 } from "node:fs/promises";
508
+ import { readFileSync as readFileSync3 } from "node:fs";
509
+ import { randomBytes as randomBytes2 } from "node:crypto";
510
+ import { homedir as homedir3 } from "node:os";
511
+ import { join as join3 } from "node:path";
512
+ var FILTER_SCHEMA_VERSION = 1;
513
+ var MAX_FILTERS = 20;
514
+ var MAX_FILTER_NAME = 40;
515
+ var MAX_FILTER_JSON = 2048;
516
+ var DEFAULT_FILTER_DIR = join3(homedir3(), ".dsh", "sessions-manager");
517
+ var FILTER_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
518
+ function randomSuffix2() {
519
+ const bytes = randomBytes2(8);
520
+ let out = "";
521
+ for (const b of bytes) out += FILTER_ID_ALPHABET[b % FILTER_ID_ALPHABET.length];
522
+ return out;
523
+ }
524
+ function isSafeFilterId(value) {
525
+ return typeof value === "string" && value.length > 0 && value.length <= 64 && !/[\\/\0]/.test(value);
526
+ }
527
+ function filterError(message, status, code) {
528
+ const error = new Error(message);
529
+ error.status = status;
530
+ if (code) error.code = code;
531
+ throw error;
532
+ }
533
+ function normalizeFilterName(value) {
534
+ if (typeof value !== "string") return null;
535
+ const name2 = value.trim();
536
+ if (!name2 || /[\\/\0]/.test(name2)) return null;
537
+ if (Array.from(name2).length > MAX_FILTER_NAME) return null;
538
+ return name2;
539
+ }
540
+ function nameKey2(name2) {
541
+ return name2.toLocaleLowerCase();
542
+ }
543
+ function normalizeFiltersPayload(value) {
544
+ let serialized;
545
+ try {
546
+ serialized = JSON.stringify(value);
547
+ } catch {
548
+ return null;
549
+ }
550
+ if (typeof serialized !== "string" || serialized.length > MAX_FILTER_JSON) return null;
551
+ try {
552
+ return JSON.parse(serialized);
553
+ } catch {
554
+ return null;
555
+ }
556
+ }
557
+ function normalizeFilterStore(raw) {
558
+ const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
559
+ const items = [];
560
+ const ids = /* @__PURE__ */ new Set();
561
+ const names = /* @__PURE__ */ new Set();
562
+ for (const item of source && Array.isArray(source.items) ? source.items : []) {
563
+ if (!item || typeof item !== "object") continue;
564
+ if (!isSafeFilterId(item.id) || ids.has(item.id)) continue;
565
+ const name2 = normalizeFilterName(item.name);
566
+ if (name2 === null) continue;
567
+ const key = nameKey2(name2);
568
+ if (names.has(key)) continue;
569
+ const filters = normalizeFiltersPayload(item.filters);
570
+ if (filters === null) continue;
571
+ ids.add(item.id);
572
+ names.add(key);
573
+ const createdAt = Number.isFinite(Number(item.createdAt)) ? Number(item.createdAt) : 0;
574
+ items.push({ id: item.id, name: name2, filters, createdAt });
575
+ if (items.length >= MAX_FILTERS) break;
576
+ }
577
+ return { schemaVersion: FILTER_SCHEMA_VERSION, items };
578
+ }
579
+ function createSavedFilters(options = {}) {
580
+ const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || DEFAULT_FILTER_DIR;
581
+ const indexPath = options.indexPath || join3(dir, "saved-filters.json");
582
+ let mutation = Promise.resolve();
583
+ async function read() {
584
+ try {
585
+ return normalizeFilterStore(JSON.parse(readFileSync3(indexPath, "utf8")));
586
+ } catch {
587
+ return normalizeFilterStore(null);
588
+ }
589
+ }
590
+ async function write(store) {
591
+ await mkdir3(dir, { recursive: true });
592
+ const tmp = join3(dir, `.saved-filters-${process.pid}-${Date.now()}.tmp`);
593
+ await writeFile3(tmp, JSON.stringify(normalizeFilterStore(store), null, 2), { encoding: "utf8", mode: 384 });
594
+ await rename2(tmp, indexPath);
595
+ }
596
+ function mutate(mutator) {
597
+ const operation = mutation.then(async () => {
598
+ const store = await read();
599
+ const result = await mutator(store);
600
+ await write(store);
601
+ return result;
602
+ });
603
+ mutation = operation.catch(() => {
604
+ });
605
+ return operation;
606
+ }
607
+ function newFilterId(store) {
608
+ for (let i = 0; i < 16; i++) {
609
+ const id = `f_${randomSuffix2()}`;
610
+ if (!store.items.some((t) => t.id === id)) return id;
611
+ }
612
+ return filterError("\u65E0\u6CD5\u751F\u6210\u7B5B\u9009 id\uFF08\u968F\u673A\u78B0\u649E\u5F02\u5E38\uFF09", 500, "DSM_FILTER_ID_COLLISION");
613
+ }
614
+ async function save(rawName, filters) {
615
+ const name2 = normalizeFilterName(rawName);
616
+ if (name2 === null) filterError(`\u7B5B\u9009\u540D\u65E0\u6548\uFF08\u975E\u7A7A\u3001\u4E0D\u542B\u659C\u6760\u3001\u4E0D\u8D85\u8FC7 ${MAX_FILTER_NAME} \u4E2A\u5B57\u7B26\uFF09`, 400, "DSM_FILTER_NAME_INVALID");
617
+ let serialized;
618
+ try {
619
+ serialized = JSON.stringify(filters);
620
+ } catch {
621
+ serialized = void 0;
622
+ }
623
+ if (typeof serialized !== "string") filterError("\u7B5B\u9009\u6761\u4EF6\u5FC5\u987B\u662F\u53EF JSON \u5E8F\u5217\u5316\u7684\u6570\u636E", 400, "DSM_FILTER_INVALID");
624
+ if (serialized === "null") filterError("\u7B5B\u9009\u6761\u4EF6\u4E0D\u80FD\u4E3A null", 400, "DSM_FILTER_INVALID");
625
+ if (serialized.length > MAX_FILTER_JSON) filterError(`\u7B5B\u9009\u6761\u4EF6\u8FC7\u5927\uFF08\u5E8F\u5217\u5316\u540E\u6700\u591A ${MAX_FILTER_JSON} \u5B57\u7B26\uFF09`, 400, "DSM_FILTER_TOO_LARGE");
626
+ const payload = JSON.parse(serialized);
627
+ const key = nameKey2(name2);
628
+ return mutate((store) => {
629
+ if (store.items.some((t) => nameKey2(t.name) === key)) filterError("\u540C\u540D\u7B5B\u9009\u5DF2\u5B58\u5728", 409, "DSM_FILTER_EXISTS");
630
+ if (store.items.length >= MAX_FILTERS) filterError(`\u4FDD\u5B58\u7684\u7B5B\u9009\u5DF2\u8FBE\u4E0A\u9650\uFF08${MAX_FILTERS}\uFF09`, 409, "DSM_FILTER_LIMIT");
631
+ const item = { id: newFilterId(store), name: name2, filters: payload, createdAt: Date.now() };
632
+ store.items.push(item);
633
+ return item;
634
+ });
635
+ }
636
+ function remove(ids) {
637
+ const wanted = new Set((Array.isArray(ids) ? ids : []).filter(isSafeFilterId).map(String));
638
+ return mutate((store) => {
639
+ const before = store.items.length;
640
+ store.items = store.items.filter((t) => !wanted.has(t.id));
641
+ return before - store.items.length;
642
+ });
643
+ }
644
+ async function list() {
645
+ const store = await read();
646
+ return store.items;
647
+ }
648
+ return { read, write, mutate, list, save, remove, indexPath, dir };
649
+ }
650
+
651
+ // src/pending-moves.js
652
+ import { readFileSync as readFileSync4 } from "node:fs";
653
+ import { mkdir as mkdir4, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
654
+ import { homedir as homedir4 } from "node:os";
655
+ import { join as join4 } from "node:path";
311
656
  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");
657
+ var DEFAULT_DIR = process.env.DSH_SESSIONS_MANAGER_PENDING_DIR || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join4(homedir4(), ".dsh", "sessions-manager");
313
658
  var MAX_ITEMS = 50;
314
659
  var MAX_ATTEMPTS = 5;
315
660
  function isSafeId(value) {
@@ -335,20 +680,20 @@ function normalizeStore(raw) {
335
680
  }
336
681
  function createPendingMoveStore(options = {}) {
337
682
  const dir = options.dir || DEFAULT_DIR;
338
- const indexPath = options.indexPath || join2(dir, "pending-moves.json");
683
+ const indexPath = options.indexPath || join4(dir, "pending-moves.json");
339
684
  let mutation = Promise.resolve();
340
685
  async function read() {
341
686
  try {
342
- return normalizeStore(JSON.parse(readFileSync2(indexPath, "utf8")));
687
+ return normalizeStore(JSON.parse(readFileSync4(indexPath, "utf8")));
343
688
  } catch (e) {
344
689
  return normalizeStore(null);
345
690
  }
346
691
  }
347
692
  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);
693
+ await mkdir4(dir, { recursive: true });
694
+ const tmp = join4(dir, `.pending-moves-${process.pid}-${Date.now()}.tmp`);
695
+ await writeFile4(tmp, JSON.stringify(normalizeStore(store), null, 2), { encoding: "utf8", mode: 384 });
696
+ await rename3(tmp, indexPath);
352
697
  }
353
698
  function mutate(mutator) {
354
699
  const operation = mutation.then(async () => {
@@ -398,9 +743,106 @@ function createPendingMoveStore(options = {}) {
398
743
  };
399
744
  }
400
745
 
746
+ // src/move-notices.js
747
+ import { readFileSync as readFileSync5 } from "node:fs";
748
+ import { mkdir as mkdir5, rename as rename4, writeFile as writeFile5 } from "node:fs/promises";
749
+ import { homedir as homedir5 } from "node:os";
750
+ import { join as join5 } from "node:path";
751
+ var SCHEMA_VERSION2 = 1;
752
+ var MAX_NOTICES = 20;
753
+ var NOTICE_MAX_AGE_MS = 7 * 24 * 3600 * 1e3;
754
+ var KINDS = /* @__PURE__ */ new Set(["moved", "abandoned"]);
755
+ function isSafeId2(value) {
756
+ return typeof value === "string" && value.length > 0 && value.length <= 200;
757
+ }
758
+ function str(v, max) {
759
+ return typeof v === "string" && v ? v.slice(0, max) : null;
760
+ }
761
+ function normalizeNotice(raw) {
762
+ if (!raw || typeof raw !== "object") return null;
763
+ if (!KINDS.has(raw.kind) || !isSafeId2(raw.sessionId)) return null;
764
+ const at = Number.isFinite(raw.at) && raw.at > 0 ? Math.floor(raw.at) : 0;
765
+ if (!at) return null;
766
+ const sessionId = String(raw.sessionId);
767
+ return {
768
+ id: str(raw.id, 400) || `${sessionId}:${raw.kind}:${at}`,
769
+ kind: raw.kind,
770
+ sessionId,
771
+ targetPath: str(raw.targetPath, 500),
772
+ attempts: Number.isSafeInteger(raw.attempts) && raw.attempts >= 0 ? raw.attempts : null,
773
+ reason: str(raw.reason, 300),
774
+ at
775
+ };
776
+ }
777
+ function normalizeStore2(raw) {
778
+ const source = raw && Array.isArray(raw.items) ? raw.items : [];
779
+ const seen = /* @__PURE__ */ new Set();
780
+ const items = [];
781
+ for (const entry of source) {
782
+ const n = normalizeNotice(entry);
783
+ if (!n || seen.has(n.id)) continue;
784
+ seen.add(n.id);
785
+ items.push(n);
786
+ }
787
+ items.sort((a, b) => a.at - b.at);
788
+ const trimmed = items.length > MAX_NOTICES ? items.slice(items.length - MAX_NOTICES) : items;
789
+ return { schemaVersion: SCHEMA_VERSION2, items: trimmed };
790
+ }
791
+ function createMoveNoticeStore(options = {}) {
792
+ const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_PENDING_DIR || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join5(homedir5(), ".dsh", "sessions-manager");
793
+ const indexPath = options.indexPath || join5(dir, "move-notices.json");
794
+ let mutation = Promise.resolve();
795
+ function read() {
796
+ try {
797
+ return normalizeStore2(JSON.parse(readFileSync5(indexPath, "utf8")));
798
+ } catch (e) {
799
+ return normalizeStore2(null);
800
+ }
801
+ }
802
+ async function write(store) {
803
+ await mkdir5(dir, { recursive: true });
804
+ const tmp = join5(dir, `.move-notices-${process.pid}-${Date.now()}.tmp`);
805
+ await writeFile5(tmp, JSON.stringify(normalizeStore2(store), null, 2), { encoding: "utf8", mode: 384 });
806
+ await rename4(tmp, indexPath);
807
+ }
808
+ function mutate(mutator) {
809
+ const operation = mutation.then(async () => {
810
+ const store = read();
811
+ const result = await mutator(store);
812
+ await write(store);
813
+ return result;
814
+ });
815
+ mutation = operation.catch(() => {
816
+ });
817
+ return operation;
818
+ }
819
+ return {
820
+ indexPath,
821
+ // 终局落一条通知;同 id(会话+类型+时刻)幂等去重。
822
+ append: (notice) => mutate((store) => {
823
+ const n = normalizeNotice(notice);
824
+ if (!n) return null;
825
+ if (store.items.some((x) => x.id === n.id)) return n;
826
+ store.items.push(n);
827
+ return n;
828
+ }),
829
+ // 只读列出未过期、未 ack 的通知(按时间升序)。过期项在下次写入时随归一化清掉。
830
+ async list() {
831
+ const now = Date.now();
832
+ return read().items.filter((n) => now - n.at <= NOTICE_MAX_AGE_MS);
833
+ },
834
+ ack: (ids) => mutate((store) => {
835
+ const drop = new Set((Array.isArray(ids) ? ids : []).filter((v) => typeof v === "string" && v.length <= 400).map(String));
836
+ const before = store.items.length;
837
+ store.items = store.items.filter((n) => !drop.has(n.id));
838
+ return before - store.items.length;
839
+ })
840
+ };
841
+ }
842
+
401
843
  // src/state-temp-sweep.js
402
844
  import { readdir, rm, stat } from "node:fs/promises";
403
- import { join as join3 } from "node:path";
845
+ import { join as join6 } from "node:path";
404
846
  var TEMP_NAME_RE = /^\..+-\d+-\d+\.tmp$/;
405
847
  async function sweepStaleStateTemps(dirs, options = {}) {
406
848
  const maxAgeMs = Number.isSafeInteger(options.maxAgeMs) && options.maxAgeMs >= 0 ? options.maxAgeMs : 36e5;
@@ -416,7 +858,7 @@ async function sweepStaleStateTemps(dirs, options = {}) {
416
858
  }
417
859
  for (const name2 of entries) {
418
860
  if (!TEMP_NAME_RE.test(name2)) continue;
419
- const path = join3(dir, name2);
861
+ const path = join6(dir, name2);
420
862
  let info;
421
863
  try {
422
864
  info = await stat(path);
@@ -487,15 +929,15 @@ function aggregateStorage(items, options = {}) {
487
929
  }
488
930
 
489
931
  // src/auto-archive.js
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";
932
+ import { mkdir as mkdir6, rename as rename5, writeFile as writeFile6 } from "node:fs/promises";
933
+ import { readFileSync as readFileSync6 } from "node:fs";
934
+ import { homedir as homedir6 } from "node:os";
935
+ import { join as join7 } from "node:path";
494
936
  var AUTO_ARCHIVE_SCHEMA_VERSION = 4;
495
937
  var INACTIVE_DAY_OPTIONS = Object.freeze([0, 30, 60, 90]);
496
938
  var DAY_MS = 864e5;
497
939
  var RUN_INTERVAL_MS = DAY_MS;
498
- var DEFAULT_DIR2 = join4(homedir3(), ".dsh", "sessions-manager");
940
+ var DEFAULT_DIR2 = join7(homedir6(), ".dsh", "sessions-manager");
499
941
  function normalizeAutoArchiveStore(raw) {
500
942
  const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
501
943
  const settings = source.settings && typeof source.settings === "object" ? source.settings : {};
@@ -540,20 +982,20 @@ function pickInactiveCandidates(items, options = {}) {
540
982
  }
541
983
  function createAutoArchiveStore(options = {}) {
542
984
  const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR || DEFAULT_DIR2;
543
- const indexPath = options.indexPath || join4(dir, "auto-archive.json");
985
+ const indexPath = options.indexPath || join7(dir, "auto-archive.json");
544
986
  let mutation = Promise.resolve();
545
987
  async function read() {
546
988
  try {
547
- return normalizeAutoArchiveStore(JSON.parse(readFileSync3(indexPath, "utf8")));
989
+ return normalizeAutoArchiveStore(JSON.parse(readFileSync6(indexPath, "utf8")));
548
990
  } catch {
549
991
  return normalizeAutoArchiveStore(null);
550
992
  }
551
993
  }
552
994
  async function write(store) {
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);
995
+ await mkdir6(dir, { recursive: true });
996
+ const tmp = join7(dir, `.auto-archive-${process.pid}-${Date.now()}.tmp`);
997
+ await writeFile6(tmp, JSON.stringify(normalizeAutoArchiveStore(store), null, 2), { encoding: "utf8", mode: 384 });
998
+ await rename5(tmp, indexPath);
557
999
  }
558
1000
  function mutate(mutator) {
559
1001
  const operation = mutation.then(async () => {
@@ -700,9 +1142,9 @@ function createSessionMetaCache(opts = {}) {
700
1142
  }
701
1143
 
702
1144
  // src/title-persist-index.js
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";
705
- var TITLE_INDEX_SCHEMA_VERSION = 1;
1145
+ import { mkdir as mkdir7, readFile, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
1146
+ import { dirname, join as join8 } from "node:path";
1147
+ var TITLE_INDEX_SCHEMA_VERSION = 2;
706
1148
  var MAX_ENTRIES = 2e4;
707
1149
  function normalizeEntry(raw) {
708
1150
  if (!raw || typeof raw !== "object") return null;
@@ -711,9 +1153,13 @@ function normalizeEntry(raw) {
711
1153
  const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
712
1154
  const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
713
1155
  const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
1156
+ const resolved = raw.resolved === true || raw.resolved === 1;
714
1157
  if (!fingerprint || fingerprint.startsWith("rev:")) return null;
715
1158
  if (!title && !cwd) return null;
716
- return { title, cwd, createdAt, fingerprint, updatedAt };
1159
+ if (!title && !resolved) return null;
1160
+ const entry = { title, cwd, createdAt, fingerprint, updatedAt };
1161
+ if (resolved) entry.resolved = 1;
1162
+ return entry;
717
1163
  }
718
1164
  function normalizeTitleIndex(raw) {
719
1165
  const entries = {};
@@ -739,7 +1185,7 @@ function mergeEntries(left, right) {
739
1185
  function createTitleIndexStore({ dir, file }) {
740
1186
  let cache = null;
741
1187
  let chain = Promise.resolve();
742
- const path = file || join5(dir, "title-index.json");
1188
+ const path = file || join8(dir, "title-index.json");
743
1189
  async function readRaw() {
744
1190
  try {
745
1191
  return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
@@ -774,10 +1220,10 @@ function createTitleIndexStore({ dir, file }) {
774
1220
  if (!Object.keys(right).length) return false;
775
1221
  await enqueue(async (store) => {
776
1222
  const next = mergeEntries(store, right);
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);
1223
+ await mkdir7(dirname(path), { recursive: true });
1224
+ const tmp = join8(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
1225
+ await writeFile7(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
1226
+ await rename6(tmp, path);
781
1227
  cache = next;
782
1228
  });
783
1229
  return true;
@@ -794,10 +1240,110 @@ function createTitleIndexStore({ dir, file }) {
794
1240
  }
795
1241
  }
796
1242
  if (!changed) return;
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);
1243
+ await mkdir7(dirname(path), { recursive: true });
1244
+ const tmp = join8(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
1245
+ await writeFile7(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
1246
+ await rename6(tmp, path);
1247
+ });
1248
+ return true;
1249
+ }
1250
+ };
1251
+ }
1252
+
1253
+ // src/empty-scan-index.js
1254
+ import { mkdir as mkdir8, readFile as readFile2, rename as rename7, writeFile as writeFile8 } from "node:fs/promises";
1255
+ import { dirname as dirname2, join as join9 } from "node:path";
1256
+ var EMPTY_SCAN_SCHEMA_VERSION = 1;
1257
+ var MAX_ENTRIES2 = 2e4;
1258
+ function normalizeEntry2(raw) {
1259
+ if (!raw || typeof raw !== "object") return null;
1260
+ const empty = raw.empty === 1 || raw.empty === true ? 1 : raw.empty === 0 || raw.empty === false ? 0 : null;
1261
+ const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
1262
+ const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
1263
+ if (empty === null || !fingerprint || fingerprint.startsWith("rev:")) return null;
1264
+ return { empty, fingerprint, updatedAt };
1265
+ }
1266
+ function normalizeEmptyScanIndex(raw) {
1267
+ const entries = {};
1268
+ if (raw && typeof raw === "object" && raw.entries && typeof raw.entries === "object") {
1269
+ for (const [id, entry] of Object.entries(raw.entries)) {
1270
+ if (typeof id !== "string" || !id || id.length > 200) continue;
1271
+ const normalized = normalizeEntry2(entry);
1272
+ if (normalized) entries[id] = normalized;
1273
+ }
1274
+ }
1275
+ return { schemaVersion: EMPTY_SCAN_SCHEMA_VERSION, entries };
1276
+ }
1277
+ function mergeEntries2(left, right) {
1278
+ const merged = { ...left };
1279
+ for (const [id, entry] of Object.entries(right)) merged[id] = entry;
1280
+ const ids = Object.keys(merged);
1281
+ if (ids.length > MAX_ENTRIES2) {
1282
+ ids.sort((a, b) => (merged[a].updatedAt || 0) - (merged[b].updatedAt || 0));
1283
+ for (const id of ids.slice(0, ids.length - MAX_ENTRIES2)) delete merged[id];
1284
+ }
1285
+ return merged;
1286
+ }
1287
+ function createEmptyScanStore({ dir, file }) {
1288
+ let cache = null;
1289
+ let chain = Promise.resolve();
1290
+ const path = file || join9(dir, "empty-scan.json");
1291
+ async function readRaw() {
1292
+ try {
1293
+ return normalizeEmptyScanIndex(JSON.parse(await readFile2(path, "utf8")));
1294
+ } catch (e) {
1295
+ return normalizeEmptyScanIndex(null);
1296
+ }
1297
+ }
1298
+ function enqueue(mutator) {
1299
+ const operation = chain.then(async () => {
1300
+ const store = cache || (cache = (await readRaw()).entries);
1301
+ await mutator(store);
1302
+ return store;
1303
+ });
1304
+ chain = operation.catch(() => {
1305
+ });
1306
+ return operation;
1307
+ }
1308
+ return {
1309
+ async entries() {
1310
+ if (cache) return cache;
1311
+ cache = (await readRaw()).entries;
1312
+ return cache;
1313
+ },
1314
+ async merge(batch) {
1315
+ const right = {};
1316
+ for (const [id, entry] of Object.entries(batch || {})) {
1317
+ const normalized = normalizeEntry2(entry);
1318
+ if (normalized) right[String(id)] = normalized;
1319
+ }
1320
+ if (!Object.keys(right).length) return false;
1321
+ await enqueue(async (store) => {
1322
+ const next = mergeEntries2(store, right);
1323
+ await mkdir8(dirname2(path), { recursive: true });
1324
+ const tmp = join9(dirname2(path), `.empty-scan-${process.pid}-${Date.now()}.tmp`);
1325
+ await writeFile8(tmp, JSON.stringify({ schemaVersion: EMPTY_SCAN_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
1326
+ await rename7(tmp, path);
1327
+ cache = next;
1328
+ });
1329
+ return true;
1330
+ },
1331
+ async remove(ids) {
1332
+ const wanted = new Set((ids || []).map(String));
1333
+ if (!wanted.size) return false;
1334
+ await enqueue(async (store) => {
1335
+ let changed = false;
1336
+ for (const id of wanted) {
1337
+ if (id in store) {
1338
+ delete store[id];
1339
+ changed = true;
1340
+ }
1341
+ }
1342
+ if (!changed) return;
1343
+ await mkdir8(dirname2(path), { recursive: true });
1344
+ const tmp = join9(dirname2(path), `.empty-scan-${process.pid}-${Date.now()}.tmp`);
1345
+ await writeFile8(tmp, JSON.stringify({ schemaVersion: EMPTY_SCAN_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
1346
+ await rename7(tmp, path);
801
1347
  });
802
1348
  return true;
803
1349
  }
@@ -806,7 +1352,7 @@ function createTitleIndexStore({ dir, file }) {
806
1352
 
807
1353
  // src/handle-era-paths.js
808
1354
  import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
809
- import { basename, join as join6 } from "node:path";
1355
+ import { basename, join as join10 } from "node:path";
810
1356
 
811
1357
  // src/path-guard.js
812
1358
  function splitSegments(target) {
@@ -879,8 +1425,8 @@ function resolveSessionRoot(sp) {
879
1425
  return typeof root === "string" && root.length > 0 ? root : null;
880
1426
  }
881
1427
  function deriveSessionDir(root, cwd, id) {
882
- const project = cwd === void 0 || cwd === null || cwd === "" ? join6(root, "_no-cwd") : join6(root, projectKeyFor(cwd));
883
- return join6(project, encodeSegmentFor(id));
1428
+ const project = cwd === void 0 || cwd === null || cwd === "" ? join10(root, "_no-cwd") : join10(root, projectKeyFor(cwd));
1429
+ return join10(project, encodeSegmentFor(id));
884
1430
  }
885
1431
  function generationVersionOf(name2) {
886
1432
  return Number((String(name2).match(/^session\.v(\d+)\./) || [])[1] || 0);
@@ -907,11 +1453,11 @@ async function locateSessionArtifacts(sp, header) {
907
1453
  const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
908
1454
  if (generationFiles.length === 0) return null;
909
1455
  generationFiles.sort((a, b) => generationVersionOf(b) - generationVersionOf(a));
910
- const logPath = join6(sessionDir, generationFiles[0]);
1456
+ const logPath = join10(sessionDir, generationFiles[0]);
911
1457
  if (!pathOwnsSession(logPath, sid)) return null;
912
1458
  return {
913
1459
  root,
914
- projectDir: join6(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
1460
+ projectDir: join10(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
915
1461
  sessionDir,
916
1462
  logPath,
917
1463
  generationFiles,
@@ -1121,8 +1667,8 @@ function requireCapability(capabilities, name2) {
1121
1667
  }
1122
1668
 
1123
1669
  // src/handle-era-ops.js
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";
1670
+ import { mkdir as mkdir9, readFile as readFile3, readdir as readdir3, rename as rename8, rm as rm2, stat as stat3, writeFile as writeFile9 } from "node:fs/promises";
1671
+ import { basename as basename2, dirname as dirname3, join as join11 } from "node:path";
1126
1672
  var MOVE_BATCH = 400;
1127
1673
  var REAL_LOG_RE = /^session.*\.jsonl(\.zst(d)?)?$/;
1128
1674
  var MOVE_TEMP_RE = /\.move-(backup|stage)-/;
@@ -1145,7 +1691,7 @@ async function cleanSiblingCopies(root, sid, keepDir, keepVersion) {
1145
1691
  const real = [];
1146
1692
  for (const ent of projects) {
1147
1693
  if (!ent.isDirectory()) continue;
1148
- const candidate = join7(root, ent.name, segment);
1694
+ const candidate = join11(root, ent.name, segment);
1149
1695
  if (keepDir && candidate === keepDir) continue;
1150
1696
  let entries;
1151
1697
  try {
@@ -1260,7 +1806,7 @@ async function purgeSessionArtifacts(sp, sid, header) {
1260
1806
  return artifacts;
1261
1807
  }
1262
1808
  async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts }) {
1263
- const original = await readFile2(backupLogPath);
1809
+ const original = await readFile3(backupLogPath);
1264
1810
  const frames = scanZstdFrames(original).frames;
1265
1811
  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");
1266
1812
  const rewritten = rewriteFrame0CwdInMemory(original, canonical);
@@ -1270,10 +1816,10 @@ async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifact
1270
1816
  throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
1271
1817
  }
1272
1818
  const targetDir = deriveSessionDir(artifacts.root, canonical, sid);
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]));
1819
+ await mkdir9(targetDir, { recursive: true });
1820
+ const staged = join11(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
1821
+ await writeFile9(staged, rewritten, { mode: 384 });
1822
+ await rename8(staged, join11(targetDir, artifacts.generationFiles[0]));
1277
1823
  return targetDir;
1278
1824
  }
1279
1825
  async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
@@ -1325,11 +1871,11 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1325
1871
  const firstSeq = events.length ? Number(events[0].seq) : 0;
1326
1872
  const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
1327
1873
  const createOptions = header.isSeeded && Number.isSafeInteger(inheritedEventCount) && inheritedEventCount > 0 ? { inheritedEventCount } : void 0;
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);
1874
+ const backupRoot = join11(dirname3(artifacts.root), "sessions-manager-move-backup");
1875
+ const backupDir = join11(backupRoot, `${encodeSegmentFor(sid)}-${process.pid}-${Date.now()}`);
1876
+ await mkdir9(backupRoot, { recursive: true });
1877
+ const backupLogPath = join11(backupDir, basename2(artifacts.logPath));
1878
+ await rename8(artifacts.sessionDir, backupDir);
1333
1879
  let writer = null;
1334
1880
  try {
1335
1881
  try {
@@ -1362,11 +1908,11 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1362
1908
  } catch (_) {
1363
1909
  }
1364
1910
  try {
1365
- await mkdir5(dirname2(artifacts.sessionDir), { recursive: true });
1911
+ await mkdir9(dirname3(artifacts.sessionDir), { recursive: true });
1366
1912
  } catch (_) {
1367
1913
  }
1368
1914
  try {
1369
- await rename5(backupDir, artifacts.sessionDir);
1915
+ await rename8(backupDir, artifacts.sessionDir);
1370
1916
  } catch (_) {
1371
1917
  }
1372
1918
  if (e && e.status) throw e;
@@ -1383,12 +1929,6 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1383
1929
  }
1384
1930
 
1385
1931
  // 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
1932
  var EMPTY_DECODE_LIMIT = 8192;
1393
1933
  var LIFECYCLE_EVENT_TYPES = /* @__PURE__ */ new Set([
1394
1934
  "session",
@@ -1403,24 +1943,26 @@ function isEmptyEventTypes(types) {
1403
1943
  }
1404
1944
  return true;
1405
1945
  }
1406
- function classifyLineage(header, sizeBytes) {
1946
+ function classifyLineage(header) {
1407
1947
  if (!header || typeof header !== "object") return null;
1408
1948
  const origin = header.origin === "subagent" ? "subagent" : null;
1409
1949
  const parentSession = typeof header.parentSession === "string" && header.parentSession ? header.parentSession : null;
1410
1950
  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 };
1951
+ if (!origin && !parentSession) return null;
1952
+ return { origin, parentSession, delegationDepth, empty: null };
1953
+ }
1954
+ function emptyScanCandidate(sizeBytes) {
1955
+ return Number.isFinite(sizeBytes) && sizeBytes <= EMPTY_DECODE_LIMIT;
1414
1956
  }
1415
1957
 
1416
1958
  // src/index.js
1417
- var BUILD_STAMP = true ? "3.6.1+f0814a9e" : "dev";
1959
+ var BUILD_STAMP = true ? "3.7.0+95376789" : "dev";
1418
1960
  var name = "dsh-sessions-manager";
1419
1961
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
1420
1962
  var MAX_TITLE = 80;
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");
1963
+ var STATE_DIR = process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join12(homedir7(), ".dsh", "sessions-manager");
1964
+ var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join12(homedir7(), ".dsh", "sessions-manager-trash");
1965
+ var TRASH_INDEX = join12(TRASH_DIR, "index.json");
1424
1966
  var TRASH_SCHEMA_VERSION = 2;
1425
1967
  var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
1426
1968
  var FETCH_TOOL_RE = /search|fetch|download|browse/i;
@@ -1467,14 +2009,14 @@ function parseIds(body) {
1467
2009
  const raw = body && body.sessionIds;
1468
2010
  if (!Array.isArray(raw)) return null;
1469
2011
  const ids = [];
1470
- for (const v of raw) if (typeof v === "string" && isSafeSessionId2(v)) ids.push(v);
2012
+ for (const v of raw) if (typeof v === "string" && isSafeSessionId3(v)) ids.push(v);
1471
2013
  return ids;
1472
2014
  }
1473
- function isSafeSessionId2(value) {
2015
+ function isSafeSessionId3(value) {
1474
2016
  return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
1475
2017
  }
1476
2018
  function requireSessionId(value) {
1477
- if (!isSafeSessionId2(value)) {
2019
+ if (!isSafeSessionId3(value)) {
1478
2020
  const error = new Error("\u65E0\u6548\u7684 sessionId");
1479
2021
  error.status = 400;
1480
2022
  throw error;
@@ -1499,20 +2041,6 @@ function getActiveSessionId(context) {
1499
2041
  }
1500
2042
  return null;
1501
2043
  }
1502
- function foldTitle(events) {
1503
- let found = null;
1504
- let firstUser = null;
1505
- for (const ev of events) {
1506
- if (ev.type === "session/title" && ev.data && typeof ev.data.title === "string" && ev.data.title.length) {
1507
- found = ev.data.title;
1508
- }
1509
- if (firstUser === null && ev.type === "user/message" && ev.data && Array.isArray(ev.data.content)) {
1510
- const txt = ev.data.content.filter((b) => b && b.type === "text").map((b) => b.text).filter(Boolean).join(" ").trim();
1511
- if (txt) firstUser = txt;
1512
- }
1513
- }
1514
- return found || firstUser || null;
1515
- }
1516
2044
  function apply(ctx) {
1517
2045
  const w = ctx.workspaceRegistry;
1518
2046
  const sp = ctx.sessionPersistence;
@@ -1522,7 +2050,9 @@ function apply(ctx) {
1522
2050
  const dom = () => ctx.storageDomain.get("workspace");
1523
2051
  const authorityTitleCache = /* @__PURE__ */ new Map();
1524
2052
  const metaCache = createSessionMetaCache();
1525
- const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join8(TRASH_DIR, "title-index.json") });
2053
+ const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join12(TRASH_DIR, "title-index.json") });
2054
+ const emptyIndex = createEmptyScanStore({ dir: TRASH_DIR });
2055
+ const EMPTY_VERDICT_TTL_MS = 14 * 24 * 3600 * 1e3;
1526
2056
  async function hydrateFromPersist(ids, statsById) {
1527
2057
  const hits = /* @__PURE__ */ new Map();
1528
2058
  if (!ids || !ids.length) return hits;
@@ -1538,7 +2068,7 @@ function apply(ctx) {
1538
2068
  if (!stat5 || !entry) continue;
1539
2069
  const fp = persistFingerprintOf(stat5);
1540
2070
  if (!fp) continue;
1541
- if (fp && entry.fingerprint === fp) {
2071
+ if (entry.fingerprint === fp && (entry.title || entry.resolved)) {
1542
2072
  hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
1543
2073
  }
1544
2074
  }
@@ -1548,10 +2078,12 @@ function apply(ctx) {
1548
2078
  if (!decoded || !decoded.size) return;
1549
2079
  const batch = {};
1550
2080
  const now = Date.now();
1551
- for (const [id, meta] of decoded) {
2081
+ for (const [id, pack] of decoded) {
2082
+ if (!pack || !pack.resolved) continue;
2083
+ const meta = pack.meta;
1552
2084
  const fp = persistFingerprintOf(statsById.get(id));
1553
2085
  if (!fp) continue;
1554
- batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
2086
+ batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now, resolved: 1 };
1555
2087
  }
1556
2088
  if (!Object.keys(batch).length) return;
1557
2089
  try {
@@ -1634,6 +2166,60 @@ function apply(ctx) {
1634
2166
  const warmQueue = /* @__PURE__ */ new Map();
1635
2167
  let warmRunning = false;
1636
2168
  let warmKickTimer = null;
2169
+ const WARM_MAX_ATTEMPTS = 5;
2170
+ const warmFail = /* @__PURE__ */ new Map();
2171
+ function warmFpOf(stat5) {
2172
+ return fingerprintOf(stat5) || persistFingerprintOf(stat5);
2173
+ }
2174
+ function warmBlocked(id, fp) {
2175
+ const rec = warmFail.get(id);
2176
+ if (!rec) return 0;
2177
+ if (fp && rec.fp && rec.fp !== fp) {
2178
+ warmFail.delete(id);
2179
+ return 0;
2180
+ }
2181
+ const now = Date.now();
2182
+ return rec.nextAt > now ? rec.nextAt - now : 0;
2183
+ }
2184
+ function warmMarkFailure(id, stat5, header) {
2185
+ const rec = warmFail.get(id) || { count: 0, fp: null, nextAt: 0 };
2186
+ rec.count += 1;
2187
+ rec.fp = warmFpOf(stat5) || rec.fp;
2188
+ if (stat5) rec.stat = stat5;
2189
+ if (header) rec.header = header;
2190
+ const backoff = rec.count >= WARM_MAX_ATTEMPTS ? 60 * 60 * 1e3 : Math.min(5 * 60 * 1e3, 15 * 1e3 * Math.pow(2, rec.count));
2191
+ rec.nextAt = Date.now() + backoff;
2192
+ warmFail.set(id, rec);
2193
+ }
2194
+ function warmPendingNow() {
2195
+ if (!warmHasApi) return false;
2196
+ if (warmRunning || warmQueue.size) return true;
2197
+ for (const rec of warmFail.values()) if (rec.nextAt > Date.now()) return true;
2198
+ return false;
2199
+ }
2200
+ let warmRetryTimer = null;
2201
+ function scheduleWarmRetry() {
2202
+ if (warmRetryTimer) return;
2203
+ let soonest = 0;
2204
+ for (const rec of warmFail.values()) {
2205
+ const wait = rec.nextAt - Date.now();
2206
+ if (wait <= 0) {
2207
+ soonest = 1;
2208
+ break;
2209
+ }
2210
+ if (!soonest || wait < soonest) soonest = wait;
2211
+ }
2212
+ if (!soonest) return;
2213
+ warmRetryTimer = setTimeout(() => {
2214
+ warmRetryTimer = null;
2215
+ const now = Date.now();
2216
+ for (const [id, rec] of warmFail) {
2217
+ if (rec.nextAt <= now && !warmQueue.has(id)) warmQueue.set(id, { stat: rec.stat || null, header: rec.header || null });
2218
+ }
2219
+ if (warmQueue.size) scheduleWarm();
2220
+ }, soonest);
2221
+ if (typeof warmRetryTimer.unref === "function") warmRetryTimer.unref();
2222
+ }
1637
2223
  function scheduleWarm() {
1638
2224
  if (warmKickTimer || warmRunning) return;
1639
2225
  warmKickTimer = setTimeout(() => {
@@ -1644,15 +2230,44 @@ function apply(ctx) {
1644
2230
  }
1645
2231
  function enqueueWarm(ids, statsById, entryById) {
1646
2232
  if (!ids || !ids.length) return;
2233
+ if (!warmHasApi) return;
1647
2234
  for (const id of ids) {
1648
2235
  const key = String(id);
2236
+ const stat5 = statsById ? statsById.get(key) || null : null;
2237
+ const fp = warmFpOf(stat5);
2238
+ if (!fp) continue;
2239
+ const blockedMs = warmBlocked(key, fp);
1649
2240
  const entry = entryById ? entryById.get(key) : null;
2241
+ const rec = warmFail.get(key);
2242
+ if (rec) {
2243
+ rec.stat = stat5 || rec.stat;
2244
+ if (entry) rec.header = entry.header || rec.header;
2245
+ }
2246
+ if (blockedMs > 0) continue;
1650
2247
  warmQueue.set(key, {
1651
- stat: statsById ? statsById.get(key) || null : null,
2248
+ stat: stat5,
1652
2249
  header: entry && entry.header || null
1653
2250
  });
1654
2251
  }
1655
- scheduleWarm();
2252
+ if (warmQueue.size) scheduleWarm();
2253
+ scheduleWarmRetry();
2254
+ }
2255
+ async function backfillTrashTitles(decoded) {
2256
+ if (!decoded || !decoded.size) return;
2257
+ try {
2258
+ const items = await readTrash();
2259
+ const worth = items.some((t) => !t.title && decoded.get(String(t.sessionId)) && decoded.get(String(t.sessionId)).meta && decoded.get(String(t.sessionId)).meta.title);
2260
+ if (!worth) return;
2261
+ await mutateTrash((store) => {
2262
+ for (const item of store.items) {
2263
+ if (item.title) continue;
2264
+ const pack = decoded.get(String(item.sessionId));
2265
+ const t = pack && pack.meta && pack.meta.title;
2266
+ if (t) item.title = String(t);
2267
+ }
2268
+ });
2269
+ } catch (e) {
2270
+ }
1656
2271
  }
1657
2272
  async function runWarm() {
1658
2273
  if (warmRunning) return;
@@ -1663,21 +2278,27 @@ function apply(ctx) {
1663
2278
  for (const [id] of batch) warmQueue.delete(id);
1664
2279
  try {
1665
2280
  const ids = batch.map(([id]) => id);
1666
- const snapshotById = await projectTitles(ids);
2281
+ const resultById = await projectTitlesStatus(ids);
1667
2282
  const decoded = /* @__PURE__ */ new Map();
1668
2283
  const statsById = /* @__PURE__ */ new Map();
1669
2284
  for (const [id, desc] of batch) {
1670
- const snapshot = snapshotById.has(id) ? snapshotById.get(id) : null;
1671
- const meta = metaFromSnapshot(snapshot);
2285
+ const r = resultById.get(id);
2286
+ if (!r || !r.resolved) {
2287
+ warmMarkFailure(id, desc.stat, desc.header);
2288
+ continue;
2289
+ }
2290
+ warmFail.delete(id);
2291
+ const meta = metaFromSnapshot(r.snapshot || null);
1672
2292
  if (desc.header) {
1673
2293
  if (typeof desc.header.cwd === "string") meta.cwd = desc.header.cwd;
1674
2294
  if (desc.header.createdAt != null) meta.createdAt = desc.header.createdAt;
1675
2295
  }
1676
2296
  metaCache.set(id, desc.stat, meta);
1677
2297
  statsById.set(id, desc.stat);
1678
- if (desc.stat) decoded.set(id, meta);
2298
+ decoded.set(id, { meta, resolved: true });
1679
2299
  }
1680
2300
  await persistDecoded(decoded, statsById);
2301
+ await backfillTrashTitles(decoded);
1681
2302
  } catch (e) {
1682
2303
  }
1683
2304
  await new Promise((resolve) => setImmediate(resolve));
@@ -1685,6 +2306,7 @@ function apply(ctx) {
1685
2306
  } finally {
1686
2307
  warmRunning = false;
1687
2308
  if (warmQueue.size) scheduleWarm();
2309
+ scheduleWarmRetry();
1688
2310
  }
1689
2311
  }
1690
2312
  async function collectUsage(preloadedEntries) {
@@ -1742,12 +2364,12 @@ function apply(ctx) {
1742
2364
  schemaVersion: TRASH_SCHEMA_VERSION,
1743
2365
  settings: { retentionDays },
1744
2366
  items: raw && Array.isArray(raw.items) ? raw.items : [],
1745
- purgedSessionIds: raw && Array.isArray(raw.purgedSessionIds) ? [...new Set(raw.purgedSessionIds.filter(isSafeSessionId2).map(String))] : []
2367
+ purgedSessionIds: raw && Array.isArray(raw.purgedSessionIds) ? [...new Set(raw.purgedSessionIds.filter(isSafeSessionId3).map(String))] : []
1746
2368
  };
1747
2369
  }
1748
2370
  async function readTrashStore() {
1749
2371
  try {
1750
- return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
2372
+ return normalizeTrashStore(JSON.parse(readFileSync7(TRASH_INDEX, "utf8")));
1751
2373
  } catch (e) {
1752
2374
  return normalizeTrashStore(null);
1753
2375
  }
@@ -1756,10 +2378,10 @@ function apply(ctx) {
1756
2378
  return (await readTrashStore()).items;
1757
2379
  }
1758
2380
  async function writeTrashStore(store) {
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);
2381
+ await mkdir10(TRASH_DIR, { recursive: true });
2382
+ const tmp = join12(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
2383
+ await writeFile10(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
2384
+ await rename9(tmp, TRASH_INDEX);
1763
2385
  }
1764
2386
  function mutateTrash(mutator) {
1765
2387
  const operation = trashMutation.then(async () => {
@@ -1773,7 +2395,10 @@ function apply(ctx) {
1773
2395
  return operation;
1774
2396
  }
1775
2397
  const stars = createStarIndex();
2398
+ const tags = createTagIndex();
2399
+ const savedFilters = createSavedFilters();
1776
2400
  const pendingMoves = createPendingMoveStore();
2401
+ const moveNotices = createMoveNoticeStore();
1777
2402
  const autoArchive = createAutoArchiveStore();
1778
2403
  sweepStaleStateTemps([STATE_DIR, TRASH_DIR], {}).catch(() => {
1779
2404
  });
@@ -1786,6 +2411,15 @@ function apply(ctx) {
1786
2411
  } catch (e) {
1787
2412
  }
1788
2413
  }
2414
+ async function gcTags(validIds) {
2415
+ try {
2416
+ const store = await tags.read();
2417
+ const valid = new Set(validIds.map(String));
2418
+ const gone = Object.keys(store.assignments).filter((id) => !valid.has(id));
2419
+ if (gone.length) await tags.removeIds(gone);
2420
+ } catch (e) {
2421
+ }
2422
+ }
1789
2423
  async function deleteOne(sid) {
1790
2424
  requireSessionId(sid);
1791
2425
  let header = null;
@@ -1804,30 +2438,19 @@ function apply(ctx) {
1804
2438
  cwd = header.cwd || null;
1805
2439
  title = header.title || header.meta && header.meta.title || null;
1806
2440
  }
1807
- if (!title) {
1808
- if (typeof sq.readTitleSnapshot === "function") {
1809
- try {
1810
- const snap = unwrapSnapshot(await sq.readTitleSnapshot(sid));
1811
- if (snap && snap.title && snap.title.title) title = String(snap.title.title);
1812
- if (snap && snap.session) {
1813
- if (!cwd) cwd = snap.session.cwd || null;
1814
- }
1815
- } catch (e) {
1816
- }
1817
- }
1818
- }
1819
2441
  if (!title) {
1820
2442
  try {
1821
- let folded = null;
1822
- const summary = await persistence.inspectSession(sid, {
1823
- onEvents: (events) => {
1824
- if (!folded) folded = foldTitle(events);
1825
- }
1826
- });
1827
- if (summary && summary.meta && !cwd) cwd = summary.meta.cwd || null;
1828
- title = folded;
2443
+ const delStat = persistenceEntry && typeof persistenceEntry.revision === "string" && persistenceEntry.revision ? { revision: persistenceEntry.revision, sizeBytes: Number.isFinite(persistenceEntry.sizeBytes) ? Number(persistenceEntry.sizeBytes) : void 0 } : removedPath ? await stat4(removedPath).then((st) => ({ mtimeMs: st.mtimeMs, size: st.size })) : null;
2444
+ if (delStat) {
2445
+ const m = metaCache.get(sid, delStat) || (await hydrateFromPersist([sid], /* @__PURE__ */ new Map([[sid, delStat]]))).get(sid);
2446
+ if (m && m.title) title = String(m.title);
2447
+ }
1829
2448
  } catch (e) {
1830
2449
  }
2450
+ if (!title) {
2451
+ const at = authorityTitleCache.get(sid);
2452
+ if (at) title = String(at);
2453
+ }
1831
2454
  }
1832
2455
  } catch (e) {
1833
2456
  }
@@ -1839,8 +2462,10 @@ function apply(ctx) {
1839
2462
  const archived = await mutateArchived((list) => ({ next: null, value: list.includes(sid) })).catch(() => false);
1840
2463
  await mutateTrash((store) => {
1841
2464
  const entry = {
2465
+ // v3.6.2 #7:不再把 cwd/裸 id 冒充标题永久定格——存 null,预热解出后
2466
+ // backfillTrashTitles 补写;渲染端(t.title || t.sessionId)自然降级。
1842
2467
  sessionId: sid,
1843
- title: title || cwd || sid,
2468
+ title: title || null,
1844
2469
  cwd: cwd || null,
1845
2470
  header: header || null,
1846
2471
  originalPath: removedPath || null,
@@ -2047,6 +2672,10 @@ function apply(ctx) {
2047
2672
  await stars.removeIds([sid]);
2048
2673
  } catch (e) {
2049
2674
  }
2675
+ try {
2676
+ await tags.removeIds([sid]);
2677
+ } catch (e) {
2678
+ }
2050
2679
  return { ok: true, purged: true };
2051
2680
  }
2052
2681
  async function trashSettings(next) {
@@ -2082,8 +2711,8 @@ function apply(ctx) {
2082
2711
  async function moveTargetWorkspace(rawPath) {
2083
2712
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
2084
2713
  let p = String(rawPath).trim();
2085
- if (p.startsWith("~/")) p = join8(homedir4(), p.slice(2));
2086
- if (!isAbsolute(p)) p = join8(homedir4(), p);
2714
+ if (p.startsWith("~/")) p = join12(homedir7(), p.slice(2));
2715
+ if (!isAbsolute(p)) p = join12(homedir7(), p);
2087
2716
  let canonical = null;
2088
2717
  try {
2089
2718
  canonical = await realpath(p);
@@ -2091,7 +2720,7 @@ function apply(ctx) {
2091
2720
  canonical = null;
2092
2721
  }
2093
2722
  if (canonical === null) {
2094
- await mkdir6(p, { recursive: true });
2723
+ await mkdir10(p, { recursive: true });
2095
2724
  canonical = await realpath(p);
2096
2725
  }
2097
2726
  return { canonical, entity: await w.create(canonical, basename3(canonical) || "workspace") };
@@ -2136,15 +2765,15 @@ function apply(ctx) {
2136
2765
  const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
2137
2766
  let destinationInstalled = false;
2138
2767
  try {
2139
- await mkdir6(dirname3(newPath), { recursive: true });
2768
+ await mkdir10(dirname4(newPath), { recursive: true });
2140
2769
  try {
2141
2770
  await stat4(newPath);
2142
2771
  throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
2143
2772
  } catch (e) {
2144
2773
  if (e && e.code !== "ENOENT") throw e;
2145
2774
  }
2146
- await rename6(oldPath, backupPath);
2147
- const original = await readFile3(backupPath);
2775
+ await rename9(oldPath, backupPath);
2776
+ const original = await readFile4(backupPath);
2148
2777
  const originalFrames = scanZstdFrames(original).frames;
2149
2778
  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");
2150
2779
  const rewritten = rewriteFrame0CwdInMemory(original, canonical);
@@ -2153,11 +2782,11 @@ function apply(ctx) {
2153
2782
  const originalTail = original.subarray(originalFrames[0].end);
2154
2783
  const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
2155
2784
  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");
2156
- await writeFile6(stagedPath, rewritten, { mode: 384 });
2157
- await rename6(stagedPath, newPath);
2785
+ await writeFile10(stagedPath, rewritten, { mode: 384 });
2786
+ await rename9(stagedPath, newPath);
2158
2787
  destinationInstalled = true;
2159
2788
  await unlink(backupPath);
2160
- const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
2789
+ const keptInSource = await cleanupMovedSourceDir(dirname4(oldPath));
2161
2790
  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`);
2162
2791
  } catch (e) {
2163
2792
  try {
@@ -2171,7 +2800,7 @@ function apply(ctx) {
2171
2800
  }
2172
2801
  }
2173
2802
  try {
2174
- await rename6(backupPath, oldPath);
2803
+ await rename9(backupPath, oldPath);
2175
2804
  } catch (_) {
2176
2805
  }
2177
2806
  if (e && e.code !== "ENOENT") throw e;
@@ -2251,7 +2880,7 @@ function apply(ctx) {
2251
2880
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
2252
2881
  if (backupPath) {
2253
2882
  try {
2254
- await rename6(oldPath, backupPath);
2883
+ await rename9(oldPath, backupPath);
2255
2884
  } catch (e) {
2256
2885
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
2257
2886
  }
@@ -2259,7 +2888,7 @@ function apply(ctx) {
2259
2888
  const restore = async () => {
2260
2889
  if (backupPath) {
2261
2890
  try {
2262
- await rename6(backupPath, oldPath);
2891
+ await rename9(backupPath, oldPath);
2263
2892
  } catch (_) {
2264
2893
  }
2265
2894
  }
@@ -2278,7 +2907,7 @@ function apply(ctx) {
2278
2907
  }
2279
2908
  }
2280
2909
  if (oldPath) {
2281
- const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
2910
+ const keptInSource = await cleanupMovedSourceDir(dirname4(oldPath));
2282
2911
  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
2912
  }
2284
2913
  } catch (e) {
@@ -2361,11 +2990,21 @@ function apply(ctx) {
2361
2990
  metaCache.invalidate(item.sessionId);
2362
2991
  moved++;
2363
2992
  notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u5DF2\u5B8C\u6210\uFF1A${String(item.sessionId).slice(0, 18)}\u2026`);
2993
+ try {
2994
+ await moveNotices.append({ kind: "moved", sessionId: item.sessionId, targetPath: item.targetPath, at: Date.now() });
2995
+ } catch (e) {
2996
+ }
2364
2997
  } catch (e) {
2365
2998
  if (isBusyError(e)) continue;
2366
2999
  if (Date.now() - bootedAt < BOOT_WARMUP_MS) continue;
2367
3000
  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`);
3001
+ if (bumped && bumped.dropped) {
3002
+ 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`);
3003
+ try {
3004
+ await moveNotices.append({ kind: "abandoned", sessionId: item.sessionId, targetPath: item.targetPath, at: Date.now(), attempts: bumped.attempts, reason: String(e && e.message || e).split("\n")[0] });
3005
+ } catch (err) {
3006
+ }
3007
+ }
2369
3008
  }
2370
3009
  }
2371
3010
  if (moved > 0) {
@@ -2455,19 +3094,50 @@ function apply(ctx) {
2455
3094
  return { next: null, value: { ok: true, archived: true } };
2456
3095
  });
2457
3096
  }
2458
- async function projectTitles(ids) {
3097
+ const warmHasApi = typeof sq.readTitleSnapshots === "function" || typeof sq.readTitleSnapshot === "function";
3098
+ async function projectTitlesStatus(ids) {
2459
3099
  const out = /* @__PURE__ */ new Map();
3100
+ const markAll = (resolved) => {
3101
+ for (const id of ids) out.set(String(id), { resolved, snapshot: null });
3102
+ };
2460
3103
  if (!ids || !ids.length) return out;
2461
- if (typeof sq.readTitleSnapshots !== "function") return out;
2462
- try {
2463
- const results = await sq.readTitleSnapshots(ids);
2464
- if (!Array.isArray(results)) return out;
2465
- results.forEach((result, index) => {
2466
- const id = String(ids[index]);
2467
- out.set(id, unwrapSnapshot(result));
2468
- });
2469
- } catch (e) {
3104
+ if (!warmHasApi) {
3105
+ markAll(false);
3106
+ return out;
3107
+ }
3108
+ if (typeof sq.readTitleSnapshots === "function") {
3109
+ let results = null;
3110
+ try {
3111
+ results = await sq.readTitleSnapshots(ids);
3112
+ } catch (e) {
3113
+ results = null;
3114
+ }
3115
+ if (Array.isArray(results)) {
3116
+ results.forEach((result, index) => {
3117
+ const id = String(ids[index]);
3118
+ if (result && result.status === "fulfilled") out.set(id, { resolved: true, snapshot: unwrapSnapshot(result) });
3119
+ else if (result && result.status === "rejected") out.set(id, { resolved: false, snapshot: null });
3120
+ else if (result) out.set(id, { resolved: true, snapshot: unwrapSnapshot(result) });
3121
+ else out.set(id, { resolved: false, snapshot: null });
3122
+ });
3123
+ for (const id of ids) if (!out.has(String(id))) out.set(String(id), { resolved: false, snapshot: null });
3124
+ return out;
3125
+ }
2470
3126
  }
3127
+ if (typeof sq.readTitleSnapshot === "function") {
3128
+ for (const raw of ids) {
3129
+ const id = String(raw);
3130
+ try {
3131
+ const r = await sq.readTitleSnapshot(id);
3132
+ if (r && r.status === "rejected") out.set(id, { resolved: false, snapshot: null });
3133
+ else out.set(id, { resolved: true, snapshot: unwrapSnapshot(r) });
3134
+ } catch (e) {
3135
+ out.set(id, { resolved: false, snapshot: null });
3136
+ }
3137
+ }
3138
+ return out;
3139
+ }
3140
+ markAll(false);
2471
3141
  return out;
2472
3142
  }
2473
3143
  async function allSessionItemsDetailed(opts = {}) {
@@ -2553,12 +3223,23 @@ function apply(ctx) {
2553
3223
  return { items: archivedItems, usage };
2554
3224
  }
2555
3225
  let starredSet = /* @__PURE__ */ new Set();
3226
+ let tagIdsBySession = /* @__PURE__ */ new Map();
2556
3227
  try {
2557
3228
  starredSet = new Set((await stars.read()).starredSessionIds);
3229
+ const tagStore = await tags.read();
3230
+ const knownTagIds = new Set(tagStore.tags.map((t) => t.id));
3231
+ for (const [sid, list] of Object.entries(tagStore.assignments)) {
3232
+ const kept = list.filter((id) => knownTagIds.has(id));
3233
+ if (kept.length) tagIdsBySession.set(sid, kept);
3234
+ }
2558
3235
  } catch (e) {
2559
3236
  }
2560
- for (const it of items) it.starred = starredSet.has(String(it.sessionId));
3237
+ for (const it of items) {
3238
+ it.starred = starredSet.has(String(it.sessionId));
3239
+ it.tags = tagIdsBySession.get(String(it.sessionId)) || [];
3240
+ }
2561
3241
  if (headersOk) await gcStars(ids);
3242
+ if (headersOk) await gcTags(ids);
2562
3243
  return { items, usage };
2563
3244
  }
2564
3245
  async function allSessionItems(opts = {}) {
@@ -2653,56 +3334,121 @@ function apply(ctx) {
2653
3334
  }
2654
3335
  }
2655
3336
  const lineage = {};
3337
+ const refineWanted = [];
3338
+ let emptyStore = null;
3339
+ try {
3340
+ emptyStore = await emptyIndex.entries();
3341
+ } catch (e) {
3342
+ emptyStore = null;
3343
+ }
3344
+ const nowTs = Date.now();
2656
3345
  for (const entry of entries) {
2657
- const info = classifyLineage(entry.header, entry.sizeBytes);
2658
- if (info) lineage[entry.id] = info;
3346
+ const id = String(entry.id);
3347
+ const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
3348
+ const candidate = emptyScanCandidate(size);
3349
+ let verdict = null;
3350
+ if (!candidate) {
3351
+ if (size !== null) verdict = false;
3352
+ } else {
3353
+ const mem = emptyScanCache.get(id);
3354
+ if (mem && mem.sizeBytes === size) {
3355
+ verdict = !!mem.empty;
3356
+ } else {
3357
+ const rec = emptyStore && emptyStore[id];
3358
+ if (rec && rec.fingerprint === `sz:${size}` && nowTs - (rec.updatedAt || 0) <= EMPTY_VERDICT_TTL_MS) {
3359
+ verdict = rec.empty === 1;
3360
+ emptyScanCache.set(id, { sizeBytes: size, empty: verdict });
3361
+ }
3362
+ }
3363
+ if (verdict === null && refineCapable() && !refineQueue.has(id)) refineWanted.push({ id, sizeBytes: size });
3364
+ }
3365
+ const info = classifyLineage(entry.header);
3366
+ if (info) lineage[id] = { origin: info.origin, parentSession: info.parentSession, delegationDepth: info.delegationDepth, empty: verdict };
3367
+ else if (verdict === true) lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: true };
2659
3368
  }
2660
- await refineEmptyLineage(lineage, entries);
2661
- return {
3369
+ enqueueRefine(refineWanted);
3370
+ for (const key of emptyScanCache.keys()) {
3371
+ const rec = emptyScanCache.get(key);
3372
+ if (!rec || !emptyScanCandidate(rec.sizeBytes)) emptyScanCache.delete(key);
3373
+ }
3374
+ const payload = {
2662
3375
  titles: Object.fromEntries(authorityTitleCache),
2663
3376
  trashedSessionIds: store.items.map((item) => String(item.sessionId)),
2664
3377
  purgedSessionIds: activeTombstones,
2665
- lineage
3378
+ lineage,
3379
+ // v3.6.2:还有预热/退避重试在途 → client 缩短轮询节拍,预热一完成就把
3380
+ // 补齐的标题送回(含面板自动刷新)。无预热 API 时恒 false,绝不假忙。
3381
+ warmPending: warmPendingNow(),
3382
+ // T1:空白精判后台队列是否还有活。无精判能力(旧 runtime 无 inspect 通道)
3383
+ // 时恒 false——永久 busy 会让 client 疯轮询,这是唯一现实的假忙陷阱。
3384
+ refinePending: refinePendingNow()
2666
3385
  };
3386
+ try {
3387
+ const noticeRows = await moveNotices.list();
3388
+ if (noticeRows.length) payload.moveNotices = noticeRows;
3389
+ } catch (e) {
3390
+ }
3391
+ return payload;
2667
3392
  }
2668
3393
  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;
3394
+ const REFINE_CHUNK = 2;
3395
+ const refineQueue = /* @__PURE__ */ new Map();
3396
+ let refineRunning = false;
3397
+ let refineKickTimer = null;
3398
+ function refineCapable() {
3399
+ return !!persistence && typeof persistence.inspectSession === "function";
3400
+ }
3401
+ function refinePendingNow() {
3402
+ return !!(refineRunning || refineQueue.size);
3403
+ }
3404
+ function scheduleRefine() {
3405
+ if (refineKickTimer || refineRunning || !refineQueue.size) return;
3406
+ refineKickTimer = setTimeout(() => {
3407
+ refineKickTimer = null;
3408
+ runRefine();
3409
+ }, 25);
3410
+ if (typeof refineKickTimer.unref === "function") refineKickTimer.unref();
3411
+ }
3412
+ function enqueueRefine(items) {
3413
+ if (!items || !items.length || !refineCapable()) return;
3414
+ for (const { id, sizeBytes } of items) {
3415
+ if (emptyScanCache.has(id) && emptyScanCache.get(id).sizeBytes === sizeBytes) continue;
3416
+ refineQueue.set(id, { sizeBytes });
3417
+ }
3418
+ scheduleRefine();
3419
+ }
3420
+ async function runRefine() {
3421
+ if (refineRunning) return;
3422
+ refineRunning = true;
3423
+ try {
3424
+ while (refineQueue.size) {
3425
+ const batch = [...refineQueue.entries()].slice(0, REFINE_CHUNK);
3426
+ for (const [id] of batch) refineQueue.delete(id);
3427
+ const persistBatch = {};
3428
+ for (const [id, desc] of batch) {
3429
+ try {
3430
+ const types = [];
3431
+ await persistence.inspectSession(id, { onEvents: (b) => {
3432
+ for (const ev of b || []) if (types.length < 64) types.push(ev && ev.type);
3433
+ } });
3434
+ const isEmpty = isEmptyEventTypes(types);
3435
+ emptyScanCache.set(id, { sizeBytes: desc.sizeBytes, empty: isEmpty });
3436
+ persistBatch[id] = { empty: isEmpty ? 1 : 0, fingerprint: `sz:${desc.sizeBytes}`, updatedAt: Date.now() };
3437
+ } catch (e) {
3438
+ emptyScanCache.set(id, { sizeBytes: desc.sizeBytes, empty: false });
3439
+ }
2686
3440
  }
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];
3441
+ if (Object.keys(persistBatch).length) {
3442
+ try {
3443
+ await emptyIndex.merge(persistBatch);
3444
+ } catch (e) {
3445
+ }
3446
+ }
3447
+ await new Promise((resolve) => setImmediate(resolve));
2697
3448
  }
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);
3449
+ } finally {
3450
+ refineRunning = false;
3451
+ if (refineQueue.size) scheduleRefine();
2706
3452
  }
2707
3453
  }
2708
3454
  async function buildDetails(sid, signal) {
@@ -3058,6 +3804,10 @@ function apply(ctx) {
3058
3804
  await titleIndex.remove([sid]);
3059
3805
  } catch (e) {
3060
3806
  }
3807
+ try {
3808
+ await emptyIndex.remove([sid]);
3809
+ } catch (e) {
3810
+ }
3061
3811
  json(res, out);
3062
3812
  } catch (e) {
3063
3813
  json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
@@ -3079,6 +3829,8 @@ function apply(ctx) {
3079
3829
  metaCache.invalidate(sid);
3080
3830
  await titleIndex.remove([sid]).catch(() => {
3081
3831
  });
3832
+ await emptyIndex.remove([sid]).catch(() => {
3833
+ });
3082
3834
  } catch (e) {
3083
3835
  results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
3084
3836
  }
@@ -3094,7 +3846,7 @@ function apply(ctx) {
3094
3846
  path: "/archived-sessions/sessions",
3095
3847
  handler: async (req, res) => {
3096
3848
  try {
3097
- json(res, { items: await allSessionItems() });
3849
+ json(res, { items: await allSessionItems({ usage: true }), warmPending: warmPendingNow() });
3098
3850
  } catch (e) {
3099
3851
  json(res, { error: String(e && e.message || e) }, 500);
3100
3852
  }
@@ -3109,13 +3861,133 @@ function apply(ctx) {
3109
3861
  const starred = !!(body && body.starred);
3110
3862
  let ids = parseIds(body);
3111
3863
  if ((!ids || ids.length === 0) && body && typeof body.sessionId === "string") {
3112
- ids = isSafeSessionId2(body.sessionId) ? [body.sessionId] : null;
3864
+ ids = isSafeSessionId3(body.sessionId) ? [body.sessionId] : null;
3113
3865
  }
3114
- if (!ids || ids.length === 0) return json(res, { ok: false, error: "missing sessionId" }, 400);
3866
+ if (!ids || ids.length === 0) return json(res, { ok: false, code: "DSM_STAR_SESSION_INVALID", error: "missing sessionId" }, 400);
3115
3867
  const starredSessionIds = await stars.setStarred(ids, starred);
3116
3868
  json(res, { ok: true, starredSessionIds });
3117
3869
  } catch (e) {
3118
- json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
3870
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3871
+ }
3872
+ }
3873
+ }));
3874
+ disposers.push(ctx.webServer.register({
3875
+ kind: "exact",
3876
+ path: "/archived-sessions/tags/list",
3877
+ handler: async (req, res) => {
3878
+ try {
3879
+ const { tags: tagList, assignments } = await tags.list();
3880
+ json(res, { ok: true, tags: tagList, assignments });
3881
+ } catch (e) {
3882
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3883
+ }
3884
+ }
3885
+ }));
3886
+ disposers.push(ctx.webServer.register({
3887
+ kind: "exact",
3888
+ path: "/archived-sessions/tags/create",
3889
+ handler: async (req, res) => {
3890
+ try {
3891
+ const body = await readJsonBody(req);
3892
+ const tag = await tags.create(body && body.name);
3893
+ json(res, { ok: true, tag });
3894
+ } catch (e) {
3895
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3896
+ }
3897
+ }
3898
+ }));
3899
+ disposers.push(ctx.webServer.register({
3900
+ kind: "exact",
3901
+ path: "/archived-sessions/tags/rename",
3902
+ handler: async (req, res) => {
3903
+ try {
3904
+ const body = await readJsonBody(req);
3905
+ await tags.rename(body && body.id, body && body.name);
3906
+ json(res, { ok: true });
3907
+ } catch (e) {
3908
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3909
+ }
3910
+ }
3911
+ }));
3912
+ disposers.push(ctx.webServer.register({
3913
+ kind: "exact",
3914
+ path: "/archived-sessions/tags/merge",
3915
+ handler: async (req, res) => {
3916
+ try {
3917
+ const body = await readJsonBody(req);
3918
+ await tags.merge(body && body.fromId, body && body.toId);
3919
+ json(res, { ok: true });
3920
+ } catch (e) {
3921
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3922
+ }
3923
+ }
3924
+ }));
3925
+ disposers.push(ctx.webServer.register({
3926
+ kind: "exact",
3927
+ path: "/archived-sessions/tags/delete",
3928
+ handler: async (req, res) => {
3929
+ try {
3930
+ const body = await readJsonBody(req);
3931
+ await tags.removeTag(body && body.id);
3932
+ json(res, { ok: true });
3933
+ } catch (e) {
3934
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3935
+ }
3936
+ }
3937
+ }));
3938
+ disposers.push(ctx.webServer.register({
3939
+ kind: "exact",
3940
+ path: "/archived-sessions/tags/set",
3941
+ handler: async (req, res) => {
3942
+ try {
3943
+ const body = await readJsonBody(req);
3944
+ const sid = body && body.sessionId;
3945
+ if (!isSafeSessionId3(sid)) {
3946
+ return json(res, { ok: false, code: "DSM_TAG_SESSION_INVALID", error: "missing/invalid sessionId" }, 400);
3947
+ }
3948
+ const assignments = await tags.setTags(sid, body && body.tagIds);
3949
+ json(res, { ok: true, assignments });
3950
+ } catch (e) {
3951
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3952
+ }
3953
+ }
3954
+ }));
3955
+ disposers.push(ctx.webServer.register({
3956
+ kind: "exact",
3957
+ path: "/archived-sessions/filters/list",
3958
+ handler: async (req, res) => {
3959
+ try {
3960
+ json(res, { ok: true, items: await savedFilters.list() });
3961
+ } catch (e) {
3962
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3963
+ }
3964
+ }
3965
+ }));
3966
+ disposers.push(ctx.webServer.register({
3967
+ kind: "exact",
3968
+ path: "/archived-sessions/filters/save",
3969
+ handler: async (req, res) => {
3970
+ try {
3971
+ const body = await readJsonBody(req);
3972
+ const item = await savedFilters.save(body && body.name, body && body.filters);
3973
+ json(res, { ok: true, item });
3974
+ } catch (e) {
3975
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3976
+ }
3977
+ }
3978
+ }));
3979
+ disposers.push(ctx.webServer.register({
3980
+ kind: "exact",
3981
+ path: "/archived-sessions/filters/delete",
3982
+ handler: async (req, res) => {
3983
+ try {
3984
+ const body = await readJsonBody(req);
3985
+ const ids = body && Array.isArray(body.ids) ? body.ids : null;
3986
+ if (!ids || ids.length === 0) return json(res, { ok: false, code: "DSM_FILTER_IDS_INVALID", error: "missing ids" }, 400);
3987
+ const removed = await savedFilters.remove(ids);
3988
+ json(res, { ok: true, removed });
3989
+ } catch (e) {
3990
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3119
3991
  }
3120
3992
  }
3121
3993
  }));
@@ -3174,7 +4046,7 @@ function apply(ctx) {
3174
4046
  try {
3175
4047
  const body = await readJsonBody(req);
3176
4048
  const rootId = body && typeof body.sessionId === "string" ? body.sessionId : "";
3177
- if (!rootId || !isSafeSessionId2(rootId)) return json(res, { error: "missing sessionId" }, 400);
4049
+ if (!rootId || !isSafeSessionId3(rootId)) return json(res, { error: "missing sessionId" }, 400);
3178
4050
  const headerById = /* @__PURE__ */ new Map();
3179
4051
  const sizeById = /* @__PURE__ */ new Map();
3180
4052
  const trashedIds = /* @__PURE__ */ new Set();
@@ -3198,9 +4070,18 @@ function apply(ctx) {
3198
4070
  });
3199
4071
  if (Number.isFinite(sizeBytes)) sizeById.set(id, sizeBytes);
3200
4072
  };
4073
+ const lineageStats = /* @__PURE__ */ new Map();
4074
+ const lineageHeaders = /* @__PURE__ */ new Map();
3201
4075
  try {
3202
4076
  for (const entry of await persistence.listEntries()) {
3203
4077
  addHeader(entry.header, entry.sizeBytes);
4078
+ const eid = entry && entry.id != null ? String(entry.id) : null;
4079
+ if (eid) {
4080
+ lineageHeaders.set(eid, { header: entry && entry.header || null });
4081
+ if (entry && typeof entry.revision === "string" && entry.revision) {
4082
+ lineageStats.set(eid, Number.isFinite(entry.sizeBytes) ? { revision: entry.revision, sizeBytes: Number(entry.sizeBytes) } : { revision: entry.revision });
4083
+ }
4084
+ }
3204
4085
  if (entry && entry.id != null) persistedIds.add(String(entry.id));
3205
4086
  }
3206
4087
  } catch (e) {
@@ -3252,11 +4133,17 @@ function apply(ctx) {
3252
4133
  n.children.forEach(walk);
3253
4134
  };
3254
4135
  nodes.forEach(walk);
3255
- const titles = await projectTitles(allIds);
4136
+ const lineageIds = allIds.map(String);
4137
+ const { cached: lineageCached, missing: lineageMissing } = metaCache.partition(lineageIds, lineageStats);
4138
+ const lineagePersisted = await hydrateFromPersist(lineageMissing, lineageStats);
4139
+ for (const [id, meta] of lineagePersisted) metaCache.set(id, lineageStats.get(id), meta);
4140
+ const lineageRest = lineageMissing.filter((id) => !lineagePersisted.has(id));
4141
+ if (lineageRest.length) enqueueWarm(lineageRest, lineageStats, lineageHeaders);
4142
+ const truncateTitle = (t) => t ? t.length > MAX_TITLE ? t.slice(0, MAX_TITLE) + "\u2026" : t : null;
3256
4143
  const titleOf = (id) => {
3257
- const snap = titles.get(id);
3258
- const m = snap ? metaFromSnapshot(snap) : null;
3259
- return m && m.title || null;
4144
+ const key = String(id);
4145
+ const m = lineageCached.get(key) || lineagePersisted.get(key);
4146
+ return truncateTitle(m && m.title || authorityTitleCache.get(key) || null);
3260
4147
  };
3261
4148
  const fill = (n) => {
3262
4149
  n.title = titleOf(n.sessionId);
@@ -3371,6 +4258,21 @@ function apply(ctx) {
3371
4258
  }
3372
4259
  }
3373
4260
  }));
4261
+ disposers.push(ctx.webServer.register({
4262
+ kind: "exact",
4263
+ path: "/archived-sessions/pending-moves/notices/ack",
4264
+ handler: async (req, res) => {
4265
+ try {
4266
+ const body = await readJsonBody(req);
4267
+ const ids = body && Array.isArray(body.ids) ? body.ids : null;
4268
+ if (!ids || !ids.length) return json(res, { ok: false, error: "missing ids" }, 400);
4269
+ const removed = await moveNotices.ack(ids);
4270
+ json(res, { ok: true, removed });
4271
+ } catch (e) {
4272
+ json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
4273
+ }
4274
+ }
4275
+ }));
3374
4276
  disposers.push(ctx.webServer.register({
3375
4277
  kind: "exact",
3376
4278
  path: "/archived-sessions/archive",