mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(cli): a plain bdrive sync clears the last session's note (BEA-107) (#172)
An explicit `bdrive sync` never touched the stored session note, so a hand
edit made after `bdrive sync --note claude-session-abc` was still stamped
with that note under its 30-minute TTL — attributed to the agent in
`bdrive log`, and grouped inside that session in hub History (which also
feeds the agent-run rollback story).
The fix is a branch deleted, not added: `SaveNote("")` already routes to
`ClearNote`, so dropping the `Changed("note")` guard makes one unconditional
call both set and clear. Clearing the *store* is what leaves the cycle
unstamped — scan falls back to `LoadNote` whenever `Session.Note` is empty.
The --hook path has its own SaveNote and the daemon calls Cycle directly, so
agent- and daemon-driven syncs keep the note until the TTL expires.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3fbe1252db
commit
432eba1b49
@@ -256,7 +256,7 @@ hub's own storage, never something a syncing client points at directly:
|
||||
| `bdrive forget <path>...` | Stop syncing a path *and* remove it from the hub — adds the rule to `.bdriveignore` (which syncs) and prunes in one step. Local files are never touched, here or on teammates' devices |
|
||||
| `bdrive url [path]` | Internal hub link for a file/folder (sign-in + membership required; `--sync` pushes first; no arg = project home). Computed locally |
|
||||
| `bdrive share <file>` | Public URL for a synced file (`--list`, `--revoke`, `--expires`) |
|
||||
| `bdrive sync [folder]` | Run one sync cycle now. `--note <text>` stamps session context (e.g. an agent session id) onto changes — shown in `bdrive log` and hub history; keeps applying to daemon-committed changes until `--note-ttl` (default 30m) expires. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing: event JSON on stdin, sync + note, gated-link formula (Claude Code hook JSON) on stdout |
|
||||
| `bdrive sync [folder]` | Run one sync cycle now. `--note <text>` stamps session context (e.g. an agent session id) onto changes — shown in `bdrive log` and hub history; keeps applying to daemon-committed changes until `--note-ttl` (default 30m) expires. A plain `bdrive sync` with no `--note` clears it, so a hand edit is never stamped with the last agent session's note. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing: event JSON on stdin, sync + note, gated-link formula (Claude Code hook JSON) on stdout |
|
||||
| `bdrive hooks [install\|uninstall]` | Register turn-boundary sync hooks in each agent platform's user config (Claude Code, Codex, Gemini CLI, Hermes) — pull each turn, push after edits, session-note stamping, agent-read tracking. Once per machine, covering every session; run automatically by `bdrive init`; idempotent (`--agent` overrides detection) |
|
||||
| `bdrive read-log [folder]` | Hook plumbing: queue agent file reads from a hook event (JSON on stdin) for the hub's read heatmap — native reads, grep matches, and files named in shell commands; drained on the next sync. Registered by `bdrive hooks install` |
|
||||
| `bdrive status [folder]` | Projects, daemon state, two separate change counts — `pending` (journalled, not yet pushed) and `local` (on disk, not yet scanned — what a stopped daemon leaves invisible) — and any synced files that looked like they held credentials when they last changed. Pure local read: no ops, no journal writes, no network |
|
||||
|
||||
+15
-9
@@ -74,16 +74,22 @@ list in .bdrive/config.json is never pruned against either.`,
|
||||
return err
|
||||
}
|
||||
defer closeSession(sess)
|
||||
if cmd.Flags().Changed("note") {
|
||||
// Persist the note so the daemon's own scans stamp it too —
|
||||
// history then links every change from this working session
|
||||
// to its context, not just the ones this invocation catches.
|
||||
// An explicit empty --note clears it. Expires after --note-ttl.
|
||||
if err := sess.Store.SaveNote(note, noteTTL); err != nil {
|
||||
return err
|
||||
}
|
||||
sess.Note = note
|
||||
// Persist the note so the daemon's own scans stamp it too —
|
||||
// history then links every change from this working session
|
||||
// to its context, not just the ones this invocation catches.
|
||||
// Expires after --note-ttl. Unconditional because an explicit
|
||||
// `bdrive sync` is a human act: whatever note the last agent
|
||||
// session left stops applying here, and empty text clears
|
||||
// (SaveNote -> ClearNote), so one call both sets and clears.
|
||||
// Clearing the *store* is what leaves this cycle unstamped —
|
||||
// scan falls back to LoadNote whenever Session.Note is empty.
|
||||
// The --hook branch below has its own SaveNote and keeps the
|
||||
// TTL, which is what the TTL is for: the daemon's own scans
|
||||
// inside an agent session.
|
||||
if err := sess.Store.SaveNote(note, noteTTL); err != nil {
|
||||
return err
|
||||
}
|
||||
sess.Note = note
|
||||
sess.Prune = prune
|
||||
sess.OnProgress = progressReporter()
|
||||
res, err := sess.Cycle(cmd.Context())
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -115,3 +116,75 @@ func TestSyncNoteCannotForgeASession(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An explicit `bdrive sync` is a human act, so it must not inherit the note
|
||||
// the last agent session left behind — otherwise a hand edit lands in history
|
||||
// (and in that session's group, and in its rollback) attributed to the agent.
|
||||
// The daemon's own scans still inherit it: that is what the TTL is for.
|
||||
func TestPlainSyncClearsThePreviousSessionNote(t *testing.T) {
|
||||
folder, proj := stampFixture(t)
|
||||
dev := thisDevice(t)
|
||||
|
||||
runSync := func(args ...string) {
|
||||
t.Helper()
|
||||
c := syncCmd()
|
||||
c.SetOut(&bytes.Buffer{})
|
||||
c.SetArgs(append([]string{folder}, args...))
|
||||
if err := c.Execute(); err != nil {
|
||||
t.Fatalf("sync %v: %v", args, err)
|
||||
}
|
||||
}
|
||||
noteFor := func(path string) string {
|
||||
t.Helper()
|
||||
for _, op := range journalOps(t, proj.ID, dev) {
|
||||
if op.Path == path {
|
||||
return op.Note
|
||||
}
|
||||
}
|
||||
t.Fatalf("no op for %q", path)
|
||||
return ""
|
||||
}
|
||||
|
||||
// An agent session stamps its note.
|
||||
runSync("--note", "s1")
|
||||
if got := noteFor("a.md"); got != "s1" {
|
||||
t.Fatalf("a.md Note = %q, want s1", got)
|
||||
}
|
||||
|
||||
// A hand edit committed by a plain `bdrive sync` is unattributed, and the
|
||||
// stored note is gone — clearing the store is what makes it unstamped,
|
||||
// since scan falls back to LoadNote whenever Session.Note is empty.
|
||||
if err := os.WriteFile(filepath.Join(folder, "b.md"), []byte("by hand\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runSync()
|
||||
if got := noteFor("b.md"); got != "" {
|
||||
t.Errorf("plain sync stamped the previous session's note: b.md Note = %q", got)
|
||||
}
|
||||
sess, _, err := openSession(context.Background(), folder, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := sess.Store.LoadNote(); got != "" {
|
||||
t.Errorf("stored note = %q after a plain sync, want it cleared", got)
|
||||
}
|
||||
closeSession(sess)
|
||||
|
||||
// The daemon's path (Session.Cycle direct, no CLI) still inherits a live
|
||||
// note inside its TTL.
|
||||
runSync("--note", "s2")
|
||||
if err := os.WriteFile(filepath.Join(folder, "c.md"), []byte("daemon tick\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sess, _, err = openSession(context.Background(), folder, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := sess.Cycle(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
closeSession(sess)
|
||||
if got := noteFor("c.md"); got != "s2" {
|
||||
t.Errorf("daemon-committed change lost the live note: c.md Note = %q, want s2", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server.
|
||||
| `bdrive forget <path>...` | Stop syncing a path and remove it from the hub. Adds the rule to `.bdriveignore` (which syncs) and prunes in one step. Local files are never touched, here or on teammates' devices |
|
||||
| `bdrive url [path]` | Internal hub link for a file or folder — sign-in and membership required. `--sync` pushes first; no argument gives the project home. Computed locally |
|
||||
| `bdrive share <file>` | Public URL for a synced file. `--list`, `--revoke`, `--expires` (the hub's Share dialog can also set an expiry on an existing link). Refuses a file whose first 1 MiB holds credential-shaped strings — `--force` shares it anyway |
|
||||
| `bdrive sync [folder]` | Run one sync cycle now. Refuses folders this device never `init`ed and folders paused by `bdrive stop`. `--note <text>` stamps session context onto changes; `--note-ttl` (default 30m) bounds it. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing: it also reports the files teammates changed since the agent's last turn |
|
||||
| `bdrive sync [folder]` | Run one sync cycle now. Refuses folders this device never `init`ed and folders paused by `bdrive stop`. `--note <text>` stamps session context onto changes; `--note-ttl` (default 30m) bounds it, and a plain `bdrive sync` with no `--note` clears it. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing: it also reports the files teammates changed since the agent's last turn |
|
||||
| `bdrive hooks [install\|uninstall]` | Register turn-boundary sync hooks in each detected agent platform's user config — once per machine, covering every folder. Run automatically by `bdrive init`; idempotent; `--agent` overrides detection. `uninstall` removes only BearDrive's own hook entries |
|
||||
| `bdrive read-log [folder]` | Hook plumbing: queue agent file reads for the hub's read heatmap. Registered by `bdrive hooks install` |
|
||||
| `bdrive status [folder]` | Projects, daemon state, pending changes, and any synced files that looked like they held credentials when they last changed |
|
||||
@@ -95,6 +95,11 @@ Stamps session context — an agent session id, say — onto changes. It shows u
|
||||
`bdrive log` and hub history, and keeps applying to daemon-committed changes
|
||||
until `--note-ttl` expires.
|
||||
|
||||
A plain `bdrive sync` with no `--note` **clears** the note: an explicit sync is a
|
||||
human act, so the edit you just made by hand is not filed under the last agent
|
||||
session. Hook- and daemon-driven syncs are unaffected and keep the note until the
|
||||
TTL expires — that is what the TTL is for.
|
||||
|
||||
### `bdrive restore` — undoing a change
|
||||
|
||||
An agent rewrote a file you liked. Put the old bytes back:
|
||||
|
||||
Reference in New Issue
Block a user