Repository navigation
fix(runtime): count Edit recovery failures by canonical file path - #1452
Conversation
Path aliases must share the existing edit recovery budget and successful mutation reset. Resolve bookkeeping identity before tool execution while preserving the original Host arguments and delegate ownership boundaries. Cover filesystem aliases, temporary-session roots, resets and independent files with runtime regressions, and document the lexical fallback.
muzimu217
left a comment
There was a problem hiding this comment.
Verified locally on macOS (arm64, current main 278e929ca + this branch).
The mechanism is correct and the design is careful. Pre-mutation identity via realpath with a documented lexical-absolute fallback for missing targets, one key shared across failure counts, per-code grace and success reset; the submitted path is never rewritten and host authority is untouched — the assertion that the exact alias sequence reaches tools.execute unmodified is a nice touch. The REM-through-link identity-clearing test covers the subtlest edge in this design. Results: vitest run src/mutation-recovery.test.ts src/runtime.test.ts → 2 failed | 297 passed; runtime.test.ts is fully green at 296/296 including all eight new alias tests. CI green on Linux.
Two new unit tests in mutation-recovery.test.ts fail on macOS. CI is ubuntu-only, so this won't surface there — but pnpm --filter @pi-desktop/agent-runtime test fails on a stock macOS checkout:
keeps existing case-distinct files independent— macOSmkdtempunder/var/folderssits on case-insensitive APFS, soa.tsandA.tsare the same file; both spellings realpath identically, which is correct per your own contract ("filesystem-supported case aliases share the budget"). The guard needs a runtime case-sensitivity probe (writea.ts, statA.ts), notskipIf(process.platform === "win32").preserves POSIX backslashes in file names—/varis a symlink to/private/var, sorealpathreturns the/privateprefix while the expectation uses lexicaljoin(root, …). Compare againstawait realpath(root)instead.
Related implementation nit, non-blocking: under a symlinked root, a target that doesn't exist keys on the lexical absolute path (/var/...) while the same target after creation keys on realpath (/private/var/...) — the budget splits across the creation boundary. Fine for bookkeeping, but worth one sentence in the new contract paragraph.
Another non-blocking: mutationFailureKey rethrows unexpected errno codes; since the key is bookkeeping-only and computed before the tool runs, treating unknown codes as the lexical fallback would be strictly safer than rejecting before execution.
Docs: this PR and #1451 both touch 18-line-anchored-edit-contract.md and 04-e2e-test-plan.md — whoever merges second will need a rebase.
fix scope, fits the current contribution window; runtime-side changes get a clean pass from me once the two macOS test assumptions are fixed.
Keep the published repair branch current without rewriting its history.
Test actual directory case sensitivity rather than OS labels, and use canonical temporary roots when asserting POSIX filenames. This removes macOS assumptions while retaining assertions on both filesystem modes. Document that lexical fallback counters need not survive a later transition to canonical identity.
|
Thanks—both test assumptions are fixed in Independent review passed. Runtime/helper tests: 299 passed, one POSIX-only skip on Windows; Linux case-sensitive and symlink-root checks, typecheck and all three delegate E2E modes pass. Unexpected-error propagation remains unchanged. I haven't run macOS locally—could you please recheck there? |
muzimu217
left a comment
There was a problem hiding this comment.
Confirmed on macOS (arm64): vitest run mutation-recovery runtime passes 351/351 across 6 files on 5f8dfac9f. The runtime case probe with both branches asserted is the right shape (it now also proves the case-insensitive path intentionally shares one identity), and the realpath(root) expectation fixes the backslash test. Nothing blocking from my side.
…/pr1452-edit-identity-landing
|
Reviewed and merged. The failure-count key now follows the filesystem canonical path for existing targets, with a lexical fallback for missing or inaccessible targets; the original path still reaches Host unchanged. Candidate validation passed: agent-runtime tests (1,315), typecheck, subagent Edit isolation E2E, host-core Edit tests (15), and GitHub CI (all 5 checks). The PR integration merge-ref tree matched the tested candidate. |
Problem
Edit recovery tracks failures by path spelling rather than file identity. Changing the spelling of the same file can grant separate failure budgets and per-error-code grace. A successful Edit or Write through another alias also fails to clear the original budget.
For example, three
EDIT_PARSE_FAILEDresults for these paths currently do not trigger the three-failure limit:src/example.tssrc/./example.tsEach spelling receives its own counter. This allows repeated unsuccessful edits to continue beyond the intended limit. Conversely, stale counts can cause premature termination after a successful mutation through another alias.
Cause and fix
The old key only replaces backslashes and removes a leading
./; it does not resolve workspace-relative paths or filesystem aliases.Resolve a bookkeeping identity before the mutation runs:
realpathwhen the target exists.Capturing the identity before execution also allows a successful removal to clear the original counter after the file disappears.
The original tool arguments still go to the Host, which remains authoritative for permissions and execution. Parent-turn and delegate-run budgets remain isolated. Canonical paths are not lowercased, preserving distinct files on case-sensitive filesystems.
Known unavailable-path errors fall back to normalized absolute spelling; unexpected resolution errors propagate. Missing paths behind directory links and Host automatic path rebinding remain outside canonical identity unification.
Validation
The recovery contract and E2E scenarios are updated. The failure threshold, Host permissions, path mutex and parent/delegate isolation contracts are unchanged.
macOS review follow-up
The helper tests now probe actual directory case sensitivity and use a canonical root for POSIX backslash filenames. The contract documents the lexical-to-canonical transition after creation. Follow-up runtime/helper tests: 299 passed, one POSIX-only skip on Windows. Linux checks cover the case-sensitive branch and a backslash filename beneath a symlinked root; build/typecheck and all three delegate E2E modes pass. Independent follow-up review passed. macOS has not been rerun locally; reviewer revalidation was requested. Validation applies to 5f8dfac.