Skip to content

fix: show a stopped event log in inspect and in a timed-out wait - #357

Merged
ThomasK33 merged 8 commits into
mainfrom
fix/inspect-shows-stopped-event-log
Nov 6, 2026
Merged

ThomasK33 merged 8 commits into
mainfrom
fix/inspect-shows-stopped-event-log

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Why

After #340 the host stops the event log at 52,428,800 bytes or at 250,000 events and writes one stop marker. Later output is not recorded. inspect showed only eventCount and eventLogBytes. A wait --text for text that arrived after the stop timed out and said nothing about the stop. A user saw frozen snapshots and waits and had no way to tell why.

This is the second version of the change. The first version read the stop marker from events.jsonl in the CLI with its own line parser. Review found these problems:

  1. It was a second parser for events.jsonl. It skipped bad lines and never checked that seq is contiguous. AGENTS.md keeps parsing and validation of the persisted log in src/storage/eventLogCodec.ts.
  2. At the moment the log stops, the host marks it stopped in memory before the marker is on disk. A wait that timed out at that moment read the disk and missed the stop.
  3. With batch --keep-going, every failed wait read the whole log again.
  4. inspect failed with INTERNAL_ERROR on a stop marker whose label names no limit. main prints the session there.
  5. inspect read the event count, the size and the stop in three reads. During the stop the numbers can disagree, with eventLogStop.seq above lastEventSeq.
  6. On an exited session, wait --text <later text> failed with REPLAY_ERROR and said nothing about the stop.
  7. No unit test failed when batch stopped passing the stop on.

What

Behavior a user sees:

  1. inspect --json has an optional field eventLogStop: { "reason": "bytes" | "events" | "unknown", "seq": <n> }. reason is the limit that stopped the log. seq is the seq of the stop marker. unknown means that the marker is in the log but its label names no limit. The host treats such a marker as a stop too, so inspect shows it and does not fail. Without --json, one line follows Event Log Bytes:, for example Event Log Stopped: at seq 7868 (byte limit); later events are not recorded. For unknown it reads (reason unknown).
  2. A render wait that times out on a stopped log adds The event log stopped at seq 7868 (byte limit), so output after it is not seen. to its message, with and without --json. error.details.eventLogStop carries { reason, seq }. The code stays WAIT_TIMEOUT and the exit code stays 11. A batch wait step adds the same sentence to its message. --idle-ms and --exit waits and waits that match do not change.
  3. On an exited session, wait replays the stored log. When the replayed screen does not satisfy the wait, the REPLAY_ERROR message ends with the same sentence and error.details.eventLogStop carries the same facts.

How it works:

  1. The stop marker is read in src/storage/eventLogCodec.ts, the one place that parses events.jsonl. EVENT_LOG_LIMIT_LABEL_PREFIX and eventLogStopLabel (the label the host writes) moved there from src/host/eventLog.ts. findEventLogStop(records) finds the first stop marker in records and reads its limit. EventLog.open and the offline wait use it. scanEventLog(path) streams the file once and returns { entryCount, byteCount, stop }. It uses the codec's own parseEventLogLine and assertSeqFollows (the checks of parseEventLogContent), so inspect and replay agree on what a valid record is. It does not enforce the 50 MB read limit, so inspect still works on a log that an older version wrote past it.
  2. scanEventLog looks for the stop only in the lines that replay accepts, up to the first bad line. A bad line ends the search and the count goes on. So inspect still shows a log that replay rejects, and it does not report a marker behind a torn last line.
  3. inspect takes the count, the size and the stop from one scanEventLog call. The scan opens the file, measures it, and reads exactly those bytes. A host that appends meanwhile cannot make the three numbers disagree, and eventLogStop.seq is never above lastEventSeq.
  4. A live wait takes the stop from the host. EventLog.getStop() reads it from the host's own record buffer. The marker is in the buffer as soon as recordStop takes its seq, before its write settles. The host's waitForRender timeout result carries it as the new optional field eventLogStop. The CLI never reads the log file for a wait, so the race is gone by construction.
  5. batch gets the stop from the same host result. A --keep-going batch with many failed waits does no extra read. The code in src/cli/commands/batch.ts is the same as on main.
  6. An offline wait takes the stop from replayInput.events, the events that the replay already read.
  7. Removed because nothing uses them now: readEventLogStop, readEventLogStopForTimeout, countEventLogEntries and statEventLogBytes.

CLI JSON and RPC contract changes. All are additions, no existing field changes:

  • inspect result: optional eventLogStop (reason is bytes, events or unknown).
  • waitForRender RPC result: optional eventLogStop, set only on a timed-out result of a host whose log stopped. It also appears under result of the WAIT_TIMEOUT envelope.
  • WAIT_TIMEOUT and the offline REPLAY_ERROR: one added sentence in the message and details.eventLogStop.

EventLogStopSchema and EventLogStopReasonSchema live in src/protocol/schemas.ts (strict) and messages.ts re-exports them. Schemas, the golden envelope tests, docs/USAGE.md, docs/TROUBLESHOOTING.md and the READMEs of src/batch, src/cli, src/cli/commands, src/host, src/protocol and src/storage changed in this PR.

Limits:

  • A CLI of an older version that talks to a host of this version gets RPC_ERROR ("RPC result failed validation for method waitForRender") instead of WAIT_TIMEOUT for a timed-out wait on a stopped log, because its result schema is strict. I saw this with the CLI of main against a host of this branch. A newer CLI with an older host works and adds no sentence.

  • When a log that agent-tty did not write leaves no room for the stop marker, the host stops without a marker. getStop(), inspect and replay then report no stop, because the stop is defined by the marker in the log.

  • inspect now parses every record up to the stop marker. On a 52 MB log, scanEventLog takes 101 to 129 ms and a line count alone takes 38 to 86 ms.

  • scanEventLog splits lines on \n only, as parseEventLogContent does, so an output that holds U+2028 or U+2029 counts as one record and a stop after it is found. (readline split on those characters, which also made the line count on main one too high.)

Validation

  • npm run format:check, npm run lint, npm run typecheck and .agents/skills/verify-session-lifecycle/scripts/check.sh --drift-only pass. The check went STALE because EventLog.open now calls findEventLogStop instead of some(isStopMarker). The lifecycle model still describes the code, because that call only sets the stopped flag that the model does not use. I ran --update-coverage, stopped it after it rewrote coverage.json (CI runs TLC), formatted the file and confirmed the check passes. Only the EventLog.open fingerprint changed.
  • npx vitest run test/unit: 147 files, 2240 tests pass. main has 2168, so 68 tests are new.
  • With --maxWorkers=1 --retry=0: 39 integration and e2e files that reach inspect, wait, batch, the host or the event log (all integration and e2e files except the eval ones and cross-backend-screen-hash) give 409 passed, 3 skipped (other-user-group needs a setpriv copy) and 3 failed. The 3 failed passed when I reran them alone: lifecycle.test.ts ("destroy of an exited session from a CLI that sees no /proc, in another pid namespace, names /proc while its leftover runs"), export-fixtures.test.ts ("captures a color-grid screenshot with a non-trivial PNG artifact") and screenshot-missing-glyphs.test.ts ("says the glyph check is incomplete when a screen has too many glyphs to probe"). The host was under heavy load from other work, and none of the three touches the event log stop. The first of them is the test that test: make the no-/proc destroy test independent of host timing #355 made independent of host timing. This branch is one commit behind main and does not have that fix yet. A trial merge with main has no conflict.
  • New and changed tests:
    • test/unit/storage/eventLogScan.test.ts (new): scanEventLog on missing, empty, counted, multibyte and over-limit files, stops from a real writer for both limits, look-alikes, first marker wins, unknown reason, bad lines before the marker (bad JSON, bad shape, gap, repeat, first seq not 0), the same logs through readEventLogRecords, a torn last line, agreement with EventLog.open, and one snapshot while a host appends.
    • eventLogCodec.test.ts: label wording, findEventLogStop and the unknown reason.
    • eventLog.test.ts: EventLog.getStop(), including that it reports the stop before the marker write settles.
    • wait.test.ts: the stop from the host result for each render flag, --json, the timeout wording, the three reason words, no stop, matched waits, legacy waits and the offline REPLAY_ERROR.
    • batch.test.ts (command level), executor.test.ts, inspect.test.ts, rpcServer.test.ts, messages.test.ts and golden-envelopes.test.ts.
    • test/integration/event-log.test.ts: a real host on a planted stopped log. Cases: live inspect, a timed-out wait, a failed batch, a --keep-going batch with two timed-out waits, an exited session (inspect and an offline wait), and an unknown marker.
  • Mutations. I broke the code once each and the named tests failed:
Mutation results
Mutation Red tests
scanEventLog skips the seq check reports no stop for a gap in the seq values, ... a repeated seq, ... a first seq that is not 0
scanEventLog skips the schema check reports no stop for a line that is not an event record before the marker
scanEventLog enforces the 50 MB limit reads a log that is over the size limit that replay reads
scanEventLog reads to the live end of the file describes one snapshot of the file ...
inspect reads the stop in a second call takes the count, the size and the stop from one read of the log
inspect drops the size from the scan takes the count, the size and the stop ..., surfaces eventLogBytes in both live and offline modes
A stop marker with no limit throws again 7 tests, among them reports a stop marker that names no limit ... reason unknown, keeps honouring a marker with the reserved prefix ... and the five findEventLogStop cases
EventLog.getStop() returns undefined 5 tests, among them reports the stop as soon as the append that stops the log returns, before its marker is written
The host timeout result leaves out eventLogStop event-log.test.ts (integration): shows the stop in inspect, in a timed-out wait and in batch waits of a live session and shows a stop marker that names no limit ...
The CLI ignores the stop of the host result 10 tests in wait.test.ts, among them names the stop that the host reports in the WAIT_TIMEOUT of a --text wait
The CLI leaves details.eventLogStop out 8 tests in wait.test.ts
The offline wait passes no stop names the stop of the replayed events when the snapshot does not satisfy the wait, names a stop whose label names no limit
The batch executor ignores the stop of the result executor.test.ts: names where the host reports ..., names a stop in each timed-out wait of a keep-going batch. batch.test.ts: names the stop in every timed-out wait of a --keep-going batch, names the stop in the error of a failed batch under --json
WaitForRenderResultSchema drops the field batch.test.ts (2 tests), rpcServer.test.ts: carries the event log stop of a timed-out wait to the client, messages.test.ts: accepts a timed-out waitForRender result that carries the event log stop
The word for unknown changes words the unknown reason as "reason unknown", names a stop whose label names no limit, reports the stop of the stored log with the unknown reason, from a live host
  • The host wiring (hostMain.ts passes getStop() into its timeout result) has no unit test, because the handler lives inside runHost. The integration test above covers it. getStop() itself has unit tests.
  • Dogfood with the real CLI (node --import tsx src/cli/main.ts) against real sessions that stopped. The same input ran under the CLI of main and under this branch.
    • Byte limit: a program printed 2,500 full-colour 80x24 screens, then AFTER-THE-STOP. The log stopped at seq 6084 with 52,423,099 bytes. Under inspect --json (live) eventLogStop is {"reason":"bytes","seq":6084} and eventCount is 6085, so the stop is the last counted record. wait --text AFTER-THE-STOP --timeout 3000 exits 11 with Wait timed out before its condition was met. The event log stopped at seq 6084 (byte limit), so output after it is not seen. and details.eventLogStop. A batch --keep-going with two timed-out waits puts the sentence in both steps. wait --regex '\S' still matches the stored screen.
    • After the program exited: inspect shows the same eventLogStop (eventCount 6086). wait --text AFTER-THE-STOP fails with REPLAY_ERROR. Its message ends with the stop sentence and details.eventLogStop is {"reason":"bytes","seq":6084}. On main the same command gives the REPLAY_ERROR message without the sentence.
    • Event limit: a program wrote 300,000 single characters 1 ms apart. The log stopped at seq 249998 with 21,445,667 bytes. The same checks give (event limit), in inspect, in a timed-out wait, in both steps of a --keep-going batch, and in the offline REPLAY_ERROR after the exit.
    • Unknown reason: a session whose log holds a reserved-prefix marker with the label ... (soon). This branch: inspect exits 0 with {"reason":"unknown","seq":1}, the line Event Log Stopped: at seq 1 (reason unknown); later events are not recorded, and a timed-out wait that says (reason unknown). The PR head 6229dbe fails with INTERNAL_ERROR: stop marker label must name its limit. main prints the session.
    • Time of inspect --json on the byte-limit session, 3 runs each, including about 1 s of start-up: live 1.16, 1.13, 1.12 s here, 1.53, 1.23, 1.11 s at the PR head, 1.44, 1.07, 1.08 s on main. Exited: 1.10, 1.12, 1.23 s here and 1.00, 1.04, 1.16 s on main. I measured no difference beyond noise. scanEventLog alone: 101 to 129 ms on this log, against 38 to 86 ms for a line count.

Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh

When the event log reached its byte or event limit, inspect gave no sign
of it and a wait for later output timed out without a reason.

- inspect --json adds eventLogStop ({ reason: 'bytes' | 'events', seq }),
  read from the stop marker in events.jsonl, and prints one line for it.
  The field is absent when the log has not stopped.
- A render wait that times out, and a timed-out batch wait step, add a
  sentence that names the stop. The error code stays WAIT_TIMEOUT.
- readEventLogStop finds the marker with the check that EventLog.open
  uses to start a reopened log stopped, so both agree.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T12:03:27.132912Z fe9a01d Manual request
🔒 Security Review ✅ Completed 2026-11-06T12:03:25.112728Z fe9a01d Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 6229dbe993

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6229dbe993

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/host/eventLog.ts Resolved
Comment thread src/cli/eventLogStop.ts Resolved
Comment thread src/cli/commands/batch.ts Resolved
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: aa5ef15e44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: aa5ef15e44

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

readline also splits on U+2028 and U+2029, which JSON.stringify writes
raw, so inspect missed a stop after such an output and counted one entry
too many. The scan now splits on '\n' and joins the pieces of a long line
once. Tests pin both separators, a bad line before a marker whose seq
follows, and an unclosed limit.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: ffe36ccbf5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: ffe36ccbf5

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: fe9a01db99

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: fe9a01db99

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit 7334f03 Nov 6, 2026
26 checks passed
@ThomasK33
ThomasK33 deleted the fix/inspect-shows-stopped-event-log branch November 6, 2026 13:31
@github-actions github-actions Bot mentioned this pull request Nov 6, 2026
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