Repository navigation
Inspect saved REPL entry evidence from the Sidekick (#932) - #955
Conversation
An entry that failed with nothing printed could not be explained. The only
read a reply had was its output, so an entry whose nine agent turns ended with
a refusal — and whose output was empty because every turn captured its reply —
answered with an empty window and nothing else. What it ran, what it asked,
what came back and where any of that is in the file were all in the journal
and reachable by nothing.
Four bounded reads answer those four questions:
```xmd
<REPL.Entry.Source entry={22} offset={0} limit={4096} />
<REPL.Entry.Prompt entry={22} prompt={9} field="reply" />
<REPL.Entry.Output entry={22} />
<REPL.Entry.Journal entry={22} offset={0} limit={20} />
```
Journal locates evidence and Prompt reads the selected turn, which is why
neither carries the other's body. A Journal page states the entry's complete
saved failure, how its turns ended, and one record per saved event — marker,
type, name, status, position, prompt number and agent, and no reply, request,
result payload, checkpoint or stack.
**Selectors and windows resolve once and are stated absolutely.** `entry` and
`prompt` are nonzero whole numbers; a positive entry number is admission order
and a positive prompt number is a saved sequence plus one, so a turn the file
never saved refuses rather than being renumbered into its neighbour. Text
windows count Unicode code points and default to 4096, Journal counts records
and defaults to 20, and every `previous`/`next` is either null or the complete
arguments of the read beside it — the resolved numbers, an absolute offset, the
requested limit and the same Prompt field. A continuation followed after
another entry has been admitted still reads the entry it was about.
**Absence, emptiness and refusal are three answers.** A completed turn's error
is `text: null`; a turn that saved an empty reply is `""`; evidence the history
does not hold refuses, and the refusal is that control's outcome with the
response going on. Entry status and `promptStatus` stay separate, so a turn
that returned parseable JSON and ended failed is failed.
**Cold reads are passive.** Terminal evidence decides an entry's status even
while its task unwinds; without one, only the entry this process is printing is
running and any other is unfinished, with an empty output window rather than an
invented one. No read opens an entry, resumes one, waits for one, executes
source or reaches an entry's provider.
**Delivery is ordinary.** Each read answers with its page object: `as` binds it
by reference, an uncaptured read renders nothing, and the same page goes back
as the read's own designated result either way. Only an explicit `<Output>` is
chat. A submission's receipt now names the admitted entry by that number and
points a completed entry at `<REPL.Entry.Output />` and a failed or cancelled
one at `<REPL.Entry.Journal />`, because an entry that failed may have printed
nothing.
**What is intentionally unchanged.** Four production files move:
`repl/model.ts` gains detached, deeply frozen per-record metadata formed inside
the existing validated projection; private `sidekick/entry-inspection.ts` holds
the pure selectors, windows and page builders; `sidekick/requests.ts` answers
each read from one acknowledged head and one printing fact; and
`sidekick/controls.ts` declares the four controls and designates them. Raw
`DurableEvent`s still terminate at the model. No new persistence, index,
cursor, ownership or recovery service, generic journal browser, Query
dependency or automatic feedback mechanism. `REPL.EntryOutput`'s historical
designation, its renderer and `output.ts`'s legacy types are untouched, and a
chat that already holds one of its results still reads as the window it was.
Evidence, all on this tree. The fixture under
`packages/cli/tests/fixtures/repl/issue-932/` is bytes a real run left behind —
`produce.ts` drives the production session, entry executions, canonical Prompt
path and paired storage with the scripted provider, and its receipt states what
it holds and the one machine-specific string substituted out of it.
- `deno task test packages/cli/tests/repl-model.test.ts
packages/cli/tests/repl-sidekick-controls.test.ts` — 15 passed. One record
per saved event with semantic outcomes and no payload retention; relative
reads pinning their continuations across a later admission; Journal paging to
the final close; defaults, tail, negative, clamped and beyond-end positions;
canonical ordering over a resequenced copy of real records; the null, empty,
missing, zero, fractional, unknown-field and bad-limit boundaries; and all
four mounted controls over a cold unfinished prefix with every execution hook
set to fail if reached.
- `deno task test packages/cli/tests/repl-sidekick-journey.test.ts
packages/cli/tests/repl-sidekick-recovery.test.ts
packages/cli/tests/repl-sidekick-output.test.ts` — 22 passed. One real
inspection exchange over a run-produced saved failure, with the entry file
byte-identical afterwards; capture, identical feedback with and without `as`,
Output-only chat and ordinary refusal delivery; the first turn being told the
four names and `<Guide.Recovery />`; a scripted agent reading the shipped
chapter and following it; and a saved `REPL.EntryOutput` pair still rendered
by its own renderer.
- `deno task test packages/cli/tests/repl-sidekick-presentation.test.ts` — 22
passed, including the receipt rows that submit from a screen frozen at an
earlier recorded position.
- `deno task test packages/cli/tests/repl-forms.test.ts
packages/cli/tests/repl-source-reading.test.ts` — 14 passed, the two
hand-built `ReplEntry` constructors the new member reached.
- `deno task lint` and `deno task check` — exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three bounded corrections on the reviewed head, from the Planner and Architect
feedback for `b6eb083e`. No production path, protocol, storage, index or
recovery machinery is added, and no settled contract moves.
**The read the four replaced is retired from admission, not from history.**
`sidekickObservations()` no longer offers `REPL.EntryOutput`, so the capture
never seals it and a response written now that names it is refused before any
control runs. Everything that reads a *saved* result stays exactly as it was:
the name keeps its `DESIGNATED` membership, `feedback.ts` keeps its case,
`renderRead` keeps its wording, and `output.ts`'s reader and types are
untouched — a conversation reopened from a chat holding that control's own
pairs is still told what its history says it asked for. Current executable
admission and the decoding of a retained result are separate responsibilities,
and only the first one moved. The active response examples and tests move to
`<REPL.Entry.Output />` and its page semantics; the direct legacy reader suite
and the saved-pair compatibility row are unchanged.
**A failure released while an earlier position is selected still names its own
entry.** The receipt rows that existed submitted from the live head, including
the one that reopens at a recorded position and goes back live first, so none
of them delivered a receipt while something else was selected. The new
presentation row admits the failing entry at the head, holds its outcome inside
the host before it can be appended, freezes the screen at the first entry's
recorded outcome while it waits, and only then releases it. The screen is still
frozen when the receipt is composed, and both the shared renderer and the
retained feedback name `entry={3}` with the exact Journal instruction and
neither earlier entry.
**A captured control-only read is the third E5 case.** The existing exchange
read uncaptured, then captured and published through `<Json>` inside Output. It
now reads a third time with `as="page"` and no Output at all: the page feedback
is byte-identical to the other two, the response declares no output region, and
the chat holds it as the reply it was with nothing to say.
The fixture's chat half is pinned rather than regenerated. It holds a genuine
`REPL.EntryOutput` pair that a corrected ceiling cannot produce, so `produce.ts`
now writes the entry half alone and `RECEIPT.md` states each half's provenance
separately, names the pre-retirement commit the chat half was taken at, and
records the exact recipe for reproducing it there. Its bytes and digests are
unchanged: `entries.jsonl` `981217b8…`, `sidekick.jsonl` `fbd0175e…`.
Evidence, all on this tree, each command's complete output captured before any
rerun under `.development/stack/932/evidence/`:
- `deno task test packages/cli/tests/repl-model.test.ts
packages/cli/tests/repl-sidekick-controls.test.ts` — exit 0, 15 passed, 94
steps. Covers the surface omitting the retired name while the designated set
retains it, the refusal of a response that writes it, and the dotted read
answering with its page.
- `deno task test packages/cli/tests/repl-sidekick-journey.test.ts
packages/cli/tests/repl-sidekick-recovery.test.ts
packages/cli/tests/repl-sidekick-output.test.ts` — exit 0, 22 passed, 97
steps. Covers the third E5 capture case and the saved-pair compatibility row.
- `deno task test packages/cli/tests/repl-sidekick-presentation.test.ts` — exit
0, 22 passed, 75 steps, including the new held-failure row.
- `deno task lint` and `deno task check` — exit 0, rerun because production and
test scope moved and the dead-code gate reads the import graph.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| if (!Number.isInteger(stated) || stated <= 0) { | ||
| // Nonpositive and fractional limits are refused rather than rounded: a read | ||
| // that silently became a different read is worse than one that said it | ||
| // could not be performed. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // could not be performed. |
| throw failed.error; | ||
| } | ||
| // Joined and not checked: this entry is supposed to fail, and its outcome | ||
| // is the thing being produced. What must succeed is the admission above. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // is the thing being produced. What must succeed is the admission above. |
PR #955: ✨ Read a failed entry's saved evidence from the Sidekick (#932)21 files, +2920 / -175 Scope🔴 PR has 3095 lines changed. Split into focused PRs. 🟡 3095 lines changed. PRs under 400 receive more thorough review. 🟡 21 files changed. Are all changes related? Structural🟡 2 console statements. Oxlint structural signals:
Slop
Static AnalysisOxlint: 10 diagnostics across 3 files (6 rules) no-unused-vars (3): packages/cli/src/repl/sidekick/controls.ts CorrectnessFILE: packages/cli/src/repl/sidekick/controls.ts, PATTERN: no-unused-vars, CONCERN: Unused variable FILE: packages/cli/src/repl/sidekick/entry-inspection.ts, PATTERN: no-misused-spread, CONCERN: Spread operator used on non-iterable FILE: packages/cli/src/repl/sidekick/controls.ts, PATTERN: no-misused-spread, CONCERN: Spread operator used on non-iterable FILE: packages/cli/src/repl/sidekick/controls.ts, PATTERN: no-unnecessary-type-conversion, CONCERN: Unnecessary type conversion of FILE: packages/cli/src/repl/model.ts, PATTERN: no-unnecessary-type-conversion, CONCERN: Unnecessary type conversion of FILE: packages/cli/src/repl/sidekick/entry-inspection.ts, PATTERN: no-array-sort, CONCERN: FILE: packages/cli/src/repl/sidekick/controls.ts, PATTERN: no-unnecessary-type-assertion, CONCERN: Type assertion FILE: packages/cli/src/repl/sidekick/entry-inspection.ts, PATTERN: no-unnecessary-template-expression, CONCERN: Template expression in FILE: packages/cli/src/repl/sidekick/entry-inspection.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file (no-misused-spread, no-unnecessary-type-conversion), QUESTION: Are these generated or unreviewed code fragments? FILE: packages/cli/src/repl/sidekick/entry-inspection.ts, PATTERN: object literal assertion, CONCERN: |
Why
A failed REPL entry can print nothing even after several agent turns have completed. Previously, the Sidekick could read its rendered output but could not inspect the saved source, prompt results or failure through its supported controls. It needed journal-file knowledge to explain what happened before proposing a retry.
Closes #932. Follows the merged Slice delivery, PR #952 (#942). Query/Yaml (#934) and the combined diagnostic example remain separate work.
What changes
Before: an empty output window could hide eight completed turns followed by a ninth that returned valid JSON but failed.
After: the Sidekick can inspect that evidence without rerunning the entry:
promptStatusstays separate from the entry'sstatus; parseable JSON from a failed turn still means failed execution.as. The explicit control/result feedback arrives with or without capture; only explicit top-level<Output>publishes chat. Missing evidence refuses, absent saved errors are null, and saved empty text remains empty.How it works
The existing validated journal projection supplies detached record metadata to four private page builders. Requests read one acknowledged head and the exact owned printing fact, then the existing control/result path delivers the page to the same conversation.
Inspection never opens, resumes, waits for or executes an entry, contacts its provider, or modifies its saved evidence. Terminal evidence wins; an entry without an outcome is
runningonly when this process owns it, otherwiseunfinished. Existing publication and secret protections remain in force.New responses cannot invoke
REPL.EntryOutput. Its saved controls/results keep their designation, legacy types and historical renderer. No storage format, persistent index, recovery service, generic journal browser, automatic-feedback framework or dependency is added.How to verify it
Retained focused evidence, all passing:
deno task test packages/cli/tests/repl-model.test.ts packages/cli/tests/repl-sidekick-controls.test.ts— 15 passed, 94 steps. Covers canonical metadata, pinned Unicode/record paging, absence/refusals and passive cold reads.deno task test packages/cli/tests/repl-sidekick-journey.test.ts packages/cli/tests/repl-sidekick-recovery.test.ts packages/cli/tests/repl-sidekick-output.test.ts— 22 passed, 97 steps. Covers diagnosis of a real saved failure without repeated work, structured captures, control-only feedback, guide discovery and historical results.deno task test packages/cli/tests/repl-sidekick-presentation.test.ts— 22 passed, 75 steps, including failure delivery while earlier history stays selected.deno task lintanddeno task checkboth exited 0.The own Planner and Architect both returned PASS on identical signed head
0361ad5726a9cf533d0e35f0cd94f8279bb88024. Detailed reports remain inplanner-entry-inspection-feedback-0361ad57-20261019.mdandarchitect-entry-inspection-feedback-0361ad57-20261019.mdin the central delivery record.Required CI green succeeded on that exact head at 2026-10-19 12:47:07 UTC: 43 successful checks and the expected ordinary-PR
composabilityskip. CI supplies full Deno/Node/Bun coverage. The dependency-changing-pushverify:cleancondition does not apply to this diff.Generated or mechanical changes
The saved-entry fixture comes from a real REPL run with scripted transport. Its chat half is pinned to the pre-retirement producer so it contains an actual legacy EntryOutput control/result pair. The receipt records provenance and the substituted machine-specific stack. Fresh temporary session identities change regenerated hashes.
Risks and limitations
Scope confirmation