dsh-sessions-manager 3.6.2 → 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,8 +1142,8 @@ 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";
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";
705
1147
  var TITLE_INDEX_SCHEMA_VERSION = 2;
706
1148
  var MAX_ENTRIES = 2e4;
707
1149
  function normalizeEntry(raw) {
@@ -743,7 +1185,7 @@ function mergeEntries(left, right) {
743
1185
  function createTitleIndexStore({ dir, file }) {
744
1186
  let cache = null;
745
1187
  let chain = Promise.resolve();
746
- const path = file || join5(dir, "title-index.json");
1188
+ const path = file || join8(dir, "title-index.json");
747
1189
  async function readRaw() {
748
1190
  try {
749
1191
  return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
@@ -778,10 +1220,10 @@ function createTitleIndexStore({ dir, file }) {
778
1220
  if (!Object.keys(right).length) return false;
779
1221
  await enqueue(async (store) => {
780
1222
  const next = mergeEntries(store, right);
781
- await mkdir4(dirname(path), { recursive: true });
782
- const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
783
- await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
784
- 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);
785
1227
  cache = next;
786
1228
  });
787
1229
  return true;
@@ -798,10 +1240,110 @@ function createTitleIndexStore({ dir, file }) {
798
1240
  }
799
1241
  }
800
1242
  if (!changed) return;
801
- await mkdir4(dirname(path), { recursive: true });
802
- const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
803
- await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
804
- 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);
805
1347
  });
806
1348
  return true;
807
1349
  }
@@ -810,7 +1352,7 @@ function createTitleIndexStore({ dir, file }) {
810
1352
 
811
1353
  // src/handle-era-paths.js
812
1354
  import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
813
- import { basename, join as join6 } from "node:path";
1355
+ import { basename, join as join10 } from "node:path";
814
1356
 
815
1357
  // src/path-guard.js
816
1358
  function splitSegments(target) {
@@ -883,8 +1425,8 @@ function resolveSessionRoot(sp) {
883
1425
  return typeof root === "string" && root.length > 0 ? root : null;
884
1426
  }
885
1427
  function deriveSessionDir(root, cwd, id) {
886
- const project = cwd === void 0 || cwd === null || cwd === "" ? join6(root, "_no-cwd") : join6(root, projectKeyFor(cwd));
887
- 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));
888
1430
  }
889
1431
  function generationVersionOf(name2) {
890
1432
  return Number((String(name2).match(/^session\.v(\d+)\./) || [])[1] || 0);
@@ -911,11 +1453,11 @@ async function locateSessionArtifacts(sp, header) {
911
1453
  const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
912
1454
  if (generationFiles.length === 0) return null;
913
1455
  generationFiles.sort((a, b) => generationVersionOf(b) - generationVersionOf(a));
914
- const logPath = join6(sessionDir, generationFiles[0]);
1456
+ const logPath = join10(sessionDir, generationFiles[0]);
915
1457
  if (!pathOwnsSession(logPath, sid)) return null;
916
1458
  return {
917
1459
  root,
918
- 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)),
919
1461
  sessionDir,
920
1462
  logPath,
921
1463
  generationFiles,
@@ -1125,8 +1667,8 @@ function requireCapability(capabilities, name2) {
1125
1667
  }
1126
1668
 
1127
1669
  // src/handle-era-ops.js
1128
- 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";
1129
- 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";
1130
1672
  var MOVE_BATCH = 400;
1131
1673
  var REAL_LOG_RE = /^session.*\.jsonl(\.zst(d)?)?$/;
1132
1674
  var MOVE_TEMP_RE = /\.move-(backup|stage)-/;
@@ -1149,7 +1691,7 @@ async function cleanSiblingCopies(root, sid, keepDir, keepVersion) {
1149
1691
  const real = [];
1150
1692
  for (const ent of projects) {
1151
1693
  if (!ent.isDirectory()) continue;
1152
- const candidate = join7(root, ent.name, segment);
1694
+ const candidate = join11(root, ent.name, segment);
1153
1695
  if (keepDir && candidate === keepDir) continue;
1154
1696
  let entries;
1155
1697
  try {
@@ -1264,7 +1806,7 @@ async function purgeSessionArtifacts(sp, sid, header) {
1264
1806
  return artifacts;
1265
1807
  }
1266
1808
  async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts }) {
1267
- const original = await readFile2(backupLogPath);
1809
+ const original = await readFile3(backupLogPath);
1268
1810
  const frames = scanZstdFrames(original).frames;
1269
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");
1270
1812
  const rewritten = rewriteFrame0CwdInMemory(original, canonical);
@@ -1274,10 +1816,10 @@ async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifact
1274
1816
  throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
1275
1817
  }
1276
1818
  const targetDir = deriveSessionDir(artifacts.root, canonical, sid);
1277
- await mkdir5(targetDir, { recursive: true });
1278
- const staged = join7(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
1279
- await writeFile5(staged, rewritten, { mode: 384 });
1280
- 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]));
1281
1823
  return targetDir;
1282
1824
  }
1283
1825
  async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
@@ -1329,11 +1871,11 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1329
1871
  const firstSeq = events.length ? Number(events[0].seq) : 0;
1330
1872
  const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
1331
1873
  const createOptions = header.isSeeded && Number.isSafeInteger(inheritedEventCount) && inheritedEventCount > 0 ? { inheritedEventCount } : void 0;
1332
- const backupRoot = join7(dirname2(artifacts.root), "sessions-manager-move-backup");
1333
- const backupDir = join7(backupRoot, `${encodeSegmentFor(sid)}-${process.pid}-${Date.now()}`);
1334
- await mkdir5(backupRoot, { recursive: true });
1335
- const backupLogPath = join7(backupDir, basename2(artifacts.logPath));
1336
- 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);
1337
1879
  let writer = null;
1338
1880
  try {
1339
1881
  try {
@@ -1366,11 +1908,11 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1366
1908
  } catch (_) {
1367
1909
  }
1368
1910
  try {
1369
- await mkdir5(dirname2(artifacts.sessionDir), { recursive: true });
1911
+ await mkdir9(dirname3(artifacts.sessionDir), { recursive: true });
1370
1912
  } catch (_) {
1371
1913
  }
1372
1914
  try {
1373
- await rename5(backupDir, artifacts.sessionDir);
1915
+ await rename8(backupDir, artifacts.sessionDir);
1374
1916
  } catch (_) {
1375
1917
  }
1376
1918
  if (e && e.status) throw e;
@@ -1387,12 +1929,6 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1387
1929
  }
1388
1930
 
1389
1931
  // src/lineage.js
1390
- var EMPTY_BASE = 190;
1391
- function isEmptyLogSize(sizeBytes, cwd) {
1392
- if (!Number.isFinite(sizeBytes) || sizeBytes < 0) return false;
1393
- const cwdLen = typeof cwd === "string" ? cwd.length : 0;
1394
- return sizeBytes <= EMPTY_BASE + cwdLen;
1395
- }
1396
1932
  var EMPTY_DECODE_LIMIT = 8192;
1397
1933
  var LIFECYCLE_EVENT_TYPES = /* @__PURE__ */ new Set([
1398
1934
  "session",
@@ -1407,24 +1943,26 @@ function isEmptyEventTypes(types) {
1407
1943
  }
1408
1944
  return true;
1409
1945
  }
1410
- function classifyLineage(header, sizeBytes) {
1946
+ function classifyLineage(header) {
1411
1947
  if (!header || typeof header !== "object") return null;
1412
1948
  const origin = header.origin === "subagent" ? "subagent" : null;
1413
1949
  const parentSession = typeof header.parentSession === "string" && header.parentSession ? header.parentSession : null;
1414
1950
  const delegationDepth = Number.isSafeInteger(header.delegationDepth) && header.delegationDepth > 0 ? header.delegationDepth : 0;
1415
- const empty = isEmptyLogSize(sizeBytes, header.cwd);
1416
- if (!origin && !parentSession && !empty) return null;
1417
- 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;
1418
1956
  }
1419
1957
 
1420
1958
  // src/index.js
1421
- var BUILD_STAMP = true ? "3.6.2+e36dc3fc" : "dev";
1959
+ var BUILD_STAMP = true ? "3.7.0+95376789" : "dev";
1422
1960
  var name = "dsh-sessions-manager";
1423
1961
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
1424
1962
  var MAX_TITLE = 80;
1425
- var STATE_DIR = process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join8(homedir4(), ".dsh", "sessions-manager");
1426
- var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join8(homedir4(), ".dsh", "sessions-manager-trash");
1427
- 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");
1428
1966
  var TRASH_SCHEMA_VERSION = 2;
1429
1967
  var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
1430
1968
  var FETCH_TOOL_RE = /search|fetch|download|browse/i;
@@ -1471,14 +2009,14 @@ function parseIds(body) {
1471
2009
  const raw = body && body.sessionIds;
1472
2010
  if (!Array.isArray(raw)) return null;
1473
2011
  const ids = [];
1474
- 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);
1475
2013
  return ids;
1476
2014
  }
1477
- function isSafeSessionId2(value) {
2015
+ function isSafeSessionId3(value) {
1478
2016
  return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
1479
2017
  }
1480
2018
  function requireSessionId(value) {
1481
- if (!isSafeSessionId2(value)) {
2019
+ if (!isSafeSessionId3(value)) {
1482
2020
  const error = new Error("\u65E0\u6548\u7684 sessionId");
1483
2021
  error.status = 400;
1484
2022
  throw error;
@@ -1512,7 +2050,9 @@ function apply(ctx) {
1512
2050
  const dom = () => ctx.storageDomain.get("workspace");
1513
2051
  const authorityTitleCache = /* @__PURE__ */ new Map();
1514
2052
  const metaCache = createSessionMetaCache();
1515
- 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;
1516
2056
  async function hydrateFromPersist(ids, statsById) {
1517
2057
  const hits = /* @__PURE__ */ new Map();
1518
2058
  if (!ids || !ids.length) return hits;
@@ -1824,12 +2364,12 @@ function apply(ctx) {
1824
2364
  schemaVersion: TRASH_SCHEMA_VERSION,
1825
2365
  settings: { retentionDays },
1826
2366
  items: raw && Array.isArray(raw.items) ? raw.items : [],
1827
- 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))] : []
1828
2368
  };
1829
2369
  }
1830
2370
  async function readTrashStore() {
1831
2371
  try {
1832
- return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
2372
+ return normalizeTrashStore(JSON.parse(readFileSync7(TRASH_INDEX, "utf8")));
1833
2373
  } catch (e) {
1834
2374
  return normalizeTrashStore(null);
1835
2375
  }
@@ -1838,10 +2378,10 @@ function apply(ctx) {
1838
2378
  return (await readTrashStore()).items;
1839
2379
  }
1840
2380
  async function writeTrashStore(store) {
1841
- await mkdir6(TRASH_DIR, { recursive: true });
1842
- const tmp = join8(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
1843
- await writeFile6(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
1844
- 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);
1845
2385
  }
1846
2386
  function mutateTrash(mutator) {
1847
2387
  const operation = trashMutation.then(async () => {
@@ -1855,7 +2395,10 @@ function apply(ctx) {
1855
2395
  return operation;
1856
2396
  }
1857
2397
  const stars = createStarIndex();
2398
+ const tags = createTagIndex();
2399
+ const savedFilters = createSavedFilters();
1858
2400
  const pendingMoves = createPendingMoveStore();
2401
+ const moveNotices = createMoveNoticeStore();
1859
2402
  const autoArchive = createAutoArchiveStore();
1860
2403
  sweepStaleStateTemps([STATE_DIR, TRASH_DIR], {}).catch(() => {
1861
2404
  });
@@ -1868,6 +2411,15 @@ function apply(ctx) {
1868
2411
  } catch (e) {
1869
2412
  }
1870
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
+ }
1871
2423
  async function deleteOne(sid) {
1872
2424
  requireSessionId(sid);
1873
2425
  let header = null;
@@ -2120,6 +2672,10 @@ function apply(ctx) {
2120
2672
  await stars.removeIds([sid]);
2121
2673
  } catch (e) {
2122
2674
  }
2675
+ try {
2676
+ await tags.removeIds([sid]);
2677
+ } catch (e) {
2678
+ }
2123
2679
  return { ok: true, purged: true };
2124
2680
  }
2125
2681
  async function trashSettings(next) {
@@ -2155,8 +2711,8 @@ function apply(ctx) {
2155
2711
  async function moveTargetWorkspace(rawPath) {
2156
2712
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
2157
2713
  let p = String(rawPath).trim();
2158
- if (p.startsWith("~/")) p = join8(homedir4(), p.slice(2));
2159
- 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);
2160
2716
  let canonical = null;
2161
2717
  try {
2162
2718
  canonical = await realpath(p);
@@ -2164,7 +2720,7 @@ function apply(ctx) {
2164
2720
  canonical = null;
2165
2721
  }
2166
2722
  if (canonical === null) {
2167
- await mkdir6(p, { recursive: true });
2723
+ await mkdir10(p, { recursive: true });
2168
2724
  canonical = await realpath(p);
2169
2725
  }
2170
2726
  return { canonical, entity: await w.create(canonical, basename3(canonical) || "workspace") };
@@ -2209,15 +2765,15 @@ function apply(ctx) {
2209
2765
  const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
2210
2766
  let destinationInstalled = false;
2211
2767
  try {
2212
- await mkdir6(dirname3(newPath), { recursive: true });
2768
+ await mkdir10(dirname4(newPath), { recursive: true });
2213
2769
  try {
2214
2770
  await stat4(newPath);
2215
2771
  throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
2216
2772
  } catch (e) {
2217
2773
  if (e && e.code !== "ENOENT") throw e;
2218
2774
  }
2219
- await rename6(oldPath, backupPath);
2220
- const original = await readFile3(backupPath);
2775
+ await rename9(oldPath, backupPath);
2776
+ const original = await readFile4(backupPath);
2221
2777
  const originalFrames = scanZstdFrames(original).frames;
2222
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");
2223
2779
  const rewritten = rewriteFrame0CwdInMemory(original, canonical);
@@ -2226,11 +2782,11 @@ function apply(ctx) {
2226
2782
  const originalTail = original.subarray(originalFrames[0].end);
2227
2783
  const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
2228
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");
2229
- await writeFile6(stagedPath, rewritten, { mode: 384 });
2230
- await rename6(stagedPath, newPath);
2785
+ await writeFile10(stagedPath, rewritten, { mode: 384 });
2786
+ await rename9(stagedPath, newPath);
2231
2787
  destinationInstalled = true;
2232
2788
  await unlink(backupPath);
2233
- const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
2789
+ const keptInSource = await cleanupMovedSourceDir(dirname4(oldPath));
2234
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`);
2235
2791
  } catch (e) {
2236
2792
  try {
@@ -2244,7 +2800,7 @@ function apply(ctx) {
2244
2800
  }
2245
2801
  }
2246
2802
  try {
2247
- await rename6(backupPath, oldPath);
2803
+ await rename9(backupPath, oldPath);
2248
2804
  } catch (_) {
2249
2805
  }
2250
2806
  if (e && e.code !== "ENOENT") throw e;
@@ -2324,7 +2880,7 @@ function apply(ctx) {
2324
2880
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
2325
2881
  if (backupPath) {
2326
2882
  try {
2327
- await rename6(oldPath, backupPath);
2883
+ await rename9(oldPath, backupPath);
2328
2884
  } catch (e) {
2329
2885
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
2330
2886
  }
@@ -2332,7 +2888,7 @@ function apply(ctx) {
2332
2888
  const restore = async () => {
2333
2889
  if (backupPath) {
2334
2890
  try {
2335
- await rename6(backupPath, oldPath);
2891
+ await rename9(backupPath, oldPath);
2336
2892
  } catch (_) {
2337
2893
  }
2338
2894
  }
@@ -2351,7 +2907,7 @@ function apply(ctx) {
2351
2907
  }
2352
2908
  }
2353
2909
  if (oldPath) {
2354
- const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
2910
+ const keptInSource = await cleanupMovedSourceDir(dirname4(oldPath));
2355
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`);
2356
2912
  }
2357
2913
  } catch (e) {
@@ -2434,11 +2990,21 @@ function apply(ctx) {
2434
2990
  metaCache.invalidate(item.sessionId);
2435
2991
  moved++;
2436
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
+ }
2437
2997
  } catch (e) {
2438
2998
  if (isBusyError(e)) continue;
2439
2999
  if (Date.now() - bootedAt < BOOT_WARMUP_MS) continue;
2440
3000
  const bumped = await pendingMoves.bumpAttempts(item.sessionId);
2441
- 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
+ }
2442
3008
  }
2443
3009
  }
2444
3010
  if (moved > 0) {
@@ -2657,12 +3223,23 @@ function apply(ctx) {
2657
3223
  return { items: archivedItems, usage };
2658
3224
  }
2659
3225
  let starredSet = /* @__PURE__ */ new Set();
3226
+ let tagIdsBySession = /* @__PURE__ */ new Map();
2660
3227
  try {
2661
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
+ }
2662
3235
  } catch (e) {
2663
3236
  }
2664
- 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
+ }
2665
3241
  if (headersOk) await gcStars(ids);
3242
+ if (headersOk) await gcTags(ids);
2666
3243
  return { items, usage };
2667
3244
  }
2668
3245
  async function allSessionItems(opts = {}) {
@@ -2757,69 +3334,121 @@ function apply(ctx) {
2757
3334
  }
2758
3335
  }
2759
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();
2760
3345
  for (const entry of entries) {
2761
- const info = classifyLineage(entry.header, entry.sizeBytes);
2762
- 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 };
2763
3368
  }
2764
- await refineEmptyLineage(lineage, entries);
2765
- 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 = {
2766
3375
  titles: Object.fromEntries(authorityTitleCache),
2767
3376
  trashedSessionIds: store.items.map((item) => String(item.sessionId)),
2768
3377
  purgedSessionIds: activeTombstones,
2769
3378
  lineage,
2770
3379
  // v3.6.2:还有预热/退避重试在途 → client 缩短轮询节拍,预热一完成就把
2771
3380
  // 补齐的标题送回(含面板自动刷新)。无预热 API 时恒 false,绝不假忙。
2772
- warmPending: warmPendingNow()
3381
+ warmPending: warmPendingNow(),
3382
+ // T1:空白精判后台队列是否还有活。无精判能力(旧 runtime 无 inspect 通道)
3383
+ // 时恒 false——永久 busy 会让 client 疯轮询,这是唯一现实的假忙陷阱。
3384
+ refinePending: refinePendingNow()
2773
3385
  };
3386
+ try {
3387
+ const noticeRows = await moveNotices.list();
3388
+ if (noticeRows.length) payload.moveNotices = noticeRows;
3389
+ } catch (e) {
3390
+ }
3391
+ return payload;
2774
3392
  }
2775
3393
  const emptyScanCache = /* @__PURE__ */ new Map();
2776
- const REFINE_BUDGET = 40;
2777
- function applyEmptyScan(lineage, id, empty) {
2778
- if (empty) {
2779
- const info = lineage[id] || (lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: false });
2780
- info.empty = true;
2781
- } else if (lineage[id]) {
2782
- const info = lineage[id];
2783
- info.empty = false;
2784
- if (!info.origin && !info.parentSession) delete lineage[id];
2785
- }
2786
- }
2787
- async function refineEmptyLineage(lineage, entries) {
2788
- if (!persistence || typeof persistence.inspectSession !== "function") return;
2789
- const limiter = createLimiter(4);
2790
- const todo = [];
2791
- for (const entry of entries) {
2792
- const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
2793
- if (size === null || size > EMPTY_DECODE_LIMIT) continue;
2794
- const id = String(entry.id);
2795
- const scanned = emptyScanCache.get(id);
2796
- if (scanned && scanned.sizeBytes === size) {
2797
- applyEmptyScan(lineage, id, scanned.empty);
2798
- continue;
2799
- }
2800
- if (todo.length < REFINE_BUDGET) todo.push({ id, size });
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 });
2801
3417
  }
2802
- await Promise.all(todo.map(({ id, size }) => limiter(async () => {
2803
- let isEmpty = false;
2804
- try {
2805
- const types = [];
2806
- await persistence.inspectSession(id, { onEvents: (batch) => {
2807
- for (const ev of batch || []) if (types.length < 64) types.push(ev && ev.type);
2808
- } });
2809
- isEmpty = isEmptyEventTypes(types);
2810
- } catch (e) {
2811
- isEmpty = false;
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
+ }
3440
+ }
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));
2812
3448
  }
2813
- emptyScanCache.set(id, { sizeBytes: size, empty: isEmpty });
2814
- applyEmptyScan(lineage, id, isEmpty);
2815
- })));
2816
- const live = /* @__PURE__ */ new Set();
2817
- for (const entry of entries) {
2818
- const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
2819
- if (size !== null && size <= EMPTY_DECODE_LIMIT) live.add(String(entry.id));
2820
- }
2821
- for (const key of emptyScanCache.keys()) {
2822
- if (!live.has(key)) emptyScanCache.delete(key);
3449
+ } finally {
3450
+ refineRunning = false;
3451
+ if (refineQueue.size) scheduleRefine();
2823
3452
  }
2824
3453
  }
2825
3454
  async function buildDetails(sid, signal) {
@@ -3175,6 +3804,10 @@ function apply(ctx) {
3175
3804
  await titleIndex.remove([sid]);
3176
3805
  } catch (e) {
3177
3806
  }
3807
+ try {
3808
+ await emptyIndex.remove([sid]);
3809
+ } catch (e) {
3810
+ }
3178
3811
  json(res, out);
3179
3812
  } catch (e) {
3180
3813
  json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
@@ -3196,6 +3829,8 @@ function apply(ctx) {
3196
3829
  metaCache.invalidate(sid);
3197
3830
  await titleIndex.remove([sid]).catch(() => {
3198
3831
  });
3832
+ await emptyIndex.remove([sid]).catch(() => {
3833
+ });
3199
3834
  } catch (e) {
3200
3835
  results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
3201
3836
  }
@@ -3211,7 +3846,7 @@ function apply(ctx) {
3211
3846
  path: "/archived-sessions/sessions",
3212
3847
  handler: async (req, res) => {
3213
3848
  try {
3214
- json(res, { items: await allSessionItems(), warmPending: warmPendingNow() });
3849
+ json(res, { items: await allSessionItems({ usage: true }), warmPending: warmPendingNow() });
3215
3850
  } catch (e) {
3216
3851
  json(res, { error: String(e && e.message || e) }, 500);
3217
3852
  }
@@ -3226,13 +3861,133 @@ function apply(ctx) {
3226
3861
  const starred = !!(body && body.starred);
3227
3862
  let ids = parseIds(body);
3228
3863
  if ((!ids || ids.length === 0) && body && typeof body.sessionId === "string") {
3229
- ids = isSafeSessionId2(body.sessionId) ? [body.sessionId] : null;
3864
+ ids = isSafeSessionId3(body.sessionId) ? [body.sessionId] : null;
3230
3865
  }
3231
- 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);
3232
3867
  const starredSessionIds = await stars.setStarred(ids, starred);
3233
3868
  json(res, { ok: true, starredSessionIds });
3234
3869
  } catch (e) {
3235
- 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));
3236
3991
  }
3237
3992
  }
3238
3993
  }));
@@ -3291,7 +4046,7 @@ function apply(ctx) {
3291
4046
  try {
3292
4047
  const body = await readJsonBody(req);
3293
4048
  const rootId = body && typeof body.sessionId === "string" ? body.sessionId : "";
3294
- if (!rootId || !isSafeSessionId2(rootId)) return json(res, { error: "missing sessionId" }, 400);
4049
+ if (!rootId || !isSafeSessionId3(rootId)) return json(res, { error: "missing sessionId" }, 400);
3295
4050
  const headerById = /* @__PURE__ */ new Map();
3296
4051
  const sizeById = /* @__PURE__ */ new Map();
3297
4052
  const trashedIds = /* @__PURE__ */ new Set();
@@ -3503,6 +4258,21 @@ function apply(ctx) {
3503
4258
  }
3504
4259
  }
3505
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
+ }));
3506
4276
  disposers.push(ctx.webServer.register({
3507
4277
  kind: "exact",
3508
4278
  path: "/archived-sessions/archive",