Skip to content

Reduce CoordinatedTextWriter secret-scan contention - #3856

Open
thomhurst wants to merge 44 commits into
mainfrom
issue-3755-coordinated-writer-perf
Open

Reduce CoordinatedTextWriter secret-scan contention#3856
thomhurst wants to merge 44 commits into
mainfrom
issue-3755-coordinated-writer-perf

Conversation

@thomhurst

@thomhurst thomhurst commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • cache a case-insensitive SearchValues<string> with each secret-pattern snapshot and use it for one-pass match discovery
  • compute retained secret-prefix length once per processing pass and avoid re-obfuscating already-clean output
  • replace the global writer lock with per-stream state locks while retaining serialized writes to the underlying console
  • add regression tests for single-pass obfuscation and concurrent module-buffer processing

Validation

  • SecretMaskingPatternTests: 25 passed
  • OutputCoordinatorTests: 19 passed
  • ModularPipelines.slnx Release build: 0 warnings, 0 errors

Closes #3755

Summary by CodeRabbit

  • Bug Fixes
    • Improved secret masking for overlapping, partial, case-sensitive, and custom masking scenarios.
    • Prevented secret fragments during concurrent writes, flushes, and direct console output.
    • Preserved trailing output correctly after masking and direct console writes.
    • Ensured completed output is processed consistently before appearing in the console.
    • Improved asynchronous flushing and synchronization when secrets are registered during output processing.
  • Performance
    • Improved repeated secret-pattern matching and enabled independent output streams to be processed concurrently.

@claude

claude Bot commented Aug 4, 2026

Copy link
Copy Markdown

Code review

Reviewed CoordinatedTextWriter.cs and the new tests in SecretMaskingPatternTests.cs. Traced the concurrency model, the secret-pattern matching, and the removed obfuscation calls end-to-end — no correctness or security-regression issues found.

Verified correct (worth calling out since this touches secret redaction):

  • WriteCompletedLine, FlushDirectPrefix, and FlushPartialPrefix no longer call _secretObfuscator.Obfuscate(...) before writing to the real console. This looks alarming at first glance in a secret-masking path, but it's safe: ObfuscateCompletePatterns already mutates state.Buffer in place before any of these flush methods run (every call site — ProcessPendingOutput, Flush(), FlushAvailableAsync() — calls it first), so the content those methods write out is already redacted. The removed call was genuinely redundant: it re-obfuscated against the same secret set (_secretProvider.GetSnapshot()), and since CoordinatedTextWriter's own pass is hardcoded OrdinalIgnoreCase while SecretObfuscator.Obfuscate defaults to case-sensitive (SecretMaskingOptions.CaseInsensitive defaults to false), the local pass was already a superset of what the removed call would have masked. Good catch eliminating dead work.
  • Lock ordering is consistent and deadlock-free: state.SyncRoot_lineBufferLock is the only nesting direction (established in ProcessPendingOutputGetSecretPatterns()); Flush()/FlushAvailableAsync() never hold _lineBufferLock while acquiring a state.SyncRoot. The new _outputLock cleanly serializes the actual writes to _realConsole independent of per-buffer locks, which is exactly what's needed now that different modules' buffers process concurrently.
  • FindFirstPattern's replacement of the old per-pattern IndexOf loop with SearchValues<string>.IndexOfAny preserves the original tie-break semantics (leftmost match, longest pattern wins) because patterns.Values stays sorted longest-first and the StartsWith scan after IndexOfAny walks that same order.

Architectural suggestion (not a bug, but worth considering as a follow-up):

CoordinatedTextWriter.GetSecretPatterns() (lines 209–236) now duplicates a fair amount of logic that already exists in SecretObfuscator.GetRegisteredSecretCache() (src/ModularPipelines/Engine/SecretObfuscator.cs): the same (version & 1) == 0 in-progress-update check, the same distinct/order-by-length/SearchValues.Create construction, sourced from the same ISecretProvider. The two implementations have already drifted once (this class hardcodes OrdinalIgnoreCase, while SecretObfuscator honors SecretMaskingOptions.CaseInsensitive) — harmless today only because the hardcoded version happens to be the more permissive one, but that's incidental, not designed.

Since this PR is already reworking CoordinatedTextWriter's pattern cache into a small SecretPatterns record (Values + SearchValues<string>?), it'd be a natural point to extract a shared SecretPatternCache-style type (mirroring SecretObfuscator.SecretCache) that both classes consume — one place computing "current secrets as sorted, deduped, cased SearchValues", parameterized by case-sensitivity. That would remove the duplication and make it impossible for the two obfuscation passes to silently diverge on matching semantics in the future. Not blocking this PR, but flagging it since the two are functionally coupled today only by convention.

No CLAUDE.md compliance issues — the changes stay within the core library (ModularPipelines.slnx scope) and don't touch build/test tooling.

@thomhurst

Copy link
Copy Markdown
Owner Author

CI diagnosis: the sole Ubuntu failure was an unrelated timing timeout in EngineCancellationTokenTests.StopOnFirstException_PendingModuleAwaiterReturnsTerminatedResult after the full core suite had run for about 15 minutes. The test expected ModuleFailedException but its outer 12-second wait expired; no CoordinatedTextWriter test failed. Triggered the one allowed failed-job rerun.

@thomhurst

Copy link
Copy Markdown
Owner Author

Refreshed onto current main at 1127d84. Current-head validation: SecretMaskingPatternTests 29/29; OutputCoordinatorTests 19/19; core Release build 0 warnings / 0 errors. Please re-review current head.

Comment thread src/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated

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

Code review

Re-reviewed current head (1127d849f, after the rebase onto main) — thanks for the ping to re-review. Confirmed the concurrency rework itself (per-buffer SyncRoot, _outputLock serializing real-console writes, lock ordering SyncRoot → _lineBufferLock/_outputLock never reversed) is sound and deadlock-free, and the new SearchValues<string>-based single-pass matching preserves the original leftmost-longest-match semantics.

Found one high-severity issue (posted inline): removing the _secretObfuscator.Obfuscate(...) calls from FlushDirectPrefix/WriteCompletedLine/FlushPartialPrefix drops a safety net that mattered. ObfuscateCompletePatterns's early break (when the longest match at the leftmost position overlaps the retained-prefix boundary) can flush a buffer region verbatim even though it contains a different, shorter, fully-contained registered secret earlier in that same region. I verified this with a concrete repro and a standalone reimplementation of the exact algorithm — confirmed the leak occurs post-PR and that the removed second Obfuscate() pass would have caught it pre-PR. Details and a suggested fix direction are in the inline comment.

My previous review on this PR (before the rebase) concluded the removed calls were "genuinely redundant" — that conclusion was wrong; this scenario slipped through because it depends on interaction between two separately-registered secrets near the retained-prefix boundary, which the earlier pass didn't construct a counterexample for.

Everything else from the previous review still stands: the architectural suggestion to extract a shared SecretPatternCache-style type (mirroring SecretObfuscator.GetRegisteredSecretCache) remains a reasonable non-blocking follow-up, not required for this PR.

@thomhurst

Copy link
Copy Markdown
Owner Author

@claude review

@thomhurst

Copy link
Copy Markdown
Owner Author

@codex review

@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code review

Re-reviewed at current head (ef575ed), which adds the "mask nested retained-prefix secrets" fix on top of what I reviewed previously. Traced the new logic end-to-end — no correctness or security-regression issues found.

Verified correct — the nested-secret fix (ObfuscateCompletePatterns):

The old behavior: once a match overlapping the retained-prefix boundary was found, the loop appended everything from searchIndex to the end of the buffer verbatim and broke — including any earlier, fully-resolved secret between searchIndex and the overlapping match, which could then leak unobfuscated once flushed. The new code instead only skips the individual overlapping match (searchIndex = match.Index + 1; continue) and keeps scanning, so earlier matches still get obfuscated. I confirmed the new DirectConsoleWrite_MasksContainedSecretBeforeRetainedPrefix test exercises exactly this case ("helloabcXYZdef" with overlapping patterns abcXYZdef/XYZ/efGHI") and traced it by hand — it correctly obfuscates XYZ while still retaining ef as a possible prefix of efGHI.

I also verified the invariant the fix depends on: when preservePotentialLongerMatch is true, no match with Index + Length > retainedPrefixStart is ever obfuscated (it's always skipped via continue), so the trailing retainedPrefixLength characters of pending are always copied verbatim into output. That's what makes it safe to keep reusing the caller-supplied retainedPrefixLength in FlushSafeOutput/FlushSafeOutput without invalidation in the common case, and retainedPrefixInvalidated correctly triggers a recompute for the preservePotentialLongerMatch: false paths (Flush/FlushAvailableAsync) where boundary-overlapping matches are obfuscated.

Also checked (unchanged from previous review, still holds at this head):

  • Lock ordering stays consistent and deadlock-free with the added _outputLock: it's always the innermost lock, never held while acquiring state.SyncRoot or _lineBufferLock, and state.SyncRoot_lineBufferLock remains the only nesting direction (via ProcessPendingOutputGetSecretPatterns()).
  • FindFirstPattern's SearchValues<string>.IndexOfAny + ordered StartsWith fallback still preserves leftmost/longest-match tie-break semantics, and the defensive InvalidOperationException for "no match found after IndexOfAny" is unreachable given patterns.Values/patterns.SearchValues are built from the same source with matching case-sensitivity.
  • New DifferentModuleBuffers_ProcessConcurrently test meaningfully validates the per-buffer locking goal of this PR (gates one module's obfuscation call and confirms a second module's write completes independently), not just a smoke test.

Still-open, non-blocking suggestion from the previous review: CoordinatedTextWriter.GetSecretPatterns() still duplicates the pattern-cache construction logic in SecretObfuscator.GetRegisteredSecretCache() (src/ModularPipelines/Engine/SecretObfuscator.cs) — same version-check, same distinct/sort/SearchValues.Create construction, same ISecretProvider source, differing only in hardcoded OrdinalIgnoreCase vs. honoring SecretMaskingOptions.CaseInsensitive. Worth extracting into a shared SecretPatternCache-style type parameterized by case-sensitivity as a follow-up, so the two obfuscation passes can't silently diverge on matching semantics again. Not blocking.

No CLAUDE.md compliance issues — both changed files stay within the core library (ModularPipelines.slnx/ModularPipelines.Tests.slnf scope) and don't touch build/test tooling or generated options classes.

@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: ef575ed76f

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs
@thomhurst
thomhurst force-pushed the issue-3755-coordinated-writer-perf branch from ef575ed to 07f7f70 Compare August 9, 2026 20:32
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

CoordinatedTextWriter now uses scoped buffering, versioned secret snapshots, tracked obfuscation, overlap-aware matching, stable secret publication, synchronized direct output, and ordered asynchronous console flushing.

Changes

Coordinated output processing

Layer / File(s) Summary
Tracked obfuscation contract
src/ModularPipelines/Engine/ISecretObfuscator.cs, src/ModularPipelines/Engine/SecretObfuscator.cs
Obfuscation reports masked output, consumed input length, and pattern comparison.
Stable secret publication
src/ModularPipelines/Engine/ISecretProvider.cs, src/ModularPipelines/Engine/SecretProvider.cs
Secret registration and guarded output processing use coordinated locking, deferred registration, and versioned snapshots.
Scoped buffered writes
src/ModularPipelines/Console/CoordinatedTextWriter.cs
Writes use independently synchronized line-buffer states for module, custom-obfuscation, and reentrant-write scopes.
Versioned matching and flushing
src/ModularPipelines/Console/CoordinatedTextWriter.cs
Pattern matching uses comparison-aware snapshots and optional SearchValues acceleration. Flush paths handle overlaps, partial prefixes, tracked consumption, synchronized output, reentrancy, and asynchronous console flushing.
Masking and concurrency validation
test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs
Tests cover matching, custom obfuscators, concurrent buffers, stable registration, reentrant operations, partial prefixes, and asynchronous flushing.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConsoleWrite
  participant CoordinatedTextWriter
  participant SecretProvider
  participant SecretObfuscator
  participant RealConsole
  ConsoleWrite->>CoordinatedTextWriter: Write output
  CoordinatedTextWriter->>SecretProvider: ExecuteWithStableSecrets
  CoordinatedTextWriter->>SecretObfuscator: ObfuscateWithConsumption
  SecretObfuscator-->>CoordinatedTextWriter: Return masked output and consumed length
  CoordinatedTextWriter->>RealConsole: Flush synchronized output
Loading

Possibly related PRs

Poem

A rabbit checks each secret line,
With locks and scopes in neat design.
Patterns match and prefixes stay,
While stable secrets guide the way.
Flushes wait, then outputs flow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: reducing secret-scan contention in CoordinatedTextWriter.
Linked Issues check ✅ Passed The changes implement all objectives in #3755, including SearchValues gating, single prefix computation, reduced obfuscation, and narrower locking.
Out of Scope Changes check ✅ Passed The additional reentrancy, registration, and concurrency changes support the required locking and output-processing behavior.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-3755-coordinated-writer-perf

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

thomhurst added a commit that referenced this pull request Aug 10, 2026
When a longer same-start secret overlaps a retained suffix, scan for the longest same-start match that ends in the safe output before skipping the position. This prevents shorter registered secrets from reaching the console unmasked.\n\nRefs #3856
thomhurst added a commit that referenced this pull request Aug 10, 2026
When a longer same-start secret overlaps a retained suffix, scan for the longest same-start match that ends in the safe output before skipping the position. This prevents shorter registered secrets from reaching the console unmasked.

Refs #3856
@thomhurst
thomhurst force-pushed the issue-3755-coordinated-writer-perf branch from 3ff7944 to b44c0c5 Compare August 10, 2026 02:25

@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: b44c0c57f0

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated

@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: 1b93b2fd3c

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated
@thomhurst

Copy link
Copy Markdown
Owner Author

Follow-up P1 fixed in f64f281: partial candidate masking now preserves the unchanged suffix and resumes overlap scanning instead of consuming the full case-insensitive candidate. Validation: SecretMaskingPatternTests 33/33; OutputCoordinatorTests 19/19; core Release build 0 warnings/errors; scoped formatting clean.

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

Code review

Re-reviewed at current head (f64f281d09, after the "rescan partial secret matches" fix — the fourth P1 fix on top of what I reviewed previously). I traced ObfuscateCompletePatterns end-to-end against all four adversarial repros raised across this thread (nested retained-prefix secret, shorter same-start secret, case-sensitivity mismatch, partially-obfuscated candidate) plus tried to construct new counterexamples of my own.

Correctness at current head: I could not find a new leak. The key invariant that makes this hold together, even with the GetUnchangedSuffixLength/consumedLength bookkeeping added in the last fix, is that everything appended to output is one of exactly two things: (a) raw pending text that a FindFirstPattern scan has already proven contains no match start, or (b) bytes taken directly from _secretObfuscator.Obfuscate(secret, null)'s own return value, which by definition has already had every registered secret inside it masked. Any content that gets "deferred" back onto searchIndex/outputIndex is always raw, unprocessed pending text, which the loop is guaranteed to re-scan with the same pattern set before it can reach output unmasked. So even where the suffix-trim heuristic in GetUnchangedSuffixLength under- or over-estimates the truly "untouched" span (it's a character-equality heuristic, not a real record of which spans Obfuscate touched), the failure mode is redundant re-scanning of already-safe text, not a leak — I could not construct an input where it causes unmasked secret bytes to reach output directly. consumedLength also can't go negative or reach 0 while looping the same position (which would hang), since unchangedSuffixLength is bounded by min(secret.Length, obfuscated.Length) and obfuscated != secret is already guaranteed at that point, so at least one byte of the match is always consumed. Lock ordering, the new SecretPatterns/SearchValues cache, and FindFirstPattern's leftmost/longest tie-break all still hold as verified in earlier passes.

Process/architecture concern (this is the part worth acting on):

This file has now needed four independent P1 fixes in six days, each one patching the exact adversarial input the previous version failed on (nested secret → same-start shorter secret → case-sensitivity → partial-obfuscation resume). That pattern — react to a counterexample, add a regression test for precisely that input — is a strong signal the invariant ObfuscateCompletePatterns is trying to maintain (single forward pass, mutate the buffer in place, retain a variable-length unsafe suffix, resume mid-candidate on partial matches) is fundamentally harder to prove correct than the incremental patches suggest. My read above is informal reasoning, not a proof, and this is the exact code path standing between a leaked cloud credential and a CI log. Two concrete suggestions, either of which would meaningfully raise confidence beyond "four humans/bots didn't find a fifth counterexample":

  1. Add a randomized/property-based test that generates many registered-secret sets (including overlapping, nested, and case-variant secrets) and many arbitrary Write split points, then asserts the resulting _realConsole output is byte-identical to a naive reference — buffer the entire stream and call _secretObfuscator.Obfuscate once on the whole thing. That's the only way to get coverage beyond the specific examples reviewers happened to think of, and it directly targets the class of bug this file keeps producing.
  2. Restore a cheap safety-net obfuscation pass, but scoped to just the small chunk being flushed (not the whole retained buffer, which is what made the original blanket Obfuscate() calls expensive). FlushDirectPrefix/WriteCompletedLine/FlushPartialPrefix currently trust ObfuscateCompletePatterns's bookkeeping unconditionally. A final _secretObfuscator.Obfuscate() call on just the bytes about to hit _realConsole (typically a single line or a short prefix, not the whole buffer) would cost little and turns any future bug in the streaming/retained-prefix logic into a redundant no-op instead of a live credential leak — i.e. defense in depth on the one path where "we optimized this and got it wrong" has already happened four times.

Neither is a blocker for this specific PR — the current head does appear correct against every repro raised so far — but given the security stakes and the repeated-P1 track record, I'd treat this as a near-term follow-up rather than a someday-maybe.

Still-open, non-blocking suggestion from earlier reviews: CoordinatedTextWriter.GetSecretPatterns() still duplicates the pattern-cache construction logic in SecretObfuscator.GetRegisteredSecretCache() (same version-check, same distinct/sort/SearchValues.Create construction, same ISecretProvider source, differing only in hardcoded OrdinalIgnoreCase vs. honoring SecretMaskingOptions.CaseInsensitive). Worth extracting into a shared SecretPatternCache-style type parameterized by case-sensitivity so the two obfuscation passes can't silently diverge on matching semantics again.

Minor, non-blocking: FlushAvailableAsync/FlushAsync now call the synchronous _realConsole.Flush() under _outputLock instead of the previous _realConsole.FlushAsync(). That's a reasonable trade-off given a plain lock can't be held across an await, but it does mean a _realConsole implementation with genuinely async I/O (e.g. network-backed) would now block a thread-pool thread during flush. If that matters here, a SemaphoreSlim instead of the object lock would let flush stay truly async.

No CLAUDE.md compliance issues — both changed files stay within the core library (ModularPipelines.slnx/ModularPipelines.Tests.slnf scope) and don't touch build/test tooling or generated options classes.

@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: f64f281d09

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated

@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: 1befd589af

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs
Comment thread src/ModularPipelines/Console/CoordinatedTextWriter.cs

@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: cbbc227729

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated

@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: a12ad75fa5

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated

@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: 1

🧹 Nitpick comments (4)
src/ModularPipelines/Engine/ISecretObfuscator.cs (1)

19-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the consumption contract.

ConsumedInputLength drives buffer advancement in CoordinatedTextWriter.ObfuscateCompletePatterns. Add XML documentation that states the invariant: ConsumedInputLength counts characters of input that Output replaces, and the remaining input.Length - ConsumedInputLength characters appear unchanged at the end of Output. This invariant is currently only implied by the caller arithmetic at lines 308-311 of src/ModularPipelines/Console/CoordinatedTextWriter.cs.

🤖 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 `@src/ModularPipelines/Engine/ISecretObfuscator.cs` around lines 19 - 22,
Document SecretObfuscationResult and its ConsumedInputLength member with XML
comments describing the consumption invariant: it counts input characters
replaced by Output, while the remaining input.Length - ConsumedInputLength
characters appear unchanged at the end of Output.
test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs (1)

211-278: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the first module output as well.

The test verifies only secondBuffer. It does not confirm that the blocked write produced the masked line. Add a verification for firstBuffer after await firstWrite. This confirms that per-state locking preserves correctness, not only progress.

💚 Proposed addition
         secondBuffer.Verify(x => x.WriteLine("ordinary output"), Times.Once);
+        firstBuffer.Verify(x => x.WriteLine("**********"), Times.Once);
🤖 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 `@test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs` around
lines 211 - 278, Update DifferentModuleBuffers_ProcessConcurrently to verify
firstBuffer after await firstWrite, asserting it received the masked
"**********" line exactly once, while retaining the existing secondBuffer
verification.
src/ModularPipelines/Console/CoordinatedTextWriter.cs (2)

209-236: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use a dedicated lock for the secret-pattern cache.

GetSecretPatterns acquires _lineBufferLock, which also guards the _lineBuffers dictionary. ProcessPendingOutput calls GetSecretPatterns on every processed write (Line 176). Every write from every module therefore serializes on one global lock, and each write also blocks concurrent GetLineBufferState calls. This works against the stated goal of removing the global writer lock.

Two lock orders also exist in this class. ProcessPendingOutput takes state.SyncRoot and then _lineBufferLock. Flush takes _lineBufferLock, releases it, and then takes state.SyncRoot. The current code does not deadlock because the second order is not nested. A future change that holds _lineBufferLock across a state.SyncRoot acquisition would deadlock.

Separate the two concerns and add a volatile fast path, as SecretObfuscator.GetRegisteredSecretCache already does.

♻️ Proposed refactor
     private readonly object _lineBufferLock = new();
+    private readonly object _secretPatternsLock = new();
     private readonly object _outputLock = new();
     private SecretPatterns _secretPatterns = new([], null);
     private long? _secretPatternsVersion;
     private SecretPatterns GetSecretPatterns()
     {
-        lock (_lineBufferLock)
-        {
-            var version = _secretProvider.Version;
-            if (_secretPatternsVersion is not null
-                && (version & 1) == 0
-                && _secretPatternsVersion == version
-                && _secretProvider.Version == version)
-            {
-                return _secretPatterns;
-            }
+        var version = _secretProvider.Version;
+        var cachedVersion = Volatile.Read(ref _secretPatternsVersion);
+        if (cachedVersion is not null
+            && (version & 1) == 0
+            && cachedVersion == version
+            && _secretProvider.Version == version)
+        {
+            return Volatile.Read(ref _secretPatterns);
+        }
 
+        lock (_secretPatternsLock)
+        {
             var snapshot = _secretProvider.GetSnapshot();
+            if (_secretPatternsVersion == snapshot.Version)
+            {
+                return _secretPatterns;
+            }
+
             var values = (snapshot.Secrets ?? [])
                 .Where(pattern => !string.IsNullOrEmpty(pattern))
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .OrderByDescending(pattern => pattern.Length)
                 .ToArray();
-            _secretPatterns = new SecretPatterns(
+            Volatile.Write(ref _secretPatterns, new SecretPatterns(
                 values,
                 values.Length == 0
                     ? null
-                    : SearchValues.Create(values, StringComparison.OrdinalIgnoreCase));
-            _secretPatternsVersion = snapshot.Version;
+                    : SearchValues.Create(values, StringComparison.OrdinalIgnoreCase)));
+            Volatile.Write(ref _secretPatternsVersion, snapshot.Version);
             return _secretPatterns;
         }
     }

_secretPatterns is a record struct, so Volatile.Read/Volatile.Write do not apply to it directly. If you adopt the fast path, change SecretPatterns to a sealed record class, or keep the field access inside the lock.

🤖 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 `@src/ModularPipelines/Console/CoordinatedTextWriter.cs` around lines 209 -
236, Update GetSecretPatterns to use a dedicated secret-pattern cache lock
instead of _lineBufferLock, keeping _lineBufferLock exclusively for line-buffer
state. Add a volatile fast path for a stable, even _secretProvider.Version, and
publish the rebuilt SecretPatterns and version safely; since SecretPatterns is
currently a record struct, convert it to a sealed record class or otherwise
retain all accesses under the dedicated lock.

254-258: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move the buffer copy after the candidate gate.

state.Buffer.ToString() allocates a full copy of the pending buffer. The code allocates it before the IndexOfAny gate on Line 255. Output that contains no secret candidate therefore pays a full string allocation on every processed write. ProcessPendingOutput runs on each newline, so this is a hot path.

Scan the StringBuilder chunks first and allocate the string only when a candidate exists.

♻️ Proposed refactor
-        var pending = state.Buffer.ToString();
-        if (pending.AsSpan().IndexOfAny(patterns.SearchValues) < 0)
+        if (!ContainsPatternCandidate(state.Buffer, patterns))
         {
             return retainedPrefixLength;
         }
+
+        var pending = state.Buffer.ToString();

Add the helper. A candidate can straddle a chunk boundary, so overlap the scan window by patterns.Values[0].Length - 1 characters, or fall back to ToString() when the buffer holds more than one chunk.

🤖 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 `@src/ModularPipelines/Console/CoordinatedTextWriter.cs` around lines 254 -
258, Update ProcessPendingOutput to avoid calling state.Buffer.ToString() before
the candidate gate. Scan StringBuilder chunks first, preserving detection across
chunk boundaries by overlapping adjacent scan windows by
patterns.Values[0].Length - 1 characters, or fall back to ToString() for
multi-chunk buffers; only create the pending string after a candidate is found,
then retain the existing IndexOfAny and retainedPrefixLength behavior.
🤖 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 `@src/ModularPipelines/Console/CoordinatedTextWriter.cs`:
- Around line 536-572: Update the shutdown lifecycle around
CoordinatedTextWriter.Flush and ConsoleCoordinator.DisposeAsync so active
writers are stopped or awaited before Uninstall restores the console streams.
Ensure no concurrent write can create or append to a line-buffer state after
Flush snapshots and drains _lineBuffers; otherwise, explicitly enforce and
document a lifecycle contract requiring all Console.Out users to finish before
disposal.

---

Nitpick comments:
In `@src/ModularPipelines/Console/CoordinatedTextWriter.cs`:
- Around line 209-236: Update GetSecretPatterns to use a dedicated
secret-pattern cache lock instead of _lineBufferLock, keeping _lineBufferLock
exclusively for line-buffer state. Add a volatile fast path for a stable, even
_secretProvider.Version, and publish the rebuilt SecretPatterns and version
safely; since SecretPatterns is currently a record struct, convert it to a
sealed record class or otherwise retain all accesses under the dedicated lock.
- Around line 254-258: Update ProcessPendingOutput to avoid calling
state.Buffer.ToString() before the candidate gate. Scan StringBuilder chunks
first, preserving detection across chunk boundaries by overlapping adjacent scan
windows by patterns.Values[0].Length - 1 characters, or fall back to ToString()
for multi-chunk buffers; only create the pending string after a candidate is
found, then retain the existing IndexOfAny and retainedPrefixLength behavior.

In `@src/ModularPipelines/Engine/ISecretObfuscator.cs`:
- Around line 19-22: Document SecretObfuscationResult and its
ConsumedInputLength member with XML comments describing the consumption
invariant: it counts input characters replaced by Output, while the remaining
input.Length - ConsumedInputLength characters appear unchanged at the end of
Output.

In `@test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs`:
- Around line 211-278: Update DifferentModuleBuffers_ProcessConcurrently to
verify firstBuffer after await firstWrite, asserting it received the masked
"**********" line exactly once, while retaining the existing secondBuffer
verification.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd8eaf9d-8272-47bf-96c9-b53694603136

📥 Commits

Reviewing files that changed from the base of the PR and between 020a014 and a12ad75.

📒 Files selected for processing (4)
  • src/ModularPipelines/Console/CoordinatedTextWriter.cs
  • src/ModularPipelines/Engine/ISecretObfuscator.cs
  • src/ModularPipelines/Engine/SecretObfuscator.cs
  • test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs

Comment thread src/ModularPipelines/Console/CoordinatedTextWriter.cs
@thomhurst

Copy link
Copy Markdown
Owner Author

Scoped custom-obfuscator reentrancy to each CoordinatedTextWriter in 0097a7c. Cross-stream sync/async flush regressions added; 76/76 focused tests and clean core Release build.

@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: 0097a7cc43

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated
@thomhurst

Copy link
Copy Markdown
Owner Author

Fixed in c0b26cf. Reentrant sink writes now enter their own active-writer scope, and line-buffer keys include the per-writer reentrancy depth. Added a two-level nested sink regression; SecretMaskingPatternTests pass 77/77 and the core Release build is clean.

@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 (4)
src/ModularPipelines/Engine/SecretProvider.cs (2)

789-798: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Remove the lock around the read of the readonly _snapshot field.

_snapshot is readonly and is assigned only in the constructor. The lock (_syncRoot) in the Snapshot getter adds contention without any benefit, and this getter runs on every Version and GetSnapshot call.

♻️ Proposed simplification
-        public SecretSnapshot Snapshot
-        {
-            get
-            {
-                lock (_syncRoot)
-                {
-                    return _snapshot;
-                }
-            }
-        }
+        public SecretSnapshot Snapshot => _snapshot;
🤖 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 `@src/ModularPipelines/Engine/SecretProvider.cs` around lines 789 - 798, Update
the SecretSnapshot property getter to return the readonly _snapshot field
directly without acquiring _syncRoot; preserve the existing property and return
value while removing only the unnecessary lock.

288-310: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The CurrentUnscopedRegistrationContext chain grows without bound.

TryDeferUnscopedRegistration pushes a new UnscopedRegistrationContext onto the async-local chain on every deferred registration, and no code removes entries. A long-lived worker that registers many secrets while emissions are active keeps a chain that grows linearly. GetUnscopedRegistrationBatch then walks that chain on every ExecuteWithStableSecrets call, so cost grows per registration.

Only the newest batch for this provider is needed. Replace the entry for the current provider instead of pushing a new one, or store the batch in a single mutable holder per provider.

Also applies to: 341-354

🤖 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 `@src/ModularPipelines/Engine/SecretProvider.cs` around lines 288 - 310, Update
TryDeferUnscopedRegistration and the related context handling used by
GetUnscopedRegistrationBatch so repeated deferred registrations for this
provider replace the existing CurrentUnscopedRegistrationContext entry rather
than append another UnscopedRegistrationContext. Preserve the parent context for
other providers and ensure lookups continue returning only the newest deferred
batch for this provider.
test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs (2)

2335-2354: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move this test method above the private helper classes.

DirectConsoleWrite_ReentrantFlushDrainsPartialPrefix sits between the helper writer declarations AsyncFlushTrackingWriter and BlockingAsyncFlushStringWriter. Every other test in the file appears before the helper section. Move this method next to FlushAsync_ReentrantFlushDrainsPartialPrefix to keep tests and helpers separated.

🤖 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 `@test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs` around
lines 2335 - 2354, Move the test method
DirectConsoleWrite_ReentrantFlushDrainsPartialPrefix above the private helper
class declarations, placing it next to
FlushAsync_ReentrantFlushDrainsPartialPrefix. Keep the test implementation
unchanged and leave the helper classes together below the test methods.

1210-1227: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated release-and-await block.

The try block already calls releaseObfuscation.Set() and awaits Task.WhenAll(write, registration) at Lines 1220-1221. The finally block repeats both statements. The repetition is only needed for the failure path, so keep it in finally and drop it from try. The same pattern appears in DirectConsoleWrite_DefersRegistrationUntilWriteCompletes (Lines 1266-1272) and StableSecretEmission_HoldsLeaseForInheritedWorker (Lines 1853-1859).

♻️ Proposed simplification
             await Assert.That(provider.Secrets).DoesNotContain("dynamic-secret");
-
-            releaseObfuscation.Set();
-            await Task.WhenAll(write, registration).WaitAsync(TimeSpan.FromSeconds(5));
         }
         finally
         {
             releaseObfuscation.Set();
             await Task.WhenAll(write, registration).WaitAsync(TimeSpan.FromSeconds(5));
         }
🤖 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 `@test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs` around
lines 1210 - 1227, Remove the redundant releaseObfuscation.Set() and
Task.WhenAll(write, registration).WaitAsync(...) calls from the try block,
leaving them only in finally for failure cleanup. Apply the same cleanup to
DirectConsoleWrite_DefersRegistrationUntilWriteCompletes and
StableSecretEmission_HoldsLeaseForInheritedWorker.
🤖 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 `@src/ModularPipelines/Engine/SecretProvider.cs`:
- Around line 227-286: Restructure EnterStableEmission and ExitStableEmission so
_secretEmissionLock.EnterReadLock and ExitReadLock occur outside the
_emissionStateLock critical sections, while preserving the existing state
updates and wait conditions. Move PublishPatterns out of the _emissionStateLock
region as well, coordinating deferred-pattern extraction and active-emission
bookkeeping before publishing, then clear _deferredMaskingSnapshot at the
appropriate post-publication point and verify published-snapshot visibility
behavior.
- Around line 219-224: Update the deferred snapshot creation in the masking
snapshot flow, including UpdateDeferredMaskingSnapshot and
GetNestedMaskingSnapshot, so its versioning cannot produce odd values
interpreted as in-progress publication states. Use a separate deferred-state
indicator or ensure generated deferred versions remain even, while preserving
existing snapshot reuse when secrets.Count equals existingCount.

---

Nitpick comments:
In `@src/ModularPipelines/Engine/SecretProvider.cs`:
- Around line 789-798: Update the SecretSnapshot property getter to return the
readonly _snapshot field directly without acquiring _syncRoot; preserve the
existing property and return value while removing only the unnecessary lock.
- Around line 288-310: Update TryDeferUnscopedRegistration and the related
context handling used by GetUnscopedRegistrationBatch so repeated deferred
registrations for this provider replace the existing
CurrentUnscopedRegistrationContext entry rather than append another
UnscopedRegistrationContext. Preserve the parent context for other providers and
ensure lookups continue returning only the newest deferred batch for this
provider.

In `@test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs`:
- Around line 2335-2354: Move the test method
DirectConsoleWrite_ReentrantFlushDrainsPartialPrefix above the private helper
class declarations, placing it next to
FlushAsync_ReentrantFlushDrainsPartialPrefix. Keep the test implementation
unchanged and leave the helper classes together below the test methods.
- Around line 1210-1227: Remove the redundant releaseObfuscation.Set() and
Task.WhenAll(write, registration).WaitAsync(...) calls from the try block,
leaving them only in finally for failure cleanup. Apply the same cleanup to
DirectConsoleWrite_DefersRegistrationUntilWriteCompletes and
StableSecretEmission_HoldsLeaseForInheritedWorker.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 891ce30a-bab9-4633-8019-a589e39b3400

📥 Commits

Reviewing files that changed from the base of the PR and between 680cdb7 and c0b26cf.

📒 Files selected for processing (3)
  • src/ModularPipelines/Console/CoordinatedTextWriter.cs
  • src/ModularPipelines/Engine/SecretProvider.cs
  • test/ModularPipelines.UnitTests/Logging/SecretMaskingPatternTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ModularPipelines/Console/CoordinatedTextWriter.cs

Comment thread src/ModularPipelines/Engine/SecretProvider.cs
Comment thread src/ModularPipelines/Engine/SecretProvider.cs

@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: c0b26cfe23

ℹ️ 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/ModularPipelines/Engine/SecretProvider.cs
@thomhurst

Copy link
Copy Markdown
Owner Author

Fixed all three new review findings in 928d458: even deferred versions, deadlock-free emission/publication lock ordering, and immediate scoped-registration visibility to direct redactors. Validation: SecretMaskingPatternTests 80/80, SecretObfuscatorCachingTests 14/14, FormattedLogValuesObfuscatorTests 11/11; core Release build 0 warnings/errors.

@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: 928d458473

ℹ️ 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/ModularPipelines/Engine/SecretProvider.cs Outdated
@thomhurst

Copy link
Copy Markdown
Owner Author

Fixed in 8bd802e. Unscoped registration state now immutably upserts the current provider entry and updates its batch instead of prepending a node per secret; execution-context isolation is preserved and chain depth is bounded by distinct providers. A 100-registration regression verifies depth remains one. SecretMaskingPatternTests pass 81/81; core Release build is clean.

@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: 8bd802e24a

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated
Comment thread src/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated

@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: 48f36f9435

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs Outdated
@thomhurst

Copy link
Copy Markdown
Owner Author

Addressed both latest P2s and integrated concurrent lease work at exact head b374449. Output and custom-obfuscation contexts now use expiring ownership scopes: active leases alone permit reentrant behavior, while weakly held scope-keyed buffers preserve exact causal identity across expiry without unbounded dictionary retention. Validation: SecretMaskingPatternTests 83/83; ModularPipelines.slnx Release build 0 warnings/errors. @codex review exact head b374449. @claude review exact head b374449.

@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: b3744497a0

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs
A late CaseInsensitive change could bypass final obfuscation and emit a differently cased secret. Carry the scanned pattern snapshot through emission and reobfuscate when its comparison no longer matches.
ConditionalWeakTable keys may expire after fire-and-forget work while their line buffers still contain accepted output. Retain non-empty scoped states until flush drains them.

@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: 4e6af1fe65

ℹ️ 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/ModularPipelines/Console/CoordinatedTextWriter.cs

@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: 9c2f357f50

ℹ️ 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".

return;
}

_outputLock.Wait();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle unflowed reentrancy during ordinary sink writes

When _realConsole.Write or WriteLine suppresses execution-context flow, starts a worker that writes back through this coordinated writer, and synchronously waits for it, the worker cannot see ActiveOutputWriterScope; because _isAsyncFlushActive is only enabled for FlushRealConsoleAsync, TryDeferOutputWrite also declines it and the worker blocks here on the semaphore still owned by the outer sink callback. The sink then waits for that worker permanently, so the unflowed-reentrancy mechanism needs to cover ordinary sink writes as well as asynchronous flush callbacks.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fccadd4. Sink-active deferral now begins for ordinary Write/WriteLine calls as well as flushes, before a callback can block on the shared line-buffer lock. Raw callback writes retain their line-buffer state and are drained only after the outer public writer operation releases that buffer, so they still go through normal secret masking without recursively mutating the active buffer.

Added DirectConsoleWrite_AllowsReentrantSinkWriteWithoutExecutionContextFlow, which suppresses ExecutionContext, synchronously waits for the callback, and verifies its diagnostic is masked. Validation: SecretMaskingPatternTests 87/87; Release ModularPipelines.csproj build 0 warnings / 0 errors.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Code review (head fccadd430)

Reviewed the delta since the last @claude review request (head b3744497a0fccadd430, commits d5d6e946a, 4e6af1fe6, 9c2f357f5, fccadd430), which adds the input/output write-deferral mechanism (TryDeferInputWrite/TryDeferOutputWrite/Drain*) and the _pendingScopedLineBuffers retention fix. Note @codex couldn't review this round (hit usage limits per its comment), so this covers that gap.

Verified correct:

  • _pendingScopedLineBuffers (a strong-ref HashSet<LineBufferState> maintained by UpdateScopedLineBufferRetention) fixes a real bug: scoped line buffers were previously reachable only through the ConditionalWeakTable-keyed _scopedLineBuffers, so a buffer with pending unflushed content could be GC'd away with its scope key and silently lose data. The new FireAndForgetSinkPartialWriteSurvivesScopeCollection test exercises exactly this (forces a GC between the partial write and the flush) and I traced it — it passes because the state is now kept alive via the pending set until its buffer drains to empty.
  • Lock ordering stays consistent: state.SyncRoot_lineBufferLock remains the only nesting direction (UpdateScopedLineBufferRetention is always called while the caller still holds state.SyncRoot, e.g. WriteCore line 266, FlushState line 1248), so this doesn't introduce a new deadlock path.

Bug: FlushAvailableAsync doesn't drain deferred input writes it can itself produce (likely lost/delayed log lines)

src/ModularPipelines/Console/CoordinatedTextWriter.cs:1171-1175:

internal async Task FlushAvailableAsync()
{
    FlushAvailableOutput();
    await FlushRealConsoleAsync().ConfigureAwait(false);
}

Every other public entry point that can trigger TryDeferInputWrite (WriteLine, Write(string), Write(char), Write(char[],int,int), Write(ReadOnlySpan<char>), Flush(), FlushAsync()) wraps its body in try { ... } finally { DrainDeferredInputWrites(); } so a write deferred during that call gets flushed before the call returns. FlushAvailableAsync is the one path that got the new deferral mechanism's write-through (FlushRealConsoleAsyncCompleteOutputWriteBeginDeferringOutputWrites/sets _isOutputSinkActive = true) but not the matching drain.

Concretely: FlushRealConsoleAsync sets _isOutputSinkActive = true while it writes to _realConsole. If _realConsole (e.g. Spectre.Console's live-display driver) reenters the writer from a callback with ExecutionContext.SuppressFlow() — the exact scenario the new UnflowedReentrantWritingStringWriter/UnflowedAsyncWritingOnFlushStringWriter tests simulate — that reentrant write is correctly detected as non-reentrant-by-AsyncLocal and gets queued via TryDeferInputWrite into _deferredInputWrites. CompleteOutputWrite (line 976) only calls DrainDeferredOutputWrites() (the output-write queue) before clearing _isOutputSinkActive — it never calls DrainDeferredInputWrites(). Since FlushAvailableAsync itself has no finally drain either, that queued input write sits in _deferredInputWrites until some other Write/Flush call happens to run on this same CoordinatedTextWriter instance.

This path isn't hypothetical: ConsoleCoordinator.FlushWriterAsync (ConsoleCoordinator.cs:656-661) calls FlushAvailableAsync for every OutputFlushKind.Incremental flush, which is exactly the periodic/threshold-driven live-console flush (FlushThresholdAsync, FlushInProgressModuleOutputAsync) — i.e. this is the hot path most likely to race with a live-rendering sink's reentrant callback. A line deferred here won't reach the console until an unrelated later Flush()/FlushAsync()/Write* call happens to run on the same writer, which for a module that goes quiet after emitting one diagnostic could mean the line only appears at final pipeline teardown (OutputFlushKind.Complete) instead of live — undermining the incremental-flush feature this PR is optimizing, and in principle could be lost entirely if no later call ever occurs on that writer before disposal.

Suggested fix: give FlushAvailableAsync the same try/finally { DrainDeferredInputWrites(); } wrapper as Flush()/FlushAsync() (or fold the drain into CompleteOutputWrite itself, so every path that can produce a deferred input write also guarantees draining it, instead of relying on each call site to remember).

Architectural observation (not blocking, but worth flagging given the pattern across this PR's ~40 commits):

Each round of review on this PR has surfaced a new, subtle concurrency edge case (secret-pattern versioning, reentrant sink writes, ExecutionContext-suppressed callbacks, scope GC lifetime, and now this deferred-drain gap), each fixed with another targeted lock/flag/queue bolted onto CoordinatedTextWriter. The class now coordinates through 6 distinct lock objects (_flushLock, _outputLock, _lineBufferLock, _customObfuscatorLock, _deferredOutputLock, _secretPatternsLock) plus per-state SyncRoot, two AsyncLocal scopes, a ConditionalWeakTable, and now two deferred-write queues — and the bug above is a direct symptom: it's easy to add a new call path (or, as here, a new mechanism) and forget one of the several places that must stay in sync with it. A single dedicated, independently unit-testable "write dispatcher" component that owns all reentrancy/deferral decisions (instead of every public method independently deciding whether to defer/drain) would make it structurally impossible to add a new entry point without going through the same guaranteed drain path, rather than relying on each future author to remember all the invariants this review history has been accumulating one bug at a time.

No CLAUDE.md compliance issues — changes stay within ModularPipelines.slnx scope (core library + its unit tests) and don't touch build/CI tooling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant