Skip to content

fix: Log Finalize Awaits Terminal Stream State - #177

Merged
droid-ash merged 4 commits into
mainfrom
260731-vojm-log-finalize-terminal-state
Aug 1, 2026
Merged

fix: Log Finalize Awaits Terminal Stream State#177
droid-ash merged 4 commits into
mainfrom
260731-vojm-log-finalize-terminal-state

Conversation

@droid-ash

@droid-ash droid-ash commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Meta

Change ID Type Confidence Plan Review
vojm fix 4.6/5.0 9/9 tasks, 14/14 acceptance ✓ ✓ 1 cycle
Impact +/− Net
true +749 / −142 +607
└ impl +633 / −134 +499
└ tests +116 / −8 +108

generated by fab-kit v2.16.8

Pipeline: intake ✓ → apply ✓ → review ✓ → hydrate ✓ → ship → review-pr

Summary

LogWriteStreamRegistry.finalize decided success/failure from entry.error at whatever moment its finally completed, instead of after the stream's terminal close state — rejecting a fully-flushed log over a stale non-destroying error, and silently missing a close-time error that landed after the check. finalize now awaits the stream's terminal 'close' state before deciding, with a state-based success predicate: resolve iff writableFinished && stream.errored === null (warn-and-resolve over a stale recorded error), else reject deterministically with entry.error ?? stream.errored.

This change supersedes the decision CodeRabbit recorded as a learning on PR #173 — that "finalize deliberately re-throws any recorded entry.error without a writableFinished guard" is intentional. PR #173 chose to document that behavior rather than change it. This is reopened on two-reviewer convergence: an operator-side adversarial review independently rated the same issue should-fix, and CodeRabbit independently rated it MAJOR (data integrity) on PR #173. Future bot reviews should treat this PR as the superseding decision, not as a contradiction of the stored learning.

Test Integrity position: the rework of 'LogWriteStreamRegistry records only the first error a stream emits' is spec-conformance test updating, not test-weakening. Coverage is moved, not dropped — first-error-wins is re-pinned on a genuinely failing stream (two emits followed by stream.destroy), and the assertion it displaced (rejection despite a clean flush) is re-pinned deterministically as the new close-time-error regression test, the only production-reachable flushed-then-errored shape.

Changes

  • finalize awaits the terminal stream state before deciding (packages/device-node/src/device/logWriteStream.ts)
  • New success/failure semantics — state-based, not history-based
  • Test updates (Test Integrity rule — stated plainly)
  • finalizeQuietly-before-finalize consumption — decided: keep once-consumed semantics, pin the safety invariant with tests
  • Memory + comment updates (hydrate stage)

Summary by CodeRabbit

  • Bug Fixes

    • Improved log capture finalization by waiting for the stream to fully close before determining success.
    • Successfully finalized cleanly flushed streams despite stale, non-terminal errors, with warnings recorded where appropriate.
    • Preserved deterministic reporting of terminal failures and the first recorded error.
    • Improved handling of asynchronous output-file failures, destroyed streams, and cleanup during stop operations.
  • Documentation

    • Updated log-capture documentation to describe terminal stream-state behavior and failure scenarios.

LogWriteStreamRegistry.finalize decided success/failure by reading
entry.error at whatever moment its finally completed, instead of after
the stream's terminal close state. That produced two wrong outcomes: a
fully-flushed log with a stale non-destroying recorded error rejected
(contradicting the file's own contract), and a close-time error
arriving after the check silently missed. finalize now awaits the
stream's terminal 'close' state before deciding, and the success
predicate is state-based: resolve iff writableFinished &&
stream.errored === null (warn-and-resolve over a stale recorded
error), else reject with entry.error ?? stream.errored.

Supersedes the PR #173 decision to document rather than fix the
unconditional recorded-error rejection, reopened on two independent
reviewers converging (operator-side should-fix; CodeRabbit MAJOR /
data integrity).
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fe6f5d63-0ac6-41a5-8a2c-cc3adbd342ca

📥 Commits

Reviewing files that changed from the base of the PR and between fbd381e and df6f589.

📒 Files selected for processing (4)
  • docs/memory/device-node/log-capture.md
  • fab/changes/260731-vojm-log-finalize-terminal-state/.history.jsonl
  • fab/changes/260731-vojm-log-finalize-terminal-state/.status.yaml
  • fab/changes/260731-vojm-log-finalize-terminal-state/intake.md

📝 Walkthrough

Walkthrough

Log finalization now waits for terminal stream closure before evaluating success. Cleanly finished streams can ignore stale non-destroying errors, while terminal failures reject with first-error precedence. Tests and documentation cover close-time failures, cleanup, quiet finalization, and provider behavior.

Changes

Terminal log stream finalization

Layer / File(s) Summary
Terminal-state finalization implementation
packages/device-node/src/device/logWriteStream.ts
Finalization waits for close, evaluates writableFinished and errored, preserves the first recorded error, warns on stale errors, and cleans up streams.
Provider and registry regression coverage
packages/device-node/src/device/test/logCaptureProviders.test.ts
Tests cover asynchronous open failures, first-error precedence, stale errors after successful flushing, close-time I/O failures, and quiet-first stopping.
Documentation and workflow records
docs/memory/device-node/*, fab/changes/260731-vojm-log-finalize-terminal-state/*
Documentation and change records define terminal-state outcomes, close waiting, guarded logging, quiet finalization, acceptance criteria, and verification status.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CaptureProvider
  participant LogWriteStreamRegistry
  participant LogWriteStream
  participant Logger
  CaptureProvider->>LogWriteStreamRegistry: finalize capture
  LogWriteStreamRegistry->>LogWriteStream: end and await close
  LogWriteStream-->>LogWriteStreamRegistry: report terminal state
  LogWriteStreamRegistry->>Logger: warn on stale recorded error
  LogWriteStreamRegistry-->>CaptureProvider: resolve or reject
Loading

Possibly related PRs

Suggested labels: bug

Suggested reviewers: arnoldlaishram

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: finalization awaits the stream's terminal state.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 260731-vojm-log-finalize-terminal-state

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (3)
fab/changes/260731-vojm-log-finalize-terminal-state/plan.md (1)

164-164: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add coverage for a throwing stale-error warning sink.

Line 164 states that no test drives a throwing sink through Logger.w. Add a test that records a stale non-destroying error, makes the warning sink throw, and asserts that finalize still resolves and cleans up the registry entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fab/changes/260731-vojm-log-finalize-terminal-state/plan.md` at line 164, Add
a test covering the stale non-destroying error path in Logger.w where the
warning sink throws, and assert that finalize still resolves while removing the
corresponding registry entry. Update the A-011 plan claim to reflect the new
test coverage.
packages/device-node/src/device/logWriteStream.ts (2)

179-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the repeated guarded-log pattern.

The try { Logger.X(...) } catch { /* deliberately empty */ } pattern now appears three times in this file: the open() error listener (Line 112-117), the stale-error warning in finalize (Line 190-197), and finalizeQuietly's catch block (Line 250-259). Each copy repeats the same rationale in its comment. Extract a small helper, for example _safeLog(fn: () => void): void, to remove the duplication and keep the guard's contract in one place.

♻️ Proposed refactor
+  private _safeLog(fn: () => void): void {
+    try {
+      fn();
+    } catch {
+      // Deliberately empty: a logger that just failed is not where a failing
+      // logger gets reported.
+    }
+  }
+
   open(outputFilePath: string): fs.WriteStream {
     ...
     stream.on('error', (error) => {
       entry.error ??= error;
-      try {
-        Logger.e(`LogWriteStreamRegistry: log write stream failed: ${outputFilePath}`, error);
-      } catch {
-        // Deliberately empty: ...
-      }
+      this._safeLog(() =>
+        Logger.e(`LogWriteStreamRegistry: log write stream failed: ${outputFilePath}`, error),
+      );
     });
     ...
   }

Also applies to: 250-259, 112-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/device-node/src/device/logWriteStream.ts` around lines 179 - 200,
Extract the repeated guarded Logger.X invocation into a private helper such as
_safeLog(fn: () => void): void, preserving the existing try/catch behavior and
intentionally swallowing logger errors. Replace the duplicated guarded logging
blocks in the open() error listener, finalize’s stale-error warning, and
finalizeQuietly’s catch block with this helper without changing their messages
or outcome handling.

289-320: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider bounding the wait for 'close'.

_endAndFlush waits for 'close' with no timeout, unlike _drain, which bounds its wait with LOG_DRAIN_TIMEOUT_MS. This relies entirely on fs.WriteStream always emitting 'close' (the documented emitClose: true default). The comment at Line 297-303 explains this reasoning well, so the design is deliberate. As a defensive measure against an unexpected Node or filesystem edge case, consider adding a bounded fallback similar to _drain's, so a stuck close cannot hang finalize (and thus stopLogCapture) indefinitely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/device-node/src/device/logWriteStream.ts` around lines 289 - 320,
Bound the close wait in _endAndFlush so finalize cannot hang indefinitely if the
stream never emits 'close'. Reuse the existing LOG_DRAIN_TIMEOUT_MS timeout
pattern from _drain, while preserving the current end/destroy behavior and
resolving normally when 'close' arrives first.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/memory/device-node/log-capture.md`:
- Around line 77-80: Correct the malformed sentence in the finalize
documentation by replacing “stream something already ended or destroyed” with “a
stream that has already ended or been destroyed,” leaving the surrounding
content unchanged.

In `@fab/changes/260731-vojm-log-finalize-terminal-state/intake.md`:
- Around line 8-10: Correct the provenance statement in intake.md to accurately
describe the contents of .history.jsonl: either ensure the complete raw request
is persisted there, or state that the file records only condensed/operative
command arguments. Keep the quoted request and its surrounding context unchanged
unless required to make the provenance claim accurate.

---

Nitpick comments:
In `@fab/changes/260731-vojm-log-finalize-terminal-state/plan.md`:
- Line 164: Add a test covering the stale non-destroying error path in Logger.w
where the warning sink throws, and assert that finalize still resolves while
removing the corresponding registry entry. Update the A-011 plan claim to
reflect the new test coverage.

In `@packages/device-node/src/device/logWriteStream.ts`:
- Around line 179-200: Extract the repeated guarded Logger.X invocation into a
private helper such as _safeLog(fn: () => void): void, preserving the existing
try/catch behavior and intentionally swallowing logger errors. Replace the
duplicated guarded logging blocks in the open() error listener, finalize’s
stale-error warning, and finalizeQuietly’s catch block with this helper without
changing their messages or outcome handling.
- Around line 289-320: Bound the close wait in _endAndFlush so finalize cannot
hang indefinitely if the stream never emits 'close'. Reuse the existing
LOG_DRAIN_TIMEOUT_MS timeout pattern from _drain, while preserving the current
end/destroy behavior and resolving normally when 'close' arrives first.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bc27f1c1-c166-4798-a177-8341b95034eb

📥 Commits

Reviewing files that changed from the base of the PR and between bc6e634 and fbd381e.

📒 Files selected for processing (8)
  • docs/memory/device-node/index.md
  • docs/memory/device-node/log-capture.md
  • fab/changes/260731-vojm-log-finalize-terminal-state/.history.jsonl
  • fab/changes/260731-vojm-log-finalize-terminal-state/.status.yaml
  • fab/changes/260731-vojm-log-finalize-terminal-state/intake.md
  • fab/changes/260731-vojm-log-finalize-terminal-state/plan.md
  • packages/device-node/src/device/logWriteStream.ts
  • packages/device-node/src/device/test/logCaptureProviders.test.ts

Comment thread docs/memory/device-node/log-capture.md Outdated
Comment thread fab/changes/260731-vojm-log-finalize-terminal-state/intake.md Outdated
droid-ash and others added 2 commits July 31, 2026 19:06
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@droid-ash
droid-ash marked this pull request as ready for review August 1, 2026 02:32
@droid-ash
droid-ash merged commit f1ba99c into main Aug 1, 2026
1 of 2 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