Skip to content

feat(live): the agent learns what it missed, and stops waiting on a closed page - #37

Merged
fiddur merged 2 commits into
developfrom
liveness
Aug 27, 2026
Merged

feat(live): the agent learns what it missed, and stops waiting on a closed page#37
fiddur merged 2 commits into
developfrom
liveness

Conversation

@fiddur

Copy link
Copy Markdown
Collaborator

The two things you asked about, and they turned out to need different mechanisms.

A review going out

Nothing wakes an agent for a submit, so it could answer a question about a finding already on the pull request and word it as though it were not. await now reports what happened while it was parked, against a per-session watermark (agent_seen_at), and the skill tells it to read that before answering.

Reported on the way back rather than raised as an event — you said it should not disconnect for this, and it does not.

One thing that only showed up in a test: datetime('now') is whole seconds, so a submit landing in the same second as the watermark compares equal and is silently never mentioned. That is precisely when it is most likely, since the agent has just been handed a question about it. Both stamps carry milliseconds now.

A closed tab

The page holds no connection — it polls — so its requests are the only evidence a window is open. The agent marks its own requests with x-diffity-agent: 1 so its polling is not mistaken for a window, and /api/live/claim never counts regardless.

A wait ends as soon as a window that was open stops asking, and await exits 4 so the loop stops rather than re-arming forever.

The distinction that matters: a window that has never been open is not the same as one that closed. An agent is usually armed before the reader opens the page, and treating "nobody watching" as "stop" would end the loop before it began. My first version got this wrong — viewerPresent: false reads identically in both cases — so the protocol now carries viewerGone explicitly.

Verified

Three cases end to end against a real server and a real await: never opened holds the wait (exit 3), a page seen then quiet stops at 20s (exit 4), a page still polling holds (exit 3). Plus HTTP-level tests for the claim route and unit tests for both pure pieces.

parser 43, git 44, github 21, ui 395, cli 209. Typecheck 0. v0.9.14.

🤖 Generated with Claude Code

https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs

fiddurand others added 2 commits August 26, 2026 15:28
…losed page
Two things an agent parked on the queue could not see.
**A review going out.** Nothing wakes for a submit, so an agent could
answer a question about a finding already on the pull request and word it
as though it were not. `await` now reports, on the way back, how many
findings went out while it was parked, against a per-session watermark.
Reported rather than raised: it is worth knowing, not worth interrupting
for.
Both stamps carry milliseconds. `datetime('now')` is whole seconds, and a
submit landing in the same second as the watermark compares equal and is
never mentioned — which is exactly when it is most likely, since the
agent has just been handed a question about it.
**A closed tab.** The page holds no connection, it polls, so its requests
are the only evidence a window is open; the agent marks its own with a
header so it is not mistaken for one. A wait now ends as soon as a window
that was open stops asking, and `await` exits 4 so the loop stops instead
of re-arming every few minutes into a page nobody has open.
A window that has never been open is deliberately different: an agent is
usually armed before the reader opens the page, and stopping then would
end the loop before it began. `viewerGone` says which, because
`viewerPresent` alone cannot.
v0.9.14.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
Self-review. Presence was inferred from the page polling, and react query
stops polling a hidden tab — `refetchIntervalInBackground` defaults to
false. So switching to another window for twenty seconds read as a closed
page and stopped the loop, which is the flow this was built for.
Silence was the wrong signal. A closed tab has a precise one: `pagehide`
with `sendBeacon`, delivered during unload where a normal request is
abandoned. That now ends a wait, at once. The page also beats every
fifteen seconds and again whenever it is looked at, as the fallback for a
crash or a kill, and the idle window is three minutes so a throttled
background timer still counts.
The wait also knows why it ended. `listenerGone` is aborted both by the
agent hanging up and by the reader leaving, and those need opposite
responses — one has no socket left to write to, the other is waiting for
an answer. The watcher sets a flag rather than the handler guessing from
state that may have moved since.
v0.9.15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
fiddur added a commit that referenced this pull request Aug 27, 2026
Both open branches bumped from develop at 0.9.13, so this one took
0.9.14 while #37 took 0.9.15 — and whichever merged second would have
sent the version backwards. The message on the previous commit named
0.9.16 on that assumption; this makes it true.
The rule in AGENTS.md says to bump inside the pull request, which cannot
be right for two branches at once. Worth a line there about the second
one to merge needing a re-bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
@fiddur
fiddur marked this pull request as ready for review August 27, 2026 08:25
@fiddur
fiddur merged commit abd11b6 into developAug 27, 2026
@fiddur
fiddur deleted the liveness branch August 27, 2026 08:25
fiddur added a commit that referenced this pull request Aug 27, 2026
…38)
Both tweaks.
### The count was one list short
The bell was fed `unseenAlerts` — what a note leaves behind *after* its
ten seconds run out. So an answer was invisible to the bell while its
bubble was on screen, and then the number rose at the moment nothing had
actually happened. Both lists reach it now, deduplicated by thread, so
it means "answers waiting" rather than "notes that have timed out".
### The tab now says so
A red mark on the favicon while anything is unread. That is the case
that matters most: a reader in another window cannot see the bell at
all, which is also the case #37 is about.
Built as SVG text rather than drawn on a canvas, because the icon
already is an SVG — the mark inherits its scaling, and its ring can use
the same `prefers-color-scheme` rule the icon already uses, so it stays
legible on a light tab strip and a dark one. The icon is fetched once
and kept, and if it cannot be read the tab is simply left alone.
Checked the geometry rather than eyeballing it: the badged icon parses
as XML, and the ring spans x 200–400, y 10–210 in a 411×395 canvas, so
nothing clips. My first coordinates put the ring 5 units past the right
edge.
### Verified
parser 43, git 44, github 21, ui 414, cli 190. Typecheck 0.
**One honest gap:** the one-line prop wiring in `diff-page.tsx` has no
test, because `DiffPage` is rendered by no test in the repo — it is the
hub, with router and loader dependencies. The *behaviour* is pinned (a
note moving from shown to expired leaves the count unchanged) via
`unreadAlerts` and the bell; only the literal pass-through is not. Same
structural gap I noted on #35.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@fiddur