Follow a live session: watch a running agent, and come back correct after a break #66

Merged
mph merged 26 commits from cc-chat into main 2026-08-27 09:21:09 +00:00
Owner

Slice 03 of the cc-chat effort — the third and last local slice on this branch. Slices 01 and 02 shipped in #64 and #65; this one is project-management/efforts/cc-chat/slices/03-follow-live-session/.

The card (frozen)

Goal. I can leave a page open on a Claude Code session I am working in and watch what happens as it happens — and when the connection breaks, the page comes back showing what actually happened rather than a gap.

Fit test. Michal opens the watcher on his own running session, works in it for a stretch, deliberately breaks the connection, and reads the page: nothing he knows happened is missing, nothing is doubled, no row from before the break has changed, and "would I keep this open instead of reading the terminal?" is answered yes.

Fit passed on his read, against a real break: he changed location, the tunnel he reads through dropped, the page said it was not following and was trying, and it reconnected on its own when the tunnel came back.

What it ships

Following a growing file. ForwardLineReader reads the complete lines appended after an offset and holds back one the writer has not finished; BackwardLineReader.lastLineEnd is the hand-over point from a tail read to a follow; ZioConversationArchive.follow puts them over the archive's own vendor-then-mirror resolution. Everything a follow remembers is one byte offset.

The stable point, decided by comparison. TranscriptFollow renders the transcript and compares the new rendering with the last one, so the page is told the smallest tail that will do. It is not a prediction about which rows may move, so it stays true whatever the rendering's rules become — and the one producer stays on the JVM, per CC-D3.

A follow operation on the contract. TranscriptEndpoint.followPath, a Tapir description over the consumer's own Streams, and TranscriptTail as the message: a count line and the markup after it. Deliberately not JSON — reading two fields with a Scala JSON stack took the bundle from 12 750 to 151 128 bytes gzipped.

The element follows. <cc-chat session="…" follow> subscribes, replaces only the tail, and keeps the settled rows as the very same DOM nodes — with them the reader's place and whatever they have opened. A reader at the newest row stays there; a reader who scrolled back is left alone. What scrolls is whatever the host made scrollable, the page otherwise.

The page says whether it is following, and says it from what it hears rather than from what its socket reports. A connection can go silent while it is still open — a frozen host, a wedged proxy — and the browser never mentions it, so the host beats every 5 s and 15 s of silence is a page that has stopped. The page reopens a connection nobody else can see is dead.

A test module for ui.js under jsdom, which the parked item asked for: the element is driven in a real DOM against a page whose subscription, timer and clock belong to the test.

Verified in a browser, on a live session

  • An append kept 59 of 60 rows as the very same nodes and redrew only the run still gathering.
  • A ~2 minute host outage was recovered by the browser alone, with nothing doubled.
  • The host frozen under the page: the line turned within the limit and the rows stayed; resumed: back to following on its own.
  • A quiet session costs one four-byte beat every five seconds.

Review

Eight reviewers (style, testing, security, scala3, zio, architecture, api, composition). Two criticals, both fixed in-loop with a failing test first and each new test proved fallible by mutation:

  • A transcript that shrank under a follow stalled it in silence — now ends with PageSourceMoved, what the reading path already says for a cursor whose source moved.
  • The element's wiring had no test — now covered: one subscription and not two, what arrives reaching the transcript, the subscription and timer given up on leaving the page, a silent connection reopened.

Also taken: one home for the line decoder, a warning logged when a running follow fails, the port saying where following lives and why it is not on it, Cache-Control: no-store and the beat stated on the follow endpoint, and a test pinning that rendered rows never end in a newline. Full report: project-management/efforts/cc-chat/slices/03-follow-live-session/review-slice-2026-08-26.md.

One review claim was measured and rejected: the host binds 127.0.0.1, not every interface.

Follow-ups (not in this PR)

  • What a break costs the top of the page: on reconnect the page takes the newest 500 entries again, so on a long session the oldest rows fall off (measured 60 → 52, and 65 → 56). Nothing that happened during the break is missing.
  • The follow tests wait on a live clock; a TestClock rewrite would make them deterministic.
  • Bounding what a long watch accumulates — parked with measurements: the host, not the browser, gives first, at roughly 2 000–3 000 entries in one watch.
  • Two backward scans to open one follow; a blocking helper in the ZIO archive.
  • The contract-shape batch (versioning, session in the path, a paging cursor, /follow as a noun) keeps its existing trigger.

Lessons

Triaged with Michal before this PR: two promoted to kanon (notes #185 and #186), one fixed at source. #185 carries a measurement made at triage — reading a JSON message costs 171 bytes through js.JSON.parse behind a facade, 19 KB through jsoniter-scala, 151 KB through upickle and 166 KB through circe, so a Scala JSON stack belongs in a page only to share real codecs with the JVM.

Numbers

Bundle 19 127 bytes gzipped against a 20 000 gate (raised twice here, each time with the measurement recorded). 908 tests green, no warnings, scalafix and format clean.

Slice 03 of the cc-chat effort — the third and last local slice on this branch. Slices 01 and 02 shipped in #64 and #65; this one is `project-management/efforts/cc-chat/slices/03-follow-live-session/`. ## The card (frozen) **Goal.** I can leave a page open on a Claude Code session I am working in and watch what happens as it happens — and when the connection breaks, the page comes back showing what actually happened rather than a gap. **Fit test.** Michal opens the watcher on his own running session, works in it for a stretch, deliberately breaks the connection, and reads the page: nothing he knows happened is missing, nothing is doubled, no row from before the break has changed, and "would I keep this open instead of reading the terminal?" is answered yes. **Fit passed** on his read, against a real break: he changed location, the tunnel he reads through dropped, the page said it was not following and was trying, and it reconnected on its own when the tunnel came back. ## What it ships **Following a growing file.** `ForwardLineReader` reads the complete lines appended after an offset and holds back one the writer has not finished; `BackwardLineReader.lastLineEnd` is the hand-over point from a tail read to a follow; `ZioConversationArchive.follow` puts them over the archive's own vendor-then-mirror resolution. Everything a follow remembers is one byte offset. **The stable point, decided by comparison.** `TranscriptFollow` renders the transcript and compares the new rendering with the last one, so the page is told the smallest tail that will do. It is not a prediction about which rows may move, so it stays true whatever the rendering's rules become — and the one producer stays on the JVM, per CC-D3. **A follow operation on the contract.** `TranscriptEndpoint.followPath`, a Tapir description over the consumer's own `Streams`, and `TranscriptTail` as the message: a count line and the markup after it. Deliberately not JSON — reading two fields with a Scala JSON stack took the bundle from 12 750 to 151 128 bytes gzipped. **The element follows.** `<cc-chat session="…" follow>` subscribes, replaces only the tail, and keeps the settled rows as the very same DOM nodes — with them the reader's place and whatever they have opened. A reader at the newest row stays there; a reader who scrolled back is left alone. What scrolls is whatever the host made scrollable, the page otherwise. **The page says whether it is following**, and says it from what it hears rather than from what its socket reports. A connection can go silent while it is still open — a frozen host, a wedged proxy — and the browser never mentions it, so the host beats every 5 s and 15 s of silence is a page that has stopped. The page reopens a connection nobody else can see is dead. **A test module for `ui.js` under jsdom**, which the parked item asked for: the element is driven in a real DOM against a page whose subscription, timer and clock belong to the test. ## Verified in a browser, on a live session - An append kept 59 of 60 rows as the very same nodes and redrew only the run still gathering. - A ~2 minute host outage was recovered by the browser alone, with nothing doubled. - The host frozen under the page: the line turned within the limit and the rows stayed; resumed: back to following on its own. - A quiet session costs one four-byte beat every five seconds. ## Review Eight reviewers (style, testing, security, scala3, zio, architecture, api, composition). Two criticals, both fixed in-loop with a failing test first and each new test proved fallible by mutation: - **A transcript that shrank under a follow stalled it in silence** — now ends with `PageSourceMoved`, what the reading path already says for a cursor whose source moved. - **The element's wiring had no test** — now covered: one subscription and not two, what arrives reaching the transcript, the subscription and timer given up on leaving the page, a silent connection reopened. Also taken: one home for the line decoder, a warning logged when a running follow fails, the port saying where following lives and why it is not on it, `Cache-Control: no-store` and the beat stated on the follow endpoint, and a test pinning that rendered rows never end in a newline. Full report: `project-management/efforts/cc-chat/slices/03-follow-live-session/review-slice-2026-08-26.md`. One review claim was measured and rejected: the host binds `127.0.0.1`, not every interface. ## Follow-ups (not in this PR) - [ ] What a break costs the top of the page: on reconnect the page takes the newest 500 entries again, so on a long session the oldest rows fall off (measured 60 → 52, and 65 → 56). Nothing that happened during the break is missing. - [ ] The follow tests wait on a live clock; a `TestClock` rewrite would make them deterministic. - [ ] Bounding what a long watch accumulates — parked with measurements: the host, not the browser, gives first, at roughly 2 000–3 000 entries in one watch. - [ ] Two backward scans to open one follow; a `blocking` helper in the ZIO archive. - [ ] The contract-shape batch (versioning, session in the path, a paging cursor, `/follow` as a noun) keeps its existing trigger. ## Lessons Triaged with Michal before this PR: two promoted to kanon (notes #185 and #186), one fixed at source. #185 carries a measurement made at triage — reading a JSON message costs 171 bytes through `js.JSON.parse` behind a facade, 19 KB through jsoniter-scala, 151 KB through upickle and 166 KB through circe, so a Scala JSON stack belongs in a page only to share real codecs with the JVM. ## Numbers Bundle 19 127 bytes gzipped against a 20 000 gate (raised twice here, each time with the measurement recorded). 908 tests green, no warnings, scalafix and format clean.
mph added 23 commits 2026-08-27 08:18:10 +00:00
Reading a transcript ends at the last complete line; following it starts
there. ForwardLineReader reads the complete lines a file has grown by
after an offset, holding back a line whose newline the writer has not
put there yet, and answers with the offset to read from next — the whole
of what a follower has to remember.

BackwardLineReader.lastLineEnd is where the two meet: the tail a reader
opens with is bounded by it and the follow starts at it, so an entry
half-written at that moment is read once, when it is finished.

ZioConversationArchive.follow puts the two together over the
vendor-then-mirror resolution the archive already does: an opening page,
then a batch for every stretch the agent writes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
A transcript that is being written does not change everywhere: what was
said is said, and the movement is at the end — a stretch of work still
gathering rows into one, a call waiting for its answer. Redrawing the
whole conversation on every entry would move the page under the reader.

TranscriptTail names the split by comparing two renderings rather than by
reasoning about which rows are allowed to move. Reasoning would have to
be re-checked whenever the rendering gains a rule; a comparison holds
whatever the rules are, and always names the smallest tail that will do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
A page that follows a session is not sent the conversation again on
every entry. It is told how much of what it holds is still right, and
given the markup for the rest — one TranscriptTail per stretch the agent
writes, over server-sent events.

The stream type is the consumer's: the description takes the Streams of
whatever effect system a host runs, so the ui module still says only what
the address is, what the question is, and what the answer means.

The first thing a page is told keeps nothing, so a page that has just
connected and a page that has just reconnected are told the same thing
and neither needs a memory of what went before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
This slice puts logic back in the browser — the element decides which
rows it keeps and which it replaces — which is the trigger the test
module was parked on. It runs under jsdom, so what the element does to a
page is asserted against a page rather than against a stand-in for one.

TranscriptNodes is everything the element makes itself: the empty
transcript it starts with, and the row it says a network failure in.
Both are built with the DOM API, so a session id stays an attribute and
an error stays text, and neither has a path by which it could become
markup. That property was prose; it is now a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
<cc-chat session="…" follow> subscribes instead of asking once. Each
message says how many of the rows the page holds are still what the
transcript renders, and carries the markup for the rest, so the settled
conversation keeps its own DOM nodes — with them its scroll and whatever
the reader has opened — and only the end, where the agent is working,
moves. Watched live: 59 of 60 rows kept their identity across an append.

A broken connection needs nothing of either side. The browser reopens
the subscription, and the first thing a new connection is told keeps
nothing, so the page takes the conversation again as it now stands.

The message is a line and a remainder rather than JSON. Measured: giving
the bundle a parser to read two fields took it from 12 750 to 151 128
bytes gzipped. The gate is raised to 18 000 in the same change that adds
the 3.5 KB following actually costs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
The bundle's name carries a hash of its contents, so a rebuild renames
it. The host had read that name once at startup and wrote it into every
page afterwards, so after a rebuild it answered the script tag it had
just written with a failure, and the page came up empty.

The manifest is now read on every request, for the same reason the
stylesheet already is: this host exists to be reacted to, so a rebuilt
element reaches the page on a reload. A bundle this build does not hold
is refused in words rather than by dying on a missing resource.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
A watcher you have to scroll by hand is worse than the terminal, so the
page stays at the newest row for a reader who is already there. A reader
who scrolled back to read something is doing that on purpose and is left
where they are. Where the reader is has to be read before the rows
change and acted on after, because inserting rows is what moves the end
away from them.

What scrolls stays the host's: this finds the nearest ancestor the host
gave a scrolling overflow and falls back to the page when there is none,
so a chat placed in a box scrolls in its box. It writes no style and
adds no box of its own.

Watched live: the page now opens at the newest row, stays there across
appends, and left a reader parked at 2000px exactly where they were
while the transcript grew by 1496 characters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
A page that has stopped receiving looks exactly like a page on a quiet
session: nothing arrives on either. A reader cannot trust such a page
over the terminal, because "nothing is happening" and "I stopped
hearing" are the same picture — so the element says which of the two it
is, in words, beside the transcript.

Three states, each a different thing for the reader to do: trust it,
wait for it, or reload it. They are read from the subscription itself
rather than from the event that woke us, because an error says the
connection is gone and only the subscription says whether the browser
will bring it back.

The line sits after the transcript rather than in it. The host counts
the rows it rendered and the page keeps that many, so a line of the
viewer's own among them would shift every count after it.

The default sheet prints only the states a reader has to act on: a line
saying "following" on every page all day is noise, and a host that wants
it always prints it with one rule, the way it prints the times. The
theme moves to cc-chat as well as cc-transcript, so what stands beside
the rows is drawn in the same ink.

Measured in a browser on a live session: hidden while following; on a
host stopped under it the line appears and the rows stay; on the host's
return it goes back to following on its own, with one line on the page
throughout. A session that does not exist now says so instead of leaving
the page blank for ever.

The bundle grows 1 222 bytes gzipped, to 18 146; the CI gate goes to
19 000 with the measurement recorded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
The sheet printed only the states a reader has to act on, so a page that
was following said nothing at all. That still asks the reader to read
silence, and silence is exactly what a broken page and a quiet session
have in common — the thing this line exists to tell apart.

All three states are printed now. Following is said in the quiet ink, at
the small size and without a box, because it is the state a reader is in
all day; the two that need acting on keep their boxes. A host that wants
it back off the page writes one rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
A page said it was following whenever its socket was open. A socket says
nothing about hearing: freeze the host under the page and the connection
stays open, nothing arrives, and the browser reports no error, because
from its side nothing broke. The line then said "Following." over a page
that had stopped. Reproduced with SIGSTOP on the host, and it is the
same failure as pulling the network in the developer tools.

So the host now says something whether or not the session is busy — a
tail that keeps every row and adds none, every five seconds — and the
page believes it is following only while it is hearing. Fifteen seconds
of silence is a page that has stopped, not a session with nothing to
say; the limit holds two beats, so one late beat does not accuse the
host.

Nobody reopens a connection that is merely silent, because nobody else
can see it, so the page throws that one away and asks again. Asking
again costs nothing: the first thing a new connection is told keeps
nothing.

Growth and beats are merged before the split rather than after it, so
every message is made from the state as it stood when it was made. A
beat that raced a change could otherwise have told the page to drop rows
it had just been given.

Measured on a live session: following; the host frozen under it, the
line turned within the limit and the rows stayed; the host resumed, the
page went back to following on its own. On the wire, a quiet session
sends one four-byte beat every five seconds. The bundle grows 981 bytes
gzipped, to 19 127; the CI gate goes to 20 000.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Critical, from the ZIO review: a transcript shorter than what has already
been read stalled the follow in silence. The cursor kept pointing past
the end, every poll read nothing, and nothing said so; had the file grown
back past that offset, bytes belonging to other entries would have been
read as if they were the ones expected there. A transcript only grows
while it is the session's own, so a shorter one is a different file at
that offset: the follow now ends with PageSourceMoved, which is what the
reading path already says for a cursor whose source moved, and a page
that subscribes again starts from what the session now is.

The element's wiring had no test — every decision it makes was covered,
and the composition that carries a reader across a break was not. It is
covered now, against a page whose subscription, timer and clock belong to
the test: one subscription and not two, what arrives reaching the
transcript, the subscription and the timer given up when the page lets go
of the element, and a silent connection closed and opened again. Both new
tests were proved fallible before being trusted.

Also from the reviews: the line decoder is one function again rather than
one per direction, since a tail read and a follow read must agree on what
a line is; a follow that fails once it is running is logged the way every
other archive read is; the port says where following lives and why it is
not on it; the follow endpoint refuses caching and states its beat, so a
consumer that is not this bundle can read the silence rule off the
contract; and a test pins that rendered rows never end in a newline,
which is what keeps the line-framed message safe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Promoted:
- JSON on a wire a browser reads → kanon note #185, re-measured at triage:
  js.JSON.parse behind a facade costs 171 bytes where circe costs 166 KB and
  upickle 151 KB, and jsoniter-scala 19 KB — so the rule is not "no JSON" but
  "no Scala JSON stack in a page unless it shares real codecs with the JVM"
- a background host that died on startup → kanon note #186, on top of the
  source fix: the trap is runBackground's, not this project's, and it was
  misread again after this repo's CLAUDE.md already carried the correction

Fixed at source, no further home:
- a content-hashed name was remembered: a process that outlives its own build
  is a dev-host trap, and a shipped host carries its assets

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
The pre-push gate refuses a warning, and the new tests carried six: a
discarded js.eval, three handler calls whose return is nothing to us, and
two removals in statement position. Each now says so, and the handlers
are driven through named helpers that read as what they do to the page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Say that a removed row is nothing to us
Some checks failed
CI / Check Formatting (pull_request) Successful in 1m0s
CI / Compile (pull_request) Successful in 1m24s
CI / Check Linting (pull_request) Successful in 1m5s
CI / Run Tests (pull_request) Successful in 1m49s
CI / Check Bundle Size (pull_request) Successful in 52s
CI / Run Integration Tests (pull_request) Failing after 2m1s
a5bf5def71
A clean compile carried one warning the incremental runs did not: the
removal in show discards a Node. The pre-push gate refuses a warning, and
this one is in the element itself rather than in a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Assert that a cleanup test left no zombie of its own
Some checks failed
CI / Check Formatting (pull_request) Successful in 52s
CI / Compile (pull_request) Successful in 1m14s
CI / Check Linting (pull_request) Successful in 1m2s
CI / Check Bundle Size (pull_request) Successful in 48s
CI / Run Integration Tests (pull_request) Failing after 1m50s
CI / Run Tests (pull_request) Successful in 1m38s
e8246e4e6b
`countZombieProcesses` runs `ps` and counts every zombie on the machine,
not the ones the test made — so the five cleanup tests, which snapshot
that number and assert it returns to exactly what it was, fail whenever
somebody else's zombie is reaped while they run. That is what CI hit:
started at 2, ended at 1, refused for being one too few.

What a cleanup test can honestly claim is that it left nothing behind:
the count does not rise above what it found. The reason is written where
the count is taken, so the next reader does not tighten it back.

Unrelated to the slice this branch carries, and fixed here with Michal's
explicit approval rather than left to bite the next pull request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
Prove a subscriber is on the hub before comparing what it heard
All checks were successful
CI / Check Formatting (pull_request) Successful in 49s
CI / Compile (pull_request) Successful in 1m8s
CI / Check Linting (pull_request) Successful in 1m8s
CI / Run Tests (pull_request) Successful in 1m42s
CI / Check Bundle Size (pull_request) Successful in 49s
CI / Run Integration Tests (pull_request) Successful in 1m52s
ad4c9e367f
The two-subscriber test waited for each collector fiber to report itself
suspended, and took that for "subscribed". A fiber is suspended before it
subscribes as well as after, so a turn sent in that window reached one
collector and not the other — which is how CI saw one subscriber hold an
assistant message the other never got.

Measured while fixing it: acquiring the stream's pull is not the
subscription either. A probe published after `ZStream.fromHub(...).toPull`
returned and the pull never saw the element, so a hub stream subscribes on
its first pull, not when its pull is acquired. Waiting on the pull would
have been the same bug wearing a better disguise.

What does prove it is a message received. The first turns are the
handshake — offered until both collectors have caught one, since a
collector that was not yet on the hub misses the turn that went out — and
the turn after that is the one compared, reaching both or neither. The
mock answers every prompt with the same two lines, so the number of turns
the handshake took does not change what the compared turn holds.

Five consecutive runs of the suite are green where the old shape hung or
mismatched on roughly one run in three.

Unrelated to the slice this branch carries, and fixed here with Michal's
explicit approval.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
workflow(sl): close 03-follow-live-session (flush parked items)
All checks were successful
CI / Check Formatting (pull_request) Successful in 44s
CI / Compile (pull_request) Successful in 1m10s
CI / Check Linting (pull_request) Successful in 1m4s
CI / Run Tests (pull_request) Successful in 1m35s
CI / Run Integration Tests (pull_request) Successful in 1m56s
CI / Check Bundle Size (pull_request) Successful in 1m2s
4bbf212bc2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZX7Zfw7nhRQZQkJHfRoM4
mph merged commit 79f1870030 into main 2026-08-27 09:21:09 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/claude-code-query!66
No description provided.