Feat/patch stream primitive#4
Open
clintberry wants to merge 10 commits into
Open
Conversation
…imitive Foundational primitive (#1 from the changes-tab ideation): a parallel event stream + dedicated table that captures every change a session produces, with explicit supersession links and per-turn lifecycle. Origin doc derives from the brainstorm; plan derives from the origin and resolves plan-time decisions on producer wiring, broadcast payload shape, and message anchoring.
Creates the patches table with session_id FK (cascade), nullable producing_message_id and parent_patch_id FKs (set null), origin_type text-with-check constraint, workspace_sha + nullable committed_sha, hunks JSONB, and derived file_count / hunk_count for cheap marker rendering. Indexes: (session_id, created_at DESC) for per-session list; partial index on parent_patch_id for supersession-chain traversal.
Three queries: CreatePatch (insert all columns, RETURNING *), ListPatchesBySession (filter by session, order newest-first, with limit), and GetPatchBySessionAndID — a two-column lookup that requires both session_id and id to match, closing the cross-session ID-oracle that a bare id lookup would otherwise create on the upcoming GetPatch endpoint.
GET /api/sessions/{id}/patches returns slim patchSummary (no hunks)
ordered newest-first; GET /api/sessions/{id}/patches/{patchID} returns
the full patchResponse including hunks JSONB. The list shape is also
what the patch_created WebSocket broadcast will carry, so consumers can
render the marker without a follow-up fetch.
Adds TypePatchCreated = 'patch_created' to ws/events.go for the
broadcast event type (producer wiring lands with U5).
Resolves A1 from doc review. Plan-time decision: emit a patch for any non-empty diff at agent turn end, regardless of isError. Set failed_mid_turn = true when the agent reported an error so the audit log preserves what the agent actually did before failing, without attributing those edits to the next successful turn's diff. The wire shape now carries failedMidTurn on patchSummary so consumers (WebSocket subscribers and the future Changes view) can render the flag without an extra fetch.
Extracts the host-FS workspace path resolver out of handler/files.go into a new internal/workspacepath package so multiple callers can share it without import cycles. handler/files.go's local helper now thinly delegates to it; DEVPOD_AGENT_CONTENT_DIR still overrides. Adds internal/workspacegit with CaptureHead and DiffSince. CaptureHead runs git rev-parse HEAD against the workspace bind-mount path so the patch primitive has a stable anchor at agent turn start. DiffSince runs git diff --no-color --unified=3 <sha> and parses the unified-diff output into a structured FileHunks/Hunk shape, returning convenient file_count and hunk_count so callers don't reparse JSONB at render time. Tests cover the parser (empty, single hunk, multi-file, add/delete, single-line hunk header, contextual header, hunk content) and the git invocations against a real ephemeral repo (HEAD capture, missing workspace surfaces ErrWorkspaceNotFound, non-git directory errors, diff with no changes, modified file, added file).
Wires the patch primitive into the existing agent completion path: - executeAgent captures workspace HEAD via workspacegit.CaptureHead immediately before executor.Execute. Failure here logs and proceeds with patch emission disabled for the turn. - finishAgent now returns the persisted agent message so its ID can be used as producing_message_id on the patch. - After finishAgent, executeAgent calls emitPatchForTurn which runs workspacegit.DiffSince against the captured SHA, skips emission on empty diff (R6), persists the patch row, and broadcasts the patch_created event over the existing per-session WS subscription. - The error path emits patches the same way, with failed_mid_turn=true, so partial work an agent committed before failing is preserved in the audit log instead of leaking into the next turn's diff (A1). - parent_patch_id is always null in v0; the producer-set supersession hint lands with the intent-only-edits brainstorm (AE3 deferred). - Emission failures are logged but do not propagate — the agent reply is already persisted and broadcast by the time emission runs.
…ain (U6) Backfills explicit IDs (60... prefix) on the auth-module session's seed messages in 002_seed_data.sql so 008_seed_patches can anchor producing_message_id deterministically. Adds three seed patches: - 5...a1: anchored to message a3 (Coder, 3h ago), parent null - 5...a2: anchored to message a5 (Coder, 1h30m ago), supersedes a1 - 5...a3: anchored to message a7 (Tester, 45m ago), parent null The supersession chain (a1 -> a2) exercises the v1->v2 collapse path the brainstorm's idea #3 (re-prompt -> supersede) will eventually render. Hunks are compact unified-diff content with no embedded tabs (Postgres JSONB rejects raw control chars in string values). 008 uses SELECT-INSERT guarded by WHERE EXISTS on the FK targets so it's safe to run on a DB whose 002 seed sessions never loaded — the inserts silently skip instead of failing the migration.
- types: add Patch, PatchOrigin, PatchHunk, PatchFile mirroring the backend wire shape. Slim and full payloads share one type via an optional hunks field. - api: add api.listPatches and api.getPatch. - store: add patches Record<sessionId, Patch[]> with addPatch (id dedup matching the addMessage/addActivity pattern) and setPatches. Lazy-load on setActiveSession the same way messages and activities load. - ws: handle patch_created with the same snake/camel normalization recipe used by new_message. Also schedules a debounced files refresh since a new patch implies file changes the FilesView should pick up.
PatchMarker is the v0 read-only chat-surface marker that confirms a patch landed: file/hunk counts, an origin badge in the producing agent's role color, a supersession caption when parent_patch_id is set, and a 'failed mid-turn' warning when the agent reported an error mid-execution. No click behavior, no expand/collapse, no hover detail — those belong to the future Changes view per plan Scope Boundaries. A11y: every marker carries a single aria-label that names origin, counts, supersession, and failure state in one phrase so screen readers get full context without relying on color or icon (D4 from doc review). ChatView indexes patches by producing_message_id and renders any matching markers immediately after the producing MessageBubble. Patches with null producing_message_id are not rendered in v0 — the only v0 producer (agent completion path) always sets the link, and the orphan-marker fallback was scope-creep per SG4. The supersession label is the plain phrase 'supersedes earlier change' with no version number, honoring D6's call against deriving version state the schema doesn't carry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is idea #1 from docs/ideation/2026-05-14-changes-tab-vs-editor-ideation.md. The requirements doc is at docs/brainstorms/2026-05-14-patch-stream-primitive-requirements.md and the implementation plan at docs/plans/2026-05-14-001-feat-patch-stream-primitive-plan.md.
What landed
Backend (Go):
Frontend (React + Zustand):
Notable plan-time decisions (origin: brainstorm + doc-review findings)
What this PR explicitly does NOT do
Each item below is named in Scope Boundaries and tracked for a separate brainstorm:
Commits
```
$COMMITS
```
Test plan
🤖 Generated with Claude Code
EOF
echo "PR body saved to /tmp/pr-body.md ($(wc -l < /tmp/pr-body.md) lines)"