@yagni-app/code-staging 1.1.4-staging.1386.1 → 1.1.4-staging.1387.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.
|
@@ -28,6 +28,7 @@ import * as os from "node:os";
|
|
|
28
28
|
import * as path from "node:path";
|
|
29
29
|
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, getAgentDir, getLanguageFromPath, highlightCode, renderDiff, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
30
30
|
import { Container, Text } from "@earendil-works/pi-tui";
|
|
31
|
+
import { logEvent } from "./errorSink.js";
|
|
31
32
|
import { ReadStateTracker, READ_FIRST_ERROR, readIsPartial, readTextOf, STALE_ERROR } from "./fileGuards.js";
|
|
32
33
|
import { isQuiet, kindForTool, ToolRunTracker } from "./toolRuns.js";
|
|
33
34
|
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
@@ -388,8 +389,23 @@ export function registerCondensedTools(pi, deps = {}) {
|
|
|
388
389
|
// nothing — its content is error text, not the file — and must not
|
|
389
390
|
// clobber a prior good record of the same path.
|
|
390
391
|
if (name === "read" && target && !isErrorResult(result)) {
|
|
391
|
-
|
|
392
|
-
|
|
392
|
+
// An image read carries an image part whose "content" is a
|
|
393
|
+
// processing note, not the file — never record it (Claude Code
|
|
394
|
+
// never stores image reads in readFileState either). The skip
|
|
395
|
+
// rides the trail so a later read-first refusal on an image-read
|
|
396
|
+
// path is diagnosable instead of confusing.
|
|
397
|
+
if (hasImages(result)) {
|
|
398
|
+
logEvent({
|
|
399
|
+
source: "condensedTools",
|
|
400
|
+
level: "info",
|
|
401
|
+
event: "read_record_skipped_image",
|
|
402
|
+
fields: {},
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const partial = readIsPartial({ path: args.path, offset: args.offset, limit: args.limit }, result.details ?? undefined);
|
|
407
|
+
await readState.recordRead(target, cwd, readTextOf(result), partial);
|
|
408
|
+
}
|
|
393
409
|
}
|
|
394
410
|
else if (name === "write" && target && typeof args.content === "string" && !isErrorResult(result)) {
|
|
395
411
|
await readState.recordWrite(target, cwd, args.content);
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* model has not read in this session, and refuse again when the file
|
|
10
10
|
* changed on disk since that read (mtime, with a content-compare fallback
|
|
11
11
|
* so a spurious timestamp bump — cloud sync, antivirus, `utimes` noise —
|
|
12
|
-
* does not produce a false refusal on a full read). New-file creates
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* does not produce a false refusal on a full read). New-file creates
|
|
13
|
+
* follow Claude Code exactly: a create needs no prior read. ANY
|
|
14
|
+
* successful read — windowed (offset/limit) or truncated — satisfies
|
|
15
|
+
* read-first, matching Claude Code, whose Read tool never marks a
|
|
16
|
+
* partial view (its `isPartialView` exists only for auto-injected memory
|
|
17
|
+
* content that differs from disk, a path this tracker does not record).
|
|
18
|
+
* The `partial` classification instead selects the staleness strategy:
|
|
19
|
+
* a full read's record holds the whole file, so a bumped mtime can be
|
|
20
|
+
* content-compared before refusing; a partial record holds only a slice
|
|
21
|
+
* (with pi's continuation notice appended), so a bumped mtime refuses
|
|
22
|
+
* outright — Claude Code's exact stance for offset/limit reads.
|
|
16
23
|
*
|
|
17
24
|
* Fail-open contract: any guard-internal filesystem error that is not one
|
|
18
25
|
* of the two refusals (EACCES, EMFILE, a racing delete, ...) delegates
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* model has not read in this session, and refuse again when the file
|
|
10
10
|
* changed on disk since that read (mtime, with a content-compare fallback
|
|
11
11
|
* so a spurious timestamp bump — cloud sync, antivirus, `utimes` noise —
|
|
12
|
-
* does not produce a false refusal on a full read). New-file creates
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* does not produce a false refusal on a full read). New-file creates
|
|
13
|
+
* follow Claude Code exactly: a create needs no prior read. ANY
|
|
14
|
+
* successful read — windowed (offset/limit) or truncated — satisfies
|
|
15
|
+
* read-first, matching Claude Code, whose Read tool never marks a
|
|
16
|
+
* partial view (its `isPartialView` exists only for auto-injected memory
|
|
17
|
+
* content that differs from disk, a path this tracker does not record).
|
|
18
|
+
* The `partial` classification instead selects the staleness strategy:
|
|
19
|
+
* a full read's record holds the whole file, so a bumped mtime can be
|
|
20
|
+
* content-compared before refusing; a partial record holds only a slice
|
|
21
|
+
* (with pi's continuation notice appended), so a bumped mtime refuses
|
|
22
|
+
* outright — Claude Code's exact stance for offset/limit reads.
|
|
16
23
|
*
|
|
17
24
|
* Fail-open contract: any guard-internal filesystem error that is not one
|
|
18
25
|
* of the two refusals (EACCES, EMFILE, a racing delete, ...) delegates
|
|
@@ -167,38 +174,43 @@ export class ReadStateTracker {
|
|
|
167
174
|
throw error; // unknown fs failure — the wrapper fails open
|
|
168
175
|
}
|
|
169
176
|
const record = this.reads.get(key);
|
|
170
|
-
if (!record
|
|
177
|
+
if (!record) {
|
|
171
178
|
throw new Error(READ_FIRST_ERROR);
|
|
172
179
|
}
|
|
173
|
-
if (Math.floor(mtimeMs)
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
180
|
+
if (Math.floor(mtimeMs) <= record.mtimeMs)
|
|
181
|
+
return; // not stale
|
|
182
|
+
// The timestamp moved. A partial record holds only a slice (plus pi's
|
|
183
|
+
// continuation notice), so it cannot be content-compared — refuse
|
|
184
|
+
// outright, Claude Code's stance for offset/limit reads.
|
|
185
|
+
if (record.partial) {
|
|
186
|
+
throw new Error(STALE_ERROR);
|
|
187
|
+
}
|
|
188
|
+
// For a full read, compare content before refusing — Claude Code's exact
|
|
189
|
+
// fallback: on some platforms (cloud sync, antivirus) the timestamp moves
|
|
190
|
+
// without content changes. The record holds the read tool's exact output
|
|
191
|
+
// and pi returns file bytes verbatim, so identical content compares equal.
|
|
192
|
+
let onDisk;
|
|
193
|
+
try {
|
|
194
|
+
onDisk = await (this.io.readFile ?? readTextFile)(key);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
// Unreadable now, but the read succeeded then — stale — but the real
|
|
198
|
+
// cause (EACCES/EIO on the re-read, not an edit) rides the trail so
|
|
199
|
+
// the refusal is not misdiagnosed as a content change.
|
|
200
|
+
logEvent({
|
|
201
|
+
source: "fileGuards",
|
|
202
|
+
level: "info",
|
|
203
|
+
event: "staleness_reread_failed",
|
|
204
|
+
fields: { code: error?.code ?? "unknown" },
|
|
205
|
+
});
|
|
206
|
+
throw new Error(STALE_ERROR);
|
|
207
|
+
}
|
|
208
|
+
if (onDisk !== record.content) {
|
|
209
|
+
throw new Error(STALE_ERROR);
|
|
201
210
|
}
|
|
211
|
+
// Content-identical: refresh the record so the next write's mtime
|
|
212
|
+
// comparison uses the bumped timestamp instead of refusing again.
|
|
213
|
+
this.reads.set(key, { ...record, mtimeMs: Math.floor(mtimeMs) });
|
|
202
214
|
}
|
|
203
215
|
isInside(absolute, base) {
|
|
204
216
|
const slashed = (p) => p.replace(/\\/g, "/");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.4-staging.
|
|
3
|
+
"version": "1.1.4-staging.1387.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "e8e7f5d88707ad223e9cbeb16216b80239af3224"
|
|
62
62
|
}
|