pi-readcache 0.1.1 → 0.2.0
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 +20 -5
- package/index.ts +2 -2
- package/package.json +1 -1
- package/src/commands.ts +3 -2
- package/src/constants.ts +2 -1
- package/src/diff.ts +13 -1
- package/src/meta.ts +60 -33
- package/src/replay.ts +1 -1
- package/src/telemetry.ts +1 -1
- package/src/tool.ts +26 -2
- package/src/types.ts +3 -2
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ When the extension is active, `read` may return:
|
|
|
23
23
|
- unchanged marker (`mode: unchanged`)
|
|
24
24
|
- unchanged range marker (`mode: unchanged_range`)
|
|
25
25
|
- unified diff for full-file reads (`mode: diff`)
|
|
26
|
-
- baseline fallback (`mode:
|
|
26
|
+
- baseline fallback (`mode: baseline_fallback`)
|
|
27
27
|
|
|
28
28
|
Plus:
|
|
29
29
|
- `/readcache-status` to inspect replay/coverage/savings
|
|
@@ -50,9 +50,9 @@ After installation, you can use pi normally. If pi is already running when you i
|
|
|
50
50
|
|
|
51
51
|
| Action | Command | Expected result |
|
|
52
52
|
|---|---|---|
|
|
53
|
-
| Baseline read | `read src/foo.ts` | `mode: full` or `mode:
|
|
53
|
+
| Baseline read | `read src/foo.ts` | `mode: full` or `mode: baseline_fallback` |
|
|
54
54
|
| Repeat read (no file change) | `read src/foo.ts` | `[readcache: unchanged, ...]` |
|
|
55
|
-
| Range read | `read src/foo.ts:1-120` | `mode: full`, `
|
|
55
|
+
| Range read | `read src/foo.ts:1-120` | `mode: full`, `baseline_fallback`, or `unchanged_range` |
|
|
56
56
|
| Inspect replay/cache state | `/readcache-status` | tracked scopes, replay window, mode counts, estimated savings |
|
|
57
57
|
| Invalidate full scope | `/readcache-refresh src/foo.ts` | next full read re-anchors |
|
|
58
58
|
| Invalidate range scope | `/readcache-refresh src/foo.ts 1-120` | next range read re-anchors |
|
|
@@ -63,9 +63,24 @@ After installation, you can use pi normally. If pi is already running when you i
|
|
|
63
63
|
- Compaction is a strict replay barrier for trust reconstruction:
|
|
64
64
|
- replay starts at the latest active `compaction + 1`.
|
|
65
65
|
- pre-compaction trust is not used after that barrier.
|
|
66
|
-
- First read after that barrier for a path/scope will re-anchor with baseline (`full`/`
|
|
66
|
+
- First read after that barrier for a path/scope will re-anchor with baseline (`full`/`baseline_fallback`).
|
|
67
67
|
- For exact current file text, the assistant should still perform an actual `read` in current context.
|
|
68
68
|
|
|
69
|
+
### Diff usefulness gate (why you sometimes get full content instead of a diff)
|
|
70
|
+
|
|
71
|
+
For full-file reads, `mode: diff` is emitted only when the generated patch is clearly more useful than baseline text.
|
|
72
|
+
|
|
73
|
+
Current defaults:
|
|
74
|
+
- `MAX_DIFF_TO_BASE_RATIO = 0.9`
|
|
75
|
+
- if `diffBytes >= selectedBytes * 0.9`, diff is considered not useful and falls back to baseline (`mode: baseline_fallback`)
|
|
76
|
+
- `MAX_DIFF_TO_BASE_LINE_RATIO = 0.85`
|
|
77
|
+
- if patch line count is greater than `selectedRequestedLines * 0.85`, diff is considered too large/noisy and falls back to baseline
|
|
78
|
+
|
|
79
|
+
Why these defaults:
|
|
80
|
+
- avoids near-full-file patch spam on high-churn edits
|
|
81
|
+
- improves readability for the model versus very large hunks
|
|
82
|
+
- keeps token savings meaningful when diff mode is used
|
|
83
|
+
|
|
69
84
|
---
|
|
70
85
|
|
|
71
86
|
## For extension developers (and curious cats)
|
|
@@ -89,7 +104,7 @@ flowchart TD
|
|
|
89
104
|
F -- no --> G[mode=full, attach metadata]
|
|
90
105
|
F -- yes + same hash --> H[mode=unchanged/unchanged_range]
|
|
91
106
|
F -- yes + full scope + useful diff --> I[mode=diff]
|
|
92
|
-
F -- otherwise --> J[mode=
|
|
107
|
+
F -- otherwise --> J[mode=baseline_fallback]
|
|
93
108
|
G --> K[persist object + overlay trust]
|
|
94
109
|
H --> K
|
|
95
110
|
I --> K
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { registerReadcacheCommands } from "./src/commands.js";
|
|
3
3
|
import { clearReplayRuntimeState, createReplayRuntimeState } from "./src/replay.js";
|
|
4
4
|
import { pruneObjectsOlderThan } from "./src/object-store.js";
|
|
@@ -6,7 +6,7 @@ import { createReadOverrideTool } from "./src/tool.js";
|
|
|
6
6
|
|
|
7
7
|
export default function (pi: ExtensionAPI): void {
|
|
8
8
|
const runtimeState = createReplayRuntimeState();
|
|
9
|
-
pi.registerTool(createReadOverrideTool(runtimeState));
|
|
9
|
+
pi.registerTool(createReadOverrideTool(runtimeState) as unknown as ToolDefinition);
|
|
10
10
|
registerReadcacheCommands(pi, runtimeState);
|
|
11
11
|
|
|
12
12
|
const clearCaches = (): void => {
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ExtensionAPI,
|
|
6
6
|
ExtensionCommandContext,
|
|
7
7
|
ExtensionContext,
|
|
8
|
+
ToolDefinition,
|
|
8
9
|
} from "@mariozechner/pi-coding-agent";
|
|
9
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
10
11
|
import { READCACHE_CUSTOM_TYPE, SCOPE_FULL, scopeRange } from "./constants.js";
|
|
@@ -50,7 +51,7 @@ function formatModeCounts(modeCounts: ReturnType<typeof collectReplayTelemetry>[
|
|
|
50
51
|
`unchanged=${modeCounts.unchanged}`,
|
|
51
52
|
`unchanged_range=${modeCounts.unchanged_range}`,
|
|
52
53
|
`diff=${modeCounts.diff}`,
|
|
53
|
-
`
|
|
54
|
+
`baseline_fallback=${modeCounts.baseline_fallback}`,
|
|
54
55
|
].join(", ");
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -324,5 +325,5 @@ export function registerReadcacheCommands(pi: ExtensionAPI, runtimeState: Replay
|
|
|
324
325
|
},
|
|
325
326
|
});
|
|
326
327
|
|
|
327
|
-
pi.registerTool(createReadcacheRefreshTool(pi, runtimeState));
|
|
328
|
+
pi.registerTool(createReadcacheRefreshTool(pi, runtimeState) as unknown as ToolDefinition);
|
|
328
329
|
}
|
package/src/constants.ts
CHANGED
|
@@ -5,7 +5,8 @@ export const SCOPE_FULL = "full" as const;
|
|
|
5
5
|
|
|
6
6
|
export const MAX_DIFF_FILE_BYTES = 2 * 1024 * 1024;
|
|
7
7
|
export const MAX_DIFF_FILE_LINES = 12_000;
|
|
8
|
-
export const MAX_DIFF_TO_BASE_RATIO =
|
|
8
|
+
export const MAX_DIFF_TO_BASE_RATIO = 0.9;
|
|
9
|
+
export const MAX_DIFF_TO_BASE_LINE_RATIO = 0.85;
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_EXCLUDED_PATH_PATTERNS = [
|
|
11
12
|
".env*",
|
package/src/diff.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { formatPatch, structuredPatch } from "diff";
|
|
|
2
2
|
import {
|
|
3
3
|
MAX_DIFF_FILE_BYTES,
|
|
4
4
|
MAX_DIFF_FILE_LINES,
|
|
5
|
+
MAX_DIFF_TO_BASE_LINE_RATIO,
|
|
5
6
|
MAX_DIFF_TO_BASE_RATIO,
|
|
6
7
|
} from "./constants.js";
|
|
7
8
|
|
|
@@ -11,6 +12,7 @@ export interface DiffLimits {
|
|
|
11
12
|
maxFileBytes: number;
|
|
12
13
|
maxFileLines: number;
|
|
13
14
|
maxDiffToBaseRatio: number;
|
|
15
|
+
maxDiffToBaseLineRatio: number;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface DiffComputation {
|
|
@@ -25,6 +27,7 @@ export const DEFAULT_DIFF_LIMITS: DiffLimits = {
|
|
|
25
27
|
maxFileBytes: MAX_DIFF_FILE_BYTES,
|
|
26
28
|
maxFileLines: MAX_DIFF_FILE_LINES,
|
|
27
29
|
maxDiffToBaseRatio: MAX_DIFF_TO_BASE_RATIO,
|
|
30
|
+
maxDiffToBaseLineRatio: MAX_DIFF_TO_BASE_LINE_RATIO,
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
function sanitizePathForPatch(pathDisplay: string): string {
|
|
@@ -125,6 +128,15 @@ export function isDiffUseful(
|
|
|
125
128
|
if (diffBytes === 0) {
|
|
126
129
|
return false;
|
|
127
130
|
}
|
|
131
|
+
if (diffBytes >= selectedBytes * limits.maxDiffToBaseRatio) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const selectedRequestedLines = lineCount(selectedCurrentText);
|
|
136
|
+
const diffLines = lineCount(diffText);
|
|
137
|
+
if (diffLines > selectedRequestedLines * limits.maxDiffToBaseLineRatio) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
128
140
|
|
|
129
|
-
return
|
|
141
|
+
return true;
|
|
130
142
|
}
|
package/src/meta.ts
CHANGED
|
@@ -22,14 +22,23 @@ function isNonNegativeInteger(value: unknown): value is number {
|
|
|
22
22
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
25
|
+
function normalizeReadCacheMode(value: unknown): ReadCacheMode | undefined {
|
|
26
|
+
if (value === "full") {
|
|
27
|
+
return "full";
|
|
28
|
+
}
|
|
29
|
+
if (value === "unchanged") {
|
|
30
|
+
return "unchanged";
|
|
31
|
+
}
|
|
32
|
+
if (value === "unchanged_range") {
|
|
33
|
+
return "unchanged_range";
|
|
34
|
+
}
|
|
35
|
+
if (value === "diff") {
|
|
36
|
+
return "diff";
|
|
37
|
+
}
|
|
38
|
+
if (value === "baseline_fallback") {
|
|
39
|
+
return "baseline_fallback";
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
function isReadCacheDebugReason(value: unknown): value is ReadCacheDebugV1["reason"] {
|
|
@@ -44,7 +53,8 @@ function isReadCacheDebugReason(value: unknown): value is ReadCacheDebugV1["reas
|
|
|
44
53
|
value === "diff_unavailable_or_empty" ||
|
|
45
54
|
value === "diff_not_useful" ||
|
|
46
55
|
value === "diff_payload_truncated" ||
|
|
47
|
-
value === "diff_emitted"
|
|
56
|
+
value === "diff_emitted" ||
|
|
57
|
+
value === "bypass_cache"
|
|
48
58
|
);
|
|
49
59
|
}
|
|
50
60
|
|
|
@@ -111,48 +121,69 @@ export function isScopeKey(value: unknown): value is ScopeKey {
|
|
|
111
121
|
return Number.isInteger(start) && Number.isInteger(end) && start > 0 && end >= start;
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
|
|
124
|
+
function parseReadCacheMetaV1(value: unknown): ReadCacheMetaV1 | undefined {
|
|
115
125
|
if (!isRecord(value)) {
|
|
116
|
-
return
|
|
126
|
+
return undefined;
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
if (value.v !== READCACHE_META_VERSION) {
|
|
120
|
-
return
|
|
130
|
+
return undefined;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
if (typeof value.pathKey !== "string" || value.pathKey.length === 0) {
|
|
124
|
-
return
|
|
134
|
+
return undefined;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
if (!isScopeKey(value.scopeKey)) {
|
|
128
|
-
return
|
|
138
|
+
return undefined;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
if (typeof value.servedHash !== "string" || value.servedHash.length === 0) {
|
|
132
|
-
return
|
|
142
|
+
return undefined;
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
const mode = normalizeReadCacheMode(value.mode);
|
|
146
|
+
if (!mode) {
|
|
147
|
+
return undefined;
|
|
137
148
|
}
|
|
138
149
|
|
|
139
|
-
const requiresBaseHash =
|
|
150
|
+
const requiresBaseHash = mode === "unchanged" || mode === "unchanged_range" || mode === "diff";
|
|
140
151
|
if (requiresBaseHash) {
|
|
141
152
|
if (typeof value.baseHash !== "string" || value.baseHash.length === 0) {
|
|
142
|
-
return
|
|
153
|
+
return undefined;
|
|
143
154
|
}
|
|
144
155
|
} else if (value.baseHash !== undefined && (typeof value.baseHash !== "string" || value.baseHash.length === 0)) {
|
|
145
|
-
return
|
|
156
|
+
return undefined;
|
|
146
157
|
}
|
|
147
158
|
|
|
148
|
-
|
|
149
|
-
isPositiveInteger(value.totalLines)
|
|
150
|
-
isPositiveInteger(value.rangeStart)
|
|
151
|
-
isPositiveInteger(value.rangeEnd)
|
|
152
|
-
value.rangeEnd
|
|
153
|
-
isNonNegativeInteger(value.bytes)
|
|
154
|
-
(value.debug
|
|
155
|
-
)
|
|
159
|
+
if (
|
|
160
|
+
!isPositiveInteger(value.totalLines) ||
|
|
161
|
+
!isPositiveInteger(value.rangeStart) ||
|
|
162
|
+
!isPositiveInteger(value.rangeEnd) ||
|
|
163
|
+
value.rangeEnd < value.rangeStart ||
|
|
164
|
+
!isNonNegativeInteger(value.bytes) ||
|
|
165
|
+
(value.debug !== undefined && !isReadCacheDebugV1(value.debug))
|
|
166
|
+
) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
v: READCACHE_META_VERSION,
|
|
172
|
+
pathKey: value.pathKey,
|
|
173
|
+
scopeKey: value.scopeKey,
|
|
174
|
+
servedHash: value.servedHash,
|
|
175
|
+
...(typeof value.baseHash === "string" ? { baseHash: value.baseHash } : {}),
|
|
176
|
+
mode,
|
|
177
|
+
totalLines: value.totalLines,
|
|
178
|
+
rangeStart: value.rangeStart,
|
|
179
|
+
rangeEnd: value.rangeEnd,
|
|
180
|
+
bytes: value.bytes,
|
|
181
|
+
...(value.debug !== undefined ? { debug: value.debug } : {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function isReadCacheMetaV1(value: unknown): value is ReadCacheMetaV1 {
|
|
186
|
+
return parseReadCacheMetaV1(value) !== undefined;
|
|
156
187
|
}
|
|
157
188
|
|
|
158
189
|
export function isReadCacheInvalidationV1(value: unknown): value is ReadCacheInvalidationV1 {
|
|
@@ -202,11 +233,7 @@ export function extractReadMetaFromSessionEntry(entry: SessionEntry): ReadCacheM
|
|
|
202
233
|
}
|
|
203
234
|
|
|
204
235
|
const candidate = message.details.readcache;
|
|
205
|
-
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return candidate;
|
|
236
|
+
return parseReadCacheMetaV1(candidate);
|
|
210
237
|
}
|
|
211
238
|
|
|
212
239
|
export function extractInvalidationFromSessionEntry(entry: SessionEntry): ReadCacheInvalidationV1 | undefined {
|
package/src/replay.ts
CHANGED
|
@@ -246,7 +246,7 @@ export function applyReadMetaTransition(
|
|
|
246
246
|
const fullTrust = getTrust(knowledge, pathKey, SCOPE_FULL);
|
|
247
247
|
const rangeTrust = scopeKey === SCOPE_FULL ? undefined : getTrust(knowledge, pathKey, scopeKey);
|
|
248
248
|
|
|
249
|
-
if (mode === "full" || mode === "
|
|
249
|
+
if (mode === "full" || mode === "baseline_fallback") {
|
|
250
250
|
setTrust(knowledge, pathKey, scopeKey, servedHash, seq);
|
|
251
251
|
if (blockedRangesByPath && isRangeScope(scopeKey)) {
|
|
252
252
|
clearRangeBlocker(blockedRangesByPath, pathKey, scopeKey);
|
package/src/telemetry.ts
CHANGED
package/src/tool.ts
CHANGED
|
@@ -50,12 +50,18 @@ export const readToolSchema = Type.Object({
|
|
|
50
50
|
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
51
51
|
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
52
52
|
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
53
|
+
bypass_cache: Type.Optional(
|
|
54
|
+
Type.Boolean({
|
|
55
|
+
description:
|
|
56
|
+
"If true, bypass readcache optimization for this call and return baseline read output for the requested scope",
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
53
59
|
});
|
|
54
60
|
|
|
55
61
|
export type ReadToolParams = Static<typeof readToolSchema>;
|
|
56
62
|
|
|
57
63
|
function buildReadDescription(): string {
|
|
58
|
-
return `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. Returns full text, unchanged marker, or unified diff. Treat output as authoritative for requested scope. Use readcache_refresh only when output is insufficient for correctness; it
|
|
64
|
+
return `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. Returns full text, unchanged marker, or unified diff. Treat output as authoritative for requested scope. Set bypass_cache=true to force baseline output for this call only. If an edit fails because exact old text was not found, re-read the same path and scope with bypass_cache=true before retrying edit. Use readcache_refresh only when output is insufficient for correctness across calls; it invalidates trust for the selected scope until that scope is re-anchored by a baseline read, and increases repeated-read context usage.`;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
function hasImageContent(result: AgentToolResult<ReadToolDetails | undefined>): boolean {
|
|
@@ -302,6 +308,24 @@ export function createReadOverrideTool(runtimeState: ReplayRuntimeState = create
|
|
|
302
308
|
throwIfAborted(signal);
|
|
303
309
|
const pathKey = parsed.absolutePath;
|
|
304
310
|
const scopeKey = scopeKeyForRange(start, end, totalLines);
|
|
311
|
+
|
|
312
|
+
if (params.bypass_cache === true) {
|
|
313
|
+
const meta = buildReadcacheMeta(
|
|
314
|
+
pathKey,
|
|
315
|
+
scopeKey,
|
|
316
|
+
current.currentHash,
|
|
317
|
+
"full",
|
|
318
|
+
totalLines,
|
|
319
|
+
start,
|
|
320
|
+
end,
|
|
321
|
+
current.bytes.byteLength,
|
|
322
|
+
undefined,
|
|
323
|
+
buildDebugInfo(scopeKey, undefined, "bypass_cache"),
|
|
324
|
+
);
|
|
325
|
+
await persistAndOverlay(runtimeState, ctx, pathKey, scopeKey, current.currentHash, current.text);
|
|
326
|
+
return attachMetaToBaseline(baselineResult, meta);
|
|
327
|
+
}
|
|
328
|
+
|
|
305
329
|
const knowledge = buildKnowledgeForLeaf(ctx.sessionManager, runtimeState);
|
|
306
330
|
const pathKnowledge = knowledge.get(pathKey);
|
|
307
331
|
const rangeScopeBlocked = isRangeScopeBlockedByInvalidation(
|
|
@@ -366,7 +390,7 @@ export function createReadOverrideTool(runtimeState: ReplayRuntimeState = create
|
|
|
366
390
|
pathKey,
|
|
367
391
|
scopeKey,
|
|
368
392
|
current.currentHash,
|
|
369
|
-
"
|
|
393
|
+
"baseline_fallback",
|
|
370
394
|
totalLines,
|
|
371
395
|
start,
|
|
372
396
|
end,
|
package/src/types.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface ScopeTrust {
|
|
|
10
10
|
seq: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export type ReadCacheMode = "full" | "unchanged" | "unchanged_range" | "diff" | "
|
|
13
|
+
export type ReadCacheMode = "full" | "unchanged" | "unchanged_range" | "diff" | "baseline_fallback";
|
|
14
14
|
|
|
15
15
|
export type ReadCacheDebugReason =
|
|
16
16
|
| "no_base_hash"
|
|
@@ -23,7 +23,8 @@ export type ReadCacheDebugReason =
|
|
|
23
23
|
| "diff_unavailable_or_empty"
|
|
24
24
|
| "diff_not_useful"
|
|
25
25
|
| "diff_payload_truncated"
|
|
26
|
-
| "diff_emitted"
|
|
26
|
+
| "diff_emitted"
|
|
27
|
+
| "bypass_cache";
|
|
27
28
|
|
|
28
29
|
export interface ReadCacheDebugV1 {
|
|
29
30
|
reason: ReadCacheDebugReason;
|