feat(hook): hand the next agent session your working state (BEA-134) - #149
Open
ssowonny wants to merge 1 commit into
Open
feat(hook): hand the next agent session your working state (BEA-134)#149ssowonny wants to merge 1 commit into
ssowonny wants to merge 1 commit into
Conversation
Every new agent session — on another machine, in a teammate's account, on another platform — re-derives the same working state from scratch. History records what a session *did*, never what it was in the middle of. AGENT_HANDOFF.md at the mount root is now that channel. It is an ordinary synced file (the sync engine has never heard of it); the UserPromptSubmit hook hands its body to a session's FIRST turn and asks every turn to overwrite it before finishing. No new hook event, no new storage, no change to internal/agenthooks. - First turn is decided by the note the hook already writes, read one line before SaveNote overwrites it — so the body is paid for once per session, not once per turn. - Bounded: 4 KB per mount, 8 KB per turn, with a truncation marker. - Multi-mount safe: each block is labelled with its own mount's path, and the reminder names every mount's file. - A scope that excludes the root (`init --only wiki`) is detected and said out loud rather than silently keeping the handoff local. - This is the first PEER-authored content the product injects into an agent's context, so each block is framed as information rather than instruction, and the provenance line goes through safeField. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
TL;DR
AGENT_HANDOFF.mdat the project root carries it: the hook hands its body to a session's first turn, and asks every turn to overwrite it before finishing.SendMessage.internal/agenthooksuntouched — the gate the issue flagged stays where it was.initdoes not seed it. Both deliberate (spec's Out column).flowchart LR A["session A ends<br/>agent overwrites<br/>AGENT_HANDOFF.md"] --> B["bdrive sync<br/>ordinary synced file"] B --> C{"session B<br/>turn 1?"} C -->|"yes<br/>note != this session"| D["inject body (<= 4 KB)<br/>+ provenance<br/>+ write reminder"] C -->|"no<br/>note == this session"| E["write reminder only<br/>body not re-paid"]The one thing to actually check
This is the first time the product injects peer-authored content into an agent's context. Everything before it was paths (
hookChanged) or the hub's own link formula. A teammate'sAGENT_HANDOFF.mdis arbitrary text arriving at the top of a session at high trust.Two guards, both load-bearing:
last changed <date> by <who>) goes throughsafeField—UserName/User/Authorare arbitrary JSON off a peer's journal, same asbdrive log.TestSyncHookModeHandoffHostileProvenanceis the guard. The body itself rides insidejson.Marshal, which escapes C0/C1, so it needs no second pass.What changed
Almost all of it is one file.
cmd/bdrive/hooksync.go+169,cmds.go+1, and nothing else in the repo changes behavior — no newstoremethod, no sync-engine change. The sync engine never learns the filename.readHandoff/handoffProvenance,hooksync.gorunHookSync, one line before theSaveNotethat destroys the evidencehookHandoffContext/hookHandoffPath,hooksync.gosyncer.LoadFilter(...).Skip("AGENT_HANDOFF.md")— the same seambdrive read-logaskscmds.go:111, one line beside the existinglink.paths = h.pathsBounds: 4 KB per mount, 8 KB across all mounts in one turn,
… (truncated)when cut. A mount whose body doesn't fit the remaining budget is dropped whole rather than shaved — and still gets its write reminder, so nothing silently disappears from the agent's view.Multi-mount is the case that bites: each block is labelled with its own mount's path (
projA/AGENT_HANDOFF.md), and the reminder names every mount's file, so a session covering two projects can't write one project's state into the other's.Why the first-turn test is the note
The hook is paid on every turn. A 4 KB body per turn is a real cost, and re-injecting it is worthless — the session already has it.
runHookSyncwas already writingStore.SaveNote(label + " session " + id, …). Reading that note one line before the write is the whole test: equal means this session has been here, unequal means turn 1. No new state, no new file.Two consequences worth knowing, both commented in the code:
bdrive sync --note "x"mid-session overwrites the same note, so the next turn re-injects once. Harmless. Same for a session idle past the 30mhookNoteTTL— by then the context was almost certainly compacted anyway.Deviations from the reviewed plan
Four, all small, none changing the shipped behavior:
internal/webapp/cli_e2e_test.go, notcmd/bdrive/hooksync_test.go. The plan's version — twoBDRIVE_HOMEs in one process over a sharedfile://remote — cannot work:runHookSyncreturns false for a non-hub remote, so afile://mount emits no context at all and there would be nothing to assert. SoTestCLIHandoffAcrossDevicesis a real two-device test against the in-process hub: device A writes and syncs, device B logs in as its own device,init --projects the same project, and its first hook turn gets A's body. NeedednewCLIDevice(secondHOME/BDRIVE_HOME, real device-code login, a runner that takes stdin) andcliDeviceSignInextracted out ofnewCLIEnv. Runs in ~8s.present boolfield. It was only there to vary the write reminder's wording, and the reminder reads the same whether or not the file exists yet.readHandoffruns after thesplitHubRemotecheck, not before it — a non-hub mount shouldn't pay a file read plus a journal read for context it will never emit.What was run
go build ./...,go vet ./...,go test ./...— all green.cmd/bdrive/hooksync_test.go: first turn, not repeated, new session, missing/empty, truncation + the per-turn total cap, two mounts, session inside a mount (../../AGENT_HANDOFF.md), out-of-scope warning, hostile provenance.TestCLIHandoffAcrossDevices— the real two-device pass described above.npm run buildinweb/docs— clean, and the new#agent_handoffmdanchor resolves in the built HTML.npm run e2e. No frontend source changed, sointernal/webapp/staticis untouched and the Playwright suite has nothing new to exercise. The onlyinternal/webappchange is a Go test file, and it passes. No UI change, so no screenshots.architecture/*.mddraws nohookLink/hookSync, and no types, seams or ownership moved.Docs
README.md(the--hookrow),web/docsreference/cli.md, a newAGENT_HANDOFF.mdsection inreference/project-files.md, and a "Hand the next session your working state" section inguides/shared-agent-memory.md— which carries the positioning the issue insisted on: async, persisted as a real file, attributed, and it works when the other person is asleep or on Codex.INSTALL_FOR_AGENTS.mdis unchanged —initdoes not seed the file, so the onboarding flow is the same.The open loop
The spec's inferred decision #2 is the one still worth arguing: one shared file, no per-device variant. Two sessions overwriting it concurrently get ordinary LWW plus a conflict copy, exactly like any other file — and only the exact name is ever read, so a conflict copy is never injected. That's the right deal if the handoff is "the project's current state." It's the wrong deal if two people work in parallel every day, and then
.bdrive-handoff/<device>.mdstarts looking better. Nothing here forecloses that; worth watching once it's in real use.Build session
(This machine only.)