Skip to content

Fix/stale summary jobs - #18

Merged
R0MADEV merged 19 commits into
mainfrom
fix/stale-summary-jobs
Aug 28, 2026
Merged

Fix/stale summary jobs#18
R0MADEV merged 19 commits into
mainfrom
fix/stale-summary-jobs

Conversation

@R0MADEV

@R0MADEV R0MADEV commented Aug 28, 2026

Copy link
Copy Markdown
Owner

No description provided.

R0MADEV and others added 19 commits August 26, 2026 09:34
The session-end hook stored the transcript, created a summary job and
processed it in the same run. If that run died, the job stayed as it was
and nobody ever picked it up again: 87 pending and 2 processing jobs had
piled up, transcripts saved that would never get a summary.

The hook now drains a small batch of those before handling the session
that triggered it, so the queue empties on its own, session by session.
It self-limits on both ends — a staleness margin above the hook's own
safety timeout, and an attempts cap — so it neither delays the current
session nor retries forever a job that can never be summarized.

Completing a job was about to be written twice, so it was extracted to
resolveSummaryJob and both paths now share it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every failed summary job carried the same text — "El resumidor no
devolvió un resultado válido." — which told apart nothing: an agent that
was never installed, a session that was never logged in and a reply that
arrived but was useless all read the same. The agent's real output was
right there and got thrown away.

It is now stored as the error, and only an agent that returned nothing at
all gets a message of its own, so a failed job is diagnosable without
reproducing it.

Both paths that resolve a job repeated that generic message separately,
so both were changed: the hook's, and the panel's retry button, where the
decision moved to classify_summary to be testable on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three points are kept rather than deleted: the why of each one is
still what someone touching that area needs, and the "decided against"
list only works next to it.

The DMG one needed no code — the bundling failure was orphaned mounts
from earlier attempts, and a full build now runs bundle_dmg.sh clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`spawn_detached` put the daemon in its own process group but left it in
our session, so closing that terminal sent it SIGHUP and it died minutes
later, looking like a crash. Pressing "n" in the panel then did nothing,
because the request failed against a daemon that was no longer there.

setsid() rather than both: it leaves the session and gives the child its
own group, and asking for a group first makes setsid() fail with EPERM
for already being a group leader.

Requests now time out too. They are awaited on the panel's event loop, so
an answer that never came froze the whole TUI — no redraw, no keys —
which is what "it stops working out of nowhere" was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The terminal panel drew its own list and Review drew another by hand,
with its own focus colour and its own idea of what a row looks like.

`Sidebar` is the left rail: a titled section, rows of a label over a
dimmed detail with a status dot, an optional action pinned to the bottom,
and fixed header lines for the context a panel needs on screen at all
times. `Pane` is its counterpart on the right: a titled frame that hands
back the area inside it.

Both are covered against the rendered buffer rather than by eye, because
the hit-testing has to mirror what is actually painted — a click that
selects a different row than the one under the pointer is worse than no
click at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Connecting used to hand the whole screen to the remote pty, so the rail
disappeared and there was no way back to the list until the session died.

A pty speaks in absolute coordinates — "go to row 5", "clear the screen"
— so its bytes could not simply be drawn in a box: cleared would have
wiped the rail with it. They now go through a vt100 parser and the
resulting grid is painted into the right pane, which is what lets the
rail stay put.

The rail keeps working while attached: dragging, folding, and clicking
another row to switch to that terminal. F12 returns to the list; F5 opens
an agent without leaving. Both are typable on any keyboard layout, unlike
telnet's Ctrl+], which on a Spanish one already needs AltGr.

vt100 underflows on a grid one row or column wide, so there is a floor
with the reason written down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The desktop app had its own orchestration in TypeScript while the CLI and
the phone client used this engine, and the two had drifted: parallelism,
lexis context, file contents in the prompt and repository snapshots each
existed in one and not the other.

The analyses now run at once rather than one after another, so three
agents no longer take three times as long. Their text is collected per
stage instead of streamed, because three agents writing into one buffer
interleave into nonsense; each report is emitted whole under its own
heading, in the order the agents were chosen.

The prompt carries the changed files with the same per-file budget the
desktop uses, and lexis is asked the same question — impact, callers,
tests, blast radius — so a review reads the same wherever it was launched
from. A snapshot before and after says whether the repo moved underneath
it, which turns a stale report into a reported one.

With several agents the last one no longer analyses: it reads the others'
analyses from disk and verifies them. Handing it the reports inline meant
truncating each to fit, so it graded material that stopped mid-sentence.
It gets the full review prompt as well — it did not analyse, so that call
is its only sight of the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cancelling aborted the task reading the socket and reported "cancelado"
while the agents carried on running and billing. A client that goes away
now cancels the run: the agents are spawned with kill_on_drop, so losing
the race against the token is what kills the process.

The daemon notices on its next write, so an agent that has been silent
for a while keeps going until it says something — better than the old
behaviour, which was never to stop at all.

The rail's action button also ran start_run() while labelled "Parar",
starting a second review instead of cancelling the first.

Each report now says which agent wrote it. The marker carries the stage's
own label, which already distinguishes an agent's pass from a slice of a
large diff — calling both "pass N" claimed three agents had run when one
had read the diff in three pieces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`review_run` streams the engine's events to the frontend the way the
agent and pty commands already do, and `review_cancel` stops a run by id
so Stop can reach one started by an earlier call.

The frontend still orchestrates its own pipeline; this is the backend it
will move onto. It was not switched over yet because the engine does not
cover three things that side does: building the final document, saving a
checkpoint after each stage, and salvaging what completed when a run
fails halfway. Migrating without those would make the desktop worse
exactly when a review crashes, which is when it matters most.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel orchestrated its own agents while the CLI and the phone client
used `bento_review::engine`, and the two drifted feature by feature:
parallelism, lexis context, per-file budgets, snapshots and a verifier
that reads the other analyses each landed in one and not the other.

It now calls `review_run` and rebuilds the runs the drawer renders from
the engine's events, so the final document, the per-stage checkpoints and
the salvage on failure keep working as they did. Stop calls
`review_cancel`, which kills the agents rather than only unsubscribing
from a stream that is still being paid for.

The overview travels as the review context: it carries the PR number,
title and body, and the engine only ever sees the diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the phone

It ran the engine in-process, which shared the logic but not the process:
a review died with the app, and the TUI could not see one started from
the desktop. All three now send `review.run` to the daemon, so a review
belongs to the daemon and outlives whoever asked for it.

Reading that stream back into events lived in the CLI, so this would have
been a second copy of the wire format — which is how two clients stop
agreeing on it. It moved to `bento_review::stream` and both parse with
it.

Stop drops the connection, which the daemon already reads as "cancel this
run" and kills the agents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The stale-job sweep selected a job and then marked it processing in a
separate statement, so two session-end hooks running at once could both
take the same one and pay for the summarizer twice. Claiming it is now a
single conditional update witnessed by the timestamp the sweep read:
whoever loses the race gets no rows back and skips it.

The Review tab awaits daemon calls on its event loop, so a slow one (PRs
shell out to `gh`) stops the redraw and the keys. It still blocks — that
needs the requests moved off the loop — but it now paints a frame saying
so first, instead of looking like it died. The header's own line count
follows that flag, so clicks keep landing on the row under the pointer.

The mouse goes to the remote program while it asks for it, which is what
makes vim and htop usable; the rail takes it back when the program drops
tracking. vt100 does not track the mode, so it is read off the stream.

The full-screen Review views drew square, undimmed borders while the rest
of the TUI is rounded and grey, which read as a different application.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Asking the daemon was awaited inline, so the panel stopped redrawing and
stopped taking keys for as long as the call lasted — and `review.prs`
shells out to `gh`, which can take a while or hang. A loading line made
that visible last time, but it was still frozen.

Deciding what to ask is synchronous now and the asking runs in its own
task, reporting back on a channel the loop selects on alongside the
review stream. Switching tab changes the tab immediately and fills the
rows in when the answer lands; the keys keep working meanwhile.

The indicator counts requests rather than holding a flag, so two
overlapping ones do not have the first to land clear it. Both channels
are awaited through one method because `select!` cannot borrow the state
twice while the same loop also has to draw it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… screen

Running a review switched to a full-screen view, so the rail and the file
list vanished for as long as it lasted — and you were somewhere else,
rather than watching the report next to the files it is about.

`Drawer` is the third component: the right-hand column, holding the
conversation about what the pane shows. It folds like the rail, with the
chevron mirrored, remembers the width it had, and only takes room when
there is a report in it. Its edge drags like the rail's, towards the
middle rather than away from it.

The full-screen output view is gone with it, and the keys it owned —
asking a follow-up, paging through the report — moved to the panel, where
the report now is. Its "d" would have clashed with deleting a checkpoint,
so folding is "w".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Its visibility was derived from "is there any output", so a run that died
before writing anything — an agent that is not installed, a bad base —
ended with the drawer gone and no sign it had ever run. The one moment
the panel had something to say, it said nothing.

Visibility is an explicit flag now: once open it stays until folded.

The reason was lost too. The daemon reports failures as progress, and
progress is only shown while running, so it vanished with the drawer. A
failure goes into the report instead, where it is still readable after
the run stops; tool lines stay as progress, because they are noise once
the report exists.

Also removed the TypeScript pipeline the engine replaced: the prompt
builders, the context provider that gathered lexis and file contents, and
the Tauri commands behind them. Nothing imported them any more, and dead
copies of logic that moved are how it grows back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…agents

The drawer had no focus of its own, so the arrow keys never reached it:
right stopped at the file list and up/down always moved a selection
somewhere else. It is a third focus now, where the arrows scroll —
there is nothing to select in a report — and right skips it when it is
closed, so the keys never land somewhere invisible.

Its hint also said "d plegar" while the key is "w", which is the third
stale shortcut label this panel has had.

And the bug the review itself found: with exactly two agents the second
is the verifier, leaving one analysis, but the verification only ran with
two or more reports. The agent was picked, shown in the rail, and never
run. It is gated on having a verifier and something to read now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A checkpoint was filed under cwd+base alone, so a second review of the
same branch overwrote the first — its report, and the verifier session
you could still ask questions of. Which is the one worth keeping: the
last agent read everyone's analysis and wrote the final report.

Each run now has its own id, stable across the saves one review makes as
it goes and different between runs, so both survive. A checkpoint without
one keeps its old location, so anything already saved stays findable.

The drawer lists them: "l" swaps it between the report and the reviews
saved for this project, with the date, the branch and whether it can
still be resumed. Enter opens one — report and session together, so the
conversation picks up where it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit added the field to the shared checkpoint and to the
CLI, but not to the Tauri command or its caller — so the desktop stopped
compiling, and I committed it without building that side.

Every review the desktop runs is now its own entry as well, rather than
overwriting the branch's last one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The run id reached the shared checkpoint, the CLI and the desktop, but
not the phone: it went on saving under cwd+base alone, so a second review
of the same branch still overwrote the first there.

Its history now opens the run you picked rather than the branch's newest,
which is what several-per-branch needs to be worth anything, and the HTTP
get takes a run id for it. Without one it still answers with the latest,
so a client that has not been updated keeps working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@R0MADEV
R0MADEV merged commit a5590a1 into main Aug 28, 2026
6 checks passed
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