dsh-vscode-mode 0.26.0 → 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.
package/lib/index.js CHANGED
@@ -2485,11 +2485,9 @@ const PENDING_PREFIX = "pending:";
2485
2485
  //#region src/pendingCompat.ts
2486
2486
  /**
2487
2487
  * 进程内"已被待审接管"的路径集合;旧写入口守卫只读它,绝不碰数据库。
2488
- * 按会话登记:待审列表按会话可见,守卫必须同口径,否则会出现"拦得住、看不见"的死锁。
2489
- * 无会话归属的登记落在 workspace 集合(兼容只有工作区+路径的旧用法)。
2488
+ * 按工作区登记,来源会话仅作追溯;任一对话重建或注销都会更新同一份集合。
2490
2489
  */
2491
- const coveredBySession = /* @__PURE__ */ new Map();
2492
- const coveredWorkspace = /* @__PURE__ */ new Set();
2490
+ const coveredWorkspace = /* @__PURE__ */ new Map();
2493
2491
  /**
2494
2492
  * @author ddj 2026年10月29号
2495
2493
  * @description: 由条目 id 派生稳定序号,保证轮询期间面板锚点不跳。
@@ -2605,16 +2603,18 @@ function coverKey(cwd, path) {
2605
2603
  return `${pathKey$1(cwd)}|${pathKey$1(path)}`;
2606
2604
  }
2607
2605
  /**
2608
- * @author ddj 2026年11月3号
2609
- * @description: 取某会话的覆盖集合;首次登记时就地建集合,避免调用方判空。
2610
- * @param sessionId 会话 id
2611
- * @returns 该会话的覆盖键集合
2612
- */
2613
- function sessionCovers(sessionId) {
2614
- const existing = coveredBySession.get(sessionId);
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
2615
  if (existing) return existing;
2616
- const created = /* @__PURE__ */ new Set();
2617
- coveredBySession.set(sessionId, created);
2616
+ const created = /* @__PURE__ */ new Map();
2617
+ coveredWorkspace.set(workspaceKey, created);
2618
2618
  return created;
2619
2619
  }
2620
2620
  /**
@@ -2628,27 +2628,27 @@ function sessionCovers(sessionId) {
2628
2628
  */
2629
2629
  function trackPendingCover(cwd, path, covered, sessionId) {
2630
2630
  const key = coverKey(cwd, path);
2631
- const target = sessionId ? sessionCovers(sessionId) : coveredWorkspace;
2632
- if (covered) target.add(key);
2631
+ const target = workspaceCovers(cwd);
2632
+ if (covered) target.set(key, sessionId ?? null);
2633
2633
  else target.delete(key);
2634
2634
  }
2635
2635
  /**
2636
2636
  * @author ddj 2026年11月3号
2637
- * @description: 用一次列表结果整体重建某会话的接管集合:条目被移除或结案后自动失效,不再残留登记。
2637
+ * @description: 用完整列表重建工作区接管集合,清除任何对话留下的过期登记。
2638
2638
  * @param cwd 工作区
2639
2639
  * @param sessionId 会话 id
2640
2640
  * @param paths 本会话当前可见的待审条目路径
2641
2641
  * @returns 无
2642
2642
  */
2643
2643
  function setPendingCovers(cwd, sessionId, paths) {
2644
- const next = /* @__PURE__ */ new Set();
2645
- for (const path of paths) if (path) next.add(coverKey(cwd, path));
2646
- coveredBySession.set(sessionId, next);
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
2647
  }
2648
2648
  /**
2649
2649
  * @author ddj 2026年10月29号
2650
2650
  * @description: 判断某路径是否已由待审投影接管;仅读进程内集合,不产生任何 IO。
2651
- * 给 sessionId 时按该会话的可见范围判定(列表看不到的接管不拦旧入口),缺省按全工作区并集判定。
2651
+ * 所有对话按同一工作区集合判定,兼容参数不影响可见范围。
2652
2652
  * @param cwd 工作区
2653
2653
  * @param path 请求路径
2654
2654
  * @param sessionId 会话 id;缺省按并集判定
@@ -2656,10 +2656,7 @@ function setPendingCovers(cwd, sessionId, paths) {
2656
2656
  */
2657
2657
  function pendingCovers(cwd, path, sessionId) {
2658
2658
  const key = coverKey(cwd, path);
2659
- if (coveredWorkspace.has(key)) return true;
2660
- if (sessionId) return coveredBySession.get(sessionId)?.has(key) === true;
2661
- for (const set of coveredBySession.values()) if (set.has(key)) return true;
2662
- return false;
2659
+ return coveredWorkspace.get(pathKey$1(cwd))?.has(key) === true;
2663
2660
  }
2664
2661
  /**
2665
2662
  * @author ddj 2026年10月29号
@@ -8780,6 +8777,67 @@ function evidenceText(evidence) {
8780
8777
  };
8781
8778
  }
8782
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
8783
8841
  //#region src/reviewCapture.ts
8784
8842
  /** Independent byte evidence; official diff remains the only display authority. @author ddj 2026年10月27号 */
8785
8843
  /** @public @author ddj 2026年10月27号 @param ctx Host lifecycle owner. @returns Scoped byte recorder; failures disable undo evidence only, never tool execution. */
@@ -8787,12 +8845,15 @@ function installEvidence(ctx) {
8787
8845
  const chains = /* @__PURE__ */ new Map();
8788
8846
  const staged = /* @__PURE__ */ new Map();
8789
8847
  let stopped = false;
8848
+ const disposed = /* @__PURE__ */ new Set();
8790
8849
  ctx.on("tools/execute", aroundTool);
8791
8850
  ctx.on("session/event", onEvent);
8792
8851
  ctx.on("session/disposed", onDisposed);
8793
8852
  /** @private @author ddj 2026年10月27号 @param session Disposed owner. Drops optional in-memory evidence, never persisted official facts. */
8794
8853
  function onDisposed(session) {
8854
+ disposed.add(session.id);
8795
8855
  chains.delete(session.id);
8856
+ for (const key of staged.keys()) if (JSON.parse(key)[0] === session.id) staged.delete(key);
8796
8857
  }
8797
8858
  /** @private @author ddj 2026年10月27号 @returns Current bounded capture footprint, excluding transient filesystem reads. */
8798
8859
  function footprint() {
@@ -8803,48 +8864,94 @@ function installEvidence(ctx) {
8803
8864
  /** @private @author ddj 2026年10月27号 @param exec Allowed tool dispatch. @param next Original dispatch. @returns Original result unchanged, regardless of capture failure. */
8804
8865
  async function aroundTool(exec, next) {
8805
8866
  const session = exec.agent?.session;
8806
- const path = exec.arguments?.file_path;
8807
- if (stopped || !session?.id || !["edit", "write"].includes(exec.name) || typeof path !== "string") return next();
8808
- let fs;
8809
- let chain;
8810
- let target;
8867
+ if (stopped || !session?.id) return next();
8868
+ const captures = [];
8811
8869
  try {
8812
- if (chains.size >= 16 && !chains.has(session.id) || footprint() >= 33554432) return next();
8813
- fs = ctx.get("fs");
8814
- target = await resolveTarget(ctx, session, path);
8815
- const identity = fs.processPath(target);
8816
- const before = await readRaw(fs, target, exec.signal);
8817
- let files = chains.get(session.id);
8818
- if (!files) {
8819
- files = /* @__PURE__ */ new Map();
8820
- chains.set(session.id, files);
8821
- }
8822
- if (files.size >= 64 && !files.has(identity)) return next();
8823
- chain = files.get(identity);
8824
- if (!chain) {
8825
- chain = {
8826
- before,
8827
- after: before,
8828
- busy: false,
8829
- invalid: false
8830
- };
8831
- files.set(identity, chain);
8832
- }
8833
- if (chain.busy || chain.after.hash !== before.hash) chain.invalid = true;
8834
- 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 {}
8835
8877
  } catch {}
8836
8878
  try {
8837
8879
  return await next();
8838
8880
  } finally {
8839
- if (chain) {
8881
+ for (const capture of captures) {
8840
8882
  try {
8841
- 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
+ }
8842
8892
  } catch {
8843
- chain.invalid = true;
8893
+ capture.chain.invalid = true;
8844
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;
8845
8930
  chain.busy = false;
8931
+ throw error;
8846
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;
8940
+ chain.busy = false;
8941
+ throw error;
8942
+ }
8943
+ }
8944
+ if (stopped || disposed.has(sessionId) || footprint() > 33554432) {
8945
+ chain.invalid = true;
8946
+ chain.busy = false;
8947
+ files.delete(identity);
8948
+ return null;
8847
8949
  }
8950
+ return {
8951
+ fs,
8952
+ target,
8953
+ chain
8954
+ };
8848
8955
  }
8849
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. */
8850
8957
  function onEvent(session, event) {
@@ -8908,10 +9015,34 @@ async function readRaw(fs, target, signal) {
8908
9015
  const WRITE_TOOLS = [
8909
9016
  "edit",
8910
9017
  "write",
8911
- "str_replace_editor"
9018
+ "str_replace_editor",
9019
+ "apply_patch"
8912
9020
  ];
8913
9021
  /** 单文件文本上限:超过即标注 oversized,不静默截断。 */
8914
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
+ }
8915
9046
  /**
8916
9047
  * @author ddj 2026年10月30号
8917
9048
  * @description: 安装写文件采集;读写失败只影响待审来源,绝不影响用户工具本身。
@@ -8922,12 +9053,18 @@ const MAX_TEXT = 1048576;
8922
9053
  function installPending(ctx, sink) {
8923
9054
  const state = {
8924
9055
  busy: /* @__PURE__ */ new Set(),
8925
- stopped: false
9056
+ stopped: false,
9057
+ disposed: /* @__PURE__ */ new Set()
8926
9058
  };
8927
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
+ });
8928
9064
  return { dispose() {
8929
9065
  state.stopped = true;
8930
9066
  state.busy.clear();
9067
+ captureGaps.clear();
8931
9068
  } };
8932
9069
  }
8933
9070
  /**
@@ -8938,27 +9075,32 @@ function installPending(ctx, sink) {
8938
9075
  * @param exec 工具分发上下文
8939
9076
  * @returns 采集计划或 null
8940
9077
  */
8941
- async function planOf(ctx, state, exec) {
8942
- const session = exec.agent?.session;
8943
- const requested = exec.arguments?.file_path ?? exec.arguments?.path;
8944
- if (state.stopped || !session?.id || typeof requested !== "string") return null;
8945
- if (!WRITE_TOOLS.includes(String(exec.name))) return null;
8946
- const cwd = cwdOf(session);
8947
- if (!cwd) return null;
8948
- const fs = ctx.get("fs");
8949
- 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 });
8950
9082
  const identity = fs.processPath(target);
8951
- if (typeof identity !== "string" || !identity || state.busy.has(identity)) return null;
8952
- const before = await readText(fs, target);
8953
- if (before !== null && before.length > MAX_TEXT) return null;
8954
- return {
8955
- cwd: String(cwd),
8956
- sessionId: String(session.id),
8957
- path: requested,
8958
- identity,
8959
- before,
8960
- session
8961
- };
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
+ }
8962
9104
  }
8963
9105
  /**
8964
9106
  * @author ddj 2026年10月30号
@@ -8971,63 +9113,87 @@ async function planOf(ctx, state, exec) {
8971
9113
  * @returns 原始工具结果
8972
9114
  */
8973
9115
  async function aroundTool(ctx, state, sink, exec, next) {
8974
- 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 = [];
8975
9126
  try {
8976
- 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
+ }
8977
9136
  } catch (error) {
8978
- 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));
8979
9139
  }
8980
- if (!plan) return next();
8981
- state.busy.add(plan.identity);
8982
9140
  try {
8983
- 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;
8984
9144
  } finally {
8985
- state.busy.delete(plan.identity);
8986
- try {
8987
- reportWrite(ctx, sink, plan);
9145
+ for (const plan of plans) try {
9146
+ await reportWrite(state, sink, plan);
8988
9147
  } catch (error) {
9148
+ if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, plan.path, error);
8989
9149
  log.debug("待审上报失败:" + String(error));
9150
+ } finally {
9151
+ state.busy.delete(plan.identity);
8990
9152
  }
8991
9153
  }
8992
9154
  }
8993
9155
  /**
8994
- * @author ddj 2026年10月29号
8995
- * @description: 写后读回并比较,内容一致视为无净变化不产生条目。
8996
- * @param ctx Host 生命周期
9156
+ * @author ddj 2026年11月04号
9157
+ * @description: 固定目标写后读回;内容一致不产生条目,卸载后不再上报。
9158
+ * @param state 采集生命周期状态
8997
9159
  * @param sink 已确认写入的接收方
8998
9160
  * @param plan 采集计划
8999
9161
  * @returns 无;读回失败只记日志
9000
9162
  */
9001
- function reportWrite(ctx, sink, plan) {
9002
- const fs = ctx.get("fs");
9003
- resolveTarget(ctx, plan.session, plan.path).then(async (target) => {
9004
- const after = await readText(fs, target);
9005
- if (after !== null && after.length > MAX_TEXT) return;
9006
- if (plan.before === after) return;
9007
- sink({
9008
- cwd: plan.cwd,
9009
- sessionId: plan.sessionId,
9010
- path: plan.path,
9011
- identity: plan.identity,
9012
- before: plan.before,
9013
- after,
9014
- now: Date.now()
9015
- });
9016
- }).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
+ });
9017
9182
  }
9018
9183
  /**
9019
9184
  * @author ddj 2026年10月29号
9020
- * @description: 读取全文;缺失或不可读按"无内容"处理,交由上层决定语义。
9185
+ * @description: 读取全文;只有明确缺失返回 null,不可读或超限错误保留失败语义。
9021
9186
  * @param fs 文件系统
9022
9187
  * @param target 目标
9023
9188
  * @returns 文本或 null
9024
9189
  */
9025
- async function readText(fs, target) {
9190
+ async function readText(fs, target, signal) {
9026
9191
  try {
9027
- 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");
9028
9194
  } catch (error) {
9029
9195
  const code = error.code;
9030
- if (code === "ENOENT" || code === "FS_NOT_FOUND" || code === "FS_TOO_LARGE") return null;
9196
+ if (code === "ENOENT" || code === "FS_NOT_FOUND") return null;
9031
9197
  throw error;
9032
9198
  }
9033
9199
  }
@@ -10155,18 +10321,39 @@ function itemOf(stored, baselineEmpty = false) {
10155
10321
  return item;
10156
10322
  }
10157
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
+ /**
10158
10346
  * @author ddj 2026年10月29号
10159
- * @description: 读一页待审条目,按更新时间升序;会话过滤走血缘集合。
10347
+ * @description: 读工作区全部待审条目,按更新时间升序;来源会话仅用于追溯。
10160
10348
  * @param cwd 工作区
10161
- * @param sessionId 当前会话
10162
- * @param wholeAdd 本次会话新建的文件身份集合,命中时基线置空
10349
+ * @param _sessionId 兼容旧调用签名,不再用于可见性过滤
10163
10350
  * @returns 列表
10164
10351
  */
10165
- function loadPage(cwd, sessionId, wholeAdd) {
10166
- const all = loadEntries(cwd);
10167
- const visible = sessionId ? all.filter((stored) => stored.entry.sessionIds.includes(sessionId)) : all;
10168
- const items = visible.map((stored) => itemOf(stored, wholeAdd?.has(stored.entry.id) === true));
10169
- 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);
10170
10357
  for (const stored of visible) if (stored.flags.oversized) gaps.push(`${stored.entry.path}: 文本超出上限,仅保留标记`);
10171
10358
  return {
10172
10359
  items,
@@ -10186,9 +10373,10 @@ function loadPage(cwd, sessionId, wholeAdd) {
10186
10373
  function loadDetail(cwd, id, baselineEmpty = false) {
10187
10374
  const stored = loadEntries(cwd).find((item) => item.entry.id === id);
10188
10375
  if (!stored) return null;
10189
- const diff = diffOf(stored.entry, baselineEmpty);
10376
+ const emptyBase = baselineEmpty || emptyBaseOf(cwd, stored);
10377
+ const diff = diffOf(stored.entry, emptyBase);
10190
10378
  return {
10191
- item: itemOf(stored, baselineEmpty),
10379
+ item: itemOf(stored, emptyBase),
10192
10380
  diff,
10193
10381
  blocks: blockStatesOf(stored.entry),
10194
10382
  capability: capabilityOf(stored.entry, stored.flags)
@@ -10288,23 +10476,25 @@ function writerFor(fs, cwd, identity, expected, signal, policy, allowCreate = fa
10288
10476
  reason: "resolve-failed"
10289
10477
  };
10290
10478
  }
10291
- const current = await readText(fs, target).catch(() => void 0);
10292
- 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 {
10293
10483
  ok: false,
10294
- reason: "read-failed"
10484
+ reason: "diverged"
10295
10485
  };
10296
- if (!(allowCreate && current === null) && current !== expected) return {
10486
+ if (snapshot.content === null && !recreate) return {
10297
10487
  ok: false,
10298
- reason: "diverged"
10488
+ reason: "missing"
10489
+ };
10490
+ const intent = recreate ? { kind: "createIfAbsent" } : {
10491
+ kind: "replaceIfVersion",
10492
+ version: snapshot.version
10299
10493
  };
10300
- const currentStat = await fs.stat(target, signal).catch(() => void 0);
10301
10494
  try {
10302
- await fs.writeText(target, content ?? "", currentStat ? {
10303
- kind: "replaceIfVersion",
10304
- version: currentStat.version
10305
- } : void 0, signal, policy);
10495
+ await fs.writeText(target, content ?? "", intent, signal, policy);
10306
10496
  const readBack = await readText(fs, target);
10307
- if ((content ?? "") !== (readBack ?? "")) return {
10497
+ if (content !== readBack) return {
10308
10498
  ok: false,
10309
10499
  reason: "read-back-mismatch"
10310
10500
  };
@@ -10347,7 +10537,7 @@ async function applyDecide(fs, cwd, request, signal, policy) {
10347
10537
  blocks: beforeBlocks,
10348
10538
  now: Date.now(),
10349
10539
  write: (content) => {
10350
- 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);
10351
10541
  return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy)(content);
10352
10542
  }
10353
10543
  });
@@ -10575,7 +10765,7 @@ async function undoDecide(fs, cwd, move, signal, policy) {
10575
10765
  * @returns 写盘结果
10576
10766
  */
10577
10767
  async function writeBack(fs, cwd, stored, target, signal, policy) {
10578
- 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);
10579
10769
  return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy, true)(target.entry.newText);
10580
10770
  }
10581
10771
  /**
@@ -10594,65 +10784,93 @@ function parseSnapshot(text) {
10594
10784
  }
10595
10785
  }
10596
10786
  /**
10597
- * @author ddj 2026年10月29号
10598
- * @description: Revert 的删除分支:FileSystem 抽象没有删除能力,只有确认仍在工作区内才用宿主 unlink。
10599
- * @param fs 文件系统
10600
- * @param cwd 工作区
10601
- * @param identity 目标身份
10787
+ * @private
10788
+ * @author ddj 2026年11月04号
10789
+ * @description 版本—内容—版本读取,防止将新版本令牌与旧内容拼成写入依据。
10790
+ * @param fs 文件提供方
10791
+ * @param target 已解析目标
10602
10792
  * @param signal 取消信号
10603
- * @returns 删除结果
10793
+ * @returns 稳定快照或拒绝原因
10604
10794
  */
10605
- async function deleteTarget(fs, cwd, identity, signal) {
10606
- let target;
10795
+ async function stableRead(fs, target, signal) {
10607
10796
  try {
10608
- target = await fs.resolve(identity, {
10609
- cwd,
10610
- signal
10611
- });
10612
- } 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) {
10613
10822
  return {
10614
10823
  ok: false,
10615
- reason: "resolve-failed"
10824
+ reason: reasonOf(error)
10616
10825
  };
10617
10826
  }
10618
- if (!await fs.stat(target, signal).catch(() => void 0)) return {
10619
- ok: true,
10620
- content: null
10621
- };
10622
- const absolute = String(fs.processPath(target));
10623
- 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 {
10624
10842
  ok: false,
10625
- reason: "outside-workspace"
10843
+ reason: "delete-unavailable"
10626
10844
  };
10627
10845
  try {
10628
- 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
+ };
10629
10868
  } catch (error) {
10630
10869
  return {
10631
10870
  ok: false,
10632
10871
  reason: reasonOf(error)
10633
10872
  };
10634
10873
  }
10635
- return await fs.stat(target, signal).catch(() => void 0) ? {
10636
- ok: false,
10637
- reason: "delete-failed"
10638
- } : {
10639
- ok: true,
10640
- content: null
10641
- };
10642
- }
10643
- /**
10644
- * @author ddj 2026年10月29号
10645
- * @description: 判断宿主路径是否位于工作区内,Windows 下按大小写不敏感比较。
10646
- * @param cwd 工作区
10647
- * @param absolute 目标绝对路径
10648
- * @returns 是否在工作区内
10649
- */
10650
- function insideWorkspace(cwd, absolute) {
10651
- const root = resolve(cwd);
10652
- const candidate = resolve(absolute);
10653
- const normalize = (value) => process.platform === "win32" ? value.toLowerCase() : value;
10654
- const rootKey = normalize(root).endsWith(sep) ? normalize(root) : normalize(root) + sep;
10655
- return normalize(candidate).startsWith(rootKey);
10656
10874
  }
10657
10875
  /**
10658
10876
  * @author ddj 2026年10月29号
@@ -16574,7 +16792,7 @@ function reviewHandlers(ctx, registry) {
16574
16792
  */
16575
16793
  function pendingPageOf(cwd, sessionId) {
16576
16794
  try {
16577
- const page = loadPage(cwd, sessionId, addedFiles(cwd, sessionId));
16795
+ const page = loadPage(cwd, sessionId);
16578
16796
  return page.items.some((item) => item.settled === true) ? {
16579
16797
  ...page,
16580
16798
  items: page.items.filter((item) => item.settled !== true)
@@ -16714,8 +16932,7 @@ function reviewHandlers(ctx, registry) {
16714
16932
  const scope = await scopeOf(args.sessionId);
16715
16933
  const cwd = scope.cwd;
16716
16934
  if (args.id.startsWith("pending:")) {
16717
- const entryId = args.id.slice(8);
16718
- const detail = loadDetail(cwd, entryId, addedFiles(cwd, args.sessionId).has(entryId));
16935
+ const detail = loadDetail(cwd, args.id.slice(8));
16719
16936
  if (!detail) return {
16720
16937
  ok: false,
16721
16938
  error: "待审条目不存在"
@@ -16763,7 +16980,7 @@ function reviewHandlers(ctx, registry) {
16763
16980
  decision: args.decision,
16764
16981
  scope: args.scope === "hunk" ? "block" : "file",
16765
16982
  ...args.blockId === void 0 ? {} : { blockId: args.blockId }
16766
- });
16983
+ }, void 0, policyOf(ctx, sessionOf(ctx, args.sessionId)));
16767
16984
  if (!result.ok) return {
16768
16985
  ok: false,
16769
16986
  error: result.reason
@@ -16901,7 +17118,7 @@ function readHandlers(kit) {
16901
17118
  return {
16902
17119
  "edrv.pending.list": async (args) => {
16903
17120
  const cwd = await kit.read(args.sessionId);
16904
- const page = loadPage(cwd, args.sessionId, addedFiles(cwd, args.sessionId));
17121
+ const page = loadPage(cwd, args.sessionId);
16905
17122
  setPendingCovers(cwd, args.sessionId, page.items.map((item) => item.path));
16906
17123
  return {
16907
17124
  ok: true,
@@ -16909,9 +17126,7 @@ function readHandlers(kit) {
16909
17126
  };
16910
17127
  },
16911
17128
  "edrv.pending.detail": async (args) => {
16912
- const cwd = await kit.read(args.sessionId);
16913
- const rawId = pendingRawId(args.id);
16914
- const detail = loadDetail(cwd, rawId, addedFiles(cwd, args.sessionId).has(rawId));
17129
+ const detail = loadDetail(await kit.read(args.sessionId), pendingRawId(args.id));
16915
17130
  return detail ? {
16916
17131
  ok: true,
16917
17132
  detail