From 5623113ff7b4e2fdf20b081f0a5cee958c593cf4 Mon Sep 17 00:00:00 2001 From: "Snow Lee (Sungwon)" Date: Mon, 10 Aug 2026 14:33:20 -0700 Subject: [PATCH] feat(sync): agents hear what teammates changed before they overwrite it (BEA-127) (#144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A teammate's agent rewrites a file and yours never hears about it — the only trace is a `.bdrive-conflict-*` nobody opens. Now the turn-start hook names the paths that arrived since the last turn: "re-read before editing". The record lives in a spool (`internal/store/inbound.go`), not in Cycle's Result, because the daemon usually materializes a peer's change seconds before the turn starts — so the hook's own cycle sees nothing. materialize appends every path it writes or removes, `bdrive sync --hook` drains it after its cycle and renders it under each mount's own prefix (stripping the session subpath when the run is inside a mount, dropping paths outside it). Advisory only: nothing blocks, nothing prompts, no per-Write remote call. The spool is capped, 0600, in the volume dir, and best-effort everywhere — a spool failure never fails a cycle or a turn. Co-authored-by: Claude Opus 5 (1M context) --- CLAUDE.md | 8 +- architecture/cli-sync.md | 2 + cmd/bdrive/cmds.go | 6 +- cmd/bdrive/hooksync.go | 100 ++++++++++++- cmd/bdrive/hooksync_test.go | 138 ++++++++++++++++++ internal/store/inbound.go | 111 ++++++++++++++ internal/store/inbound_test.go | 118 +++++++++++++++ internal/syncer/syncer.go | 14 ++ internal/syncer/syncer_test.go | 81 ++++++++++ .../docs/guides/shared-agent-memory.md | 16 ++ web/docs/src/content/docs/reference/cli.md | 2 +- 11 files changed, 584 insertions(+), 12 deletions(-) create mode 100644 internal/store/inbound.go create mode 100644 internal/store/inbound_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 0331479..d75904c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,7 +84,13 @@ a single JSON object, so the formula carries **every** mount as a `prefix → URL` pair — the prefix being the mount's path as the agent sees it from the session's folder, or an empty prefix with the session's own subpath baked into the URL when the session runs inside the mount; emitting only the first mount -hung one project's paths on another project's base URL. Then an +hung one project's paths on another project's base URL. The same context also +names what teammates changed since the last turn ("re-read before editing"), +drained from the **inbound spool** (`internal/store/inbound.go`, a near-copy of +the read spool): `materialize` appends every path it writes or removes, and the +hook drains it *after* its own cycle — a `Result` field would report nothing, +because the daemon has usually materialized the peer's change seconds earlier. +Advisory only: nothing blocks a write. Then an async push on PostToolUse Write/Edit, and `bdrive read-log` on Read/Grep/Bash for the read heatmap. The inline hook commands `internal/agenthooks` writes must stay a fast no-op outside BearDrive folders diff --git a/architecture/cli-sync.md b/architecture/cli-sync.md index beb33e3..662fc27 100644 --- a/architecture/cli-sync.md +++ b/architecture/cli-sync.md @@ -116,9 +116,11 @@ classDiagram +SaveNote / LoadNote +LogRead(rel, session) read spool +PendingReads dedup on path+session + +LogInbound / DrainInbound +Lock() flock } note for Store "internal/store — ~/.bdrive/volumes/mount-id: content-addressed blobs, per-device journal copies, state cache, paused marker (free funcs Paused/SetPaused, no flock)" + note for Store "inbound.jsonl is the read spool's twin, running the other way: materialize appends every path it wrote or removed for a peer, and `sync --hook` drains it into the turn's context (re-read before editing). A spool and not a Result field because the daemon usually materializes the change seconds before the turn starts, so the hook's own cycle sees nothing. Capped, best-effort, never fails a cycle" class Op { +Seq +Lamport +Time +Device diff --git a/cmd/bdrive/cmds.go b/cmd/bdrive/cmds.go index 8f3c0ff..52fb127 100644 --- a/cmd/bdrive/cmds.go +++ b/cmd/bdrive/cmds.go @@ -105,8 +105,10 @@ list in .bdrive/config.json is never pruned against either.`, if err != nil || !ok || syncBlocked(proj) != "" { continue } - if base, ok := runHookSync(cmd, target, sessionID, hookLabel); ok { - links = append(links, hookLinkFor(folder, target, base)) + if h, ok := runHookSync(cmd, target, sessionID, hookLabel); ok { + link := hookLinkFor(folder, target, h.base) + link.paths = h.paths + links = append(links, link) } } emitHookContext(cmd, links) diff --git a/cmd/bdrive/hooksync.go b/cmd/bdrive/hooksync.go index 9b9162a..8832bf9 100644 --- a/cmd/bdrive/hooksync.go +++ b/cmd/bdrive/hooksync.go @@ -9,6 +9,8 @@ import ( "time" "github.com/spf13/cobra" + + "github.com/runbear-io/beardrive/internal/store" ) // `bdrive sync --hook