Skip to content

apply_changes: revision-guarded structured multi-file edits (0.5.0 program) #65

Description

@cf-pages

Targeted: 0.5.0. Baseline: release/0.3.0. This is the main feature of the reliability release. All design decisions are now resolved (maintainer, 2026-08-13); the previously open ones are recorded at the end.

Shape. A structured sibling to apply_patch, sharing the staging/commit kernel (patching.py's FileBaseline + atomic multi-file committer):

  • Ops: create / write / edit / delete / move / copy. write is an upsert (revision required when the target exists) and replaces the replace_all-as-edit special case; create asserts non-existence and takes no revision.
  • edit uses whole-line semantics: content is split on \n and replaces [start_line, end_line]; content: "" is special-cased to zero lines (range deletion); both insert_after (0..total) and insert_before (1..total+1) exist, boundary values documented in the schema.
  • revision = full-file sha256, required for edit/delete/move/copy. Escape hatch is a server start flag (--allow-unversioned-changes), never a wire value. apply_patch does not gain revision: hunk context is content-level stale protection and better at it; the two entrances share result shape (affected_files with previous_revision/revision) but not the protection mechanism — documented explicitly.
  • read_file computes the hash from the same buffer it decodes (removes the TOCTOU between hash and content; also removes the separate binary-sniff open), returns revision in payload and in the model-visible line view. MAX_REVISION_BYTES ~8 MB; oversized files get revision: null.
  • Committer: StagedFile gains explicit action: write | delete | verify (copy must validate its source baseline without writing it), replacing the implicit content is None encoding.
  • Same-path handling (amended after field evidence). The planner is strictly declarative; true chaining (an op reading another op's intermediate state) is removed. But the apply_patch frontend gains a stateless duplicate-Update normalizer: multiple *** Update File: blocks for one resolved path are concatenated into a single block before planning. The engine already anchors every hunk against the original content and rejects overlaps, so the merged block gets exactly the declarative semantics for free. Any same-path duplication involving Add/Delete, or two blocks with contradictory *** Move to:, is rejected with PATCH_PATH_CONFLICT; a block written against another block's intermediate state simply fails to anchor and reports PATCH_CONTEXT_NOT_FOUND (indistinguishable from stale context, same recovery). Normalizations are counted in telemetry. apply_changes itself stays strict: one path per call.
    Evidence: OpenAI-family models routinely emit independent duplicate Update blocks (apply_patch rejects multiple update blocks for the same file, which openai models try to do a lot microsoft/vscode#329812, ~daily); Codex executed Delete → Add chains whose merged report hid the delete — an audit hole (apply_patch can delete without visible Guardian review and report Delete+Add as Add-only openai/codex#34515); Aider forbids duplicates in its prompt but merges them in its parser; gpt-oss's reference parser rejects them outright. Tolerant-but-principled beats both extremes.
  • Limits: 100 changes × 100 edits as sanity caps; the real bound is MAX_HTTP_REQUEST_BYTES (1 MiB → HTTP 413), documented. Empty changes behaves like apply_patch's "No files were modified". Path validation (reject_write_symlink + resolve_for_write) runs in the planner before staging, and same-path identity is judged on resolved paths.
  • Catalog goes 18 → 19: REQUIRED_TOOLS, schema-drift, contract, READMEs all move together.

Resolved decisions:

  1. revision required for edit/delete/move/copy (create takes none); escape hatch is a server start flag only.
  2. Same-path chaining removed; stateless duplicate-Update normalization at the apply_patch frontend as specified above.
  3. Structured-only workspace enforcement dropped — see Workspace mutation reporting: honest enforcement fields, drop the dead write_generated_or_ignored enum #66.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions