pi-hashline-edit-pro 4.2.4 → 4.2.5
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 +4 -4
- package/src/config.ts +3 -3
package/README.md
CHANGED
|
@@ -192,7 +192,7 @@ On non-Windows platforms the directory honors `XDG_CONFIG_HOME` when set (fallin
|
|
|
192
192
|
|
|
193
193
|
## How anchors work
|
|
194
194
|
|
|
195
|
-
Anchors are allocated, never derived. Every line that is served to you, by `read`, `anchor_grep`, the auto-read block after `write`, or a post-edit diff, gets the next free anchor from the session's pool, claimed by walking the table with a large stride (roughly the golden ratio of the anchor space) coprime to it, so consecutively minted anchors land in unrelated regions of the table instead of sharing leading characters. Ownership is exclusive: an anchor is owned by one file's line until it is freed (the line was edited, the file was written or deleted, or you ran `/clear-anchors`). Minting prefers anchors the session has never used; when every unused anchor has been spent, freed anchors are recycled after their stale served records are purged, so an anchor is never shared by two live lines. Because ownership is exclusive, an anchor resolves to exactly one file. Two byte-identical lines never share an anchor, and that guarantee sets the file size cap: at most 1,353,139 lines per file, beyond which `read`, `replace`, and `insert` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
|
|
195
|
+
Anchors are allocated, never derived. Every line that is served to you, by `read`, `anchor_grep`, the auto-read block after `write`, or a post-edit diff, gets the next free anchor from the session's pool, claimed by walking the table with a large stride (roughly the golden ratio of the anchor space) coprime to it, so consecutively minted anchors land in unrelated regions of the table instead of sharing leading characters. Each session seeds its walk from its own offset (derived from the session key and the process id), so concurrent sessions mint disjoint sequences instead of identical ones: an anchor minted in one session is unknown in another and is rejected with `[E_STALE_ANCHOR]` rather than resolving to a different file. Ownership is exclusive: an anchor is owned by one file's line until it is freed (the line was edited, the file was written or deleted, or you ran `/clear-anchors`). Minting prefers anchors the session has never used; when every unused anchor has been spent, freed anchors are recycled after their stale served records are purged, so an anchor is never shared by two live lines. Because ownership is exclusive, an anchor resolves to exactly one file. Two byte-identical lines never share an anchor, and that guarantee sets the file size cap: at most 1,353,139 lines per file, beyond which `read`, `replace`, and `insert` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
|
|
196
196
|
|
|
197
197
|
The table is curated for tokenizers, not for humans. Every anchor is the concatenation of two 2-character pieces that each encode as a single token, and beside the `│` separator the whole 5-character `anchor│` unit is verified to tokenize as exactly three tokens in each of eight modern open-weights tokenizers (Qwen 3.5, DeepSeek V4, Gemma 4, GLM 5.3 Flash, Tencent Hy4-preview, MiniMax M3, MiMo V2.5, Kimi K3). The shipped table is the intersection that satisfies the criterion on all of them; Nemotron 3 Ultra is the one modern tokenizer excluded. An anchor therefore costs 2 tokens on a read row and 2 in an edit call, with the `│` separator as the third. Anchors are letters only. The table is shipped as `src/hashline/anchor-table.json`.
|
|
198
198
|
|
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.5",
|
|
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
|
@@ -60,8 +60,8 @@ function seedServedFromOwned(state: SessionState): void {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export function foldRegistryEvents(events: RegistryEvent[]): SessionState {
|
|
64
|
-
const state = newSessionState();
|
|
63
|
+
export function foldRegistryEvents(events: RegistryEvent[], seed?: string): SessionState {
|
|
64
|
+
const state = newSessionState(seed);
|
|
65
65
|
for (const event of events) {
|
|
66
66
|
if (event.kind === "clear") {
|
|
67
67
|
state.owned.clear();
|
|
@@ -157,7 +157,7 @@ export async function initRegistry(sessionFile: string | undefined): Promise<voi
|
|
|
157
157
|
if (!sessionFile) {
|
|
158
158
|
currentKey = `__ephemeral__-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
159
159
|
currentSidecar = undefined;
|
|
160
|
-
registries.set(currentKey, newSessionState());
|
|
160
|
+
registries.set(currentKey, newSessionState(currentKey));
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
const key = sidecarKeyFor(sessionFile);
|
|
@@ -173,7 +173,7 @@ export async function initRegistry(sessionFile: string | undefined): Promise<voi
|
|
|
173
173
|
console.error("Failed to read anchor registry sidecar:", error);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
const folded = foldRegistryEvents(events);
|
|
176
|
+
const folded = foldRegistryEvents(events, `${key}:${process.pid}`);
|
|
177
177
|
seedServedFromOwned(folded);
|
|
178
178
|
registries.set(key, folded);
|
|
179
179
|
if (rawLog.length > 0) {
|
package/src/config.ts
CHANGED
|
@@ -46,10 +46,10 @@ export function normalizeDiffContextLines(value: unknown): number {
|
|
|
46
46
|
|
|
47
47
|
function parseConfig(content: string): Config {
|
|
48
48
|
const parsed = JSON.parse(content) as unknown;
|
|
49
|
-
|
|
50
|
-
if (typeof autoRead !== "boolean") {
|
|
49
|
+
if (!isRec(parsed) || (parsed.autoRead !== undefined && typeof parsed.autoRead !== "boolean")) {
|
|
51
50
|
throw new Error("config.json must be an object with a boolean autoRead field");
|
|
52
51
|
}
|
|
52
|
+
const autoRead = parsed.autoRead;
|
|
53
53
|
const anchorGrepEnabled = isRec(parsed) ? parsed.anchorGrepEnabled : undefined;
|
|
54
54
|
const requirePath = isRec(parsed) ? parsed.requirePath : undefined;
|
|
55
55
|
const strictInput = isRec(parsed) ? parsed.strictInput : undefined;
|
|
@@ -57,7 +57,7 @@ function parseConfig(content: string): Config {
|
|
|
57
57
|
const legacyBoundaryDedup = isRec(parsed) ? parsed.boundaryDedupEnabled : undefined;
|
|
58
58
|
const diffContextLines = isRec(parsed) ? parsed.diffContextLines : undefined;
|
|
59
59
|
return {
|
|
60
|
-
autoRead,
|
|
60
|
+
autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
|
|
61
61
|
anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
|
|
62
62
|
requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
|
|
63
63
|
strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
|