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