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 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.9",
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",
@@ -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;