pi-readcache 0.1.0 → 0.1.1
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 +11 -11
- package/package.json +1 -1
- package/src/replay.ts +156 -43
- package/src/tool.ts +23 -3
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# 🧠 pi-readcache
|
|
2
2
|
|
|
3
3
|
[](https://pi.dev/)
|
|
4
|
-
[](https://www.npmjs.com/package/pi-readcache)
|
|
5
|
+
[](LICENSE)
|
|
5
6
|
|
|
6
7
|
A pi extension that overrides the built-in `read` tool with hash-based, replay-aware caching.
|
|
7
8
|
|
|
@@ -127,15 +128,15 @@ Rules:
|
|
|
127
128
|
|
|
128
129
|
## File map
|
|
129
130
|
|
|
130
|
-
- `
|
|
131
|
-
- `
|
|
132
|
-
- `
|
|
133
|
-
- `
|
|
134
|
-
- `
|
|
135
|
-
- `
|
|
136
|
-
- `
|
|
137
|
-
- `
|
|
138
|
-
- `
|
|
131
|
+
- `index.ts` - extension entrypoint + lifecycle reset hooks
|
|
132
|
+
- `src/tool.ts` - `read` override decision engine
|
|
133
|
+
- `src/replay.ts` - replay reconstruction, trust transitions, overlay
|
|
134
|
+
- `src/meta.ts` - metadata/invalidation validators and extractors
|
|
135
|
+
- `src/commands.ts` - `/readcache-status`, `/readcache-refresh`, `readcache_refresh`
|
|
136
|
+
- `src/object-store.ts` - content-addressed storage (`.pi/readcache/objects`)
|
|
137
|
+
- `src/diff.ts` - unified diff creation + usefulness gating
|
|
138
|
+
- `src/path.ts` - path/range parsing and normalization
|
|
139
|
+
- `src/telemetry.ts` - replay window/mode/savings reporting
|
|
139
140
|
|
|
140
141
|
## Tool-override compatibility contract
|
|
141
142
|
|
|
@@ -147,7 +148,6 @@ Because this overrides built-in `read`, it must preserve:
|
|
|
147
148
|
## Development
|
|
148
149
|
|
|
149
150
|
```bash
|
|
150
|
-
cd extension
|
|
151
151
|
npm install
|
|
152
152
|
npm run typecheck
|
|
153
153
|
npm test
|
package/package.json
CHANGED
package/src/replay.ts
CHANGED
|
@@ -6,20 +6,28 @@ import type {
|
|
|
6
6
|
ReadCacheInvalidationV1,
|
|
7
7
|
ReadCacheMetaV1,
|
|
8
8
|
ScopeKey,
|
|
9
|
+
ScopeRangeKey,
|
|
9
10
|
ScopeTrust,
|
|
10
11
|
} from "./types.js";
|
|
11
12
|
|
|
12
13
|
type SessionManagerView = ExtensionContext["sessionManager"];
|
|
13
14
|
|
|
15
|
+
type RangeBlockersByPath = Map<string, Set<ScopeRangeKey>>;
|
|
16
|
+
|
|
14
17
|
const OVERLAY_SEQ_START = 1_000_000_000;
|
|
15
18
|
|
|
19
|
+
interface ReplayMemoEntry {
|
|
20
|
+
knowledge: KnowledgeMap;
|
|
21
|
+
blockedRangesByPath: RangeBlockersByPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
interface OverlayState {
|
|
17
25
|
leafId: string | null;
|
|
18
26
|
knowledge: KnowledgeMap;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export interface ReplayRuntimeState {
|
|
22
|
-
memoByLeaf: Map<string,
|
|
30
|
+
memoByLeaf: Map<string, ReplayMemoEntry>;
|
|
23
31
|
overlayBySession: Map<string, OverlayState>;
|
|
24
32
|
nextOverlaySeq: number;
|
|
25
33
|
}
|
|
@@ -41,6 +49,21 @@ function cloneKnowledgeMap(source: KnowledgeMap): KnowledgeMap {
|
|
|
41
49
|
return cloned;
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
function cloneRangeBlockersByPath(source: RangeBlockersByPath): RangeBlockersByPath {
|
|
53
|
+
const cloned: RangeBlockersByPath = new Map();
|
|
54
|
+
for (const [pathKey, scopes] of source.entries()) {
|
|
55
|
+
cloned.set(pathKey, new Set(scopes));
|
|
56
|
+
}
|
|
57
|
+
return cloned;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function cloneReplayMemoEntry(source: ReplayMemoEntry): ReplayMemoEntry {
|
|
61
|
+
return {
|
|
62
|
+
knowledge: cloneKnowledgeMap(source.knowledge),
|
|
63
|
+
blockedRangesByPath: cloneRangeBlockersByPath(source.blockedRangesByPath),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
function getMemoKey(sessionId: string, leafId: string | null, boundaryKey: string): string {
|
|
45
68
|
return `${sessionId}:${leafId ?? "null"}:${boundaryKey}`;
|
|
46
69
|
}
|
|
@@ -55,6 +78,36 @@ function ensureScopeMap(knowledge: KnowledgeMap, pathKey: string): Map<ScopeKey,
|
|
|
55
78
|
return created;
|
|
56
79
|
}
|
|
57
80
|
|
|
81
|
+
function isRangeScope(scopeKey: ScopeKey): scopeKey is ScopeRangeKey {
|
|
82
|
+
return scopeKey !== SCOPE_FULL;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ensureRangeBlockerSet(blockedRangesByPath: RangeBlockersByPath, pathKey: string): Set<ScopeRangeKey> {
|
|
86
|
+
const existing = blockedRangesByPath.get(pathKey);
|
|
87
|
+
if (existing) {
|
|
88
|
+
return existing;
|
|
89
|
+
}
|
|
90
|
+
const created = new Set<ScopeRangeKey>();
|
|
91
|
+
blockedRangesByPath.set(pathKey, created);
|
|
92
|
+
return created;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function setRangeBlocker(blockedRangesByPath: RangeBlockersByPath, pathKey: string, scopeKey: ScopeRangeKey): void {
|
|
96
|
+
const scopes = ensureRangeBlockerSet(blockedRangesByPath, pathKey);
|
|
97
|
+
scopes.add(scopeKey);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function clearRangeBlocker(blockedRangesByPath: RangeBlockersByPath, pathKey: string, scopeKey: ScopeRangeKey): void {
|
|
101
|
+
const scopes = blockedRangesByPath.get(pathKey);
|
|
102
|
+
if (!scopes) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
scopes.delete(scopeKey);
|
|
106
|
+
if (scopes.size === 0) {
|
|
107
|
+
blockedRangesByPath.delete(pathKey);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
58
111
|
export function getTrust(knowledge: KnowledgeMap, pathKey: string, scopeKey: ScopeKey): ScopeTrust | undefined {
|
|
59
112
|
return knowledge.get(pathKey)?.get(scopeKey);
|
|
60
113
|
}
|
|
@@ -95,6 +148,61 @@ function leafHasChildren(sessionManager: SessionManagerView, leafId: string | nu
|
|
|
95
148
|
return sessionManager.getEntries().some((entry) => entry.parentId === leafId);
|
|
96
149
|
}
|
|
97
150
|
|
|
151
|
+
function replaySnapshotFromBranch(branchEntries: SessionEntry[], startIndex: number): ReplayMemoEntry {
|
|
152
|
+
const knowledge: KnowledgeMap = new Map();
|
|
153
|
+
const blockedRangesByPath: RangeBlockersByPath = new Map();
|
|
154
|
+
const normalizedStart = Math.max(0, Math.min(startIndex, branchEntries.length));
|
|
155
|
+
let seq = 0;
|
|
156
|
+
|
|
157
|
+
for (let index = normalizedStart; index < branchEntries.length; index += 1) {
|
|
158
|
+
const entry = branchEntries[index];
|
|
159
|
+
if (!entry) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const meta = extractReadMetaFromSessionEntry(entry);
|
|
164
|
+
if (meta) {
|
|
165
|
+
seq += 1;
|
|
166
|
+
applyReadMetaTransition(knowledge, meta, seq, blockedRangesByPath);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const invalidation = extractInvalidationFromSessionEntry(entry);
|
|
171
|
+
if (invalidation) {
|
|
172
|
+
applyInvalidation(knowledge, invalidation, blockedRangesByPath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
knowledge,
|
|
178
|
+
blockedRangesByPath,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getReplayMemoEntryForLeaf(
|
|
183
|
+
sessionManager: SessionManagerView,
|
|
184
|
+
runtimeState: ReplayRuntimeState,
|
|
185
|
+
): { memoEntry: ReplayMemoEntry; sessionId: string; leafId: string | null } {
|
|
186
|
+
const sessionId = sessionManager.getSessionId();
|
|
187
|
+
const leafId = sessionManager.getLeafId();
|
|
188
|
+
const branchEntries = sessionManager.getBranch();
|
|
189
|
+
const boundary = findReplayStartIndex(branchEntries);
|
|
190
|
+
const memoKey = getMemoKey(sessionId, leafId, boundary.boundaryKey);
|
|
191
|
+
|
|
192
|
+
let memoEntry = runtimeState.memoByLeaf.get(memoKey);
|
|
193
|
+
if (!memoEntry) {
|
|
194
|
+
const replayMemo = replaySnapshotFromBranch(branchEntries, boundary.startIndex);
|
|
195
|
+
memoEntry = cloneReplayMemoEntry(replayMemo);
|
|
196
|
+
runtimeState.memoByLeaf.set(memoKey, memoEntry);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
memoEntry,
|
|
201
|
+
sessionId,
|
|
202
|
+
leafId,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
98
206
|
export function createReplayRuntimeState(): ReplayRuntimeState {
|
|
99
207
|
return {
|
|
100
208
|
memoByLeaf: new Map(),
|
|
@@ -128,13 +236,21 @@ export function findReplayStartIndex(branchEntries: SessionEntry[]): ReplayBound
|
|
|
128
236
|
};
|
|
129
237
|
}
|
|
130
238
|
|
|
131
|
-
export function applyReadMetaTransition(
|
|
239
|
+
export function applyReadMetaTransition(
|
|
240
|
+
knowledge: KnowledgeMap,
|
|
241
|
+
meta: ReadCacheMetaV1,
|
|
242
|
+
seq: number,
|
|
243
|
+
blockedRangesByPath?: RangeBlockersByPath,
|
|
244
|
+
): void {
|
|
132
245
|
const { pathKey, scopeKey, servedHash, baseHash, mode } = meta;
|
|
133
246
|
const fullTrust = getTrust(knowledge, pathKey, SCOPE_FULL);
|
|
134
247
|
const rangeTrust = scopeKey === SCOPE_FULL ? undefined : getTrust(knowledge, pathKey, scopeKey);
|
|
135
248
|
|
|
136
249
|
if (mode === "full" || mode === "full_fallback") {
|
|
137
250
|
setTrust(knowledge, pathKey, scopeKey, servedHash, seq);
|
|
251
|
+
if (blockedRangesByPath && isRangeScope(scopeKey)) {
|
|
252
|
+
clearRangeBlocker(blockedRangesByPath, pathKey, scopeKey);
|
|
253
|
+
}
|
|
138
254
|
return;
|
|
139
255
|
}
|
|
140
256
|
|
|
@@ -174,14 +290,24 @@ export function applyReadMetaTransition(knowledge: KnowledgeMap, meta: ReadCache
|
|
|
174
290
|
}
|
|
175
291
|
}
|
|
176
292
|
|
|
177
|
-
export function applyInvalidation(
|
|
293
|
+
export function applyInvalidation(
|
|
294
|
+
knowledge: KnowledgeMap,
|
|
295
|
+
invalidation: ReadCacheInvalidationV1,
|
|
296
|
+
blockedRangesByPath?: RangeBlockersByPath,
|
|
297
|
+
): void {
|
|
178
298
|
const scopes = knowledge.get(invalidation.pathKey);
|
|
179
|
-
if (!scopes) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
299
|
|
|
183
300
|
if (invalidation.scopeKey === SCOPE_FULL) {
|
|
184
301
|
knowledge.delete(invalidation.pathKey);
|
|
302
|
+
blockedRangesByPath?.delete(invalidation.pathKey);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (blockedRangesByPath && isRangeScope(invalidation.scopeKey)) {
|
|
307
|
+
setRangeBlocker(blockedRangesByPath, invalidation.pathKey, invalidation.scopeKey);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!scopes) {
|
|
185
311
|
return;
|
|
186
312
|
}
|
|
187
313
|
|
|
@@ -192,53 +318,40 @@ export function applyInvalidation(knowledge: KnowledgeMap, invalidation: ReadCac
|
|
|
192
318
|
}
|
|
193
319
|
|
|
194
320
|
export function replayKnowledgeFromBranch(branchEntries: SessionEntry[], startIndex: number): KnowledgeMap {
|
|
195
|
-
|
|
196
|
-
const normalizedStart = Math.max(0, Math.min(startIndex, branchEntries.length));
|
|
197
|
-
let seq = 0;
|
|
198
|
-
|
|
199
|
-
for (let index = normalizedStart; index < branchEntries.length; index += 1) {
|
|
200
|
-
const entry = branchEntries[index];
|
|
201
|
-
if (!entry) {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const meta = extractReadMetaFromSessionEntry(entry);
|
|
206
|
-
if (meta) {
|
|
207
|
-
seq += 1;
|
|
208
|
-
applyReadMetaTransition(knowledge, meta, seq);
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const invalidation = extractInvalidationFromSessionEntry(entry);
|
|
213
|
-
if (invalidation) {
|
|
214
|
-
applyInvalidation(knowledge, invalidation);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return knowledge;
|
|
321
|
+
return replaySnapshotFromBranch(branchEntries, startIndex).knowledge;
|
|
219
322
|
}
|
|
220
323
|
|
|
221
324
|
export function buildKnowledgeForLeaf(
|
|
222
325
|
sessionManager: SessionManagerView,
|
|
223
326
|
runtimeState: ReplayRuntimeState,
|
|
224
327
|
): KnowledgeMap {
|
|
225
|
-
const sessionId = sessionManager
|
|
226
|
-
const leafId = sessionManager.getLeafId();
|
|
227
|
-
const branchEntries = sessionManager.getBranch();
|
|
228
|
-
const boundary = findReplayStartIndex(branchEntries);
|
|
229
|
-
const memoKey = getMemoKey(sessionId, leafId, boundary.boundaryKey);
|
|
230
|
-
|
|
231
|
-
let replayKnowledge = runtimeState.memoByLeaf.get(memoKey);
|
|
232
|
-
if (!replayKnowledge) {
|
|
233
|
-
replayKnowledge = replayKnowledgeFromBranch(branchEntries, boundary.startIndex);
|
|
234
|
-
runtimeState.memoByLeaf.set(memoKey, cloneKnowledgeMap(replayKnowledge));
|
|
235
|
-
}
|
|
236
|
-
|
|
328
|
+
const { memoEntry, sessionId, leafId } = getReplayMemoEntryForLeaf(sessionManager, runtimeState);
|
|
237
329
|
const overlayState = ensureOverlayForLeaf(runtimeState, sessionId, leafId);
|
|
238
330
|
if (leafHasChildren(sessionManager, leafId)) {
|
|
239
331
|
overlayState.knowledge.clear();
|
|
240
332
|
}
|
|
241
|
-
return mergeKnowledge(
|
|
333
|
+
return mergeKnowledge(memoEntry.knowledge, overlayState.knowledge);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function isRangeScopeBlockedByInvalidation(
|
|
337
|
+
sessionManager: SessionManagerView,
|
|
338
|
+
runtimeState: ReplayRuntimeState,
|
|
339
|
+
pathKey: string,
|
|
340
|
+
scopeKey: ScopeKey,
|
|
341
|
+
): boolean {
|
|
342
|
+
if (!isRangeScope(scopeKey)) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const { memoEntry, sessionId, leafId } = getReplayMemoEntryForLeaf(sessionManager, runtimeState);
|
|
347
|
+
const blockedScopes = memoEntry.blockedRangesByPath.get(pathKey);
|
|
348
|
+
if (!blockedScopes?.has(scopeKey)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const overlayState = ensureOverlayForLeaf(runtimeState, sessionId, leafId);
|
|
353
|
+
const overlayScopeTrust = overlayState.knowledge.get(pathKey)?.get(scopeKey);
|
|
354
|
+
return overlayScopeTrust === undefined;
|
|
242
355
|
}
|
|
243
356
|
|
|
244
357
|
export function overlaySet(
|
package/src/tool.ts
CHANGED
|
@@ -20,7 +20,13 @@ import { computeUnifiedDiff, isDiffUseful } from "./diff.js";
|
|
|
20
20
|
import { buildReadCacheMetaV1 } from "./meta.js";
|
|
21
21
|
import { hashBytes, loadObject, persistObjectIfAbsent } from "./object-store.js";
|
|
22
22
|
import { normalizeOffsetLimit, parseTrailingRangeIfNeeded, scopeKeyForRange } from "./path.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
buildKnowledgeForLeaf,
|
|
25
|
+
createReplayRuntimeState,
|
|
26
|
+
isRangeScopeBlockedByInvalidation,
|
|
27
|
+
overlaySet,
|
|
28
|
+
type ReplayRuntimeState,
|
|
29
|
+
} from "./replay.js";
|
|
24
30
|
import { compareSlices, splitLines, truncateForReadcache } from "./text.js";
|
|
25
31
|
import type {
|
|
26
32
|
ReadCacheDebugReason,
|
|
@@ -161,7 +167,11 @@ function buildDebugInfo(
|
|
|
161
167
|
};
|
|
162
168
|
}
|
|
163
169
|
|
|
164
|
-
function selectBaseTrust(
|
|
170
|
+
function selectBaseTrust(
|
|
171
|
+
pathKnowledge: Map<ScopeKey, ScopeTrust> | undefined,
|
|
172
|
+
scopeKey: ScopeKey,
|
|
173
|
+
rangeScopeBlocked: boolean,
|
|
174
|
+
): ScopeTrust | undefined {
|
|
165
175
|
if (!pathKnowledge) {
|
|
166
176
|
return undefined;
|
|
167
177
|
}
|
|
@@ -170,6 +180,10 @@ function selectBaseTrust(pathKnowledge: Map<ScopeKey, ScopeTrust> | undefined, s
|
|
|
170
180
|
return pathKnowledge.get(SCOPE_FULL);
|
|
171
181
|
}
|
|
172
182
|
|
|
183
|
+
if (rangeScopeBlocked) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
173
187
|
const exactTrust = pathKnowledge.get(scopeKey);
|
|
174
188
|
const fullTrust = pathKnowledge.get(SCOPE_FULL);
|
|
175
189
|
if (!exactTrust) {
|
|
@@ -290,7 +304,13 @@ export function createReadOverrideTool(runtimeState: ReplayRuntimeState = create
|
|
|
290
304
|
const scopeKey = scopeKeyForRange(start, end, totalLines);
|
|
291
305
|
const knowledge = buildKnowledgeForLeaf(ctx.sessionManager, runtimeState);
|
|
292
306
|
const pathKnowledge = knowledge.get(pathKey);
|
|
293
|
-
const
|
|
307
|
+
const rangeScopeBlocked = isRangeScopeBlockedByInvalidation(
|
|
308
|
+
ctx.sessionManager,
|
|
309
|
+
runtimeState,
|
|
310
|
+
pathKey,
|
|
311
|
+
scopeKey,
|
|
312
|
+
);
|
|
313
|
+
const baseHash = selectBaseTrust(pathKnowledge, scopeKey, rangeScopeBlocked)?.hash;
|
|
294
314
|
|
|
295
315
|
if (!baseHash) {
|
|
296
316
|
const meta = buildReadcacheMeta(
|