pi-readcache 0.1.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/LICENSE +21 -0
- package/README.md +168 -0
- package/index.ts +27 -0
- package/package.json +58 -0
- package/src/commands.ts +328 -0
- package/src/constants.ts +33 -0
- package/src/diff.ts +130 -0
- package/src/meta.ts +222 -0
- package/src/object-store.ts +190 -0
- package/src/path.ts +246 -0
- package/src/replay.ts +257 -0
- package/src/telemetry.ts +108 -0
- package/src/text.ts +43 -0
- package/src/tool.ts +437 -0
- package/src/types.ts +100 -0
package/src/tool.ts
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_MAX_BYTES,
|
|
5
|
+
DEFAULT_MAX_LINES,
|
|
6
|
+
createReadTool,
|
|
7
|
+
type AgentToolResult,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
type ReadToolDetails,
|
|
10
|
+
type TruncationResult,
|
|
11
|
+
} from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_EXCLUDED_PATH_PATTERNS,
|
|
15
|
+
MAX_DIFF_FILE_BYTES,
|
|
16
|
+
MAX_DIFF_FILE_LINES,
|
|
17
|
+
SCOPE_FULL,
|
|
18
|
+
} from "./constants.js";
|
|
19
|
+
import { computeUnifiedDiff, isDiffUseful } from "./diff.js";
|
|
20
|
+
import { buildReadCacheMetaV1 } from "./meta.js";
|
|
21
|
+
import { hashBytes, loadObject, persistObjectIfAbsent } from "./object-store.js";
|
|
22
|
+
import { normalizeOffsetLimit, parseTrailingRangeIfNeeded, scopeKeyForRange } from "./path.js";
|
|
23
|
+
import { buildKnowledgeForLeaf, createReplayRuntimeState, overlaySet, type ReplayRuntimeState } from "./replay.js";
|
|
24
|
+
import { compareSlices, splitLines, truncateForReadcache } from "./text.js";
|
|
25
|
+
import type {
|
|
26
|
+
ReadCacheDebugReason,
|
|
27
|
+
ReadCacheDebugV1,
|
|
28
|
+
ReadCacheMetaV1,
|
|
29
|
+
ReadToolDetailsExt,
|
|
30
|
+
ScopeKey,
|
|
31
|
+
ScopeTrust,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
|
|
34
|
+
const UTF8_STRICT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
35
|
+
|
|
36
|
+
interface CurrentTextState {
|
|
37
|
+
bytes: Buffer;
|
|
38
|
+
text: string;
|
|
39
|
+
totalLines: number;
|
|
40
|
+
currentHash: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const readToolSchema = Type.Object({
|
|
44
|
+
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
45
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
46
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type ReadToolParams = Static<typeof readToolSchema>;
|
|
50
|
+
|
|
51
|
+
function buildReadDescription(): string {
|
|
52
|
+
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 forces full baseline payload for file and increases repeated-read context usage.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasImageContent(result: AgentToolResult<ReadToolDetails | undefined>): boolean {
|
|
56
|
+
return result.content.some((block) => block.type === "image");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
60
|
+
if (signal?.aborted) {
|
|
61
|
+
throw new Error("Operation aborted");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isExcludedPath(pathKey: string): boolean {
|
|
66
|
+
const baseName = basename(pathKey).toLowerCase();
|
|
67
|
+
return DEFAULT_EXCLUDED_PATH_PATTERNS.some((pattern) => {
|
|
68
|
+
if (pattern === ".env*") {
|
|
69
|
+
return baseName.startsWith(".env");
|
|
70
|
+
}
|
|
71
|
+
if (pattern.startsWith("*")) {
|
|
72
|
+
return baseName.endsWith(pattern.slice(1));
|
|
73
|
+
}
|
|
74
|
+
return baseName === pattern;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function withReadcacheDetails(details: ReadToolDetails | undefined, readcache: ReadCacheMetaV1): ReadToolDetailsExt {
|
|
79
|
+
return {
|
|
80
|
+
...(details ?? {}),
|
|
81
|
+
readcache,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function attachMetaToBaseline(
|
|
86
|
+
baselineResult: AgentToolResult<ReadToolDetails | undefined>,
|
|
87
|
+
meta: ReadCacheMetaV1,
|
|
88
|
+
): AgentToolResult<ReadToolDetailsExt | undefined> {
|
|
89
|
+
return {
|
|
90
|
+
...baselineResult,
|
|
91
|
+
details: withReadcacheDetails(baselineResult.details, meta),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildTextResult(
|
|
96
|
+
text: string,
|
|
97
|
+
meta: ReadCacheMetaV1,
|
|
98
|
+
truncation?: TruncationResult,
|
|
99
|
+
): AgentToolResult<ReadToolDetailsExt | undefined> {
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text }],
|
|
102
|
+
details: withReadcacheDetails(truncation ? { truncation } : undefined, meta),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildMarkerResult(marker: string, meta: ReadCacheMetaV1): AgentToolResult<ReadToolDetailsExt | undefined> {
|
|
107
|
+
return buildTextResult(marker, meta);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildBaselineInput(path: string, offset: number | undefined, limit: number | undefined): ReadToolParams {
|
|
111
|
+
const input: ReadToolParams = { path };
|
|
112
|
+
if (offset !== undefined) {
|
|
113
|
+
input.offset = offset;
|
|
114
|
+
}
|
|
115
|
+
if (limit !== undefined) {
|
|
116
|
+
input.limit = limit;
|
|
117
|
+
}
|
|
118
|
+
return input;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildReadcacheMeta(
|
|
122
|
+
pathKey: string,
|
|
123
|
+
scopeKey: ScopeKey,
|
|
124
|
+
servedHash: string,
|
|
125
|
+
mode: ReadCacheMetaV1["mode"],
|
|
126
|
+
totalLines: number,
|
|
127
|
+
rangeStart: number,
|
|
128
|
+
rangeEnd: number,
|
|
129
|
+
bytes: number,
|
|
130
|
+
baseHash?: string,
|
|
131
|
+
debug?: ReadCacheDebugV1,
|
|
132
|
+
): ReadCacheMetaV1 {
|
|
133
|
+
return buildReadCacheMetaV1({
|
|
134
|
+
pathKey,
|
|
135
|
+
scopeKey,
|
|
136
|
+
servedHash,
|
|
137
|
+
...(baseHash !== undefined ? { baseHash } : {}),
|
|
138
|
+
mode,
|
|
139
|
+
totalLines,
|
|
140
|
+
rangeStart,
|
|
141
|
+
rangeEnd,
|
|
142
|
+
bytes,
|
|
143
|
+
...(debug !== undefined ? { debug } : {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildDebugInfo(
|
|
148
|
+
scopeKey: ScopeKey,
|
|
149
|
+
baseHash: string | undefined,
|
|
150
|
+
reason: ReadCacheDebugReason,
|
|
151
|
+
overrides: Partial<Omit<ReadCacheDebugV1, "reason" | "scope" | "baseHashFound" | "diffAttempted">> & {
|
|
152
|
+
diffAttempted?: boolean;
|
|
153
|
+
} = {},
|
|
154
|
+
): ReadCacheDebugV1 {
|
|
155
|
+
return {
|
|
156
|
+
reason,
|
|
157
|
+
scope: scopeKey === SCOPE_FULL ? "full" : "range",
|
|
158
|
+
baseHashFound: baseHash !== undefined,
|
|
159
|
+
diffAttempted: overrides.diffAttempted ?? false,
|
|
160
|
+
...overrides,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function selectBaseTrust(pathKnowledge: Map<ScopeKey, ScopeTrust> | undefined, scopeKey: ScopeKey): ScopeTrust | undefined {
|
|
165
|
+
if (!pathKnowledge) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (scopeKey === SCOPE_FULL) {
|
|
170
|
+
return pathKnowledge.get(SCOPE_FULL);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const exactTrust = pathKnowledge.get(scopeKey);
|
|
174
|
+
const fullTrust = pathKnowledge.get(SCOPE_FULL);
|
|
175
|
+
if (!exactTrust) {
|
|
176
|
+
return fullTrust;
|
|
177
|
+
}
|
|
178
|
+
if (!fullTrust) {
|
|
179
|
+
return exactTrust;
|
|
180
|
+
}
|
|
181
|
+
return exactTrust.seq >= fullTrust.seq ? exactTrust : fullTrust;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildUnchangedMarker(scopeKey: ScopeKey, start: number, end: number, totalLines: number, outsideRangeChanged: boolean): string {
|
|
185
|
+
if (scopeKey === SCOPE_FULL) {
|
|
186
|
+
return `[readcache: unchanged, ${totalLines} lines]`;
|
|
187
|
+
}
|
|
188
|
+
if (outsideRangeChanged) {
|
|
189
|
+
return `[readcache: unchanged in lines ${start}-${end}; changes exist outside this range]`;
|
|
190
|
+
}
|
|
191
|
+
return `[readcache: unchanged in lines ${start}-${end} of ${totalLines}]`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildDiffPayload(changedLines: number, totalLines: number, diffText: string): string {
|
|
195
|
+
return `[readcache: ${changedLines} lines changed of ${totalLines}]\n${diffText}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readCurrentTextStrict(absolutePath: string): Promise<CurrentTextState | undefined> {
|
|
199
|
+
let fileBytes: Buffer;
|
|
200
|
+
try {
|
|
201
|
+
fileBytes = await readFile(absolutePath);
|
|
202
|
+
} catch {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let text: string;
|
|
207
|
+
try {
|
|
208
|
+
text = UTF8_STRICT_DECODER.decode(fileBytes);
|
|
209
|
+
} catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const totalLines = splitLines(text).length;
|
|
214
|
+
return {
|
|
215
|
+
bytes: fileBytes,
|
|
216
|
+
text,
|
|
217
|
+
totalLines,
|
|
218
|
+
currentHash: hashBytes(fileBytes),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function persistAndOverlay(
|
|
223
|
+
runtimeState: ReplayRuntimeState,
|
|
224
|
+
ctx: ExtensionContext,
|
|
225
|
+
pathKey: string,
|
|
226
|
+
scopeKey: ScopeKey,
|
|
227
|
+
servedHash: string,
|
|
228
|
+
text: string,
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
try {
|
|
231
|
+
await persistObjectIfAbsent(ctx.cwd, servedHash, text);
|
|
232
|
+
} catch {
|
|
233
|
+
// Object persistence failures are fail-open.
|
|
234
|
+
}
|
|
235
|
+
overlaySet(runtimeState, ctx.sessionManager, pathKey, scopeKey, servedHash);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function createReadOverrideTool(runtimeState: ReplayRuntimeState = createReplayRuntimeState()) {
|
|
239
|
+
return {
|
|
240
|
+
name: "read",
|
|
241
|
+
label: "read",
|
|
242
|
+
description: buildReadDescription(),
|
|
243
|
+
parameters: readToolSchema,
|
|
244
|
+
execute: async (
|
|
245
|
+
toolCallId: string,
|
|
246
|
+
params: ReadToolParams,
|
|
247
|
+
signal?: AbortSignal,
|
|
248
|
+
onUpdate?: (partial: AgentToolResult<ReadToolDetailsExt | undefined>) => void,
|
|
249
|
+
ctx?: ExtensionContext,
|
|
250
|
+
): Promise<AgentToolResult<ReadToolDetailsExt | undefined>> => {
|
|
251
|
+
if (!ctx) {
|
|
252
|
+
throw new Error("read override requires extension context");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
throwIfAborted(signal);
|
|
256
|
+
|
|
257
|
+
const parsed = parseTrailingRangeIfNeeded(params.path, params.offset, params.limit, ctx.cwd);
|
|
258
|
+
const baseline = createReadTool(ctx.cwd);
|
|
259
|
+
const baselineInput = buildBaselineInput(parsed.pathInput, parsed.offset, parsed.limit);
|
|
260
|
+
const baselineResult = await baseline.execute(toolCallId, baselineInput, signal, onUpdate);
|
|
261
|
+
|
|
262
|
+
if (hasImageContent(baselineResult)) {
|
|
263
|
+
return baselineResult;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (isExcludedPath(parsed.absolutePath)) {
|
|
267
|
+
return baselineResult;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throwIfAborted(signal);
|
|
271
|
+
const current = await readCurrentTextStrict(parsed.absolutePath);
|
|
272
|
+
if (!current) {
|
|
273
|
+
return baselineResult;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let start: number;
|
|
277
|
+
let end: number;
|
|
278
|
+
let totalLines: number;
|
|
279
|
+
try {
|
|
280
|
+
const normalizedRange = normalizeOffsetLimit(parsed.offset, parsed.limit, current.totalLines);
|
|
281
|
+
start = normalizedRange.start;
|
|
282
|
+
end = normalizedRange.end;
|
|
283
|
+
totalLines = normalizedRange.totalLines;
|
|
284
|
+
} catch {
|
|
285
|
+
return baselineResult;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throwIfAborted(signal);
|
|
289
|
+
const pathKey = parsed.absolutePath;
|
|
290
|
+
const scopeKey = scopeKeyForRange(start, end, totalLines);
|
|
291
|
+
const knowledge = buildKnowledgeForLeaf(ctx.sessionManager, runtimeState);
|
|
292
|
+
const pathKnowledge = knowledge.get(pathKey);
|
|
293
|
+
const baseHash = selectBaseTrust(pathKnowledge, scopeKey)?.hash;
|
|
294
|
+
|
|
295
|
+
if (!baseHash) {
|
|
296
|
+
const meta = buildReadcacheMeta(
|
|
297
|
+
pathKey,
|
|
298
|
+
scopeKey,
|
|
299
|
+
current.currentHash,
|
|
300
|
+
"full",
|
|
301
|
+
totalLines,
|
|
302
|
+
start,
|
|
303
|
+
end,
|
|
304
|
+
current.bytes.byteLength,
|
|
305
|
+
undefined,
|
|
306
|
+
buildDebugInfo(scopeKey, baseHash, "no_base_hash"),
|
|
307
|
+
);
|
|
308
|
+
await persistAndOverlay(runtimeState, ctx, pathKey, scopeKey, current.currentHash, current.text);
|
|
309
|
+
return attachMetaToBaseline(baselineResult, meta);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (baseHash === current.currentHash) {
|
|
313
|
+
const mode = scopeKey === SCOPE_FULL ? "unchanged" : "unchanged_range";
|
|
314
|
+
const meta = buildReadcacheMeta(
|
|
315
|
+
pathKey,
|
|
316
|
+
scopeKey,
|
|
317
|
+
current.currentHash,
|
|
318
|
+
mode,
|
|
319
|
+
totalLines,
|
|
320
|
+
start,
|
|
321
|
+
end,
|
|
322
|
+
current.bytes.byteLength,
|
|
323
|
+
baseHash,
|
|
324
|
+
buildDebugInfo(scopeKey, baseHash, "hash_match"),
|
|
325
|
+
);
|
|
326
|
+
const marker = buildUnchangedMarker(scopeKey, start, end, totalLines, false);
|
|
327
|
+
await persistAndOverlay(runtimeState, ctx, pathKey, scopeKey, current.currentHash, current.text);
|
|
328
|
+
return buildMarkerResult(marker, meta);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let baseText: string | undefined;
|
|
332
|
+
try {
|
|
333
|
+
baseText = await loadObject(ctx.cwd, baseHash);
|
|
334
|
+
} catch {
|
|
335
|
+
baseText = undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const fallbackResult = async (
|
|
339
|
+
reason: ReadCacheDebugReason,
|
|
340
|
+
overrides: Partial<Omit<ReadCacheDebugV1, "reason" | "scope" | "baseHashFound" | "diffAttempted">> & {
|
|
341
|
+
diffAttempted?: boolean;
|
|
342
|
+
} = {},
|
|
343
|
+
): Promise<AgentToolResult<ReadToolDetailsExt | undefined>> => {
|
|
344
|
+
throwIfAborted(signal);
|
|
345
|
+
const meta = buildReadcacheMeta(
|
|
346
|
+
pathKey,
|
|
347
|
+
scopeKey,
|
|
348
|
+
current.currentHash,
|
|
349
|
+
"full_fallback",
|
|
350
|
+
totalLines,
|
|
351
|
+
start,
|
|
352
|
+
end,
|
|
353
|
+
current.bytes.byteLength,
|
|
354
|
+
baseHash,
|
|
355
|
+
buildDebugInfo(scopeKey, baseHash, reason, overrides),
|
|
356
|
+
);
|
|
357
|
+
await persistAndOverlay(runtimeState, ctx, pathKey, scopeKey, current.currentHash, current.text);
|
|
358
|
+
return attachMetaToBaseline(baselineResult, meta);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
if (!baseText) {
|
|
362
|
+
return fallbackResult("base_object_missing", { baseObjectFound: false });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (scopeKey !== SCOPE_FULL) {
|
|
366
|
+
if (compareSlices(baseText, current.text, start, end)) {
|
|
367
|
+
const meta = buildReadcacheMeta(
|
|
368
|
+
pathKey,
|
|
369
|
+
scopeKey,
|
|
370
|
+
current.currentHash,
|
|
371
|
+
"unchanged_range",
|
|
372
|
+
totalLines,
|
|
373
|
+
start,
|
|
374
|
+
end,
|
|
375
|
+
current.bytes.byteLength,
|
|
376
|
+
baseHash,
|
|
377
|
+
buildDebugInfo(scopeKey, baseHash, "range_slice_unchanged", { outsideRangeChanged: true }),
|
|
378
|
+
);
|
|
379
|
+
const marker = buildUnchangedMarker(scopeKey, start, end, totalLines, true);
|
|
380
|
+
await persistAndOverlay(runtimeState, ctx, pathKey, scopeKey, current.currentHash, current.text);
|
|
381
|
+
return buildMarkerResult(marker, meta);
|
|
382
|
+
}
|
|
383
|
+
return fallbackResult("range_slice_changed", { outsideRangeChanged: true });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const baseBytes = Buffer.byteLength(baseText, "utf-8");
|
|
387
|
+
const largestBytes = Math.max(baseBytes, current.bytes.byteLength);
|
|
388
|
+
if (largestBytes > MAX_DIFF_FILE_BYTES) {
|
|
389
|
+
return fallbackResult("diff_file_too_large_bytes", { diffAttempted: true, largestBytes });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const maxLines = Math.max(splitLines(baseText).length, totalLines);
|
|
393
|
+
if (maxLines > MAX_DIFF_FILE_LINES) {
|
|
394
|
+
return fallbackResult("diff_file_too_large_lines", { diffAttempted: true, maxLines });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
throwIfAborted(signal);
|
|
398
|
+
const diff = computeUnifiedDiff(baseText, current.text, parsed.pathInput);
|
|
399
|
+
if (!diff) {
|
|
400
|
+
return fallbackResult("diff_unavailable_or_empty", { diffAttempted: true });
|
|
401
|
+
}
|
|
402
|
+
if (!isDiffUseful(diff.diffText, baseText, current.text)) {
|
|
403
|
+
return fallbackResult("diff_not_useful", { diffAttempted: true, diffBytes: diff.diffBytes });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const diffPayload = buildDiffPayload(diff.changedLines, totalLines, diff.diffText);
|
|
407
|
+
const truncation = truncateForReadcache(diffPayload);
|
|
408
|
+
if (truncation.truncated) {
|
|
409
|
+
return fallbackResult("diff_payload_truncated", {
|
|
410
|
+
diffAttempted: true,
|
|
411
|
+
diffBytes: diff.diffBytes,
|
|
412
|
+
diffChangedLines: diff.changedLines,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
throwIfAborted(signal);
|
|
417
|
+
const meta = buildReadcacheMeta(
|
|
418
|
+
pathKey,
|
|
419
|
+
scopeKey,
|
|
420
|
+
current.currentHash,
|
|
421
|
+
"diff",
|
|
422
|
+
totalLines,
|
|
423
|
+
start,
|
|
424
|
+
end,
|
|
425
|
+
current.bytes.byteLength,
|
|
426
|
+
baseHash,
|
|
427
|
+
buildDebugInfo(scopeKey, baseHash, "diff_emitted", {
|
|
428
|
+
diffAttempted: true,
|
|
429
|
+
diffBytes: diff.diffBytes,
|
|
430
|
+
diffChangedLines: diff.changedLines,
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
await persistAndOverlay(runtimeState, ctx, pathKey, scopeKey, current.currentHash, current.text);
|
|
434
|
+
return buildTextResult(truncation.content, meta, truncation.truncated ? truncation : undefined);
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { ReadToolDetails } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { SCOPE_FULL } from "./constants.js";
|
|
4
|
+
|
|
5
|
+
export type ScopeRangeKey = `r:${number}:${number}`;
|
|
6
|
+
export type ScopeKey = typeof SCOPE_FULL | ScopeRangeKey;
|
|
7
|
+
|
|
8
|
+
export interface ScopeTrust {
|
|
9
|
+
hash: string;
|
|
10
|
+
seq: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ReadCacheMode = "full" | "unchanged" | "unchanged_range" | "diff" | "full_fallback";
|
|
14
|
+
|
|
15
|
+
export type ReadCacheDebugReason =
|
|
16
|
+
| "no_base_hash"
|
|
17
|
+
| "hash_match"
|
|
18
|
+
| "base_object_missing"
|
|
19
|
+
| "range_slice_unchanged"
|
|
20
|
+
| "range_slice_changed"
|
|
21
|
+
| "diff_file_too_large_bytes"
|
|
22
|
+
| "diff_file_too_large_lines"
|
|
23
|
+
| "diff_unavailable_or_empty"
|
|
24
|
+
| "diff_not_useful"
|
|
25
|
+
| "diff_payload_truncated"
|
|
26
|
+
| "diff_emitted";
|
|
27
|
+
|
|
28
|
+
export interface ReadCacheDebugV1 {
|
|
29
|
+
reason: ReadCacheDebugReason;
|
|
30
|
+
scope: "full" | "range";
|
|
31
|
+
baseHashFound: boolean;
|
|
32
|
+
diffAttempted: boolean;
|
|
33
|
+
outsideRangeChanged?: boolean;
|
|
34
|
+
baseObjectFound?: boolean;
|
|
35
|
+
largestBytes?: number;
|
|
36
|
+
maxLines?: number;
|
|
37
|
+
diffBytes?: number;
|
|
38
|
+
diffChangedLines?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ReadCacheMetaV1 {
|
|
42
|
+
v: 1;
|
|
43
|
+
pathKey: string;
|
|
44
|
+
scopeKey: ScopeKey;
|
|
45
|
+
servedHash: string;
|
|
46
|
+
baseHash?: string;
|
|
47
|
+
mode: ReadCacheMode;
|
|
48
|
+
totalLines: number;
|
|
49
|
+
rangeStart: number;
|
|
50
|
+
rangeEnd: number;
|
|
51
|
+
bytes: number;
|
|
52
|
+
debug?: ReadCacheDebugV1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ReadCacheInvalidationV1 {
|
|
56
|
+
v: 1;
|
|
57
|
+
kind: "invalidate";
|
|
58
|
+
pathKey: string;
|
|
59
|
+
scopeKey: ScopeKey;
|
|
60
|
+
at: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ReadKnowledgeEvent {
|
|
64
|
+
kind: "read";
|
|
65
|
+
pathKey: string;
|
|
66
|
+
scopeKey: ScopeKey;
|
|
67
|
+
servedHash: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ReadInvalidationEvent {
|
|
71
|
+
kind: "invalidate";
|
|
72
|
+
pathKey: string;
|
|
73
|
+
scopeKey: ScopeKey;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ReplayEvent = ReadKnowledgeEvent | ReadInvalidationEvent;
|
|
77
|
+
|
|
78
|
+
export type KnowledgeMap = Map<string, Map<ScopeKey, ScopeTrust>>;
|
|
79
|
+
|
|
80
|
+
export interface NormalizedReadRequest {
|
|
81
|
+
inputPath: string;
|
|
82
|
+
absolutePath: string;
|
|
83
|
+
pathKey: string;
|
|
84
|
+
offset?: number;
|
|
85
|
+
limit?: number;
|
|
86
|
+
start: number;
|
|
87
|
+
end: number;
|
|
88
|
+
totalLines: number;
|
|
89
|
+
scopeKey: ScopeKey;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ReadToolDetailsExt extends ReadToolDetails {
|
|
93
|
+
readcache?: ReadCacheMetaV1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ExtractedReplayData {
|
|
97
|
+
entry: SessionEntry;
|
|
98
|
+
read?: ReadCacheMetaV1;
|
|
99
|
+
invalidation?: ReadCacheInvalidationV1;
|
|
100
|
+
}
|