Skip to content

Inspect saved REPL entry evidence from the Sidekick (#932) - #955

Merged
taras merged 2 commits into
mainfrom
agent/repl-entry-inspection-932-20261019
Oct 19, 2026
Merged

taras merged 2 commits into
mainfrom
agent/repl-entry-inspection-932-20261019

Conversation

@taras

@taras taras commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

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:

<REPL.Entry.Journal entry={22} />
<REPL.Entry.Prompt entry={22} prompt={9} field="error" />
<REPL.Entry.Source entry={22} />
<REPL.Entry.Output entry={22} />
  • Journal reports the complete saved entry failure, prompt outcome counts and a page of record metadata. Prompt reads the located turn's reply, request or error. Its promptStatus stays separate from the entry's status; parseable JSON from a failed turn still means failed execution.
  • Source reads the exact admitted document. Output reads what it printed, defaulting to the tail. Text pages count Unicode code points; Journal pages count records. Previous/next arguments pin the resolved entry, prompt and field so later admissions do not redirect a continuation.
  • Reads return structured objects with ordinary optional 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.
  • Failure receipts point to the admitted entry's Journal even while earlier history is selected. The shipped Recovery guide and available syntax teach this path without filesystem hints.

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 running only when this process owns it, otherwise unfinished. 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.
  • Existing forms/source-reading suites — 14 passed, 100 steps; deno task lint and deno task check both exited 0.

The own Planner and Architect both returned PASS on identical signed head 0361ad5726a9cf533d0e35f0cd94f8279bb88024. Detailed reports remain in planner-entry-inspection-feedback-0361ad57-20261019.md and architect-entry-inspection-feedback-0361ad57-20261019.md in 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 composability skip. CI supplies full Deno/Node/Bun coverage. The dependency-changing-push verify:clean condition 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

  • Historical partial-window feedback retains its old EntryOutput continuation spelling. A new response following that spelling is refused; current reads use dotted Output. Historical records are not re-evaluated or rewritten.
  • The successful signing recovery's Git numeric exit was not captured because its wrapper failed after Git ran. The commit, tested-tree equality and valid original-key signature independently establish the outcome. The original R3i first-failure file is a reconstructed excerpt, not complete raw output; discarded output is unrecoverable. Later correction failure logs are complete. Detailed evidence stays in the central reports.
  • An earlier unintended dependency install in the control checkout remains disclosed and untouched; its tracked files were unchanged. No cleanup is included here.

Scope confirmation

  • Every changed file supports this purpose.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated changes and their provenance are identified.
  • The description matches the final diff and recorded results.

taras and others added 2 commits October 19, 2026 05:44
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>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 redundant comments. Inline suggestions to remove them below.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is the thing being produced. What must succeed is the admission above.

@github-actions

Copy link
Copy Markdown

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:

  • no-unused-vars ×3: packages/cli/src/repl/sidekick/controls.ts
  • no-unnecessary-type-assertion ×1: packages/cli/src/repl/model.ts

Slop

  • packages/cli/src/repl/sidekick/entry-inspection.ts:458 — // could not be performed.
  • packages/cli/tests/fixtures/repl/issue-932/produce.ts:72 — // is the thing being produced. What must succeed is the admission above.

Static Analysis

Oxlint: 10 diagnostics across 3 files (6 rules)
Density: 0.003 violations/added-line

no-unused-vars (3): packages/cli/src/repl/sidekick/controls.ts
no-misused-spread (2): packages/cli/src/repl/sidekick/entry-inspection.ts, packages/cli/src/repl/sidekick/controls.ts
no-unnecessary-type-conversion (2): packages/cli/src/repl/sidekick/controls.ts, packages/cli/src/repl/model.ts
no-array-sort (1): packages/cli/src/repl/model.ts
no-unnecessary-type-assertion (1): packages/cli/src/repl/model.ts
no-unnecessary-template-expression (1): packages/cli/src/repl/model.ts

Correctness

FILE: packages/cli/src/repl/sidekick/controls.ts, PATTERN: no-unused-vars, CONCERN: Unused variable ENTRY_INSPECTIONS in entryInspections function, QUESTION: Why is ENTRY_INSPECTIONS declared but not used in the function body?

FILE: packages/cli/src/repl/sidekick/entry-inspection.ts, PATTERN: no-misused-spread, CONCERN: Spread operator used on non-iterable read in pageRecord function, QUESTION: What is read and why is it being spread?

FILE: packages/cli/src/repl/sidekick/controls.ts, PATTERN: no-misused-spread, CONCERN: Spread operator used on non-iterable read in pageRecord function, QUESTION: What is read and why is it being spread?

FILE: packages/cli/src/repl/sidekick/controls.ts, PATTERN: no-unnecessary-type-conversion, CONCERN: Unnecessary type conversion of entry.order to number in textPage, QUESTION: Why convert to number when entry.order is already a number?

FILE: packages/cli/src/repl/model.ts, PATTERN: no-unnecessary-type-conversion, CONCERN: Unnecessary type conversion of stated to number in resolveEntry, QUESTION: Why convert to number when stated is already a number?

FILE: packages/cli/src/repl/sidekick/entry-inspection.ts, PATTERN: no-array-sort, CONCERN: turns array sorted in resolvePrompt without clear justification, QUESTION: Why sort turns when using find on a sequence-based lookup?

FILE: packages/cli/src/repl/sidekick/controls.ts, PATTERN: no-unnecessary-type-assertion, CONCERN: Type assertion as ReplRecord[] in entryRecords, QUESTION: Why assert type when events.map already produces ReplRecord[]?

FILE: packages/cli/src/repl/sidekick/entry-inspection.ts, PATTERN: no-unnecessary-template-expression, CONCERN: Template expression in readEntryPrompt for field.value, QUESTION: Why use template literal when field.value is already a string?

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: Object.freeze({ ...read }) without explicit type, QUESTION: Why avoid explicit type annotation on frozen object?

@taras
taras marked this pull request as ready for review October 19, 2026 12:35
@taras taras changed the title ✨ Read a failed entry's saved evidence from the Sidekick (#932) Inspect saved REPL entry evidence from the Sidekick (#932) Oct 19, 2026
@taras
taras merged commit dd74be5 into main Oct 19, 2026
44 checks passed
@taras
taras deleted the agent/repl-entry-inspection-932-20261019 branch October 19, 2026 12:57
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.

Inspect saved entry failures from the xmd repl Sidekick

1 participant