feat(webapp): tell agent runs from everything else in History (BEA-157) - #191
Open
ssowonny wants to merge 1 commit into
Open
feat(webapp): tell agent runs from everything else in History (BEA-157)#191ssowonny wants to merge 1 commit into
ssowonny wants to merge 1 commit into
Conversation
The hub has classified every READ three ways for a while — human, share, agent — and shows it on every folder and file header. It has the same signal for every CHANGE (Op.Session, set only by `bdrive sync --hook`, already on the wire and already grouping run cards) and rendered it nowhere. Asking "what did the agents do here" got you a path box and a list of email addresses, neither of which answers it: the agent runs as the person, so the same account is on both. Server: ?by=agent|unattributed on handleHistory, one case in the existing filter switch, before the sort and the cursor skip — so it composes with q/user/since/until/path/prefix and pages with no new machinery. Anything else is a 400, like `invalid n` and `invalid since`. Frontend: "by" joins HISTORY_FILTER_KEYS, which is the whole wiring (URL parse, URL write, the query, Clear, Back). A three-state segment reuses the Dashboard's lens pills; the chip goes on bare rows and once on a run card's header, gated on run.session rather than on cardness — groupRuns keys a card on a session OR a note, and a note-keyed card claims nothing. The second class is "unattributed", never "human". The daemon scans every 3s and usually commits an agent's write before the hook's cycle runs, so an empty Session is absence of evidence — a "Human" button would confidently mislabel agent work as a person's. Only the positive class is ever badged. 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
AGENTchip on the rows.Op.Session, hook-set, un-forgeable) and already shipped it to the browser. It just rendered it nowhere. This is plumbing.casein the existing filter switch, one entry inHISTORY_FILTER_KEYS. No new table, no new repo, nothing nearjournal.LessorReplay.?by=lives in the URL, so a filtered feed is a link you can send.The one thing to not get wrong
flowchart TD S["one change in the feed<br/>op.Session"] S --> A["NON-EMPTY<br/>the hook drove the cycle<br/><b>an agent run, always</b><br/>safe to badge"] S --> B["EMPTY<br/>a person edited it, OR<br/>the daemon beat the hook<br/><b>cannot tell the two apart</b>"] A --> A2["?by=agent · AGENT chip"] B --> B2["?by=unattributed · no chip"]Session != ""is a positive claim that is always true — it is set at exactly oneplace in the tree (
syncer.go:666, fromhooksync.go:106), and only whenbdrive sync --hookdrives the cycle.Session == ""is an absence of evidence:the daemon scans every 3s and usually commits an agent's write before the hook's
cycle runs, and browser writes (upload / remove / restore) journal under the hub's
own device with no session at all.
So the two classes are
agentandunattributed, neveragentandhuman.A "Human" button would confidently mislabel agent work as a person's — the exact
failure the issue was conditioned on ("a badge that guesses wrong is worse than no
badge"). Only the positive class is ever badged; an unbadged row asserts nothing,
which is the correct amount to assert.
What changed
Server —
?by=agent|unattributedonhandleHistory, as one morecasein thefilter switch beside
q/user/since/until. It lands before the sort and thecursor skip, so
next_cursorkeeps meaning "the next matching entry" and pagingunder the filter needs no new machinery. It composes with every existing filter.
Anything other than those two values is a
400, matching the endpoint's owninvalid n/invalid sinceprecedent —?by=humanis in the 400 test on purpose.Frontend —
"by"joinsHISTORY_FILTER_KEYS, and that one array is the wholewiring: URL parse, URL write,
historyFilterQuery,hasHistoryFilters, and the APIcall all read it, so deep link, reload, Back and
Clearwork with no further edits.The segment reuses the Dashboard's lens pills (
.in-lens-btn, already themed,already 44px on mobile) rather than adding a component.
The badge is gated on
run.session, never on "is this a card".groupRunskeys acard on a session id or on a note, so a note-keyed card carries no agent claim —
and correctly gets no chip. Bare rows get the chip in
.hline; a run card gets itonce on
hrun-head, not repeated on each of its rows.Deviation from the plan:
byis typedstringonHistoryFilters, not theliteral union
"agent" | "unattributed".npm run buildistsc && vite build, andthe union is a hard failure at the URL parse loop and at
set()'s computed key.Same treatment
since?: stringalready gets — the URL is user input and the serveris the one validator. This was called out in the plan comment before I started.
Screenshots
The seeded e2e hub, same state on both sides.
The filter bar's first row is untouched; the segment takes its own row below it, and
the seeded run card picks up one
AGENTchip on its header.?by=agent?by=unattributedNote the second one: zero chips, and the agent run is gone. Note also that it still
contains real agent work — that is the honest reading, not a bug.
What was run
go build ./...,go vet ./...,go test ./...TestHistoryFiltersby=agent,by=unattributed, bareby=, and 4 composed withuser/q/prefixTestHistoryFilterPagingby=agent&n=1walks the whole agent feed, no repeats, no skipsTestHistoryBadDateRangeby=garbageandby=humanboth 400npm run test(frontend)?by=round-trip inrouter.test.tsnpm run e2efrontend/check-dist.shstatic/committed)Union-and-disjointness is satisfied by construction in the Go test: the fixture stamps
exactly two of four seeded ops, one per author, so neither class is empty and neither
maps onto a single person.
One thing the e2e spec had to give up: disjointness is per op, not per path.
Later specs rewrite some of the same files unattributed, so
runbook.mdlegitimatelyappears under both filters. The set assertion lives in the Go test where op identity
is available.
Docs
reference/cli.md— history URL example gains&by=agent, plus a sentence on why the other class isn't called "human".architecture/webapp-frontend.md— theHistoryFiltersnote enumerating the filters was going stale. That's a note, not a class or a relationship, so there is no "Architecture changes" section andgh pr createcarries# skip-diagram-check.Not done, deliberately
The issue's own stated follow-ups, none of which this needs: no
by=-aware entrypoint in
undorun.go, no agent/unattributed split on the Dashboard, no per-device"this is an agent" flag in
devices.go, no back-fill ofSessiononto ops alreadywritten, and no badging from
Op.Note(forgeable, and the TTL'd daemon fallbackmisattributes exactly the case this refuses to guess at).
The open question I'd want your read on: run cards still show up under
Unattributed when they're note-keyed rather than session-keyed. That's correct by
this PR's own rule — a note is forgeable and the daemon-fallback note is the case we
refuse to call agent work — but it will look like a bug to the first person who
notices a run card sitting in the "no agent claimed this" feed.
Ship it: the filter is accurate, the badge only ever over-claims by staying silent,
and the URL makes it linkable.
Build session
(this machine only)
🤖 Generated with Claude Code