workspace.commit: a merge conflict with a few hundred hunks is refused by the control-record encoder instead of truncated

オープン 初心者向け
#689 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
82/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
rust
領域
api

調査の方向性

put_report(engine/crates/layerfs-bridge/src/control_merge.rs:212-227)から始め、その hunk カウントを累積的な scan(隣接する put_lines と put_lww が使う)と比較し、layerfs-bridge/src/wire.rs のレコード上限を確認してください。提供された encoder-only の公開 API 経由のテストを engine/crates/layerfs-bridge/tests/ 以下に追加し、cargo test -p layerfs-bridge --all-targets を変更前と変更後に実行してください。完了条件は、600 hunk のレポートが wire 上限内でエンコードされ、truncated = true でデコードし戻り、ドキュメントの blocks に関する主張が修正されるか、注記された判断とともに残されていることです。

索引モデルが issue の本文から書いたものです。

説明

Environment

  • LayerFS 1.1.0, checkout 35d36575c1eb; Rust 1.99.1, macOS arm64
  • The bug is still present on main and layerfs_v2_0_0 as of 2026-11-15
    (engine/crates/layerfs-bridge/src/control_merge.rs is byte-identical to the
    checkout above on both branches)
  • Reproduced on the encoder directly; no daemon, Docker or Store involved

Summary

A Workspace Commit whose Branch head moved returns a Conflict reply carrying every
conflicting hunk. The control wire holds one reply in at most u16::MAX - 16
bytes (layerfs-bridge/src/wire.rs:5-18) and the encoder is supposed to truncate
what does not fit, saying so with truncated (control_merge.rs module docs:
"conflicts and hunk lines are written while they fit and truncated says that some
were left out").

The hunk list is never bounded. put_report counts hunks with

let hunks = conflict.hunks.iter().take_while(|_| fits(out, 64)).count();

(engine/crates/layerfs-bridge/src/control_merge.rs:223). The predicate ignores
its element and out does not change during take_while, so the result is
constant: every hunk is kept whenever any hunk fits. The write loop then emits all
of them, Writer::put returns ControlError("control record limit") once the
record passes the limit, and answer_call
(engine/crates/layerfs-daemon/src/control/serve.rs:97-132) turns that into
ServeCause::Protocol and closes the send half of the connection.

The merge has already run: the conflicted Workspace has marker files installed
("A conflicted Workspace: its local base is the merge with markers",
store/commit_merge.rs). The caller gets a lost connection instead of the conflict
report, and never learns which paths conflicted.

Repro

A unit test on the encoder alone, using only public API, is enough. Drop this in
engine/crates/layerfs-bridge/tests/ and run cargo test -p layerfs-bridge:

use layerfs_bridge::control::{Answer, ConflictReport, Reply};
use layerfs_content::merge::{Conflict, ConflictKind, Hunk};

#[test]
fn large_conflict_report_encodes() {
    let hunk = |i: usize| Hunk {
        line: i * 4 + 1,
        ours: (0..3).map(|k| format!("a{}", "x".repeat(39 - (i + k) % 7))).collect(),
        theirs: (0..3).map(|k| format!("b{}", "x".repeat(39 - (i + k) % 7))).collect(),
    };
    let report = ConflictReport {
        head: None,
        base: None,
        markers: false,
        conflicts: vec![Conflict {
            path: "/project/generated.config".into(),
            kind: ConflictKind::Content,
            hunks: (0..600).map(hunk).collect(),
        }],
        lww: vec![],
        written: vec![],
        truncated: false,
    };
    Answer { id: 7, reply: Reply::Conflict(Box::new(report)) }
        .encode()
        .expect("a valid conflict report must encode");
}

Measured on 35d3657 with the same 40-byte lines, 3 per side:

hunks encoded result
20 4,890 B ok
200 48,466 B ok
300 64,740 B ok
400 — refused: control record limit
600 — refused

The same happens with the more realistic shape of many files: 60 files × 10
hunks each (600 hunks total) is refused.

Input size is well inside the merge's own bound: MAXIMUM_TEXT_BYTES = 1 MiB
(core/crates/layerfs-content/src/compare/text.rs:12), and 400 hunks × 6 lines × 40 B ≈ 96 KiB of text. There is no cap on conflicts or hunks in
layerfs-content/src/merge.

Root cause

engine/crates/layerfs-bridge/src/control_merge.rs:212-227. Every other bounded
list in this file (put_lines, put_lww, the conflicts list) uses a cumulative
scan and stops at the first entry that does not fit; the hunk count alone uses a
constant predicate.

Impact

  • The agent-platform case the project targets — many Agents editing large files
    and merging often — produces exactly this shape: a merge conflicting in a few
    dozen files, or one large generated file with several hundred conflicting
    regions.
  • The Commit side effects have already happened (markers installed); the call
    reports a transport/protocol failure. A retry then hits the marker refusal
    instead, so the original conflict report is lost.
  • The documented contract is truncation, and conflictsTruncated is the flag the
    SDK already exposes (layerfs-api/src/sandbox/values.rs), so callers are
    prepared for a truncated report.

Proposed fix

Replace the constant predicate with the cumulative budget the neighbouring
encoders use:

let hunks = conflict
    .hunks
    .iter()
    .scan(0usize, |size, hunk| {
        *size += 8 + lines_len(&hunk.ours) + lines_len(&hunk.theirs);
        Some(*size)
    })
    .take_while(|size| fits(out, *size))
    .count();

with lines_len summing each line's clipped length plus its two-byte prefix
(the 8 covers the hunk's line number and both list counts). Result with the
patch, same inputs:

input encoded kept truncated
400 / 600 / 1200 hunks 64,426 B 266 hunks true
60 files × 10 hunks 65,226 B 261 hunks true

cargo test -p layerfs-bridge --all-targets passes with the patch, and the
encoded reply decodes back to a Conflict with truncated = true.

One follow-up to decide with the fix: api/docs/agent.md says "Each conflict
keeps its original blocks count", but once the wire truncates, the SDK's
blocks becomes the kept count on the workspace.commit path (the wire does
not carry the original). Either carry the original count or amend that sentence.

Overlap check

Searched open and closed issues and PRs for conflict, truncat,
control record, hunks, merge, written twice, Workspace capacity.
Two adjacent items, both different defects:

  • #355 F-041: the merge report drops its truncated flag for the 4,096 written
    paths in store-service/src/store/commit_merge.rs. This issue is about the
    control encoder refusing an over-limit reply; the flag already exists here.
  • #517: the bounded diff fallback silently merges away a change over ~1023 lines.
    Different layer and different symptom.

No issue or PR mentions put_report, hunk truncation, or control record limit.

Verification standard

For any conflict report, Answer::encode either returns a record within
wire::LIMIT whose decoded truncated flag is true when anything was omitted,
or the input must not be valid. It must not return an error for a report the
merge can produce.

主要言語
Rust
スター
2.1k
フォーク
110
平均マージ
23時間 52分
マージ済み PR(30日)
138

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

Ephemeral-AI-Lab/layerfs のほかの issue

Ephemeral-AI-Lab/layerfs の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。