better-dsh 0.2.2-c → 0.2.3-b

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 (27) 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/v0.2.3b-hashline-content-locator/345/256/236/346/265/213/346/212/245/345/221/212.md +73 -0
  6. package/docs/60_exploration-and-research/01-cordis-runtime/cordis-engineering-feasibility.md +155 -0
  7. package/docs/60_exploration-and-research/01-cordis-runtime/cordis-research.md +385 -152
  8. package/docs/60_exploration-and-research/01-cordis-runtime/js-ts-language-fundamentals.md +628 -0
  9. package/docs/60_exploration-and-research/02-dsh/dash-research.md +592 -0
  10. package/docs/60_exploration-and-research/02-dsh/dsh-web-profile-package-map.md +186 -0
  11. package/docs/60_exploration-and-research/02-dsh/dsh-web-ui-slot-system-research.md +310 -0
  12. package/docs/60_exploration-and-research/02-dsh/dsh-webui-backend-data-inventory.md +633 -0
  13. package/docs/60_exploration-and-research/02-dsh/dsh-webui-strip-boundary-research.md +300 -0
  14. package/docs/60_exploration-and-research/02-dsh/dsh-webui-wire-appendix.md +3729 -0
  15. package/docs/60_exploration-and-research/02-dsh/web-frontend-composability-research.md +191 -0
  16. package/docs/60_exploration-and-research/02-dsh/webui-wire-data/capture-live-turn.json +1 -0
  17. package/docs/60_exploration-and-research/02-dsh/webui-wire-data/observed-endpoints.json +224 -0
  18. package/docs/60_exploration-and-research/02-dsh/webui-wire-data/remote-inventory.json +110954 -0
  19. package/docs/60_exploration-and-research/02-dsh/webui-wire-data/served-index-sample.html +47 -0
  20. package/docs/60_exploration-and-research/02-dsh/webui-wire-data/session-events.json +9411 -0
  21. package/docs/60_exploration-and-research/02-dsh/webui-wire-data/ws-frame-examples.json +20 -0
  22. package/docs/superd/00-blueprint.md +331 -0
  23. package/docs/superd/01-component-boundaries.md +261 -0
  24. package/docs/superd/02-dsh-embed-modes.md +151 -0
  25. package/docs/superd/image.png +0 -0
  26. package/lib/index.js +1214 -1293
  27. package/package.json +1 -1
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);
@@ -5166,7 +5166,7 @@ async function renderRoster(family, descendants, agents) {
5166
5166
  /** One child's last activity: its live log's last event, else its persisted one. */
5167
5167
  async function lastActivityOf(rawId, family) {
5168
5168
  const live = family.sessions.get(SessionId(rawId));
5169
- if (live !== void 0) return lastEventTime(live.events);
5169
+ if (live !== void 0) return lastEventTime(live.snapshotEvents());
5170
5170
  const inspection = await family.sessionPersistence.inspect(SessionId(rawId));
5171
5171
  return inspection === void 0 ? "-" : lastEventTime(inspection.events);
5172
5172
  }
@@ -5941,7 +5941,7 @@ function createGrepTool(deps) {
5941
5941
  const MAX_BYTES = 100 * 1024 * 1024;
5942
5942
  const MAX_READ_LINE_BYTES = 200 * 1024;
5943
5943
  const HASH_STORE_BUSY_TIMEOUT = 1e3;
5944
- const HASH_STORE_VERSION = 6;
5944
+ const HASH_STORE_VERSION = 7;
5945
5945
  const EDITS_MAX_ITEMS = 32;
5946
5946
  const SERVED_TTL_MS = 10080 * 60 * 1e3;
5947
5947
  const SERVED_ECHO_CAP = 150;
@@ -6095,20 +6095,18 @@ function assertUndoRequest(request) {
6095
6095
  //#endregion
6096
6096
  //#region src/url-schema/vendored/hashline/paths.js
6097
6097
  /**
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.
6098
+ * On-disk home for dsh-better-edit state: ONE centralized store per harness
6099
+ * home — `$DSH_HOME/storages/dsh-better-edit` (default
6100
+ * `~/.dsh/storages/dsh-better-edit`), resolved through the harness home
6101
+ * resolver so `DSH_HOME` isolates deployments (test homes never touch prod).
6102
+ * There is no per-workspace state directory: the store keys everything by
6103
+ * canonical absolute path.
6106
6104
  */
6107
- function configDir(cwd) {
6108
- return cwd !== void 0 ? join(resolve(cwd), ".dsh_better_edit") : join(resolveDshHome(), "plugins", "dsh-better-edit");
6105
+ function configDir() {
6106
+ return join(resolveDshHome(), "storages", "dsh-better-edit");
6109
6107
  }
6110
- function hashStorePath(cwd) {
6111
- return join(configDir(cwd), "hash-store.sqlite");
6108
+ function hashStorePath() {
6109
+ return join(configDir(), "hash-store.sqlite");
6112
6110
  }
6113
6111
 
6114
6112
  //#endregion
@@ -6330,769 +6328,655 @@ function mapStableHashes(oldContent, oldHashes, newContent, removedHashes) {
6330
6328
  }
6331
6329
 
6332
6330
  //#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));
6331
+ //#region src/url-schema/vendored/hashline/hash-store.js
6332
+ function isValidHashList(value) {
6333
+ if (!Array.isArray(value)) return false;
6334
+ for (const hash of value) if (typeof hash !== "string" || !HASH_RE.test(hash)) return false;
6335
+ return true;
6345
6336
  }
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");
6337
+ function isValidSnapshot(value) {
6338
+ if (typeof value !== "object" || value === null) return false;
6339
+ const v = value;
6340
+ if (typeof v.content !== "string") return false;
6341
+ return isValidHashList(v.hashes);
6353
6342
  }
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
- };
6343
+ /** A served-row array: per-position hash, or null for never-served slots. */
6344
+ function isValidServedList(value) {
6345
+ if (!Array.isArray(value)) return false;
6346
+ for (const entry of value) {
6347
+ if (entry === null) continue;
6348
+ if (typeof entry !== "string" || !HASH_RE.test(entry)) return false;
6349
+ }
6350
+ return true;
6370
6351
  }
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}).`);
6352
+ function cacheKey(checksum) {
6353
+ return `${CANON_VERSION}:${checksum}`;
6373
6354
  }
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
- }
6355
+ function isCorruptionError(error) {
6356
+ if (error && typeof error === "object") {
6357
+ const errcode = error.errcode;
6358
+ if (typeof errcode === "number") return errcode === 11 || errcode === 24 || errcode === 26;
6359
+ const code = error.code;
6360
+ if (typeof code === "string" && /NOTADB|CORRUPT/.test(code)) return true;
6407
6361
  }
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
- }
6362
+ return error instanceof Error && /corrupt|not a database|malformed|database disk image/i.test(error.message);
6363
+ }
6364
+ function isBusyError(error) {
6365
+ if (error && typeof error === "object") {
6366
+ const errcode = error.errcode;
6367
+ if (typeof errcode === "number") return errcode === 5 || errcode === 6;
6421
6368
  }
6422
- return {
6423
- message: out.join("\n"),
6424
- servedRows
6425
- };
6369
+ return error instanceof Error && /busy|locked/i.test(error.message);
6426
6370
  }
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).`);
6371
+ function sleepSync(ms) {
6372
+ const sab = new Int32Array(new SharedArrayBuffer(4));
6373
+ Atomics.wait(sab, 0, 0, ms);
6439
6374
  }
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
- };
6375
+ const BUSY_RETRIES = 3;
6376
+ const BUSY_RETRY_DELAY_MS = 100;
6377
+ function withBusyRetry(fn) {
6378
+ let lastError;
6379
+ for (let attempt = 0; attempt <= BUSY_RETRIES; attempt++) try {
6380
+ return fn();
6381
+ } catch (error) {
6382
+ lastError = error;
6383
+ if (!isBusyError(error) || attempt === BUSY_RETRIES) throw error;
6384
+ sleepSync(BUSY_RETRY_DELAY_MS);
6385
+ }
6386
+ throw lastError;
6461
6387
  }
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.");
6388
+ function openDbWithBusyRetry(storePath) {
6389
+ return withBusyRetry(() => openDb(storePath));
6464
6390
  }
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
- };
6391
+ /** One open store per store path (per workspace); parallel sessions share per-workspace dbs. */
6392
+ const stores = /* @__PURE__ */ new Map();
6393
+ const openings = /* @__PURE__ */ new Map();
6394
+ let exitHandlerRegistered = false;
6395
+ function openDb(storePath) {
6396
+ const db = new DatabaseSync(storePath, { timeout: HASH_STORE_BUSY_TIMEOUT });
6397
+ try {
6398
+ return buildStore(db);
6399
+ } catch (error) {
6400
+ try {
6401
+ db.close();
6402
+ } catch {}
6403
+ throw error;
6404
+ }
6487
6405
  }
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}.`);
6406
+ function buildStore(db) {
6407
+ db.exec("PRAGMA journal_mode = WAL");
6408
+ db.exec("PRAGMA synchronous = NORMAL");
6409
+ 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)");
6410
+ db.exec("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
6411
+ 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)");
6412
+ const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'version'").get();
6413
+ const versionChanged = versionRow !== void 0 && versionRow.value !== String(HASH_STORE_VERSION);
6414
+ if (versionChanged) {
6415
+ db.exec("DELETE FROM snapshots");
6416
+ db.exec("DELETE FROM undo");
6417
+ }
6418
+ const servedColumns = db.prepare("PRAGMA table_info(served)").all();
6419
+ if (versionChanged || servedColumns.some((column) => column.name === "session_id")) db.exec("DROP TABLE IF EXISTS served");
6420
+ 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)");
6421
+ db.prepare("INSERT INTO meta (key, value) VALUES ('version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(HASH_STORE_VERSION));
6422
+ const getStmt = db.prepare("SELECT hashes FROM snapshots WHERE path = ? AND checksum = ? AND line_count = ?");
6423
+ const allStmt = db.prepare("SELECT path FROM snapshots UNION SELECT path FROM undo UNION SELECT path FROM served");
6424
+ const allHashesStmt = db.prepare("SELECT path, hashes FROM snapshots");
6425
+ const delStmt = db.prepare("DELETE FROM snapshots WHERE path = ?");
6426
+ 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");
6427
+ 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");
6428
+ const undoGetStmt = db.prepare("SELECT content, bom, ending, hashes, result_content FROM undo WHERE path = ?");
6429
+ const undoDelStmt = db.prepare("DELETE FROM undo WHERE path = ?");
6430
+ const servedGetStmt = db.prepare("SELECT hashes, reported FROM served WHERE path = ?");
6431
+ 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");
6432
+ 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");
6433
+ const servedReportedClearStmt = db.prepare("UPDATE served SET reported = NULL, updated_at = ? WHERE path = ?");
6434
+ const servedDeleteStmt = db.prepare("DELETE FROM served WHERE path = ?");
6435
+ const servedDeletePathStmt = db.prepare("DELETE FROM served WHERE path = ?");
6436
+ const servedPruneOlderThanStmt = db.prepare("DELETE FROM served WHERE updated_at < ?");
6507
6437
  return {
6508
- ...edit,
6509
- content_lines: contentLines
6510
- };
6438
+ db,
6439
+ stmts: {
6440
+ get: (...params) => getStmt.get(...params),
6441
+ allPaths: (...params) => allStmt.all(...params),
6442
+ allHashes: (...params) => allHashesStmt.all(...params),
6443
+ deleteOne: (...params) => {
6444
+ withBusyRetry(() => {
6445
+ delStmt.run(...params);
6446
+ });
6447
+ },
6448
+ upsert: (...params) => {
6449
+ withBusyRetry(() => {
6450
+ upsertStmt.run(...params);
6451
+ });
6452
+ },
6453
+ undoUpsert: (...params) => {
6454
+ withBusyRetry(() => {
6455
+ undoUpsertStmt.run(...params);
6456
+ });
6457
+ },
6458
+ undoGet: (...params) => undoGetStmt.get(...params),
6459
+ undoDelete: (...params) => {
6460
+ withBusyRetry(() => {
6461
+ undoDelStmt.run(...params);
6462
+ });
6463
+ },
6464
+ servedGet: (...params) => servedGetStmt.get(...params),
6465
+ servedUpsert: (...params) => {
6466
+ withBusyRetry(() => {
6467
+ servedUpsertStmt.run(...params);
6468
+ });
6469
+ },
6470
+ servedReportedUpsert: (...params) => {
6471
+ withBusyRetry(() => {
6472
+ servedReportedUpsertStmt.run(...params);
6473
+ });
6474
+ },
6475
+ servedReportedClear: (path$1) => {
6476
+ withBusyRetry(() => {
6477
+ servedReportedClearStmt.run(Date.now(), path$1);
6478
+ });
6479
+ },
6480
+ servedDelete: (...params) => {
6481
+ withBusyRetry(() => {
6482
+ servedDeleteStmt.run(...params);
6483
+ });
6484
+ },
6485
+ servedDeletePath: (...params) => {
6486
+ withBusyRetry(() => {
6487
+ servedDeletePathStmt.run(...params);
6488
+ });
6489
+ },
6490
+ servedPruneOlderThan: (...params) => {
6491
+ withBusyRetry(() => {
6492
+ servedPruneOlderThanStmt.run(...params);
6493
+ });
6494
+ }
6495
+ }
6496
+ };
6511
6497
  }
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.`);
6498
+ /** Wire the domain methods over the prepared statements. */
6499
+ function makeDomainStore(stmts) {
6521
6500
  return {
6522
- ...edit,
6523
- hash_bounds: [endRef, startRef]
6501
+ engine: "node:sqlite",
6502
+ getSnapshot(path$1, content, deleteCorrupt = true) {
6503
+ const checksum = cacheKey(contentChecksum(content));
6504
+ const lineCount = splitLines(content).length;
6505
+ const row = stmts.get(path$1, checksum, lineCount);
6506
+ if (!row) return void 0;
6507
+ try {
6508
+ const parsed = JSON.parse(row.hashes);
6509
+ if (isValidHashList(parsed)) return parsed;
6510
+ if (deleteCorrupt) stmts.deleteOne(path$1);
6511
+ return;
6512
+ } catch {
6513
+ if (deleteCorrupt) stmts.deleteOne(path$1);
6514
+ return;
6515
+ }
6516
+ },
6517
+ upsertSnapshot(path$1, checksum, lineCount, hashes) {
6518
+ stmts.upsert(path$1, cacheKey(checksum), lineCount, JSON.stringify(hashes), Date.now());
6519
+ },
6520
+ allKnownPaths() {
6521
+ return stmts.allPaths();
6522
+ },
6523
+ allSnapshotHashes() {
6524
+ return stmts.allHashes();
6525
+ },
6526
+ deleteSnapshot(path$1) {
6527
+ stmts.deleteOne(path$1);
6528
+ },
6529
+ findSnapshotPaths(hashes) {
6530
+ const rows = stmts.allHashes();
6531
+ const matches = [];
6532
+ for (const row of rows) try {
6533
+ const parsed = JSON.parse(row.hashes);
6534
+ if (!isValidHashList(parsed)) continue;
6535
+ if (hashes.every((h) => parsed.includes(h))) matches.push(row.path);
6536
+ } catch {}
6537
+ return matches;
6538
+ },
6539
+ getUndo(path$1) {
6540
+ const row = stmts.undoGet(path$1);
6541
+ if (!row) return void 0;
6542
+ try {
6543
+ const parsed = JSON.parse(row.hashes);
6544
+ if (!isValidHashList(parsed)) {
6545
+ stmts.undoDelete(path$1);
6546
+ return;
6547
+ }
6548
+ return {
6549
+ content: row.content,
6550
+ bom: row.bom,
6551
+ ending: row.ending,
6552
+ hashes: parsed,
6553
+ resultContent: row.result_content
6554
+ };
6555
+ } catch {
6556
+ stmts.undoDelete(path$1);
6557
+ return;
6558
+ }
6559
+ },
6560
+ upsertUndo(path$1, entry) {
6561
+ stmts.undoUpsert(path$1, entry.content, entry.bom, entry.ending, JSON.stringify(entry.hashes), entry.resultContent, Date.now());
6562
+ },
6563
+ deleteUndo(path$1) {
6564
+ stmts.undoDelete(path$1);
6565
+ },
6566
+ getServed(path$1) {
6567
+ const row = stmts.servedGet(path$1);
6568
+ if (!row) return [];
6569
+ try {
6570
+ const parsed = JSON.parse(row.hashes);
6571
+ if (isValidServedList(parsed)) return parsed;
6572
+ stmts.servedDelete(path$1);
6573
+ return [];
6574
+ } catch {
6575
+ stmts.servedDelete(path$1);
6576
+ return [];
6577
+ }
6578
+ },
6579
+ getServedReported(path$1) {
6580
+ const row = stmts.servedGet(path$1);
6581
+ if (!row) return /* @__PURE__ */ new Set();
6582
+ const raw = row.reported;
6583
+ if (typeof raw !== "string" || raw.length === 0) return /* @__PURE__ */ new Set();
6584
+ try {
6585
+ const parsed = JSON.parse(raw);
6586
+ if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
6587
+ return new Set(parsed.filter((h) => typeof h === "string" && HASH_RE.test(h)));
6588
+ } catch {
6589
+ return /* @__PURE__ */ new Set();
6590
+ }
6591
+ },
6592
+ upsertServed(path$1, hashesJson) {
6593
+ stmts.servedUpsert(path$1, hashesJson, Date.now());
6594
+ },
6595
+ upsertServedReported(path$1, reportedJson) {
6596
+ stmts.servedReportedUpsert(path$1, reportedJson, Date.now());
6597
+ },
6598
+ clearServedReported(path$1) {
6599
+ stmts.servedReportedClear(path$1);
6600
+ },
6601
+ deleteServed(path$1) {
6602
+ stmts.servedDelete(path$1);
6603
+ },
6604
+ deleteServedByPath(path$1) {
6605
+ stmts.servedDeletePath(path$1);
6606
+ },
6607
+ pruneServedOlderThan(ts) {
6608
+ stmts.servedPruneOlderThan(ts);
6609
+ },
6610
+ async pruneMissing() {
6611
+ const missing = await statMissing(stmts.allPaths());
6612
+ if (missing.length === 0) return;
6613
+ withStore(() => {
6614
+ for (const path$1 of missing) {
6615
+ stmts.deleteOne(path$1);
6616
+ stmts.undoDelete(path$1);
6617
+ stmts.servedDeletePath(path$1);
6618
+ }
6619
+ });
6620
+ }
6524
6621
  };
6525
6622
  }
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
- });
6623
+ function isHealthy(db) {
6624
+ try {
6625
+ return db.prepare("PRAGMA quick_check").get()?.quick_check === "ok";
6626
+ } catch (error) {
6627
+ if (isCorruptionError(error)) return false;
6628
+ return true;
6537
6629
  }
6538
- return dups;
6539
6630
  }
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
- });
6631
+ async function quarantineStore(storePath) {
6632
+ const suffix = `.corrupt-${Date.now()}`;
6633
+ for (const candidate of [
6634
+ storePath,
6635
+ `${storePath}-wal`,
6636
+ `${storePath}-shm`
6637
+ ]) try {
6638
+ await rename(candidate, `${candidate}${suffix}`);
6639
+ } catch (error) {
6640
+ if (errCode(error) !== "ENOENT") console.error("Failed to quarantine corrupt hash store file:", error);
6551
6641
  }
6552
- return dups;
6553
6642
  }
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;
6643
+ function shutdownDb(db) {
6644
+ try {
6645
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
6646
+ } catch {}
6647
+ db.close();
6648
+ }
6649
+ const STAT_BATCH = 64;
6650
+ async function statMissing(rows) {
6651
+ const missing = [];
6652
+ for (let i = 0; i < rows.length; i += STAT_BATCH) {
6653
+ const batch = rows.slice(i, i + STAT_BATCH);
6654
+ const results = await Promise.all(batch.map(async (row) => {
6655
+ try {
6656
+ await stat(row.path);
6657
+ return;
6658
+ } catch {
6659
+ return row.path;
6660
+ }
6661
+ }));
6662
+ for (const path$1 of results) if (path$1 !== void 0) missing.push(path$1);
6562
6663
  }
6563
- return true;
6664
+ return missing;
6564
6665
  }
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
6666
+ async function openStore(storePath) {
6667
+ await initHasher();
6668
+ await mkdir(dirname(storePath), { recursive: true });
6669
+ let existed = existsSync(storePath);
6670
+ let opened;
6671
+ try {
6672
+ opened = openDbWithBusyRetry(storePath);
6673
+ } catch (error) {
6674
+ if (!isCorruptionError(error)) throw error;
6675
+ console.error("Hash store failed to open, rebuilding:", error);
6676
+ await quarantineStore(storePath);
6677
+ existed = false;
6678
+ opened = openDbWithBusyRetry(storePath);
6679
+ }
6680
+ if (!isHealthy(opened.db)) {
6681
+ shutdownDb(opened.db);
6682
+ await quarantineStore(storePath);
6683
+ existed = false;
6684
+ opened = openDbWithBusyRetry(storePath);
6685
+ }
6686
+ const { db, stmts } = opened;
6687
+ if (!existed) await migrateLegacy(db, storePath);
6688
+ withBusyRetry(() => {
6689
+ stmts.servedPruneOlderThan(Date.now() - SERVED_TTL_MS);
6576
6690
  });
6577
- return dups;
6691
+ const store = makeDomainStore(stmts);
6692
+ stores.set(storePath, {
6693
+ path: storePath,
6694
+ db,
6695
+ stmts,
6696
+ store
6697
+ });
6698
+ if (!exitHandlerRegistered) {
6699
+ exitHandlerRegistered = true;
6700
+ process.once("exit", () => shutdownHashStore());
6701
+ for (const sig of ["SIGINT", "SIGTERM"]) process.once(sig, () => {
6702
+ shutdownHashStore();
6703
+ process.kill(process.pid, sig);
6704
+ });
6705
+ }
6706
+ return store;
6578
6707
  }
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
6708
+ /** Resolve the store path: the single centralized store under `$DSH_HOME/storages`. */
6709
+ function storePathFor() {
6710
+ return hashStorePath();
6711
+ }
6712
+ /**
6713
+ * Load (and cache) the centralized hash store. There is exactly one store per
6714
+ * harness home; `DSH_HOME` isolates deployments (test homes never touch prod).
6715
+ */
6716
+ function loadHashStore() {
6717
+ const storePath = storePathFor();
6718
+ const cached = stores.get(storePath);
6719
+ if (cached && cached.db.isOpen) return Promise.resolve(cached.store);
6720
+ const existing = openings.get(storePath);
6721
+ if (existing) return existing;
6722
+ const promise = openStore(storePath).finally(() => {
6723
+ openings.delete(storePath);
6591
6724
  });
6592
- return dups;
6725
+ openings.set(storePath, promise);
6726
+ return promise;
6593
6727
  }
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;
6728
+ /** The cached store entry for the active workspace (or the shared-home fallback), if open. */
6729
+ function currentStore() {
6730
+ const entry = stores.get(storePathFor());
6731
+ return entry?.db.isOpen ? entry : void 0;
6601
6732
  }
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
- }
6733
+ /** Close every open store (process exit, HMR, tests). */
6734
+ function shutdownHashStore() {
6735
+ for (const [, entry] of stores) shutdownDb(entry.db);
6736
+ stores.clear();
6737
+ openings.clear();
6618
6738
  }
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);
6630
- }
6631
- const tryResolve = (ref) => {
6632
- const result = resAnchorFromMap(ref, hashIndex);
6633
- if ("kind" in result) {
6634
- mismatches.push(result);
6635
- return;
6636
- }
6637
- return result;
6638
- };
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;
6739
+ /**
6740
+ * Run `fn` inside one BEGIN IMMEDIATE transaction on the active workspace's
6741
+ * store. Without an open store for this context the call runs bare (the
6742
+ * caller has already loaded the store in every in-process path).
6743
+ */
6744
+ function withStore(fn) {
6745
+ const store = currentStore();
6746
+ if (store) withBusyRetry(() => {
6747
+ store.db.exec("BEGIN IMMEDIATE");
6748
+ try {
6749
+ fn();
6750
+ store.db.exec("COMMIT");
6751
+ } catch (e) {
6752
+ try {
6753
+ store.db.exec("ROLLBACK");
6754
+ } catch {}
6755
+ throw e;
6649
6756
  }
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));
6661
- return {
6662
- resolved: {
6663
- content_lines: edit.content_lines,
6664
- hash_bounds: [startResolved, endResolved]
6665
- },
6666
- mismatches,
6667
- boundaryDups
6668
- };
6757
+ });
6758
+ else fn();
6669
6759
  }
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;
6760
+ async function migrateLegacy(db, storePath) {
6761
+ const legacyPath = join(dirname(storePath), "hash-store.json");
6762
+ let content;
6763
+ try {
6764
+ content = await readFile(legacyPath, "utf-8");
6765
+ } catch (error) {
6766
+ if (errCode(error) === "ENOENT") return;
6767
+ console.error("Failed to read legacy hash store for migration:", error);
6768
+ return;
6680
6769
  }
6681
- };
6682
- var AnchorMismatchError = class extends Error {
6683
- servedRows;
6684
- constructor(message, servedRows) {
6685
- super(message);
6686
- this.name = "AnchorMismatchError";
6687
- this.servedRows = servedRows;
6770
+ let parsed;
6771
+ try {
6772
+ parsed = JSON.parse(content);
6773
+ } catch (error) {
6774
+ console.error("Failed to parse legacy hash store, skipping migration:", error);
6775
+ return;
6688
6776
  }
6689
- };
6690
- function buildRangeEcho(startLine, endLine, fileHashes) {
6691
- const total = endLine - startLine + 1;
6692
- const shown = Math.min(total, SERVED_ECHO_CAP);
6777
+ const raw = parsed.snapshots;
6778
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
6693
6779
  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");
6780
+ for (const [key, value] of Object.entries(raw)) {
6781
+ if (!isValidSnapshot(value)) continue;
6782
+ if (new Set(value.hashes).size !== value.hashes.length) {
6783
+ console.warn(`Skipped legacy snapshot with duplicate hashes for ${key}; it will be re-hashed on next read.`);
6784
+ continue;
6785
+ }
6786
+ rows.push([
6787
+ key,
6788
+ contentChecksum(value.content),
6789
+ splitLines(value.content).length,
6790
+ JSON.stringify(value.hashes),
6791
+ Date.now()
6792
+ ]);
6793
+ }
6794
+ if (rows.length > 0) {
6795
+ db.exec("BEGIN IMMEDIATE");
6796
+ try {
6797
+ const stmt = db.prepare("INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)");
6798
+ for (const row of rows) stmt.run(...row);
6799
+ db.exec("COMMIT");
6800
+ } catch (e) {
6801
+ db.exec("ROLLBACK");
6802
+ throw e;
6803
+ }
6804
+ }
6805
+ try {
6806
+ await rename(legacyPath, `${legacyPath}.bak`);
6807
+ } catch (error) {
6808
+ console.error("Failed to rename legacy hash store after migration:", error);
6809
+ }
6702
6810
  }
6703
- function retryHint() {
6704
- return "Retry with these anchors (no read needed).";
6811
+ /** Find files whose stored snapshot hashes contain every given anchor. */
6812
+ async function findSnapshotPathsByHashes(hashes) {
6813
+ return (await loadHashStore()).findSnapshotPaths(hashes);
6705
6814
  }
6706
- function paginationHint(nextOffset, more) {
6707
- return `[... ${more} more — read offset=${nextOffset}]`;
6815
+ /** Persist a hash snapshot for one path (async over the active store). */
6816
+ async function upsertSnapshotFor(path$1, checksum, lineCount, hashes) {
6817
+ (await loadHashStore()).upsertSnapshot(path$1, checksum, lineCount, hashes);
6708
6818
  }
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
- });
6819
+
6820
+ //#endregion
6821
+ //#region src/url-schema/vendored/hashline/hashline/hash.js
6822
+ let defaultHashSnapshotIO;
6823
+ function snapshotIOFor(store) {
6824
+ if (store) return {
6825
+ get: (path$1, content, deleteCorrupt) => Promise.resolve(store.getSnapshot(path$1, content, deleteCorrupt)),
6826
+ upsert: (path$1, checksum, lineCount, hashes) => {
6827
+ store.upsertSnapshot(path$1, checksum, lineCount, hashes);
6828
+ return Promise.resolve();
6739
6829
  }
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;
6830
+ };
6831
+ return defaultHashSnapshotIO ?? {
6832
+ get: async (path$1, content, deleteCorrupt) => {
6833
+ return (await loadHashStore()).getSnapshot(path$1, content, deleteCorrupt);
6834
+ },
6835
+ upsert: async (path$1, checksum, lineCount, hashes) => {
6836
+ (await loadHashStore()).upsertSnapshot(path$1, checksum, lineCount, hashes);
6837
+ }
6838
+ };
6839
+ }
6840
+ async function lineHashes(content, path$1, previous, ioOrStore, persist) {
6841
+ await initHasher();
6842
+ if (!path$1) return lineHashesPure(content);
6843
+ const io = ioOrStore && "getSnapshot" in ioOrStore ? snapshotIOFor(ioOrStore) : ioOrStore ?? snapshotIOFor(void 0);
6844
+ if (previous) {
6845
+ const newHashes$1 = mapStableHashes(previous.content, previous.hashes, content, previous.removedHashes);
6846
+ if (persist !== false && io) try {
6847
+ await io.upsert(path$1, contentChecksum(content), splitLines(content).length, newHashes$1);
6848
+ } catch (e) {
6849
+ console.error("Failed to persist hash snapshot:", e);
6747
6850
  }
6851
+ return newHashes$1;
6748
6852
  }
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
- });
6853
+ let cached;
6854
+ if (io) try {
6855
+ cached = await io.get(path$1, content, persist !== false);
6856
+ } catch (e) {
6857
+ console.error("Failed to read hash store snapshot:", e);
6762
6858
  }
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
6768
- });
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
6775
- });
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
6783
- });
6859
+ if (cached) return cached;
6860
+ const newHashes = lineHashesPure(content);
6861
+ if (persist !== false && io) try {
6862
+ await io.upsert(path$1, contentChecksum(content), splitLines(content).length, newHashes);
6863
+ } catch (e) {
6864
+ console.error("Failed to persist hash snapshot:", e);
6784
6865
  }
6866
+ return newHashes;
6785
6867
  }
6786
- async function recordEchoServes(sessionKey, path$1, rows, policy, lineCount) {
6787
- if (policy !== "live") return;
6788
- await recordServed(sessionKey, path$1, rows, lineCount);
6868
+
6869
+ //#endregion
6870
+ //#region src/url-schema/vendored/hashline/session-view.js
6871
+ const current = new AsyncLocalStorage();
6872
+ function withWorkspace(cwd, fn) {
6873
+ return current.run(cwd, fn);
6789
6874
  }
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;
6875
+ function execCwd(exec) {
6876
+ return exec.agent?.session.header.cwd ?? process.cwd();
6877
+ }
6878
+ /**
6879
+ * Merge served rows into a copy of the stored array. This single helper owns
6880
+ * the served-merge invariant shared by recordServed and recordServedTruncated.
6881
+ * Eagerly heals orphaned serves: if the same hash is written at a new position
6882
+ * the old position is nulled (O(n) scan, no extra I/O). This prevents a
6883
+ * partial re-serve from leaving a stale duplicate behind (ADR-0008).
6884
+ */
6885
+ function _mergeServedRows(current$1, rows, options) {
6886
+ const updated = current$1.slice();
6887
+ if (options?.truncateTo !== void 0 && updated.length > options.truncateTo) updated.length = options.truncateTo;
6888
+ if (options?.clearFrom !== void 0) for (let i = options.clearFrom; i < updated.length; i++) updated[i] = null;
6889
+ const index = /* @__PURE__ */ new Map();
6890
+ for (let i = 0; i < updated.length; i++) {
6891
+ const h = updated[i];
6892
+ if (h === null) continue;
6893
+ const prev = index.get(h);
6894
+ if (prev !== void 0) updated[prev] = null;
6895
+ index.set(h, i);
6798
6896
  }
6799
- return {
6800
- fileLines,
6801
- lineStarts
6802
- };
6897
+ for (const entry of rows) {
6898
+ if (!Number.isInteger(entry.position) || entry.position < 0) throw new TypeError(`Invalid served position: ${entry.position}`);
6899
+ if (entry.hash !== null && (typeof entry.hash !== "string" || !HASH_RE.test(entry.hash))) throw new TypeError(`Invalid served hash: ${String(entry.hash)}`);
6900
+ while (updated.length <= entry.position) updated.push(null);
6901
+ if (entry.hash !== null) {
6902
+ const existing = index.get(entry.hash);
6903
+ if (existing !== void 0 && existing !== entry.position) {
6904
+ updated[existing] = null;
6905
+ index.delete(entry.hash);
6906
+ }
6907
+ const oldAtPos = updated[entry.position];
6908
+ if (oldAtPos !== null && oldAtPos !== entry.hash) index.delete(oldAtPos);
6909
+ index.set(entry.hash, entry.position);
6910
+ } else {
6911
+ const oldAtPos = updated[entry.position];
6912
+ if (oldAtPos !== null) index.delete(oldAtPos);
6913
+ }
6914
+ updated[entry.position] = entry.hash;
6915
+ }
6916
+ while (updated.length > 0 && updated[updated.length - 1] === null) updated.pop();
6917
+ return updated;
6803
6918
  }
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.");
6919
+ async function loadServed(path$1) {
6920
+ return (await loadHashStore()).getServed(path$1);
6806
6921
  }
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
- };
6922
+ async function recordServed(path$1, rows, lineCount) {
6923
+ if (rows.length === 0) return;
6924
+ try {
6925
+ const store = await loadHashStore();
6926
+ withStore(() => {
6927
+ const current$1 = store.getServed(path$1);
6928
+ const updated = _mergeServedRows(current$1, rows, lineCount === void 0 ? void 0 : { truncateTo: lineCount });
6929
+ if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
6930
+ store.upsertServed(path$1, JSON.stringify(updated));
6931
+ });
6932
+ } catch (error) {
6933
+ console.error("Failed to record served rows:", error);
6934
+ }
6848
6935
  }
6849
- function assemble(content, span, signal) {
6850
- abortIf(signal);
6851
- return content.slice(0, span.start) + span.replacement + content.slice(span.end);
6936
+ async function recordServedTruncated(path$1, rows, lineCount, clearFrom = 0) {
6937
+ if (rows.length === 0) return;
6938
+ try {
6939
+ const store = await loadHashStore();
6940
+ withStore(() => {
6941
+ const current$1 = store.getServed(path$1);
6942
+ const updated = _mergeServedRows(current$1, rows, {
6943
+ truncateTo: lineCount,
6944
+ clearFrom
6945
+ });
6946
+ if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
6947
+ store.upsertServed(path$1, JSON.stringify(updated));
6948
+ });
6949
+ } catch (error) {
6950
+ console.error("Failed to record truncated served rows:", error);
6951
+ }
6852
6952
  }
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);
6953
+ async function driftReported(path$1) {
6954
+ try {
6955
+ return (await loadHashStore()).getServedReported(path$1);
6956
+ } catch (error) {
6957
+ console.error("Failed to load reported drift set:", error);
6958
+ return /* @__PURE__ */ new Set();
6863
6959
  }
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
- });
6890
- }
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);
6895
- }
6896
- resolved = correctedResult.resolved;
6897
- }
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
- });
6911
- }
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
- }
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
- };
6945
- }
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");
6949
- }
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
6955
- };
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
6971
- };
6972
- }
6973
-
6974
- //#endregion
6975
- //#region src/url-schema/vendored/hashline/session-view.js
6976
- const current = new AsyncLocalStorage();
6977
- function withWorkspace(cwd, fn) {
6978
- return current.run(cwd, fn);
6979
- }
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
- function execCwd(exec) {
6989
- return exec.agent?.session.header.cwd ?? process.cwd();
6990
- }
6991
- function execSessionKey(exec) {
6992
- return sessionKeyFor(exec.agent?.session.id);
6993
- }
6994
- /**
6995
- * Merge served rows into a copy of the stored array. This single helper owns
6996
- * the served-merge invariant shared by recordServed and recordServedTruncated.
6997
- * Eagerly heals orphaned serves: if the same hash is written at a new position
6998
- * the old position is nulled (O(n) scan, no extra I/O). This prevents a
6999
- * partial re-serve from leaving a stale duplicate behind (ADR-0008).
7000
- */
7001
- function _mergeServedRows(current$1, rows, options) {
7002
- const updated = current$1.slice();
7003
- if (options?.truncateTo !== void 0 && updated.length > options.truncateTo) updated.length = options.truncateTo;
7004
- if (options?.clearFrom !== void 0) for (let i = options.clearFrom; i < updated.length; i++) updated[i] = null;
7005
- const index = /* @__PURE__ */ new Map();
7006
- for (let i = 0; i < updated.length; i++) {
7007
- const h = updated[i];
7008
- if (h === null) continue;
7009
- const prev = index.get(h);
7010
- if (prev !== void 0) updated[prev] = null;
7011
- index.set(h, i);
7012
- }
7013
- for (const entry of rows) {
7014
- if (!Number.isInteger(entry.position) || entry.position < 0) throw new TypeError(`Invalid served position: ${entry.position}`);
7015
- if (entry.hash !== null && (typeof entry.hash !== "string" || !HASH_RE.test(entry.hash))) throw new TypeError(`Invalid served hash: ${String(entry.hash)}`);
7016
- while (updated.length <= entry.position) updated.push(null);
7017
- if (entry.hash !== null) {
7018
- const existing = index.get(entry.hash);
7019
- if (existing !== void 0 && existing !== entry.position) {
7020
- updated[existing] = null;
7021
- index.delete(entry.hash);
7022
- }
7023
- const oldAtPos = updated[entry.position];
7024
- if (oldAtPos !== null && oldAtPos !== entry.hash) index.delete(oldAtPos);
7025
- index.set(entry.hash, entry.position);
7026
- } else {
7027
- const oldAtPos = updated[entry.position];
7028
- if (oldAtPos !== null) index.delete(oldAtPos);
7029
- }
7030
- updated[entry.position] = entry.hash;
7031
- }
7032
- while (updated.length > 0 && updated[updated.length - 1] === null) updated.pop();
7033
- return updated;
7034
- }
7035
- async function loadServed(sessionKey, path$1) {
7036
- return (await loadHashStore()).getServed(sessionKey, path$1);
7037
6960
  }
7038
- async function recordServed(sessionKey, path$1, rows, lineCount) {
7039
- if (rows.length === 0) return;
7040
- try {
7041
- const store = await loadHashStore();
7042
- withStore(() => {
7043
- const current$1 = store.getServed(sessionKey, path$1);
7044
- const updated = _mergeServedRows(current$1, rows, lineCount === void 0 ? void 0 : { truncateTo: lineCount });
7045
- if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
7046
- store.upsertServed(sessionKey, path$1, JSON.stringify(updated));
7047
- });
7048
- } catch (error) {
7049
- console.error("Failed to record served rows:", error);
7050
- }
7051
- }
7052
- async function recordServedTruncated(sessionKey, path$1, rows, lineCount, clearFrom = 0) {
7053
- if (rows.length === 0) return;
7054
- try {
7055
- const store = await loadHashStore();
7056
- withStore(() => {
7057
- const current$1 = store.getServed(sessionKey, path$1);
7058
- const updated = _mergeServedRows(current$1, rows, {
7059
- truncateTo: lineCount,
7060
- clearFrom
7061
- });
7062
- if (current$1.length === updated.length && current$1.every((v, i) => v === updated[i])) return;
7063
- store.upsertServed(sessionKey, path$1, JSON.stringify(updated));
7064
- });
7065
- } catch (error) {
7066
- console.error("Failed to record truncated served rows:", error);
7067
- }
7068
- }
7069
- async function driftReported(sessionKey, path$1) {
7070
- try {
7071
- return (await loadHashStore()).getServedReported(sessionKey, path$1);
7072
- } catch (error) {
7073
- console.error("Failed to load reported drift set:", error);
7074
- return /* @__PURE__ */ new Set();
7075
- }
7076
- }
7077
- async function markDriftReported(sessionKey, path$1, hashes) {
6961
+ async function markDriftReported(path$1, hashes) {
7078
6962
  try {
7079
6963
  const valid = hashes.filter((hash) => HASH_RE.test(hash));
7080
6964
  if (valid.length === 0) return;
7081
6965
  const store = await loadHashStore();
7082
6966
  withStore(() => {
7083
- const current$1 = store.getServedReported(sessionKey, path$1);
6967
+ const current$1 = store.getServedReported(path$1);
7084
6968
  for (const hash of valid) current$1.add(hash);
7085
- store.upsertServedReported(sessionKey, path$1, JSON.stringify([...current$1]));
6969
+ store.upsertServedReported(path$1, JSON.stringify([...current$1]));
7086
6970
  });
7087
6971
  } catch (error) {
7088
6972
  console.error("Failed to record reported drift set:", error);
7089
6973
  }
7090
6974
  }
7091
- async function clearDriftReported(sessionKey, path$1) {
6975
+ async function clearDriftReported(path$1) {
7092
6976
  try {
7093
6977
  const store = await loadHashStore();
7094
6978
  withStore(() => {
7095
- store.clearServedReported(sessionKey, path$1);
6979
+ store.clearServedReported(path$1);
7096
6980
  });
7097
6981
  } catch (error) {
7098
6982
  console.error("Failed to clear reported drift set:", error);
@@ -7189,568 +7073,661 @@ function computeDrift(input) {
7189
7073
  };
7190
7074
  }
7191
7075
  async function scanDrift(input) {
7192
- const reported = await driftReported(input.sessionKey, input.path);
7076
+ const reported = await driftReported(input.path);
7193
7077
  const result = computeDrift({
7194
7078
  ...input,
7195
7079
  reported
7196
7080
  });
7197
7081
  if (!result || result.allAlreadyReported) return result?.text;
7198
- await recordServed(input.sessionKey, input.path, result.rows.map((row) => ({
7082
+ await recordServed(input.path, result.rows.map((row) => ({
7199
7083
  position: row.position,
7200
7084
  hash: row.hash
7201
7085
  })), input.resultLines.length);
7202
- await markDriftReported(input.sessionKey, input.path, result.rows.filter((row) => row.drifted).map((row) => row.hash));
7086
+ await markDriftReported(input.path, result.rows.filter((row) => row.drifted).map((row) => row.hash));
7203
7087
  return result.text;
7204
7088
  }
7205
7089
 
7206
7090
  //#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;
7091
+ //#region src/url-schema/vendored/hashline/hashline/anchor-pipeline.js
7092
+ function diagRef(ref) {
7093
+ const trimmed = ref.trim();
7094
+ if (!trimmed.length) return `[E_BAD_REF] Invalid anchor. Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
7095
+ if (/^\d+/.test(trimmed)) return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. "aB3") — no line numbers or trailing content.`;
7096
+ 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.`;
7097
+ return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
7212
7098
  }
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);
7099
+ function parseRef(ref) {
7100
+ const trimmed = ref.trim();
7101
+ if (trimmed.length === ANCHOR_LEN && ALPH_RE.test(trimmed)) return { hash: trimmed };
7102
+ throw new Error(diagRef(ref));
7218
7103
  }
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;
7225
- }
7226
- return true;
7104
+ const parseHashRef = parseRef;
7105
+ function parseText(edit) {
7106
+ if (typeof edit !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
7107
+ const normalized = edit.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
7108
+ if (normalized === "") return [];
7109
+ if (/^\n+$/.test(normalized)) return new Array(normalized.length).fill("");
7110
+ return normalized.split("\n");
7227
7111
  }
7228
- function cacheKey(checksum) {
7229
- return `${CANON_VERSION}:${checksum}`;
7112
+ function resAnchorFromMap(ref, hashIndex) {
7113
+ const hashMatches = hashIndex.get(ref.hash);
7114
+ if (!hashMatches || hashMatches.length === 0) return {
7115
+ ref,
7116
+ kind: "not_found"
7117
+ };
7118
+ if (hashMatches.length === 1) return {
7119
+ line: hashMatches[0],
7120
+ hash: ref.hash,
7121
+ hashMatched: true
7122
+ };
7123
+ return {
7124
+ ref,
7125
+ kind: "ambiguous",
7126
+ candidates: hashMatches
7127
+ };
7230
7128
  }
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;
7237
- }
7238
- return error instanceof Error && /corrupt|not a database|malformed|database disk image/i.test(error.message);
7129
+ function assertAligned(fileLines, fileHashes, ctx) {
7130
+ if (fileHashes.length !== fileLines.length) throw new Error(`${ctx}: fileHashes.length (${fileHashes.length}) must match fileLines.length (${fileLines.length}).`);
7239
7131
  }
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;
7132
+ function fmtMismatchWithServes(mismatches, fileLines, fileHashes, filePath) {
7133
+ assertAligned(fileLines, fileHashes, "fmtMismatch");
7134
+ const out = [];
7135
+ const servedRows = [];
7136
+ const seen = /* @__PURE__ */ new Set();
7137
+ const pushRow = (ln) => {
7138
+ if (ln < 1 || ln > fileLines.length) return;
7139
+ const position = ln - 1;
7140
+ if (seen.has(position)) return;
7141
+ seen.add(position);
7142
+ servedRows.push({
7143
+ position,
7144
+ hash: fileHashes[ln - 1]
7145
+ });
7146
+ };
7147
+ const notFound = mismatches.filter((m) => m.kind === "not_found");
7148
+ const ambiguous = mismatches.filter((m) => m.kind === "ambiguous");
7149
+ const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
7150
+ if (notFound.length > 0) {
7151
+ out.push(`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. Re-read for fresh anchors.`);
7152
+ for (const m of notFound) {
7153
+ const ctx = m.context;
7154
+ if (!ctx) continue;
7155
+ const from = Math.max(1, ctx.line - 1);
7156
+ const to = Math.min(fileLines.length, ctx.line + 1);
7157
+ const rows = [];
7158
+ for (let ln = from; ln <= to; ln++) {
7159
+ rows.push(` ${ln}: ${fileHashes[ln - 1]}│${clipLine(fileLines[ln - 1] ?? "")}`);
7160
+ pushRow(ln);
7161
+ }
7162
+ out.push("");
7163
+ out.push(` Current context around resolved anchor "${ctx.hash}" (line ${ctx.line}):\n${rows.join("\n")}`);
7164
+ }
7244
7165
  }
7245
- return error instanceof Error && /busy|locked/i.test(error.message);
7166
+ if (ambiguous.length > 0) {
7167
+ if (out.length > 0) out.push("");
7168
+ out.push(`[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}. Re-read for fresh anchors.`);
7169
+ for (const m of ambiguous) {
7170
+ const sample = (m.candidates ?? []).slice(0, 5);
7171
+ const more = (m.candidates?.length ?? 0) > sample.length ? `, ... (+${(m.candidates?.length ?? 0) - sample.length} more)` : "";
7172
+ const lines = sample.map((line) => {
7173
+ const content = clipLine(fileLines[line - 1] ?? "");
7174
+ pushRow(line);
7175
+ return ` ${line}: ${fileHashes[line - 1]}│${content}`;
7176
+ }).join("\n");
7177
+ out.push(` Hash "${m.ref.hash}" matches lines ${sample.join(", ")}${more}.\n${lines}`);
7178
+ }
7179
+ }
7180
+ return {
7181
+ message: out.join("\n"),
7182
+ servedRows
7183
+ };
7246
7184
  }
7247
- function sleepSync(ms) {
7248
- const sab = new Int32Array(new SharedArrayBuffer(4));
7249
- Atomics.wait(sab, 0, 0, ms);
7185
+ const ITEM_KS = new Set([
7186
+ "replacement_text",
7187
+ "remove_from",
7188
+ "remove_to"
7189
+ ]);
7190
+ function assertItem(edit) {
7191
+ rejectUnknownFields(edit, ITEM_KS, "Edit", "The edit takes only { replacement_text, remove_from, remove_to }.");
7192
+ 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).`);
7193
+ 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).`);
7194
+ 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).`);
7195
+ if (typeof edit.replacement_text !== "string") throw new Error(NEW_CONTENT_NOT_STRING_MSG);
7196
+ 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).`);
7250
7197
  }
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);
7198
+ const ANCHOR_ROW_RE = /* @__PURE__ */ new RegExp(`^([+-]?)(${HASH_CLASS})│`);
7199
+ function resEdit(edit, warnings) {
7200
+ assertItem(edit);
7201
+ const editLines = parseText(edit.replacement_text);
7202
+ const bounds = [edit.remove_from, edit.remove_to].map((ref) => {
7203
+ const trimmed = ref.trim();
7204
+ const match = trimmed.match(ANCHOR_ROW_RE);
7205
+ if (match) {
7206
+ let message;
7207
+ if (match[1] === "+") message = `[E_BAD_REF] stripped diff-preview marker from remove_from/remove_to "${trimmed}".`;
7208
+ else if (match[1] === "-") message = `[E_BAD_REF] stripped leading "-" marker from remove_from/remove_to "${trimmed}".`;
7209
+ else message = `[E_BAD_REF] stripped "HASH│" prefix from remove_from/remove_to "${trimmed}".`;
7210
+ warnings?.push(message);
7211
+ return match[2];
7212
+ }
7213
+ return ref;
7214
+ });
7215
+ return {
7216
+ content_lines: editLines,
7217
+ hash_bounds: [parseHashRef(bounds[0]), parseHashRef(bounds[1])]
7218
+ };
7219
+ }
7220
+ function warnUnicodeEsc(edit, warnings) {
7221
+ 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.");
7222
+ }
7223
+ /** @internal — private to anchor-pipeline seam; do not import directly, use anchor-pipeline.ts */
7224
+ function stripBarePrefixes(edit, fileHashes, warnings) {
7225
+ const fileHashSet = new Set(fileHashes);
7226
+ const stripped = [];
7227
+ const contentLines = edit.content_lines.map((line, lineIndex) => {
7228
+ const match = line.match(HL_BARE_PREFIX_RE);
7229
+ if (!match) return line;
7230
+ stripped.push({
7231
+ lineIndex,
7232
+ matched: fileHashSet.has(match[1])
7233
+ });
7234
+ return line.slice(match[0].length);
7235
+ });
7236
+ if (stripped.length === 0) return edit;
7237
+ const locations = stripped.map((s) => `replacement_text line ${s.lineIndex + 1}`).join(", ");
7238
+ const matchedCount = stripped.filter((s) => s.matched).length;
7239
+ const evidence = matchedCount === 0 ? "0 matched — verify literal 'HASH│' content" : `${matchedCount}/${stripped.length} matched`;
7240
+ warnings.push(`[E_BARE_HASH_PREFIX] stripped "HASH│" prefix from ${locations} (${evidence}).`);
7241
+ return {
7242
+ ...edit,
7243
+ content_lines: contentLines
7244
+ };
7245
+ }
7246
+ /** @internal — private to anchor-pipeline seam */
7247
+ function stripDiffPrefixes(edit, warnings) {
7248
+ const stripped = [];
7249
+ const contentLines = edit.content_lines.map((line, lineIndex) => {
7250
+ const plus = line.match(HL_PREFIX_PLUS_RE);
7251
+ if (plus) {
7252
+ stripped.push(lineIndex);
7253
+ return line.slice(plus[0].length);
7254
+ }
7255
+ const minus = line.match(HL_PREFIX_MINUS_RE);
7256
+ if (minus) {
7257
+ stripped.push(lineIndex);
7258
+ return line.slice(minus[0].length);
7259
+ }
7260
+ return line;
7261
+ });
7262
+ if (stripped.length === 0) return edit;
7263
+ const locations = stripped.map((i) => `replacement_text line ${i + 1}`).join(", ");
7264
+ warnings.push(`[E_INVALID_PATCH] stripped diff-preview marker from ${locations}.`);
7265
+ return {
7266
+ ...edit,
7267
+ content_lines: contentLines
7268
+ };
7269
+ }
7270
+ /** @internal — private to anchor-pipeline seam */
7271
+ function swapReversedRanges(edit, fileHashes, warnings) {
7272
+ const lineByHash = /* @__PURE__ */ new Map();
7273
+ for (let i = 0; i < fileHashes.length; i++) lineByHash.set(fileHashes[i], i + 1);
7274
+ const [startRef, endRef] = edit.hash_bounds;
7275
+ const startLine = lineByHash.get(startRef.hash);
7276
+ const endLine = lineByHash.get(endRef.hash);
7277
+ if (startLine === void 0 || endLine === void 0 || startLine <= endLine) return edit;
7278
+ warnings.push(`[E_BAD_OP] reversed remove_from/remove_to (${startRef.hash} after ${endRef.hash}); swapped.`);
7279
+ return {
7280
+ ...edit,
7281
+ hash_bounds: [endRef, startRef]
7282
+ };
7283
+ }
7284
+ function trailingDups(contentLines, fileLines, endLine) {
7285
+ const start = lastNonEmptyIndex(contentLines);
7286
+ if (start < 0) return [];
7287
+ const dups = [];
7288
+ const maxK = Math.min(start + 1, fileLines.length - endLine);
7289
+ for (let k = 0; k < maxK; k++) {
7290
+ if (contentLines[start - k] !== fileLines[endLine + k]) break;
7291
+ dups.push({
7292
+ kind: "trailing",
7293
+ replacementLineIndex: start - k
7294
+ });
7261
7295
  }
7262
- throw lastError;
7296
+ return dups;
7263
7297
  }
7264
- function openDbWithBusyRetry(storePath) {
7265
- return withBusyRetry(() => openDb(storePath));
7298
+ function leadingDups(contentLines, fileLines, startLine) {
7299
+ const start = firstNonEmptyIndex(contentLines);
7300
+ if (start < 0) return [];
7301
+ const dups = [];
7302
+ const maxK = Math.min(contentLines.length - start, startLine - 1);
7303
+ for (let k = 0; k < maxK; k++) {
7304
+ if (contentLines[start + k] !== fileLines[startLine - 2 - k]) break;
7305
+ dups.push({
7306
+ kind: "leading",
7307
+ replacementLineIndex: start + k
7308
+ });
7309
+ }
7310
+ return dups;
7266
7311
  }
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;
7312
+ function sectionIsUnique(canonLines, start, length) {
7313
+ let count = 0;
7314
+ for (let i = 0; i + length <= canonLines.length; i++) {
7315
+ let k = 0;
7316
+ while (k < length && canonLines[i + k] === canonLines[start + k]) k++;
7317
+ if (k < length) continue;
7318
+ count++;
7319
+ if (count > 1) return false;
7280
7320
  }
7321
+ return true;
7281
7322
  }
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");
7323
+ function firstNewAfterDups(contentLines, rangeLines, canonLines, endLine) {
7324
+ const firstNew = findNewEdge(contentLines, rangeLines, false);
7325
+ if (!firstNew) return [];
7326
+ const maxK = Math.min(contentLines.length - firstNew.index, canonLines.length - endLine);
7327
+ let runLen = 0;
7328
+ while (runLen < maxK && canon(contentLines[firstNew.index + runLen]) === canonLines[endLine + runLen]) runLen++;
7329
+ if (runLen === 0 || !sectionIsUnique(canonLines, endLine, runLen)) return [];
7330
+ const dups = [];
7331
+ for (let k = 0; k < runLen; k++) dups.push({
7332
+ kind: "first-new-after",
7333
+ replacementLineIndex: firstNew.index + k
7334
+ });
7335
+ return dups;
7336
+ }
7337
+ function lastNewBeforeDups(contentLines, rangeLines, canonLines, startLine) {
7338
+ const lastNew = findNewEdge(contentLines, rangeLines, true);
7339
+ if (!lastNew) return [];
7340
+ const maxK = Math.min(lastNew.index + 1, startLine - 1);
7341
+ let runLen = 0;
7342
+ while (runLen < maxK && canon(contentLines[lastNew.index - runLen]) === canonLines[startLine - 2 - runLen]) runLen++;
7343
+ if (runLen === 0) return [];
7344
+ if (!sectionIsUnique(canonLines, startLine - 1 - runLen, runLen)) return [];
7345
+ const dups = [];
7346
+ for (let k = 0; k < runLen; k++) dups.push({
7347
+ kind: "last-new-before",
7348
+ replacementLineIndex: lastNew.index - k
7349
+ });
7350
+ return dups;
7351
+ }
7352
+ function canonCounts(lines) {
7353
+ const counts = /* @__PURE__ */ new Map();
7354
+ for (const line of lines) {
7355
+ const key = canon(line);
7356
+ counts.set(key, (counts.get(key) ?? 0) + 1);
7293
7357
  }
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
- }
7358
+ return counts;
7359
+ }
7360
+ /** @internal — private to anchor-pipeline seam */
7361
+ function findNewEdge(contentLines, rangeLines, fromEnd) {
7362
+ const multiset = canonCounts(rangeLines);
7363
+ const step = fromEnd ? -1 : 1;
7364
+ const start = fromEnd ? contentLines.length - 1 : 0;
7365
+ for (let i = start; i >= 0 && i < contentLines.length; i += step) {
7366
+ const line = contentLines[i];
7367
+ if (line.length === 0) continue;
7368
+ const key = canon(line);
7369
+ const count = multiset.get(key) ?? 0;
7370
+ if (count > 0) multiset.set(key, count - 1);
7371
+ else return {
7372
+ index: i,
7373
+ line
7374
+ };
7375
+ }
7376
+ }
7377
+ /** @internal — private to anchor-pipeline seam: detection + boundaryDups belongs to AnchorPipeline ordering */
7378
+ function valEdit(edit, fileLines, fileHashes, warnings, signal) {
7379
+ assertAligned(fileLines, fileHashes, "valEdit");
7380
+ const mismatches = [];
7381
+ const boundaryDups = [];
7382
+ const hashIndex = /* @__PURE__ */ new Map();
7383
+ for (let i = 0; i < fileHashes.length; i++) {
7384
+ const h = fileHashes[i];
7385
+ const list = hashIndex.get(h) ?? [];
7386
+ list.push(i + 1);
7387
+ hashIndex.set(h, list);
7388
+ }
7389
+ const tryResolve = (ref) => {
7390
+ const result = resAnchorFromMap(ref, hashIndex);
7391
+ if ("kind" in result) {
7392
+ mismatches.push(result);
7393
+ return;
7377
7394
  }
7395
+ return result;
7378
7396
  };
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
- });
7397
+ abortIf(signal);
7398
+ const startResolved = tryResolve(edit.hash_bounds[0]);
7399
+ const endResolved = tryResolve(edit.hash_bounds[1]);
7400
+ if (!startResolved || !endResolved) {
7401
+ if (!startResolved && endResolved) {
7402
+ const startMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[0]);
7403
+ if (startMismatch && startMismatch.kind === "not_found") startMismatch.context = endResolved;
7404
+ } else if (startResolved && !endResolved) {
7405
+ const endMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[1]);
7406
+ if (endMismatch && endMismatch.kind === "not_found") endMismatch.context = startResolved;
7505
7407
  }
7408
+ return {
7409
+ resolved: void 0,
7410
+ mismatches,
7411
+ boundaryDups
7412
+ };
7413
+ }
7414
+ 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}).`);
7415
+ const endLine = endResolved.line;
7416
+ const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
7417
+ const canonLines = fileLines.map((line) => canon(line));
7418
+ 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));
7419
+ return {
7420
+ resolved: {
7421
+ content_lines: edit.content_lines,
7422
+ hash_bounds: [startResolved, endResolved]
7423
+ },
7424
+ mismatches,
7425
+ boundaryDups
7506
7426
  };
7507
7427
  }
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;
7428
+ var ServedRejectionError = class extends Error {
7429
+ code;
7430
+ firstOffendingLine;
7431
+ servedRows;
7432
+ constructor(opts) {
7433
+ super(opts.message);
7434
+ this.name = "ServedRejectionError";
7435
+ this.code = opts.code;
7436
+ this.firstOffendingLine = opts.firstOffendingLine;
7437
+ this.servedRows = opts.servedRows;
7514
7438
  }
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);
7439
+ };
7440
+ var AnchorMismatchError = class extends Error {
7441
+ servedRows;
7442
+ constructor(message, servedRows) {
7443
+ super(message);
7444
+ this.name = "AnchorMismatchError";
7445
+ this.servedRows = servedRows;
7526
7446
  }
7447
+ };
7448
+ function buildRangeEcho(startLine, endLine, fileHashes) {
7449
+ const total = endLine - startLine + 1;
7450
+ const shown = Math.min(total, SERVED_ECHO_CAP);
7451
+ const rows = [];
7452
+ for (let ln = startLine; ln < startLine + shown; ln++) rows.push({
7453
+ position: ln - 1,
7454
+ hash: fileHashes[ln - 1]
7455
+ });
7456
+ return rows;
7527
7457
  }
7528
- function shutdownDb(db) {
7529
- try {
7530
- db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
7531
- } catch {}
7532
- db.close();
7458
+ function fmtServedRows(rows, fileLines) {
7459
+ return rows.map((row) => `${row.hash}${HASH_SEP}${fileLines[row.position] ?? ""}`).join("\n");
7533
7460
  }
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;
7461
+ function retryHint() {
7462
+ return "Read the file once to refresh anchors, then resubmit the whole batch.";
7550
7463
  }
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);
7464
+ function paginationHint(nextOffset, more) {
7465
+ return `[... ${more} more — read offset=${nextOffset}]`;
7466
+ }
7467
+ function verifyServedRange(args) {
7468
+ const { served, startHash, endHash, startLine, endLine, fileHashes, fileLines, filePath } = args;
7469
+ const where = filePath ? ` in ${filePath}` : "";
7470
+ const echoRows = buildRangeEcho(startLine, endLine, fileHashes);
7471
+ const totalLen = endLine - startLine + 1;
7472
+ const tail = echoRows.length < totalLen ? `\n${paginationHint(startLine + echoRows.length, totalLen - echoRows.length)}` : "";
7473
+ const echo = fmtServedRows(echoRows, fileLines) + tail;
7474
+ const startPositions = servedPositionsOf(served, startHash);
7475
+ const endPositions = servedPositionsOf(served, endHash);
7476
+ if (startPositions.length === 0 && endPositions.length === 0) return;
7477
+ const currentLen = endLine - startLine + 1;
7478
+ let from;
7479
+ let to;
7480
+ if (startPositions.length === 1 && endPositions.length === 1) {
7481
+ from = Math.min(startPositions[0], endPositions[0]);
7482
+ to = Math.max(startPositions[0], endPositions[0]);
7483
+ } else {
7484
+ const candidates = [];
7485
+ for (const s of startPositions) for (const e of endPositions) {
7486
+ const candFrom = Math.min(s, e);
7487
+ const candTo = Math.max(s, e);
7488
+ if (candTo - candFrom + 1 !== currentLen) continue;
7489
+ let ok = true;
7490
+ for (let k = 0; k < currentLen; k++) if (served[candFrom + k] !== fileHashes[startLine - 1 + k]) {
7491
+ ok = false;
7492
+ break;
7493
+ }
7494
+ if (ok) candidates.push({
7495
+ from: candFrom,
7496
+ to: candTo
7497
+ });
7498
+ }
7499
+ if (candidates.length === 1) {
7500
+ from = candidates[0].from;
7501
+ to = candidates[0].to;
7502
+ } else if (candidates.length > 1) {
7503
+ candidates.sort((a, b) => Math.abs(a.from - (startLine - 1)) - Math.abs(b.from - (startLine - 1)));
7504
+ from = candidates[0].from;
7505
+ to = candidates[0].to;
7506
+ }
7564
7507
  }
7565
- if (!isHealthy(opened.db)) {
7566
- shutdownDb(opened.db);
7567
- await quarantineStore(storePath);
7568
- existed = false;
7569
- opened = openDbWithBusyRetry(storePath);
7508
+ if (from === void 0 || to === void 0) {
7509
+ const problems = [];
7510
+ if (startPositions.length === 0) problems.push(`remove_from "${startHash}" has no served position`);
7511
+ else if (startPositions.length > 1) problems.push(`remove_from "${startHash}" was served at ${startPositions.length} positions`);
7512
+ if (endPositions.length === 0) problems.push(`remove_to "${endHash}" has no served position`);
7513
+ else if (endPositions.length > 1) problems.push(`remove_to "${endHash}" was served at ${endPositions.length} positions`);
7514
+ throw new ServedRejectionError({
7515
+ code: "E_RANGE_UNVERIFIED",
7516
+ message: `[E_RANGE_UNVERIFIED] cannot verify range against served state${where}: ${problems.join("; ")}. Current range:
7517
+ ${echo}
7518
+ ${retryHint()}`,
7519
+ servedRows: echoRows
7520
+ });
7570
7521
  }
7571
- const { db, stmts } = opened;
7572
- if (!existed) await migrateLegacy(db, storePath);
7573
- withBusyRetry(() => {
7574
- stmts.servedPruneOlderThan(Date.now() - SERVED_TTL_MS);
7522
+ for (let i = from; i <= to; i++) if (served[i] === null) throw new ServedRejectionError({
7523
+ code: "E_RANGE_UNSERVED",
7524
+ message: `[E_RANGE_UNSERVED] line ${i + 1}${where} was never served.\nCurrent range:\n${echo}\n${retryHint()}`,
7525
+ firstOffendingLine: i + 1,
7526
+ servedRows: echoRows
7575
7527
  });
7576
- const store = makeDomainStore(stmts);
7577
- stores.set(storePath, {
7578
- path: storePath,
7579
- db,
7580
- stmts,
7581
- store
7528
+ const servedLen = to - from + 1;
7529
+ if (servedLen !== currentLen) throw new ServedRejectionError({
7530
+ code: "E_RANGE_STALE",
7531
+ message: `[E_RANGE_STALE] served span (${servedLen} lines) no longer matches current range (${currentLen} lines)${where}.\nCurrent range:\n${echo}\n${retryHint()}`,
7532
+ firstOffendingLine: startLine,
7533
+ servedRows: echoRows
7582
7534
  });
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);
7535
+ for (let k = 0; k < servedLen; k++) if (served[from + k] !== fileHashes[startLine - 1 + k]) {
7536
+ const offendingLine = startLine + k;
7537
+ throw new ServedRejectionError({
7538
+ code: "E_RANGE_STALE",
7539
+ 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()}`,
7540
+ firstOffendingLine: offendingLine,
7541
+ servedRows: echoRows
7589
7542
  });
7590
7543
  }
7591
- return store;
7592
7544
  }
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());
7545
+ async function recordEchoServes(path$1, rows, policy, lineCount) {
7546
+ if (policy !== "live") return;
7547
+ await recordServed(path$1, rows, lineCount);
7596
7548
  }
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;
7549
+ function buildIdx(content) {
7550
+ const fileLines = splitLines(content);
7551
+ const lineStarts = [];
7552
+ let offset = 0;
7553
+ for (let index = 0; index < fileLines.length; index++) {
7554
+ lineStarts.push(offset);
7555
+ offset += fileLines[index].length;
7556
+ if (index < fileLines.length - 1) offset += 1;
7557
+ }
7558
+ return {
7559
+ fileLines,
7560
+ lineStarts
7561
+ };
7614
7562
  }
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;
7563
+ function assertNotEmpty(originalContent, result) {
7564
+ 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
7565
  }
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();
7566
+ function resToSpan(edit, content, lineIndex) {
7567
+ const { fileLines, lineStarts } = lineIndex;
7568
+ const startLine = edit.hash_bounds[0].line;
7569
+ const endLine = edit.hash_bounds[1].line;
7570
+ const originalLines = fileLines.slice(startLine - 1, endLine);
7571
+ if (originalLines.length === edit.content_lines.length && originalLines.every((line, lineIndex$1) => line === edit.content_lines[lineIndex$1])) return {
7572
+ kind: "noop",
7573
+ loc: edit.hash_bounds[0].hash,
7574
+ currentContent: originalLines.join("\n")
7575
+ };
7576
+ if (edit.content_lines.length > 0) return {
7577
+ kind: "replace",
7578
+ start: lineStarts[startLine - 1],
7579
+ end: lineStarts[endLine - 1] + fileLines[endLine - 1].length,
7580
+ replacement: edit.content_lines.join("\n")
7581
+ };
7582
+ if (startLine === 1 && endLine === fileLines.length) return {
7583
+ kind: "replace",
7584
+ start: 0,
7585
+ end: content.length,
7586
+ replacement: ""
7587
+ };
7588
+ if (endLine < fileLines.length) return {
7589
+ kind: "replace",
7590
+ start: lineStarts[startLine - 1],
7591
+ end: lineStarts[endLine],
7592
+ replacement: ""
7593
+ };
7594
+ if (content.endsWith("\n")) return {
7595
+ kind: "replace",
7596
+ start: lineStarts[startLine - 1],
7597
+ end: content.length,
7598
+ replacement: ""
7599
+ };
7600
+ const prevLine = startLine >= 2 ? fileLines[startLine - 2] : void 0;
7601
+ return {
7602
+ kind: "replace",
7603
+ start: prevLine !== void 0 && prevLine.length === 0 ? lineStarts[startLine - 1] : Math.max(0, lineStarts[startLine - 1] - 1),
7604
+ end: content.length,
7605
+ replacement: ""
7606
+ };
7625
7607
  }
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();
7608
+ function assemble(content, span, signal) {
7609
+ abortIf(signal);
7610
+ return content.slice(0, span.start) + span.replacement + content.slice(span.end);
7646
7611
  }
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;
7612
+ function applyEdit(content, edit, signal, precomputedHashes, filePath, served) {
7613
+ abortIf(signal);
7614
+ const lineIndex = buildIdx(content);
7615
+ const fileHashes = precomputedHashes ?? lineHashesPure(content);
7616
+ const warnings = [];
7617
+ const prefixFixed = stripDiffPrefixes(stripBarePrefixes(swapReversedRanges(edit, fileHashes, warnings), fileHashes, warnings), warnings);
7618
+ const { resolved: initialResolved, mismatches, boundaryDups } = valEdit(prefixFixed, lineIndex.fileLines, fileHashes, warnings, signal);
7619
+ if (mismatches.length || !initialResolved) {
7620
+ const { message, servedRows } = fmtMismatchWithServes(mismatches, lineIndex.fileLines, fileHashes, filePath);
7621
+ throw new AnchorMismatchError(message, servedRows);
7663
7622
  }
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;
7623
+ warnUnicodeEsc(prefixFixed, warnings);
7624
+ let resolved = initialResolved;
7625
+ let autoFixes;
7626
+ if (boundaryDups.length > 0) {
7627
+ autoFixes = [];
7628
+ const correctedEdit = {
7629
+ ...prefixFixed,
7630
+ content_lines: [...prefixFixed.content_lines]
7631
+ };
7632
+ const seen = /* @__PURE__ */ new Set();
7633
+ const uniqueDups = [];
7634
+ for (const dup of boundaryDups) {
7635
+ if (seen.has(dup.replacementLineIndex)) continue;
7636
+ seen.add(dup.replacementLineIndex);
7637
+ uniqueDups.push(dup);
7672
7638
  }
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;
7639
+ const dupsByIndex = uniqueDups.sort((a, b) => b.replacementLineIndex - a.replacementLineIndex);
7640
+ for (const dup of dupsByIndex) {
7641
+ const idx = dup.replacementLineIndex;
7642
+ if (idx < 0 || idx >= correctedEdit.content_lines.length) continue;
7643
+ const removed = correctedEdit.content_lines.splice(idx, 1)[0];
7644
+ autoFixes.push({
7645
+ kind: dup.kind,
7646
+ removedLine: removed,
7647
+ removedLineIndex: idx
7648
+ });
7649
+ }
7650
+ const correctedResult = valEdit(correctedEdit, lineIndex.fileLines, fileHashes, warnings, signal);
7651
+ if (correctedResult.mismatches.length || !correctedResult.resolved) {
7652
+ const { message, servedRows } = fmtMismatchWithServes(correctedResult.mismatches, lineIndex.fileLines, fileHashes, filePath);
7653
+ throw new AnchorMismatchError(message, servedRows);
7690
7654
  }
7655
+ resolved = correctedResult.resolved;
7691
7656
  }
7692
- try {
7693
- await rename(legacyPath, `${legacyPath}.bak`);
7694
- } catch (error) {
7695
- console.error("Failed to rename legacy hash store after migration:", error);
7657
+ if (served) {
7658
+ const startAnchor = resolved.hash_bounds[0];
7659
+ const endAnchor = resolved.hash_bounds[1];
7660
+ verifyServedRange({
7661
+ served,
7662
+ startHash: startAnchor.hash,
7663
+ endHash: endAnchor.hash,
7664
+ startLine: startAnchor.line,
7665
+ endLine: endAnchor.line,
7666
+ fileHashes,
7667
+ fileLines: lineIndex.fileLines,
7668
+ filePath
7669
+ });
7696
7670
  }
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();
7671
+ const spanResult = resToSpan(resolved, content, lineIndex);
7672
+ if (spanResult.kind === "noop") return {
7673
+ content,
7674
+ firstChangedLine: void 0,
7675
+ lastChangedLine: void 0,
7676
+ range: resolvedRange(resolved),
7677
+ ...warnings.length ? { warnings } : {},
7678
+ noopEdit: {
7679
+ loc: spanResult.loc,
7680
+ currentContent: spanResult.currentContent
7716
7681
  }
7717
7682
  };
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
- }
7683
+ const result = assemble(content, spanResult, signal);
7684
+ assertNotEmpty(content, result);
7685
+ const changed = changedRange(content, result);
7686
+ return {
7687
+ content: result,
7688
+ firstChangedLine: changed?.firstChangedLine,
7689
+ lastChangedLine: changed?.lastChangedLine,
7690
+ range: resolvedRange(resolved),
7691
+ ...warnings.length ? { warnings } : {},
7692
+ ...autoFixes ? { autoFixes } : {}
7725
7693
  };
7726
7694
  }
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);
7695
+ function resolvedRange(resolved) {
7696
+ const [start, end] = resolved.hash_bounds;
7697
+ return {
7698
+ startLine: start.line,
7699
+ endLine: end.line,
7700
+ startHash: start.hash,
7701
+ endHash: end.hash,
7702
+ delta: resolved.content_lines.length - (Math.abs(end.line - start.line) + 1)
7703
+ };
7704
+ }
7705
+ function fmtRegion(hashes, lines) {
7706
+ if (hashes.length !== lines.length) throw new Error(`fmtRegion: hashes.length (${hashes.length}) must match lines.length (${lines.length}).`);
7707
+ return lines.map((line, index) => `${hashes[index]}${HASH_SEP}${line}`).join("\n");
7708
+ }
7709
+ function changedRange(original, result) {
7710
+ if (original === result) return null;
7711
+ if (original.length === 0) return {
7712
+ firstChangedLine: 1,
7713
+ lastChangedLine: splitLines(result).length
7714
+ };
7715
+ const originalLines = splitLines(original);
7716
+ const resultLines = splitLines(result);
7717
+ if (originalLines.length === resultLines.length && originalLines.every((line, index) => line === resultLines[index])) return null;
7718
+ const minLen = Math.min(originalLines.length, resultLines.length);
7719
+ let first = 0;
7720
+ while (first < minLen && originalLines[first] === resultLines[first]) first++;
7721
+ let lastOrig = originalLines.length - 1;
7722
+ let lastRes = resultLines.length - 1;
7723
+ while (lastOrig >= first && lastRes >= first && originalLines[lastOrig] === resultLines[lastRes]) {
7724
+ lastOrig--;
7725
+ lastRes--;
7752
7726
  }
7753
- return newHashes;
7727
+ return {
7728
+ firstChangedLine: first + 1,
7729
+ lastChangedLine: Math.max(first, lastRes) + 1
7730
+ };
7754
7731
  }
7755
7732
 
7756
7733
  //#endregion
@@ -8182,15 +8159,15 @@ const UTF8_REWRITE_NOTE = "[Non-UTF-8 bytes shown as U+FFFD; editing rewrites th
8182
8159
  * context.
8183
8160
  */
8184
8161
  async function readAndServe(io, rawPath, cwd, options) {
8185
- const { sessionKey, signal } = options;
8162
+ const { signal } = options;
8186
8163
  abortIf(signal);
8187
8164
  const view = await readView(io, rawPath, cwd, {
8188
8165
  offset: options.offset,
8189
8166
  limit: options.limit,
8190
8167
  signal
8191
8168
  });
8192
- if (view.served.length > 0) await recordServed(sessionKey, view.absolutePath, view.served, view.hashes.length);
8193
- await clearDriftReported(sessionKey, view.absolutePath);
8169
+ if (view.served.length > 0) await recordServed(view.absolutePath, view.served, view.hashes.length);
8170
+ await clearDriftReported(view.absolutePath);
8194
8171
  return {
8195
8172
  text: view.hadUtf8DecodeErrors ? `${view.text}\n\n${UTF8_REWRITE_NOTE}` : view.text,
8196
8173
  served: view.served,
@@ -8254,10 +8231,8 @@ function createReadTool(deps) {
8254
8231
  }
8255
8232
  return withWorkspace(execCwd(exec), async () => {
8256
8233
  const cwd = execCwd(exec);
8257
- const sessionKey = execSessionKey(exec);
8258
8234
  const signal = exec.signal;
8259
8235
  const { text, absolutePath } = await readAndServe(io, rawPath, cwd, {
8260
- sessionKey,
8261
8236
  signal,
8262
8237
  offset: canonical.offset,
8263
8238
  limit: canonical.limit
@@ -8684,12 +8659,12 @@ async function applyOne(input, onReject) {
8684
8659
  * with no change. Messages are byte-identical to the pre-engine tools.
8685
8660
  */
8686
8661
  async function enforceNoopLoop(opts) {
8687
- const { absolutePath, removeFrom, removeTo, displayPath, index, count, sessionKey, originalHashes } = opts;
8662
+ const { absolutePath, removeFrom, removeTo, displayPath, index, count, originalHashes } = opts;
8688
8663
  if (index === void 0) {
8689
8664
  if (count >= NOOP_LOOP_THRESHOLD) {
8690
8665
  const echoRows = buildRangeEcho(opts.range.startLine, opts.range.endLine, originalHashes);
8691
8666
  const echo = fmtServedRows(echoRows, splitLines(opts.originalNormalized));
8692
- await recordEchoServes(sessionKey, absolutePath, echoRows, "live", originalHashes.length);
8667
+ await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
8693
8668
  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
8669
  }
8695
8670
  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 +8673,7 @@ async function enforceNoopLoop(opts) {
8698
8673
  if (count >= NOOP_LOOP_THRESHOLD) {
8699
8674
  const originalLines = splitLines(opts.originalNormalized);
8700
8675
  const echoRows = opts.echoRows;
8701
- if (echoRows) await recordEchoServes(sessionKey, absolutePath, echoRows, "live", originalHashes.length);
8676
+ if (echoRows) await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
8702
8677
  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
8678
  }
8704
8679
  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 +8703,7 @@ async function runFileEdits(io, items, opts) {
8728
8703
  signal: opts.signal,
8729
8704
  maxLines: MAX_HASH_LINES
8730
8705
  });
8731
- const served = await loadServed(opts.sessionKey, absolutePath);
8706
+ const served = await loadServed(absolutePath);
8732
8707
  const warnings = [];
8733
8708
  let currentContent = originalNormalized;
8734
8709
  let currentHashes = originalHashes;
@@ -8760,7 +8735,7 @@ async function runFileEdits(io, items, opts) {
8760
8735
  if (error instanceof AnchorMismatchError || error instanceof ServedRejectionError) {
8761
8736
  const originalLines = splitLines(originalNormalized);
8762
8737
  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);
8738
+ if (echoRows) await recordEchoServes(absolutePath, echoRows, "live", originalHashes.length);
8764
8739
  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
8740
  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
8741
  }
@@ -8787,7 +8762,6 @@ async function runFileEdits(io, items, opts) {
8787
8762
  displayPath: item.path,
8788
8763
  index: item.index,
8789
8764
  count,
8790
- sessionKey: opts.sessionKey,
8791
8765
  originalHashes,
8792
8766
  originalNormalized,
8793
8767
  echoRows: echoRowsForItem(applied.edit, originalHashes)
@@ -8826,7 +8800,6 @@ async function runFileEdits(io, items, opts) {
8826
8800
  const originalLines = splitLines(originalNormalized);
8827
8801
  try {
8828
8802
  driftNotice = await scanDrift({
8829
- sessionKey: opts.sessionKey,
8830
8803
  served,
8831
8804
  resultHashes,
8832
8805
  resultLines,
@@ -9036,11 +9009,8 @@ function buildBatchResult(sections) {
9036
9009
  * across every tool — it concentrates (deep).
9037
9010
  */
9038
9011
  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
- });
9012
+ const { io, items, signal, exec, sandbox, sandboxPolicy } = opts;
9013
+ const fileResult = await applySequence(io, items, { signal });
9044
9014
  const toSection = () => ({
9045
9015
  path: fileResult.displayPath,
9046
9016
  originalNormalized: fileResult.originalNormalized,
@@ -9057,7 +9027,7 @@ async function execute(opts) {
9057
9027
  const recordIfNeeded = async (built$1) => {
9058
9028
  if (built$1.details.servedRows && built$1.details.servedRows.length > 0) {
9059
9029
  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);
9030
+ if (entry) await recordServedTruncated(fileResult.absolutePath, entry.servedRows, splitLines(fileResult.result).length, fileResult.range.startLine - 1);
9061
9031
  }
9062
9032
  };
9063
9033
  if (items.length === 1 && fileResult.appliedCount + fileResult.noopCount === 1) {
@@ -9214,7 +9184,6 @@ function buildEditTool(io, sandbox) {
9214
9184
  async execute(args, exec) {
9215
9185
  return withWorkspace(execCwd(exec), async () => {
9216
9186
  const cwd = execCwd(exec);
9217
- const sessionKey = execSessionKey(exec);
9218
9187
  const signal = exec.signal;
9219
9188
  const canonical = normalizeRequest(args);
9220
9189
  assertEditRequest(canonical);
@@ -9251,7 +9220,6 @@ function buildEditTool(io, sandbox) {
9251
9220
  return execute({
9252
9221
  io,
9253
9222
  items,
9254
- sessionKey,
9255
9223
  signal,
9256
9224
  exec,
9257
9225
  sandbox,
@@ -9296,7 +9264,6 @@ function buildUndoTool(io, sandbox) {
9296
9264
  async execute(args, exec) {
9297
9265
  return withWorkspace(execCwd(exec), async () => {
9298
9266
  const cwd = execCwd(exec);
9299
- const sessionKey = execSessionKey(exec);
9300
9267
  const signal = exec.signal;
9301
9268
  const canonical = normalizeRequest(args);
9302
9269
  assertUndoRequest(canonical);
@@ -9347,7 +9314,7 @@ function buildUndoTool(io, sandbox) {
9347
9314
  const parts = [`Undone last edit on ${path$1}.`];
9348
9315
  if (linesAddedByEdit > 0 || linesRemovedByEdit > 0) parts.push(`Removed ${linesAddedByEdit} line(s) that were added and restored ${linesRemovedByEdit} line(s) that were removed.`);
9349
9316
  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);
9317
+ if (undoDenseRows.length > 0) await recordServedTruncated(absolutePath, undoDenseRows, splitLines(undo.content).length, restoredRange?.firstChangedLine ?? undoDiffResult.firstChangedLine ?? 0);
9351
9318
  return [
9352
9319
  parts.join("\n"),
9353
9320
  "",
@@ -9366,51 +9333,6 @@ function registerUndoTool(_rootCtx, agentCtx, io, sandbox) {
9366
9333
  return agentCtx.tools.register(buildUndoTool(io, sandbox));
9367
9334
  }
9368
9335
 
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
9336
  //#endregion
9415
9337
  //#region src/url-schema/vendored/hashline/guidance/parse.js
9416
9338
  /**
@@ -9847,7 +9769,6 @@ function installAgentTools(rootCtx, agent, resolver) {
9847
9769
  const hashlineIo = ctxFsIO(rootCtx.fs, rootCtx);
9848
9770
  disposers.push(registerEditTool(rootCtx, agent.ctx, hashlineIo, hashlineSandbox));
9849
9771
  disposers.push(registerUndoTool(rootCtx, agent.ctx, hashlineIo, hashlineSandbox));
9850
- disposers.push(registerWriteHook(rootCtx, agent.ctx, hashlineIo));
9851
9772
  {
9852
9773
  const diagnosticsHook = buildLspWriteFeedback().postWrite;
9853
9774
  disposers.push(agent.ctx.on("tools/post-execute", async (exec, result, next) => {
@@ -11754,7 +11675,7 @@ function apply(ctx, config) {
11754
11675
  if (session === void 0) return "";
11755
11676
  const sandboxPolicy = runtimeCtx.get("sandboxPolicy");
11756
11677
  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." : "";
11678
+ 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
11679
  }
11759
11680
  });
11760
11681
  });