dsh-vscode-mode 0.25.1 → 0.27.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.
Files changed (42) hide show
  1. package/README.md +25 -11
  2. package/lib/client.js +1969 -1344
  3. package/lib/client.js.map +2 -2
  4. package/lib/index.js +1443 -716
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/compatSummary.ts +4 -0
  8. package/src/client/contextMenusOfficial.ts +80 -0
  9. package/src/client/forgeBridge.ts +28 -0
  10. package/src/client/index.ts +18 -3
  11. package/src/client/monaco/lsp/lspClient.ts +25 -7
  12. package/src/client/officialSidebar.ts +15 -3
  13. package/src/client/reviewBar.ts +3 -0
  14. package/src/client/reviewBatch.ts +9 -1
  15. package/src/client/reviewKeepAll.ts +72 -0
  16. package/src/client/sidebar/menuItems.ts +6 -0
  17. package/src/client/sidebar/panels/FileExplorer.ts +26 -1
  18. package/src/client/sidebar/types.ts +3 -0
  19. package/src/client/state/diffSummary.ts +34 -7
  20. package/src/client/state/recordsFeed.ts +40 -3
  21. package/src/client/state/reviewFeed.ts +9 -2
  22. package/src/client/tabMenu.ts +5 -0
  23. package/src/client/ui/ClaimRouter.ts +11 -5
  24. package/src/client/ui/EditorPane.ts +9 -1
  25. package/src/client/ui/EditorView.ts +164 -39
  26. package/src/client/ui/OfficialSideTab.ts +13 -5
  27. package/src/client/ui/ReviewAllConfirm.ts +1 -1
  28. package/src/index.ts +1 -1
  29. package/src/pending/capture.ts +96 -47
  30. package/src/pending/persist.ts +94 -5
  31. package/src/pending/store.ts +171 -58
  32. package/src/pendingCompat.ts +72 -9
  33. package/src/pendingRpc.ts +10 -10
  34. package/src/reviewCapture.ts +64 -23
  35. package/src/reviewDb.ts +119 -8
  36. package/src/reviewEvidence.ts +1 -1
  37. package/src/reviewRpc.ts +115 -9
  38. package/src/rpc.ts +14 -10
  39. package/src/shared/review.ts +21 -0
  40. package/src/shared/rpc.ts +3 -1
  41. package/src/store.ts +44 -0
  42. package/src/toolTargets.ts +63 -0
package/lib/index.js CHANGED
@@ -2445,6 +2445,568 @@ function closeArchiveDbs() {
2445
2445
  connections.clear();
2446
2446
  }
2447
2447
  //#endregion
2448
+ //#region src/shared/pathKey.ts
2449
+ /**
2450
+ * 路径比较口径:Host 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
2451
+ * @author ddj 2026年10月30号
2452
+ */
2453
+ /** 远端仓库身份:`scheme://...` 区分大小写,绝不能折叠。 */
2454
+ const REMOTE = /^[a-z]+:\/\//i;
2455
+ /** 盘符绝对路径:大小写不敏感与宿主平台无关,两边都必须折叠。 */
2456
+ const DRIVE = /^[a-z]:\//i;
2457
+ /**
2458
+ * @author ddj 2026年10月30号
2459
+ * @description: 统一分隔符为 `/`,其余字符原样保留。
2460
+ * @param path 原始路径
2461
+ * @returns 反斜杠已归一化的路径
2462
+ */
2463
+ function unifiedPath(path) {
2464
+ return path.replace(/\\/g, "/");
2465
+ }
2466
+ /**
2467
+ * @author ddj 2026年10月30号
2468
+ * @description: 生成路径比较键。盘符路径恒折叠大小写;其余路径是否折叠由调用方按运行平台决定
2469
+ * (Host 在 Windows 上折叠,客户端不在宿主进程里,取 false)。
2470
+ * @param path 原始路径
2471
+ * @param foldCase 非盘符路径是否折叠大小写
2472
+ * @returns 比较键
2473
+ */
2474
+ function pathKeyFor(path, foldCase) {
2475
+ const unified = unifiedPath(path);
2476
+ if (REMOTE.test(unified)) return unified;
2477
+ if (DRIVE.test(unified)) return unified.toLowerCase();
2478
+ return foldCase ? unified.toLowerCase() : unified;
2479
+ }
2480
+ //#endregion
2481
+ //#region src/shared/pendingReview.ts
2482
+ /** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
2483
+ const PENDING_PREFIX = "pending:";
2484
+ //#endregion
2485
+ //#region src/pendingCompat.ts
2486
+ /**
2487
+ * 进程内"已被待审接管"的路径集合;旧写入口守卫只读它,绝不碰数据库。
2488
+ * 按工作区登记,来源会话仅作追溯;任一对话重建或注销都会更新同一份集合。
2489
+ */
2490
+ const coveredWorkspace = /* @__PURE__ */ new Map();
2491
+ /**
2492
+ * @author ddj 2026年10月29号
2493
+ * @description: 由条目 id 派生稳定序号,保证轮询期间面板锚点不跳。
2494
+ * @param id 待审条目身份
2495
+ * @returns 正整数序号
2496
+ */
2497
+ function stableSeq(id) {
2498
+ let hash = 2166136261;
2499
+ for (let index = 0; index < id.length; index++) {
2500
+ hash ^= id.charCodeAt(index);
2501
+ hash = Math.imul(hash, 16777619);
2502
+ }
2503
+ return Math.abs(hash) % 2147483647 + 1;
2504
+ }
2505
+ /**
2506
+ * @author ddj 2026年10月29号
2507
+ * @description: Host 侧的路径比较口径:远端身份保留大小写,其余按运行平台折叠。
2508
+ * @param path 路径
2509
+ * @returns 比较键
2510
+ */
2511
+ function pathKey$1(path) {
2512
+ return pathKeyFor(path, process.platform === "win32");
2513
+ }
2514
+ /**
2515
+ * @author ddj 2026年10月30号
2516
+ * @description: 判断两个路径是否指同一文件:比较键相同,或一方是另一方的工作区相对写法。
2517
+ * 面板传下来的路径可能是相对路径,而条目里存的是绝对路径。
2518
+ * @param left 路径
2519
+ * @param right 路径
2520
+ * @returns 是否同一文件
2521
+ */
2522
+ function pendingSamePath(left, right) {
2523
+ const a = pathKey$1(left);
2524
+ const b = pathKey$1(right);
2525
+ if (!a || !b) return false;
2526
+ return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
2527
+ }
2528
+ /**
2529
+ * @author ddj 2026年10月30号
2530
+ * @description: 列表里的条目 id 带待审前缀用于区分通道,内核按原始身份存取,这里统一还原。
2531
+ * @param id 请求里的条目 id
2532
+ * @returns 待审内核使用的原始 id
2533
+ */
2534
+ function pendingRawId(id) {
2535
+ return id.startsWith("pending:") ? id.slice(8) : id;
2536
+ }
2537
+ /**
2538
+ * @author ddj 2026年10月30号
2539
+ * @description: 列表显示用的短路径:工作区内显示相对路径,区外保留原文。
2540
+ * @param cwd 工作区
2541
+ * @param path 条目路径
2542
+ * @returns 显示用路径
2543
+ */
2544
+ function pendingDisplay(cwd, path) {
2545
+ const root = cwd.replace(/[\\/]+$/, "");
2546
+ const unified = path.replace(/\\/g, "/");
2547
+ const base = root.replace(/\\/g, "/");
2548
+ if (base && (unified === base || unified.toLowerCase().startsWith(base.toLowerCase() + "/"))) return unified.slice(base.length + 1);
2549
+ return path;
2550
+ }
2551
+ /**
2552
+ * @author ddj 2026年10月29号
2553
+ * @description: 待审条目转面板列表项。
2554
+ * @param item 待审条目
2555
+ * @param cwd 工作区,用于显示相对路径;缺省显示原始路径
2556
+ * @returns 面板列表项
2557
+ */
2558
+ function pendingItemOf(item, cwd) {
2559
+ return {
2560
+ id: PENDING_PREFIX + item.id,
2561
+ sessionId: item.sessionId,
2562
+ turn: 0,
2563
+ seq: stableSeq(item.id),
2564
+ path: item.path,
2565
+ currentPath: item.path,
2566
+ display: cwd ? pendingDisplay(cwd, item.path) : item.path,
2567
+ added: item.added,
2568
+ deleted: item.deleted,
2569
+ kind: item.oversized ? "oversized" : "text",
2570
+ decision: item.settled ? "accepted" : "pending",
2571
+ revision: item.revision,
2572
+ current: true
2573
+ };
2574
+ }
2575
+ /**
2576
+ * @author ddj 2026年10月29号
2577
+ * @description: 待审详情转面板详情;Revert 映射到面板的 Undo 能力位。
2578
+ * @param detail 待审详情
2579
+ * @param capturedAt 采集时间文案
2580
+ * @returns 面板详情
2581
+ */
2582
+ function pendingDetailOf(detail, capturedAt, cwd) {
2583
+ return {
2584
+ item: pendingItemOf(detail.item, cwd),
2585
+ diff: detail.diff,
2586
+ capturedAt,
2587
+ blocks: detail.blocks,
2588
+ capability: {
2589
+ keep: detail.capability.keep,
2590
+ undo: detail.capability.revert,
2591
+ reason: detail.capability.reason
2592
+ }
2593
+ };
2594
+ }
2595
+ /**
2596
+ * @author ddj 2026年11月3号
2597
+ * @description: 覆盖登记键:工作区与路径各自归一化后再拼接,避免同一文件多种写法登记成两条。
2598
+ * @param cwd 工作区
2599
+ * @param path 路径
2600
+ * @returns 覆盖登记键
2601
+ */
2602
+ function coverKey(cwd, path) {
2603
+ return `${pathKey$1(cwd)}|${pathKey$1(path)}`;
2604
+ }
2605
+ /**
2606
+ * @private
2607
+ * @author ddj 2026年11月04号
2608
+ * @description: 取工作区唯一接管集合;来源对话仅保留为诊断信息。
2609
+ * @param cwd 工作区路径
2610
+ * @returns 路径到登记来源的映射
2611
+ */
2612
+ function workspaceCovers(cwd) {
2613
+ const workspaceKey = pathKey$1(cwd);
2614
+ const existing = coveredWorkspace.get(workspaceKey);
2615
+ if (existing) return existing;
2616
+ const created = /* @__PURE__ */ new Map();
2617
+ coveredWorkspace.set(workspaceKey, created);
2618
+ return created;
2619
+ }
2620
+ /**
2621
+ * @author ddj 2026年11月3号
2622
+ * @description: 记住/忘记"该路径由待审投影接管"。进程内集合,绝不在旧写入口上查库(避免撞上写事务锁)。
2623
+ * @param cwd 工作区
2624
+ * @param path 路径
2625
+ * @param covered 是否接管
2626
+ * @param sessionId 会话 id;缺省落到无归属的工作区集合
2627
+ * @returns 无
2628
+ */
2629
+ function trackPendingCover(cwd, path, covered, sessionId) {
2630
+ const key = coverKey(cwd, path);
2631
+ const target = workspaceCovers(cwd);
2632
+ if (covered) target.set(key, sessionId ?? null);
2633
+ else target.delete(key);
2634
+ }
2635
+ /**
2636
+ * @author ddj 2026年11月3号
2637
+ * @description: 用完整列表重建工作区接管集合,清除任何对话留下的过期登记。
2638
+ * @param cwd 工作区
2639
+ * @param sessionId 会话 id
2640
+ * @param paths 本会话当前可见的待审条目路径
2641
+ * @returns 无
2642
+ */
2643
+ function setPendingCovers(cwd, sessionId, paths) {
2644
+ const next = /* @__PURE__ */ new Map();
2645
+ for (const path of paths) if (path) next.set(coverKey(cwd, path), sessionId);
2646
+ coveredWorkspace.set(pathKey$1(cwd), next);
2647
+ }
2648
+ /**
2649
+ * @author ddj 2026年10月29号
2650
+ * @description: 判断某路径是否已由待审投影接管;仅读进程内集合,不产生任何 IO。
2651
+ * 所有对话按同一工作区集合判定,兼容参数不影响可见范围。
2652
+ * @param cwd 工作区
2653
+ * @param path 请求路径
2654
+ * @param sessionId 会话 id;缺省按并集判定
2655
+ * @returns 是否已被接管
2656
+ */
2657
+ function pendingCovers(cwd, path, sessionId) {
2658
+ const key = coverKey(cwd, path);
2659
+ return coveredWorkspace.get(pathKey$1(cwd))?.has(key) === true;
2660
+ }
2661
+ /**
2662
+ * @author ddj 2026年10月29号
2663
+ * @description: 合并官方页与待审页:待审覆盖的路径从官方结果中剔除,其余原样保留。
2664
+ * @param official 官方页
2665
+ * @param pending 待审页
2666
+ * @param cwd 工作区,用于把待审行显示成相对路径
2667
+ * @returns 合并后的页
2668
+ */
2669
+ function mergedPage(official, pending, cwd) {
2670
+ const covered = /* @__PURE__ */ new Set();
2671
+ for (const item of pending.items) {
2672
+ covered.add(pathKey$1(item.path));
2673
+ covered.add(pathKey$1(item.id));
2674
+ }
2675
+ const kept = official.items.filter((item) => !covered.has(pathKey$1(item.currentPath)) && !covered.has(pathKey$1(item.path)));
2676
+ return {
2677
+ items: [...pending.items.map((item) => pendingItemOf(item, cwd)), ...kept],
2678
+ pending: pending.pending + kept.filter((item) => item.decision === "pending").length,
2679
+ nextCursor: official.nextCursor,
2680
+ gaps: official.gaps,
2681
+ ...official.overview ? { overview: official.overview } : {}
2682
+ };
2683
+ }
2684
+ //#endregion
2685
+ //#region src/pending/persist.ts
2686
+ /** 待审库版本号,写进 metadata 便于日后迁移。v3 给评论表加了引文与上下文列。 */
2687
+ const SCHEMA_VERSION = "3";
2688
+ /**
2689
+ * @author ddj 2026年10月29号
2690
+ * @description: 打开工作区库并确保待审表存在;DDL 与版本标记在同一事务里提交。
2691
+ * @param cwd 已迁移工作区
2692
+ * @returns 可读写的连接
2693
+ * @throws 未完成活跃库迁移时拒绝
2694
+ */
2695
+ function pendingDb(cwd) {
2696
+ if (!activeReady(cwd)) throw new Error("待审投影要求先完成活跃库迁移");
2697
+ const db = dbFor(cwd);
2698
+ if (db.prepare("SELECT value FROM metadata WHERE name='pending_schema'").get()?.value === SCHEMA_VERSION) return db;
2699
+ db.exec("BEGIN IMMEDIATE");
2700
+ try {
2701
+ db.exec(DDL);
2702
+ migrateHistory(db);
2703
+ db.prepare("INSERT INTO metadata(name,value) VALUES('pending_schema',?) ON CONFLICT(name) DO UPDATE SET value=excluded.value").run(SCHEMA_VERSION);
2704
+ db.exec("COMMIT");
2705
+ } catch (error) {
2706
+ if (db.isTransaction) db.exec("ROLLBACK");
2707
+ throw error;
2708
+ }
2709
+ return db;
2710
+ }
2711
+ /** 待审表结构:条目、块决定、幂等操作、撤销栈。 */
2712
+ const DDL = `
2713
+ CREATE TABLE IF NOT EXISTS pending_entries (
2714
+ id TEXT PRIMARY KEY, path TEXT NOT NULL, earlier TEXT NOT NULL,
2715
+ old_text TEXT NOT NULL, new_text TEXT NOT NULL, updated_at INTEGER NOT NULL,
2716
+ unseen INTEGER NOT NULL DEFAULT 0, missing INTEGER NOT NULL DEFAULT 0,
2717
+ diverged INTEGER NOT NULL DEFAULT 0, oversized INTEGER NOT NULL DEFAULT 0,
2718
+ revision INTEGER NOT NULL DEFAULT 0, session_id TEXT NOT NULL,
2719
+ session_ids TEXT NOT NULL, lineage TEXT);
2720
+ CREATE TABLE IF NOT EXISTS pending_blocks (
2721
+ entry_id TEXT NOT NULL, block_id TEXT NOT NULL, decision TEXT NOT NULL,
2722
+ PRIMARY KEY(entry_id,block_id));
2723
+ CREATE TABLE IF NOT EXISTS pending_ops (
2724
+ operation_id TEXT PRIMARY KEY, request TEXT NOT NULL, state TEXT NOT NULL,
2725
+ result TEXT, undo TEXT);
2726
+ CREATE TABLE IF NOT EXISTS pending_history (
2727
+ id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, entry_id TEXT,
2728
+ snapshot TEXT NOT NULL, created_at INTEGER NOT NULL,
2729
+ state TEXT NOT NULL DEFAULT 'applied');
2730
+ `;
2731
+ /**
2732
+ * @author ddj 2026年10月29号
2733
+ * @description: v1 历史表补 state 列(rebuild 成本高于一次 ALTER)。
2734
+ * @param db 已打开的工作区库
2735
+ * @returns 无
2736
+ */
2737
+ function migrateHistory(db) {
2738
+ if (db.prepare("PRAGMA table_info(pending_history)").all().some((column) => column.name === "state")) return;
2739
+ db.exec("ALTER TABLE pending_history ADD COLUMN state TEXT NOT NULL DEFAULT 'applied'");
2740
+ }
2741
+ /**
2742
+ * @author ddj 2026年10月29号
2743
+ * @description: 读出全部待审条目,按更新时间升序。
2744
+ * @param cwd 工作区
2745
+ * @returns 条目与标记
2746
+ */
2747
+ function loadEntries(cwd) {
2748
+ return pendingDb(cwd).prepare("SELECT * FROM pending_entries ORDER BY updated_at ASC").all().map((row) => ({
2749
+ entry: entryOfRow(row),
2750
+ flags: flagsOfRow(row)
2751
+ }));
2752
+ }
2753
+ /**
2754
+ * @author ddj 2026年10月29号
2755
+ * @description: 写入或更新一条条目(含标记),单条原子完成。
2756
+ * @param cwd 工作区
2757
+ * @param stored 条目与标记
2758
+ * @returns 无
2759
+ */
2760
+ function saveEntry(cwd, stored) {
2761
+ upsertEntry(pendingDb(cwd), stored);
2762
+ }
2763
+ /**
2764
+ * @private
2765
+ * @author ddj 2026年11月03号
2766
+ * @description: 在给定连接上写入或更新一条条目(含标记),供单条与批量两个入口复用。
2767
+ * @param db 已迁移连接(可能已在事务中)
2768
+ * @param stored 条目与标记
2769
+ * @returns 无
2770
+ */
2771
+ function upsertEntry(db, stored) {
2772
+ const { entry, flags } = stored;
2773
+ db.prepare(`INSERT INTO pending_entries
2774
+ (id,path,earlier,old_text,new_text,updated_at,unseen,missing,diverged,oversized,revision,session_id,session_ids,lineage)
2775
+ VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
2776
+ ON CONFLICT(id) DO UPDATE SET path=excluded.path,earlier=excluded.earlier,old_text=excluded.old_text,
2777
+ new_text=excluded.new_text,updated_at=excluded.updated_at,unseen=excluded.unseen,missing=excluded.missing,
2778
+ diverged=excluded.diverged,oversized=excluded.oversized,revision=excluded.revision,
2779
+ session_id=excluded.session_id,session_ids=excluded.session_ids,lineage=excluded.lineage`).run(entry.id, entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt, entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0, flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds), entry.lineage ? JSON.stringify(entry.lineage) : null);
2780
+ }
2781
+ /**
2782
+ * @author ddj 2026年10月29号
2783
+ * @description: 删除条目及其块决定,用于"已解决条目移除"。
2784
+ * @param cwd 工作区
2785
+ * @param id 条目身份
2786
+ * @returns 无
2787
+ */
2788
+ function dropEntry(cwd, id) {
2789
+ const db = pendingDb(cwd);
2790
+ db.exec("BEGIN IMMEDIATE");
2791
+ try {
2792
+ db.prepare("DELETE FROM pending_entries WHERE id=?").run(id);
2793
+ db.prepare("DELETE FROM pending_blocks WHERE entry_id=?").run(id);
2794
+ db.exec("COMMIT");
2795
+ } catch (error) {
2796
+ if (db.isTransaction) db.exec("ROLLBACK");
2797
+ throw error;
2798
+ }
2799
+ }
2800
+ /**
2801
+ * @author ddj 2026年10月29号
2802
+ * @description: 覆盖某条目的块决定集合。
2803
+ * @param cwd 工作区
2804
+ * @param entryId 条目身份
2805
+ * @param blocks 块决定
2806
+ * @returns 无
2807
+ */
2808
+ function saveBlocks(cwd, entryId, blocks) {
2809
+ const db = pendingDb(cwd);
2810
+ db.exec("BEGIN IMMEDIATE");
2811
+ try {
2812
+ writeBlocks(db, entryId, blocks);
2813
+ db.exec("COMMIT");
2814
+ } catch (error) {
2815
+ if (db.isTransaction) db.exec("ROLLBACK");
2816
+ throw error;
2817
+ }
2818
+ }
2819
+ /**
2820
+ * @private
2821
+ * @author ddj 2026年11月03号
2822
+ * @description: 在给定连接上覆盖某条目的块决定集合,供单条与批量两个入口复用。
2823
+ * @param db 已迁移连接(可能已在事务中)
2824
+ * @param entryId 条目身份
2825
+ * @param blocks 块决定
2826
+ * @returns 无
2827
+ */
2828
+ function writeBlocks(db, entryId, blocks) {
2829
+ db.prepare("DELETE FROM pending_blocks WHERE entry_id=?").run(entryId);
2830
+ const insert = db.prepare("INSERT INTO pending_blocks VALUES(?,?,?)");
2831
+ for (const block of blocks) insert.run(entryId, block.id, block.decision);
2832
+ }
2833
+ /**
2834
+ * @author ddj 2026年10月29号
2835
+ * @description: 读取幂等操作记录;同 operationId 的重复请求必须复用结果。
2836
+ * @param cwd 工作区
2837
+ * @param operationId 操作身份
2838
+ * @returns 请求原文、状态与结果
2839
+ */
2840
+ function readOp(cwd, operationId) {
2841
+ const row = pendingDb(cwd).prepare("SELECT request,state,result FROM pending_ops WHERE operation_id=?").get(operationId);
2842
+ return row ? {
2843
+ request: String(row.request),
2844
+ state: String(row.state),
2845
+ result: row.result === null ? null : String(row.result)
2846
+ } : void 0;
2847
+ }
2848
+ /**
2849
+ * @author ddj 2026年10月29号
2850
+ * @description: 记录一次操作的阶段与撤销快照。
2851
+ * @param cwd 工作区
2852
+ * @param operationId 操作身份
2853
+ * @param request 请求原文
2854
+ * @param state prepared / committed
2855
+ * @param payload 结果与撤销快照
2856
+ * @returns 无
2857
+ */
2858
+ function writeOp(cwd, operationId, request, state, payload = {}) {
2859
+ pendingDb(cwd).prepare(`INSERT INTO pending_ops VALUES(?,?,?,?,?)
2860
+ ON CONFLICT(operation_id) DO UPDATE SET state=excluded.state,
2861
+ result=COALESCE(excluded.result,pending_ops.result), undo=COALESCE(excluded.undo,pending_ops.undo)`).run(operationId, request, state, payload.result ?? null, payload.undo ?? null);
2862
+ }
2863
+ /**
2864
+ * @author ddj 2026年10月29号
2865
+ * @description: 追加一条撤销栈记录,并截断重做分支(新决定之后旧的"已撤销"记录不能再重做)。
2866
+ * @param cwd 工作区
2867
+ * @param kind 操作种类
2868
+ * @param entryId 涉及的条目
2869
+ * @param snapshot 可重放快照
2870
+ * @param now 时间
2871
+ * @returns 无
2872
+ */
2873
+ function pushHistory(cwd, kind, entryId, snapshot, now) {
2874
+ insertHistory(pendingDb(cwd), kind, entryId, snapshot, now);
2875
+ }
2876
+ /**
2877
+ * @private
2878
+ * @author ddj 2026年11月03号
2879
+ * @description: 在给定连接上追加撤销栈记录并截断重做分支,供单条与批量两个入口复用。
2880
+ * @param db 已迁移连接(可能已在事务中)
2881
+ * @param kind 操作种类
2882
+ * @param entryId 涉及的条目
2883
+ * @param snapshot 可重放快照
2884
+ * @param now 时间
2885
+ * @returns 无
2886
+ */
2887
+ function insertHistory(db, kind, entryId, snapshot, now) {
2888
+ db.prepare("DELETE FROM pending_history WHERE state='undone'").run();
2889
+ db.prepare("INSERT INTO pending_history(kind,entry_id,snapshot,created_at,state) VALUES(?,?,?,?,'applied')").run(kind, entryId, snapshot, now);
2890
+ }
2891
+ /**
2892
+ * @author ddj 2026年10月30号
2893
+ * @description: 取最近一条仍生效的历史记录,撤销就作用在它上面。
2894
+ * @param cwd 工作区
2895
+ * @returns 记录或 undefined
2896
+ */
2897
+ function latestApplied(cwd) {
2898
+ const row = pendingDb(cwd).prepare("SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='applied' ORDER BY id DESC LIMIT 1").get();
2899
+ return row ? rowOfHistory(row) : void 0;
2900
+ }
2901
+ /**
2902
+ * @author ddj 2026年10月30号
2903
+ * @description: 取最早一条被撤销的记录,重做按"撤销的逆序"重新生效。
2904
+ * @param cwd 工作区
2905
+ * @returns 记录或 undefined
2906
+ */
2907
+ function earliestUndone(cwd) {
2908
+ const row = pendingDb(cwd).prepare("SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='undone' ORDER BY id ASC LIMIT 1").get();
2909
+ return row ? rowOfHistory(row) : void 0;
2910
+ }
2911
+ /**
2912
+ * @author ddj 2026年10月30号
2913
+ * @description: 翻转一条历史记录的生效状态。
2914
+ * @param cwd 工作区
2915
+ * @param id 记录主键
2916
+ * @param state applied / undone
2917
+ * @returns 无
2918
+ */
2919
+ function setHistoryState(cwd, id, state) {
2920
+ pendingDb(cwd).prepare("UPDATE pending_history SET state=? WHERE id=?").run(state, id);
2921
+ }
2922
+ /**
2923
+ * @author ddj 2026年10月30号
2924
+ * @description: 把数据库行还原成历史记录。
2925
+ * @param row 数据库行
2926
+ * @returns 记录
2927
+ */
2928
+ function rowOfHistory(row) {
2929
+ return {
2930
+ id: Number(row.id),
2931
+ kind: String(row.kind),
2932
+ entryId: String(row.entry_id),
2933
+ snapshot: String(row.snapshot)
2934
+ };
2935
+ }
2936
+ /**
2937
+ * @author ddj 2026年10月29号
2938
+ * @description: 由数据库行还原条目。
2939
+ * @param row 数据库行
2940
+ * @returns 条目
2941
+ */
2942
+ function entryOfRow(row) {
2943
+ const lineage = row.lineage === null ? void 0 : JSON.parse(String(row.lineage));
2944
+ const entry = {
2945
+ id: String(row.id),
2946
+ path: String(row.path),
2947
+ earlierVersion: String(row.earlier) === "none" ? "none" : "file",
2948
+ oldText: String(row.old_text),
2949
+ newText: String(row.new_text),
2950
+ updatedAt: Number(row.updated_at),
2951
+ sessionId: String(row.session_id),
2952
+ sessionIds: JSON.parse(String(row.session_ids))
2953
+ };
2954
+ if (Number(row.unseen) === 1) entry.unseen = true;
2955
+ if (lineage !== void 0) entry.lineage = lineage;
2956
+ return entry;
2957
+ }
2958
+ /**
2959
+ * @author ddj 2026年10月29号
2960
+ * @description: 由数据库行还原标记。
2961
+ * @param row 数据库行
2962
+ * @returns 标记
2963
+ */
2964
+ function flagsOfRow(row) {
2965
+ return {
2966
+ missing: Number(row.missing) === 1,
2967
+ diverged: Number(row.diverged) === 1,
2968
+ oversized: Number(row.oversized) === 1,
2969
+ revision: Number(row.revision)
2970
+ };
2971
+ }
2972
+ /**
2973
+ * @public
2974
+ * @author ddj 2026年11月03号
2975
+ * @description: 在一次事务内批量推进多条待审条目(批量 Keep 用)。每条按 revision 做 CAS,
2976
+ * 只跳过冲突的那一条,不牵连整批;块决定与撤销快照与单条路径同口径。
2977
+ * @param cwd 工作区
2978
+ * @param records 待落库记录
2979
+ * @returns 因版本已变被跳过的条目
2980
+ * @throws 事务级异常(SQL/程序错误)整体回滚后抛出
2981
+ */
2982
+ function saveDecisions(cwd, records) {
2983
+ const db = pendingDb(cwd);
2984
+ const skipped = [];
2985
+ const advance = db.prepare(`UPDATE pending_entries SET path=?,earlier=?,old_text=?,new_text=?,updated_at=?,unseen=?,
2986
+ missing=?,diverged=?,oversized=?,revision=?,session_id=?,session_ids=?,lineage=? WHERE id=? AND revision=?`);
2987
+ db.exec("BEGIN IMMEDIATE");
2988
+ try {
2989
+ for (const record of records) {
2990
+ const { entry, flags } = record.next;
2991
+ const expected = record.stored.flags.revision;
2992
+ if (advance.run(entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt, entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0, flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds), entry.lineage ? JSON.stringify(entry.lineage) : null, record.entryId, expected).changes !== 1) {
2993
+ skipped.push({
2994
+ id: record.entryId,
2995
+ reason: "待审条目已变化,请重试"
2996
+ });
2997
+ continue;
2998
+ }
2999
+ writeBlocks(db, record.entryId, record.blocks);
3000
+ insertHistory(db, "decide", record.entryId, record.snapshot, Date.now());
3001
+ }
3002
+ db.exec("COMMIT");
3003
+ return skipped;
3004
+ } catch (error) {
3005
+ if (db.isTransaction) db.exec("ROLLBACK");
3006
+ throw error;
3007
+ }
3008
+ }
3009
+ //#endregion
2448
3010
  //#region src/store.ts
2449
3011
  const READ_CAP$1 = 8388608;
2450
3012
  /** 二进制预览/写回上限(PDF 等整文件 base64 传输;文本侧仍用 READ_CAP)。 */
@@ -2718,6 +3280,50 @@ async function autoArchiveStale(ctx, session, cwd, bucket) {
2718
3280
  }
2719
3281
  /**
2720
3282
  * @public
3283
+ * @author ddj 2026年11月3号
3284
+ * 待审累计基线接管某路径后,旧 sidecar 记录不再有独立决策价值:标记 superseded 并归档,
3285
+ * 连编辑器的行内 Keep/Undo 一起消失,彻底去掉"旧通道还能点、点了又被守卫拒绝"的双权威残留。
3286
+ * 接管是文件级事实,按工作区全部待审条目判定,不跟随"本会话能否看见"漂移。
3287
+ * @param ctx DSH 上下文
3288
+ * @param session 当前会话
3289
+ * @param cwd 工作区
3290
+ * @param bucket 活跃记录桶
3291
+ * @returns 本次结案记录数
3292
+ * @throws 归档失败时恢复内存标志并透传错误
3293
+ */
3294
+ async function retireCovered(ctx, session, cwd, bucket) {
3295
+ const covers = loadEntries(cwd).flatMap((stored) => [stored.entry.path, stored.entry.id]).filter((path) => !!path);
3296
+ if (!covers.length) return 0;
3297
+ const taken = [];
3298
+ for (const rec of bucket.values()) {
3299
+ if (rec.archived || rec.superseded === true) continue;
3300
+ if (covers.some((cover) => pendingSamePath(cover, rec.path))) taken.push(rec);
3301
+ }
3302
+ if (!taken.length) return 0;
3303
+ const original = taken.map((rec) => ({
3304
+ rec,
3305
+ at: rec.at,
3306
+ superseded: rec.superseded
3307
+ }));
3308
+ const at = (/* @__PURE__ */ new Date()).toISOString();
3309
+ for (const rec of taken) {
3310
+ rec.superseded = true;
3311
+ rec.at = at;
3312
+ }
3313
+ try {
3314
+ await archiveRecords(ctx, session, cwd, bucket, taken, "已由待审累计基线接管,旧工具记录不再单独决策");
3315
+ } catch (error) {
3316
+ for (const item of original) {
3317
+ if (item.rec.archived) continue;
3318
+ item.rec.superseded = item.superseded;
3319
+ item.rec.at = item.at;
3320
+ }
3321
+ throw error;
3322
+ }
3323
+ return taken.length;
3324
+ }
3325
+ /**
3326
+ * @public
2721
3327
  * @author ddj 2026年10月10号
2722
3328
  * 归档整记录精确抵消对;捕获时仅检查新记录,列表维护时检查整个桶。
2723
3329
  * @param ctx DSH 上下文
@@ -7720,17 +8326,41 @@ function reviewDetail(cwd, id) {
7720
8326
  }
7721
8327
  };
7722
8328
  }
8329
+ /**
8330
+ * @private
8331
+ * @author ddj 2026年11月03号
8332
+ * @description: 把一个文件的待处理差异块整体置为目标决策,块级判定仍走 changeBlocks 的 hunk 分支。
8333
+ * @param db 判定事务连接
8334
+ * @param id 文件身份
8335
+ * @param decision 目标决策
8336
+ * @returns 无
8337
+ */
8338
+ function markBlocks(db, id, decision) {
8339
+ db.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND decision='pending'").run(decision, id);
8340
+ }
8341
+ /**
8342
+ * @private
8343
+ * @author ddj 2026年11月03号
8344
+ * @description: 按块决定聚合回写文件决定:仍含 pending 即 pending,多值即 mixed,单一值取该值。
8345
+ * @param db 判定事务连接
8346
+ * @param id 文件身份
8347
+ * @param fallback 没有任何块时的兜底决策
8348
+ * @returns 无
8349
+ */
8350
+ function aggregateDecision(db, id, fallback) {
8351
+ const decisions = db.prepare("SELECT DISTINCT decision FROM review_blocks WHERE file_id=?").all(id).map((row) => String(row.decision));
8352
+ let aggregate = fallback;
8353
+ if (decisions.includes("pending")) aggregate = "pending";
8354
+ else if (decisions.length > 1) aggregate = "mixed";
8355
+ else if (decisions.length === 1) aggregate = decisions[0];
8356
+ db.prepare("UPDATE review_files SET decision=? WHERE id=?").run(aggregate, id);
8357
+ }
7723
8358
  /** @public @author ddj 2026年10月27号 @param db Active decision transaction. @param request File or block action. Updates aggregate without losing mixed decisions. @throws Invalid or already-decided block. */
7724
8359
  function changeBlocks(db, request) {
7725
8360
  if (request.scope === "hunk") {
7726
8361
  if (db.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND block_id=? AND decision='pending'").run(request.decision, request.id, request.blockId ?? "").changes !== 1) throw new Error("差异块不存在或已处理");
7727
- } else db.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND decision='pending'").run(request.decision, request.id);
7728
- const decisions = db.prepare("SELECT DISTINCT decision FROM review_blocks WHERE file_id=?").all(request.id).map((row) => String(row.decision));
7729
- let aggregate = request.decision;
7730
- if (decisions.includes("pending")) aggregate = "pending";
7731
- else if (decisions.length > 1) aggregate = "mixed";
7732
- else if (decisions.length === 1) aggregate = decisions[0];
7733
- db.prepare("UPDATE review_files SET decision=? WHERE id=?").run(aggregate, request.id);
8362
+ } else markBlocks(db, request.id, request.decision);
8363
+ aggregateDecision(db, request.id, request.decision);
7734
8364
  }
7735
8365
  /** @public @author ddj 2026年10月26号 @param cwd Workspace. @param request Frozen identity/revision. @returns Idempotent decision; rejection never guesses a target. */
7736
8366
  function decideReview(cwd, request) {
@@ -7760,6 +8390,87 @@ function decideReview(cwd, request) {
7760
8390
  throw error;
7761
8391
  }
7762
8392
  }
8393
+ /**
8394
+ * @private
8395
+ * @author ddj 2026年11月03号
8396
+ * @description: 取当前仍待审的官方记录摘要,作为批量写入的权威依据(不用客户端快照值)。
8397
+ * @param db 判定事务连接
8398
+ * @returns id → 库内 seq/revision/identity
8399
+ */
8400
+ function pendingRows(db) {
8401
+ const rows = db.prepare("SELECT id,seq,revision,identity FROM review_files WHERE current=1 AND decision='pending'").all();
8402
+ const map = /* @__PURE__ */ new Map();
8403
+ for (const row of rows) map.set(String(row.id), {
8404
+ seq: Number(row.seq),
8405
+ revision: Number(row.revision),
8406
+ identity: String(row.identity)
8407
+ });
8408
+ return map;
8409
+ }
8410
+ /**
8411
+ * @public
8412
+ * @author ddj 2026年11月03号
8413
+ * @description: 一次事务内批量保留官方待审记录。写入条件取库内当前 revision,客户端的 seq/revision 只用于派生操作号,
8414
+ * 因此快照漂移不会让整批中止;已结案、有尚待核对 Undo、操作号冲突的条目只跳过自己。
8415
+ * @param cwd 工作区
8416
+ * @param items 待保留条目(含派生好的 operationId)
8417
+ * @returns 成功保留的 identity 列表与跳过明细
8418
+ * @throws 事务级异常(SQL/程序错误)整体回滚后抛出
8419
+ */
8420
+ function keepOfficialAll(cwd, items) {
8421
+ const db = reviewDb(cwd);
8422
+ const rows = pendingRows(db);
8423
+ const outcome = {
8424
+ paths: [],
8425
+ skipped: []
8426
+ };
8427
+ const keep = db.prepare(`UPDATE review_files SET decision='accepted',revision=revision+1
8428
+ WHERE id=? AND seq=? AND revision=? AND current=1 AND decision='pending'`);
8429
+ const prepared = db.prepare("SELECT 1 FROM review_ops WHERE state='prepared' AND json_extract(request,'$.id')=?");
8430
+ db.exec("BEGIN IMMEDIATE");
8431
+ try {
8432
+ for (const item of items) {
8433
+ const encoded = JSON.stringify({
8434
+ id: item.id,
8435
+ seq: item.seq
8436
+ });
8437
+ const previous = db.prepare("SELECT request,result FROM review_ops WHERE operation_id=?").get(item.operationId);
8438
+ if (previous) {
8439
+ if (previous.request !== encoded || !previous.result) outcome.skipped.push({
8440
+ id: item.id,
8441
+ reason: "该条目已由其他操作处理"
8442
+ });
8443
+ else outcome.paths.push(String(JSON.parse(String(previous.result)).identity));
8444
+ continue;
8445
+ }
8446
+ const row = rows.get(item.id);
8447
+ if (!row || prepared.get(item.id)) {
8448
+ outcome.skipped.push({
8449
+ id: item.id,
8450
+ reason: row ? "该文件有尚待核对的 Undo,暂不能 Keep" : "已结案或不再是待审记录"
8451
+ });
8452
+ continue;
8453
+ }
8454
+ if (keep.run(item.id, row.seq, row.revision).changes !== 1) {
8455
+ outcome.skipped.push({
8456
+ id: item.id,
8457
+ reason: "审查版本冲突,请刷新后重试"
8458
+ });
8459
+ continue;
8460
+ }
8461
+ markBlocks(db, item.id, "accepted");
8462
+ aggregateDecision(db, item.id, "accepted");
8463
+ db.prepare("INSERT INTO review_ops VALUES(?,?,'committed',?,NULL)").run(item.operationId, encoded, JSON.stringify({ identity: row.identity }));
8464
+ outcome.paths.push(row.identity);
8465
+ rows.delete(item.id);
8466
+ }
8467
+ db.exec("COMMIT");
8468
+ return outcome;
8469
+ } catch (error) {
8470
+ if (db.isTransaction) db.exec("ROLLBACK");
8471
+ throw error;
8472
+ }
8473
+ }
7763
8474
  //#endregion
7764
8475
  //#region src/officialCapture.ts
7765
8476
  /** Deferred official-event collection; no private snapshot access. @author ddj 2026年10月26号 */
@@ -8066,6 +8777,67 @@ function evidenceText(evidence) {
8066
8777
  };
8067
8778
  }
8068
8779
  //#endregion
8780
+ //#region src/toolTargets.ts
8781
+ /** Optional Host patch target extraction; results never substitute raw preimages. @author ddj 2026年11月04号 */
8782
+ /** @public @author ddj 2026年11月04号 @param result Untrusted tool envelope. @returns Diagnostic-only status; never writable preimages. */
8783
+ function patchDiagnostics(result) {
8784
+ const payload = result?.value ?? result;
8785
+ if (payload?.phase !== "commit" || !Array.isArray(payload.operations)) return ["apply_patch result unavailable or malformed"];
8786
+ const diagnostics = [];
8787
+ for (const operation of payload.operations) {
8788
+ if (!operation || typeof operation.path !== "string" || ![
8789
+ "add",
8790
+ "update",
8791
+ "delete",
8792
+ "move"
8793
+ ].includes(operation.kind) || ![
8794
+ "completed",
8795
+ "failed",
8796
+ "unknown",
8797
+ "skipped"
8798
+ ].includes(operation.status)) {
8799
+ diagnostics.push("apply_patch operation malformed");
8800
+ continue;
8801
+ }
8802
+ if (operation.status !== "completed") diagnostics.push(operation.path + ": " + operation.status);
8803
+ if (operation.changes !== void 0 && (!Array.isArray(operation.changes) || operation.changes.some((change) => !change || typeof change.path !== "string" || !(change.before === null || typeof change.before === "string") || !(change.after === null || typeof change.after === "string")))) diagnostics.push(operation.path + ": changes malformed");
8804
+ }
8805
+ return diagnostics;
8806
+ }
8807
+ let parserLoad;
8808
+ /** @public @author ddj 2026年11月04号 @returns Optional official parser; missing capabilities remain retryable. */
8809
+ async function patchParser() {
8810
+ if (!parserLoad) parserLoad = loadHostModule("@deepseek-ai/dsh-fs-patch").then((module) => {
8811
+ const parser = module.parsePatch;
8812
+ return typeof parser === "function" ? parser : null;
8813
+ }).catch(() => null);
8814
+ const parser = await parserLoad;
8815
+ if (!parser) parserLoad = void 0;
8816
+ return parser;
8817
+ }
8818
+ /** @public @author ddj 2026年11月04号 @param call Tool identity and arguments. @param parser Optional injectable official parser. @returns Unique unresolved source/destination paths. @throws Unsupported parser or malformed patch. */
8819
+ async function toolTargets(call, parser) {
8820
+ if ([
8821
+ "edit",
8822
+ "write",
8823
+ "str_replace_editor"
8824
+ ].includes(String(call.name))) {
8825
+ const path = call.arguments?.file_path ?? call.arguments?.path;
8826
+ return typeof path === "string" && path ? [path] : [];
8827
+ }
8828
+ if (call.name !== "apply_patch") return [];
8829
+ if (typeof call.arguments?.input !== "string") throw new Error("apply_patch input unavailable");
8830
+ const parse = parser === void 0 ? await patchParser() : parser;
8831
+ if (!parse) throw new Error("Host patch parser unavailable");
8832
+ const paths = /* @__PURE__ */ new Set();
8833
+ for (const operation of parse(call.arguments.input)) {
8834
+ if (typeof operation.path !== "string" || !operation.path) throw new Error("Invalid patch target");
8835
+ paths.add(operation.path);
8836
+ if (operation.moveTo) paths.add(operation.moveTo);
8837
+ }
8838
+ return [...paths];
8839
+ }
8840
+ //#endregion
8069
8841
  //#region src/reviewCapture.ts
8070
8842
  /** Independent byte evidence; official diff remains the only display authority. @author ddj 2026年10月27号 */
8071
8843
  /** @public @author ddj 2026年10月27号 @param ctx Host lifecycle owner. @returns Scoped byte recorder; failures disable undo evidence only, never tool execution. */
@@ -8073,12 +8845,15 @@ function installEvidence(ctx) {
8073
8845
  const chains = /* @__PURE__ */ new Map();
8074
8846
  const staged = /* @__PURE__ */ new Map();
8075
8847
  let stopped = false;
8848
+ const disposed = /* @__PURE__ */ new Set();
8076
8849
  ctx.on("tools/execute", aroundTool);
8077
8850
  ctx.on("session/event", onEvent);
8078
8851
  ctx.on("session/disposed", onDisposed);
8079
8852
  /** @private @author ddj 2026年10月27号 @param session Disposed owner. Drops optional in-memory evidence, never persisted official facts. */
8080
8853
  function onDisposed(session) {
8854
+ disposed.add(session.id);
8081
8855
  chains.delete(session.id);
8856
+ for (const key of staged.keys()) if (JSON.parse(key)[0] === session.id) staged.delete(key);
8082
8857
  }
8083
8858
  /** @private @author ddj 2026年10月27号 @returns Current bounded capture footprint, excluding transient filesystem reads. */
8084
8859
  function footprint() {
@@ -8089,48 +8864,94 @@ function installEvidence(ctx) {
8089
8864
  /** @private @author ddj 2026年10月27号 @param exec Allowed tool dispatch. @param next Original dispatch. @returns Original result unchanged, regardless of capture failure. */
8090
8865
  async function aroundTool(exec, next) {
8091
8866
  const session = exec.agent?.session;
8092
- const path = exec.arguments?.file_path;
8093
- if (stopped || !session?.id || !["edit", "write"].includes(exec.name) || typeof path !== "string") return next();
8094
- let fs;
8095
- let chain;
8096
- let target;
8867
+ if (stopped || !session?.id) return next();
8868
+ const captures = [];
8097
8869
  try {
8098
- if (chains.size >= 16 && !chains.has(session.id) || footprint() >= 33554432) return next();
8099
- fs = ctx.get("fs");
8100
- target = await resolveTarget(ctx, session, path);
8101
- const identity = fs.processPath(target);
8102
- const before = await readRaw(fs, target, exec.signal);
8103
- let files = chains.get(session.id);
8104
- if (!files) {
8105
- files = /* @__PURE__ */ new Map();
8106
- chains.set(session.id, files);
8107
- }
8108
- if (files.size >= 64 && !files.has(identity)) return next();
8109
- chain = files.get(identity);
8110
- if (!chain) {
8111
- chain = {
8112
- before,
8113
- after: before,
8114
- busy: false,
8115
- invalid: false
8116
- };
8117
- files.set(identity, chain);
8118
- }
8119
- if (chain.busy || chain.after.hash !== before.hash) chain.invalid = true;
8120
- chain.busy = true;
8870
+ const cwd = cwdOfSession(ctx, session);
8871
+ const fs = ctx.get("fs");
8872
+ const paths = await toolTargets(exec);
8873
+ for (const path of paths) try {
8874
+ const capture = await prepareRaw(fs, session.id, cwd ?? void 0, path, exec.signal);
8875
+ if (capture) captures.push(capture);
8876
+ } catch {}
8121
8877
  } catch {}
8122
8878
  try {
8123
8879
  return await next();
8124
8880
  } finally {
8125
- if (chain) {
8881
+ for (const capture of captures) {
8126
8882
  try {
8127
- chain.after = await readRaw(fs, target, exec.signal);
8883
+ const after = await readRaw(capture.fs, capture.target, exec.signal);
8884
+ if (!stopped && !disposed.has(session.id)) {
8885
+ capture.chain.after = after;
8886
+ if (footprint() > 33554432) {
8887
+ capture.chain.invalid = true;
8888
+ capture.chain.before = rawReviewFile(null);
8889
+ capture.chain.after = rawReviewFile(null);
8890
+ }
8891
+ }
8128
8892
  } catch {
8129
- chain.invalid = true;
8893
+ capture.chain.invalid = true;
8130
8894
  }
8895
+ capture.chain.busy = false;
8896
+ }
8897
+ }
8898
+ }
8899
+ /** @private @author ddj 2026年11月04号 @param sessionId Frozen owner. @param cwd Frozen workspace. @param path Patch target. @param signal Tool cancellation. @returns Reserved raw capture or no evidence. */
8900
+ async function prepareRaw(fs, sessionId, cwd, path, signal) {
8901
+ if (stopped || disposed.has(sessionId) || !cwd || chains.size >= 16 && !chains.has(sessionId) || footprint() >= 33554432) return null;
8902
+ const target = await fs.resolve(path, { cwd });
8903
+ if (stopped || disposed.has(sessionId)) return null;
8904
+ const identity = fs.processPath(target);
8905
+ let files = chains.get(sessionId);
8906
+ if (!files) {
8907
+ files = /* @__PURE__ */ new Map();
8908
+ chains.set(sessionId, files);
8909
+ }
8910
+ if (files.size >= 64 && !files.has(identity)) return null;
8911
+ let chain = files.get(identity);
8912
+ if (chain?.busy) {
8913
+ chain.invalid = true;
8914
+ return null;
8915
+ }
8916
+ const empty = rawReviewFile(null);
8917
+ if (!chain) {
8918
+ chain = {
8919
+ before: empty,
8920
+ after: empty,
8921
+ busy: true,
8922
+ invalid: false
8923
+ };
8924
+ files.set(identity, chain);
8925
+ try {
8926
+ chain.before = await readRaw(fs, target, signal);
8927
+ chain.after = chain.before;
8928
+ } catch (error) {
8929
+ chain.invalid = true;
8930
+ chain.busy = false;
8931
+ throw error;
8932
+ }
8933
+ } else {
8934
+ chain.busy = true;
8935
+ try {
8936
+ const before = await readRaw(fs, target, signal);
8937
+ if (chain.after.hash !== before.hash) chain.invalid = true;
8938
+ } catch (error) {
8939
+ chain.invalid = true;
8131
8940
  chain.busy = false;
8941
+ throw error;
8132
8942
  }
8133
8943
  }
8944
+ if (stopped || disposed.has(sessionId) || footprint() > 33554432) {
8945
+ chain.invalid = true;
8946
+ chain.busy = false;
8947
+ files.delete(identity);
8948
+ return null;
8949
+ }
8950
+ return {
8951
+ fs,
8952
+ target,
8953
+ chain
8954
+ };
8134
8955
  }
8135
8956
  /** @private @author ddj 2026年10月27号 @param session Source session. @param event Published official turn. Freezes the chain before the next turn can mutate it. */
8136
8957
  function onEvent(session, event) {
@@ -8194,10 +9015,34 @@ async function readRaw(fs, target, signal) {
8194
9015
  const WRITE_TOOLS = [
8195
9016
  "edit",
8196
9017
  "write",
8197
- "str_replace_editor"
9018
+ "str_replace_editor",
9019
+ "apply_patch"
8198
9020
  ];
8199
9021
  /** 单文件文本上限:超过即标注 oversized,不静默截断。 */
8200
9022
  const MAX_TEXT = 1048576;
9023
+ const captureGaps = /* @__PURE__ */ new Map();
9024
+ /** @public @author ddj 2026年11月04号 @param cwd Workspace identity. @returns Bounded runtime-only capture diagnostics. */
9025
+ function pendingGaps(cwd) {
9026
+ return [...captureGaps.values()].filter((gap) => gap.cwd === cwd).map((gap) => gap.message);
9027
+ }
9028
+ /** @private @author ddj 2026年11月04号 @param ctx Host scope. @param exec Original call. @param path Failed target. @param error Evidence failure. Records no writable state. */
9029
+ function recordGap(scope, exec, path, error) {
9030
+ const { cwd, sessionId } = scope;
9031
+ const key = JSON.stringify([
9032
+ cwd,
9033
+ sessionId,
9034
+ exec.id,
9035
+ path
9036
+ ]);
9037
+ captureGaps.set(key, {
9038
+ cwd,
9039
+ sessionId,
9040
+ message: path + ": " + String(error)
9041
+ });
9042
+ const local = [...captureGaps].filter(([, gap]) => gap.cwd === cwd);
9043
+ while (local.length > 50) captureGaps.delete(local.shift()[0]);
9044
+ while (captureGaps.size > 500) captureGaps.delete(captureGaps.keys().next().value);
9045
+ }
8201
9046
  /**
8202
9047
  * @author ddj 2026年10月30号
8203
9048
  * @description: 安装写文件采集;读写失败只影响待审来源,绝不影响用户工具本身。
@@ -8208,12 +9053,18 @@ const MAX_TEXT = 1048576;
8208
9053
  function installPending(ctx, sink) {
8209
9054
  const state = {
8210
9055
  busy: /* @__PURE__ */ new Set(),
8211
- stopped: false
9056
+ stopped: false,
9057
+ disposed: /* @__PURE__ */ new Set()
8212
9058
  };
8213
9059
  ctx.on("tools/execute", (exec, next) => aroundTool(ctx, state, sink, exec, next));
9060
+ ctx.on("session/disposed", (session) => {
9061
+ state.disposed.add(session.id);
9062
+ for (const [key, gap] of captureGaps) if (gap.sessionId === session.id) captureGaps.delete(key);
9063
+ });
8214
9064
  return { dispose() {
8215
9065
  state.stopped = true;
8216
9066
  state.busy.clear();
9067
+ captureGaps.clear();
8217
9068
  } };
8218
9069
  }
8219
9070
  /**
@@ -8224,27 +9075,32 @@ function installPending(ctx, sink) {
8224
9075
  * @param exec 工具分发上下文
8225
9076
  * @returns 采集计划或 null
8226
9077
  */
8227
- async function planOf(ctx, state, exec) {
8228
- const session = exec.agent?.session;
8229
- const requested = exec.arguments?.file_path ?? exec.arguments?.path;
8230
- if (state.stopped || !session?.id || typeof requested !== "string") return null;
8231
- if (!WRITE_TOOLS.includes(String(exec.name))) return null;
8232
- const cwd = cwdOf(session);
8233
- if (!cwd) return null;
8234
- const fs = ctx.get("fs");
8235
- const target = await resolveTarget(ctx, session, requested);
9078
+ async function planOf(scope, state, requested) {
9079
+ const { cwd, sessionId, fs } = scope;
9080
+ if (state.stopped || state.disposed.has(sessionId)) return null;
9081
+ const target = await fs.resolve(requested, { cwd });
8236
9082
  const identity = fs.processPath(target);
8237
- if (typeof identity !== "string" || !identity || state.busy.has(identity)) return null;
8238
- const before = await readText(fs, target);
8239
- if (before !== null && before.length > MAX_TEXT) return null;
8240
- return {
8241
- cwd: String(cwd),
8242
- sessionId: String(session.id),
8243
- path: requested,
8244
- identity,
8245
- before,
8246
- session
8247
- };
9083
+ if (state.stopped || state.disposed.has(sessionId) || typeof identity !== "string" || !identity || state.busy.has(identity)) return null;
9084
+ state.busy.add(identity);
9085
+ try {
9086
+ const before = await readText(fs, target);
9087
+ if (state.stopped || state.disposed.has(sessionId) || before !== null && before.length > MAX_TEXT) {
9088
+ state.busy.delete(identity);
9089
+ return null;
9090
+ }
9091
+ return {
9092
+ cwd: String(cwd),
9093
+ sessionId,
9094
+ path: requested,
9095
+ identity,
9096
+ before,
9097
+ target,
9098
+ fs
9099
+ };
9100
+ } catch (error) {
9101
+ state.busy.delete(identity);
9102
+ throw error;
9103
+ }
8248
9104
  }
8249
9105
  /**
8250
9106
  * @author ddj 2026年10月30号
@@ -8257,63 +9113,87 @@ async function planOf(ctx, state, exec) {
8257
9113
  * @returns 原始工具结果
8258
9114
  */
8259
9115
  async function aroundTool(ctx, state, sink, exec, next) {
8260
- let plan = null;
9116
+ if (state.stopped || !WRITE_TOOLS.includes(String(exec.name))) return next();
9117
+ const session = exec.agent?.session;
9118
+ const cwd = session ? cwdOfSession(ctx, session) : null;
9119
+ if (!session?.id || !cwd) return next();
9120
+ const scope = {
9121
+ cwd,
9122
+ sessionId: session.id,
9123
+ fs: ctx.get("fs")
9124
+ };
9125
+ const plans = [];
8261
9126
  try {
8262
- plan = await planOf(ctx, state, exec);
9127
+ const paths = await toolTargets(exec);
9128
+ for (const path of paths) try {
9129
+ if (plans.length >= 16) throw new Error("capture target budget exceeded");
9130
+ const plan = await planOf(scope, state, path);
9131
+ if (plan) plans.push(plan);
9132
+ } catch (error) {
9133
+ if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, path, error);
9134
+ log.debug("待审采集跳过:" + path + ": " + String(error));
9135
+ }
8263
9136
  } catch (error) {
8264
- log.debug("待审采集跳过:" + String(error));
9137
+ if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, String(exec.name), error);
9138
+ log.debug("待审目标不可用:" + String(error));
8265
9139
  }
8266
- if (!plan) return next();
8267
- state.busy.add(plan.identity);
8268
9140
  try {
8269
- return await next();
9141
+ const result = await next();
9142
+ if (exec.name === "apply_patch" && !state.stopped && !state.disposed.has(scope.sessionId)) for (const diagnostic of patchDiagnostics(result)) recordGap(scope, exec, "apply_patch", diagnostic);
9143
+ return result;
8270
9144
  } finally {
8271
- state.busy.delete(plan.identity);
8272
- try {
8273
- reportWrite(ctx, sink, plan);
9145
+ for (const plan of plans) try {
9146
+ await reportWrite(state, sink, plan);
8274
9147
  } catch (error) {
9148
+ if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, plan.path, error);
8275
9149
  log.debug("待审上报失败:" + String(error));
9150
+ } finally {
9151
+ state.busy.delete(plan.identity);
8276
9152
  }
8277
9153
  }
8278
9154
  }
8279
9155
  /**
8280
- * @author ddj 2026年10月29号
8281
- * @description: 写后读回并比较,内容一致视为无净变化不产生条目。
8282
- * @param ctx Host 生命周期
9156
+ * @author ddj 2026年11月04号
9157
+ * @description: 固定目标写后读回;内容一致不产生条目,卸载后不再上报。
9158
+ * @param state 采集生命周期状态
8283
9159
  * @param sink 已确认写入的接收方
8284
9160
  * @param plan 采集计划
8285
9161
  * @returns 无;读回失败只记日志
8286
9162
  */
8287
- function reportWrite(ctx, sink, plan) {
8288
- const fs = ctx.get("fs");
8289
- resolveTarget(ctx, plan.session, plan.path).then(async (target) => {
8290
- const after = await readText(fs, target);
8291
- if (after !== null && after.length > MAX_TEXT) return;
8292
- if (plan.before === after) return;
8293
- sink({
8294
- cwd: plan.cwd,
8295
- sessionId: plan.sessionId,
8296
- path: plan.path,
8297
- identity: plan.identity,
8298
- before: plan.before,
8299
- after,
8300
- now: Date.now()
8301
- });
8302
- }).catch((error) => log.debug("待审读回失败:" + String(error)));
9163
+ async function reportWrite(state, sink, plan) {
9164
+ if (state.stopped || state.disposed.has(plan.sessionId)) return;
9165
+ const after = await readText(plan.fs, plan.target);
9166
+ if (state.stopped || state.disposed.has(plan.sessionId)) return;
9167
+ if (after !== null && after.length > MAX_TEXT) throw new Error("capture text budget exceeded");
9168
+ for (const [key, gap] of captureGaps) {
9169
+ const identity = JSON.parse(key);
9170
+ if (gap.cwd === plan.cwd && gap.sessionId === plan.sessionId && identity[3] === plan.path) captureGaps.delete(key);
9171
+ }
9172
+ if (plan.before === after) return;
9173
+ sink({
9174
+ cwd: plan.cwd,
9175
+ sessionId: plan.sessionId,
9176
+ path: plan.path,
9177
+ identity: plan.identity,
9178
+ before: plan.before,
9179
+ after,
9180
+ now: Date.now()
9181
+ });
8303
9182
  }
8304
9183
  /**
8305
9184
  * @author ddj 2026年10月29号
8306
- * @description: 读取全文;缺失或不可读按"无内容"处理,交由上层决定语义。
9185
+ * @description: 读取全文;只有明确缺失返回 null,不可读或超限错误保留失败语义。
8307
9186
  * @param fs 文件系统
8308
9187
  * @param target 目标
8309
9188
  * @returns 文本或 null
8310
9189
  */
8311
- async function readText(fs, target) {
9190
+ async function readText(fs, target, signal) {
8312
9191
  try {
8313
- return String(await fs.readText(target));
9192
+ if (typeof fs.readBytes === "function") return rawText(rawReviewFile(await fs.readBytes(target, signal, MAX_TEXT)));
9193
+ throw new Error("Raw byte capture unavailable");
8314
9194
  } catch (error) {
8315
9195
  const code = error.code;
8316
- if (code === "ENOENT" || code === "FS_NOT_FOUND" || code === "FS_TOO_LARGE") return null;
9196
+ if (code === "ENOENT" || code === "FS_NOT_FOUND") return null;
8317
9197
  throw error;
8318
9198
  }
8319
9199
  }
@@ -9216,375 +10096,123 @@ async function revert(input, targets, whole) {
9216
10096
  revision: input.flags.revision + 1,
9217
10097
  missing: written.content === null
9218
10098
  },
9219
- blocks: markAll(input.blocks, targets, "rejected")
9220
- };
9221
- }
9222
- /**
9223
- * @author ddj 2026年10月29号
9224
- * @description: 整文件回退目标:基线为"文件不存在"时删除,否则写回基线全文。
9225
- * @param entry 待审条目
9226
- * @returns 目标内容,null 表示删除
9227
- */
9228
- function wholeRevert(entry) {
9229
- return entry.earlierVersion === "none" ? null : entry.oldText;
9230
- }
9231
- /**
9232
- * @author ddj 2026年10月29号
9233
- * @description: 块级回退:在最新内容里依次换回目标块,任一不可定位即整体放弃。
9234
- * @param entry 待审条目
9235
- * @param targets 目标块
9236
- * @returns 新内容或 undefined
9237
- */
9238
- function blockRevert(entry, targets) {
9239
- let newText = entry.newText;
9240
- for (const block of targets) {
9241
- const next = revertBlock(newText, block);
9242
- if (next === null) return void 0;
9243
- newText = next;
9244
- }
9245
- return newText;
9246
- }
9247
- /**
9248
- * @author ddj 2026年10月29号
9249
- * @description: 按请求粒度挑出目标块;块不存在时返回 null 以区分"没有块"。
9250
- * @param blocks 当前差异块
9251
- * @param input 判定输入
9252
- * @returns 目标块,未知块 ID 时为 null
9253
- */
9254
- function pickBlocks(blocks, input) {
9255
- const scope = input.request.scope;
9256
- if (scope === "file" || scope === "all") return [];
9257
- if (scope === "block") {
9258
- const block = blocks.find((item) => item.id === input.request.blockId);
9259
- return block ? [block] : null;
9260
- }
9261
- const start = input.request.startLine ?? 0;
9262
- const end = input.request.endLine ?? start;
9263
- return blocks.filter((block) => block.newStart <= end && block.newStart + Math.max(block.newLines.length, 1) >= start);
9264
- }
9265
- /**
9266
- * @author ddj 2026年10月29号
9267
- * @description: 把目标块标记为已决定;整文件时把全部块一起标记。
9268
- * @param states 已存块决定
9269
- * @param targets 目标块
9270
- * @param decision 决定
9271
- * @returns 新的块决定列表
9272
- */
9273
- function markAll(states, targets, decision) {
9274
- const next = states.slice();
9275
- for (const block of targets) {
9276
- const at = next.findIndex((state) => state.id === block.id);
9277
- if (at >= 0) next[at] = {
9278
- id: block.id,
9279
- decision
9280
- };
9281
- else next.push({
9282
- id: block.id,
9283
- decision
9284
- });
9285
- }
9286
- return next;
9287
- }
9288
- /**
9289
- * @author ddj 2026年10月29号
9290
- * @description: 能力口径:Keep 恒可;Revert 受超大/缺失/已解决限制,拒绝原因要能展示。
9291
- * @param entry 待审条目
9292
- * @param flags 持久标记
9293
- * @returns 能力与原因
9294
- */
9295
- function capabilityOf(entry, flags) {
9296
- if (isSettled(entry)) return {
9297
- keep: false,
9298
- revert: false,
9299
- reason: "settled"
9300
- };
9301
- if (flags.oversized) return {
9302
- keep: false,
9303
- revert: false,
9304
- reason: "oversized"
9305
- };
9306
- if (entry.earlierVersion === "none" && entry.newText.length === 0) return {
9307
- keep: true,
9308
- revert: false,
9309
- reason: "empty-create"
9310
- };
9311
- return {
9312
- keep: true,
9313
- revert: true,
9314
- reason: ""
9315
- };
9316
- }
9317
- /**
9318
- * @author ddj 2026年10月29号
9319
- * @description: 供详情接口复用的差异构造。
9320
- * @param entry 待审条目
9321
- * @param baselineEmpty 基线置空:文件由本次会话新建,旧基线只是会话自己的中间产物
9322
- * @returns 官方差异
9323
- */
9324
- function diffOf(entry, baselineEmpty = false) {
9325
- if (!baselineEmpty) return textDiff(entry.path, entry.path, entry.oldText, entry.newText);
9326
- if (entry.newText === "") return textDiff(entry.path, entry.path, "", "");
9327
- const diff = textDiff(entry.path, entry.path, "", entry.newText);
9328
- if (diff.kind !== "text") return diff;
9329
- return {
9330
- ...diff,
9331
- hunks: diff.hunks.map((hunk) => ({
9332
- ...hunk,
9333
- oldLines: 0,
9334
- lines: hunk.lines.filter((line) => line !== "-")
9335
- }))
9336
- };
9337
- }
9338
- //#endregion
9339
- //#region src/pending/persist.ts
9340
- /** 待审库版本号,写进 metadata 便于日后迁移。v3 给评论表加了引文与上下文列。 */
9341
- const SCHEMA_VERSION = "3";
9342
- /**
9343
- * @author ddj 2026年10月29号
9344
- * @description: 打开工作区库并确保待审表存在;DDL 与版本标记在同一事务里提交。
9345
- * @param cwd 已迁移工作区
9346
- * @returns 可读写的连接
9347
- * @throws 未完成活跃库迁移时拒绝
9348
- */
9349
- function pendingDb(cwd) {
9350
- if (!activeReady(cwd)) throw new Error("待审投影要求先完成活跃库迁移");
9351
- const db = dbFor(cwd);
9352
- if (db.prepare("SELECT value FROM metadata WHERE name='pending_schema'").get()?.value === SCHEMA_VERSION) return db;
9353
- db.exec("BEGIN IMMEDIATE");
9354
- try {
9355
- db.exec(DDL);
9356
- migrateHistory(db);
9357
- db.prepare("INSERT INTO metadata(name,value) VALUES('pending_schema',?) ON CONFLICT(name) DO UPDATE SET value=excluded.value").run(SCHEMA_VERSION);
9358
- db.exec("COMMIT");
9359
- } catch (error) {
9360
- if (db.isTransaction) db.exec("ROLLBACK");
9361
- throw error;
9362
- }
9363
- return db;
9364
- }
9365
- /** 待审表结构:条目、块决定、幂等操作、撤销栈。 */
9366
- const DDL = `
9367
- CREATE TABLE IF NOT EXISTS pending_entries (
9368
- id TEXT PRIMARY KEY, path TEXT NOT NULL, earlier TEXT NOT NULL,
9369
- old_text TEXT NOT NULL, new_text TEXT NOT NULL, updated_at INTEGER NOT NULL,
9370
- unseen INTEGER NOT NULL DEFAULT 0, missing INTEGER NOT NULL DEFAULT 0,
9371
- diverged INTEGER NOT NULL DEFAULT 0, oversized INTEGER NOT NULL DEFAULT 0,
9372
- revision INTEGER NOT NULL DEFAULT 0, session_id TEXT NOT NULL,
9373
- session_ids TEXT NOT NULL, lineage TEXT);
9374
- CREATE TABLE IF NOT EXISTS pending_blocks (
9375
- entry_id TEXT NOT NULL, block_id TEXT NOT NULL, decision TEXT NOT NULL,
9376
- PRIMARY KEY(entry_id,block_id));
9377
- CREATE TABLE IF NOT EXISTS pending_ops (
9378
- operation_id TEXT PRIMARY KEY, request TEXT NOT NULL, state TEXT NOT NULL,
9379
- result TEXT, undo TEXT);
9380
- CREATE TABLE IF NOT EXISTS pending_history (
9381
- id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, entry_id TEXT,
9382
- snapshot TEXT NOT NULL, created_at INTEGER NOT NULL,
9383
- state TEXT NOT NULL DEFAULT 'applied');
9384
- `;
9385
- /**
9386
- * @author ddj 2026年10月29号
9387
- * @description: v1 历史表补 state 列(rebuild 成本高于一次 ALTER)。
9388
- * @param db 已打开的工作区库
9389
- * @returns 无
9390
- */
9391
- function migrateHistory(db) {
9392
- if (db.prepare("PRAGMA table_info(pending_history)").all().some((column) => column.name === "state")) return;
9393
- db.exec("ALTER TABLE pending_history ADD COLUMN state TEXT NOT NULL DEFAULT 'applied'");
9394
- }
9395
- /**
9396
- * @author ddj 2026年10月29号
9397
- * @description: 读出全部待审条目,按更新时间升序。
9398
- * @param cwd 工作区
9399
- * @returns 条目与标记
9400
- */
9401
- function loadEntries(cwd) {
9402
- return pendingDb(cwd).prepare("SELECT * FROM pending_entries ORDER BY updated_at ASC").all().map((row) => ({
9403
- entry: entryOfRow(row),
9404
- flags: flagsOfRow(row)
9405
- }));
9406
- }
9407
- /**
9408
- * @author ddj 2026年10月29号
9409
- * @description: 写入或更新一条条目(含标记),单条原子完成。
9410
- * @param cwd 工作区
9411
- * @param stored 条目与标记
9412
- * @returns 无
9413
- */
9414
- function saveEntry(cwd, stored) {
9415
- const { entry, flags } = stored;
9416
- pendingDb(cwd).prepare(`INSERT INTO pending_entries
9417
- (id,path,earlier,old_text,new_text,updated_at,unseen,missing,diverged,oversized,revision,session_id,session_ids,lineage)
9418
- VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
9419
- ON CONFLICT(id) DO UPDATE SET path=excluded.path,earlier=excluded.earlier,old_text=excluded.old_text,
9420
- new_text=excluded.new_text,updated_at=excluded.updated_at,unseen=excluded.unseen,missing=excluded.missing,
9421
- diverged=excluded.diverged,oversized=excluded.oversized,revision=excluded.revision,
9422
- session_id=excluded.session_id,session_ids=excluded.session_ids,lineage=excluded.lineage`).run(entry.id, entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt, entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0, flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds), entry.lineage ? JSON.stringify(entry.lineage) : null);
9423
- }
9424
- /**
9425
- * @author ddj 2026年10月29号
9426
- * @description: 删除条目及其块决定,用于"已解决条目移除"。
9427
- * @param cwd 工作区
9428
- * @param id 条目身份
9429
- * @returns 无
9430
- */
9431
- function dropEntry(cwd, id) {
9432
- const db = pendingDb(cwd);
9433
- db.exec("BEGIN IMMEDIATE");
9434
- try {
9435
- db.prepare("DELETE FROM pending_entries WHERE id=?").run(id);
9436
- db.prepare("DELETE FROM pending_blocks WHERE entry_id=?").run(id);
9437
- db.exec("COMMIT");
9438
- } catch (error) {
9439
- if (db.isTransaction) db.exec("ROLLBACK");
9440
- throw error;
9441
- }
9442
- }
9443
- /**
9444
- * @author ddj 2026年10月29号
9445
- * @description: 覆盖某条目的块决定集合。
9446
- * @param cwd 工作区
9447
- * @param entryId 条目身份
9448
- * @param blocks 块决定
9449
- * @returns 无
9450
- */
9451
- function saveBlocks(cwd, entryId, blocks) {
9452
- const db = pendingDb(cwd);
9453
- db.exec("BEGIN IMMEDIATE");
9454
- try {
9455
- db.prepare("DELETE FROM pending_blocks WHERE entry_id=?").run(entryId);
9456
- const insert = db.prepare("INSERT INTO pending_blocks VALUES(?,?,?)");
9457
- for (const block of blocks) insert.run(entryId, block.id, block.decision);
9458
- db.exec("COMMIT");
9459
- } catch (error) {
9460
- if (db.isTransaction) db.exec("ROLLBACK");
9461
- throw error;
9462
- }
9463
- }
9464
- /**
9465
- * @author ddj 2026年10月29号
9466
- * @description: 读取幂等操作记录;同 operationId 的重复请求必须复用结果。
9467
- * @param cwd 工作区
9468
- * @param operationId 操作身份
9469
- * @returns 请求原文、状态与结果
9470
- */
9471
- function readOp(cwd, operationId) {
9472
- const row = pendingDb(cwd).prepare("SELECT request,state,result FROM pending_ops WHERE operation_id=?").get(operationId);
9473
- return row ? {
9474
- request: String(row.request),
9475
- state: String(row.state),
9476
- result: row.result === null ? null : String(row.result)
9477
- } : void 0;
10099
+ blocks: markAll(input.blocks, targets, "rejected")
10100
+ };
9478
10101
  }
9479
10102
  /**
9480
10103
  * @author ddj 2026年10月29号
9481
- * @description: 记录一次操作的阶段与撤销快照。
9482
- * @param cwd 工作区
9483
- * @param operationId 操作身份
9484
- * @param request 请求原文
9485
- * @param state prepared / committed
9486
- * @param payload 结果与撤销快照
9487
- * @returns 无
10104
+ * @description: 整文件回退目标:基线为"文件不存在"时删除,否则写回基线全文。
10105
+ * @param entry 待审条目
10106
+ * @returns 目标内容,null 表示删除
9488
10107
  */
9489
- function writeOp(cwd, operationId, request, state, payload = {}) {
9490
- pendingDb(cwd).prepare(`INSERT INTO pending_ops VALUES(?,?,?,?,?)
9491
- ON CONFLICT(operation_id) DO UPDATE SET state=excluded.state,
9492
- result=COALESCE(excluded.result,pending_ops.result), undo=COALESCE(excluded.undo,pending_ops.undo)`).run(operationId, request, state, payload.result ?? null, payload.undo ?? null);
10108
+ function wholeRevert(entry) {
10109
+ return entry.earlierVersion === "none" ? null : entry.oldText;
9493
10110
  }
9494
10111
  /**
9495
10112
  * @author ddj 2026年10月29号
9496
- * @description: 追加一条撤销栈记录,并截断重做分支(新决定之后旧的"已撤销"记录不能再重做)。
9497
- * @param cwd 工作区
9498
- * @param kind 操作种类
9499
- * @param entryId 涉及的条目
9500
- * @param snapshot 可重放快照
9501
- * @param now 时间
9502
- * @returns 无
9503
- */
9504
- function pushHistory(cwd, kind, entryId, snapshot, now) {
9505
- const db = pendingDb(cwd);
9506
- db.prepare("DELETE FROM pending_history WHERE state='undone'").run();
9507
- db.prepare("INSERT INTO pending_history(kind,entry_id,snapshot,created_at,state) VALUES(?,?,?,?,'applied')").run(kind, entryId, snapshot, now);
9508
- }
9509
- /**
9510
- * @author ddj 2026年10月30号
9511
- * @description: 取最近一条仍生效的历史记录,撤销就作用在它上面。
9512
- * @param cwd 工作区
9513
- * @returns 记录或 undefined
9514
- */
9515
- function latestApplied(cwd) {
9516
- const row = pendingDb(cwd).prepare("SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='applied' ORDER BY id DESC LIMIT 1").get();
9517
- return row ? rowOfHistory(row) : void 0;
9518
- }
9519
- /**
9520
- * @author ddj 2026年10月30号
9521
- * @description: 取最早一条被撤销的记录,重做按"撤销的逆序"重新生效。
9522
- * @param cwd 工作区
9523
- * @returns 记录或 undefined
10113
+ * @description: 块级回退:在最新内容里依次换回目标块,任一不可定位即整体放弃。
10114
+ * @param entry 待审条目
10115
+ * @param targets 目标块
10116
+ * @returns 新内容或 undefined
9524
10117
  */
9525
- function earliestUndone(cwd) {
9526
- const row = pendingDb(cwd).prepare("SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='undone' ORDER BY id ASC LIMIT 1").get();
9527
- return row ? rowOfHistory(row) : void 0;
10118
+ function blockRevert(entry, targets) {
10119
+ let newText = entry.newText;
10120
+ for (const block of targets) {
10121
+ const next = revertBlock(newText, block);
10122
+ if (next === null) return void 0;
10123
+ newText = next;
10124
+ }
10125
+ return newText;
9528
10126
  }
9529
10127
  /**
9530
- * @author ddj 2026年10月30号
9531
- * @description: 翻转一条历史记录的生效状态。
9532
- * @param cwd 工作区
9533
- * @param id 记录主键
9534
- * @param state applied / undone
9535
- * @returns 无
10128
+ * @author ddj 2026年10月29号
10129
+ * @description: 按请求粒度挑出目标块;块不存在时返回 null 以区分"没有块"。
10130
+ * @param blocks 当前差异块
10131
+ * @param input 判定输入
10132
+ * @returns 目标块,未知块 ID 时为 null
9536
10133
  */
9537
- function setHistoryState(cwd, id, state) {
9538
- pendingDb(cwd).prepare("UPDATE pending_history SET state=? WHERE id=?").run(state, id);
10134
+ function pickBlocks(blocks, input) {
10135
+ const scope = input.request.scope;
10136
+ if (scope === "file" || scope === "all") return [];
10137
+ if (scope === "block") {
10138
+ const block = blocks.find((item) => item.id === input.request.blockId);
10139
+ return block ? [block] : null;
10140
+ }
10141
+ const start = input.request.startLine ?? 0;
10142
+ const end = input.request.endLine ?? start;
10143
+ return blocks.filter((block) => block.newStart <= end && block.newStart + Math.max(block.newLines.length, 1) >= start);
9539
10144
  }
9540
10145
  /**
9541
- * @author ddj 2026年10月30号
9542
- * @description: 把数据库行还原成历史记录。
9543
- * @param row 数据库行
9544
- * @returns 记录
10146
+ * @author ddj 2026年10月29号
10147
+ * @description: 把目标块标记为已决定;整文件时把全部块一起标记。
10148
+ * @param states 已存块决定
10149
+ * @param targets 目标块
10150
+ * @param decision 决定
10151
+ * @returns 新的块决定列表
9545
10152
  */
9546
- function rowOfHistory(row) {
9547
- return {
9548
- id: Number(row.id),
9549
- kind: String(row.kind),
9550
- entryId: String(row.entry_id),
9551
- snapshot: String(row.snapshot)
9552
- };
10153
+ function markAll(states, targets, decision) {
10154
+ const next = states.slice();
10155
+ for (const block of targets) {
10156
+ const at = next.findIndex((state) => state.id === block.id);
10157
+ if (at >= 0) next[at] = {
10158
+ id: block.id,
10159
+ decision
10160
+ };
10161
+ else next.push({
10162
+ id: block.id,
10163
+ decision
10164
+ });
10165
+ }
10166
+ return next;
9553
10167
  }
9554
10168
  /**
9555
10169
  * @author ddj 2026年10月29号
9556
- * @description: 由数据库行还原条目。
9557
- * @param row 数据库行
9558
- * @returns 条目
10170
+ * @description: 能力口径:Keep 恒可;Revert 受超大/缺失/已解决限制,拒绝原因要能展示。
10171
+ * @param entry 待审条目
10172
+ * @param flags 持久标记
10173
+ * @returns 能力与原因
9559
10174
  */
9560
- function entryOfRow(row) {
9561
- const lineage = row.lineage === null ? void 0 : JSON.parse(String(row.lineage));
9562
- const entry = {
9563
- id: String(row.id),
9564
- path: String(row.path),
9565
- earlierVersion: String(row.earlier) === "none" ? "none" : "file",
9566
- oldText: String(row.old_text),
9567
- newText: String(row.new_text),
9568
- updatedAt: Number(row.updated_at),
9569
- sessionId: String(row.session_id),
9570
- sessionIds: JSON.parse(String(row.session_ids))
10175
+ function capabilityOf(entry, flags) {
10176
+ if (isSettled(entry)) return {
10177
+ keep: false,
10178
+ revert: false,
10179
+ reason: "settled"
10180
+ };
10181
+ if (flags.oversized) return {
10182
+ keep: false,
10183
+ revert: false,
10184
+ reason: "oversized"
10185
+ };
10186
+ if (entry.earlierVersion === "none" && entry.newText.length === 0) return {
10187
+ keep: true,
10188
+ revert: false,
10189
+ reason: "empty-create"
10190
+ };
10191
+ return {
10192
+ keep: true,
10193
+ revert: true,
10194
+ reason: ""
9571
10195
  };
9572
- if (Number(row.unseen) === 1) entry.unseen = true;
9573
- if (lineage !== void 0) entry.lineage = lineage;
9574
- return entry;
9575
10196
  }
9576
10197
  /**
9577
10198
  * @author ddj 2026年10月29号
9578
- * @description: 由数据库行还原标记。
9579
- * @param row 数据库行
9580
- * @returns 标记
10199
+ * @description: 供详情接口复用的差异构造。
10200
+ * @param entry 待审条目
10201
+ * @param baselineEmpty 基线置空:文件由本次会话新建,旧基线只是会话自己的中间产物
10202
+ * @returns 官方差异
9581
10203
  */
9582
- function flagsOfRow(row) {
10204
+ function diffOf(entry, baselineEmpty = false) {
10205
+ if (!baselineEmpty) return textDiff(entry.path, entry.path, entry.oldText, entry.newText);
10206
+ if (entry.newText === "") return textDiff(entry.path, entry.path, "", "");
10207
+ const diff = textDiff(entry.path, entry.path, "", entry.newText);
10208
+ if (diff.kind !== "text") return diff;
9583
10209
  return {
9584
- missing: Number(row.missing) === 1,
9585
- diverged: Number(row.diverged) === 1,
9586
- oversized: Number(row.oversized) === 1,
9587
- revision: Number(row.revision)
10210
+ ...diff,
10211
+ hunks: diff.hunks.map((hunk) => ({
10212
+ ...hunk,
10213
+ oldLines: 0,
10214
+ lines: hunk.lines.filter((line) => line !== "-")
10215
+ }))
9588
10216
  };
9589
10217
  }
9590
10218
  //#endregion
@@ -9693,18 +10321,39 @@ function itemOf(stored, baselineEmpty = false) {
9693
10321
  return item;
9694
10322
  }
9695
10323
  /**
10324
+ * @private
10325
+ * @author ddj 2026年11月04号
10326
+ * @description 从条目来源判断创建基线;任何决定历史都阻止展示层复活已推进的旧基线。
10327
+ * @param cwd 工作区
10328
+ * @param stored 累计条目
10329
+ * @param sourceAdds 单次读取内的来源创建集合缓存
10330
+ * @returns 是否将展示基线置空
10331
+ */
10332
+ function emptyBaseOf(cwd, stored, sourceAdds = /* @__PURE__ */ new Map()) {
10333
+ if (isSettled(stored.entry)) return false;
10334
+ if (pendingDb(cwd).prepare("SELECT 1 FROM pending_history WHERE entry_id=? LIMIT 1").get(stored.entry.id)) return false;
10335
+ for (const sourceId of stored.entry.sessionIds) {
10336
+ let createdPaths = sourceAdds.get(sourceId);
10337
+ if (!createdPaths) {
10338
+ createdPaths = addedFiles(cwd, sourceId);
10339
+ sourceAdds.set(sourceId, createdPaths);
10340
+ }
10341
+ if (createdPaths.has(stored.entry.id)) return true;
10342
+ }
10343
+ return false;
10344
+ }
10345
+ /**
9696
10346
  * @author ddj 2026年10月29号
9697
- * @description: 读一页待审条目,按更新时间升序;会话过滤走血缘集合。
10347
+ * @description: 读工作区全部待审条目,按更新时间升序;来源会话仅用于追溯。
9698
10348
  * @param cwd 工作区
9699
- * @param sessionId 当前会话
9700
- * @param wholeAdd 本次会话新建的文件身份集合,命中时基线置空
10349
+ * @param _sessionId 兼容旧调用签名,不再用于可见性过滤
9701
10350
  * @returns 列表
9702
10351
  */
9703
- function loadPage(cwd, sessionId, wholeAdd) {
9704
- const all = loadEntries(cwd);
9705
- const visible = sessionId ? all.filter((stored) => stored.entry.sessionIds.includes(sessionId)) : all;
9706
- const items = visible.map((stored) => itemOf(stored, wholeAdd?.has(stored.entry.id) === true));
9707
- const gaps = [];
10352
+ function loadPage(cwd, _sessionId) {
10353
+ const visible = loadEntries(cwd);
10354
+ const sourceAdds = /* @__PURE__ */ new Map();
10355
+ const items = visible.map((stored) => itemOf(stored, emptyBaseOf(cwd, stored, sourceAdds)));
10356
+ const gaps = pendingGaps(cwd);
9708
10357
  for (const stored of visible) if (stored.flags.oversized) gaps.push(`${stored.entry.path}: 文本超出上限,仅保留标记`);
9709
10358
  return {
9710
10359
  items,
@@ -9724,9 +10373,10 @@ function loadPage(cwd, sessionId, wholeAdd) {
9724
10373
  function loadDetail(cwd, id, baselineEmpty = false) {
9725
10374
  const stored = loadEntries(cwd).find((item) => item.entry.id === id);
9726
10375
  if (!stored) return null;
9727
- const diff = diffOf(stored.entry, baselineEmpty);
10376
+ const emptyBase = baselineEmpty || emptyBaseOf(cwd, stored);
10377
+ const diff = diffOf(stored.entry, emptyBase);
9728
10378
  return {
9729
- item: itemOf(stored, baselineEmpty),
10379
+ item: itemOf(stored, emptyBase),
9730
10380
  diff,
9731
10381
  blocks: blockStatesOf(stored.entry),
9732
10382
  capability: capabilityOf(stored.entry, stored.flags)
@@ -9826,23 +10476,25 @@ function writerFor(fs, cwd, identity, expected, signal, policy, allowCreate = fa
9826
10476
  reason: "resolve-failed"
9827
10477
  };
9828
10478
  }
9829
- const current = await readText(fs, target).catch(() => void 0);
9830
- if (current === void 0) return {
10479
+ const snapshot = await stableRead(fs, target, signal);
10480
+ if (!snapshot.ok) return snapshot;
10481
+ const recreate = allowCreate && snapshot.content === null;
10482
+ if (!recreate && snapshot.content !== expected) return {
9831
10483
  ok: false,
9832
- reason: "read-failed"
10484
+ reason: "diverged"
9833
10485
  };
9834
- if (!(allowCreate && current === null) && current !== expected) return {
10486
+ if (snapshot.content === null && !recreate) return {
9835
10487
  ok: false,
9836
- reason: "diverged"
10488
+ reason: "missing"
10489
+ };
10490
+ const intent = recreate ? { kind: "createIfAbsent" } : {
10491
+ kind: "replaceIfVersion",
10492
+ version: snapshot.version
9837
10493
  };
9838
- const currentStat = await fs.stat(target, signal).catch(() => void 0);
9839
10494
  try {
9840
- await fs.writeText(target, content ?? "", currentStat ? {
9841
- kind: "replaceIfVersion",
9842
- version: currentStat.version
9843
- } : void 0, signal, policy);
10495
+ await fs.writeText(target, content ?? "", intent, signal, policy);
9844
10496
  const readBack = await readText(fs, target);
9845
- if ((content ?? "") !== (readBack ?? "")) return {
10497
+ if (content !== readBack) return {
9846
10498
  ok: false,
9847
10499
  reason: "read-back-mismatch"
9848
10500
  };
@@ -9885,7 +10537,7 @@ async function applyDecide(fs, cwd, request, signal, policy) {
9885
10537
  blocks: beforeBlocks,
9886
10538
  now: Date.now(),
9887
10539
  write: (content) => {
9888
- if (content === null) return deleteTarget(fs, cwd, stored.entry.id, signal);
10540
+ if (content === null) return deleteTarget(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy);
9889
10541
  return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy)(content);
9890
10542
  }
9891
10543
  });
@@ -9914,6 +10566,113 @@ async function applyDecide(fs, cwd, request, signal, policy) {
9914
10566
  return outcome;
9915
10567
  }
9916
10568
  /**
10569
+ * @public
10570
+ * @author ddj 2026年11月03号
10571
+ * @description: 一次读取全表后批量保留待审条目(Keep 不写盘)。写入用刚读到的最新 revision,
10572
+ * 客户端快照漂移不会整批中止;单条 oversized、不存在或落库时被抢先推进只跳过自己。
10573
+ * @param cwd 工作区
10574
+ * @param items 待保留条目
10575
+ * @returns 保留成功与跳过的明细
10576
+ * @throws 待审库迁移未就绪等存储级异常
10577
+ */
10578
+ async function keepAll(cwd, items) {
10579
+ const stored = new Map(loadEntries(cwd).map((item) => [item.entry.id, item]));
10580
+ const now = Date.now();
10581
+ const outcome = {
10582
+ kept: [],
10583
+ skipped: []
10584
+ };
10585
+ const records = [];
10586
+ for (const item of items) {
10587
+ const before = stored.get(item.id);
10588
+ if (!before) {
10589
+ outcome.skipped.push({
10590
+ id: item.id,
10591
+ display: item.display,
10592
+ reason: "待审条目不存在"
10593
+ });
10594
+ continue;
10595
+ }
10596
+ if (before.flags.oversized) {
10597
+ outcome.skipped.push({
10598
+ id: item.id,
10599
+ display: item.display,
10600
+ reason: "文本超出上限,不能整文件保留"
10601
+ });
10602
+ continue;
10603
+ }
10604
+ if (isSettled(before.entry)) {
10605
+ outcome.kept.push({
10606
+ id: item.id,
10607
+ path: before.entry.path
10608
+ });
10609
+ continue;
10610
+ }
10611
+ const beforeBlocks = blockStatesOf(before.entry);
10612
+ const outcomeOf = await decide({
10613
+ request: {
10614
+ sessionId: before.entry.sessionId,
10615
+ id: item.id,
10616
+ revision: before.flags.revision,
10617
+ operationId: item.operationId,
10618
+ decision: "accepted",
10619
+ scope: "file"
10620
+ },
10621
+ entry: before.entry,
10622
+ flags: before.flags,
10623
+ blocks: beforeBlocks,
10624
+ now,
10625
+ write: () => {
10626
+ throw new Error("批量 Keep 不写盘");
10627
+ }
10628
+ });
10629
+ if (!outcomeOf.ok) {
10630
+ outcome.skipped.push({
10631
+ id: item.id,
10632
+ display: item.display,
10633
+ reason: outcomeOf.reason
10634
+ });
10635
+ continue;
10636
+ }
10637
+ outcome.kept.push({
10638
+ id: item.id,
10639
+ path: before.entry.path
10640
+ });
10641
+ records.push({
10642
+ entryId: item.id,
10643
+ stored: before,
10644
+ next: {
10645
+ entry: outcomeOf.entry,
10646
+ flags: outcomeOf.flags
10647
+ },
10648
+ blocks: outcomeOf.blocks,
10649
+ snapshot: JSON.stringify({
10650
+ before: {
10651
+ entry: before.entry,
10652
+ flags: before.flags,
10653
+ blocks: beforeBlocks
10654
+ },
10655
+ after: {
10656
+ entry: outcomeOf.entry,
10657
+ flags: outcomeOf.flags,
10658
+ blocks: outcomeOf.blocks
10659
+ },
10660
+ wrote: outcomeOf.wrote
10661
+ })
10662
+ });
10663
+ }
10664
+ const displayOf = new Map(items.map((item) => [item.id, item.display]));
10665
+ for (const conflict of saveDecisions(cwd, records)) {
10666
+ outcome.kept = outcome.kept.filter((entry) => entry.id !== conflict.id);
10667
+ outcome.skipped.push({
10668
+ id: conflict.id,
10669
+ display: displayOf.get(conflict.id) ?? conflict.id,
10670
+ reason: conflict.reason
10671
+ });
10672
+ }
10673
+ return outcome;
10674
+ }
10675
+ /**
9917
10676
  * @author ddj 2026年10月30号
9918
10677
  * @description: 版本库导入:把工作副本相对基线的改动折进待审条目,已存在条目按折叠规则并入。
9919
10678
  * @param shell shell 能力
@@ -10006,7 +10765,7 @@ async function undoDecide(fs, cwd, move, signal, policy) {
10006
10765
  * @returns 写盘结果
10007
10766
  */
10008
10767
  async function writeBack(fs, cwd, stored, target, signal, policy) {
10009
- if (target.flags.missing) return deleteTarget(fs, cwd, stored.entry.id, signal);
10768
+ if (target.flags.missing) return deleteTarget(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy);
10010
10769
  return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy, true)(target.entry.newText);
10011
10770
  }
10012
10771
  /**
@@ -10025,65 +10784,93 @@ function parseSnapshot(text) {
10025
10784
  }
10026
10785
  }
10027
10786
  /**
10028
- * @author ddj 2026年10月29号
10029
- * @description: Revert 的删除分支:FileSystem 抽象没有删除能力,只有确认仍在工作区内才用宿主 unlink。
10030
- * @param fs 文件系统
10031
- * @param cwd 工作区
10032
- * @param identity 目标身份
10787
+ * @private
10788
+ * @author ddj 2026年11月04号
10789
+ * @description 版本—内容—版本读取,防止将新版本令牌与旧内容拼成写入依据。
10790
+ * @param fs 文件提供方
10791
+ * @param target 已解析目标
10033
10792
  * @param signal 取消信号
10034
- * @returns 删除结果
10793
+ * @returns 稳定快照或拒绝原因
10035
10794
  */
10036
- async function deleteTarget(fs, cwd, identity, signal) {
10037
- let target;
10795
+ async function stableRead(fs, target, signal) {
10038
10796
  try {
10039
- target = await fs.resolve(identity, {
10040
- cwd,
10041
- signal
10042
- });
10043
- } catch {
10797
+ const before = await fs.stat(target, signal);
10798
+ if (!before) return {
10799
+ ok: true,
10800
+ content: null
10801
+ };
10802
+ if (before.type !== "file" || !before.version) return {
10803
+ ok: false,
10804
+ reason: "unguarded-file"
10805
+ };
10806
+ const content = await readText(fs, target, signal);
10807
+ if (content === null) return {
10808
+ ok: false,
10809
+ reason: "revision-mismatch"
10810
+ };
10811
+ const after = await fs.stat(target, signal);
10812
+ if (after?.type !== "file" || after.version !== before.version) return {
10813
+ ok: false,
10814
+ reason: "revision-mismatch"
10815
+ };
10816
+ return {
10817
+ ok: true,
10818
+ content,
10819
+ version: before.version
10820
+ };
10821
+ } catch (error) {
10044
10822
  return {
10045
10823
  ok: false,
10046
- reason: "resolve-failed"
10824
+ reason: reasonOf(error)
10047
10825
  };
10048
10826
  }
10049
- if (!await fs.stat(target, signal).catch(() => void 0)) return {
10050
- ok: true,
10051
- content: null
10052
- };
10053
- const absolute = String(fs.processPath(target));
10054
- if (!insideWorkspace(cwd, absolute)) return {
10827
+ }
10828
+ /**
10829
+ * @private
10830
+ * @author ddj 2026年11月04号
10831
+ * @description 通过 provider 受控删除,保留会话策略与原子版本校验;旧 Host 不回落直删。
10832
+ * @param fs 文件提供方
10833
+ * @param cwd 工作目录
10834
+ * @param identity 目标身份
10835
+ * @param expected 应有内容
10836
+ * @param signal 取消信号
10837
+ * @param policy 会话沙箱策略
10838
+ * @returns 删除结果;结果不明不提交决定
10839
+ */
10840
+ async function deleteTarget(fs, cwd, identity, expected, signal, policy) {
10841
+ if (typeof fs.deleteFile !== "function") return {
10055
10842
  ok: false,
10056
- reason: "outside-workspace"
10843
+ reason: "delete-unavailable"
10057
10844
  };
10058
10845
  try {
10059
- await unlink(absolute);
10846
+ const target = await fs.resolve(identity, {
10847
+ cwd,
10848
+ signal
10849
+ });
10850
+ const snapshot = await stableRead(fs, target, signal);
10851
+ if (!snapshot.ok) return snapshot;
10852
+ if (snapshot.content === null) return {
10853
+ ok: true,
10854
+ content: null
10855
+ };
10856
+ if (snapshot.content !== expected) return {
10857
+ ok: false,
10858
+ reason: "diverged"
10859
+ };
10860
+ await fs.deleteFile(target, { version: snapshot.version }, signal, policy);
10861
+ return await fs.stat(target, signal) ? {
10862
+ ok: false,
10863
+ reason: "delete-failed"
10864
+ } : {
10865
+ ok: true,
10866
+ content: null
10867
+ };
10060
10868
  } catch (error) {
10061
10869
  return {
10062
10870
  ok: false,
10063
10871
  reason: reasonOf(error)
10064
10872
  };
10065
10873
  }
10066
- return await fs.stat(target, signal).catch(() => void 0) ? {
10067
- ok: false,
10068
- reason: "delete-failed"
10069
- } : {
10070
- ok: true,
10071
- content: null
10072
- };
10073
- }
10074
- /**
10075
- * @author ddj 2026年10月29号
10076
- * @description: 判断宿主路径是否位于工作区内,Windows 下按大小写不敏感比较。
10077
- * @param cwd 工作区
10078
- * @param absolute 目标绝对路径
10079
- * @returns 是否在工作区内
10080
- */
10081
- function insideWorkspace(cwd, absolute) {
10082
- const root = resolve(cwd);
10083
- const candidate = resolve(absolute);
10084
- const normalize = (value) => process.platform === "win32" ? value.toLowerCase() : value;
10085
- const rootKey = normalize(root).endsWith(sep) ? normalize(root) : normalize(root) + sep;
10086
- return normalize(candidate).startsWith(rootKey);
10087
10874
  }
10088
10875
  /**
10089
10876
  * @author ddj 2026年10月29号
@@ -10110,197 +10897,6 @@ function reasonOf(error) {
10110
10897
  return typeof code === "string" ? code : String(error);
10111
10898
  }
10112
10899
  //#endregion
10113
- //#region src/shared/pathKey.ts
10114
- /**
10115
- * 路径比较口径:Host 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
10116
- * @author ddj 2026年10月30号
10117
- */
10118
- /** 远端仓库身份:`scheme://...` 区分大小写,绝不能折叠。 */
10119
- const REMOTE = /^[a-z]+:\/\//i;
10120
- /** 盘符绝对路径:大小写不敏感与宿主平台无关,两边都必须折叠。 */
10121
- const DRIVE = /^[a-z]:\//i;
10122
- /**
10123
- * @author ddj 2026年10月30号
10124
- * @description: 统一分隔符为 `/`,其余字符原样保留。
10125
- * @param path 原始路径
10126
- * @returns 反斜杠已归一化的路径
10127
- */
10128
- function unifiedPath(path) {
10129
- return path.replace(/\\/g, "/");
10130
- }
10131
- /**
10132
- * @author ddj 2026年10月30号
10133
- * @description: 生成路径比较键。盘符路径恒折叠大小写;其余路径是否折叠由调用方按运行平台决定
10134
- * (Host 在 Windows 上折叠,客户端不在宿主进程里,取 false)。
10135
- * @param path 原始路径
10136
- * @param foldCase 非盘符路径是否折叠大小写
10137
- * @returns 比较键
10138
- */
10139
- function pathKeyFor(path, foldCase) {
10140
- const unified = unifiedPath(path);
10141
- if (REMOTE.test(unified)) return unified;
10142
- if (DRIVE.test(unified)) return unified.toLowerCase();
10143
- return foldCase ? unified.toLowerCase() : unified;
10144
- }
10145
- //#endregion
10146
- //#region src/shared/pendingReview.ts
10147
- /** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
10148
- const PENDING_PREFIX = "pending:";
10149
- //#endregion
10150
- //#region src/pendingCompat.ts
10151
- /** 进程内"已被待审接管"的路径集合;旧写入口守卫只读它,绝不碰数据库。 */
10152
- const coveredPaths = /* @__PURE__ */ new Set();
10153
- /**
10154
- * @author ddj 2026年10月29号
10155
- * @description: 由条目 id 派生稳定序号,保证轮询期间面板锚点不跳。
10156
- * @param id 待审条目身份
10157
- * @returns 正整数序号
10158
- */
10159
- function stableSeq(id) {
10160
- let hash = 2166136261;
10161
- for (let index = 0; index < id.length; index++) {
10162
- hash ^= id.charCodeAt(index);
10163
- hash = Math.imul(hash, 16777619);
10164
- }
10165
- return Math.abs(hash) % 2147483647 + 1;
10166
- }
10167
- /**
10168
- * @author ddj 2026年10月29号
10169
- * @description: Host 侧的路径比较口径:远端身份保留大小写,其余按运行平台折叠。
10170
- * @param path 路径
10171
- * @returns 比较键
10172
- */
10173
- function pathKey$1(path) {
10174
- return pathKeyFor(path, process.platform === "win32");
10175
- }
10176
- /**
10177
- * @author ddj 2026年10月30号
10178
- * @description: 判断两个路径是否指同一文件:比较键相同,或一方是另一方的工作区相对写法。
10179
- * 面板传下来的路径可能是相对路径,而条目里存的是绝对路径。
10180
- * @param left 路径
10181
- * @param right 路径
10182
- * @returns 是否同一文件
10183
- */
10184
- function pendingSamePath(left, right) {
10185
- const a = pathKey$1(left);
10186
- const b = pathKey$1(right);
10187
- if (!a || !b) return false;
10188
- return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
10189
- }
10190
- /**
10191
- * @author ddj 2026年10月30号
10192
- * @description: 列表里的条目 id 带待审前缀用于区分通道,内核按原始身份存取,这里统一还原。
10193
- * @param id 请求里的条目 id
10194
- * @returns 待审内核使用的原始 id
10195
- */
10196
- function pendingRawId(id) {
10197
- return id.startsWith("pending:") ? id.slice(8) : id;
10198
- }
10199
- /**
10200
- * @author ddj 2026年10月30号
10201
- * @description: 列表显示用的短路径:工作区内显示相对路径,区外保留原文。
10202
- * @param cwd 工作区
10203
- * @param path 条目路径
10204
- * @returns 显示用路径
10205
- */
10206
- function pendingDisplay(cwd, path) {
10207
- const root = cwd.replace(/[\\/]+$/, "");
10208
- const unified = path.replace(/\\/g, "/");
10209
- const base = root.replace(/\\/g, "/");
10210
- if (base && (unified === base || unified.toLowerCase().startsWith(base.toLowerCase() + "/"))) return unified.slice(base.length + 1);
10211
- return path;
10212
- }
10213
- /**
10214
- * @author ddj 2026年10月29号
10215
- * @description: 待审条目转面板列表项。
10216
- * @param item 待审条目
10217
- * @param cwd 工作区,用于显示相对路径;缺省显示原始路径
10218
- * @returns 面板列表项
10219
- */
10220
- function pendingItemOf(item, cwd) {
10221
- return {
10222
- id: PENDING_PREFIX + item.id,
10223
- sessionId: item.sessionId,
10224
- turn: 0,
10225
- seq: stableSeq(item.id),
10226
- path: item.path,
10227
- currentPath: item.path,
10228
- display: cwd ? pendingDisplay(cwd, item.path) : item.path,
10229
- added: item.added,
10230
- deleted: item.deleted,
10231
- kind: item.oversized ? "oversized" : "text",
10232
- decision: item.settled ? "accepted" : "pending",
10233
- revision: item.revision,
10234
- current: true
10235
- };
10236
- }
10237
- /**
10238
- * @author ddj 2026年10月29号
10239
- * @description: 待审详情转面板详情;Revert 映射到面板的 Undo 能力位。
10240
- * @param detail 待审详情
10241
- * @param capturedAt 采集时间文案
10242
- * @returns 面板详情
10243
- */
10244
- function pendingDetailOf(detail, capturedAt, cwd) {
10245
- return {
10246
- item: pendingItemOf(detail.item, cwd),
10247
- diff: detail.diff,
10248
- capturedAt,
10249
- blocks: detail.blocks,
10250
- capability: {
10251
- keep: detail.capability.keep,
10252
- undo: detail.capability.revert,
10253
- reason: detail.capability.reason
10254
- }
10255
- };
10256
- }
10257
- /**
10258
- * @author ddj 2026年10月29号
10259
- * @description: 记住/忘记"该路径由待审投影接管"。进程内集合,绝不在旧写入口上查库(避免撞上写事务锁)。
10260
- * @param cwd 工作区
10261
- * @param path 路径
10262
- * @param covered 是否接管
10263
- * @returns 无
10264
- */
10265
- function trackPendingCover(cwd, path, covered) {
10266
- const key = `${pathKey$1(cwd)}|${pathKey$1(path)}`;
10267
- if (covered) coveredPaths.add(key);
10268
- else coveredPaths.delete(key);
10269
- }
10270
- /**
10271
- * @author ddj 2026年10月29号
10272
- * @description: 判断某路径是否已由待审投影接管;仅读进程内集合,不产生任何 IO。
10273
- * @param cwd 工作区
10274
- * @param path 请求路径
10275
- * @returns 是否已被接管
10276
- */
10277
- function pendingCovers(cwd, path) {
10278
- return coveredPaths.has(`${pathKey$1(cwd)}|${pathKey$1(path)}`);
10279
- }
10280
- /**
10281
- * @author ddj 2026年10月29号
10282
- * @description: 合并官方页与待审页:待审覆盖的路径从官方结果中剔除,其余原样保留。
10283
- * @param official 官方页
10284
- * @param pending 待审页
10285
- * @param cwd 工作区,用于把待审行显示成相对路径
10286
- * @returns 合并后的页
10287
- */
10288
- function mergedPage(official, pending, cwd) {
10289
- const covered = /* @__PURE__ */ new Set();
10290
- for (const item of pending.items) {
10291
- covered.add(pathKey$1(item.path));
10292
- covered.add(pathKey$1(item.id));
10293
- }
10294
- const kept = official.items.filter((item) => !covered.has(pathKey$1(item.currentPath)) && !covered.has(pathKey$1(item.path)));
10295
- return {
10296
- items: [...pending.items.map((item) => pendingItemOf(item, cwd)), ...kept],
10297
- pending: pending.pending + kept.filter((item) => item.decision === "pending").length,
10298
- nextCursor: official.nextCursor,
10299
- gaps: official.gaps,
10300
- ...official.overview ? { overview: official.overview } : {}
10301
- };
10302
- }
10303
- //#endregion
10304
10900
  //#region src/search/query.ts
10305
10901
  /**
10306
10902
  * 规范化用户搜索文本。
@@ -16040,6 +16636,7 @@ function finishUndo(cwd, request) {
16040
16636
  }
16041
16637
  //#endregion
16042
16638
  //#region src/reviewRpc.ts
16639
+ /** Session-scoped official review RPC. @author ddj 2026年10月26号 */
16043
16640
  /** 批量写入快照的硬上限:待审积压超过它说明状态异常,宁可拒绝也不做无界写循环。 */
16044
16641
  const BATCH_LIMIT = 2e3;
16045
16642
  /** 冷端 Undo 说明:Undo 会校验证据并写回源文件,只能在持有该会话的那一端执行。 */
@@ -16195,7 +16792,7 @@ function reviewHandlers(ctx, registry) {
16195
16792
  */
16196
16793
  function pendingPageOf(cwd, sessionId) {
16197
16794
  try {
16198
- const page = loadPage(cwd, sessionId, addedFiles(cwd, sessionId));
16795
+ const page = loadPage(cwd, sessionId);
16199
16796
  return page.items.some((item) => item.settled === true) ? {
16200
16797
  ...page,
16201
16798
  items: page.items.filter((item) => item.settled !== true)
@@ -16209,6 +16806,99 @@ function reviewHandlers(ctx, registry) {
16209
16806
  };
16210
16807
  }
16211
16808
  }
16809
+ /**
16810
+ * @private
16811
+ * @author ddj 2026年11月03号
16812
+ * @description: 按批次与条目派生幂等操作号:同一批次重放复用同一结果,不重复推进 revision。
16813
+ * @param batchId 客户端批次身份
16814
+ * @param id 条目身份
16815
+ * @param seq 事件序号
16816
+ * @returns 64 位 hex 操作号
16817
+ */
16818
+ function operationOf(batchId, id, seq) {
16819
+ return createHash("sha256").update(`${batchId}|${id}|${seq}`).digest("hex");
16820
+ }
16821
+ /**
16822
+ * @private
16823
+ * @author ddj 2026年11月03号
16824
+ * @description: 取条目展示名,缺失时退回身份本身。
16825
+ * @param items 本批条目
16826
+ * @param id 条目身份
16827
+ * @returns 展示名
16828
+ */
16829
+ function displayOf(items, id) {
16830
+ return items.find((item) => item.id === id)?.display ?? id;
16831
+ }
16832
+ /**
16833
+ * @private
16834
+ * @author ddj 2026年11月03号
16835
+ * @description: 官方段的批量保留。段级异常只记进 result.error,不取消待审段。
16836
+ * @param cwd 工作区
16837
+ * @param batchId 批次身份
16838
+ * @param items 官方条目
16839
+ * @param result 汇总结果
16840
+ * @returns 无
16841
+ */
16842
+ function keepOfficialStage(cwd, batchId, items, result) {
16843
+ try {
16844
+ const outcome = keepOfficialAll(cwd, items.map((item) => ({
16845
+ id: item.id,
16846
+ seq: item.seq,
16847
+ revision: item.revision,
16848
+ operationId: operationOf(batchId, item.id, item.seq)
16849
+ })));
16850
+ result.completed += outcome.paths.length;
16851
+ result.paths.push(...outcome.paths);
16852
+ for (const skip of outcome.skipped) result.skipped.push({
16853
+ id: skip.id,
16854
+ display: displayOf(items, skip.id),
16855
+ reason: skip.reason
16856
+ });
16857
+ } catch (error) {
16858
+ const message = error instanceof Error ? error.message : String(error);
16859
+ log.warn(`[review] keepAll 官方段失败: ${message}`);
16860
+ if (!result.error) result.error = message;
16861
+ }
16862
+ }
16863
+ /**
16864
+ * @private
16865
+ * @author ddj 2026年11月03号
16866
+ * @description: 待审段的批量保留。冷端只跳过待审条目(判定需要活跃会话),官方条目不受影响。
16867
+ * @param scope 已解析作用域
16868
+ * @param batchId 批次身份
16869
+ * @param items 待审条目(带 pending 前缀)
16870
+ * @param result 汇总结果
16871
+ * @returns 无
16872
+ */
16873
+ async function keepPendingStage(scope, batchId, items, result) {
16874
+ if (!scope.live) {
16875
+ for (const item of items) result.skipped.push({
16876
+ id: item.id,
16877
+ display: item.display,
16878
+ reason: "待审条目需要在会话活跃的一端判定"
16879
+ });
16880
+ return;
16881
+ }
16882
+ const targets = items.map((item) => ({
16883
+ id: item.id.slice(8),
16884
+ display: item.display,
16885
+ operationId: operationOf(batchId, item.id, item.seq)
16886
+ }));
16887
+ try {
16888
+ const outcome = await keepAll(scope.cwd, targets);
16889
+ result.completed += outcome.kept.length;
16890
+ for (const entry of outcome.kept) result.paths.push(entry.path);
16891
+ for (const skip of outcome.skipped) result.skipped.push({
16892
+ id: PENDING_PREFIX + skip.id,
16893
+ display: skip.display,
16894
+ reason: skip.reason
16895
+ });
16896
+ } catch (error) {
16897
+ const message = error instanceof Error ? error.message : String(error);
16898
+ log.warn(`[review] keepAll 待审段失败: ${message}`);
16899
+ if (!result.error) result.error = message;
16900
+ }
16901
+ }
16212
16902
  return {
16213
16903
  "edrv.review.list": async (args) => {
16214
16904
  const { cwd } = await scopeOf(args.sessionId);
@@ -16242,8 +16932,7 @@ function reviewHandlers(ctx, registry) {
16242
16932
  const scope = await scopeOf(args.sessionId);
16243
16933
  const cwd = scope.cwd;
16244
16934
  if (args.id.startsWith("pending:")) {
16245
- const entryId = args.id.slice(8);
16246
- const detail = loadDetail(cwd, entryId, addedFiles(cwd, args.sessionId).has(entryId));
16935
+ const detail = loadDetail(cwd, args.id.slice(8));
16247
16936
  if (!detail) return {
16248
16937
  ok: false,
16249
16938
  error: "待审条目不存在"
@@ -16291,7 +16980,7 @@ function reviewHandlers(ctx, registry) {
16291
16980
  decision: args.decision,
16292
16981
  scope: args.scope === "hunk" ? "block" : "file",
16293
16982
  ...args.blockId === void 0 ? {} : { blockId: args.blockId }
16294
- });
16983
+ }, void 0, policyOf(ctx, sessionOf(ctx, args.sessionId)));
16295
16984
  if (!result.ok) return {
16296
16985
  ok: false,
16297
16986
  error: result.reason
@@ -16325,6 +17014,43 @@ function reviewHandlers(ctx, registry) {
16325
17014
  error: message
16326
17015
  };
16327
17016
  }
17017
+ },
17018
+ "edrv.review.keepAll": async (args) => {
17019
+ try {
17020
+ const scope = await scopeOf(args.sessionId);
17021
+ const items = Array.isArray(args.items) ? args.items : [];
17022
+ if (!items.length) return {
17023
+ ok: false,
17024
+ error: "没有可保留的待审记录"
17025
+ };
17026
+ if (items.length > BATCH_LIMIT) return {
17027
+ ok: false,
17028
+ error: `待审记录过多(${items.length} 条,上限 ${BATCH_LIMIT}),请先分批处理后重试`
17029
+ };
17030
+ const result = {
17031
+ completed: 0,
17032
+ total: items.length,
17033
+ paths: [],
17034
+ skipped: [],
17035
+ error: ""
17036
+ };
17037
+ const official = items.filter((item) => !item.id.startsWith(PENDING_PREFIX));
17038
+ const pending = items.filter((item) => item.id.startsWith(PENDING_PREFIX));
17039
+ if (official.length) keepOfficialStage(scope.cwd, args.batchId, official, result);
17040
+ if (pending.length) await keepPendingStage(scope, args.batchId, pending, result);
17041
+ log.info(`[review] keepAll live=${scope.live ? 1 : 0} total=${result.total} kept=${result.completed} skipped=${result.skipped.length}`);
17042
+ return {
17043
+ ok: true,
17044
+ result
17045
+ };
17046
+ } catch (error) {
17047
+ const message = error instanceof Error ? error.message : String(error);
17048
+ log.warn(`[review] keepAll 失败: ${message}`);
17049
+ return {
17050
+ ok: false,
17051
+ error: message
17052
+ };
17053
+ }
16328
17054
  }
16329
17055
  };
16330
17056
  }
@@ -16392,17 +17118,15 @@ function readHandlers(kit) {
16392
17118
  return {
16393
17119
  "edrv.pending.list": async (args) => {
16394
17120
  const cwd = await kit.read(args.sessionId);
16395
- const page = loadPage(cwd, args.sessionId, addedFiles(cwd, args.sessionId));
16396
- for (const item of page.items) trackPendingCover(cwd, item.path, true);
17121
+ const page = loadPage(cwd, args.sessionId);
17122
+ setPendingCovers(cwd, args.sessionId, page.items.map((item) => item.path));
16397
17123
  return {
16398
17124
  ok: true,
16399
17125
  page
16400
17126
  };
16401
17127
  },
16402
17128
  "edrv.pending.detail": async (args) => {
16403
- const cwd = await kit.read(args.sessionId);
16404
- const rawId = pendingRawId(args.id);
16405
- const detail = loadDetail(cwd, rawId, addedFiles(cwd, args.sessionId).has(rawId));
17129
+ const detail = loadDetail(await kit.read(args.sessionId), pendingRawId(args.id));
16406
17130
  return detail ? {
16407
17131
  ok: true,
16408
17132
  detail
@@ -16455,7 +17179,7 @@ function decideHandlers(kit) {
16455
17179
  const id = pendingRawId(args.id);
16456
17180
  const path = loadEntries(cwd).find((stored) => stored.entry.id === id)?.entry.path;
16457
17181
  const removed = removeEntry(cwd, id, args.revision);
16458
- if (removed && path) trackPendingCover(cwd, path, false);
17182
+ if (removed && path) trackPendingCover(cwd, path, false, args.sessionId);
16459
17183
  return {
16460
17184
  ok: true,
16461
17185
  removed
@@ -16512,7 +17236,8 @@ function refreshHandlers(kit) {
16512
17236
  includeUntracked: args.includeUntracked,
16513
17237
  path: args.path
16514
17238
  });
16515
- for (const item of loadPage(cwd, args.sessionId).items) trackPendingCover(cwd, item.path, true);
17239
+ const page = loadPage(cwd, args.sessionId);
17240
+ setPendingCovers(cwd, args.sessionId, page.items.map((item) => item.path));
16516
17241
  return {
16517
17242
  ok: true,
16518
17243
  imported: result.imported,
@@ -16795,15 +17520,16 @@ function recView(record, ws = null) {
16795
17520
  };
16796
17521
  }
16797
17522
  /**
16798
- * 旧写入口守卫:路径已由待审累计基线接管时一律拒绝,避免两套权威同时写同一文件。
16799
- * @author ddj 2026年10月29号
16800
- * @param cwd 工作区
17523
+ * 旧写入口守卫:本会话可见的待审累计基线接管了该路径时一律拒绝,避免两套权威同时写同一文件。
17524
+ * 只按会话可见范围判定:别的会话在管、本会话看不见的条目不能拦人,否则用户无从处理。
17525
+ * @author ddj 2026年11月3号
17526
+ * @param sc 会话与工作区
16801
17527
  * @param path 请求涉及的路径(可为空表示无法定位)
16802
17528
  * @returns 拒绝原因,未接管时为 null
16803
17529
  */
16804
- function legacyBlocked(cwd, path) {
17530
+ function legacyBlocked(sc, path) {
16805
17531
  if (!path) return null;
16806
- if (!pendingCovers(cwd, path)) return null;
17532
+ if (!pendingCovers(sc.cwd, path, sc.session.id)) return null;
16807
17533
  return "该文件已由待审累计基线接管,请使用面板 Keep/Revert(edrv.review.decide)";
16808
17534
  }
16809
17535
  /** 会话/工作区公共前置:返回 {session,cwd} 或错误文案。 */
@@ -17309,6 +18035,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
17309
18035
  const staleStart = performance.now();
17310
18036
  await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
17311
18037
  await archiveCanceled(ctx, sc.session, sc.cwd, bucket);
18038
+ await retireCovered(ctx, sc.session, sc.cwd, bucket);
17312
18039
  staleMs = performance.now() - staleStart;
17313
18040
  }
17314
18041
  }
@@ -17380,7 +18107,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
17380
18107
  error: sc.err
17381
18108
  };
17382
18109
  const bucket = await bucketOf(registry, ctx, sc.cwd);
17383
- const blocked = legacyBlocked(sc.cwd, bucket.get(args.callId)?.path);
18110
+ const blocked = legacyBlocked(sc, bucket.get(args.callId)?.path);
17384
18111
  if (blocked) return {
17385
18112
  ok: false,
17386
18113
  error: blocked
@@ -17407,7 +18134,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
17407
18134
  error: sc.err
17408
18135
  };
17409
18136
  const bucket = await bucketOf(registry, ctx, sc.cwd);
17410
- const blocked = legacyBlocked(sc.cwd, bucket.get(args.callId)?.path);
18137
+ const blocked = legacyBlocked(sc, bucket.get(args.callId)?.path);
17411
18138
  if (blocked) return {
17412
18139
  ok: false,
17413
18140
  error: blocked
@@ -17433,7 +18160,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
17433
18160
  ok: false,
17434
18161
  error: sc.err
17435
18162
  };
17436
- const blocked = legacyBlocked(sc.cwd, args.path);
18163
+ const blocked = legacyBlocked(sc, args.path);
17437
18164
  if (blocked) return {
17438
18165
  ok: false,
17439
18166
  error: blocked
@@ -17463,7 +18190,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
17463
18190
  ok: false,
17464
18191
  error: sc.err
17465
18192
  };
17466
- const blocked = legacyBlocked(sc.cwd, missingPath(args.token));
18193
+ const blocked = legacyBlocked(sc, missingPath(args.token));
17467
18194
  if (blocked) return {
17468
18195
  ok: false,
17469
18196
  error: blocked
@@ -17489,7 +18216,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
17489
18216
  };
17490
18217
  const bucket = await bucketOf(registry, ctx, sc.cwd);
17491
18218
  const items = Array.isArray(args.items) ? args.items : [];
17492
- const blocked = items.map((item) => legacyBlocked(sc.cwd, bucket.get(item.callId)?.path)).find((reason) => reason !== null);
18219
+ const blocked = items.map((item) => legacyBlocked(sc, bucket.get(item.callId)?.path)).find((reason) => reason !== null);
17493
18220
  if (blocked) return {
17494
18221
  ok: false,
17495
18222
  error: blocked
@@ -29579,7 +30306,7 @@ function attachRuntime(ctx, runtime) {
29579
30306
  });
29580
30307
  const pending = installPending(ctx, (write) => {
29581
30308
  try {
29582
- trackPendingCover(write.cwd, write.path, true);
30309
+ trackPendingCover(write.cwd, write.path, true, write.sessionId);
29583
30310
  } catch {}
29584
30311
  try {
29585
30312
  recordWrite(write);