better-dsh 0.2.2-c → 0.2.3-c

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 (63) hide show
  1. package/docs/50_test-reports/2026-09-06-write/345/267/245/345/205/267sandbox/345/215/207/347/272/247/351/200/217/344/274/240bug/345/244/215/345/217/221/345/217/212/346/214/202/350/265/267-/344/272/213/344/273/266/346/212/245/345/221/212.md +133 -0
  2. package/docs/50_test-reports/2026-09-08-hashline-edit-E_RANGE_UNVERIFIED/350/267/250/350/275/256/344/274/232/350/257/235/351/224/256/345/244/261/346/225/210-/350/257/212/346/226/255/346/212/245/345/221/212.md +226 -0
  3. package/docs/50_test-reports/upstream-dsh-0.1.3-alpha.2-local-test-report.md +44 -0
  4. package/docs/50_test-reports/upstream-dsh-0.1.3-alpha.2-report.md +110 -0
  5. package/docs/50_test-reports/upstream-dsh-0.1.5-rc.2-local-test-report.md +79 -0
  6. package/docs/50_test-reports/v0.2.3b-hashline-content-locator/345/256/236/346/265/213/346/212/245/345/221/212.md +73 -0
  7. package/docs/upstream-dsh-0.1.5-rc.2-report.md +156 -0
  8. package/lib/client/index.js +60 -51
  9. package/lib/index.d.ts +2 -3
  10. package/lib/index.js +1203 -1278
  11. package/package.json +2 -2
  12. package/docs/50_test-reports/v0.1.8d_artifacts/README.md +0 -138
  13. package/docs/50_test-reports/v0.1.8d_artifacts/code-mode-repl-only.observation.md +0 -74
  14. package/docs/50_test-reports/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.jsonl +0 -3890
  15. package/docs/50_test-reports/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.w-sample-0435.jsonl +0 -544
  16. package/docs/50_test-reports/v0.1.8d_artifacts/functions.json +0 -592
  17. package/docs/50_test-reports/v0.1.8d_artifacts/skills-catalog.snapshot.md +0 -30
  18. package/docs/50_test-reports/v0.1.8d_artifacts/tools-sdk.output-schemas.json +0 -1236
  19. package/docs/50_test-reports/v0.1.8d_artifacts/tools-sdk.python.txt +0 -592
  20. package/docs/50_test-reports/v0.1.8d_artifacts/tools-sdk.typescript.txt +0 -516
  21. package/docs/50_test-reports/v0.1.8d_artifacts/wire-vs-transcription.diff.md +0 -54
  22. package/docs/50_test-reports/v0.2.0b_artifacts/f2probe/Cargo.lock +0 -7
  23. package/docs/50_test-reports/v0.2.0b_artifacts/f2probe/Cargo.toml +0 -6
  24. package/docs/50_test-reports/v0.2.0b_artifacts/f2probe/src/bin/messy.rs +0 -8
  25. package/docs/50_test-reports/v0.2.0b_artifacts/f2probe/src/main.rs +0 -4
  26. package/docs/50_test-reports/v0.2.0b_artifacts/hashline-probe.md +0 -5
  27. package/docs/50_test-reports/v0.2.0b_artifacts/slowprobe/Cargo.lock +0 -7
  28. package/docs/50_test-reports/v0.2.0b_artifacts/slowprobe/Cargo.toml +0 -7
  29. package/docs/50_test-reports/v0.2.0b_artifacts/slowprobe/build.rs +0 -4
  30. package/docs/50_test-reports/v0.2.0b_artifacts/slowprobe/src/main.rs +0 -13
  31. package/docs/60_exploration-and-research/01-cordis-runtime/bun-compile-cordis-runtime-bootstrap-research.md +0 -536
  32. package/docs/60_exploration-and-research/01-cordis-runtime/cordis-customization-and-override-mechanics.md +0 -418
  33. package/docs/60_exploration-and-research/01-cordis-runtime/cordis-research.md +0 -350
  34. package/docs/60_exploration-and-research/01-cordis-runtime/dsh-cordis-hotplug-mcp-patch-research.md +0 -265
  35. package/docs/60_exploration-and-research/02-dsh-webui/dsh-web-profile-package-map.md +0 -186
  36. package/docs/60_exploration-and-research/02-dsh-webui/dsh-web-ui-slot-system-research.md +0 -310
  37. package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-backend-data-inventory.md +0 -633
  38. package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-strip-boundary-research.md +0 -300
  39. package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-wire-appendix.md +0 -3729
  40. package/docs/60_exploration-and-research/02-dsh-webui/web-frontend-composability-research.md +0 -191
  41. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/capture-live-turn.json +0 -1
  42. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/observed-endpoints.json +0 -224
  43. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/remote-inventory.json +0 -110954
  44. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/served-index-sample.html +0 -47
  45. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/session-events.json +0 -9411
  46. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/ws-frame-examples.json +0 -20
  47. package/docs/60_exploration-and-research/03-mobile-ios/dsh-mobile-spa-ios-input-experience-research.md +0 -160
  48. package/docs/60_exploration-and-research/03-mobile-ios/ios-chat-app-bridge-research.md +0 -324
  49. package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-jsonl-mapping.md +0 -1532
  50. package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode-failed.json +0 -46
  51. package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode1.json +0 -1124
  52. package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode2.json +0 -1240
  53. package/docs/60_exploration-and-research/05-dashr-dev/plugin-development.md +0 -148
  54. package/docs/60_exploration-and-research/05-dashr-dev/upstream-alignment.md +0 -102
  55. package/docs/60_exploration-and-research/README.md +0 -87
  56. package/docs/60_exploration-and-research/bun-compile-cordis-runtime-bootstrap-research.md +0 -348
  57. package/docs/60_exploration-and-research/cordis-research.md +0 -350
  58. package/docs/60_exploration-and-research/dsh-mobile-spa-ios-input-experience-research.md +0 -160
  59. package/docs/60_exploration-and-research/dsh-web-profile-package-map.md +0 -186
  60. package/docs/60_exploration-and-research/dsh-web-ui-slot-system-research.md +0 -310
  61. package/docs/60_exploration-and-research/dsh-webui-strip-boundary-research.md +0 -300
  62. package/docs/60_exploration-and-research/ios-chat-app-bridge-research.md +0 -324
  63. package/docs/60_exploration-and-research/web-frontend-composability-research.md +0 -191
package/lib/index.js CHANGED
@@ -2556,15 +2556,15 @@ const here = path.dirname(fileURLToPath(import.meta.url));
2556
2556
  /** `require` anchored at this module — used to dlopen absolute `.node` paths. */
2557
2557
  const requireHere = createRequire(import.meta.url);
2558
2558
  /**
2559
- * Locate the leaf package directory: first via `require.resolve` from
2560
- * `fromDir` (honors the usual node_modules walk), then via an explicit
2561
- * node_modules-walk upward from `fromDir`. Returns `undefined` when absent.
2559
+ * Locate the leaf package directory with an explicit node_modules-walk upward
2560
+ * from `fromDir` — the ONLY mechanism. `require.resolve` is deliberately not
2561
+ * used here: its fallback through the process global paths (which include the
2562
+ * pnpm virtual-store hoist root `.pnpm/node_modules`) can resolve the leaf
2563
+ * package from ANY anchor, defeating the anchor-bounded contract this loader
2564
+ * guarantees for its degradation tests and unsupported-platform probes.
2565
+ * Returns `undefined` when absent.
2562
2566
  */
2563
2567
  function findPackageDir(packageName, fromDir) {
2564
- try {
2565
- const manifest = createRequire(path.join(fromDir, "noop.js")).resolve(`${packageName}/package.json`);
2566
- return path.dirname(manifest);
2567
- } catch {}
2568
2568
  let dir = fromDir;
2569
2569
  for (;;) {
2570
2570
  const candidate = path.join(dir, "node_modules", packageName);
@@ -5121,7 +5121,21 @@ async function newestByCreation(entries, family) {
5121
5121
  async function createdAtOf(entry, family) {
5122
5122
  const live = family.sessions.get(entry.id);
5123
5123
  if (live !== void 0) return live.header.createdAt;
5124
- return (await family.sessionPersistence.inspect(entry.id))?.meta.createdAt ?? 0;
5124
+ return (await family.sessionPersistence.stat(entry.id))?.header.createdAt ?? 0;
5125
+ }
5126
+ /**
5127
+ * The persisted log's events for one settled session, or `undefined` when the
5128
+ * session was never materialized (0.1.5 seam: `stat` guards materialization,
5129
+ * then a read-mode `open` handle slices the validated contiguous log).
5130
+ */
5131
+ async function persistedEventsOf(id, family) {
5132
+ if (await family.sessionPersistence.stat(id) === void 0) return void 0;
5133
+ const handle = await family.sessionPersistence.open(id, "read");
5134
+ try {
5135
+ return (await handle.read()).events;
5136
+ } finally {
5137
+ await handle.close();
5138
+ }
5125
5139
  }
5126
5140
  /**
5127
5141
  * The output artifact for one resolved address: the live session's last
@@ -5131,9 +5145,9 @@ async function createdAtOf(entry, family) {
5131
5145
  async function outputFor(target, family) {
5132
5146
  const live = family.sessions.get(SessionId(target.rawId));
5133
5147
  if (live !== void 0) return outputArtifact(live);
5134
- const inspection = await family.sessionPersistence.inspect(SessionId(target.rawId));
5135
- if (inspection === void 0) throw unknownId(target, `agent "${target.rawId}" has no live session and no persisted log`);
5136
- return persistedOutput(inspection.events);
5148
+ const events = await persistedEventsOf(SessionId(target.rawId), family);
5149
+ if (events === void 0) throw unknownId(target, `agent "${target.rawId}" has no live session and no persisted log`);
5150
+ return persistedOutput(events);
5137
5151
  }
5138
5152
  /** The live session for one resolved address, or a structured error. */
5139
5153
  function requireLiveSession(target, family) {
@@ -5166,9 +5180,9 @@ async function renderRoster(family, descendants, agents) {
5166
5180
  /** One child's last activity: its live log's last event, else its persisted one. */
5167
5181
  async function lastActivityOf(rawId, family) {
5168
5182
  const live = family.sessions.get(SessionId(rawId));
5169
- if (live !== void 0) return lastEventTime(live.events);
5170
- const inspection = await family.sessionPersistence.inspect(SessionId(rawId));
5171
- return inspection === void 0 ? "-" : lastEventTime(inspection.events);
5183
+ if (live !== void 0) return lastEventTime(live.snapshotEvents());
5184
+ const events = await persistedEventsOf(SessionId(rawId), family);
5185
+ return events === void 0 ? "-" : lastEventTime(events);
5172
5186
  }
5173
5187
  /** The last event's time as ISO 8601, `-` for an empty log. */
5174
5188
  function lastEventTime(events) {
@@ -5941,7 +5955,7 @@ function createGrepTool(deps) {
5941
5955
  const MAX_BYTES = 100 * 1024 * 1024;
5942
5956
  const MAX_READ_LINE_BYTES = 200 * 1024;
5943
5957
  const HASH_STORE_BUSY_TIMEOUT = 1e3;
5944
- const HASH_STORE_VERSION = 6;
5958
+ const HASH_STORE_VERSION = 7;
5945
5959
  const EDITS_MAX_ITEMS = 32;
5946
5960
  const SERVED_TTL_MS = 10080 * 60 * 1e3;
5947
5961
  const SERVED_ECHO_CAP = 150;
@@ -6095,20 +6109,18 @@ function assertUndoRequest(request) {
6095
6109
  //#endregion
6096
6110
  //#region src/url-schema/vendored/hashline/paths.js
6097
6111
  /**
6098
- * On-disk home for dsh-better-edit state. Inside a tool call the store lives
6099
- * co-located with the files being edited: `<workspace>/.dsh_better_edit/` (the
6100
- * workspace is the session cwd, carried through the execution by
6101
- * `withWorkspace`). Outside a tool call — tests, previews, startup — the store
6102
- * falls back to the shared DeepSeek Harness home
6103
- * (`$DSH_HOME/plugins/dsh-better-edit`, default `~/.dsh/plugins/dsh-better-edit`),
6104
- * so a caller without a workspace never writes into an arbitrary cwd.
6105
- * @param cwd - the workspace root, or undefined for the shared-home fallback.
6112
+ * On-disk home for dsh-better-edit state: ONE centralized store per harness
6113
+ * home — `$DSH_HOME/storages/dsh-better-edit` (default
6114
+ * `~/.dsh/storages/dsh-better-edit`), resolved through the harness home
6115
+ * resolver so `DSH_HOME` isolates deployments (test homes never touch prod).
6116
+ * There is no per-workspace state directory: the store keys everything by
6117
+ * canonical absolute path.
6106
6118
  */
6107
- function configDir(cwd) {
6108
- return cwd !== void 0 ? join(resolve(cwd), ".dsh_better_edit") : join(resolveDshHome(), "plugins", "dsh-better-edit");
6119
+ function configDir() {
6120
+ return join(resolveDshHome(), "storages", "dsh-better-edit");
6109
6121
  }
6110
- function hashStorePath(cwd) {
6111
- return join(configDir(cwd), "hash-store.sqlite");
6122
+ function hashStorePath() {
6123
+ return join(configDir(), "hash-store.sqlite");
6112
6124
  }
6113
6125
 
6114
6126
  //#endregion
@@ -6330,646 +6342,543 @@ function mapStableHashes(oldContent, oldHashes, newContent, removedHashes) {
6330
6342
  }
6331
6343
 
6332
6344
  //#endregion
6333
- //#region src/url-schema/vendored/hashline/hashline/anchor-pipeline.js
6334
- function diagRef(ref) {
6335
- const trimmed = ref.trim();
6336
- if (!trimmed.length) return `[E_BAD_REF] Invalid anchor. Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
6337
- if (/^\d+/.test(trimmed)) return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. "aB3") — no line numbers or trailing content.`;
6338
- if (trimmed.includes("│")) return `[E_BAD_REF] Invalid anchor "${trimmed}". remove_from and remove_to must contain the 3-char hash only — remove everything from "│" onward.`;
6339
- return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
6340
- }
6341
- function parseRef(ref) {
6342
- const trimmed = ref.trim();
6343
- if (trimmed.length === ANCHOR_LEN && ALPH_RE.test(trimmed)) return { hash: trimmed };
6344
- throw new Error(diagRef(ref));
6345
+ //#region src/url-schema/vendored/hashline/hash-store.js
6346
+ function isValidHashList(value) {
6347
+ if (!Array.isArray(value)) return false;
6348
+ for (const hash of value) if (typeof hash !== "string" || !HASH_RE.test(hash)) return false;
6349
+ return true;
6345
6350
  }
6346
- const parseHashRef = parseRef;
6347
- function parseText(edit) {
6348
- if (typeof edit !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
6349
- const normalized = edit.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
6350
- if (normalized === "") return [];
6351
- if (/^\n+$/.test(normalized)) return new Array(normalized.length).fill("");
6352
- return normalized.split("\n");
6351
+ function isValidSnapshot(value) {
6352
+ if (typeof value !== "object" || value === null) return false;
6353
+ const v = value;
6354
+ if (typeof v.content !== "string") return false;
6355
+ return isValidHashList(v.hashes);
6353
6356
  }
6354
- function resAnchorFromMap(ref, hashIndex) {
6355
- const hashMatches = hashIndex.get(ref.hash);
6356
- if (!hashMatches || hashMatches.length === 0) return {
6357
- ref,
6358
- kind: "not_found"
6359
- };
6360
- if (hashMatches.length === 1) return {
6361
- line: hashMatches[0],
6362
- hash: ref.hash,
6363
- hashMatched: true
6364
- };
6365
- return {
6366
- ref,
6367
- kind: "ambiguous",
6368
- candidates: hashMatches
6369
- };
6357
+ /** A served-row array: per-position hash, or null for never-served slots. */
6358
+ function isValidServedList(value) {
6359
+ if (!Array.isArray(value)) return false;
6360
+ for (const entry of value) {
6361
+ if (entry === null) continue;
6362
+ if (typeof entry !== "string" || !HASH_RE.test(entry)) return false;
6363
+ }
6364
+ return true;
6370
6365
  }
6371
- function assertAligned(fileLines, fileHashes, ctx) {
6372
- if (fileHashes.length !== fileLines.length) throw new Error(`${ctx}: fileHashes.length (${fileHashes.length}) must match fileLines.length (${fileLines.length}).`);
6366
+ function cacheKey(checksum) {
6367
+ return `${CANON_VERSION}:${checksum}`;
6373
6368
  }
6374
- function fmtMismatchWithServes(mismatches, fileLines, fileHashes, filePath) {
6375
- assertAligned(fileLines, fileHashes, "fmtMismatch");
6376
- const out = [];
6377
- const servedRows = [];
6378
- const seen = /* @__PURE__ */ new Set();
6379
- const pushRow = (ln) => {
6380
- if (ln < 1 || ln > fileLines.length) return;
6381
- const position = ln - 1;
6382
- if (seen.has(position)) return;
6383
- seen.add(position);
6384
- servedRows.push({
6385
- position,
6386
- hash: fileHashes[ln - 1]
6387
- });
6388
- };
6389
- const notFound = mismatches.filter((m) => m.kind === "not_found");
6390
- const ambiguous = mismatches.filter((m) => m.kind === "ambiguous");
6391
- const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
6392
- if (notFound.length > 0) {
6393
- out.push(`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. Re-read for fresh anchors.`);
6394
- for (const m of notFound) {
6395
- const ctx = m.context;
6396
- if (!ctx) continue;
6397
- const from = Math.max(1, ctx.line - 1);
6398
- const to = Math.min(fileLines.length, ctx.line + 1);
6399
- const rows = [];
6400
- for (let ln = from; ln <= to; ln++) {
6401
- rows.push(` ${ln}: ${fileHashes[ln - 1]}│${clipLine(fileLines[ln - 1] ?? "")}`);
6402
- pushRow(ln);
6403
- }
6404
- out.push("");
6405
- out.push(` Current context around resolved anchor "${ctx.hash}" (line ${ctx.line}):\n${rows.join("\n")}`);
6406
- }
6407
- }
6408
- if (ambiguous.length > 0) {
6409
- if (out.length > 0) out.push("");
6410
- out.push(`[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}. Re-read for fresh anchors.`);
6411
- for (const m of ambiguous) {
6412
- const sample = (m.candidates ?? []).slice(0, 5);
6413
- const more = (m.candidates?.length ?? 0) > sample.length ? `, ... (+${(m.candidates?.length ?? 0) - sample.length} more)` : "";
6414
- const lines = sample.map((line) => {
6415
- const content = clipLine(fileLines[line - 1] ?? "");
6416
- pushRow(line);
6417
- return ` ${line}: ${fileHashes[line - 1]}│${content}`;
6418
- }).join("\n");
6419
- out.push(` Hash "${m.ref.hash}" matches lines ${sample.join(", ")}${more}.\n${lines}`);
6420
- }
6369
+ function isCorruptionError(error) {
6370
+ if (error && typeof error === "object") {
6371
+ const errcode = error.errcode;
6372
+ if (typeof errcode === "number") return errcode === 11 || errcode === 24 || errcode === 26;
6373
+ const code = error.code;
6374
+ if (typeof code === "string" && /NOTADB|CORRUPT/.test(code)) return true;
6421
6375
  }
6422
- return {
6423
- message: out.join("\n"),
6424
- servedRows
6425
- };
6426
- }
6427
- const ITEM_KS = new Set([
6428
- "replacement_text",
6429
- "remove_from",
6430
- "remove_to"
6431
- ]);
6432
- function assertItem(edit) {
6433
- rejectUnknownFields(edit, ITEM_KS, "Edit", "The edit takes only { replacement_text, remove_from, remove_to }.");
6434
- if ("remove_from" in edit && typeof edit.remove_from !== "string") throw new Error(`[E_BAD_SHAPE] Field "remove_from" must be an anchor string (3-char hash).`);
6435
- if ("remove_to" in edit && typeof edit.remove_to !== "string") throw new Error(`[E_BAD_SHAPE] Field "remove_to" must be an anchor string (3-char hash).`);
6436
- if (!("replacement_text" in edit)) throw new Error(`[E_BAD_SHAPE] The edit requires a "replacement_text" field. Provide the replacement text (use "" to delete).`);
6437
- if (typeof edit.replacement_text !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
6438
- if (typeof edit.remove_from !== "string" || typeof edit.remove_to !== "string") throw new Error(`[E_BAD_SHAPE] The edit requires "remove_from" and "remove_to" anchor strings (3-char hashes from read output).`);
6376
+ return error instanceof Error && /corrupt|not a database|malformed|database disk image/i.test(error.message);
6439
6377
  }
6440
- const ANCHOR_ROW_RE = /* @__PURE__ */ new RegExp(`^([+-]?)(${HASH_CLASS})│`);
6441
- function resEdit(edit, warnings) {
6442
- assertItem(edit);
6443
- const editLines = parseText(edit.replacement_text);
6444
- const bounds = [edit.remove_from, edit.remove_to].map((ref) => {
6445
- const trimmed = ref.trim();
6446
- const match = trimmed.match(ANCHOR_ROW_RE);
6447
- if (match) {
6448
- let message;
6449
- if (match[1] === "+") message = `[E_BAD_REF] stripped diff-preview marker from remove_from/remove_to "${trimmed}".`;
6450
- else if (match[1] === "-") message = `[E_BAD_REF] stripped leading "-" marker from remove_from/remove_to "${trimmed}".`;
6451
- else message = `[E_BAD_REF] stripped "HASH│" prefix from remove_from/remove_to "${trimmed}".`;
6452
- warnings?.push(message);
6453
- return match[2];
6454
- }
6455
- return ref;
6456
- });
6457
- return {
6458
- content_lines: editLines,
6459
- hash_bounds: [parseHashRef(bounds[0]), parseHashRef(bounds[1])]
6460
- };
6378
+ function isBusyError(error) {
6379
+ if (error && typeof error === "object") {
6380
+ const errcode = error.errcode;
6381
+ if (typeof errcode === "number") return errcode === 5 || errcode === 6;
6382
+ }
6383
+ return error instanceof Error && /busy|locked/i.test(error.message);
6461
6384
  }
6462
- function warnUnicodeEsc(edit, warnings) {
6463
- if (edit.content_lines.some((line) => /\\uDDDD/i.test(line))) warnings.push("Detected literal \\uDDDD in edit content; no autocorrection applied. Verify whether this should be a real Unicode escape or plain text.");
6385
+ function sleepSync(ms) {
6386
+ const sab = new Int32Array(new SharedArrayBuffer(4));
6387
+ Atomics.wait(sab, 0, 0, ms);
6464
6388
  }
6465
- /** @internal — private to anchor-pipeline seam; do not import directly, use anchor-pipeline.ts */
6466
- function stripBarePrefixes(edit, fileHashes, warnings) {
6467
- const fileHashSet = new Set(fileHashes);
6468
- const stripped = [];
6469
- const contentLines = edit.content_lines.map((line, lineIndex) => {
6470
- const match = line.match(HL_BARE_PREFIX_RE);
6471
- if (!match) return line;
6472
- stripped.push({
6473
- lineIndex,
6474
- matched: fileHashSet.has(match[1])
6475
- });
6476
- return line.slice(match[0].length);
6477
- });
6478
- if (stripped.length === 0) return edit;
6479
- const locations = stripped.map((s) => `replacement_text line ${s.lineIndex + 1}`).join(", ");
6480
- const matchedCount = stripped.filter((s) => s.matched).length;
6481
- const evidence = matchedCount === 0 ? "0 matched — verify literal 'HASH│' content" : `${matchedCount}/${stripped.length} matched`;
6482
- warnings.push(`[E_BARE_HASH_PREFIX] stripped "HASH│" prefix from ${locations} (${evidence}).`);
6483
- return {
6484
- ...edit,
6485
- content_lines: contentLines
6486
- };
6389
+ const BUSY_RETRIES = 3;
6390
+ const BUSY_RETRY_DELAY_MS = 100;
6391
+ function withBusyRetry(fn) {
6392
+ let lastError;
6393
+ for (let attempt = 0; attempt <= BUSY_RETRIES; attempt++) try {
6394
+ return fn();
6395
+ } catch (error) {
6396
+ lastError = error;
6397
+ if (!isBusyError(error) || attempt === BUSY_RETRIES) throw error;
6398
+ sleepSync(BUSY_RETRY_DELAY_MS);
6399
+ }
6400
+ throw lastError;
6487
6401
  }
6488
- /** @internal — private to anchor-pipeline seam */
6489
- function stripDiffPrefixes(edit, warnings) {
6490
- const stripped = [];
6491
- const contentLines = edit.content_lines.map((line, lineIndex) => {
6492
- const plus = line.match(HL_PREFIX_PLUS_RE);
6493
- if (plus) {
6494
- stripped.push(lineIndex);
6495
- return line.slice(plus[0].length);
6496
- }
6497
- const minus = line.match(HL_PREFIX_MINUS_RE);
6498
- if (minus) {
6499
- stripped.push(lineIndex);
6500
- return line.slice(minus[0].length);
6501
- }
6502
- return line;
6503
- });
6504
- if (stripped.length === 0) return edit;
6505
- const locations = stripped.map((i) => `replacement_text line ${i + 1}`).join(", ");
6506
- warnings.push(`[E_INVALID_PATCH] stripped diff-preview marker from ${locations}.`);
6507
- return {
6508
- ...edit,
6509
- content_lines: contentLines
6510
- };
6402
+ function openDbWithBusyRetry(storePath) {
6403
+ return withBusyRetry(() => openDb(storePath));
6511
6404
  }
6512
- /** @internal — private to anchor-pipeline seam */
6513
- function swapReversedRanges(edit, fileHashes, warnings) {
6514
- const lineByHash = /* @__PURE__ */ new Map();
6515
- for (let i = 0; i < fileHashes.length; i++) lineByHash.set(fileHashes[i], i + 1);
6516
- const [startRef, endRef] = edit.hash_bounds;
6517
- const startLine = lineByHash.get(startRef.hash);
6518
- const endLine = lineByHash.get(endRef.hash);
6519
- if (startLine === void 0 || endLine === void 0 || startLine <= endLine) return edit;
6520
- warnings.push(`[E_BAD_OP] reversed remove_from/remove_to (${startRef.hash} after ${endRef.hash}); swapped.`);
6521
- return {
6522
- ...edit,
6523
- hash_bounds: [endRef, startRef]
6524
- };
6405
+ /** One open store per store path (per workspace); parallel sessions share per-workspace dbs. */
6406
+ const stores = /* @__PURE__ */ new Map();
6407
+ const openings = /* @__PURE__ */ new Map();
6408
+ let exitHandlerRegistered = false;
6409
+ function openDb(storePath) {
6410
+ const db = new DatabaseSync(storePath, { timeout: HASH_STORE_BUSY_TIMEOUT });
6411
+ try {
6412
+ return buildStore(db);
6413
+ } catch (error) {
6414
+ try {
6415
+ db.close();
6416
+ } catch {}
6417
+ throw error;
6418
+ }
6525
6419
  }
6526
- function trailingDups(contentLines, fileLines, endLine) {
6527
- const start = lastNonEmptyIndex(contentLines);
6528
- if (start < 0) return [];
6529
- const dups = [];
6530
- const maxK = Math.min(start + 1, fileLines.length - endLine);
6531
- for (let k = 0; k < maxK; k++) {
6532
- if (contentLines[start - k] !== fileLines[endLine + k]) break;
6533
- dups.push({
6534
- kind: "trailing",
6535
- replacementLineIndex: start - k
6536
- });
6537
- }
6538
- return dups;
6539
- }
6540
- function leadingDups(contentLines, fileLines, startLine) {
6541
- const start = firstNonEmptyIndex(contentLines);
6542
- if (start < 0) return [];
6543
- const dups = [];
6544
- const maxK = Math.min(contentLines.length - start, startLine - 1);
6545
- for (let k = 0; k < maxK; k++) {
6546
- if (contentLines[start + k] !== fileLines[startLine - 2 - k]) break;
6547
- dups.push({
6548
- kind: "leading",
6549
- replacementLineIndex: start + k
6550
- });
6551
- }
6552
- return dups;
6553
- }
6554
- function sectionIsUnique(canonLines, start, length) {
6555
- let count = 0;
6556
- for (let i = 0; i + length <= canonLines.length; i++) {
6557
- let k = 0;
6558
- while (k < length && canonLines[i + k] === canonLines[start + k]) k++;
6559
- if (k < length) continue;
6560
- count++;
6561
- if (count > 1) return false;
6562
- }
6563
- return true;
6564
- }
6565
- function firstNewAfterDups(contentLines, rangeLines, canonLines, endLine) {
6566
- const firstNew = findNewEdge(contentLines, rangeLines, false);
6567
- if (!firstNew) return [];
6568
- const maxK = Math.min(contentLines.length - firstNew.index, canonLines.length - endLine);
6569
- let runLen = 0;
6570
- while (runLen < maxK && canon(contentLines[firstNew.index + runLen]) === canonLines[endLine + runLen]) runLen++;
6571
- if (runLen === 0 || !sectionIsUnique(canonLines, endLine, runLen)) return [];
6572
- const dups = [];
6573
- for (let k = 0; k < runLen; k++) dups.push({
6574
- kind: "first-new-after",
6575
- replacementLineIndex: firstNew.index + k
6576
- });
6577
- return dups;
6578
- }
6579
- function lastNewBeforeDups(contentLines, rangeLines, canonLines, startLine) {
6580
- const lastNew = findNewEdge(contentLines, rangeLines, true);
6581
- if (!lastNew) return [];
6582
- const maxK = Math.min(lastNew.index + 1, startLine - 1);
6583
- let runLen = 0;
6584
- while (runLen < maxK && canon(contentLines[lastNew.index - runLen]) === canonLines[startLine - 2 - runLen]) runLen++;
6585
- if (runLen === 0) return [];
6586
- if (!sectionIsUnique(canonLines, startLine - 1 - runLen, runLen)) return [];
6587
- const dups = [];
6588
- for (let k = 0; k < runLen; k++) dups.push({
6589
- kind: "last-new-before",
6590
- replacementLineIndex: lastNew.index - k
6591
- });
6592
- return dups;
6593
- }
6594
- function canonCounts(lines) {
6595
- const counts = /* @__PURE__ */ new Map();
6596
- for (const line of lines) {
6597
- const key = canon(line);
6598
- counts.set(key, (counts.get(key) ?? 0) + 1);
6599
- }
6600
- return counts;
6601
- }
6602
- /** @internal — private to anchor-pipeline seam */
6603
- function findNewEdge(contentLines, rangeLines, fromEnd) {
6604
- const multiset = canonCounts(rangeLines);
6605
- const step = fromEnd ? -1 : 1;
6606
- const start = fromEnd ? contentLines.length - 1 : 0;
6607
- for (let i = start; i >= 0 && i < contentLines.length; i += step) {
6608
- const line = contentLines[i];
6609
- if (line.length === 0) continue;
6610
- const key = canon(line);
6611
- const count = multiset.get(key) ?? 0;
6612
- if (count > 0) multiset.set(key, count - 1);
6613
- else return {
6614
- index: i,
6615
- line
6616
- };
6617
- }
6618
- }
6619
- /** @internal — private to anchor-pipeline seam: detection + boundaryDups belongs to AnchorPipeline ordering */
6620
- function valEdit(edit, fileLines, fileHashes, warnings, signal) {
6621
- assertAligned(fileLines, fileHashes, "valEdit");
6622
- const mismatches = [];
6623
- const boundaryDups = [];
6624
- const hashIndex = /* @__PURE__ */ new Map();
6625
- for (let i = 0; i < fileHashes.length; i++) {
6626
- const h = fileHashes[i];
6627
- const list = hashIndex.get(h) ?? [];
6628
- list.push(i + 1);
6629
- hashIndex.set(h, list);
6420
+ function buildStore(db) {
6421
+ db.exec("PRAGMA journal_mode = WAL");
6422
+ db.exec("PRAGMA synchronous = NORMAL");
6423
+ db.exec("CREATE TABLE IF NOT EXISTS snapshots (path TEXT PRIMARY KEY, checksum TEXT NOT NULL, line_count INTEGER NOT NULL, hashes TEXT NOT NULL, updated_at INTEGER NOT NULL)");
6424
+ db.exec("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
6425
+ db.exec("CREATE TABLE IF NOT EXISTS undo (path TEXT PRIMARY KEY, content TEXT NOT NULL, bom TEXT NOT NULL, ending TEXT NOT NULL, hashes TEXT NOT NULL, result_content TEXT NOT NULL, updated_at INTEGER NOT NULL)");
6426
+ const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'version'").get();
6427
+ const versionChanged = versionRow !== void 0 && versionRow.value !== String(HASH_STORE_VERSION);
6428
+ if (versionChanged) {
6429
+ db.exec("DELETE FROM snapshots");
6430
+ db.exec("DELETE FROM undo");
6630
6431
  }
6631
- const tryResolve = (ref) => {
6632
- const result = resAnchorFromMap(ref, hashIndex);
6633
- if ("kind" in result) {
6634
- mismatches.push(result);
6635
- return;
6432
+ const servedColumns = db.prepare("PRAGMA table_info(served)").all();
6433
+ if (versionChanged || servedColumns.some((column) => column.name === "session_id")) db.exec("DROP TABLE IF EXISTS served");
6434
+ db.exec("CREATE TABLE IF NOT EXISTS served (path TEXT NOT NULL PRIMARY KEY, hashes TEXT NOT NULL, reported TEXT, updated_at INTEGER NOT NULL)");
6435
+ db.prepare("INSERT INTO meta (key, value) VALUES ('version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(HASH_STORE_VERSION));
6436
+ const getStmt = db.prepare("SELECT hashes FROM snapshots WHERE path = ? AND checksum = ? AND line_count = ?");
6437
+ const allStmt = db.prepare("SELECT path FROM snapshots UNION SELECT path FROM undo UNION SELECT path FROM served");
6438
+ const allHashesStmt = db.prepare("SELECT path, hashes FROM snapshots");
6439
+ const delStmt = db.prepare("DELETE FROM snapshots WHERE path = ?");
6440
+ const upsertStmt = db.prepare("INSERT INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET checksum = excluded.checksum, line_count = excluded.line_count, hashes = excluded.hashes, updated_at = excluded.updated_at");
6441
+ const undoUpsertStmt = db.prepare("INSERT INTO undo (path, content, bom, ending, hashes, result_content, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET content = excluded.content, bom = excluded.bom, ending = excluded.ending, hashes = excluded.hashes, result_content = excluded.result_content, updated_at = excluded.updated_at");
6442
+ const undoGetStmt = db.prepare("SELECT content, bom, ending, hashes, result_content FROM undo WHERE path = ?");
6443
+ const undoDelStmt = db.prepare("DELETE FROM undo WHERE path = ?");
6444
+ const servedGetStmt = db.prepare("SELECT hashes, reported FROM served WHERE path = ?");
6445
+ const servedUpsertStmt = db.prepare("INSERT INTO served (path, hashes, updated_at) VALUES (?, ?, ?) ON CONFLICT(path) DO UPDATE SET hashes = excluded.hashes, updated_at = excluded.updated_at");
6446
+ const servedReportedUpsertStmt = db.prepare("INSERT INTO served (path, hashes, reported, updated_at) VALUES (?, '[]', ?, ?) ON CONFLICT(path) DO UPDATE SET reported = excluded.reported, updated_at = excluded.updated_at");
6447
+ const servedReportedClearStmt = db.prepare("UPDATE served SET reported = NULL, updated_at = ? WHERE path = ?");
6448
+ const servedDeleteStmt = db.prepare("DELETE FROM served WHERE path = ?");
6449
+ const servedDeletePathStmt = db.prepare("DELETE FROM served WHERE path = ?");
6450
+ const servedPruneOlderThanStmt = db.prepare("DELETE FROM served WHERE updated_at < ?");
6451
+ return {
6452
+ db,
6453
+ stmts: {
6454
+ get: (...params) => getStmt.get(...params),
6455
+ allPaths: (...params) => allStmt.all(...params),
6456
+ allHashes: (...params) => allHashesStmt.all(...params),
6457
+ deleteOne: (...params) => {
6458
+ withBusyRetry(() => {
6459
+ delStmt.run(...params);
6460
+ });
6461
+ },
6462
+ upsert: (...params) => {
6463
+ withBusyRetry(() => {
6464
+ upsertStmt.run(...params);
6465
+ });
6466
+ },
6467
+ undoUpsert: (...params) => {
6468
+ withBusyRetry(() => {
6469
+ undoUpsertStmt.run(...params);
6470
+ });
6471
+ },
6472
+ undoGet: (...params) => undoGetStmt.get(...params),
6473
+ undoDelete: (...params) => {
6474
+ withBusyRetry(() => {
6475
+ undoDelStmt.run(...params);
6476
+ });
6477
+ },
6478
+ servedGet: (...params) => servedGetStmt.get(...params),
6479
+ servedUpsert: (...params) => {
6480
+ withBusyRetry(() => {
6481
+ servedUpsertStmt.run(...params);
6482
+ });
6483
+ },
6484
+ servedReportedUpsert: (...params) => {
6485
+ withBusyRetry(() => {
6486
+ servedReportedUpsertStmt.run(...params);
6487
+ });
6488
+ },
6489
+ servedReportedClear: (path$1) => {
6490
+ withBusyRetry(() => {
6491
+ servedReportedClearStmt.run(Date.now(), path$1);
6492
+ });
6493
+ },
6494
+ servedDelete: (...params) => {
6495
+ withBusyRetry(() => {
6496
+ servedDeleteStmt.run(...params);
6497
+ });
6498
+ },
6499
+ servedDeletePath: (...params) => {
6500
+ withBusyRetry(() => {
6501
+ servedDeletePathStmt.run(...params);
6502
+ });
6503
+ },
6504
+ servedPruneOlderThan: (...params) => {
6505
+ withBusyRetry(() => {
6506
+ servedPruneOlderThanStmt.run(...params);
6507
+ });
6508
+ }
6636
6509
  }
6637
- return result;
6638
6510
  };
6639
- abortIf(signal);
6640
- const startResolved = tryResolve(edit.hash_bounds[0]);
6641
- const endResolved = tryResolve(edit.hash_bounds[1]);
6642
- if (!startResolved || !endResolved) {
6643
- if (!startResolved && endResolved) {
6644
- const startMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[0]);
6645
- if (startMismatch && startMismatch.kind === "not_found") startMismatch.context = endResolved;
6646
- } else if (startResolved && !endResolved) {
6647
- const endMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[1]);
6648
- if (endMismatch && endMismatch.kind === "not_found") endMismatch.context = startResolved;
6649
- }
6650
- return {
6651
- resolved: void 0,
6652
- mismatches,
6653
- boundaryDups
6654
- };
6655
- }
6656
- if (startResolved.line > endResolved.line) throw new Error(`[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.hash_bounds[0].hash} and ${edit.hash_bounds[1].hash}).`);
6657
- const endLine = endResolved.line;
6658
- const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
6659
- const canonLines = fileLines.map((line) => canon(line));
6660
- boundaryDups.push(...trailingDups(edit.content_lines, fileLines, endLine), ...leadingDups(edit.content_lines, fileLines, startResolved.line), ...firstNewAfterDups(edit.content_lines, rangeLines, canonLines, endLine), ...lastNewBeforeDups(edit.content_lines, rangeLines, canonLines, startResolved.line));
6511
+ }
6512
+ /** Wire the domain methods over the prepared statements. */
6513
+ function makeDomainStore(stmts) {
6661
6514
  return {
6662
- resolved: {
6663
- content_lines: edit.content_lines,
6664
- hash_bounds: [startResolved, endResolved]
6515
+ engine: "node:sqlite",
6516
+ getSnapshot(path$1, content, deleteCorrupt = true) {
6517
+ const checksum = cacheKey(contentChecksum(content));
6518
+ const lineCount = splitLines(content).length;
6519
+ const row = stmts.get(path$1, checksum, lineCount);
6520
+ if (!row) return void 0;
6521
+ try {
6522
+ const parsed = JSON.parse(row.hashes);
6523
+ if (isValidHashList(parsed)) return parsed;
6524
+ if (deleteCorrupt) stmts.deleteOne(path$1);
6525
+ return;
6526
+ } catch {
6527
+ if (deleteCorrupt) stmts.deleteOne(path$1);
6528
+ return;
6529
+ }
6665
6530
  },
6666
- mismatches,
6667
- boundaryDups
6531
+ upsertSnapshot(path$1, checksum, lineCount, hashes) {
6532
+ stmts.upsert(path$1, cacheKey(checksum), lineCount, JSON.stringify(hashes), Date.now());
6533
+ },
6534
+ allKnownPaths() {
6535
+ return stmts.allPaths();
6536
+ },
6537
+ allSnapshotHashes() {
6538
+ return stmts.allHashes();
6539
+ },
6540
+ deleteSnapshot(path$1) {
6541
+ stmts.deleteOne(path$1);
6542
+ },
6543
+ findSnapshotPaths(hashes) {
6544
+ const rows = stmts.allHashes();
6545
+ const matches = [];
6546
+ for (const row of rows) try {
6547
+ const parsed = JSON.parse(row.hashes);
6548
+ if (!isValidHashList(parsed)) continue;
6549
+ if (hashes.every((h) => parsed.includes(h))) matches.push(row.path);
6550
+ } catch {}
6551
+ return matches;
6552
+ },
6553
+ getUndo(path$1) {
6554
+ const row = stmts.undoGet(path$1);
6555
+ if (!row) return void 0;
6556
+ try {
6557
+ const parsed = JSON.parse(row.hashes);
6558
+ if (!isValidHashList(parsed)) {
6559
+ stmts.undoDelete(path$1);
6560
+ return;
6561
+ }
6562
+ return {
6563
+ content: row.content,
6564
+ bom: row.bom,
6565
+ ending: row.ending,
6566
+ hashes: parsed,
6567
+ resultContent: row.result_content
6568
+ };
6569
+ } catch {
6570
+ stmts.undoDelete(path$1);
6571
+ return;
6572
+ }
6573
+ },
6574
+ upsertUndo(path$1, entry) {
6575
+ stmts.undoUpsert(path$1, entry.content, entry.bom, entry.ending, JSON.stringify(entry.hashes), entry.resultContent, Date.now());
6576
+ },
6577
+ deleteUndo(path$1) {
6578
+ stmts.undoDelete(path$1);
6579
+ },
6580
+ getServed(path$1) {
6581
+ const row = stmts.servedGet(path$1);
6582
+ if (!row) return [];
6583
+ try {
6584
+ const parsed = JSON.parse(row.hashes);
6585
+ if (isValidServedList(parsed)) return parsed;
6586
+ stmts.servedDelete(path$1);
6587
+ return [];
6588
+ } catch {
6589
+ stmts.servedDelete(path$1);
6590
+ return [];
6591
+ }
6592
+ },
6593
+ getServedReported(path$1) {
6594
+ const row = stmts.servedGet(path$1);
6595
+ if (!row) return /* @__PURE__ */ new Set();
6596
+ const raw = row.reported;
6597
+ if (typeof raw !== "string" || raw.length === 0) return /* @__PURE__ */ new Set();
6598
+ try {
6599
+ const parsed = JSON.parse(raw);
6600
+ if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
6601
+ return new Set(parsed.filter((h) => typeof h === "string" && HASH_RE.test(h)));
6602
+ } catch {
6603
+ return /* @__PURE__ */ new Set();
6604
+ }
6605
+ },
6606
+ upsertServed(path$1, hashesJson) {
6607
+ stmts.servedUpsert(path$1, hashesJson, Date.now());
6608
+ },
6609
+ upsertServedReported(path$1, reportedJson) {
6610
+ stmts.servedReportedUpsert(path$1, reportedJson, Date.now());
6611
+ },
6612
+ clearServedReported(path$1) {
6613
+ stmts.servedReportedClear(path$1);
6614
+ },
6615
+ deleteServed(path$1) {
6616
+ stmts.servedDelete(path$1);
6617
+ },
6618
+ deleteServedByPath(path$1) {
6619
+ stmts.servedDeletePath(path$1);
6620
+ },
6621
+ pruneServedOlderThan(ts) {
6622
+ stmts.servedPruneOlderThan(ts);
6623
+ },
6624
+ async pruneMissing() {
6625
+ const missing = await statMissing(stmts.allPaths());
6626
+ if (missing.length === 0) return;
6627
+ withStore(() => {
6628
+ for (const path$1 of missing) {
6629
+ stmts.deleteOne(path$1);
6630
+ stmts.undoDelete(path$1);
6631
+ stmts.servedDeletePath(path$1);
6632
+ }
6633
+ });
6634
+ }
6668
6635
  };
6669
6636
  }
6670
- var ServedRejectionError = class extends Error {
6671
- code;
6672
- firstOffendingLine;
6673
- servedRows;
6674
- constructor(opts) {
6675
- super(opts.message);
6676
- this.name = "ServedRejectionError";
6677
- this.code = opts.code;
6678
- this.firstOffendingLine = opts.firstOffendingLine;
6679
- this.servedRows = opts.servedRows;
6680
- }
6681
- };
6682
- var AnchorMismatchError = class extends Error {
6683
- servedRows;
6684
- constructor(message, servedRows) {
6685
- super(message);
6686
- this.name = "AnchorMismatchError";
6687
- this.servedRows = servedRows;
6637
+ function isHealthy(db) {
6638
+ try {
6639
+ return db.prepare("PRAGMA quick_check").get()?.quick_check === "ok";
6640
+ } catch (error) {
6641
+ if (isCorruptionError(error)) return false;
6642
+ return true;
6688
6643
  }
6689
- };
6690
- function buildRangeEcho(startLine, endLine, fileHashes) {
6691
- const total = endLine - startLine + 1;
6692
- const shown = Math.min(total, SERVED_ECHO_CAP);
6693
- const rows = [];
6694
- for (let ln = startLine; ln < startLine + shown; ln++) rows.push({
6695
- position: ln - 1,
6696
- hash: fileHashes[ln - 1]
6697
- });
6698
- return rows;
6699
- }
6700
- function fmtServedRows(rows, fileLines) {
6701
- return rows.map((row) => `${row.hash}${HASH_SEP}${fileLines[row.position] ?? ""}`).join("\n");
6702
6644
  }
6703
- function retryHint() {
6704
- return "Retry with these anchors (no read needed).";
6645
+ async function quarantineStore(storePath) {
6646
+ const suffix = `.corrupt-${Date.now()}`;
6647
+ for (const candidate of [
6648
+ storePath,
6649
+ `${storePath}-wal`,
6650
+ `${storePath}-shm`
6651
+ ]) try {
6652
+ await rename(candidate, `${candidate}${suffix}`);
6653
+ } catch (error) {
6654
+ if (errCode(error) !== "ENOENT") console.error("Failed to quarantine corrupt hash store file:", error);
6655
+ }
6705
6656
  }
6706
- function paginationHint(nextOffset, more) {
6707
- return `[... ${more} more — read offset=${nextOffset}]`;
6657
+ function shutdownDb(db) {
6658
+ try {
6659
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
6660
+ } catch {}
6661
+ db.close();
6708
6662
  }
6709
- function verifyServedRange(args) {
6710
- const { served, startHash, endHash, startLine, endLine, fileHashes, fileLines, filePath } = args;
6711
- const where = filePath ? ` in ${filePath}` : "";
6712
- const echoRows = buildRangeEcho(startLine, endLine, fileHashes);
6713
- const totalLen = endLine - startLine + 1;
6714
- const tail = echoRows.length < totalLen ? `\n${paginationHint(startLine + echoRows.length, totalLen - echoRows.length)}` : "";
6715
- const echo = fmtServedRows(echoRows, fileLines) + tail;
6716
- const startPositions = servedPositionsOf(served, startHash);
6717
- const endPositions = servedPositionsOf(served, endHash);
6718
- const currentLen = endLine - startLine + 1;
6719
- let from;
6720
- let to;
6721
- if (startPositions.length === 1 && endPositions.length === 1) {
6722
- from = Math.min(startPositions[0], endPositions[0]);
6723
- to = Math.max(startPositions[0], endPositions[0]);
6724
- } else {
6725
- const candidates = [];
6726
- for (const s of startPositions) for (const e of endPositions) {
6727
- const candFrom = Math.min(s, e);
6728
- const candTo = Math.max(s, e);
6729
- if (candTo - candFrom + 1 !== currentLen) continue;
6730
- let ok = true;
6731
- for (let k = 0; k < currentLen; k++) if (served[candFrom + k] !== fileHashes[startLine - 1 + k]) {
6732
- ok = false;
6733
- break;
6734
- }
6735
- if (ok) candidates.push({
6736
- from: candFrom,
6737
- to: candTo
6738
- });
6739
- }
6740
- if (candidates.length === 1) {
6741
- from = candidates[0].from;
6742
- to = candidates[0].to;
6743
- } else if (candidates.length > 1) {
6744
- candidates.sort((a, b) => Math.abs(a.from - (startLine - 1)) - Math.abs(b.from - (startLine - 1)));
6745
- from = candidates[0].from;
6746
- to = candidates[0].to;
6747
- }
6663
+ const STAT_BATCH = 64;
6664
+ async function statMissing(rows) {
6665
+ const missing = [];
6666
+ for (let i = 0; i < rows.length; i += STAT_BATCH) {
6667
+ const batch = rows.slice(i, i + STAT_BATCH);
6668
+ const results = await Promise.all(batch.map(async (row) => {
6669
+ try {
6670
+ await stat(row.path);
6671
+ return;
6672
+ } catch {
6673
+ return row.path;
6674
+ }
6675
+ }));
6676
+ for (const path$1 of results) if (path$1 !== void 0) missing.push(path$1);
6748
6677
  }
6749
- if (from === void 0 || to === void 0) {
6750
- const problems = [];
6751
- if (startPositions.length === 0) problems.push(`remove_from "${startHash}" has no served position`);
6752
- else if (startPositions.length > 1) problems.push(`remove_from "${startHash}" was served at ${startPositions.length} positions`);
6753
- if (endPositions.length === 0) problems.push(`remove_to "${endHash}" has no served position`);
6754
- else if (endPositions.length > 1) problems.push(`remove_to "${endHash}" was served at ${endPositions.length} positions`);
6755
- throw new ServedRejectionError({
6756
- code: "E_RANGE_UNVERIFIED",
6757
- message: `[E_RANGE_UNVERIFIED] cannot verify range against served state${where}: ${problems.join("; ")}. Current range:
6758
- ${echo}
6759
- ${retryHint()}`,
6760
- servedRows: echoRows
6761
- });
6678
+ return missing;
6679
+ }
6680
+ async function openStore(storePath) {
6681
+ await initHasher();
6682
+ await mkdir(dirname(storePath), { recursive: true });
6683
+ let existed = existsSync(storePath);
6684
+ let opened;
6685
+ try {
6686
+ opened = openDbWithBusyRetry(storePath);
6687
+ } catch (error) {
6688
+ if (!isCorruptionError(error)) throw error;
6689
+ console.error("Hash store failed to open, rebuilding:", error);
6690
+ await quarantineStore(storePath);
6691
+ existed = false;
6692
+ opened = openDbWithBusyRetry(storePath);
6762
6693
  }
6763
- for (let i = from; i <= to; i++) if (served[i] === null) throw new ServedRejectionError({
6764
- code: "E_RANGE_UNSERVED",
6765
- message: `[E_RANGE_UNSERVED] line ${i + 1}${where} was never served.\nCurrent range:\n${echo}\n${retryHint()}`,
6766
- firstOffendingLine: i + 1,
6767
- servedRows: echoRows
6694
+ if (!isHealthy(opened.db)) {
6695
+ shutdownDb(opened.db);
6696
+ await quarantineStore(storePath);
6697
+ existed = false;
6698
+ opened = openDbWithBusyRetry(storePath);
6699
+ }
6700
+ const { db, stmts } = opened;
6701
+ if (!existed) await migrateLegacy(db, storePath);
6702
+ withBusyRetry(() => {
6703
+ stmts.servedPruneOlderThan(Date.now() - SERVED_TTL_MS);
6768
6704
  });
6769
- const servedLen = to - from + 1;
6770
- if (servedLen !== currentLen) throw new ServedRejectionError({
6771
- code: "E_RANGE_STALE",
6772
- message: `[E_RANGE_STALE] served span (${servedLen} lines) no longer matches current range (${currentLen} lines)${where}.\nCurrent range:\n${echo}\n${retryHint()}`,
6773
- firstOffendingLine: startLine,
6774
- servedRows: echoRows
6705
+ const store = makeDomainStore(stmts);
6706
+ stores.set(storePath, {
6707
+ path: storePath,
6708
+ db,
6709
+ stmts,
6710
+ store
6775
6711
  });
6776
- for (let k = 0; k < servedLen; k++) if (served[from + k] !== fileHashes[startLine - 1 + k]) {
6777
- const offendingLine = startLine + k;
6778
- throw new ServedRejectionError({
6779
- code: "E_RANGE_STALE",
6780
- message: `[E_RANGE_STALE] Line ${offendingLine}${where} differs from what you were served — the file changed on disk since it was read. Current range:\n${echo}\n${retryHint()}`,
6781
- firstOffendingLine: offendingLine,
6782
- servedRows: echoRows
6712
+ if (!exitHandlerRegistered) {
6713
+ exitHandlerRegistered = true;
6714
+ process.once("exit", () => shutdownHashStore());
6715
+ for (const sig of ["SIGINT", "SIGTERM"]) process.once(sig, () => {
6716
+ shutdownHashStore();
6717
+ process.kill(process.pid, sig);
6783
6718
  });
6784
6719
  }
6720
+ return store;
6785
6721
  }
6786
- async function recordEchoServes(sessionKey, path$1, rows, policy, lineCount) {
6787
- if (policy !== "live") return;
6788
- await recordServed(sessionKey, path$1, rows, lineCount);
6722
+ /** Resolve the store path: the single centralized store under `$DSH_HOME/storages`. */
6723
+ function storePathFor() {
6724
+ return hashStorePath();
6789
6725
  }
6790
- function buildIdx(content) {
6791
- const fileLines = splitLines(content);
6792
- const lineStarts = [];
6793
- let offset = 0;
6794
- for (let index = 0; index < fileLines.length; index++) {
6795
- lineStarts.push(offset);
6796
- offset += fileLines[index].length;
6797
- if (index < fileLines.length - 1) offset += 1;
6798
- }
6799
- return {
6800
- fileLines,
6801
- lineStarts
6802
- };
6726
+ /**
6727
+ * Load (and cache) the centralized hash store. There is exactly one store per
6728
+ * harness home; `DSH_HOME` isolates deployments (test homes never touch prod).
6729
+ */
6730
+ function loadHashStore() {
6731
+ const storePath = storePathFor();
6732
+ const cached = stores.get(storePath);
6733
+ if (cached && cached.db.isOpen) return Promise.resolve(cached.store);
6734
+ const existing = openings.get(storePath);
6735
+ if (existing) return existing;
6736
+ const promise = openStore(storePath).finally(() => {
6737
+ openings.delete(storePath);
6738
+ });
6739
+ openings.set(storePath, promise);
6740
+ return promise;
6803
6741
  }
6804
- function assertNotEmpty(originalContent, result) {
6805
- if (originalContent.length > 0 && result.length === 0) throw new Error("[E_WOULD_EMPTY] Cannot empty a non-empty file via edit. Use `write` if you need to clear the file.");
6742
+ /** The cached store entry for the active workspace (or the shared-home fallback), if open. */
6743
+ function currentStore() {
6744
+ const entry = stores.get(storePathFor());
6745
+ return entry?.db.isOpen ? entry : void 0;
6806
6746
  }
6807
- function resToSpan(edit, content, lineIndex) {
6808
- const { fileLines, lineStarts } = lineIndex;
6809
- const startLine = edit.hash_bounds[0].line;
6810
- const endLine = edit.hash_bounds[1].line;
6811
- const originalLines = fileLines.slice(startLine - 1, endLine);
6812
- if (originalLines.length === edit.content_lines.length && originalLines.every((line, lineIndex$1) => line === edit.content_lines[lineIndex$1])) return {
6813
- kind: "noop",
6814
- loc: edit.hash_bounds[0].hash,
6815
- currentContent: originalLines.join("\n")
6816
- };
6817
- if (edit.content_lines.length > 0) return {
6818
- kind: "replace",
6819
- start: lineStarts[startLine - 1],
6820
- end: lineStarts[endLine - 1] + fileLines[endLine - 1].length,
6821
- replacement: edit.content_lines.join("\n")
6822
- };
6823
- if (startLine === 1 && endLine === fileLines.length) return {
6824
- kind: "replace",
6825
- start: 0,
6826
- end: content.length,
6827
- replacement: ""
6828
- };
6829
- if (endLine < fileLines.length) return {
6830
- kind: "replace",
6831
- start: lineStarts[startLine - 1],
6832
- end: lineStarts[endLine],
6833
- replacement: ""
6834
- };
6835
- if (content.endsWith("\n")) return {
6836
- kind: "replace",
6837
- start: lineStarts[startLine - 1],
6838
- end: content.length,
6839
- replacement: ""
6840
- };
6841
- const prevLine = startLine >= 2 ? fileLines[startLine - 2] : void 0;
6842
- return {
6843
- kind: "replace",
6844
- start: prevLine !== void 0 && prevLine.length === 0 ? lineStarts[startLine - 1] : Math.max(0, lineStarts[startLine - 1] - 1),
6845
- end: content.length,
6846
- replacement: ""
6847
- };
6747
+ /** Close every open store (process exit, HMR, tests). */
6748
+ function shutdownHashStore() {
6749
+ for (const [, entry] of stores) shutdownDb(entry.db);
6750
+ stores.clear();
6751
+ openings.clear();
6848
6752
  }
6849
- function assemble(content, span, signal) {
6850
- abortIf(signal);
6851
- return content.slice(0, span.start) + span.replacement + content.slice(span.end);
6753
+ /**
6754
+ * Run `fn` inside one BEGIN IMMEDIATE transaction on the active workspace's
6755
+ * store. Without an open store for this context the call runs bare (the
6756
+ * caller has already loaded the store in every in-process path).
6757
+ */
6758
+ function withStore(fn) {
6759
+ const store = currentStore();
6760
+ if (store) withBusyRetry(() => {
6761
+ store.db.exec("BEGIN IMMEDIATE");
6762
+ try {
6763
+ fn();
6764
+ store.db.exec("COMMIT");
6765
+ } catch (e) {
6766
+ try {
6767
+ store.db.exec("ROLLBACK");
6768
+ } catch {}
6769
+ throw e;
6770
+ }
6771
+ });
6772
+ else fn();
6852
6773
  }
6853
- function applyEdit(content, edit, signal, precomputedHashes, filePath, served) {
6854
- abortIf(signal);
6855
- const lineIndex = buildIdx(content);
6856
- const fileHashes = precomputedHashes ?? lineHashesPure(content);
6857
- const warnings = [];
6858
- const prefixFixed = stripDiffPrefixes(stripBarePrefixes(swapReversedRanges(edit, fileHashes, warnings), fileHashes, warnings), warnings);
6859
- const { resolved: initialResolved, mismatches, boundaryDups } = valEdit(prefixFixed, lineIndex.fileLines, fileHashes, warnings, signal);
6860
- if (mismatches.length || !initialResolved) {
6861
- const { message, servedRows } = fmtMismatchWithServes(mismatches, lineIndex.fileLines, fileHashes, filePath);
6862
- throw new AnchorMismatchError(message, servedRows);
6774
+ async function migrateLegacy(db, storePath) {
6775
+ const legacyPath = join(dirname(storePath), "hash-store.json");
6776
+ let content;
6777
+ try {
6778
+ content = await readFile(legacyPath, "utf-8");
6779
+ } catch (error) {
6780
+ if (errCode(error) === "ENOENT") return;
6781
+ console.error("Failed to read legacy hash store for migration:", error);
6782
+ return;
6863
6783
  }
6864
- warnUnicodeEsc(prefixFixed, warnings);
6865
- let resolved = initialResolved;
6866
- let autoFixes;
6867
- if (boundaryDups.length > 0) {
6868
- autoFixes = [];
6869
- const correctedEdit = {
6870
- ...prefixFixed,
6871
- content_lines: [...prefixFixed.content_lines]
6872
- };
6873
- const seen = /* @__PURE__ */ new Set();
6874
- const uniqueDups = [];
6875
- for (const dup of boundaryDups) {
6876
- if (seen.has(dup.replacementLineIndex)) continue;
6877
- seen.add(dup.replacementLineIndex);
6878
- uniqueDups.push(dup);
6879
- }
6880
- const dupsByIndex = uniqueDups.sort((a, b) => b.replacementLineIndex - a.replacementLineIndex);
6881
- for (const dup of dupsByIndex) {
6882
- const idx = dup.replacementLineIndex;
6883
- if (idx < 0 || idx >= correctedEdit.content_lines.length) continue;
6884
- const removed = correctedEdit.content_lines.splice(idx, 1)[0];
6885
- autoFixes.push({
6886
- kind: dup.kind,
6887
- removedLine: removed,
6888
- removedLineIndex: idx
6889
- });
6784
+ let parsed;
6785
+ try {
6786
+ parsed = JSON.parse(content);
6787
+ } catch (error) {
6788
+ console.error("Failed to parse legacy hash store, skipping migration:", error);
6789
+ return;
6790
+ }
6791
+ const raw = parsed.snapshots;
6792
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
6793
+ const rows = [];
6794
+ for (const [key, value] of Object.entries(raw)) {
6795
+ if (!isValidSnapshot(value)) continue;
6796
+ if (new Set(value.hashes).size !== value.hashes.length) {
6797
+ console.warn(`Skipped legacy snapshot with duplicate hashes for ${key}; it will be re-hashed on next read.`);
6798
+ continue;
6890
6799
  }
6891
- const correctedResult = valEdit(correctedEdit, lineIndex.fileLines, fileHashes, warnings, signal);
6892
- if (correctedResult.mismatches.length || !correctedResult.resolved) {
6893
- const { message, servedRows } = fmtMismatchWithServes(correctedResult.mismatches, lineIndex.fileLines, fileHashes, filePath);
6894
- throw new AnchorMismatchError(message, servedRows);
6800
+ rows.push([
6801
+ key,
6802
+ contentChecksum(value.content),
6803
+ splitLines(value.content).length,
6804
+ JSON.stringify(value.hashes),
6805
+ Date.now()
6806
+ ]);
6807
+ }
6808
+ if (rows.length > 0) {
6809
+ db.exec("BEGIN IMMEDIATE");
6810
+ try {
6811
+ const stmt = db.prepare("INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)");
6812
+ for (const row of rows) stmt.run(...row);
6813
+ db.exec("COMMIT");
6814
+ } catch (e) {
6815
+ db.exec("ROLLBACK");
6816
+ throw e;
6895
6817
  }
6896
- resolved = correctedResult.resolved;
6897
6818
  }
6898
- if (served) {
6899
- const startAnchor = resolved.hash_bounds[0];
6900
- const endAnchor = resolved.hash_bounds[1];
6901
- verifyServedRange({
6902
- served,
6903
- startHash: startAnchor.hash,
6904
- endHash: endAnchor.hash,
6905
- startLine: startAnchor.line,
6906
- endLine: endAnchor.line,
6907
- fileHashes,
6908
- fileLines: lineIndex.fileLines,
6909
- filePath
6910
- });
6819
+ try {
6820
+ await rename(legacyPath, `${legacyPath}.bak`);
6821
+ } catch (error) {
6822
+ console.error("Failed to rename legacy hash store after migration:", error);
6911
6823
  }
6912
- const spanResult = resToSpan(resolved, content, lineIndex);
6913
- if (spanResult.kind === "noop") return {
6914
- content,
6915
- firstChangedLine: void 0,
6916
- lastChangedLine: void 0,
6917
- range: resolvedRange(resolved),
6918
- ...warnings.length ? { warnings } : {},
6919
- noopEdit: {
6920
- loc: spanResult.loc,
6921
- currentContent: spanResult.currentContent
6922
- }
6923
- };
6924
- const result = assemble(content, spanResult, signal);
6925
- assertNotEmpty(content, result);
6926
- const changed = changedRange(content, result);
6927
- return {
6928
- content: result,
6929
- firstChangedLine: changed?.firstChangedLine,
6930
- lastChangedLine: changed?.lastChangedLine,
6931
- range: resolvedRange(resolved),
6932
- ...warnings.length ? { warnings } : {},
6933
- ...autoFixes ? { autoFixes } : {}
6934
- };
6935
6824
  }
6936
- function resolvedRange(resolved) {
6937
- const [start, end] = resolved.hash_bounds;
6938
- return {
6939
- startLine: start.line,
6940
- endLine: end.line,
6941
- startHash: start.hash,
6942
- endHash: end.hash,
6943
- delta: resolved.content_lines.length - (Math.abs(end.line - start.line) + 1)
6944
- };
6825
+ /** Find files whose stored snapshot hashes contain every given anchor. */
6826
+ async function findSnapshotPathsByHashes(hashes) {
6827
+ return (await loadHashStore()).findSnapshotPaths(hashes);
6945
6828
  }
6946
- function fmtRegion(hashes, lines) {
6947
- if (hashes.length !== lines.length) throw new Error(`fmtRegion: hashes.length (${hashes.length}) must match lines.length (${lines.length}).`);
6948
- return lines.map((line, index) => `${hashes[index]}${HASH_SEP}${line}`).join("\n");
6829
+ /** Persist a hash snapshot for one path (async over the active store). */
6830
+ async function upsertSnapshotFor(path$1, checksum, lineCount, hashes) {
6831
+ (await loadHashStore()).upsertSnapshot(path$1, checksum, lineCount, hashes);
6949
6832
  }
6950
- function changedRange(original, result) {
6951
- if (original === result) return null;
6952
- if (original.length === 0) return {
6953
- firstChangedLine: 1,
6954
- lastChangedLine: splitLines(result).length
6833
+
6834
+ //#endregion
6835
+ //#region src/url-schema/vendored/hashline/hashline/hash.js
6836
+ let defaultHashSnapshotIO;
6837
+ function snapshotIOFor(store) {
6838
+ if (store) return {
6839
+ get: (path$1, content, deleteCorrupt) => Promise.resolve(store.getSnapshot(path$1, content, deleteCorrupt)),
6840
+ upsert: (path$1, checksum, lineCount, hashes) => {
6841
+ store.upsertSnapshot(path$1, checksum, lineCount, hashes);
6842
+ return Promise.resolve();
6843
+ }
6955
6844
  };
6956
- const originalLines = splitLines(original);
6957
- const resultLines = splitLines(result);
6958
- if (originalLines.length === resultLines.length && originalLines.every((line, index) => line === resultLines[index])) return null;
6959
- const minLen = Math.min(originalLines.length, resultLines.length);
6960
- let first = 0;
6961
- while (first < minLen && originalLines[first] === resultLines[first]) first++;
6962
- let lastOrig = originalLines.length - 1;
6963
- let lastRes = resultLines.length - 1;
6964
- while (lastOrig >= first && lastRes >= first && originalLines[lastOrig] === resultLines[lastRes]) {
6965
- lastOrig--;
6966
- lastRes--;
6967
- }
6968
- return {
6969
- firstChangedLine: first + 1,
6970
- lastChangedLine: Math.max(first, lastRes) + 1
6845
+ return defaultHashSnapshotIO ?? {
6846
+ get: async (path$1, content, deleteCorrupt) => {
6847
+ return (await loadHashStore()).getSnapshot(path$1, content, deleteCorrupt);
6848
+ },
6849
+ upsert: async (path$1, checksum, lineCount, hashes) => {
6850
+ (await loadHashStore()).upsertSnapshot(path$1, checksum, lineCount, hashes);
6851
+ }
6971
6852
  };
6972
6853
  }
6854
+ async function lineHashes(content, path$1, previous, ioOrStore, persist) {
6855
+ await initHasher();
6856
+ if (!path$1) return lineHashesPure(content);
6857
+ const io = ioOrStore && "getSnapshot" in ioOrStore ? snapshotIOFor(ioOrStore) : ioOrStore ?? snapshotIOFor(void 0);
6858
+ if (previous) {
6859
+ const newHashes$1 = mapStableHashes(previous.content, previous.hashes, content, previous.removedHashes);
6860
+ if (persist !== false && io) try {
6861
+ await io.upsert(path$1, contentChecksum(content), splitLines(content).length, newHashes$1);
6862
+ } catch (e) {
6863
+ console.error("Failed to persist hash snapshot:", e);
6864
+ }
6865
+ return newHashes$1;
6866
+ }
6867
+ let cached;
6868
+ if (io) try {
6869
+ cached = await io.get(path$1, content, persist !== false);
6870
+ } catch (e) {
6871
+ console.error("Failed to read hash store snapshot:", e);
6872
+ }
6873
+ if (cached) return cached;
6874
+ const newHashes = lineHashesPure(content);
6875
+ if (persist !== false && io) try {
6876
+ await io.upsert(path$1, contentChecksum(content), splitLines(content).length, newHashes);
6877
+ } catch (e) {
6878
+ console.error("Failed to persist hash snapshot:", e);
6879
+ }
6880
+ return newHashes;
6881
+ }
6973
6882
 
6974
6883
  //#endregion
6975
6884
  //#region src/url-schema/vendored/hashline/session-view.js
@@ -6977,20 +6886,9 @@ const current = new AsyncLocalStorage();
6977
6886
  function withWorkspace(cwd, fn) {
6978
6887
  return current.run(cwd, fn);
6979
6888
  }
6980
- function workspaceCwd() {
6981
- return current.getStore();
6982
- }
6983
- let fallbackSessionKey;
6984
- function sessionKeyFor(sessionId) {
6985
- if (sessionId && sessionId.length > 0) return sessionId;
6986
- return fallbackSessionKey ??= randomUUID();
6987
- }
6988
6889
  function execCwd(exec) {
6989
6890
  return exec.agent?.session.header.cwd ?? process.cwd();
6990
6891
  }
6991
- function execSessionKey(exec) {
6992
- return sessionKeyFor(exec.agent?.session.id);
6993
- }
6994
6892
  /**
6995
6893
  * Merge served rows into a copy of the stored array. This single helper owns
6996
6894
  * the served-merge invariant shared by recordServed and recordServedTruncated.
@@ -7032,67 +6930,67 @@ function _mergeServedRows(current$1, rows, options) {
7032
6930
  while (updated.length > 0 && updated[updated.length - 1] === null) updated.pop();
7033
6931
  return updated;
7034
6932
  }
7035
- async function loadServed(sessionKey, path$1) {
7036
- return (await loadHashStore()).getServed(sessionKey, path$1);
6933
+ async function loadServed(path$1) {
6934
+ return (await loadHashStore()).getServed(path$1);
7037
6935
  }
7038
- async function recordServed(sessionKey, path$1, rows, lineCount) {
6936
+ async function recordServed(path$1, rows, lineCount) {
7039
6937
  if (rows.length === 0) return;
7040
6938
  try {
7041
6939
  const store = await loadHashStore();
7042
6940
  withStore(() => {
7043
- const current$1 = store.getServed(sessionKey, path$1);
6941
+ const current$1 = store.getServed(path$1);
7044
6942
  const updated = _mergeServedRows(current$1, rows, lineCount === void 0 ? void 0 : { truncateTo: lineCount });
7045
6943
  if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
7046
- store.upsertServed(sessionKey, path$1, JSON.stringify(updated));
6944
+ store.upsertServed(path$1, JSON.stringify(updated));
7047
6945
  });
7048
6946
  } catch (error) {
7049
6947
  console.error("Failed to record served rows:", error);
7050
6948
  }
7051
6949
  }
7052
- async function recordServedTruncated(sessionKey, path$1, rows, lineCount, clearFrom = 0) {
6950
+ async function recordServedTruncated(path$1, rows, lineCount, clearFrom = 0) {
7053
6951
  if (rows.length === 0) return;
7054
6952
  try {
7055
6953
  const store = await loadHashStore();
7056
6954
  withStore(() => {
7057
- const current$1 = store.getServed(sessionKey, path$1);
6955
+ const current$1 = store.getServed(path$1);
7058
6956
  const updated = _mergeServedRows(current$1, rows, {
7059
6957
  truncateTo: lineCount,
7060
6958
  clearFrom
7061
6959
  });
7062
6960
  if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
7063
- store.upsertServed(sessionKey, path$1, JSON.stringify(updated));
6961
+ store.upsertServed(path$1, JSON.stringify(updated));
7064
6962
  });
7065
6963
  } catch (error) {
7066
6964
  console.error("Failed to record truncated served rows:", error);
7067
6965
  }
7068
6966
  }
7069
- async function driftReported(sessionKey, path$1) {
6967
+ async function driftReported(path$1) {
7070
6968
  try {
7071
- return (await loadHashStore()).getServedReported(sessionKey, path$1);
6969
+ return (await loadHashStore()).getServedReported(path$1);
7072
6970
  } catch (error) {
7073
6971
  console.error("Failed to load reported drift set:", error);
7074
6972
  return /* @__PURE__ */ new Set();
7075
6973
  }
7076
6974
  }
7077
- async function markDriftReported(sessionKey, path$1, hashes) {
6975
+ async function markDriftReported(path$1, hashes) {
7078
6976
  try {
7079
6977
  const valid = hashes.filter((hash) => HASH_RE.test(hash));
7080
6978
  if (valid.length === 0) return;
7081
6979
  const store = await loadHashStore();
7082
6980
  withStore(() => {
7083
- const current$1 = store.getServedReported(sessionKey, path$1);
6981
+ const current$1 = store.getServedReported(path$1);
7084
6982
  for (const hash of valid) current$1.add(hash);
7085
- store.upsertServedReported(sessionKey, path$1, JSON.stringify([...current$1]));
6983
+ store.upsertServedReported(path$1, JSON.stringify([...current$1]));
7086
6984
  });
7087
6985
  } catch (error) {
7088
6986
  console.error("Failed to record reported drift set:", error);
7089
6987
  }
7090
6988
  }
7091
- async function clearDriftReported(sessionKey, path$1) {
6989
+ async function clearDriftReported(path$1) {
7092
6990
  try {
7093
6991
  const store = await loadHashStore();
7094
6992
  withStore(() => {
7095
- store.clearServedReported(sessionKey, path$1);
6993
+ store.clearServedReported(path$1);
7096
6994
  });
7097
6995
  } catch (error) {
7098
6996
  console.error("Failed to clear reported drift set:", error);
@@ -7157,600 +7055,693 @@ function computeDrift(input) {
7157
7055
  if (currentPos >= 0 && currentPos < resultHashes.length && currentPos < resultLines.length) driftedPositions.push(currentPos);
7158
7056
  else unshown++;
7159
7057
  }
7160
- if (total === 0) return void 0;
7161
- const countLabel = `${total} line(s)`;
7162
- if (!anyNotReported) return {
7163
- text: `${DRIFT_NOTICE_HEADING} ${countLabel} changed outside the range (already reported) — re-read to refresh.`,
7164
- rows: [],
7165
- total,
7166
- allAlreadyReported: true
7058
+ if (total === 0) return void 0;
7059
+ const countLabel = `${total} line(s)`;
7060
+ if (!anyNotReported) return {
7061
+ text: `${DRIFT_NOTICE_HEADING} ${countLabel} changed outside the range (already reported) — re-read to refresh.`,
7062
+ rows: [],
7063
+ total,
7064
+ allAlreadyReported: true
7065
+ };
7066
+ const driftedSet = new Set(driftedPositions);
7067
+ const windowSet = /* @__PURE__ */ new Set();
7068
+ for (const pos of driftedPositions) for (const w of [
7069
+ pos - 1,
7070
+ pos,
7071
+ pos + 1
7072
+ ]) if (w >= 0 && w < resultLines.length) windowSet.add(w);
7073
+ const windowPositions = [...windowSet].sort((a, b) => a - b);
7074
+ const shownPositions = windowPositions.slice(0, cap);
7075
+ unshown += windowPositions.length - shownPositions.length;
7076
+ const rows = shownPositions.map((position) => ({
7077
+ position,
7078
+ hash: resultHashes[position],
7079
+ content: resultLines[position],
7080
+ drifted: driftedSet.has(position)
7081
+ }));
7082
+ return {
7083
+ text: `${DRIFT_NOTICE_HEADING} ${countLabel} changed outside the range:\n${fmtServedRows(rows, resultLines)}${unshown > 0 ? `\n[... ${unshown} more — re-read to see]` : ""}`,
7084
+ rows,
7085
+ total,
7086
+ allAlreadyReported: false
7087
+ };
7088
+ }
7089
+ async function scanDrift(input) {
7090
+ const reported = await driftReported(input.path);
7091
+ const result = computeDrift({
7092
+ ...input,
7093
+ reported
7094
+ });
7095
+ if (!result || result.allAlreadyReported) return result?.text;
7096
+ await recordServed(input.path, result.rows.map((row) => ({
7097
+ position: row.position,
7098
+ hash: row.hash
7099
+ })), input.resultLines.length);
7100
+ await markDriftReported(input.path, result.rows.filter((row) => row.drifted).map((row) => row.hash));
7101
+ return result.text;
7102
+ }
7103
+
7104
+ //#endregion
7105
+ //#region src/url-schema/vendored/hashline/hashline/anchor-pipeline.js
7106
+ function diagRef(ref) {
7107
+ const trimmed = ref.trim();
7108
+ if (!trimmed.length) return `[E_BAD_REF] Invalid anchor. Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
7109
+ if (/^\d+/.test(trimmed)) return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. "aB3") — no line numbers or trailing content.`;
7110
+ if (trimmed.includes("│")) return `[E_BAD_REF] Invalid anchor "${trimmed}". remove_from and remove_to must contain the 3-char hash only — remove everything from "│" onward.`;
7111
+ return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
7112
+ }
7113
+ function parseRef(ref) {
7114
+ const trimmed = ref.trim();
7115
+ if (trimmed.length === ANCHOR_LEN && ALPH_RE.test(trimmed)) return { hash: trimmed };
7116
+ throw new Error(diagRef(ref));
7117
+ }
7118
+ const parseHashRef = parseRef;
7119
+ function parseText(edit) {
7120
+ if (typeof edit !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
7121
+ const normalized = edit.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
7122
+ if (normalized === "") return [];
7123
+ if (/^\n+$/.test(normalized)) return new Array(normalized.length).fill("");
7124
+ return normalized.split("\n");
7125
+ }
7126
+ function resAnchorFromMap(ref, hashIndex) {
7127
+ const hashMatches = hashIndex.get(ref.hash);
7128
+ if (!hashMatches || hashMatches.length === 0) return {
7129
+ ref,
7130
+ kind: "not_found"
7131
+ };
7132
+ if (hashMatches.length === 1) return {
7133
+ line: hashMatches[0],
7134
+ hash: ref.hash,
7135
+ hashMatched: true
7136
+ };
7137
+ return {
7138
+ ref,
7139
+ kind: "ambiguous",
7140
+ candidates: hashMatches
7141
+ };
7142
+ }
7143
+ function assertAligned(fileLines, fileHashes, ctx) {
7144
+ if (fileHashes.length !== fileLines.length) throw new Error(`${ctx}: fileHashes.length (${fileHashes.length}) must match fileLines.length (${fileLines.length}).`);
7145
+ }
7146
+ function fmtMismatchWithServes(mismatches, fileLines, fileHashes, filePath) {
7147
+ assertAligned(fileLines, fileHashes, "fmtMismatch");
7148
+ const out = [];
7149
+ const servedRows = [];
7150
+ const seen = /* @__PURE__ */ new Set();
7151
+ const pushRow = (ln) => {
7152
+ if (ln < 1 || ln > fileLines.length) return;
7153
+ const position = ln - 1;
7154
+ if (seen.has(position)) return;
7155
+ seen.add(position);
7156
+ servedRows.push({
7157
+ position,
7158
+ hash: fileHashes[ln - 1]
7159
+ });
7160
+ };
7161
+ const notFound = mismatches.filter((m) => m.kind === "not_found");
7162
+ const ambiguous = mismatches.filter((m) => m.kind === "ambiguous");
7163
+ const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
7164
+ if (notFound.length > 0) {
7165
+ out.push(`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. Re-read for fresh anchors.`);
7166
+ for (const m of notFound) {
7167
+ const ctx = m.context;
7168
+ if (!ctx) continue;
7169
+ const from = Math.max(1, ctx.line - 1);
7170
+ const to = Math.min(fileLines.length, ctx.line + 1);
7171
+ const rows = [];
7172
+ for (let ln = from; ln <= to; ln++) {
7173
+ rows.push(` ${ln}: ${fileHashes[ln - 1]}│${clipLine(fileLines[ln - 1] ?? "")}`);
7174
+ pushRow(ln);
7175
+ }
7176
+ out.push("");
7177
+ out.push(` Current context around resolved anchor "${ctx.hash}" (line ${ctx.line}):\n${rows.join("\n")}`);
7178
+ }
7179
+ }
7180
+ if (ambiguous.length > 0) {
7181
+ if (out.length > 0) out.push("");
7182
+ out.push(`[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}. Re-read for fresh anchors.`);
7183
+ for (const m of ambiguous) {
7184
+ const sample = (m.candidates ?? []).slice(0, 5);
7185
+ const more = (m.candidates?.length ?? 0) > sample.length ? `, ... (+${(m.candidates?.length ?? 0) - sample.length} more)` : "";
7186
+ const lines = sample.map((line) => {
7187
+ const content = clipLine(fileLines[line - 1] ?? "");
7188
+ pushRow(line);
7189
+ return ` ${line}: ${fileHashes[line - 1]}│${content}`;
7190
+ }).join("\n");
7191
+ out.push(` Hash "${m.ref.hash}" matches lines ${sample.join(", ")}${more}.\n${lines}`);
7192
+ }
7193
+ }
7194
+ return {
7195
+ message: out.join("\n"),
7196
+ servedRows
7167
7197
  };
7168
- const driftedSet = new Set(driftedPositions);
7169
- const windowSet = /* @__PURE__ */ new Set();
7170
- for (const pos of driftedPositions) for (const w of [
7171
- pos - 1,
7172
- pos,
7173
- pos + 1
7174
- ]) if (w >= 0 && w < resultLines.length) windowSet.add(w);
7175
- const windowPositions = [...windowSet].sort((a, b) => a - b);
7176
- const shownPositions = windowPositions.slice(0, cap);
7177
- unshown += windowPositions.length - shownPositions.length;
7178
- const rows = shownPositions.map((position) => ({
7179
- position,
7180
- hash: resultHashes[position],
7181
- content: resultLines[position],
7182
- drifted: driftedSet.has(position)
7183
- }));
7198
+ }
7199
+ const ITEM_KS = new Set([
7200
+ "replacement_text",
7201
+ "remove_from",
7202
+ "remove_to"
7203
+ ]);
7204
+ function assertItem(edit) {
7205
+ rejectUnknownFields(edit, ITEM_KS, "Edit", "The edit takes only { replacement_text, remove_from, remove_to }.");
7206
+ if ("remove_from" in edit && typeof edit.remove_from !== "string") throw new Error(`[E_BAD_SHAPE] Field "remove_from" must be an anchor string (3-char hash).`);
7207
+ if ("remove_to" in edit && typeof edit.remove_to !== "string") throw new Error(`[E_BAD_SHAPE] Field "remove_to" must be an anchor string (3-char hash).`);
7208
+ if (!("replacement_text" in edit)) throw new Error(`[E_BAD_SHAPE] The edit requires a "replacement_text" field. Provide the replacement text (use "" to delete).`);
7209
+ if (typeof edit.replacement_text !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
7210
+ if (typeof edit.remove_from !== "string" || typeof edit.remove_to !== "string") throw new Error(`[E_BAD_SHAPE] The edit requires "remove_from" and "remove_to" anchor strings (3-char hashes from read output).`);
7211
+ }
7212
+ const ANCHOR_ROW_RE = /* @__PURE__ */ new RegExp(`^([+-]?)(${HASH_CLASS})│`);
7213
+ function resEdit(edit, warnings) {
7214
+ assertItem(edit);
7215
+ const editLines = parseText(edit.replacement_text);
7216
+ const bounds = [edit.remove_from, edit.remove_to].map((ref) => {
7217
+ const trimmed = ref.trim();
7218
+ const match = trimmed.match(ANCHOR_ROW_RE);
7219
+ if (match) {
7220
+ let message;
7221
+ if (match[1] === "+") message = `[E_BAD_REF] stripped diff-preview marker from remove_from/remove_to "${trimmed}".`;
7222
+ else if (match[1] === "-") message = `[E_BAD_REF] stripped leading "-" marker from remove_from/remove_to "${trimmed}".`;
7223
+ else message = `[E_BAD_REF] stripped "HASH│" prefix from remove_from/remove_to "${trimmed}".`;
7224
+ warnings?.push(message);
7225
+ return match[2];
7226
+ }
7227
+ return ref;
7228
+ });
7184
7229
  return {
7185
- text: `${DRIFT_NOTICE_HEADING} ${countLabel} changed outside the range:\n${fmtServedRows(rows, resultLines)}${unshown > 0 ? `\n[... ${unshown} more — re-read to see]` : ""}`,
7186
- rows,
7187
- total,
7188
- allAlreadyReported: false
7230
+ content_lines: editLines,
7231
+ hash_bounds: [parseHashRef(bounds[0]), parseHashRef(bounds[1])]
7189
7232
  };
7190
7233
  }
7191
- async function scanDrift(input) {
7192
- const reported = await driftReported(input.sessionKey, input.path);
7193
- const result = computeDrift({
7194
- ...input,
7195
- reported
7234
+ function warnUnicodeEsc(edit, warnings) {
7235
+ if (edit.content_lines.some((line) => /\\uDDDD/i.test(line))) warnings.push("Detected literal \\uDDDD in edit content; no autocorrection applied. Verify whether this should be a real Unicode escape or plain text.");
7236
+ }
7237
+ /** @internal — private to anchor-pipeline seam; do not import directly, use anchor-pipeline.ts */
7238
+ function stripBarePrefixes(edit, fileHashes, warnings) {
7239
+ const fileHashSet = new Set(fileHashes);
7240
+ const stripped = [];
7241
+ const contentLines = edit.content_lines.map((line, lineIndex) => {
7242
+ const match = line.match(HL_BARE_PREFIX_RE);
7243
+ if (!match) return line;
7244
+ stripped.push({
7245
+ lineIndex,
7246
+ matched: fileHashSet.has(match[1])
7247
+ });
7248
+ return line.slice(match[0].length);
7196
7249
  });
7197
- if (!result || result.allAlreadyReported) return result?.text;
7198
- await recordServed(input.sessionKey, input.path, result.rows.map((row) => ({
7199
- position: row.position,
7200
- hash: row.hash
7201
- })), input.resultLines.length);
7202
- await markDriftReported(input.sessionKey, input.path, result.rows.filter((row) => row.drifted).map((row) => row.hash));
7203
- return result.text;
7250
+ if (stripped.length === 0) return edit;
7251
+ const locations = stripped.map((s) => `replacement_text line ${s.lineIndex + 1}`).join(", ");
7252
+ const matchedCount = stripped.filter((s) => s.matched).length;
7253
+ const evidence = matchedCount === 0 ? "0 matched — verify literal 'HASH│' content" : `${matchedCount}/${stripped.length} matched`;
7254
+ warnings.push(`[E_BARE_HASH_PREFIX] stripped "HASH│" prefix from ${locations} (${evidence}).`);
7255
+ return {
7256
+ ...edit,
7257
+ content_lines: contentLines
7258
+ };
7204
7259
  }
7205
-
7206
- //#endregion
7207
- //#region src/url-schema/vendored/hashline/hash-store.js
7208
- function isValidHashList(value) {
7209
- if (!Array.isArray(value)) return false;
7210
- for (const hash of value) if (typeof hash !== "string" || !HASH_RE.test(hash)) return false;
7211
- return true;
7260
+ /** @internal — private to anchor-pipeline seam */
7261
+ function stripDiffPrefixes(edit, warnings) {
7262
+ const stripped = [];
7263
+ const contentLines = edit.content_lines.map((line, lineIndex) => {
7264
+ const plus = line.match(HL_PREFIX_PLUS_RE);
7265
+ if (plus) {
7266
+ stripped.push(lineIndex);
7267
+ return line.slice(plus[0].length);
7268
+ }
7269
+ const minus = line.match(HL_PREFIX_MINUS_RE);
7270
+ if (minus) {
7271
+ stripped.push(lineIndex);
7272
+ return line.slice(minus[0].length);
7273
+ }
7274
+ return line;
7275
+ });
7276
+ if (stripped.length === 0) return edit;
7277
+ const locations = stripped.map((i) => `replacement_text line ${i + 1}`).join(", ");
7278
+ warnings.push(`[E_INVALID_PATCH] stripped diff-preview marker from ${locations}.`);
7279
+ return {
7280
+ ...edit,
7281
+ content_lines: contentLines
7282
+ };
7212
7283
  }
7213
- function isValidSnapshot(value) {
7214
- if (typeof value !== "object" || value === null) return false;
7215
- const v = value;
7216
- if (typeof v.content !== "string") return false;
7217
- return isValidHashList(v.hashes);
7284
+ /** @internal — private to anchor-pipeline seam */
7285
+ function swapReversedRanges(edit, fileHashes, warnings) {
7286
+ const lineByHash = /* @__PURE__ */ new Map();
7287
+ for (let i = 0; i < fileHashes.length; i++) lineByHash.set(fileHashes[i], i + 1);
7288
+ const [startRef, endRef] = edit.hash_bounds;
7289
+ const startLine = lineByHash.get(startRef.hash);
7290
+ const endLine = lineByHash.get(endRef.hash);
7291
+ if (startLine === void 0 || endLine === void 0 || startLine <= endLine) return edit;
7292
+ warnings.push(`[E_BAD_OP] reversed remove_from/remove_to (${startRef.hash} after ${endRef.hash}); swapped.`);
7293
+ return {
7294
+ ...edit,
7295
+ hash_bounds: [endRef, startRef]
7296
+ };
7218
7297
  }
7219
- /** A served-row array: per-position hash, or null for never-served slots. */
7220
- function isValidServedList(value) {
7221
- if (!Array.isArray(value)) return false;
7222
- for (const entry of value) {
7223
- if (entry === null) continue;
7224
- if (typeof entry !== "string" || !HASH_RE.test(entry)) return false;
7298
+ function trailingDups(contentLines, fileLines, endLine) {
7299
+ const start = lastNonEmptyIndex(contentLines);
7300
+ if (start < 0) return [];
7301
+ const dups = [];
7302
+ const maxK = Math.min(start + 1, fileLines.length - endLine);
7303
+ for (let k = 0; k < maxK; k++) {
7304
+ if (contentLines[start - k] !== fileLines[endLine + k]) break;
7305
+ dups.push({
7306
+ kind: "trailing",
7307
+ replacementLineIndex: start - k
7308
+ });
7225
7309
  }
7226
- return true;
7227
- }
7228
- function cacheKey(checksum) {
7229
- return `${CANON_VERSION}:${checksum}`;
7310
+ return dups;
7230
7311
  }
7231
- function isCorruptionError(error) {
7232
- if (error && typeof error === "object") {
7233
- const errcode = error.errcode;
7234
- if (typeof errcode === "number") return errcode === 11 || errcode === 24 || errcode === 26;
7235
- const code = error.code;
7236
- if (typeof code === "string" && /NOTADB|CORRUPT/.test(code)) return true;
7312
+ function leadingDups(contentLines, fileLines, startLine) {
7313
+ const start = firstNonEmptyIndex(contentLines);
7314
+ if (start < 0) return [];
7315
+ const dups = [];
7316
+ const maxK = Math.min(contentLines.length - start, startLine - 1);
7317
+ for (let k = 0; k < maxK; k++) {
7318
+ if (contentLines[start + k] !== fileLines[startLine - 2 - k]) break;
7319
+ dups.push({
7320
+ kind: "leading",
7321
+ replacementLineIndex: start + k
7322
+ });
7237
7323
  }
7238
- return error instanceof Error && /corrupt|not a database|malformed|database disk image/i.test(error.message);
7324
+ return dups;
7239
7325
  }
7240
- function isBusyError(error) {
7241
- if (error && typeof error === "object") {
7242
- const errcode = error.errcode;
7243
- if (typeof errcode === "number") return errcode === 5 || errcode === 6;
7326
+ function sectionIsUnique(canonLines, start, length) {
7327
+ let count = 0;
7328
+ for (let i = 0; i + length <= canonLines.length; i++) {
7329
+ let k = 0;
7330
+ while (k < length && canonLines[i + k] === canonLines[start + k]) k++;
7331
+ if (k < length) continue;
7332
+ count++;
7333
+ if (count > 1) return false;
7244
7334
  }
7245
- return error instanceof Error && /busy|locked/i.test(error.message);
7335
+ return true;
7246
7336
  }
7247
- function sleepSync(ms) {
7248
- const sab = new Int32Array(new SharedArrayBuffer(4));
7249
- Atomics.wait(sab, 0, 0, ms);
7337
+ function firstNewAfterDups(contentLines, rangeLines, canonLines, endLine) {
7338
+ const firstNew = findNewEdge(contentLines, rangeLines, false);
7339
+ if (!firstNew) return [];
7340
+ const maxK = Math.min(contentLines.length - firstNew.index, canonLines.length - endLine);
7341
+ let runLen = 0;
7342
+ while (runLen < maxK && canon(contentLines[firstNew.index + runLen]) === canonLines[endLine + runLen]) runLen++;
7343
+ if (runLen === 0 || !sectionIsUnique(canonLines, endLine, runLen)) return [];
7344
+ const dups = [];
7345
+ for (let k = 0; k < runLen; k++) dups.push({
7346
+ kind: "first-new-after",
7347
+ replacementLineIndex: firstNew.index + k
7348
+ });
7349
+ return dups;
7250
7350
  }
7251
- const BUSY_RETRIES = 3;
7252
- const BUSY_RETRY_DELAY_MS = 100;
7253
- function withBusyRetry(fn) {
7254
- let lastError;
7255
- for (let attempt = 0; attempt <= BUSY_RETRIES; attempt++) try {
7256
- return fn();
7257
- } catch (error) {
7258
- lastError = error;
7259
- if (!isBusyError(error) || attempt === BUSY_RETRIES) throw error;
7260
- sleepSync(BUSY_RETRY_DELAY_MS);
7261
- }
7262
- throw lastError;
7351
+ function lastNewBeforeDups(contentLines, rangeLines, canonLines, startLine) {
7352
+ const lastNew = findNewEdge(contentLines, rangeLines, true);
7353
+ if (!lastNew) return [];
7354
+ const maxK = Math.min(lastNew.index + 1, startLine - 1);
7355
+ let runLen = 0;
7356
+ while (runLen < maxK && canon(contentLines[lastNew.index - runLen]) === canonLines[startLine - 2 - runLen]) runLen++;
7357
+ if (runLen === 0) return [];
7358
+ if (!sectionIsUnique(canonLines, startLine - 1 - runLen, runLen)) return [];
7359
+ const dups = [];
7360
+ for (let k = 0; k < runLen; k++) dups.push({
7361
+ kind: "last-new-before",
7362
+ replacementLineIndex: lastNew.index - k
7363
+ });
7364
+ return dups;
7263
7365
  }
7264
- function openDbWithBusyRetry(storePath) {
7265
- return withBusyRetry(() => openDb(storePath));
7366
+ function canonCounts(lines) {
7367
+ const counts = /* @__PURE__ */ new Map();
7368
+ for (const line of lines) {
7369
+ const key = canon(line);
7370
+ counts.set(key, (counts.get(key) ?? 0) + 1);
7371
+ }
7372
+ return counts;
7266
7373
  }
7267
- /** One open store per store path (per workspace); parallel sessions share per-workspace dbs. */
7268
- const stores = /* @__PURE__ */ new Map();
7269
- const openings = /* @__PURE__ */ new Map();
7270
- let exitHandlerRegistered = false;
7271
- function openDb(storePath) {
7272
- const db = new DatabaseSync(storePath, { timeout: HASH_STORE_BUSY_TIMEOUT });
7273
- try {
7274
- return buildStore(db);
7275
- } catch (error) {
7276
- try {
7277
- db.close();
7278
- } catch {}
7279
- throw error;
7374
+ /** @internal — private to anchor-pipeline seam */
7375
+ function findNewEdge(contentLines, rangeLines, fromEnd) {
7376
+ const multiset = canonCounts(rangeLines);
7377
+ const step = fromEnd ? -1 : 1;
7378
+ const start = fromEnd ? contentLines.length - 1 : 0;
7379
+ for (let i = start; i >= 0 && i < contentLines.length; i += step) {
7380
+ const line = contentLines[i];
7381
+ if (line.length === 0) continue;
7382
+ const key = canon(line);
7383
+ const count = multiset.get(key) ?? 0;
7384
+ if (count > 0) multiset.set(key, count - 1);
7385
+ else return {
7386
+ index: i,
7387
+ line
7388
+ };
7280
7389
  }
7281
7390
  }
7282
- function buildStore(db) {
7283
- db.exec("PRAGMA journal_mode = WAL");
7284
- db.exec("PRAGMA synchronous = NORMAL");
7285
- db.exec("CREATE TABLE IF NOT EXISTS snapshots (path TEXT PRIMARY KEY, checksum TEXT NOT NULL, line_count INTEGER NOT NULL, hashes TEXT NOT NULL, updated_at INTEGER NOT NULL)");
7286
- db.exec("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
7287
- db.exec("CREATE TABLE IF NOT EXISTS undo (path TEXT PRIMARY KEY, content TEXT NOT NULL, bom TEXT NOT NULL, ending TEXT NOT NULL, hashes TEXT NOT NULL, result_content TEXT NOT NULL, updated_at INTEGER NOT NULL)");
7288
- const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'version'").get();
7289
- const versionChanged = versionRow !== void 0 && versionRow.value !== String(HASH_STORE_VERSION);
7290
- if (versionChanged) {
7291
- db.exec("DELETE FROM snapshots");
7292
- db.exec("DELETE FROM undo");
7391
+ /** @internal — private to anchor-pipeline seam: detection + boundaryDups belongs to AnchorPipeline ordering */
7392
+ function valEdit(edit, fileLines, fileHashes, warnings, signal) {
7393
+ assertAligned(fileLines, fileHashes, "valEdit");
7394
+ const mismatches = [];
7395
+ const boundaryDups = [];
7396
+ const hashIndex = /* @__PURE__ */ new Map();
7397
+ for (let i = 0; i < fileHashes.length; i++) {
7398
+ const h = fileHashes[i];
7399
+ const list = hashIndex.get(h) ?? [];
7400
+ list.push(i + 1);
7401
+ hashIndex.set(h, list);
7293
7402
  }
7294
- const servedColumns = db.prepare("PRAGMA table_info(served)").all();
7295
- if (versionChanged || !servedColumns.some((column) => column.name === "session_id")) db.exec("DROP TABLE IF EXISTS served");
7296
- db.exec("CREATE TABLE IF NOT EXISTS served (session_id TEXT NOT NULL, path TEXT NOT NULL, hashes TEXT NOT NULL, reported TEXT, updated_at INTEGER NOT NULL, PRIMARY KEY (session_id, path))");
7297
- db.prepare("INSERT INTO meta (key, value) VALUES ('version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(HASH_STORE_VERSION));
7298
- const getStmt = db.prepare("SELECT hashes FROM snapshots WHERE path = ? AND checksum = ? AND line_count = ?");
7299
- const allStmt = db.prepare("SELECT path FROM snapshots UNION SELECT path FROM undo UNION SELECT path FROM served");
7300
- const allHashesStmt = db.prepare("SELECT path, hashes FROM snapshots");
7301
- const delStmt = db.prepare("DELETE FROM snapshots WHERE path = ?");
7302
- const upsertStmt = db.prepare("INSERT INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET checksum = excluded.checksum, line_count = excluded.line_count, hashes = excluded.hashes, updated_at = excluded.updated_at");
7303
- const undoUpsertStmt = db.prepare("INSERT INTO undo (path, content, bom, ending, hashes, result_content, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET content = excluded.content, bom = excluded.bom, ending = excluded.ending, hashes = excluded.hashes, result_content = excluded.result_content, updated_at = excluded.updated_at");
7304
- const undoGetStmt = db.prepare("SELECT content, bom, ending, hashes, result_content FROM undo WHERE path = ?");
7305
- const undoDelStmt = db.prepare("DELETE FROM undo WHERE path = ?");
7306
- const servedGetStmt = db.prepare("SELECT hashes, reported FROM served WHERE session_id = ? AND path = ?");
7307
- const servedUpsertStmt = db.prepare("INSERT INTO served (session_id, path, hashes, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(session_id, path) DO UPDATE SET hashes = excluded.hashes, updated_at = excluded.updated_at");
7308
- const servedReportedUpsertStmt = db.prepare("INSERT INTO served (session_id, path, hashes, reported, updated_at) VALUES (?, ?, '[]', ?, ?) ON CONFLICT(session_id, path) DO UPDATE SET reported = excluded.reported, updated_at = excluded.updated_at");
7309
- const servedReportedClearStmt = db.prepare("UPDATE served SET reported = NULL, updated_at = ? WHERE session_id = ? AND path = ?");
7310
- const servedDeleteStmt = db.prepare("DELETE FROM served WHERE session_id = ? AND path = ?");
7311
- const servedDeletePathStmt = db.prepare("DELETE FROM served WHERE path = ?");
7312
- const servedWipeStmt = db.prepare("DELETE FROM served WHERE session_id = ?");
7313
- const servedPruneOlderThanStmt = db.prepare("DELETE FROM served WHERE updated_at < ?");
7314
- return {
7315
- db,
7316
- stmts: {
7317
- get: (...params) => getStmt.get(...params),
7318
- allPaths: (...params) => allStmt.all(...params),
7319
- allHashes: (...params) => allHashesStmt.all(...params),
7320
- deleteOne: (...params) => {
7321
- withBusyRetry(() => {
7322
- delStmt.run(...params);
7323
- });
7324
- },
7325
- upsert: (...params) => {
7326
- withBusyRetry(() => {
7327
- upsertStmt.run(...params);
7328
- });
7329
- },
7330
- undoUpsert: (...params) => {
7331
- withBusyRetry(() => {
7332
- undoUpsertStmt.run(...params);
7333
- });
7334
- },
7335
- undoGet: (...params) => undoGetStmt.get(...params),
7336
- undoDelete: (...params) => {
7337
- withBusyRetry(() => {
7338
- undoDelStmt.run(...params);
7339
- });
7340
- },
7341
- servedGet: (...params) => servedGetStmt.get(...params),
7342
- servedUpsert: (...params) => {
7343
- withBusyRetry(() => {
7344
- servedUpsertStmt.run(...params);
7345
- });
7346
- },
7347
- servedReportedUpsert: (...params) => {
7348
- withBusyRetry(() => {
7349
- servedReportedUpsertStmt.run(...params);
7350
- });
7351
- },
7352
- servedReportedClear: (...params) => {
7353
- withBusyRetry(() => {
7354
- servedReportedClearStmt.run(params[1], params[0], params[2]);
7355
- });
7356
- },
7357
- servedDelete: (...params) => {
7358
- withBusyRetry(() => {
7359
- servedDeleteStmt.run(...params);
7360
- });
7361
- },
7362
- servedDeletePath: (...params) => {
7363
- withBusyRetry(() => {
7364
- servedDeletePathStmt.run(...params);
7365
- });
7366
- },
7367
- servedWipe: (...params) => {
7368
- withBusyRetry(() => {
7369
- servedWipeStmt.run(...params);
7370
- });
7371
- },
7372
- servedPruneOlderThan: (...params) => {
7373
- withBusyRetry(() => {
7374
- servedPruneOlderThanStmt.run(...params);
7375
- });
7376
- }
7403
+ const tryResolve = (ref) => {
7404
+ const result = resAnchorFromMap(ref, hashIndex);
7405
+ if ("kind" in result) {
7406
+ mismatches.push(result);
7407
+ return;
7377
7408
  }
7409
+ return result;
7378
7410
  };
7379
- }
7380
- /** Wire the domain methods over the prepared statements. */
7381
- function makeDomainStore(stmts) {
7382
- return {
7383
- engine: "node:sqlite",
7384
- getSnapshot(path$1, content, deleteCorrupt = true) {
7385
- const checksum = cacheKey(contentChecksum(content));
7386
- const lineCount = splitLines(content).length;
7387
- const row = stmts.get(path$1, checksum, lineCount);
7388
- if (!row) return void 0;
7389
- try {
7390
- const parsed = JSON.parse(row.hashes);
7391
- if (isValidHashList(parsed)) return parsed;
7392
- if (deleteCorrupt) stmts.deleteOne(path$1);
7393
- return;
7394
- } catch {
7395
- if (deleteCorrupt) stmts.deleteOne(path$1);
7396
- return;
7397
- }
7398
- },
7399
- upsertSnapshot(path$1, checksum, lineCount, hashes) {
7400
- stmts.upsert(path$1, cacheKey(checksum), lineCount, JSON.stringify(hashes), Date.now());
7401
- },
7402
- allKnownPaths() {
7403
- return stmts.allPaths();
7404
- },
7405
- allSnapshotHashes() {
7406
- return stmts.allHashes();
7407
- },
7408
- deleteSnapshot(path$1) {
7409
- stmts.deleteOne(path$1);
7410
- },
7411
- findSnapshotPaths(hashes) {
7412
- const rows = stmts.allHashes();
7413
- const matches = [];
7414
- for (const row of rows) try {
7415
- const parsed = JSON.parse(row.hashes);
7416
- if (!isValidHashList(parsed)) continue;
7417
- if (hashes.every((h) => parsed.includes(h))) matches.push(row.path);
7418
- } catch {}
7419
- return matches;
7420
- },
7421
- getUndo(path$1) {
7422
- const row = stmts.undoGet(path$1);
7423
- if (!row) return void 0;
7424
- try {
7425
- const parsed = JSON.parse(row.hashes);
7426
- if (!isValidHashList(parsed)) {
7427
- stmts.undoDelete(path$1);
7428
- return;
7429
- }
7430
- return {
7431
- content: row.content,
7432
- bom: row.bom,
7433
- ending: row.ending,
7434
- hashes: parsed,
7435
- resultContent: row.result_content
7436
- };
7437
- } catch {
7438
- stmts.undoDelete(path$1);
7439
- return;
7440
- }
7441
- },
7442
- upsertUndo(path$1, entry) {
7443
- stmts.undoUpsert(path$1, entry.content, entry.bom, entry.ending, JSON.stringify(entry.hashes), entry.resultContent, Date.now());
7444
- },
7445
- deleteUndo(path$1) {
7446
- stmts.undoDelete(path$1);
7447
- },
7448
- getServed(sessionKey, path$1) {
7449
- const row = stmts.servedGet(sessionKey, path$1);
7450
- if (!row) return [];
7451
- try {
7452
- const parsed = JSON.parse(row.hashes);
7453
- if (isValidServedList(parsed)) return parsed;
7454
- stmts.servedDelete(sessionKey, path$1);
7455
- return [];
7456
- } catch {
7457
- stmts.servedDelete(sessionKey, path$1);
7458
- return [];
7459
- }
7460
- },
7461
- getServedReported(sessionKey, path$1) {
7462
- const row = stmts.servedGet(sessionKey, path$1);
7463
- if (!row) return /* @__PURE__ */ new Set();
7464
- const raw = row.reported;
7465
- if (typeof raw !== "string" || raw.length === 0) return /* @__PURE__ */ new Set();
7466
- try {
7467
- const parsed = JSON.parse(raw);
7468
- if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
7469
- return new Set(parsed.filter((h) => typeof h === "string" && HASH_RE.test(h)));
7470
- } catch {
7471
- return /* @__PURE__ */ new Set();
7472
- }
7473
- },
7474
- upsertServed(sessionKey, path$1, hashesJson) {
7475
- stmts.servedUpsert(sessionKey, path$1, hashesJson, Date.now());
7476
- },
7477
- upsertServedReported(sessionKey, path$1, reportedJson) {
7478
- stmts.servedReportedUpsert(sessionKey, path$1, reportedJson, Date.now());
7479
- },
7480
- clearServedReported(sessionKey, path$1) {
7481
- stmts.servedReportedClear(sessionKey, Date.now(), path$1);
7482
- },
7483
- deleteServed(sessionKey, path$1) {
7484
- stmts.servedDelete(sessionKey, path$1);
7485
- },
7486
- deleteServedByPath(path$1) {
7487
- stmts.servedDeletePath(path$1);
7488
- },
7489
- wipeServed(sessionKey) {
7490
- stmts.servedWipe(sessionKey);
7491
- },
7492
- pruneServedOlderThan(ts) {
7493
- stmts.servedPruneOlderThan(ts);
7494
- },
7495
- async pruneMissing() {
7496
- const missing = await statMissing(stmts.allPaths());
7497
- if (missing.length === 0) return;
7498
- withStore(() => {
7499
- for (const path$1 of missing) {
7500
- stmts.deleteOne(path$1);
7501
- stmts.undoDelete(path$1);
7502
- stmts.servedDeletePath(path$1);
7503
- }
7504
- });
7411
+ abortIf(signal);
7412
+ const startResolved = tryResolve(edit.hash_bounds[0]);
7413
+ const endResolved = tryResolve(edit.hash_bounds[1]);
7414
+ if (!startResolved || !endResolved) {
7415
+ if (!startResolved && endResolved) {
7416
+ const startMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[0]);
7417
+ if (startMismatch && startMismatch.kind === "not_found") startMismatch.context = endResolved;
7418
+ } else if (startResolved && !endResolved) {
7419
+ const endMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[1]);
7420
+ if (endMismatch && endMismatch.kind === "not_found") endMismatch.context = startResolved;
7505
7421
  }
7422
+ return {
7423
+ resolved: void 0,
7424
+ mismatches,
7425
+ boundaryDups
7426
+ };
7427
+ }
7428
+ if (startResolved.line > endResolved.line) throw new Error(`[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.hash_bounds[0].hash} and ${edit.hash_bounds[1].hash}).`);
7429
+ const endLine = endResolved.line;
7430
+ const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
7431
+ const canonLines = fileLines.map((line) => canon(line));
7432
+ boundaryDups.push(...trailingDups(edit.content_lines, fileLines, endLine), ...leadingDups(edit.content_lines, fileLines, startResolved.line), ...firstNewAfterDups(edit.content_lines, rangeLines, canonLines, endLine), ...lastNewBeforeDups(edit.content_lines, rangeLines, canonLines, startResolved.line));
7433
+ return {
7434
+ resolved: {
7435
+ content_lines: edit.content_lines,
7436
+ hash_bounds: [startResolved, endResolved]
7437
+ },
7438
+ mismatches,
7439
+ boundaryDups
7506
7440
  };
7507
7441
  }
7508
- function isHealthy(db) {
7509
- try {
7510
- return db.prepare("PRAGMA quick_check").get()?.quick_check === "ok";
7511
- } catch (error) {
7512
- if (isCorruptionError(error)) return false;
7513
- return true;
7442
+ var ServedRejectionError = class extends Error {
7443
+ code;
7444
+ firstOffendingLine;
7445
+ servedRows;
7446
+ constructor(opts) {
7447
+ super(opts.message);
7448
+ this.name = "ServedRejectionError";
7449
+ this.code = opts.code;
7450
+ this.firstOffendingLine = opts.firstOffendingLine;
7451
+ this.servedRows = opts.servedRows;
7514
7452
  }
7515
- }
7516
- async function quarantineStore(storePath) {
7517
- const suffix = `.corrupt-${Date.now()}`;
7518
- for (const candidate of [
7519
- storePath,
7520
- `${storePath}-wal`,
7521
- `${storePath}-shm`
7522
- ]) try {
7523
- await rename(candidate, `${candidate}${suffix}`);
7524
- } catch (error) {
7525
- if (errCode(error) !== "ENOENT") console.error("Failed to quarantine corrupt hash store file:", error);
7453
+ };
7454
+ var AnchorMismatchError = class extends Error {
7455
+ servedRows;
7456
+ constructor(message, servedRows) {
7457
+ super(message);
7458
+ this.name = "AnchorMismatchError";
7459
+ this.servedRows = servedRows;
7526
7460
  }
7461
+ };
7462
+ function buildRangeEcho(startLine, endLine, fileHashes) {
7463
+ const total = endLine - startLine + 1;
7464
+ const shown = Math.min(total, SERVED_ECHO_CAP);
7465
+ const rows = [];
7466
+ for (let ln = startLine; ln < startLine + shown; ln++) rows.push({
7467
+ position: ln - 1,
7468
+ hash: fileHashes[ln - 1]
7469
+ });
7470
+ return rows;
7527
7471
  }
7528
- function shutdownDb(db) {
7529
- try {
7530
- db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
7531
- } catch {}
7532
- db.close();
7472
+ function fmtServedRows(rows, fileLines) {
7473
+ return rows.map((row) => `${row.hash}${HASH_SEP}${fileLines[row.position] ?? ""}`).join("\n");
7533
7474
  }
7534
- const STAT_BATCH = 64;
7535
- async function statMissing(rows) {
7536
- const missing = [];
7537
- for (let i = 0; i < rows.length; i += STAT_BATCH) {
7538
- const batch = rows.slice(i, i + STAT_BATCH);
7539
- const results = await Promise.all(batch.map(async (row) => {
7540
- try {
7541
- await stat(row.path);
7542
- return;
7543
- } catch {
7544
- return row.path;
7545
- }
7546
- }));
7547
- for (const path$1 of results) if (path$1 !== void 0) missing.push(path$1);
7548
- }
7549
- return missing;
7475
+ function retryHint() {
7476
+ return "Read the file once to refresh anchors, then resubmit the whole batch.";
7550
7477
  }
7551
- async function openStore(storePath) {
7552
- await initHasher();
7553
- await mkdir(dirname(storePath), { recursive: true });
7554
- let existed = existsSync(storePath);
7555
- let opened;
7556
- try {
7557
- opened = openDbWithBusyRetry(storePath);
7558
- } catch (error) {
7559
- if (!isCorruptionError(error)) throw error;
7560
- console.error("Hash store failed to open, rebuilding:", error);
7561
- await quarantineStore(storePath);
7562
- existed = false;
7563
- opened = openDbWithBusyRetry(storePath);
7478
+ function paginationHint(nextOffset, more) {
7479
+ return `[... ${more} more — read offset=${nextOffset}]`;
7480
+ }
7481
+ function verifyServedRange(args) {
7482
+ const { served, startHash, endHash, startLine, endLine, fileHashes, fileLines, filePath } = args;
7483
+ const where = filePath ? ` in ${filePath}` : "";
7484
+ const echoRows = buildRangeEcho(startLine, endLine, fileHashes);
7485
+ const totalLen = endLine - startLine + 1;
7486
+ const tail = echoRows.length < totalLen ? `\n${paginationHint(startLine + echoRows.length, totalLen - echoRows.length)}` : "";
7487
+ const echo = fmtServedRows(echoRows, fileLines) + tail;
7488
+ const startPositions = servedPositionsOf(served, startHash);
7489
+ const endPositions = servedPositionsOf(served, endHash);
7490
+ if (startPositions.length === 0 && endPositions.length === 0) return;
7491
+ const currentLen = endLine - startLine + 1;
7492
+ let from;
7493
+ let to;
7494
+ if (startPositions.length === 1 && endPositions.length === 1) {
7495
+ from = Math.min(startPositions[0], endPositions[0]);
7496
+ to = Math.max(startPositions[0], endPositions[0]);
7497
+ } else {
7498
+ const candidates = [];
7499
+ for (const s of startPositions) for (const e of endPositions) {
7500
+ const candFrom = Math.min(s, e);
7501
+ const candTo = Math.max(s, e);
7502
+ if (candTo - candFrom + 1 !== currentLen) continue;
7503
+ let ok = true;
7504
+ for (let k = 0; k < currentLen; k++) if (served[candFrom + k] !== fileHashes[startLine - 1 + k]) {
7505
+ ok = false;
7506
+ break;
7507
+ }
7508
+ if (ok) candidates.push({
7509
+ from: candFrom,
7510
+ to: candTo
7511
+ });
7512
+ }
7513
+ if (candidates.length === 1) {
7514
+ from = candidates[0].from;
7515
+ to = candidates[0].to;
7516
+ } else if (candidates.length > 1) {
7517
+ candidates.sort((a, b) => Math.abs(a.from - (startLine - 1)) - Math.abs(b.from - (startLine - 1)));
7518
+ from = candidates[0].from;
7519
+ to = candidates[0].to;
7520
+ }
7564
7521
  }
7565
- if (!isHealthy(opened.db)) {
7566
- shutdownDb(opened.db);
7567
- await quarantineStore(storePath);
7568
- existed = false;
7569
- opened = openDbWithBusyRetry(storePath);
7522
+ if (from === void 0 || to === void 0) {
7523
+ const problems = [];
7524
+ if (startPositions.length === 0) problems.push(`remove_from "${startHash}" has no served position`);
7525
+ else if (startPositions.length > 1) problems.push(`remove_from "${startHash}" was served at ${startPositions.length} positions`);
7526
+ if (endPositions.length === 0) problems.push(`remove_to "${endHash}" has no served position`);
7527
+ else if (endPositions.length > 1) problems.push(`remove_to "${endHash}" was served at ${endPositions.length} positions`);
7528
+ throw new ServedRejectionError({
7529
+ code: "E_RANGE_UNVERIFIED",
7530
+ message: `[E_RANGE_UNVERIFIED] cannot verify range against served state${where}: ${problems.join("; ")}. Current range:
7531
+ ${echo}
7532
+ ${retryHint()}`,
7533
+ servedRows: echoRows
7534
+ });
7570
7535
  }
7571
- const { db, stmts } = opened;
7572
- if (!existed) await migrateLegacy(db, storePath);
7573
- withBusyRetry(() => {
7574
- stmts.servedPruneOlderThan(Date.now() - SERVED_TTL_MS);
7536
+ for (let i = from; i <= to; i++) if (served[i] === null) throw new ServedRejectionError({
7537
+ code: "E_RANGE_UNSERVED",
7538
+ message: `[E_RANGE_UNSERVED] line ${i + 1}${where} was never served.\nCurrent range:\n${echo}\n${retryHint()}`,
7539
+ firstOffendingLine: i + 1,
7540
+ servedRows: echoRows
7575
7541
  });
7576
- const store = makeDomainStore(stmts);
7577
- stores.set(storePath, {
7578
- path: storePath,
7579
- db,
7580
- stmts,
7581
- store
7542
+ const servedLen = to - from + 1;
7543
+ if (servedLen !== currentLen) throw new ServedRejectionError({
7544
+ code: "E_RANGE_STALE",
7545
+ message: `[E_RANGE_STALE] served span (${servedLen} lines) no longer matches current range (${currentLen} lines)${where}.\nCurrent range:\n${echo}\n${retryHint()}`,
7546
+ firstOffendingLine: startLine,
7547
+ servedRows: echoRows
7582
7548
  });
7583
- if (!exitHandlerRegistered) {
7584
- exitHandlerRegistered = true;
7585
- process.once("exit", () => shutdownHashStore());
7586
- for (const sig of ["SIGINT", "SIGTERM"]) process.once(sig, () => {
7587
- shutdownHashStore();
7588
- process.kill(process.pid, sig);
7549
+ for (let k = 0; k < servedLen; k++) if (served[from + k] !== fileHashes[startLine - 1 + k]) {
7550
+ const offendingLine = startLine + k;
7551
+ throw new ServedRejectionError({
7552
+ code: "E_RANGE_STALE",
7553
+ message: `[E_RANGE_STALE] Line ${offendingLine}${where} differs from what you were served — the file changed on disk since it was read. Current range:\n${echo}\n${retryHint()}`,
7554
+ firstOffendingLine: offendingLine,
7555
+ servedRows: echoRows
7589
7556
  });
7590
7557
  }
7591
- return store;
7592
7558
  }
7593
- /** Resolve the store path for this call: explicit cwd, the active workspace, or the shared-home fallback. */
7594
- function storePathFor(cwd) {
7595
- return hashStorePath(cwd ?? workspaceCwd());
7559
+ async function recordEchoServes(path$1, rows, policy, lineCount) {
7560
+ if (policy !== "live") return;
7561
+ await recordServed(path$1, rows, lineCount);
7596
7562
  }
7597
- /**
7598
- * Load (and cache) the hash store for the given cwd — or, when omitted, the
7599
- * workspace active for this async execution (`withWorkspace`), falling back to
7600
- * the shared `$DSH_HOME` store outside a tool call.
7601
- * @param cwd - optional explicit workspace root; defaults to the active workspace.
7602
- */
7603
- function loadHashStore(cwd) {
7604
- const storePath = storePathFor(cwd);
7605
- const cached = stores.get(storePath);
7606
- if (cached && cached.db.isOpen) return Promise.resolve(cached.store);
7607
- const existing = openings.get(storePath);
7608
- if (existing) return existing;
7609
- const promise = openStore(storePath).finally(() => {
7610
- openings.delete(storePath);
7611
- });
7612
- openings.set(storePath, promise);
7613
- return promise;
7563
+ function buildIdx(content) {
7564
+ const fileLines = splitLines(content);
7565
+ const lineStarts = [];
7566
+ let offset = 0;
7567
+ for (let index = 0; index < fileLines.length; index++) {
7568
+ lineStarts.push(offset);
7569
+ offset += fileLines[index].length;
7570
+ if (index < fileLines.length - 1) offset += 1;
7571
+ }
7572
+ return {
7573
+ fileLines,
7574
+ lineStarts
7575
+ };
7614
7576
  }
7615
- /** The cached store entry for the active workspace (or the shared-home fallback), if open. */
7616
- function currentStore() {
7617
- const entry = stores.get(storePathFor());
7618
- return entry?.db.isOpen ? entry : void 0;
7577
+ function assertNotEmpty(originalContent, result) {
7578
+ if (originalContent.length > 0 && result.length === 0) throw new Error("[E_WOULD_EMPTY] Cannot empty a non-empty file via edit. Use `write` if you need to clear the file.");
7619
7579
  }
7620
- /** Close every open store (process exit, HMR, tests). */
7621
- function shutdownHashStore() {
7622
- for (const [, entry] of stores) shutdownDb(entry.db);
7623
- stores.clear();
7624
- openings.clear();
7580
+ function resToSpan(edit, content, lineIndex) {
7581
+ const { fileLines, lineStarts } = lineIndex;
7582
+ const startLine = edit.hash_bounds[0].line;
7583
+ const endLine = edit.hash_bounds[1].line;
7584
+ const originalLines = fileLines.slice(startLine - 1, endLine);
7585
+ if (originalLines.length === edit.content_lines.length && originalLines.every((line, lineIndex$1) => line === edit.content_lines[lineIndex$1])) return {
7586
+ kind: "noop",
7587
+ loc: edit.hash_bounds[0].hash,
7588
+ currentContent: originalLines.join("\n")
7589
+ };
7590
+ if (edit.content_lines.length > 0) return {
7591
+ kind: "replace",
7592
+ start: lineStarts[startLine - 1],
7593
+ end: lineStarts[endLine - 1] + fileLines[endLine - 1].length,
7594
+ replacement: edit.content_lines.join("\n")
7595
+ };
7596
+ if (startLine === 1 && endLine === fileLines.length) return {
7597
+ kind: "replace",
7598
+ start: 0,
7599
+ end: content.length,
7600
+ replacement: ""
7601
+ };
7602
+ if (endLine < fileLines.length) return {
7603
+ kind: "replace",
7604
+ start: lineStarts[startLine - 1],
7605
+ end: lineStarts[endLine],
7606
+ replacement: ""
7607
+ };
7608
+ if (content.endsWith("\n")) return {
7609
+ kind: "replace",
7610
+ start: lineStarts[startLine - 1],
7611
+ end: content.length,
7612
+ replacement: ""
7613
+ };
7614
+ const prevLine = startLine >= 2 ? fileLines[startLine - 2] : void 0;
7615
+ return {
7616
+ kind: "replace",
7617
+ start: prevLine !== void 0 && prevLine.length === 0 ? lineStarts[startLine - 1] : Math.max(0, lineStarts[startLine - 1] - 1),
7618
+ end: content.length,
7619
+ replacement: ""
7620
+ };
7625
7621
  }
7626
- /**
7627
- * Run `fn` inside one BEGIN IMMEDIATE transaction on the active workspace's
7628
- * store. Without an open store for this context the call runs bare (the
7629
- * caller has already loaded the store in every in-process path).
7630
- */
7631
- function withStore(fn) {
7632
- const store = currentStore();
7633
- if (store) withBusyRetry(() => {
7634
- store.db.exec("BEGIN IMMEDIATE");
7635
- try {
7636
- fn();
7637
- store.db.exec("COMMIT");
7638
- } catch (e) {
7639
- try {
7640
- store.db.exec("ROLLBACK");
7641
- } catch {}
7642
- throw e;
7643
- }
7644
- });
7645
- else fn();
7622
+ function assemble(content, span, signal) {
7623
+ abortIf(signal);
7624
+ return content.slice(0, span.start) + span.replacement + content.slice(span.end);
7646
7625
  }
7647
- async function migrateLegacy(db, storePath) {
7648
- const legacyPath = join(dirname(storePath), "hash-store.json");
7649
- let content;
7650
- try {
7651
- content = await readFile(legacyPath, "utf-8");
7652
- } catch (error) {
7653
- if (errCode(error) === "ENOENT") return;
7654
- console.error("Failed to read legacy hash store for migration:", error);
7655
- return;
7656
- }
7657
- let parsed;
7658
- try {
7659
- parsed = JSON.parse(content);
7660
- } catch (error) {
7661
- console.error("Failed to parse legacy hash store, skipping migration:", error);
7662
- return;
7626
+ function applyEdit(content, edit, signal, precomputedHashes, filePath, served) {
7627
+ abortIf(signal);
7628
+ const lineIndex = buildIdx(content);
7629
+ const fileHashes = precomputedHashes ?? lineHashesPure(content);
7630
+ const warnings = [];
7631
+ const prefixFixed = stripDiffPrefixes(stripBarePrefixes(swapReversedRanges(edit, fileHashes, warnings), fileHashes, warnings), warnings);
7632
+ const { resolved: initialResolved, mismatches, boundaryDups } = valEdit(prefixFixed, lineIndex.fileLines, fileHashes, warnings, signal);
7633
+ if (mismatches.length || !initialResolved) {
7634
+ const { message, servedRows } = fmtMismatchWithServes(mismatches, lineIndex.fileLines, fileHashes, filePath);
7635
+ throw new AnchorMismatchError(message, servedRows);
7663
7636
  }
7664
- const raw = parsed.snapshots;
7665
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
7666
- const rows = [];
7667
- for (const [key, value] of Object.entries(raw)) {
7668
- if (!isValidSnapshot(value)) continue;
7669
- if (new Set(value.hashes).size !== value.hashes.length) {
7670
- console.warn(`Skipped legacy snapshot with duplicate hashes for ${key}; it will be re-hashed on next read.`);
7671
- continue;
7637
+ warnUnicodeEsc(prefixFixed, warnings);
7638
+ let resolved = initialResolved;
7639
+ let autoFixes;
7640
+ if (boundaryDups.length > 0) {
7641
+ autoFixes = [];
7642
+ const correctedEdit = {
7643
+ ...prefixFixed,
7644
+ content_lines: [...prefixFixed.content_lines]
7645
+ };
7646
+ const seen = /* @__PURE__ */ new Set();
7647
+ const uniqueDups = [];
7648
+ for (const dup of boundaryDups) {
7649
+ if (seen.has(dup.replacementLineIndex)) continue;
7650
+ seen.add(dup.replacementLineIndex);
7651
+ uniqueDups.push(dup);
7672
7652
  }
7673
- rows.push([
7674
- key,
7675
- contentChecksum(value.content),
7676
- splitLines(value.content).length,
7677
- JSON.stringify(value.hashes),
7678
- Date.now()
7679
- ]);
7680
- }
7681
- if (rows.length > 0) {
7682
- db.exec("BEGIN IMMEDIATE");
7683
- try {
7684
- const stmt = db.prepare("INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)");
7685
- for (const row of rows) stmt.run(...row);
7686
- db.exec("COMMIT");
7687
- } catch (e) {
7688
- db.exec("ROLLBACK");
7689
- throw e;
7653
+ const dupsByIndex = uniqueDups.sort((a, b) => b.replacementLineIndex - a.replacementLineIndex);
7654
+ for (const dup of dupsByIndex) {
7655
+ const idx = dup.replacementLineIndex;
7656
+ if (idx < 0 || idx >= correctedEdit.content_lines.length) continue;
7657
+ const removed = correctedEdit.content_lines.splice(idx, 1)[0];
7658
+ autoFixes.push({
7659
+ kind: dup.kind,
7660
+ removedLine: removed,
7661
+ removedLineIndex: idx
7662
+ });
7663
+ }
7664
+ const correctedResult = valEdit(correctedEdit, lineIndex.fileLines, fileHashes, warnings, signal);
7665
+ if (correctedResult.mismatches.length || !correctedResult.resolved) {
7666
+ const { message, servedRows } = fmtMismatchWithServes(correctedResult.mismatches, lineIndex.fileLines, fileHashes, filePath);
7667
+ throw new AnchorMismatchError(message, servedRows);
7690
7668
  }
7669
+ resolved = correctedResult.resolved;
7691
7670
  }
7692
- try {
7693
- await rename(legacyPath, `${legacyPath}.bak`);
7694
- } catch (error) {
7695
- console.error("Failed to rename legacy hash store after migration:", error);
7671
+ if (served) {
7672
+ const startAnchor = resolved.hash_bounds[0];
7673
+ const endAnchor = resolved.hash_bounds[1];
7674
+ verifyServedRange({
7675
+ served,
7676
+ startHash: startAnchor.hash,
7677
+ endHash: endAnchor.hash,
7678
+ startLine: startAnchor.line,
7679
+ endLine: endAnchor.line,
7680
+ fileHashes,
7681
+ fileLines: lineIndex.fileLines,
7682
+ filePath
7683
+ });
7696
7684
  }
7697
- }
7698
- /** Find files whose stored snapshot hashes contain every given anchor. */
7699
- async function findSnapshotPathsByHashes(hashes) {
7700
- return (await loadHashStore()).findSnapshotPaths(hashes);
7701
- }
7702
- /** Persist a hash snapshot for one path (async over the active store). */
7703
- async function upsertSnapshotFor(path$1, checksum, lineCount, hashes) {
7704
- (await loadHashStore()).upsertSnapshot(path$1, checksum, lineCount, hashes);
7705
- }
7706
-
7707
- //#endregion
7708
- //#region src/url-schema/vendored/hashline/hashline/hash.js
7709
- let defaultHashSnapshotIO;
7710
- function snapshotIOFor(store) {
7711
- if (store) return {
7712
- get: (path$1, content, deleteCorrupt) => Promise.resolve(store.getSnapshot(path$1, content, deleteCorrupt)),
7713
- upsert: (path$1, checksum, lineCount, hashes) => {
7714
- store.upsertSnapshot(path$1, checksum, lineCount, hashes);
7715
- return Promise.resolve();
7685
+ const spanResult = resToSpan(resolved, content, lineIndex);
7686
+ if (spanResult.kind === "noop") return {
7687
+ content,
7688
+ firstChangedLine: void 0,
7689
+ lastChangedLine: void 0,
7690
+ range: resolvedRange(resolved),
7691
+ ...warnings.length ? { warnings } : {},
7692
+ noopEdit: {
7693
+ loc: spanResult.loc,
7694
+ currentContent: spanResult.currentContent
7716
7695
  }
7717
7696
  };
7718
- return defaultHashSnapshotIO ?? {
7719
- get: async (path$1, content, deleteCorrupt) => {
7720
- return (await loadHashStore()).getSnapshot(path$1, content, deleteCorrupt);
7721
- },
7722
- upsert: async (path$1, checksum, lineCount, hashes) => {
7723
- (await loadHashStore()).upsertSnapshot(path$1, checksum, lineCount, hashes);
7724
- }
7697
+ const result = assemble(content, spanResult, signal);
7698
+ assertNotEmpty(content, result);
7699
+ const changed = changedRange(content, result);
7700
+ return {
7701
+ content: result,
7702
+ firstChangedLine: changed?.firstChangedLine,
7703
+ lastChangedLine: changed?.lastChangedLine,
7704
+ range: resolvedRange(resolved),
7705
+ ...warnings.length ? { warnings } : {},
7706
+ ...autoFixes ? { autoFixes } : {}
7725
7707
  };
7726
7708
  }
7727
- async function lineHashes(content, path$1, previous, ioOrStore, persist) {
7728
- await initHasher();
7729
- if (!path$1) return lineHashesPure(content);
7730
- const io = ioOrStore && "getSnapshot" in ioOrStore ? snapshotIOFor(ioOrStore) : ioOrStore ?? snapshotIOFor(void 0);
7731
- if (previous) {
7732
- const newHashes$1 = mapStableHashes(previous.content, previous.hashes, content, previous.removedHashes);
7733
- if (persist !== false && io) try {
7734
- await io.upsert(path$1, contentChecksum(content), splitLines(content).length, newHashes$1);
7735
- } catch (e) {
7736
- console.error("Failed to persist hash snapshot:", e);
7737
- }
7738
- return newHashes$1;
7739
- }
7740
- let cached;
7741
- if (io) try {
7742
- cached = await io.get(path$1, content, persist !== false);
7743
- } catch (e) {
7744
- console.error("Failed to read hash store snapshot:", e);
7745
- }
7746
- if (cached) return cached;
7747
- const newHashes = lineHashesPure(content);
7748
- if (persist !== false && io) try {
7749
- await io.upsert(path$1, contentChecksum(content), splitLines(content).length, newHashes);
7750
- } catch (e) {
7751
- console.error("Failed to persist hash snapshot:", e);
7709
+ function resolvedRange(resolved) {
7710
+ const [start, end] = resolved.hash_bounds;
7711
+ return {
7712
+ startLine: start.line,
7713
+ endLine: end.line,
7714
+ startHash: start.hash,
7715
+ endHash: end.hash,
7716
+ delta: resolved.content_lines.length - (Math.abs(end.line - start.line) + 1)
7717
+ };
7718
+ }
7719
+ function fmtRegion(hashes, lines) {
7720
+ if (hashes.length !== lines.length) throw new Error(`fmtRegion: hashes.length (${hashes.length}) must match lines.length (${lines.length}).`);
7721
+ return lines.map((line, index) => `${hashes[index]}${HASH_SEP}${line}`).join("\n");
7722
+ }
7723
+ function changedRange(original, result) {
7724
+ if (original === result) return null;
7725
+ if (original.length === 0) return {
7726
+ firstChangedLine: 1,
7727
+ lastChangedLine: splitLines(result).length
7728
+ };
7729
+ const originalLines = splitLines(original);
7730
+ const resultLines = splitLines(result);
7731
+ if (originalLines.length === resultLines.length && originalLines.every((line, index) => line === resultLines[index])) return null;
7732
+ const minLen = Math.min(originalLines.length, resultLines.length);
7733
+ let first = 0;
7734
+ while (first < minLen && originalLines[first] === resultLines[first]) first++;
7735
+ let lastOrig = originalLines.length - 1;
7736
+ let lastRes = resultLines.length - 1;
7737
+ while (lastOrig >= first && lastRes >= first && originalLines[lastOrig] === resultLines[lastRes]) {
7738
+ lastOrig--;
7739
+ lastRes--;
7752
7740
  }
7753
- return newHashes;
7741
+ return {
7742
+ firstChangedLine: first + 1,
7743
+ lastChangedLine: Math.max(first, lastRes) + 1
7744
+ };
7754
7745
  }
7755
7746
 
7756
7747
  //#endregion
@@ -8182,15 +8173,15 @@ const UTF8_REWRITE_NOTE = "[Non-UTF-8 bytes shown as U+FFFD; editing rewrites th
8182
8173
  * context.
8183
8174
  */
8184
8175
  async function readAndServe(io, rawPath, cwd, options) {
8185
- const { sessionKey, signal } = options;
8176
+ const { signal } = options;
8186
8177
  abortIf(signal);
8187
8178
  const view = await readView(io, rawPath, cwd, {
8188
8179
  offset: options.offset,
8189
8180
  limit: options.limit,
8190
8181
  signal
8191
8182
  });
8192
- if (view.served.length > 0) await recordServed(sessionKey, view.absolutePath, view.served, view.hashes.length);
8193
- await clearDriftReported(sessionKey, view.absolutePath);
8183
+ if (view.served.length > 0) await recordServed(view.absolutePath, view.served, view.hashes.length);
8184
+ await clearDriftReported(view.absolutePath);
8194
8185
  return {
8195
8186
  text: view.hadUtf8DecodeErrors ? `${view.text}\n\n${UTF8_REWRITE_NOTE}` : view.text,
8196
8187
  served: view.served,
@@ -8254,10 +8245,8 @@ function createReadTool(deps) {
8254
8245
  }
8255
8246
  return withWorkspace(execCwd(exec), async () => {
8256
8247
  const cwd = execCwd(exec);
8257
- const sessionKey = execSessionKey(exec);
8258
8248
  const signal = exec.signal;
8259
8249
  const { text, absolutePath } = await readAndServe(io, rawPath, cwd, {
8260
- sessionKey,
8261
8250
  signal,
8262
8251
  offset: canonical.offset,
8263
8252
  limit: canonical.limit
@@ -8684,12 +8673,12 @@ async function applyOne(input, onReject) {
8684
8673
  * with no change. Messages are byte-identical to the pre-engine tools.
8685
8674
  */
8686
8675
  async function enforceNoopLoop(opts) {
8687
- const { absolutePath, removeFrom, removeTo, displayPath, index, count, sessionKey, originalHashes } = opts;
8676
+ const { absolutePath, removeFrom, removeTo, displayPath, index, count, originalHashes } = opts;
8688
8677
  if (index === void 0) {
8689
8678
  if (count >= NOOP_LOOP_THRESHOLD) {
8690
8679
  const echoRows = buildRangeEcho(opts.range.startLine, opts.range.endLine, originalHashes);
8691
8680
  const echo = fmtServedRows(echoRows, splitLines(opts.originalNormalized));
8692
- await recordEchoServes(sessionKey, absolutePath, echoRows, "live", originalHashes.length);
8681
+ await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
8693
8682
  throw new Error(`[E_NOOP_LOOP] identical edit (${removeFrom} → ${removeTo} in ${displayPath}) submitted ${count}×, no changes each time. Range already contains this text; resend will reject. Current range:\n${echo}`);
8694
8683
  }
8695
8684
  if (count === 2) return `[E_NOOP_LOOP] Notice: identical edit (${removeFrom} → ${removeTo} in ${displayPath}) no-op'd twice; range already has this text. Resend will reject.`;
@@ -8698,7 +8687,7 @@ async function enforceNoopLoop(opts) {
8698
8687
  if (count >= NOOP_LOOP_THRESHOLD) {
8699
8688
  const originalLines = splitLines(opts.originalNormalized);
8700
8689
  const echoRows = opts.echoRows;
8701
- if (echoRows) await recordEchoServes(sessionKey, absolutePath, echoRows, "live", originalHashes.length);
8690
+ if (echoRows) await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
8702
8691
  throw new Error(`[E_NOOP_LOOP] edits[${index}] (${displayPath}): identical edit (${removeFrom} → ${removeTo}) submitted ${count}×, no changes each time. Range already has this text; resend will reject the batch.` + (echoRows ? ` Current on-disk range:\n${fmtServedRows(echoRows, originalLines)}` : ""));
8703
8692
  }
8704
8693
  if (count === 2) return `[E_NOOP_LOOP] Notice: edits[${index}] (${displayPath}) — identical edit no-op'd twice; range already has this text. Resend will reject the batch.`;
@@ -8728,7 +8717,7 @@ async function runFileEdits(io, items, opts) {
8728
8717
  signal: opts.signal,
8729
8718
  maxLines: MAX_HASH_LINES
8730
8719
  });
8731
- const served = await loadServed(opts.sessionKey, absolutePath);
8720
+ const served = await loadServed(absolutePath);
8732
8721
  const warnings = [];
8733
8722
  let currentContent = originalNormalized;
8734
8723
  let currentHashes = originalHashes;
@@ -8760,7 +8749,7 @@ async function runFileEdits(io, items, opts) {
8760
8749
  if (error instanceof AnchorMismatchError || error instanceof ServedRejectionError) {
8761
8750
  const originalLines = splitLines(originalNormalized);
8762
8751
  const echoRows = error.servedRows.length > 0 ? error.servedRows : edit ? echoRowsForItem(edit, originalHashes) : void 0;
8763
- if (echoRows) await recordEchoServes(opts.sessionKey, absolutePath, echoRows, "live", originalHashes.length);
8752
+ if (echoRows) await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
8764
8753
  const echoBlock = echoRows ? ` Current on-disk range for edits[${item.index}] (unchanged — nothing was written):\n${fmtServedRows(echoRows, originalLines)}` : " Call read() to get fresh anchors.";
8765
8754
  throw new Error(`[E_BATCH_ABORT] edits[${item.index}] (${item.path}) failed: ${error.message}${echoBlock}\nThe whole batch was rejected and NOTHING was written — no file changed and earlier items in the batch were NOT applied. Fix the failing edit (and any later edit that depends on it), then resubmit the batch.`);
8766
8755
  }
@@ -8787,7 +8776,6 @@ async function runFileEdits(io, items, opts) {
8787
8776
  displayPath: item.path,
8788
8777
  index: item.index,
8789
8778
  count,
8790
- sessionKey: opts.sessionKey,
8791
8779
  originalHashes,
8792
8780
  originalNormalized,
8793
8781
  echoRows: echoRowsForItem(applied.edit, originalHashes)
@@ -8826,7 +8814,6 @@ async function runFileEdits(io, items, opts) {
8826
8814
  const originalLines = splitLines(originalNormalized);
8827
8815
  try {
8828
8816
  driftNotice = await scanDrift({
8829
- sessionKey: opts.sessionKey,
8830
8817
  served,
8831
8818
  resultHashes,
8832
8819
  resultLines,
@@ -9036,11 +9023,8 @@ function buildBatchResult(sections) {
9036
9023
  * across every tool — it concentrates (deep).
9037
9024
  */
9038
9025
  async function execute(opts) {
9039
- const { io, items, sessionKey, signal, exec, sandbox, sandboxPolicy } = opts;
9040
- const fileResult = await applySequence(io, items, {
9041
- signal,
9042
- sessionKey
9043
- });
9026
+ const { io, items, signal, exec, sandbox, sandboxPolicy } = opts;
9027
+ const fileResult = await applySequence(io, items, { signal });
9044
9028
  const toSection = () => ({
9045
9029
  path: fileResult.displayPath,
9046
9030
  originalNormalized: fileResult.originalNormalized,
@@ -9057,7 +9041,7 @@ async function execute(opts) {
9057
9041
  const recordIfNeeded = async (built$1) => {
9058
9042
  if (built$1.details.servedRows && built$1.details.servedRows.length > 0) {
9059
9043
  const entry = built$1.details.servedByPath?.[0];
9060
- if (entry) await recordServedTruncated(sessionKey, fileResult.absolutePath, entry.servedRows, splitLines(fileResult.result).length, fileResult.range.startLine - 1);
9044
+ if (entry) await recordServedTruncated(fileResult.absolutePath, entry.servedRows, splitLines(fileResult.result).length, fileResult.range.startLine - 1);
9061
9045
  }
9062
9046
  };
9063
9047
  if (items.length === 1 && fileResult.appliedCount + fileResult.noopCount === 1) {
@@ -9214,7 +9198,6 @@ function buildEditTool(io, sandbox) {
9214
9198
  async execute(args, exec) {
9215
9199
  return withWorkspace(execCwd(exec), async () => {
9216
9200
  const cwd = execCwd(exec);
9217
- const sessionKey = execSessionKey(exec);
9218
9201
  const signal = exec.signal;
9219
9202
  const canonical = normalizeRequest(args);
9220
9203
  assertEditRequest(canonical);
@@ -9251,7 +9234,6 @@ function buildEditTool(io, sandbox) {
9251
9234
  return execute({
9252
9235
  io,
9253
9236
  items,
9254
- sessionKey,
9255
9237
  signal,
9256
9238
  exec,
9257
9239
  sandbox,
@@ -9296,7 +9278,6 @@ function buildUndoTool(io, sandbox) {
9296
9278
  async execute(args, exec) {
9297
9279
  return withWorkspace(execCwd(exec), async () => {
9298
9280
  const cwd = execCwd(exec);
9299
- const sessionKey = execSessionKey(exec);
9300
9281
  const signal = exec.signal;
9301
9282
  const canonical = normalizeRequest(args);
9302
9283
  assertUndoRequest(canonical);
@@ -9347,7 +9328,7 @@ function buildUndoTool(io, sandbox) {
9347
9328
  const parts = [`Undone last edit on ${path$1}.`];
9348
9329
  if (linesAddedByEdit > 0 || linesRemovedByEdit > 0) parts.push(`Removed ${linesAddedByEdit} line(s) that were added and restored ${linesRemovedByEdit} line(s) that were removed.`);
9349
9330
  parts.push("File reverted to previous state. The post-edit diff rows carry the restored file’s fresh anchors for follow-up edits.");
9350
- if (undoDenseRows.length > 0) await recordServedTruncated(sessionKey, absolutePath, undoDenseRows, splitLines(undo.content).length, restoredRange?.firstChangedLine ?? undoDiffResult.firstChangedLine ?? 0);
9331
+ if (undoDenseRows.length > 0) await recordServedTruncated(absolutePath, undoDenseRows, splitLines(undo.content).length, restoredRange?.firstChangedLine ?? undoDiffResult.firstChangedLine ?? 0);
9351
9332
  return [
9352
9333
  parts.join("\n"),
9353
9334
  "",
@@ -9366,51 +9347,6 @@ function registerUndoTool(_rootCtx, agentCtx, io, sandbox) {
9366
9347
  return agentCtx.tools.register(buildUndoTool(io, sandbox));
9367
9348
  }
9368
9349
 
9369
- //#endregion
9370
- //#region src/url-schema/vendored/hashline/write-hook.js
9371
- const AUTO_READ_HEADING = "--- Auto-read (hashline anchors) ---";
9372
- /**
9373
- * Register the post-write auto-read listener on the calling agent's scope.
9374
- * Replaces the result content (never the canonical value) with the original
9375
- * content plus the hashline preview; any failure falls back to the untouched
9376
- * decision so a broken auto-read never breaks the write.
9377
- * @param rootCtx - host context for diagnostics.
9378
- * @param agentCtx - the agent's scoped context; the listener receives only
9379
- * this agent's tool results.
9380
- * @param io - the filesystem bridge.
9381
- * @returns the exact disposer that unregisters the listener.
9382
- */
9383
- function registerWriteHook(rootCtx, agentCtx, io) {
9384
- return agentCtx.on("tools/post-execute", async (exec, result, next) => {
9385
- return withWorkspace(execCwd(exec), async () => {
9386
- const decision = await next();
9387
- if (exec.name !== "write" || result.isError || decision.kind !== "accept") return decision;
9388
- const decisionContent = decision.content ?? result.content;
9389
- const rawPath = exec.arguments?.file_path ?? exec.arguments?.path;
9390
- if (typeof rawPath !== "string") return decision;
9391
- try {
9392
- const cwd = execCwd(exec);
9393
- const sessionKey = execSessionKey(exec);
9394
- const signal = exec.signal;
9395
- const { text } = await readAndServe(io, rawPath, cwd, {
9396
- sessionKey,
9397
- signal
9398
- });
9399
- return {
9400
- kind: "accept",
9401
- content: [...decisionContent, {
9402
- type: "text",
9403
- text: `\n\n${AUTO_READ_HEADING}\n${text}`
9404
- }]
9405
- };
9406
- } catch (error) {
9407
- rootCtx.logger.warn(`dsh-better-edit: auto-read after write failed: ${error instanceof Error ? error.message : String(error)}`);
9408
- return decision;
9409
- }
9410
- });
9411
- });
9412
- }
9413
-
9414
9350
  //#endregion
9415
9351
  //#region src/url-schema/vendored/hashline/guidance/parse.js
9416
9352
  /**
@@ -9847,7 +9783,6 @@ function installAgentTools(rootCtx, agent, resolver) {
9847
9783
  const hashlineIo = ctxFsIO(rootCtx.fs, rootCtx);
9848
9784
  disposers.push(registerEditTool(rootCtx, agent.ctx, hashlineIo, hashlineSandbox));
9849
9785
  disposers.push(registerUndoTool(rootCtx, agent.ctx, hashlineIo, hashlineSandbox));
9850
- disposers.push(registerWriteHook(rootCtx, agent.ctx, hashlineIo));
9851
9786
  {
9852
9787
  const diagnosticsHook = buildLspWriteFeedback().postWrite;
9853
9788
  disposers.push(agent.ctx.on("tools/post-execute", async (exec, result, next) => {
@@ -10192,17 +10127,10 @@ const agentMessageExecutor = async (rawArgs, exec, deps) => {
10192
10127
  const subagents = deps.requireSubagents?.();
10193
10128
  if (!subagents) return { error: "agent_message(receiver='child') is unavailable: no ctx.subagents service is mounted in this composition" };
10194
10129
  try {
10195
- return { messageId: await subagents.followup(exec.agent, subagentId, [{
10130
+ return { messageId: await subagents.sendMessage(exec.agent, subagentId, [{
10196
10131
  type: "text",
10197
10132
  text: message
10198
- }], {
10199
- source: {
10200
- kind: "coordinator",
10201
- form: "relay",
10202
- senderSessionId: exec.agent.id
10203
- },
10204
- signal: exec.signal
10205
- }) };
10133
+ }], { signal: exec.signal }) };
10206
10134
  } catch (error) {
10207
10135
  return { error: `agent_message(receiver='child') failed: ${error instanceof Error ? error.message : String(error)}` };
10208
10136
  }
@@ -10213,15 +10141,14 @@ const agentMessageExecutor = async (rawArgs, exec, deps) => {
10213
10141
  const subagents = deps.requireSubagents?.();
10214
10142
  if (!subagents) return { error: "agent_message(receiver='parent') is unavailable: no ctx.subagents service is mounted in this composition" };
10215
10143
  try {
10144
+ const parentId = exec.agent.session.header.parentSession;
10145
+ if (parentId === void 0) return { error: "agent_message(receiver='parent') rejected: only a live continuable child agent can report to its parent (a root agent has none)" };
10216
10146
  return {
10217
10147
  delivered: true,
10218
- message_id: await subagents.reportFrom(exec.agent, [{
10148
+ message_id: await subagents.sendMessage(exec.agent, parentId, [{
10219
10149
  type: "text",
10220
10150
  text: message
10221
- }], {
10222
- delivery: "next-step",
10223
- signal: exec.signal
10224
- })
10151
+ }], { signal: exec.signal })
10225
10152
  };
10226
10153
  } catch (error) {
10227
10154
  if (error.code === "UNAUTHORIZED") return { error: `agent_message(receiver='parent') rejected: only a live continuable child agent can report to its parent (a root agent has none) — ${error instanceof Error ? error.message : String(error)}` };
@@ -10994,7 +10921,6 @@ function buildBootScript(config) {
10994
10921
  const mobile = config.mobile;
10995
10922
  const mobilePayload = mobile?.enabled !== false ? {
10996
10923
  enabled: true,
10997
- ...mobile?.breakpoint !== void 0 ? { breakpoint: mobile.breakpoint } : {},
10998
10924
  ...mobile?.swipeDistancePx !== void 0 ? { swipeDistancePx: mobile.swipeDistancePx } : {},
10999
10925
  ...mobile?.dominanceRatio !== void 0 ? { dominanceRatio: mobile.dominanceRatio } : {},
11000
10926
  ...mobile?.leftEdgeBandPx !== void 0 ? { leftEdgeBandPx: mobile.leftEdgeBandPx } : {},
@@ -11048,7 +10974,6 @@ const inject = ["tools"];
11048
10974
  /** Runtime schema. */
11049
10975
  const MOBILE_CONFIG = z.object({
11050
10976
  enabled: z.boolean().default(true),
11051
- breakpoint: z.natural().min(200).default(768),
11052
10977
  swipeDistancePx: z.natural().min(8).default(40),
11053
10978
  dominanceRatio: z.number().min(1).default(1.3),
11054
10979
  leftEdgeBandPx: z.natural().min(8).default(120),
@@ -11274,7 +11199,7 @@ function renderValue(value) {
11274
11199
  * in the module doc. Sub-calls ride the registry's exported staged scheduler
11275
11200
  * (`prepare`/`dispatch`/`finalize`/`finish`) under the native concurrency
11276
11201
  * contract; each sub-dispatch is logged for reconstruction
11277
- * (`tool/code-dispatch-start` / `tool/code-dispatch`) while only the outer
11202
+ * (`tool/ptc-dispatch-start` / `tool/ptc-dispatch`) while only the outer
11278
11203
  * curated result enters model history.
11279
11204
  * @param registry - the host tool registry (sub-calls go through its staged
11280
11205
  * scheduler, bindings cover its registered tools).
@@ -11457,7 +11382,7 @@ function createRunCellTool(registry, options) {
11457
11382
  isError: result.isError,
11458
11383
  content: result.content
11459
11384
  });
11460
- agent.session.append("tool/code-dispatch", {
11385
+ agent.session.append("tool/ptc-dispatch", {
11461
11386
  rootCallId: exec.rootCallId,
11462
11387
  parentCallId: exec.callId,
11463
11388
  subCallId,
@@ -11491,7 +11416,7 @@ function createRunCellTool(registry, options) {
11491
11416
  settleError(error);
11492
11417
  },
11493
11418
  async start() {
11494
- exec.agent?.session.append("tool/code-dispatch-start", {
11419
+ exec.agent?.session.append("tool/ptc-dispatch-start", {
11495
11420
  rootCallId: exec.rootCallId,
11496
11421
  parentCallId: exec.callId,
11497
11422
  subCallId,
@@ -11754,7 +11679,7 @@ function apply(ctx, config) {
11754
11679
  if (session === void 0) return "";
11755
11680
  const sandboxPolicy = runtimeCtx.get("sandboxPolicy");
11756
11681
  if (!sandboxPolicy) return "";
11757
- return sandboxPolicy.resolve({ session }).mode === "workspace-write" ? "Restricted operations may be retried once with sandbox_permissions for single-call escalation, pending user approval." : "";
11682
+ return sandboxPolicy.resolve({ session }).mode === "workspace-write" ? "A sandbox-deniable/denied call may be escalated with `sandbox_permissions=\"danger-full-access\"` and a one-line `justification`; the runtime will prompt for user approval. Escalation and its approval/denial are per-call." : "";
11758
11683
  }
11759
11684
  });
11760
11685
  });