Skip to content

feat(relayfile): language-agnostic three-way line merge (Phase 5)#375

Merged
khaliqgant merged 3 commits into
mainfrom
goal-b/phase-5-language-agnostic-merge
Jul 25, 2026
Merged

feat(relayfile): language-agnostic three-way line merge (Phase 5)#375
khaliqgant merged 3 commits into
mainfrom
goal-b/phase-5-language-agnostic-merge

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary

Phase 5 of the goal-B real-time multi-agent collaboration work (follows #370-#374). The user pushed back on Phase 3/4's design directly: "why does this need to be language specific?" — correctly. AST-based symbol-level merge (go-top-level-functions-v1) only works for Go because it needs a Go parser, and there's no equivalent mature parser for TypeScript/Python/etc. in Go — every option investigated (vendoring esbuild's internal Go parser, a Node sidecar around the real TypeScript compiler, tree-sitter+CGo) was a real new dependency or architecture change, and only solved one language.

This adds MergeStrategyThreeWayLines (three-way-lines-v1) — a diff3-style three-way line merge, the same class of algorithm git and every VCS use. It operates on raw lines, not any language's AST, so it works identically for TypeScript, Python, JSON, Markdown, or anything else, with zero new dependencies.

The algorithm

internal/relayfile/merge_lines.go: a bounded (maxMergeDiffLines = 3000 lines/side) full LCS DP table (memory-safe by construction — ~36MB worst case — rather than needing the more involved Hirschberg linear-space technique). Resolution walks "sync points" (base lines unchanged in both mine and theirs) and between them takes whichever side actually changed something relative to base; if both changed it identically, that's not a conflict either — only a genuine divergence (both sides changed the same span to different content) conflicts. Same all-or-nothing safety invariant as the Go strategy: any conflict means nothing is written, ever.

Store.MergeFile now dispatches on req.Strategy: the Go-AST path is completely unchanged for go-top-level-functions-v1; the new strategy has no file-extension restriction, bounded only by content size/line count.

Also fixes two latent bugs the second strategy exposed: commitMergeResult and findExistingMergeOpLocked both unconditionally reported MergeStrategyGoTopLevelFunctions in their MergeResponse regardless of which strategy was actually used — harmless while only one strategy existed, wrong the moment a second one did.

Mount rollout

mountRolloutMergeEligible is broadened from ".go files only" to any text-like file (isTextLikeContentType(detectContentType(remotePath))) — TypeScript, Python, JSON, Markdown, extensionless files, etc. — while still excluding binary content.

Found along the way: Go's stdlib MIME registry maps .ts to video/mp2t (MPEG transport stream), not TypeScript — which would have made every .ts file classified as binary and therefore ineligible, exactly backwards from this phase's goal. detectContentType now special-cases .tstext/typescript, the same way it already special-cases .md/.markdown.

Tests

  • 15 pure-function tests for the line-merge algorithm (disjoint/same-line/identical-change/insertion/deletion/same-position-insertion-conflict/no-trailing-newline/oversized-input) — written alongside the algorithm, not after.
  • 6 store-integration tests (end-to-end success, conflict-leaves-file-unchanged, unverifiable-base, oversized-content, no-file-extension-restriction, ContentIdentity-dedup-reports-correct-strategy) — deliberately exercised through a .ts file throughout to prove the "any language" claim concretely, not just in prose.
  • TestMountRolloutMergeEligible: table test proving .ts/.py/.json/extensionless-README are now eligible, .png/.zip correctly remain ineligible.
  • TestMountRolloutConflictPathAutoMergesTypeScriptFile: the TypeScript sibling of the existing Go integration test, through the real syncer entrypoints.

Full suite green: go build/vet/test ./... (including -race on new and existing merge/mount-rollout tests), TS SDK build/typecheck/233 tests, scripts/check-contract-surface.sh.

Split

Core algorithm + store dispatch: written directly (safety-critical, novel). Mountsync eligibility broadening + HTTP/OpenAPI/SDK touch-ups + test coverage: built by Codex against the store-layer work, reviewed and independently re-validated end to end before merging into this branch.

Test plan

  • go build ./..., go vet ./..., go test ./...
  • New merge/mount-rollout tests at -race
  • TS SDK build/typecheck/tests, contract-surface check
  • Live cross-machine proof with an actual .ts file through the real mount daemon on two physical machines (to follow before merge, per this initiative's standing verification requirement)

Review in cubic

…nes-v1)

Phase 5 of goal-B: the user pushed back on Phase 3/4's Go-only symbol-level
merge — "why does this need to be language specific?" — and they're right.
AST-based merge was one way to get symbol-level auto-merge, not the only
way, and it doesn't generalize (every language needs its own parser; there
is no mature pure-Go TypeScript parser, and every option investigated —
vendoring esbuild's internal Go parser, a Node sidecar around tsc, tree-
sitter+CGo — was a real new dependency or architecture change for a single
language, with no path to covering Python/JSON/Markdown/etc. either).

MergeStrategyThreeWayLines (merge_lines.go) is the standard diff3-style
three-way merge — the same algorithm git and every other VCS use — over
raw lines, not any language's AST. It works identically for TypeScript,
Python, JSON, Markdown, or anything else, with zero new dependencies: a
bounded (maxMergeDiffLines=3000/side) full LCS DP table, memory-safe by
construction rather than by the more involved Hirschberg linear-space
technique, since the bound keeps the naive table under ~36MB even at its
limit. Resolution walks "sync points" (base lines unchanged in both mine
and theirs) and between them takes whichever side actually changed
something relative to base, or — if both changed it identically — either;
only a genuine divergence (both changed the same span to different
content) is a conflict. Same all-or-nothing safety invariant as the Go
strategy: any conflict means nothing is written.

Store.MergeFile now dispatches on req.Strategy: existing Go-AST path
unchanged for go-top-level-functions-v1, new line-based path for
three-way-lines-v1 (no separate extraction step — it works directly on the
raw base/mine/theirs strings). Also fixes two latent bugs the second
strategy exposed: commitMergeResult and findExistingMergeOpLocked both
unconditionally reported MergeStrategyGoTopLevelFunctions in their
MergeResponse regardless of which strategy was actually used — harmless
while only one strategy existed, wrong the moment a second one did. Both
now report req.Strategy / the strategy stashed in the op's ProviderResult
metadata. New regression test (TestMergeFileDedupedRetryReportsCorrectStrategy)
covers the ContentIdentity dedup path directly, which had no prior test
coverage at all even for the original strategy.

Tests written alongside the algorithm (not after): 15 pure-function cases
covering disjoint/same-line/identical-change/insertion/deletion/same-
position-insertion-conflict/no-trailing-newline/oversized-input, plus 6
store-integration tests (end-to-end success, conflict-leaves-file-
unchanged, unverifiable-base, oversized-content, no-file-extension-
restriction, dedup) — the latter deliberately exercised through a .ts file
throughout to prove the "any language" claim concretely, not just in
prose.

Mountsync's mount-rollout wiring (broadening eligibility beyond .go,
switching the strategy it requests, updating tests) is scoped as a
follow-up commit, split to Codex per the established pattern for this
initiative.
…e strategy

Mount rollout now requests three-way-lines-v1 instead of
go-top-level-functions-v1, and mountRolloutMergeEligible is broadened from
".go files only" to any text-like file (isTextLikeContentType(
detectContentType(remotePath))) — TypeScript, Python, JSON, Markdown,
extensionless files like README, etc. — while still correctly excluding
binary content (images, archives).

Found and fixed a real, independent bug along the way: Go's stdlib MIME
registry maps ".ts" to "video/mp2t" (MPEG transport stream), not
TypeScript — which would have made every .ts file classified as binary
and therefore ineligible for mount rollout, exactly backwards from the
goal of this phase. detectContentType now special-cases ".ts" to
"text/typescript", the same way it already special-cased ".md"/".markdown".

Also updates the two content-type fallbacks that hardcoded "text/x-go"
(inside attemptMountRolloutMerge and HTTPClient.MergeFile) to the neutral
"text/plain", the OpenAPI spec's strategy enum (now lists both merge
strategies), and the TypeScript SDK's MergeFileInput/MergeFileResponse
strategy union type + its own contentType default.

New test coverage: TestMountRolloutMergeEligible (table test proving .ts/
.py/.json/extensionless-README are now eligible, .png/.zip are correctly
still not), and TestMountRolloutConflictPathAutoMergesTypeScriptFile (the
TypeScript sibling of the existing Go integration test, through the real
syncer entrypoints, not a direct unit call). Existing tests' mock
mergeFileFunc strategy assertions updated from MergeStrategyGoTopLevelFunctions
to MergeStrategyThreeWayLines, since that's what mount rollout now sends.

Built by Codex (finn-mac-mini) against the store-layer work from the prior
commit; reviewed and independently re-validated end to end before merging
into this branch: go build/vet/test ./... (including -race on the new and
existing mount-rollout tests), TS SDK build/typecheck/233 tests, and
scripts/check-contract-surface.sh, all green.
@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@khaliqgant, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: caa551a9-fd4b-4a5b-bc50-9e5e35fce7c9

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3a04f and 7d493e6.

📒 Files selected for processing (10)
  • internal/mountsync/mount_rollout_test.go
  • internal/mountsync/syncer.go
  • internal/relayfile/merge.go
  • internal/relayfile/merge_lines.go
  • internal/relayfile/merge_lines_store_test.go
  • internal/relayfile/merge_lines_test.go
  • openapi/relayfile-v1.openapi.yaml
  • packages/sdk/typescript/src/client.test.ts
  • packages/sdk/typescript/src/client.ts
  • packages/sdk/typescript/src/types.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch goal-b/phase-5-language-agnostic-merge

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-07-25T21-47-02-103Z-HEAD-provider
Mode: provider
Git SHA: a8d198b

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa5dbaeaf7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +166 to +170
case mineText == theirsText:
// Includes the common "neither side changed this span" case,
// and "both sides made the identical change" — neither is a
// real conflict.
output = append(output, mineRegion...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve duplicate insertions across ambiguous LCS matches

When repeated lines make the LCS alignment ambiguous, treating equal regions as the same edit can silently discard one side's insertion. For example, with base x\na\ny\n, mine x\na\na\ny\n, and theirs x\nb\na\na\ny\n, the independent insertions should produce x\nb\na\na\na\ny\n (as git merge-file does), but this branch emits only two a lines because both trailing regions contain a\n and are collapsed here. Detect ambiguous alignments or merge explicit edit hunks rather than deduplicating solely by regional text equality.

Useful? React with 👍 / 👎.

Comment thread packages/sdk/typescript/src/client.ts Outdated
Comment on lines +1617 to +1619
// A neutral default matches the server and works for either merge
// strategy when the caller did not provide a more specific type.
contentType: input.contentType ?? "text/plain",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain the Go-specific content type default

For existing go-top-level-functions-v1 callers that omit the optional contentType, this changes the request from text/x-go to text/plain. A successful merge overwrites the stored file's content type with this value and propagates it to provider writeback, so upgrading the SDK silently changes metadata for Go files. Select the default based on the strategy (retaining text/x-go for the existing Go strategy) rather than applying the neutral default to both.

Useful? React with 👍 / 👎.

…contentType regression

- linesThreeWayMerge could silently DROP one side's independent insertion
  when repeated/duplicated lines near an edit made the LCS alignment
  genuinely ambiguous (P1, Codex review). Concrete example: base "x\na\ny\n",
  mine independently inserts a second "a", theirs independently inserts
  "b" AND a second "a" — a single greedy-leftmost alignment can attribute
  both sides' inserted "a" to the same logical gap purely because they're
  textually identical, collapsing two real insertions into one and losing
  content. `git merge-file` resolves this correctly by picking a different,
  non-colliding alignment; rather than replicate that exact heuristic,
  this now computes the merge TWICE — once preferring the earliest valid
  occurrence when a repeated line admits a choice (lcsMatchPreferFirst,
  the existing algorithm), once preferring the latest (new
  lcsMatchPreferLast, backtracking a prefix-LCS table from the end) — and
  only trusts the result when both agree on content and conflict-freeness.
  Disagreement means the alignment was ambiguous, and the safe response is
  a conflict, never a guess — same fail-closed philosophy as every other
  ambiguity case in this file. Roughly doubles the algorithm's cost
  (measured 138ms at the 3000-line bound, worst case) for a rare,
  already-bounded operation. New regression test
  (TestLinesMergeAmbiguousDuplicateInsertionsConflict) reproduces the
  reviewer's exact scenario and confirms it now conflicts instead of
  silently losing content.

- The TypeScript SDK's mergeFile default contentType regressed existing
  go-top-level-functions-v1 callers (P2, Codex review): changing the
  blanket default from "text/x-go" to "text/plain" meant an existing Go
  caller that omits contentType would silently get its stored file's
  content type (and provider-writeback metadata) rewritten on every merge.
  The default is now chosen per-strategy: "text/x-go" for
  go-top-level-functions-v1 (restores prior behavior), "text/plain" for
  three-way-lines-v1 (the correct neutral default for a language-agnostic
  strategy). New test (mergeFile defaults contentType per strategy when
  omitted) locks this in.

Full suite green: go build/vet/test ./... (including -race on the merge
package), TS SDK build/typecheck/234 tests.
@khaliqgant
khaliqgant merged commit 1b4842b into main Jul 25, 2026
10 checks passed
@khaliqgant
khaliqgant deleted the goal-b/phase-5-language-agnostic-merge branch July 25, 2026 21:50
khaliqgant added a commit that referenced this pull request Jul 25, 2026
Phase 5 (language-agnostic three-way line merge, PR #375) landed and
merged since this doc was last updated after Phase 4. Adds gap #6 (the
Go-only merge restriction, fixed), renumbers the CRDT/OT gap, and updates
the verdict/recommendation to reflect all six gaps from the assessment
being closed — no remaining scope gap from the original assessment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant