pi-hashline-edit-pro 4.2.9 → 4.2.10
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/README.md +1 -1
- package/package.json +1 -1
- package/src/anchor-registry.ts +28 -1
package/README.md
CHANGED
|
@@ -199,7 +199,7 @@ The table is curated for tokenizers, not for humans. Every anchor is the concate
|
|
|
199
199
|
|
|
200
200
|
Each line also carries a content checksum. The line is canonicalized (carriage returns stripped, trailing whitespace trimmed) and hashed with [xxhash-wasm](https://github.com/jungomi/xxhash-wasm). The canonicalization keeps the checksum stable across editor-save cycles that add or remove trailing whitespace. A line over 500 bytes is hashed from its first 500 bytes.
|
|
201
201
|
|
|
202
|
-
Allocated anchors live in a persistent per-file snapshot (`~/.config/pi-hashline-edit-pro/hash-store.sqlite`) keyed by content checksum, so resume-after-restart and cross-session edits reuse ownership instead of minting duplicates. Each session also appends an ownership log (`allocate`/`free`/`clear` events) to a sidecar file under `~/.config/pi-hashline-edit-pro/sessions/`; the fold of that log is the session's source of truth, sidecars whose session file is gone are garbage-collected at startup, and the in-memory ownership of a session is released when that session shuts down and rebuilt from its sidecar on next use.
|
|
202
|
+
Allocated anchors live in a persistent per-file snapshot (`~/.config/pi-hashline-edit-pro/hash-store.sqlite`) keyed by content checksum, so resume-after-restart and cross-session edits reuse ownership instead of minting duplicates. Each session also appends an ownership log (`allocate`/`free`/`clear` events) to a sidecar file under `~/.config/pi-hashline-edit-pro/sessions/`; the fold of that log is the session's source of truth, sidecars whose session file is gone are garbage-collected at startup (never the sidecar of a session that is currently loaded in this process), and the in-memory ownership of a session is released when that session shuts down and rebuilt from its sidecar on next use.
|
|
203
203
|
|
|
204
204
|
When a range is edited, the mapping between old and new content is computed per span: lines whose content is unchanged keep their allocated anchors, anchors of removed lines are freed, and every genuinely new line is minted a fresh anchor. Anchors are never assigned by matching content; only positional survival across an edit preserves one.
|
|
205
205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
package/src/anchor-registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chmod, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { appendFileSync, chmodSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, chmodSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -163,6 +163,7 @@ export const SIDECAR_HEADER_BYTES = 64 * 1024;
|
|
|
163
163
|
const SIDECAR_HEADER_CHUNK = 4096;
|
|
164
164
|
let currentKey: string | undefined;
|
|
165
165
|
const sidecarByKey = new Map<string, string>();
|
|
166
|
+
const sessionFileByKey = new Map<string, string>();
|
|
166
167
|
const pendingInits = new Map<string, Promise<void>>();
|
|
167
168
|
const loadTokens = new Map<string, object>();
|
|
168
169
|
const registries = new Map<string, SessionState>();
|
|
@@ -368,6 +369,7 @@ async function ensureRegistryForKey(key: string, sessionFile: string | undefined
|
|
|
368
369
|
registries.set(key, newSessionState(key));
|
|
369
370
|
return;
|
|
370
371
|
}
|
|
372
|
+
sessionFileByKey.set(key, sessionFile);
|
|
371
373
|
await loadRegistryState(key, sessionFile, token);
|
|
372
374
|
})();
|
|
373
375
|
pendingInits.set(key, promise);
|
|
@@ -423,12 +425,27 @@ function current(): SessionState | undefined {
|
|
|
423
425
|
return registries.get(key);
|
|
424
426
|
}
|
|
425
427
|
|
|
428
|
+
function sidecarNeedsSessionRecord(sidecar: string): boolean {
|
|
429
|
+
try {
|
|
430
|
+
return statSync(sidecar).size === 0;
|
|
431
|
+
} catch {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function ensureSidecarSessionRecord(key: string, sidecar: string): void {
|
|
437
|
+
const sessionFile = sessionFileByKey.get(key);
|
|
438
|
+
if (sessionFile === undefined || !sidecarNeedsSessionRecord(sidecar)) return;
|
|
439
|
+
appendEvent({ kind: "session", sessionFile } satisfies RegistryEvent);
|
|
440
|
+
}
|
|
441
|
+
|
|
426
442
|
function appendEvent(event: RegistryEvent): void {
|
|
427
443
|
const key = activeKey();
|
|
428
444
|
if (!key) return;
|
|
429
445
|
const sidecar = sidecarByKey.get(key);
|
|
430
446
|
if (!sidecar) return;
|
|
431
447
|
try {
|
|
448
|
+
if (event.kind !== "session") ensureSidecarSessionRecord(key, sidecar);
|
|
432
449
|
appendFileSync(sidecar, JSON.stringify(event) + "\n", "utf-8");
|
|
433
450
|
if (process.platform !== "win32") {
|
|
434
451
|
try { chmodSync(sidecar, 0o600); } catch (error) { if (errCode(error) !== "ENOENT") console.error("Failed to secure anchor registry sidecar:", error); }
|
|
@@ -880,6 +897,7 @@ export function adoptAnchors(path: string, entries: Map<string, string>): void {
|
|
|
880
897
|
export function resetRegistryForTests(): void {
|
|
881
898
|
currentKey = undefined;
|
|
882
899
|
sidecarByKey.clear();
|
|
900
|
+
sessionFileByKey.clear();
|
|
883
901
|
pendingInits.clear();
|
|
884
902
|
loadTokens.clear();
|
|
885
903
|
registries.clear();
|
|
@@ -924,9 +942,17 @@ export function releaseRegistrySession(key: string): void {
|
|
|
924
942
|
if (currentKey === key) currentKey = undefined;
|
|
925
943
|
pendingInits.delete(key);
|
|
926
944
|
sidecarByKey.delete(key);
|
|
945
|
+
sessionFileByKey.delete(key);
|
|
927
946
|
registries.delete(key);
|
|
928
947
|
}
|
|
929
948
|
|
|
949
|
+
function isClaimedSidecar(sidecar: string): boolean {
|
|
950
|
+
for (const claimed of sidecarByKey.values()) {
|
|
951
|
+
if (claimed === sidecar) return true;
|
|
952
|
+
}
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
|
|
930
956
|
export async function gcRegistrySidecars(): Promise<void> {
|
|
931
957
|
let names: string[];
|
|
932
958
|
try {
|
|
@@ -948,6 +974,7 @@ export async function gcRegistrySidecars(): Promise<void> {
|
|
|
948
974
|
}
|
|
949
975
|
if (!name.endsWith(SIDECAR_SUFFIX)) continue;
|
|
950
976
|
const sidecar = join(sessionClaimsDir(), name);
|
|
977
|
+
if (isClaimedSidecar(sidecar)) continue;
|
|
951
978
|
try {
|
|
952
979
|
const sessionFile = await readSidecarSessionFile(sidecar);
|
|
953
980
|
if (sessionFile === undefined) continue;
|