Skip to content

Anchor the --status exit-code footer scan to the log tail - #153

Merged
konard merged 4 commits into
mainfrom
issue-150-140b61c8f491
Aug 4, 2026
Merged

Anchor the --status exit-code footer scan to the log tail#153
konard merged 4 commits into
mainfrom
issue-150-140b61c8f491

Conversation

@konard

@konard konard commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #150.

$ --status could report an exit code the command never produced: the exit code of a detached session was derived from an unanchored Exit Code: N scan over the whole session log, so any text the wrapped command printed containing that substring was indistinguishable from the terminal footer start appends itself.

Changes

Applied in both implementations (js/src/lib/status-formatter.js and rust/src/lib/status_formatter.rs):

  1. Anchored footer matching. The exit code is now read only from the three-line footer block start writes itself, matched at line starts:

    ==================================================
    Finished: 2026-07-30 23:36:20.295
    Exit Code: 0
    

    A bare Exit Code: N inside a JSON payload, a quoted log excerpt or an rg -n dump no longer counts.

  2. Tail-only read. Only the last 16 KB of the log are scanned (the footer is always last), which also removes a full-file read from every --status call. The partial first line of the tail slice is dropped so a mid-line fragment cannot act as a line start.

  3. Backend beats the log text. In the !alive branch the resolution order is now docker inspect .State.ExitCode → anchored footer → 137 when the only evidence left is the OOM observation ($ --status reports executed / exitCode 137 from the container OOM flag while docker inspect still says running (session later exits 0) #151) → -1 sentinel (was: footer first). docker inspect is authoritative and cannot be spoofed by command output.

Item 4 from the issue (a machine-readable, per-execution marker such as the execution UUID in the footer) is not included — it touches every footer writer in both implementations and would change the on-disk log format; worth a separate change if the anchored footer proves insufficient.

Reproduction

The issue's CLI reproduction, run against this branch (Docker, --isolated docker --detached, container docker rm -f'd with the host-side watcher killed so the window is permanent):

$ -i docker -d --image alpine:3 --session "$SESSION" -- sh -c 'echo "Exit Code: 1"; sleep 300'
pkill -9 -f "docker logs -f '?$SESSION'?"; docker rm -f "$SESSION"
$ --status "$SESSION"
before after
status executed executing
exitCode 1 (fabricated) not set

And with the container still inspectable (SIGKILL → 137) while the command printed a verbatim forged footer claiming Exit Code: 0, --status now reports the backend's exitCode 137.

Tests

  • js/test/regression-150.js — 11 tests: the incident payload, bare/mid-line Exit Code: lines, CRLF logs, negative codes, tail-boundary handling, and enrichDetachedStatus() on a detached-docker record whose container cannot be inspected. 10 of them fail on main.
  • rust/tests/regression_150.rs — the same 11 cases for the Rust implementation.
  • experiments/issue-150-forged-exit-code.mjs — end-to-end reproduction through the real $ --status CLI (no mocks, no Docker daemon needed); exits non-zero on main, zero with the fix.

Full suites pass locally: bun run test (706 pass, 1 pre-existing execution-store lino failure that also fails on main), cargo test (all targets), plus bun run lint, bun run format:check, cargo fmt --check, cargo clippy -D warnings.

Release triggers: js/.changeset/issue-150-anchored-exit-code-footer.md and rust/changelog.d/issue-150-anchored-exit-code-footer.md (both patch).

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #150
@konard konard self-assigned this Aug 4, 2026
`$ --status` derived a detached session's exit code from an unanchored
`Exit Code: N` scan over the whole session log, so any text the wrapped
command printed containing that substring was indistinguishable from the
footer `start` appends itself, and the fabricated code was reported as
the session's exit code.

- Match the three-line footer block (separator / `Finished:` / `Exit Code:`)
  at line starts instead of a bare substring, in both the JS and Rust
  implementations.
- Read only the last 16 KB of the log (the footer is always last), dropping
  the partial first line so it cannot act as a line start. This also removes
  a full-file read from every `--status` call.
- Prefer `docker inspect .State.ExitCode` over the log text when a detached
  session has ended: the backend is authoritative and cannot be spoofed by
  command output.

Adds regression tests (js/test/regression-150.js, rust/tests/regression_150.rs)
and an end-to-end CLI reproduction in experiments/.
@konard konard changed the title [WIP] $ --status fabricates a detached session exit code from the command's own output (unanchored Exit Code: scan over the whole log) Anchor the --status exit-code footer scan to the log tail Aug 4, 2026
@konard
konard marked this pull request as ready for review August 4, 2026 04:05
@konard

konard commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Working session summary

PR #153 is finalized and green.

#153 — "Anchor the --status exit-code footer scan to the log tail", ready for review, MERGEABLE / CLEAN, both workflows (JavaScript CI/CD and Rust CI/CD) passing on d0656c2; every check is SUCCESS or SKIPPED (skips are release-only jobs). No comments or reviews exist on issue #150 or PR #153, working tree is clean, and main is already merged.

What shipped, in both implementations:

  • readExitCodeFromLog now matches only the anchored three-line footer start writes (====… / Finished: / Exit Code: N) at line starts, instead of an unanchored substring scan.
  • Only the last 16 KB of the log are read, with the tail's partial first line dropped so a mid-line fragment can't pose as a line start.
  • In the !alive branch, docker inspect .State.ExitCode now takes precedence over the log text (readBackendExitCode() ?? footerExit ?? -1).

Issue item 4 (per-execution UUID marker in the footer) was deliberately left out — it's marked optional and would change the on-disk log format across every footer writer; this is stated in the PR description.

Verification: 11 new regression tests per language (js/test/regression-150.js, rust/tests/regression_150.rs) — 10 of the JS ones fail on main; experiments/issue-150-forged-exit-code.mjs reproduces the incident through the real CLI; and the issue's Docker repro now reports status executing instead of the fabricated executed / exitCode 1, while a container killed with SIGKILL despite a verbatim forged Exit Code: 0 footer reports the backend's 137.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $4.588273

📊 Context and tokens usage:

Claude Opus 5: (2 sub-sessions)

  1. 116.4K / 1M (12%) input tokens, 28.6K / 128K (22%) output tokens
  2. 40.2K / 1M (4%) input tokens, 1.5K / 128K (1%) output tokens

Total: (2.7K new + 121.4K cache writes + 4.8M cache reads) input tokens, 38.0K output tokens, $4.588273 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: medium (~15999 tokens)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (1893KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard

konard commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

@konard
konard marked this pull request as draft August 4, 2026 08:30
@konard

konard commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

🤖 AI Work Session Started

Starting automated work session at 2026-08-04T08:30:36.408Z

The PR has been converted to draft mode while work is in progress.

This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback.

Resolve the exit-code resolution order in enrichDetachedStatus / the Rust
equivalent: backend (docker inspect .State.ExitCode, #150) -> anchored log
footer -> 137 when only the OOM observation is left (#151) -> -1 sentinel.

Update the two #151 test fixtures to write the real anchored footer block
(with the '=' separator line) that start itself emits.
@konard
konard marked this pull request as ready for review August 4, 2026 08:44
@konard

konard commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Working session summary

Done. PR #153 is merge-clean, green, and out of draft: #153

What I did this round:

Merged origin/main (which had landed #151's OOM-observation work) into the branch. Two conflicts, both in the !alive branch of the detached-status enrichment:

  • js/src/lib/status-formatter.js
  • rust/src/lib/status_formatter.rs

Resolution — combined both fixes into one order, backend first (issue #150's core point) then the anchored footer, then #151's OOM fallback:

enriched.exitCode =
  backendExitCode(dockerState) ??   // docker inspect — unspoofable
  footerExit ??                     // anchored footer block only
  (oomKilled === true ? 137 : -1);

Main's backendExitCode(dockerState)/backend_exit_code(docker_state) helper superseded my branch's readBackendExitCode, so the merged code uses main's.

Test fixtures updated. Two pre-existing #151 tests wrote a bare Finished: now\nExit Code: 0 as the "footer". With the anchored pattern that no longer counts as a footer, so both now write the real block including the =×50 separator line that start itself emits:

  • js/test/session-name-status.js:849
  • rust/tests/status_formatter.rs:519

Verification: cargo test all green; bun run test 706 pass / 1 fail — that one failure (ExecutionStore > should persist data to lino file) is pre-existing, and I confirmed it by running the same test in a clean worktree of origin/main, where it also fails. cargo fmt --check, cargo clippy --all-targets -D warnings, eslint and prettier all clean. CI on the pushed merge is fully green.

I also corrected the PR description's resolution-order bullet (it still claimed footer → -1, omitting #151's 137) and its now-stale local test count.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $1.660718

📊 Context and tokens usage:

  • 63.4K / 1M (6%) input tokens, 11.9K / 128K (9%) output tokens

Total: (68 new + 49.6K cache writes + 1.7M cache reads) input tokens, 11.9K output tokens, $1.660718 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: off (disabled)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (1145KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
konard merged commit 296ff97 into main Aug 4, 2026
25 checks passed
@konard

konard commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

🎉 Auto-merged

This pull request has been automatically merged by hive-mind.

  • All CI checks have passed

Auto-merged by hive-mind with --auto-merge flag

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.

$ --status fabricates a detached session exit code from the command's own output (unanchored Exit Code: scan over the whole log)

1 participant