Skip to content

feat(webapp): tell agent runs from everything else in History (BEA-157) - #191

Open
ssowonny wants to merge 1 commit into
mainfrom
bea-157-ph-idea-tell-human-work-from-agent-work-at-a-glance
Open

feat(webapp): tell agent runs from everything else in History (BEA-157)#191
ssowonny wants to merge 1 commit into
mainfrom
bea-157-ph-idea-tell-human-work-from-agent-work-at-a-glance

Conversation

@ssowonny

Copy link
Copy Markdown
Contributor

TL;DR

  • You can finally ask History "what did the agents do here" and get an answer — segment control, plus an AGENT chip on the rows.
  • The hub already knew this (Op.Session, hook-set, un-forgeable) and already shipped it to the browser. It just rendered it nowhere. This is plumbing.
  • One case in the existing filter switch, one entry in HISTORY_FILTER_KEYS. No new table, no new repo, nothing near journal.Less or Replay.
  • Known gap, on purpose: the other button says Unattributed, not Human — an empty session often just means the daemon beat the hook, so only agent work gets badged.
  • ?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"]
Loading

Session != "" is a positive claim that is always true — it is set at exactly one
place in the tree (syncer.go:666, from hooksync.go:106), and only when
bdrive sync --hook drives 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 agent and unattributed, never agent and human.
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|unattributed on handleHistory, as one more case in the
filter switch beside q/user/since/until. It lands before the sort and the
cursor skip, so next_cursor keeps meaning "the next matching entry" and paging
under 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 own
invalid n / invalid since precedent — ?by=human is in the 400 test on purpose.

Frontend"by" joins HISTORY_FILTER_KEYS, and that one array is the whole
wiring: URL parse, URL write, historyFilterQuery, hasHistoryFilters, and the API
call all read it, so deep link, reload, Back and Clear work 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". groupRuns keys a
card 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 it
once on hrun-head, not repeated on each of its rows.

Deviation from the plan: by is typed string on HistoryFilters, not the
literal union "agent" | "unattributed". npm run build is tsc && vite build, and
the union is a hard failure at the URL parse loop and at set()'s computed key.
Same treatment since?: string already gets — the URL is user input and the server
is the one validator. This was called out in the plan comment before I started.

Screenshots

The seeded e2e hub, same state on both sides.

Before After
before after

The filter bar's first row is untouched; the segment takes its own row below it, and
the seeded run card picks up one AGENT chip on its header.

?by=agent ?by=unattributed
agent unattributed

Note 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.

Bare row (per-file feed) Mobile
bare row mobile

What was run

Check Result
go build ./..., go vet ./..., go test ./... pass
TestHistoryFilters +7 cases: by=agent, by=unattributed, bare by=, and 4 composed with user/q/prefix
TestHistoryFilterPaging now table-driven; by=agent&n=1 walks the whole agent feed, no repeats, no skips
TestHistoryBadDateRange by=garbage and by=human both 400
npm run test (frontend) 116 pass, incl. a new ?by= round-trip in router.test.ts
npm run e2e 192 pass, 1 skipped — incl. a new spec asserting the bar never contains the word "Human"
frontend/check-dist.sh fresh (rebuilt static/ 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.md legitimately
appears 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 — the HistoryFilters note enumerating the filters was going stale. That's a note, not a class or a relationship, so there is no "Architecture changes" section and gh pr create carries # skip-diagram-check.

Not done, deliberately

The issue's own stated follow-ups, none of which this needs: no by=-aware entry
point in undorun.go, no agent/unattributed split on the Dashboard, no per-device
"this is an agent" flag in devices.go, no back-fill of Session onto ops already
written, and no badging from Op.Note (forgeable, and the TTL'd daemon fallback
misattributes 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

cd $(git worktree list | grep bea-157 | awk '{print $1}') && claude --resume 29650161-2c35-4e53-863b-875c77902502

(this machine only)

🤖 Generated with Claude Code

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant