Streaming Partial Patches: Apply Validated Hunks as Tokens Arrive

Streaming Partial Patches: Apply Validated Hunks as Tokens Arrive

Waiting for a full unified diff before showing anything is how IDE agents feel slow even when the model is streaming. The unfair advantage is hunk-oriented streaming: parse patch frames as tokens arrive, validate each hunk against the live tree, and apply only ranges that still match — so time-to-first-diff collapses without accepting stale or overlapping edits.

⚡ TL;DR: Stream NDJSON hunk frames (path, oldStart, oldLines, newLines, hash). Validate context lines + file SHA before apply; skip or re-request failed hunks. Never buffer the whole answer then git apply. Pair with LLM Output Validators and AST-Guided Edits when mechanical transforms beat freeform hunks.

Frame the stream as typed hunks

// patch/stream-frames.ts
export type HunkFrame = {
  path: string;
  oldStart: number;
  oldLines: string[];   // exact context expected on disk
  newLines: string[];
  baseSha: string;      // blob hash at plan time
  seq: number;
};

export function parseFrame(line: string): HunkFrame | null {
  try {
    const f = JSON.parse(line) as HunkFrame;
    if (!f.path || !Array.isArray(f.oldLines) || !Array.isArray(f.newLines)) return null;
    return f;
  } catch {
    return null; // ✅ ignore incomplete SSE chunks
  }
}

❌ Treating raw @@ markdown fences as trusted once the stream ends — by then the file may have changed under the cursor.

Validate before every apply

import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";

export function validateHunk(f: HunkFrame): "ok" | "stale" | "mismatch" {
  const disk = readFileSync(f.path, "utf8");
  const sha = createHash("sha1").update(disk).digest("hex");
  if (sha !== f.baseSha) return "stale";

  const lines = disk.split("\n");
  const slice = lines.slice(f.oldStart - 1, f.oldStart - 1 + f.oldLines.length);
  // ✅ Exact context match — fuzzy apply is how you corrupt JSX
  if (slice.join("\n") !== f.oldLines.join("\n")) return "mismatch";
  return "ok";
}

export function applyHunk(f: HunkFrame) {
  if (validateHunk(f) !== "ok") throw new Error(`hunk_rejected:${f.path}:${f.seq}`);
  const lines = readFileSync(f.path, "utf8").split("\n");
  lines.splice(f.oldStart - 1, f.oldLines.length, ...f.newLines);
  // write atomically via tmp+rename in production
}

Pipeline: parse → validate → apply → UI paint

// agent/stream-apply.ts — illustrative Node consumer
async function consumePatchStream(readable: AsyncIterable<string>) {
  let buf = "";
  for await (const chunk of readable) {
    buf += chunk;
    const parts = buf.split("\n");
    buf = parts.pop() ?? "";
    for (const line of parts) {
      const frame = parseFrame(line);
      if (!frame) continue;
      const v = validateHunk(frame);
      if (v === "ok") {
        applyHunk(frame);
        ui.paintDiff(frame); // ✅ first paint in <1s typical
      } else {
        // ❌ Don't invent offsets — re-request this path only
        await agent.rerequestHunk(frame.path, frame.seq, reason: v);
      }
    }
  }
}

Conflict and abort rules

Event Action
User typed in same file Mark baseSha stale; pause apply
Hunk overlap Serialize by seq; reject later overlapping
Client abort Keep applied hunks; write undo journal
Parse fail mid-frame Drop incomplete; wait for next newline

Same abort discipline as Lambda Plus Bedrock token streaming — never buffer the whole completion in memory.

Closing checklist

✅ Dos
– ✅ Stream NDJSON hunk frames with base blob SHA
– ✅ Exact context match before splice
– ✅ Paint UI on first valid hunk
– ✅ Re-request only failed paths
– ✅ Keep an undo journal per session

❌ Don’ts
– ❌ Don’t git apply a mega-diff after the stream ends
– ❌ Don’t fuzzy-match context “close enough”
– ❌ Don’t apply overlapping hunks out of order
– ❌ Don’t ignore user edits mid-stream
– ❌ Don’t skip parse/typecheck after the last hunk

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply