Skip to content

.pr_agent_accepted_suggestions

qodo-merge-bot edited this page Jul 17, 2026 · 37 revisions
                     PR 5678 (2026-07-15)                    
[reliability] FakeLifetime CTS not disposed
FakeLifetime CTS not disposed `FakeLifetime` creates a `CancellationTokenSource` (`_stopping`) but never disposes it, which can leak registrations/resources and violates CTS lifecycle ownership expectations. Even in tests, this can accumulate across runs and mask disposal issues.

Issue description

FakeLifetime creates a CancellationTokenSource but never disposes it.

Issue Context

PR compliance requires the creator of a CancellationTokenSource to manage its lifecycle (cancel/dispose). In this test helper, _stopping is canceled but not disposed.

Fix Focus Areas

  • src/KurrentDB.Core.XUnit.Tests/Services/Transport/Grpc/GrpcStreamingShutdownMiddlewareTests.cs[113-119]

[reliability] Client streaming not cancelled
Client streaming not cancelled GrpcStreamingShutdownMiddleware only links ApplicationStopping for ServerStreaming and DuplexStreaming calls, so MethodType.ClientStreaming RPCs bypass shutdown cancellation and can remain active during graceful shutdown. This leaves a class of streaming calls (e.g., Streams.Append) able to delay Kestrel draining if the client keeps the request stream open.

Issue description

GrpcStreamingShutdownMiddleware cancels only server-streaming and duplex-streaming gRPC calls on IHostApplicationLifetime.ApplicationStopping. Client-streaming calls still use the original HttpContext.RequestAborted, so they may remain in-flight during shutdown if the client holds the request stream open.

Issue Context

The codebase defines at least one client-streaming RPC (Streams.Append). The PR goal is to cancel open streaming gRPC calls during shutdown so Kestrel drains promptly.

Fix Focus Areas

  • src/KurrentDB.Core/Services/Transport/Grpc/GrpcStreamingShutdownMiddleware.cs[27-33]
  • src/KurrentDB.Core.XUnit.Tests/Services/Transport/Grpc/GrpcStreamingShutdownMiddlewareTests.cs[18-55]


                     PR 5667 (2026-07-06)                    
[correctness] API V2 ignores config
API V2 ignores config The new GrpcOptions.CompressionLevel setting is only applied in ClusterVNodeStartup, but API V2 gRPC services still enable gzip compression using the default CompressionLevel.Optimal via WithGrpcService/WithCompression. As a result, setting CompressionLevel=NoCompression will not actually disable response compression for API V2 endpoints.

Issue description

The PR introduces GrpcOptions.CompressionLevel and uses it to control response compression in ClusterVNodeStartup, but API V2 gRPC wiring still unconditionally enables gzip compression with default CompressionLevel.Optimal. This makes the new setting behave inconsistently across gRPC endpoints.

Issue Context

KurrentDB.Plugins.Api.V2.ApiV2Plugin registers services with WithGrpcService, which calls options.WithCompression() without consulting ClusterVNodeOptions.Grpc.CompressionLevel.

Fix Focus Areas

  • src/KurrentDB.Api.V2/Infrastructure/DependencyInjection/GrpcServerBuilderExtensions.cs[22-26]
  • src/KurrentDB.Api.V2/Infrastructure/Grpc/Compression/GrpcCompressionExtensions.cs[11-26]
  • src/KurrentDB.Plugins.Api.V2/ApiV2Plugin.cs[24-57]
  • src/KurrentDB.Core/ClusterVNodeStartup.cs[291-316]

Suggested fix

Make API V2 service compression honor ClusterVNodeOptions.Grpc.CompressionLevel:

  • Option A (localized): In ApiV2Plugin.ConfigureServices, add services.Configure<GrpcServiceOptions<IndexesService>>((sp, options) => { ... }) and similarly for StreamsService, reading sp.GetRequiredService<ClusterVNodeOptions>().Grpc.CompressionLevel and applying:
    • options.WithoutCompression() when NoCompression
    • options.WithCompression(level) otherwise
  • Option B (more general): Refactor WithGrpcService to not force compression by default, or to configure compression via an overload that can access ClusterVNodeOptions.

Ensure the gzip provider remains registered if you still need request decompression when response compression is disabled.



                     PR 5665 (2026-07-01)                    
[reliability] `CallbackEnvelope` used with `TCS`
`CallbackEnvelope` used with `TCS` The new gRPC `TruncateParked` handler awaits a bus reply using `CallbackEnvelope` + `TaskCompletionSource`, which can run continuations on the bus dispatch thread and risks deadlocks/unsafe reentrancy. This violates the required `TcsEnvelope` pattern for awaiting bus responses.

Issue description

The new gRPC TruncateParked implementation publishes a bus message using CallbackEnvelope and manually waits via TaskCompletionSource, which violates the repository’s safe envelope pattern.

Issue Context

The codebase provides TcsEnvelope<T> which uses RunContinuationsAsynchronously and avoids continuations running on the bus thread.

Fix Focus Areas

  • src/KurrentDB.Core/Services/Transport/Grpc/PersistentSubscriptions.TruncateParked.cs[19-49]

[maintainability] Unnamed boolean argument `true`
Unnamed boolean argument `true` `BeginMarkParkedMessagesReprocessed(truncateBefore, null, true)` passes a boolean literal positionally, obscuring the meaning at the call site. This reduces readability and violates the requirement to use named boolean arguments.

Issue description

A boolean literal is passed positionally, making it unclear what the true value represents.

Issue Context

The method parameter is named updateOldestParkedMessage, so the call site should use a named argument for clarity.

Fix Focus Areas

  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscription.cs[589-590]

[correctness] Oldest parked timestamp cleared
Oldest parked timestamp cleared PersistentSubscription.TruncateParkedMessages always calls BeginMarkParkedMessagesReprocessed(truncateBefore, null, true), which clears the cached oldest parked timestamp even when truncating only up to stopAt (leaving parked messages). This makes PersistentSubscriptionStats report OldestParkedMessage as 0/empty despite remaining parked messages.

Issue description

TruncateParkedMessages clears the cached oldest parked message timestamp by calling BeginMarkParkedMessagesReprocessed(..., null, true) unconditionally. When truncating only part of the parked stream, parked messages still remain but OldestParkedMessage stats become incorrect.

Issue Context

  • PersistentSubscriptionStats computes OldestParkedMessage from MessageParker.GetOldestParkedMessage.
  • PersistentSubscriptionMessageParker.BeginMarkParkedMessagesReprocessed assigns _oldestParkedMessage = timestamp when updateOldestParkedMessage is true.

Fix Focus Areas

  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscription.cs[580-593]

Implementation notes

  • Only clear oldest timestamp when truncating all parked messages.
  • For partial truncation, recompute the oldest remaining parked timestamp (e.g., after writing the new truncate metadata, trigger a stats refresh via BeginLoadStats, or introduce a parker API to read the first remaining event timestamp from truncateBefore).

[reliability] No cache refresh on NoStream
No cache refresh on NoStream PersistentSubscription.TruncateParkedMessages returns early when BeginReadEndSequence yields null (NoStream), so it performs no cache refresh/update. If the parked stream was deleted externally, the in-memory cached parked stats (count/oldest) can remain stale after calling the new API.

Issue description

When the parked stream does not exist (BeginReadEndSequence returns null), TruncateParkedMessages exits without updating any cached stats. This undermines the intent of the API for cases where the parked stream was removed externally.

Issue Context

  • BeginReadEndSequence returns null on ReadStreamResult.NoStream.
  • The parker’s cached ParkedMessageCount depends on cached _lastTruncateBefore and _lastParkedEventNumber.
  • BeginLoadStats is the mechanism that resets these cached values based on the current stream state.

Fix Focus Areas

  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscription.cs[580-593]

Implementation notes

  • In the !end.HasValue branch, trigger a MessageParker.BeginLoadStats(...) to resync cached stats (or introduce a more targeted reset method on the parker).

[observability] Truncate inflates replay metric
Truncate inflates replay metric TruncateParkedMessages calls BeginReadEndSequence, which increments the parker’s _parkedMessageReplays counter, so truncation requests will be counted as parked-message replays in stats/metrics. This skews ParkedMessageReplays observability.

Issue description

BeginReadEndSequence increments _parkedMessageReplays, but TruncateParkedMessages now uses it for truncation, causing truncate calls to inflate the replay counter.

Issue Context

  • PersistentSubscriptionStats exposes ParkedMessageReplays.

Fix Focus Areas

  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscription.cs[580-593]

Implementation notes

  • Options: add a new parker method for reading end sequence without incrementing the replay counter; or move the increment to the replay-specific path only and keep BeginReadEndSequence “pure”.


                     PR 5664 (2026-07-01)                    
[reliability] `CallbackEnvelope` used with `TaskCompletionSource`
`CallbackEnvelope` used with `TaskCompletionSource` The new gRPC `TruncateParked` handler publishes a bus message using `CallbackEnvelope` and awaits completion via a `TaskCompletionSource`, which the checklist disallows due to continuation execution risks on the bus dispatch thread. This should use `TcsEnvelope` for awaiting bus responses.

Issue description

TruncateParked uses CallbackEnvelope + TaskCompletionSource to await a bus response, which is disallowed by the checklist.

Issue Context

The code currently creates a TaskCompletionSource<TruncateParkedResp> and publishes ClientMessage.TruncateParkedMessages with new CallbackEnvelope(...), then completes the TCS from the callback. The approved pattern is to use TcsEnvelope<T> (which uses RunContinuationsAsynchronously) instead of wiring callbacks manually.

Fix Focus Areas

  • src/KurrentDB.Core/Services/Transport/Grpc/PersistentSubscriptions.TruncateParked.cs[19-79]

[correctness] Oldest timestamp cleared
Oldest timestamp cleared PersistentSubscription.TruncateParkedMessages passes (timestamp: null, updateOldestParkedMessage: true) to BeginMarkParkedMessagesReprocessed, which overwrites the cached oldest-parked-message timestamp with null even when parked messages still remain after a partial truncate. This makes OldestParkedMessage in persistent subscription stats/metrics incorrect.

Issue description

PersistentSubscription.TruncateParkedMessages calls BeginMarkParkedMessagesReprocessed(truncateBefore, null, true). In PersistentSubscriptionMessageParker, updateOldestParkedMessage == true causes _oldestParkedMessage to be set to the provided timestamp; with null this clears the cached value even when the parked stream still has events after truncation.

This breaks OldestParkedMessage reporting for partial truncations (e.g., truncating up to event #10 should set oldest to the first remaining event’s timestamp, not null).

Issue Context

  • The parker’s “oldest parked message” is exposed via PersistentSubscriptionStats and exported as a metric.
  • Truncation currently does not read the first remaining parked event to compute the new oldest timestamp.

Fix Focus Areas

  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscription.cs[580-592]
  • src/KurrentDB.Core/Services/PersistentSubscription/IPersistentSubscriptionMessageParker.cs[1-30]
  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscriptionMessageParker.cs[129-246]
  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscriptionStats.cs[80-125]

Suggested fix approach

  • Add a parker API specifically for truncation that can update both truncateBefore and the oldest timestamp correctly (e.g., write metadata then read-forward from the new truncate point to find the first remaining event timestamp, or detect empty stream).
  • Alternatively, extend BeginMarkParkedMessagesReprocessed to support a completion callback and then refresh stats (BeginLoadStats) after the metadata write completes so _oldestParkedMessage is recomputed reliably.

[maintainability] Positional `true` boolean argument
Positional `true` boolean argument The new truncation logic calls `BeginMarkParkedMessagesReprocessed(..., true)` using a positional boolean argument, which reduces clarity and violates the named-boolean call-site policy. This increases the risk of incorrectly passing `true/false` as the API evolves.

Issue description

A boolean parameter is passed positionally as true, obscuring intent.

Issue Context

In TruncateParkedMessages, the call to _settings.MessageParker.BeginMarkParkedMessagesReprocessed(...) includes a trailing true argument without naming, making it unclear what behavior is being enabled.

Fix Focus Areas

  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscription.cs[585-591]

[observability] Replay metric inflated
Replay metric inflated TruncateParkedMessages calls BeginReadEndSequence to find the last parked event number, but BeginReadEndSequence increments the ParkedMessageReplays counter. This causes truncation requests to be counted as replays, inflating the parked replay metric.

Issue description

PersistentSubscription.TruncateParkedMessages uses IPersistentSubscriptionMessageParker.BeginReadEndSequence(...) to determine the end of the parked stream. In PersistentSubscriptionMessageParker, BeginReadEndSequence increments _parkedMessageReplays, so truncation operations incorrectly increase the ParkedMessageReplays metric.

Issue Context

ParkedMessageReplays is exported in subscription stats/metrics and is intended to represent replay activity, not administrative truncations.

Fix Focus Areas

  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscription.cs[580-592]
  • src/KurrentDB.Core/Services/PersistentSubscription/IPersistentSubscriptionMessageParker.cs[1-30]
  • src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscriptionMessageParker.cs[129-152]

Suggested fix approach

  • Introduce a separate parker method for “read last parked event number” that does not increment ParkedMessageReplays, and use it from TruncateParkedMessages.
  • Or add a flag/overload to BeginReadEndSequence (e.g., countAsReplay: bool) and pass false for truncation.


                     PR 5643 (2026-06-17)                    
[maintainability] `DeleteAsync` uses bare booleans
`DeleteAsync` uses bare booleans The call to `ProjectionsService.DeleteAsync` passes multiple `true` literals positionally, making intent unclear and increasing the risk of parameter-order mistakes. Compliance requires named boolean arguments at call sites for readability and correctness.

Issue description

A call site passes multiple boolean literals positionally (e.g., true, true, true), which obscures what each flag means.

Issue Context

The compliance policy requires named boolean arguments to prevent misinterpretation and future bugs when signatures evolve.

Fix Focus Areas

  • src/KurrentDB/Components/Projections/ProjectionDetail.razor.cs[155-156]
  • src/KurrentDB/Components/Projections/ProjectionsService.cs[97-101]
  • src/KurrentDB.Components.Tests/ProjectionsServiceAuthorizationTests.cs[82-85]

[maintainability] `EngineVersion` defaults to `1`
`EngineVersion` defaults to `1` The PR introduces raw numeric literals like `1` for projection engine version defaults/fallbacks instead of using `ProjectionConstants.EngineV1`/`EngineDefault`. This violates the magic-number policy for versions and makes future changes/error-checking harder.

Issue description

Engine version is represented using the magic number 1 in new/modified code, despite an existing named constant.

Issue Context

Projection engine versions are a well-known sentinel/version value and should use ProjectionConstants.EngineV1 (or ProjectionConstants.EngineDefault) rather than raw numeric literals.

Fix Focus Areas

  • src/KurrentDB.Projections.Shared/Services/ProjectionStatistics.cs[78-79]
  • src/KurrentDB.Projections.Management/Services/Management/ManagedProjection.cs[320-323]
  • src/KurrentDB/Components/Projections/ProjectionConfigDialog.razor[45-47]
  • src/KurrentDB/Components/Projections/ProjectionDetail.razor.cs[180-185]


                     PR 5639 (2026-06-09)                    
[maintainability] Unnamed `requireLeader` boolean
Unnamed `requireLeader` boolean `ClientMessage.WriteEvents.ForSingleEvent` is called with an unnamed boolean literal for `requireLeader`, reducing clarity and violating the named-boolean-arguments convention. This makes it harder to audit leadership semantics at the call site.

Issue description

A boolean literal is passed positionally to ForSingleEvent(...) where the parameter is requireLeader, violating the convention that boolean arguments should be named at call sites.

Issue Context

Naming the argument (e.g., requireLeader: true) improves readability and prevents mistakes when signatures change.

Fix Focus Areas

  • src/KurrentDB.Projections.Management/Services/Management/ManagedProjection.cs[745-747]

[correctness] Unhandled RecordTooLarge RPCs
Unhandled RecordTooLarge RPCs ManagedProjection now replies ProjectionManagementMessage.RecordTooLarge when a definition write would exceed TFConsts.EffectiveMaxLogRecordSize. Several projection gRPC endpoints (e.g., Enable) still treat this reply as an UnknownMessage and return StatusCode.Unknown instead of a meaningful status (like InvalidArgument).

Issue description

ManagedProjection can now reply with ProjectionManagementMessage.RecordTooLarge, but gRPC handlers other than Create/Update (e.g., Enable) still only handle Updated/NotFound and fall back to UnknownMessage, surfacing the new error as StatusCode.Unknown.

Issue Context

  • Enable/Reset set _pendingWritePersistedState = true and use _lastReplyEnvelope, so they can traverse WriteStartOrLoadStopped() and hit FailPersistedStateWrite().
  • FailPersistedStateWrite() replies RecordTooLarge.

Fix Focus Areas

  • src/KurrentDB.Projections.Management/Services/Management/ManagedProjection.cs[714-760]
  • src/KurrentDB.Projections.Management/Services/Management/ManagedProjection.cs[410-428]
  • src/KurrentDB.Projections.Management/Services/Management/ManagedProjection.cs[542-550]
  • src/KurrentDB.Projections.Management/Services/Grpc/ProjectionManagement.Enable.cs[38-49]
  • src/KurrentDB.Projections.Management/Services/Grpc/ProjectionManagement.Disable.cs[40-51]
  • src/KurrentDB.Projections.Management/Services/Grpc/ProjectionManagement.Reset.cs[38-49]
  • src/KurrentDB.Projections.Management/Services/Grpc/ProjectionManagement.Delete.cs[41-52]

Suggested fix

  • Mirror the Create/Update pattern across the other RPC handlers:
    • Add case ProjectionManagementMessage.RecordTooLarge tooLarge: ...RecordTooLarge(tooLarge.Reason).
    • Add case ProjectionManagementMessage.OperationFailed failed: ...OperationFailed(failed.Reason).
  • Consider factoring a shared helper to map ProjectionManagementMessage.OperationFailed subtypes consistently across all endpoints.

[correctness] gRPC errors lose semantics
gRPC errors lose semantics ProjectionManagement.Create/Update map all ProjectionManagementMessage.OperationFailed subtypes (e.g., Conflict/NotAuthorized) to StatusCode.FailedPrecondition via OperationFailed(), so clients cannot distinguish AlreadyExists vs PermissionDenied-style failures. This can break client error handling that branches on gRPC status codes.

Issue description

Create/Update treat any ProjectionManagementMessage.OperationFailed (including Conflict and NotAuthorized) as StatusCode.FailedPrecondition, collapsing distinct error semantics into one status code.

Issue Context

  • Conflict is emitted for duplicate projection names.
  • NotAuthorized is emitted by RunAs.ValidateRunAs.
  • Both derive from OperationFailed and are currently mapped through a single helper.

Fix Focus Areas

  • src/KurrentDB.Projections.Management/Services/Grpc/ProjectionManagement.Create.cs[78-93]
  • src/KurrentDB.Projections.Management/Services/Grpc/ProjectionManagement.Update.cs[48-65]
  • src/KurrentDB.Projections.Management/Services/Grpc/ProjectionManagement.cs[38-55]

Suggested fix

  • Add explicit switch cases in Create/Update:
    • ProjectionManagementMessage.Conflict -> new RpcException(new Status(StatusCode.AlreadyExists, reason)) (or another agreed code).
    • ProjectionManagementMessage.NotAuthorized -> new RpcException(new Status(StatusCode.PermissionDenied, reason)).
    • Keep the existing RecordTooLarge mapping to InvalidArgument.
  • Optionally keep OperationFailed as FailedPrecondition only for the truly-generic cases.


                     PR 5628 (2026-05-13)                    
[maintainability] License notice version pinned
License notice version pinned tools/license-notices/generate-notice.ps1 overrides SharpCompress as version 0.30.1 via overridden-packages.json, so NOTICE.md (which is packed into the shipped KurrentDB artifact) will report SharpCompress as 0.30.1 regardless of any SharpCompress version change introduced by this dependency upgrade. This risks shipping inaccurate third‑party attribution and makes it harder to validate the CVE remediation outcome from this PR.

Issue description

The repo’s NOTICE generation hardcodes SharpCompress as 0.30.1 via tools/license-notices/overridden-packages.json, and NOTICE.md is packed into the main KurrentDB artifact. After updating package versions in this PR, the shipped/generated NOTICE cannot accurately reflect any SharpCompress version change resulting from the new dependency graph.

Issue Context

  • generate-notice.ps1 calls nuget-license with --override-package-information overridden-packages.json, which includes SharpCompress with a fixed version.
  • NOTICE.md currently lists SharpCompress 0.30.1 and is packed into the output artifact.

Fix Focus Areas

  • src/Directory.Packages.props[50-61]
  • tools/license-notices/generate-notice.ps1[4-21]
  • tools/license-notices/overridden-packages.json[57-61]
  • NOTICE.md[146-149]
  • src/KurrentDB/KurrentDB.csproj[50-58]

What to do

  1. Re-resolve dependencies (restore) with the updated package set.
  2. Regenerate NOTICE.md using tools/license-notices/generate-notice.ps1.
  3. Update/remove the SharpCompress entry in overridden-packages.json so it matches the resolved dependency version (or so it is no longer forcibly pinned if it can be automatically detected).
  4. Commit the updated NOTICE.md (and override file changes, if any).


                     PR 5625 (2026-05-12)                    
[maintainability] Stale license override version
Stale license override version The repo pins SharpCompress to 0.48.0, but the license-notice override file still hard-codes SharpCompress 0.30.1, so regenerating NOTICE.md via the provided script can produce inconsistent/outdated notice metadata. This undermines repeatability of the license-notice generation process and can reintroduce stale NOTICE output later.

Issue description

tools/license-notices/overridden-packages.json still contains an override entry for SharpCompress version 0.30.1, but this PR pins SharpCompress to 0.48.0. The NOTICE generation script always supplies this override file, so future NOTICE regenerations can drift or become incorrect.

Issue Context

  • SharpCompress is now pinned centrally to 0.48.0.
  • The NOTICE generation script passes --override-package-information overridden-packages.json.

Fix Focus Areas

  • tools/license-notices/overridden-packages.json[36-42]
  • tools/license-notices/generate-notice.ps1[1-12]
  • src/Directory.Packages.props[147-148]

What to change

  • Update the SharpCompress override entry to version 0.48.0 or remove the SharpCompress override entry entirely if it’s no longer needed for that version.
  • (Optional but recommended) Re-run tools/license-notices/generate-notice.ps1 to confirm NOTICE.md remains stable and correct after the override change.


                     PR 5622 (2026-05-08)                    
[maintainability] Stale NOTICE/license override
Stale NOTICE/license override Snappier is pinned to 1.3.1, but the repo’s committed license disclosure artifacts still record Snappier as 1.0.0, making the published NOTICE/license metadata inconsistent with the dependency actually restored. This can break internal compliance expectations and/or cause incorrect license reporting for releases built from this branch.

Issue description

Snappier was upgraded/pinned, but the repository’s license disclosure artifacts still list the old Snappier version.

Issue Context

  • src/Directory.Packages.props now pins Snappier to 1.3.1.
  • NOTICE.md and tools/license-notices/overridden-packages.json still reference Snappier 1.0.0.
  • tools/license-notices/generate-notice.ps1 generates NOTICE.md using overridden-packages.json.

Fix Focus Areas

  • src/Directory.Packages.props[149-151]
  • NOTICE.md[145-148]
  • tools/license-notices/overridden-packages.json[42-52]
  • tools/license-notices/generate-notice.ps1[1-22]

Suggested fix

  1. Update (or remove, if no longer needed) the Snappier entry in tools/license-notices/overridden-packages.json so it matches the version being restored (1.3.1).
  2. Re-run tools/license-notices/generate-notice.ps1 and commit the updated NOTICE.md so it reflects the upgraded dependency set.


                     PR 5608 (2026-04-30)                    
[correctness] Misleading date-range example
Misleading date-range example The date-range example computes `append_time` via `epoch_ms(created_at)` even though `created_at` is documented as `INT64`, then filters using string dates on the computed alias, which is inconsistent and likely to produce errors or surprising results when run.

Issue description

The date-range query example is inconsistent with the documented type of created_at (INT64) and mixes an epoch conversion with date-string filtering on a derived alias.

Issue Context

This is an “Examples” code block; readers will copy/paste it. Prefer an example that clearly converts created_at to a timestamp (or filters on created_at directly) and uses standard SQL comments.

Fix Focus Areas

  • docs/server/features/queries/flightsql.md[33-53]

[correctness] Invalid JSON operator usage
Invalid JSON operator usage The complex example uses `data::json->>'Address'->>'Country'`, which applies a JSON operator (`->>`) to the text result of a prior `->>`, and the user-defined index examples use `data->>` without casting even though `data` is documented as `VARCHAR`.

Issue description

The examples should consistently cast data to JSON before using ->/->> operators, and the nested JSON access should use correct navigation (e.g., -> for object access then ->> for text extraction).

Issue Context

The same page says data is VARCHAR and must be cast to JSON to use arrow navigation operators.

Fix Focus Areas

  • docs/server/features/queries/flightsql.md[33-63]
  • docs/server/features/queries/flightsql.md[76-90]

[maintainability] Missing 'to' in sentence
Missing 'to' in sentence The Introduction sentence is missing a word (“required to query”), which reduces clarity for readers configuring FlightSQL authentication requirements.

Issue description

The sentence “Authentication ... is required query ...” is missing “to”, making it ungrammatical.

Issue Context

This is user-facing documentation and is likely to be copy/pasted or read quickly; fixing the grammar improves comprehension.

Fix Focus Areas

  • docs/server/features/queries/flightsql.md[18-22]


                     PR 5607 (2026-04-29)                    
[correctness] Invalid OAuth key syntax
Invalid OAuth key syntax The new OAuth setting is documented as `OAuth::DisableCodeChallengeMethodsSupportedValidation`, but the docs’ configuration conventions use YAML nesting (`OAuth:`) and `__` for nesting in env/CLI; `::` is inconsistent and will likely mislead users into setting a non-functional key.

Issue description

The docs introduce a new OAuth setting using OAuth::DisableCodeChallengeMethodsSupportedValidation, but the documentation’s established configuration patterns use YAML nesting (OAuth:) and __ for nesting in env/CLI. The :: syntax is inconsistent and likely wrong.

Issue Context

In the OAuth docs, configuration is shown under an OAuth: section and options are listed as direct property names (e.g., DisableCodeChallengeMethodsSupportedValidation). The configuration guide documents nesting via __.

Fix Focus Areas

  • docs/server/quick-start/whatsnew.md[64-73]
  • docs/server/security/user-authentication.md[366-415]
  • docs/server/configuration/configuration.md[86-98]


                     PR 5606 (2026-04-29)                    
[reliability] Bearer token crashes auth
Bearer token crashes auth BasicHttpAuthenticationProvider now accepts the "Bearer" scheme and then calls Convert.FromBase64String without handling invalid input, so a non-base64 Bearer token can throw and fail the request with an unhandled exception instead of returning 401/Unauthenticated. This exception is not caught by AuthenticationMiddleware (it only suppresses cancellation), so it propagates out of the request pipeline.

Issue description

BasicHttpAuthenticationProvider now treats Authorization: Bearer <token> as a base64-encoded username:password. If <token> is not valid base64 (e.g., JWT/opaque bearer tokens, malformed input), Convert.FromBase64String throws and the request fails with an unhandled exception.

Issue Context

This is newly more likely after the PR because the provider now accepts the Bearer scheme, but still uses a non-Try* base64 decode. Authentication middleware only suppresses cancellation exceptions, so format errors will propagate.

Fix Focus Areas

  • src/KurrentDB.Core/Services/Transport/Http/Authentication/BasicHttpAuthenticationProvider.cs[14-46]
  • src/KurrentDB.Core/Services/Transport/Http/AuthenticationMiddleware.cs[28-37]

Suggested change

  • Update TryDecodeCredential to:
    • return false when value is null/empty
    • catch FormatException (and possibly ArgumentNullException) from base64 decoding, returning false
    • optionally use Convert.TryFromBase64String(...) to avoid exceptions entirely
  • (Optional but recommended) add/extend tests to cover:
    • Authorization: Bearer not_base64 should not throw; it should return false (allowing other providers/anonymous handling)
    • Authorization: Bearer <valid-base64-user:pass> should authenticate equivalently to Basic

[correctness] Handshake response can stall
Handshake response can stall FlightSqlServer.Handshake reads until the request stream completes before writing any response, so clients that send a handshake request but don’t complete/half-close the stream promptly may see the handshake response delayed or never received until cancellation/deadline. This can break or severely slow JDBC tooling that expects a prompt handshake response.

Issue description

FlightSqlServer.Handshake drains the entire request stream before sending response headers and the handshake response. For a bidirectional streaming RPC, some clients may send a request and wait for the server response without completing the stream, which delays the response indefinitely (or until cancellation/deadline).

Issue Context

The PR adds this Handshake override to support JDBC tooling by echoing Basic credentials as a Bearer token in response headers.

Fix Focus Areas

  • src/KurrentDB.SecondaryIndexing/FlightSql/FlightSqlServer.cs[41-61]

Suggested change

  • Read at most the first request message (e.g., a single await requestStream.MoveNext(...)) and then immediately:
    • set response headers (if needed)
    • write the FlightHandshakeResponse
  • If additional client messages are possible, consider ignoring them without blocking the initial response (or document/validate expected client behavior).


                     PR 5600 (2026-04-28)                    
[correctness] Webhook connector not discoverable
Webhook connector not discoverable The PR adds the `Kurrent.Connectors.Webhook` package, but the runtime only creates/validates connectors that are explicitly registered in `ConnectorCatalogue`, which currently contains no Webhook connector types. Any config that sets `ConnectorOptions.InstanceTypeName` to a webhook connector will fail validation and/or throw during connector creation.

Issue description

Kurrent.Connectors.Webhook is added as a dependency, but webhook connectors still cannot be created/validated because connector discovery is hard-coded to ConnectorCatalogue and webhook types aren’t registered there.

Issue Context

Connector creation (SystemConnectorsFactory), validation (SystemConnectorsValidation), and data protection (ConnectorsMasterDataProtector) all depend on ConnectorCatalogue.TryGetConnector(...). If the webhook connector types are meant to be supported, they must be added to the catalogue along with validator and data protector types (and any required entitlements).

Fix Focus Areas

  • src/Connectors/KurrentDB.Connectors/Infrastructure/Connect/Components/Connectors/ConnectorCatalogue.cs[20-58]
  • src/Connectors/KurrentDB.Connectors/Infrastructure/Connect/Components/Connectors/ConnectorDataProtectors.cs[16-99]
  • src/Connectors/KurrentDB.Connectors/Infrastructure/Connect/Components/Connectors/ConnectorsMasterDataProtector.cs[69-76]
  • src/Connectors/KurrentDB.Connectors/Infrastructure/Connect/Components/Connectors/SystemConnectorsValidation.cs[11-22]

Implementation notes

  • If webhook support is intended: add the webhook connector type(s) from Kurrent.Connectors.Webhook into the Items dictionary, pointing to their validator and a new ...ConnectorDataProtector that marks any secrets (tokens, headers, etc.) as sensitive.
  • If webhook support is not intended yet: remove the added PackageReference and central PackageVersion entry to avoid a misleading/unused dependency.


                     PR 5592 (2026-04-23)                    
[observability] Cache size metric lags
Cache size metric lags `PartitionStateCache.Count` is maintained via increments on `Set` and decrements in the async eviction callback, so it can temporarily exceed the true bounded cache size. This value is exposed as `PartitionStateCacheSize` in HTTP stats, potentially misleading operators/alerts expecting it to be ≤ `MaxPartitionStateCacheSize`.

Issue description

The PartitionStateCacheSize surfaced to operators is backed by a lagging application-level counter that can temporarily exceed the configured capacity due to asynchronous eviction callbacks.

Issue Context

Tests already acknowledge eviction is asynchronous and avoid asserting Size <= capacity.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[73-84]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionStateCache.cs[37-69]
  • src/KurrentDB.Projections.V2.Tests/Integration/PartitionStateCacheEvictionTests.cs[194-214]

Suggested fix

Choose one:

  1. If RandomAccessCache exposes an internal count/current size, use that instead of the manual _count for Size.
  2. If not available, adjust the operator surface to avoid implying a hard bound:
    • rename the stat to indicate it’s approximate (and/or document it explicitly in HTTP stats output), or
    • clamp/report min(Count, capacity) plus report capacity separately.
  3. Update test comments/assertions if the metric semantics change.

[maintainability] `ProjectionSubsystemOptions` booleans unnamed
`ProjectionSubsystemOptions` booleans unnamed Several updated `ProjectionSubsystemOptions(...)` call sites pass boolean arguments positionally (`true`/`false`) instead of using named arguments, reducing clarity and increasing risk of misordered parameters. This violates the convention requiring named boolean arguments at call sites where it improves readability.

Issue description

Boolean arguments in ProjectionSubsystemOptions(...) calls are passed positionally, which is unclear and easy to break when adding/reordering parameters.

Issue Context

The PR added a new trailing parameter, so these call sites were touched and are now in-scope to align with the named-boolean-arguments convention.

Fix Focus Areas

  • src/KurrentDB.Projections.JavaScript.Tests/Integration/ProjectionRuntimeScenario.cs[30-30]
  • src/KurrentDB.Projections.Management.Tests/ClientAPI/specification_with_standard_projections_runnning.cs[43-46]

[maintainability] `ExpectedRevision` hardcoded `-2`
`ExpectedRevision` hardcoded `-2` The new integration test hardcodes `ExpectedRevision = -2` ("Any") instead of using a named constant/helper, which obscures intent and embeds protocol-specific magic numbers. This violates the no-magic-numbers convention where a named representation exists.

Issue description

The test uses ExpectedRevision = -2 to mean "Any", which is a magic number.

Issue Context

There is a named representation available in the codebase (AnyStreamRevision.Any.ToInt64()), which preserves intent and avoids encoding protocol constants inline.

Fix Focus Areas

  • src/KurrentDB.Projections.V2.Tests/Integration/PartitionStateCacheEvictionTests.cs[48-52]

[reliability] Drain uses canceled token
Drain uses canceled token Partition processors are intentionally run with `CancellationToken.None` to drain and checkpoint, but the `loadPersistedState` callback uses the engine cancellation token (`ct`), so a cache miss during shutdown can throw `OperationCanceledException` and abort draining/checkpoint completion. Bounded caches increase cache misses, making this shutdown race more likely than before.

Issue description

On shutdown, partitions are designed to keep draining (partitionCt = CancellationToken.None), but persisted state loads still honor the canceled engine token. With bounded caches, a cache miss can happen during drain and cause OperationCanceledException, faulting a partition task and potentially preventing a final checkpoint from completing.

Issue Context

  • Partitions drain to preserve correctness/checkpointing on stop.
  • Cache eviction increases the probability of LoadPartitionState needing to call loadPersistedState during drain.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[116-133]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionProcessor.cs[59-78]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[276-291]

Suggested fix

  1. Change the loadPersistedState lambda passed to PartitionProcessor to use CancellationToken.None (or the same partitionCt) instead of the engine ct.
  2. Optionally add defensive handling in PartitionProcessor.LoadPartitionState:
    • Catch OperationCanceledException from loadPersistedState and retry once with CancellationToken.None (to preserve the drain semantics).
  3. Add/adjust a test that cancels the engine while partitions are draining and verifies the engine still reaches a final checkpoint without faulting due to persisted-state reads.

[maintainability] `MaxPartitionStateCacheSize` uses `100_000`
`MaxPartitionStateCacheSize` uses `100_000` The new `MaxPartitionStateCacheSize` option and multiple updated call sites hardcode `100_000` rather than using a named constant/default, reducing readability and making future changes error-prone. This violates the no-magic-numbers convention for domain defaults.

Issue description

100_000 is hardcoded as the default/argument for MaxPartitionStateCacheSize, which violates the project convention to avoid magic numbers for domain defaults.

Issue Context

The codebase already uses named defaults like Opts.MaxProjectionStateSizeDefault. The new cache-size default should follow the same pattern so that changing the default is centralized.

Fix Focus Areas

  • src/KurrentDB.Core/Configuration/ClusterVNodeOptions.cs[573-577]
  • src/KurrentDB.Projections.JavaScript.Tests/Integration/ProjectionRuntimeScenario.cs[30-30]

[correctness] Restart test replays with state
Restart test replays with state `evicted_partition_state_is_recovered_from_stream` starts engine2 from `TFPos(0,0)` while also loading the latest persisted partition state from the `-state` stream, which can apply earlier events on top of a later snapshot and yield incorrect state. This does not match the production restart path, where `CoreProjectionV2` starts the engine from the last persisted checkpoint position.

Issue description

PartitionStateCacheEvictionTests.evicted_partition_state_is_recovered_from_stream starts the second engine from TFPos(0,0) even though it also relies on loading the latest persisted partition snapshot from the -state stream. This can cause earlier events (before the snapshot) to be applied on top of the snapshot, producing incorrect state and making the test inconsistent with the real restart flow.

Issue Context

In production, CoreProjectionV2 reads the last checkpoint event and starts the engine from that checkpoint. The -state stream snapshot is implicitly tied to that checkpoint position.

Fix Focus Areas

  • src/KurrentDB.Projections.V2.Tests/Integration/PartitionStateCacheEvictionTests.cs[306-323]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/CoreProjectionV2.cs[187-209]

Suggested fix

  1. Parse the checkpoint position written by engine1 (from the checkpoint stream event payload), and start engine2 from that TFPos.
  2. Alternatively, drive the test via the same harness/path that CoreProjectionV2 uses (read checkpoint then start), rather than constructing ProjectionEngineV2 directly for the recovery scenario.
  3. Keep the assertion that p1 reaches count=2, but ensure engine2 processes only events after the checkpoint that corresponds to the persisted -state snapshot.


                     PR 5590 (2026-04-22)                    
[correctness] Projection name inconsistency
Projection name inconsistency Docs now mix hyphenated and underscored system projection names (e.g., `$stream-by-category` vs `$stream_by_category`, `$by-category` vs `$by_category`), which can lead users to call non-existent projection endpoints and get 404s. The server’s canonical projection names use underscores, so the hyphenated variants should be corrected for consistency and correctness.

Issue description

Some docs refer to system projections using hyphenated names (e.g. $stream-by-category, $by-category, $by-event-type) while others (including the newly-added/updated sections in this PR) use underscore names. The server code indicates the canonical names use underscores, so the hyphenated references should be updated to prevent users from copying invalid projection names into API calls.

Issue Context

The projections subsystem’s standard projection list uses underscore names (e.g. $stream_by_category). Docs should match this exact spelling because projection management endpoints are name-based.

Fix Focus Areas

  • docs/server/features/projections/README.md[73-82]
  • docs/server/features/projections/settings.md[40-47]
  • docs/server/features/indexes/secondary.md[11-11]
  • docs/server/features/indexes/secondary.md[26-26]
  • src/KurrentDB.Projections.Management/ProjectionsSubsystem.cs[91-97]

What to change

  • Replace $stream-by-category with $stream_by_category.
  • Replace $by-category with $by_category.
  • Replace $by-event-type with $by_event_type.
  • Ensure any other occurrences of these hyphenated names in docs are updated to underscore variants for consistency.


                     PR 5587 (2026-04-20)                    
[performance] Decodes dropped event payloads
Decodes dropped event payloads The new event-type filter calls ConvertToProjectionEvent before checking whether the event type is handled, which decodes Data/Metadata to UTF8 strings even for events that are immediately discarded. This adds avoidable CPU/allocation overhead on streams with many filtered-out event types.

Issue description

The event-type filter allocates a ProjectionResolvedEvent (including UTF8-decoding data/metadata) before checking whether the event will be handled.

Issue Context

coreEvent.Event.EventType is already available and used in the same method for system-event checks, so the handled-type check can be done before calling ConvertToProjectionEvent.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[155-177]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[265-280]

Implementation notes

  • Check handledEventTypes.Contains(coreEvent.Event.EventType) before calling ConvertToProjectionEvent(coreEvent).
  • Only construct ProjectionResolvedEvent (and decode payload) when the event will be dispatched (or when it must be inspected for delete handling).

[correctness] Silent fallback for `Events`
Silent fallback for `Events` When `AllEvents` is `false`, `_config.SourceDefinition.Events ?? []` silently treats a missing `Events` list as empty, which can cause the projection to drop all events without surfacing a configuration bug. Required dependencies/config should fail fast (e.g., `?? throw`) instead of using silent fallbacks.

Issue description

RunReadLoop silently falls back to an empty event-type list via _config.SourceDefinition.Events ?? [] when AllEvents == false, which can drop all events without reporting a configuration error.

Issue Context

When a projection declares specific event types (AllEvents == false), Events should be treated as required configuration. Per compliance, missing required config should fail fast.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[148-151]

[correctness] Checkpoint stalls on skipped
Checkpoint stalls on skipped In ProjectionEngineV2.RunReadLoop, when an event type is not declared the code breaks out of the EventReceived case before updating lastLogPosition/bytesProcessed, so checkpoints may not advance past skipped events (especially if the tail contains only skipped types). This can cause repeated re-reading of skipped events after restart and prevent persisting EOF progress for those streams/categories.

Issue description

ProjectionEngineV2.RunReadLoop currently breaks on filtered-out event types before updating lastLogPosition/bytesProcessed. This prevents checkpoint markers from advancing past skipped events (regression vs V1 semantics), especially when the end of the read contains only skipped types.

Issue Context

  • Skipped events are intentionally not dispatched to partitions, but the engine still needs to advance its notion of the last seen TF position so it can checkpoint beyond them.
  • Today lastLogPosition is only updated after DispatchEvent, and the final checkpoint injection only happens when eventsProcessed > 0.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[155-217]

Implementation notes

  • Ensure lastLogPosition advances to logPosition for every EventReceived (even when the event is filtered out).
  • Ensure skipped events contribute to the “unhandled bytes” accounting (so CheckpointUnhandledBytesThreshold can trigger progress).
  • Update the final checkpoint condition to also inject when only skipped/unhandled events were seen since the last checkpoint (e.g., if (eventsProcessed > 0 || bytesProcessed > 0) or if (lastLogPosition != checkpoint)).


                     PR 5585 (2026-04-16)                    
[correctness] CRT write can corrupt PFX
CRT write can corrupt PFX Program.cs always writes a public PEM to `Path.ChangeExtension(devCertPath, ".crt")`; if the user passes a dev cert path that already ends with `.crt`, the `.crt` output path equals the input path and the code overwrites/corrupts the PFX file. This can break subsequent startups because the file is no longer a valid PFX.

Issue description

Writing the public certificate uses Path.ChangeExtension(devCertPath, ".crt") and can produce the same path as the input, overwriting the PFX file with PEM content.

Issue Context

This happens when devCertPath already ends with .crt (or other edge cases where ChangeExtension returns an equal path). The code currently does not validate the extension or check for equality.

Fix Focus Areas

  • src/KurrentDB/Program.cs[176-186]
  • src/KurrentDB.Core/Configuration/ClusterVNodeOptions.cs[113-118]

Suggested fix approach

  • Validate DevCertPath extension early (allow .pfx, .p12, or empty extension; reject .crt/.pem/.cer with a clear fatal message).
  • Additionally guard if (StringComparer.OrdinalIgnoreCase.Equals(crtPath, devCertPath)) and choose a safe alternate output name (e.g., ${devCertPath}.public.crt) or abort with an error.

[reliability] Dev cert still missing
Dev cert still missing When the Unix certificate store is inaccessible and --dev-cert-path is not provided, dev mode can still exit with "Could not create dev certificate" because the ephemeral fallback certificate is never reloadable from the X509 store. The new Unix ephemeral fallback only helps if a file export path is provided (so Program.cs can reload from disk).

Issue description

On Unix, when the user cert store cannot be used (UnauthorizedAccessException/DirectoryNotFoundException), the code falls back to an ephemeral certificate that is not stored anywhere. If --dev-cert-path is not set, Program.cs still reloads from the X509 store and can exit fatally because the store is empty.

Issue Context

  • UnixCertificateManager.SaveCertificateCore creates an EphemeralKeySet cert in the fallback path and does not call store.Add.
  • CertificateManager.EnsureDevelopmentCertificate does not return the generated certificate and disposes it at the end.
  • Program.cs only uses a file reload path when DevCertPath is provided.

Fix Focus Areas

  • src/KurrentDB/Program.cs[145-173]
  • src/KurrentDB.Common/DevCertificates/UnixCertificateManager.cs[23-45]
  • src/KurrentDB.Common/DevCertificates/CertificateManager.cs[248-310]

Suggested fix approach

  • Introduce an API that returns the ensured certificate to the caller (e.g., an overload like EnsureDevelopmentCertificate(..., out X509Certificate2 certificate)), and ensure it does not dispose the returned certificate.
  • Update Program.cs to use the returned cert directly when store persistence fails (or when the store reload returns empty).
  • Keep the existing store/file behaviors for the cases where persistence is successful.

[reliability] Loaded cert not fully validated
Loaded cert not fully validated When loading an existing certificate from --dev-cert-path, Program.cs only checks the custom OID and NotAfter and can accept a certificate that is not yet valid or lacks a private key. This can lead to TLS configuration failures later because the server certificate context requires a certificate usable for server authentication (including private key material).

Issue description

The --dev-cert-path load path accepts certificates that may be unusable at runtime (e.g., not-yet-valid or missing a private key).

Issue Context

Current checks are:

  • IsHttpsDevelopmentCertificate(loaded)
  • loaded.NotAfter > UtcNow

Missing checks include:

  • loaded.NotBefore <= UtcNow
  • loaded.HasPrivateKey (and possibly GetRSAPrivateKey() != null)

Fix Focus Areas

  • src/KurrentDB/Program.cs[128-142]

Suggested fix approach

  • Extend the acceptance condition to include loaded.NotBefore <= now && now <= loaded.NotAfter && loaded.HasPrivateKey.
  • If invalid, dispose and regenerate (current behavior) with a warning that includes the reason (expired, not-yet-valid, missing private key).


                     PR 5584 (2026-04-16)                    
[correctness] Computed flags appear configurable
Computed flags appear configurable TlsDisabled/AuthDisabled are read-only computed properties but will be treated as normal options by the reflection-based help/defaults/unknown-option logic, so users may see and pass flags like --tls-disabled that appear valid yet have no effect. This also pollutes --help/--what-if output with internal-only options.

Issue description

TlsDisabled and AuthDisabled are computed/read-only, but the options/help/unknown-option machinery reflects over all public properties and will expose these as if they were user-configurable (e.g., --tls-disabled). Because they are not bindable, passing them has no effect, creating silent misconfiguration and confusing --help/--what-if output.

Issue Context

The options system builds metadata/defaults/help text/known-keys via GetProperties() without checking for a setter/init accessor.

Fix Focus Areas

  • src/KurrentDB.Core/Configuration/ClusterVNodeOptions.cs[161-170]
  • src/KurrentDB.Core/Configuration/Sources/KurrentConfigurationKeys.cs[31-45]
  • src/KurrentDB.Core/Configuration/ClusterVNodeOptions.Framework.cs[214-215]
  • src/KurrentDB.Core/Configuration/SectionMetadata.cs[23-42]

Suggested fix

Filter option properties to those that are actually configurable (e.g., property.SetMethod != null / property.CanWrite) when:

  1. building OptionsKeys/AllKnownKeys
  2. generating SectionMetadata/OptionMetadata
  3. generating default values and help text

This keeps computed properties usable internally while preventing them from appearing as CLI/config options.


[correctness] Cleartext auth not proven
Cleartext auth not proven The disable-tls integration test sends Basic credentials to /info, but /info is allowed anonymously, so the test can pass even if credentials are ignored/blocked over cleartext transport. This misses the PR’s key guarantee: authenticated access should work over non-TLS.

Issue description

authenticated_request_succeeds currently targets /info, which is permitted anonymously, so the test does not prove that credentials are accepted over cleartext when disableTls: true.

Issue Context

The PR’s main behavioral requirement is: auth is still enforced, and valid credentials succeed, even when TLS is disabled.

Fix Focus Areas

  • src/KurrentDB.Core.Tests/Integration/disable_tls_with_auth.cs[37-55]

Suggested fix

Change the authenticated test to call an endpoint that requires authentication and deterministically returns 401 without a user and 200 with valid Basic auth, e.g. GET /users/$current.

  • Keep the existing unauthenticated assertion, but consider aligning it to the same auth-required endpoint for clarity.
  • Assert the authenticated call returns 200 OK (and optionally validate response content indicates the admin user).


                     PR 5567 (2026-03-25)                    
[reliability] Forgets allow DoS range
Forgets allow DoS range `query_events` processes `forgets` with an inclusive loop from `EventNumber` to `To` and adds every generated key into `session.Forgotten`, so a client can submit a huge range and force unbounded CPU and memory growth.

Issue description

QueryEventsTool iterates from EventNumber to To and unconditionally adds each computed key into session.Forgotten. A large range can cause severe CPU use and memory exhaustion.

Issue Context

  • The loop is inclusive and runs once per number in the range.
  • Each iteration constructs a new string key and inserts it into a HashSet, growing memory linearly with the range size.

Fix Focus Areas

  • src/KurrentDB.Kontext/QueryEventsTool.cs[74-96]

Suggested fix approach

  • Validate ranges:
    • Require To >= EventNumber.
    • Enforce a maximum range length (e.g., 1k/10k) and reject larger requests.
  • Alternatively, avoid generating keys for the whole numeric range:
    • Iterate over existing session.Events.Keys (and/or session.Forgotten) and remove/add only keys that fall within the requested range.
  • Consider adding cancellation checks inside loops (ct.ThrowIfCancellationRequested()), and plumb a CancellationToken into QueryEvents if possible.

[reliability] Checkpoint skips failed batches
Checkpoint skips failed batches `SubscriptionServiceBase.FlushBatch` clears the batch and flushes the checkpoint even if `IndexBatch` throws, so events that failed indexing can be permanently skipped on restart and never become searchable.

Issue description

SubscriptionServiceBase.FlushBatch persists the checkpoint even when IndexBatch fails. This can permanently skip events from indexing, causing missing results in Kontext search.

Issue Context

  • FlushBatch catches exceptions from IndexBatch, logs, then continues to batch.Clear(), SaveIndexes(), and FlushCheckpoint().
  • FlushCheckpoint() writes the last seen commit/prepare positions to disk and resets _eventsSinceCheckpoint, so the subscription will resume after the failed batch.

Fix Focus Areas

  • src/KurrentDB.Kontext/SubscriptionServiceBase.cs[134-171]
  • src/KurrentDB.Kontext/CheckpointIO.cs[5-18]

Suggested fix approach

  • Only clear the batch and flush the checkpoint after a successful IndexBatch (and ideally after SaveIndexes).
  • If indexing fails, do not advance the checkpoint; either:
    • rethrow to force the subscription loop to restart from the last persisted checkpoint, or
    • keep the batch intact and retry indexing with backoff.
  • Consider making checkpoint writes more robust (e.g., atomic write-then-rename) to avoid corruption on crash.

[security] Session hijack leaks data
Session hijack leaks data `SessionManager` issues short (8-hex) session IDs and stores sessions globally without binding them to a user/MCP connection, so another client can guess/steal a session ID and use `view_events` to read that session’s working set.

Issue description

MCP search sessions are globally accessible by a short ID and are not bound to a user or MCP connection. This enables session hijack and cross-user data leakage via view_events / query_events.

Issue Context

  • Session.Id is Guid.NewGuid().ToString("N")[..8] (32-bit space).
  • SessionManager.Get(id) has no association to ClaimsPrincipal or MCP session.
  • ViewEvents prints session.Events without per-request authorization checks.

Fix Focus Areas

  • src/KurrentDB.Kontext/SessionManager.cs[5-72]
  • src/KurrentDB.Kontext/QueryEventsTool.cs[54-237]

Suggested fix approach

  • Use a strong, non-truncated ID (full GUID or cryptographically random token).
  • Store and validate session ownership:
    • bind to MCP server session (server.SessionId) or
    • bind to ClaimsPrincipal (e.g., NameIdentifier) and reject access from other users.
  • Ensure view_events (and other session operations) re-check authorization/redact based on the current caller (or store only references and re-hydrate per call).
  • Consider thread-safety: if sessions can be accessed concurrently, protect Session.Events/Session.Forgotten with a lock or use concurrent collections.

[maintainability] Mutable `SearchResult` collections exposed
Mutable `SearchResult` collections exposed `SearchService.SearchResult` exposes mutable `List`/`Dictionary` properties, allowing external mutation of results and weakening encapsulation. The encapsulation rule requires returning read-only collection interfaces where callers should not modify the data.

Issue description

SearchService.SearchResult publicly exposes mutable collections (List/Dictionary), enabling callers to mutate search results.

Issue Context

Search results should generally be immutable from the caller's perspective; expose IReadOnlyList/IReadOnlyDictionary (or wrap with ReadOnlyCollection/ReadOnlyDictionary) and keep mutable implementations internal.

Fix Focus Areas

  • src/KurrentDB.Kontext/SearchService.cs[211-220]

[observability] Per-batch `LogInformation` in `FlushBatch`
Per-batch `LogInformation` in `FlushBatch` `SubscriptionServiceBase.FlushBatch` logs a high-frequency per-batch message at `Information` level, which can flood logs during normal indexing. Policy requires per-batch/per-event logs to be below Information (e.g., Debug/Verbose).

Issue description

SubscriptionServiceBase.FlushBatch logs per-batch indexing progress using Logger.LogInformation(...), which violates the log-level policy for high-frequency messages.

Issue Context

Batch flushes can occur frequently (size-based and timer-based), so Information logs will be noisy in production.

Fix Focus Areas

  • src/KurrentDB.Kontext/SubscriptionServiceBase.cs[153-153]

[security] Imports bypass stream authz
Imports bypass stream authz The bulk import endpoint writes events via `ISystemClient` using `SystemAccounts.System` and does not consult `IAuthorizationProvider`, so stream ACLs can be bypassed by any caller who can reach the endpoint with a valid MCP session ID.

Issue description

Kontext write paths (bulk import + retain_facts) write events as SystemAccounts.System and do not verify the caller has write permissions for the target streams.

Issue Context

  • Bulk import handler calls ISystemClient.WriteBatchAsync(valid) without any IAuthorizationProvider checks.
  • WriteBatchAsync uses principal: SystemAccounts.System.
  • retain_facts writes to $kontext-memory using the same system principal.

Fix Focus Areas

  • src/KurrentDB.Plugins.Kontext/EndpointRouteBuilderExtensions.cs[18-68]
  • src/KurrentDB.Kontext/SystemClientExtensions.cs[140-167]
  • src/KurrentDB.Kontext/MemoryTool.cs[44-87]

Suggested fix approach

  • In the HTTP import handler:
    • resolve IAuthorizationProvider and validate CanWriteStreamAsync(context.User, stream) for each distinct target stream.
    • reject unauthorized streams with 403.
  • Avoid writing as SystemAccounts.System when the operation should be scoped to the caller.
    • If you need a KurrentDB principal type, add an adapter from HttpContext.User to the expected principal, or use an API that accepts the current user.
  • Apply the same principle to retain_facts (and any other write tools): require appropriate permissions and use the caller’s principal.
  • If the design intentionally requires privileged writes, explicitly restrict the endpoint/tool to an admin role and document it.

[maintainability] Mutable `Session.Events` exposed
Mutable `Session.Events` exposed `Session` publicly exposes mutable collections (`Dictionary`/`HashSet`), allowing external mutation and making session invariants hard to enforce. The encapsulation rule requires exposing read-only collection types to consumers that should not mutate state.

Issue description

Session.Events and Session.Forgotten are exposed as mutable collections, allowing any consumer to mutate them.

Issue Context

The session working set and forgotten-set should be mutated only through controlled methods (e.g., on Session or SessionManager) while consumers should receive IReadOnlyDictionary/IReadOnlySet views.

Fix Focus Areas

  • src/KurrentDB.Kontext/SessionManager.cs[12-16]


                     PR 5562 (2026-03-23)                    
[reliability] V2 engine stop race
V2 engine stop race CoreProjectionV2 cancels and disposes the engine CTS without awaiting the ProjectionEngineV2 task, then clears `_engine`. A rapid Stop/Start can run multiple engines concurrently, causing overlapping checkpoint/emitted writes and inconsistent projection state.

Issue description

StopEngine() cancels the engine but does not wait for it to stop, so restarting can overlap engine instances.

Issue Context

ProjectionEngineV2 performs async draining/checkpointing and waits for partition tasks in its finally; callers must not start a second engine instance until the first has completed.

Fix Focus Areas

  • Store and await the engine run task during Stop/Kill/Suspend (ideally off the projection worker thread if deadlock risk exists).
  • Gate Start so it cannot run until prior engine completion is observed.

Fix Focus Areas (code locations)

  • src/KurrentDB.Projections.V2/Services/Processing/V2/CoreProjectionV2.cs[215-236]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/CoreProjectionV2.cs[278-285]


                     PR 5554 (2026-03-16)                    
[reliability] Stale OTLP options value
Stale OTLP options value `OpenTelemetryLogger.OtlpOptions` is not cleared when `OtlpLogsEnabled()` is false, so if the method is called once with logs enabled and later called with logs disabled, `OtlpOptions` can remain non-null and stale. This violates the property's own documented contract ("null if log export is disabled").

Issue description

OpenTelemetryLogger.OtlpOptions is a static property documented as "null if log export is disabled", but the implementation never clears it on the early-return path when logs export is disabled.

Issue Context

Even if runtime reconfiguration is uncommon, this can cause confusing behavior in the same process (e.g., tests, integration harnesses, or repeated logger initialization) where a stale non-null value remains.

Fix Focus Areas

  • Set OtlpOptions = null; before returning when !configuration.OtlpLogsEnabled().

  • Optionally: set OtlpOptions = null; at the start of the method to ensure a clean slate, then set it when enabled.

  • src/KurrentDB.Logging/OpenTelemetryLogger.cs[21-30]


[security] Leaky public OTLP options
Leaky public OTLP options `OpenTelemetryLogger` exposes the resolved `OtlpExporterOptions` (including `Headers`) via a public static property, making auth headers easily accessible to any in-process code and increasing the chance they get logged/serialized accidentally. This is a new security exposure introduced by the PR.

Issue description

OpenTelemetryLogger.OtlpOptions is a public static reference to the resolved OtlpExporterOptions, which includes Headers (often API keys/tokens). This creates a new in-process secret exposure surface and makes accidental logging/serialization more likely.

Issue Context

The property appears to be introduced primarily to support tests asserting the resolved effective configuration.

Fix Focus Areas

  • Make the property internal (or remove it) and expose only what tests need (e.g., endpoint + protocol, or a sanitized copy with headers removed/masked).

  • If tests need access from another assembly, prefer InternalsVisibleTo for the test project.

  • Avoid returning a mutable reference that other code can modify.

  • src/KurrentDB.Logging/OpenTelemetryLogger.cs[15-57]

  • src/KurrentDB.Common.Tests/OpenTelemetry/OpenTelemetryLoggerTests.cs[1-87]



                     PR 5544 (2026-03-07)                    
[correctness] Engine v2 always faults
Engine v2 always faults EngineVersion=2 is externally selectable, but the selected V2ProjectionProcessingStrategy throws in CreateProcessingPhases, so projection creation will fault immediately in the existing V1 CoreProjection pipeline.

Issue description

EngineVersion=2 can be requested through the management API, but the code path still constructs a v1 CoreProjection which always calls CreateProcessingPhases(). The v2 strategy currently throws, so projections will fault immediately.

Issue Context

This is user-triggerable via gRPC create options, and will lead to immediate projection failures when engine v2 is selected.

Fix Focus Areas

  • src/KurrentDB.Projections.Management/Services/Grpc/ProjectionManagement.Create.cs[60-71]
  • src/KurrentDB.Projections.V1/Services/Processing/Strategies/ProcessingStrategySelector.cs[22-35]
  • src/KurrentDB.Projections.V1/Services/Processing/V2/V2ProjectionProcessingStrategy.cs[50-63]
  • src/KurrentDB.Projections.V1/Services/Processing/CoreProjection.cs[121-132]

[correctness] StateHandler shared concurrently
StateHandler shared concurrently ProjectionEngineV2 passes the same IProjectionStateHandler instance into multiple PartitionProcessor tasks, causing unsafe concurrent access and state corruption (especially for JintProjectionStateHandler).

Issue description

ProjectionEngineV2 starts N partition processors concurrently but passes the same IProjectionStateHandler instance to all of them. Projection state handlers maintain internal mutable state, so concurrent calls will corrupt state and/or crash.

Issue Context

This is especially problematic for JS projections (JintProjectionStateHandler) which encapsulate a single Jint Engine and mutable _state.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[75-89]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2Config.cs[9-18]
  • src/KurrentDB.Projections.JavaScript/Services/Interpreted/JintProjectionStateHandler.cs[35-70]

[correctness] BiState shared state races
BiState shared state races BiState shared state is tracked per PartitionProcessor, so with multiple partitions the shared state will be updated concurrently from stale snapshots and written multiple times, losing updates and producing incorrect shared aggregates.

Issue description

BiState shared state is handled locally per partition processor, but shared state is conceptually global. With multiple partitions this produces stale reads and lost updates.

Issue Context

Each partition writes to the same shared result stream name ($projections-{name}--result) without coordination.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionProcessor.cs[94-101]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionProcessor.cs[127-131]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[75-89]

[correctness] Custom partition routing mismatch
Custom partition routing mismatch Deferred partition-key mode routes events by EventStreamId but later computes the actual custom partition key; custom partitions can group across streams, so the same computed partition can be processed on different processors, splitting/corrupting per-partition state.

Issue description

Deferred partitioning routes by stream id but the real partition key is computed later. Custom partitioning can group across streams (e.g., event.body.region), so per-partition state can be split across processors.

Issue Context

Each processor has its own state cache; there is no cross-processor coordination for a computed partition key.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[52-67]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionDispatcher.cs[86-92]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionProcessor.cs[70-92]
  • src/KurrentDB.Projections.Core.Tests/Services/Jint/when_partitioning_by_custom_rule.cs[15-33]

[correctness] Checkpoint sequence reset drops
Checkpoint sequence reset drops CheckpointCoordinator discards partially collected buffers when it sees a new markerSequence. Since the read loop can inject marker N+1 before all partitions reported marker N, checkpoint data can be dropped, causing lost state/emits or duplication on restart.

Issue description

Checkpoint collection is single-slot and resets on any new marker sequence. If partitions report out of step, earlier checkpoint buffers are discarded.

Issue Context

The read loop injects markers based on thresholds, independent of whether the prior marker has fully completed across all partitions.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/CheckpointCoordinator.cs[49-65]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2.cs[138-147]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionDispatcher.cs[95-105]

[correctness] Checkpoint write can hang
Checkpoint write can hang CheckpointCoordinator awaits a TaskCompletionSource without cancellation/timeout; if the write response never arrives, checkpointing stalls indefinitely and the semaphore remains held, blocking progress and potentially shutdown.

Issue description

Checkpoint writes can block forever waiting for a reply message; this stalls all subsequent checkpoints and can hang shutdown.

Issue Context

This is a reliability issue under partial failures (dropped messages, node failover, etc.).

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/CheckpointCoordinator.cs[67-112]

[correctness] Null passed to Load
Null passed to Load PartitionProcessor caches null states and later calls _stateHandler.Load(cachedState) even when cachedState is null. This violates the IProjectionStateHandler contract and can crash handlers that don’t accept null.

Issue description

PartitionProcessor may call Load(null) due to null-state caching, but the handler interface does not promise null is accepted.

Issue Context

Null states appear to be part of projection semantics (tests handle null expected states), so this should be made an explicit contract.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionProcessor.cs[87-92]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionProcessor.cs[119-125]
  • src/KurrentDB.Projections.Shared/Services/IProjectionStateHandler.cs[18-37]

[correctness] EmitEnabled flag unused
EmitEnabled flag unused ProjectionEngineV2Config exposes EmitEnabled, but emitted events are buffered and written unconditionally; this can allow side-effecting emits even when emit is intended to be disabled by configuration.

Issue description

EmitEnabled is present in the V2 engine config but not enforced, while the engine always writes emitted events.

Issue Context

The management layer already treats emit enablement as a configuration flag, so V2 should provide equivalent enforcement to avoid unexpected side effects.

Fix Focus Areas

  • src/KurrentDB.Projections.V2/Services/Processing/V2/ProjectionEngineV2Config.cs[9-18]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/PartitionProcessor.cs[110-135]
  • src/KurrentDB.Projections.V2/Services/Processing/V2/CheckpointCoordinator.cs[139-150]
  • src/KurrentDB.Projections.Management/Services/Management/ManagedProjection.cs[953-983]


                     PR 5459 (2026-01-14)                    
[possible issue] Prevent crashes from unhandled exceptions

✅ Prevent crashes from unhandled exceptions

Add a try-catch block inside the async void method AuthorizeManyAsync to prevent unhandled exceptions from await accessCheck from crashing the application.

src/KurrentDB.Core/Services/AuthorizationGateway.cs [422-434]

 async void AuthorizeManyAsync<TRequest>(
 	ValueTask<bool> accessCheck,
 	ClaimsPrincipal user,
 	ReadOnlyMemory<Operation> operations,
 	IEnvelope replyTo,
 	IPublisher destination,
 	TRequest request,
 	Func<TRequest, Message> createAccessDenied) where TRequest : Message {
-	if (await accessCheck)
-		AuthorizeMany(user, operations, replyTo, destination, request, createAccessDenied);
-	else
+	try {
+		if (await accessCheck)
+			AuthorizeMany(user, operations, replyTo, destination, request, createAccessDenied);
+		else
+			replyTo.ReplyWith(createAccessDenied(request));
+	} catch (Exception ex) {
+		// It's important to log the exception. Assuming a logger is available.
+		// Log.Error(ex, "Error during asynchronous authorization for multi-stream write.");
 		replyTo.ReplyWith(createAccessDenied(request));
+	}
 }

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that an unhandled exception in an async void method can crash the application and proposes a try-catch block to handle potential exceptions from await accessCheck gracefully.



                     PR 5452 (2026-01-13)                    
[possible issue] Pass correct affinity parameter

✅ Pass correct affinity parameter

Pass message.Affinity instead of the message object to Strategy.GetSynchronizationGroup to ensure correct affinity-based message grouping.

src/KurrentDB.Core/Bus/ThreadPoolMessageScheduler.cs [131]

-stateMachine.Schedule(message, Strategy.GetSynchronizationGroup(message));
+stateMachine.Schedule(message, Strategy.GetSynchronizationGroup(message.Affinity));

Suggestion importance[1-10]: 8

__

Why: This is a critical bug fix. The current implementation passes the entire message object to GetSynchronizationGroup, which uses object reference equality, defeating the purpose of affinity-based grouping and causing incorrect synchronization behavior.


[possible issue] Prevent potential unregistration error

✅ Prevent potential unregistration error

Conditionally unregister from the Monitor in RequestStop only if a _queueLengthListener exists to prevent a potential exception.

src/KurrentDB.Core/Bus/ThreadPoolMessageScheduler.cs [84-91]

 	public void RequestStop() {
 		if (Interlocked.Exchange(ref _lifetimeSource, null) is { } cts) {
 			cts.Cancel();
-			Monitor.Unregister(this);
-			_queueLengthListener?.Dispose();
-			_queueLengthObserver = null;
+			if (_queueLengthListener is not null) {
+				Monitor.Unregister(this);
+				_queueLengthListener.Dispose();
+				_queueLengthObserver = null;
+			}
 		}
 	}

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential ArgumentException when unregistering from the Monitor if the queue was never registered, which happens with certain strategies.


[high-level] Consider simplifying the custom object pooling

✅ Consider simplifying the custom object pooling

The custom lock-free object pooling for AsyncStateMachine adds complexity. Consider reverting to the simpler, previous implementation that used System.Collections.Concurrent.ConcurrentBag to reduce maintenance overhead and risk.

Examples:

src/KurrentDB.Core/Bus/ThreadPoolMessageScheduler.Pooling.cs [9-32]

	private volatile AsyncStateMachine _firstNode;

	private void ReturnToPool(AsyncStateMachine node) {
		AsyncStateMachine current;
		do {
			current = _firstNode;
			node.NextInPool = current;
		} while (Interlocked.CompareExchange(ref _firstNode, node, current) != current);
	}


 ... (clipped 14 lines)

src/KurrentDB.Core/Bus/ThreadPoolMessageScheduler.cs [121-123]

		var stateMachine = messageCount > MaxPoolSize
			? new(this)
			: RentFromPool();

Solution Walkthrough:

Before:

// src/KurrentDB.Core/Bus/ThreadPoolMessageScheduler.cs
public partial class ThreadPoolMessageScheduler : IQueuedHandler {
    private readonly ConcurrentBag _pool;
    // ...
    public void Publish(Message message) {
        // ...
        AsyncStateMachine stateMachine;
        if (!_pool.TryTake(out stateMachine)) {
            stateMachine = new PoolingAsyncStateMachine(this);
        }
        // ...
    }
}

// src/KurrentDB.Core/Bus/ThreadPoolMessageScheduler.StateMachine.cs
private class AsyncStateMachine {
    // ...
    protected void ReturnToPool() => _scheduler._pool.Add(this);
}

After:

// src/KurrentDB.Core/Bus/ThreadPoolMessageScheduler.cs
public partial class ThreadPoolMessageScheduler : IQueuedHandler {
    // ...
    public void Publish(Message message) {
        // ...
        var stateMachine = messageCount > MaxPoolSize
            ? new(this)
            : RentFromPool();
        // ...
    }
}

// src/KurrentDB.Core/Bus/ThreadPoolMessageScheduler.Pooling.cs
partial class ThreadPoolMessageScheduler {
    private volatile AsyncStateMachine _firstNode;

    private void ReturnToPool(AsyncStateMachine node) {
        // lock-free push to linked list
        do { ... } while (Interlocked.CompareExchange(...) != ...);
    }

    private AsyncStateMachine RentFromPool() {
        // lock-free pop from linked list
        do { ... } while (Interlocked.CompareExchange(...) != ...);
        return current;
    }
}

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies the replacement of ConcurrentBag with a custom lock-free pooling implementation, raising a valid concern about increased complexity and risk in a critical component for a performance gain that should be justified.



                     PR 5445 (2026-01-12)                    
[general] Improve timeout handling in tests

✅ Improve timeout handling in tests

In ProcessConnectorEvents, replace DateTime.UtcNow with a Stopwatch for more reliable timeout measurement and throw a TimeoutException if the connector is not found, instead of returning silently.

src/Connectors/KurrentDB.Connectors.Tests/Planes/Management/ManagementServerFixture.cs [41-60]

 public async Task ProcessConnectorEvents(string connectorId, CancellationToken cancellationToken = default) {
 	// Wait for the background projection to process events for this connector.
 	// We poll until the connector appears in the snapshot to avoid race conditions
 	// with the background ConnectorsStateProjection service.
 	var timeout = TimeSpan.FromSeconds(5);
-	var start   = DateTime.UtcNow;
+	var stopwatch = System.Diagnostics.Stopwatch.StartNew();
 
-	while (DateTime.UtcNow - start < timeout) {
+	while (stopwatch.Elapsed < timeout) {
 		cancellationToken.ThrowIfCancellationRequested();
 
 		var (snapshot, _, _) = await AssemblyFixture.SnapshotProjectionsStore.LoadSnapshot<ConnectorsSnapshot>(
 			ConnectorQueryConventions.Streams.ConnectorsStateProjectionStream
 		);
 
 		if (snapshot.Connectors.Any(c => c.ConnectorId == connectorId))
 			return;
 
 		await Task.Delay(50, cancellationToken);
 	}
+	
+	throw new TimeoutException($"Connector with ID '{connectorId}' did not appear in the snapshot within the {timeout.TotalSeconds}s timeout.");
 }

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out two significant issues in the test helper method: using DateTime.UtcNow for timeouts and silent failure. The proposed changes to use Stopwatch and throw a TimeoutException substantially improve test reliability and debuggability.



                     PR 5444 (2026-01-12)                    
[possible issue] Avoid converting UTC time to local

✅ Avoid converting UTC time to local

Avoid converting the created timestamp to local time. Instead, format the UTC DateTime directly using the ISO 8601 round-trip format specifier (o) to ensure timezone consistency.

src/KurrentDB.Core/DuckDB/InlineFunctions.cs [47]

-			$"{{ \"data\": {dataString}, \"metadata\": {metaString}, \"stream_id\": \"{stream}\", \"created\": \"{created.ToLocalTime():yyyy-MM-dd'T'HH:mm:ssK}\", \"event_type\": \"{eventType}\" }}";
+			$"{{ \"data\": {dataString}, \"metadata\": {metaString}, \"stream_id\": \"{stream}\", \"created\": \"{created:o}\", \"event_type\": \"{eventType}\" }}";

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that converting UTC time to local server time is not a best practice and can lead to timezone-related issues. Proposing to use the round-trip format specifier (o) on the original UTC DateTime is a robust solution that improves data consistency.


[possible issue] Cast JSON timestamp to TIMESTAMP

✅ Cast JSON timestamp to TIMESTAMP

In the AllCteTemplate query, cast the event->>'created' string value to a TIMESTAMP type to ensure correct sorting and filtering.

src/KurrentDB/Components/Query/QueryService.cs [97]

-event->>'created' as created_at
+CAST(event->>'created' AS TIMESTAMP) as created_at

Suggestion importance[1-10]: 8

__

Why: The PR changes the created_at column to be a string, which would break any sorting or filtering operations that expect a timestamp. This suggestion correctly points out the need to cast this string back to a TIMESTAMP type to maintain query functionality.



                     PR 5437 (2026-01-06)                    
[high-level] Consider a more generic multi-append API

✅ Consider a more generic multi-append API

The new multi-stream append feature uses a low-level, index-based API. It is suggested to create a more abstract, higher-level API to improve safety and ease of use for future implementations.

Examples:

src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs [45-52]

	public static async Task WriteEvents(
		this IPublisher publisher,
		LowAllocReadOnlyMemory streams,
		LowAllocReadOnlyMemory expectedRevisions,
		LowAllocReadOnlyMemory events,
		LowAllocReadOnlyMemory eventStreamIndexes,
		CancellationToken cancellationToken = default
	) {

src/KurrentDB.SecondaryIndexing/Indexes/User/Management/UserIndexEventStore.cs [46-102]

		LowAllocReadOnlyMemory streams = duplicate
			? [stream, UserIndexConstants.ManagementAllStream]
			: new(stream);

		LowAllocReadOnlyMemory expectedVersions = duplicate
			? [expectedVersion.Value, ExpectedVersion.Any]
			: new(expectedVersion.Value);

		var totalEventCount = duplicate ? events.Count * 2 : events.Count;
		Event[] processedEvents = new Event[totalEventCount];

 ... (clipped 47 lines)

Solution Walkthrough:

Before:

// In PublisherWriteExtensions.cs
public static async Task WriteEvents(
    this IPublisher publisher,
    LowAllocReadOnlyMemory streams,
    LowAllocReadOnlyMemory expectedRevisions,
    LowAllocReadOnlyMemory events,
    LowAllocReadOnlyMemory eventStreamIndexes,
    CancellationToken cancellationToken = default
) { ... }

// Usage in UserIndexEventStore.cs
// ... build parallel arrays for streams, expectedVersions, events, and eventStreamIndexes
await _client.Writing.WriteEvents(
    streams: streams,
    expectedRevisions: expectedVersions,
    events: processedEvents,
    eventStreamIndexes: eventStreamIndexes,
    cancellationToken);

After:

// Proposed higher-level API
public record MultiStreamWrite(
    string Stream,
    long ExpectedRevision,
    IReadOnlyCollection Events
);

public static async Task<...> WriteEvents(
    this IPublisher publisher,
    IReadOnlyCollection writes,
    CancellationToken cancellationToken = default
) {
    // Internally, this would build the low-level parallel arrays
    // and call the existing implementation.
}

// Simplified usage
var writes = new List { new(stream, expectedVersion, events) };
if (duplicate) {
    writes.Add(new(allStream, ExpectedVersion.Any, events));
}
await _client.Writing.WriteEvents(writes, cancellationToken);

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that the new multi-stream write API is low-level and potentially error-prone, proposing a significant architectural improvement for this new core feature that would enhance usability and maintainability.


[possible issue] Fix bug with empty event collections

✅ Fix bug with empty event collections

Add a guard clause to handle empty event collections to prevent a potential crash, and refactor the event processing loop for improved readability.

src/KurrentDB.SecondaryIndexing/Indexes/User/Management/UserIndexEventStore.cs [37-94]

 	public async Task<AppendEventsResult> AppendEvents(
 		StreamName stream,
 		ExpectedStreamVersion expectedVersion,
 		IReadOnlyCollection<NewStreamEvent> events,
 		CancellationToken cancellationToken) {
+
+		if (events.Count == 0) {
+			var streamRevision = await _inner.GetStreamVersion(stream, cancellationToken);
+			return AppendEventsResult.StreamExists(streamRevision.Value);
+		}
 
 		// decide if we are going to duplicate the events to the user index all Stream
 		var duplicate = stream.ToString().StartsWith(UserIndexConstants.Category);
 
 		LowAllocReadOnlyMemory<string> streams = duplicate
 			? [stream, UserIndexConstants.ManagementAllStream]
 			: new(stream);
 
 		LowAllocReadOnlyMemory<long> expectedVersions = duplicate
 			? [expectedVersion.Value, ExpectedVersion.Any]
 			: new(expectedVersion.Value);
 
 		var totalEventCount = duplicate ? events.Count * 2 : events.Count;
-		Event[] processedEvents = new Event[totalEventCount];
-		int[] eventStreamIndexes = new int[totalEventCount];
+		var processedEvents = new Event[totalEventCount];
+		var eventStreamIndexes = new int[totalEventCount];
 
 		var originalStreamIndex = 0;
 		var allStreamIndex = 1;
 
 		int i = 0;
-		int j = events.Count;
 		foreach (var evt in events) {
 			var message = Message.Builder
 				.Value(evt.Payload!)
 				.Headers(new Headers(evt.Metadata.ToHeaders()))
 				.WithSchemaType(SchemaDataFormat.Json)
 				.Create();
 			var data = await _serialize(evt.Payload, message.Headers);
 			var dataArray = data.ToArray();
 			var schema = SchemaInfo.FromHeaders(message.Headers);
 			var isJson = schema.SchemaDataFormat == SchemaDataFormat.Json;
 
 			// process the events into the original stream
 			processedEvents[i] = new Event(
 				eventId: evt.Id,
 				eventType: schema.SchemaName,
 				isJson: isJson,
 				data: dataArray);
 			eventStreamIndexes[i] = originalStreamIndex;
 
 			// process the events into the management stream if necessary
 			if (duplicate) {
-				processedEvents[j] = new Event(
+				var duplicateIndex = i + events.Count;
+				processedEvents[duplicateIndex] = new Event(
 					eventId: Guid.NewGuid(),
 					eventType: schema.SchemaName,
 					isJson: isJson,
 					data: dataArray);
-				eventStreamIndexes[j] = allStreamIndex;
+				eventStreamIndexes[duplicateIndex] = allStreamIndex;
 			}
 
 			i++;
-			j++;
 		}
 ...

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential bug when handling an empty events collection and proposes a valid refactoring to simplify the loop logic, but the provided improved_code for the bug fix is incorrect as it calls a non-existent method _inner.GetStreamVersion.


[general] Fix typo in error message string

✅ Fix typo in error message string

Fix a typo in the SecondaryIndexingDisabled error message by adding a missing closing parenthesis.

src/KurrentDB.Api.V2/Modules/Indexes/ApiErrors.cs [32-34]

 	public static RpcException SecondaryIndexingDisabled() => RpcExceptions.FromError(
 		error: IndexesError.SecondaryIndexingDisabled,
-		message: "Secondary indexing is disabled (configuration key KurrentDB::SecondaryIndexing::Enabled is false");
+		message: "Secondary indexing is disabled (configuration key KurrentDB::SecondaryIndexing::Enabled is false)");

Suggestion importance[1-10]: 2

__

Why: The suggestion correctly identifies and fixes a typo (a missing parenthesis) in an error message string, which is a minor but valid improvement for message clarity.



                     PR 5432 (2026-01-05)                    
[general] Fix typo in error text

✅ Fix typo in error text

Correct the grammatical error "is has" to "has" in the error message string.

src/KurrentDB.Api.V2/Modules/Streams/ApiErrors.cs [75-76]

-var message = $"Stream '{stream}' is has a different group of messages in this session. " +
+var message = $"Stream '{stream}' has a different group of messages in this session. " +
                 $"Appends for the same stream must currently be grouped together and not interleaved with appends for other streams.";

Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies and fixes a grammatical error ("is has") in the new error message, improving its clarity and professionalism.



                     PR 5430 (2026-01-02)                    
[high-level] Refactor logging dependency injection strategy

Refactor logging dependency injection strategy


Refactor the code to inject specific ILogger instances directly into classes instead of injecting ILoggerFactory. This change simplifies constructors and improves testability.

Examples:

src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexEngine.cs [39-54]

	public UserIndexEngine(
		ISystemClient client,
		IPublisher publisher,
		ISubscriber subscriber,
		ISchemaSerializer serializer,
		SecondaryIndexingPluginOptions options,
		DuckDBConnectionPool db,
		IReadIndex index,
		TFChunkDbConfig chunkDbConfig,
		[FromKeyedServices(SecondaryIndexingConstants.InjectionKey)]

 ... (clipped 6 lines)

src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexEngineSubscription.cs [26-41]

public partial class UserIndexEngineSubscription(
	ISystemClient client,
	IPublisher publisher,
	ISchemaSerializer serializer,
	SecondaryIndexingPluginOptions options,
	DuckDBConnectionPool db,
	IReadIndex readIndex,
	Meter meter,
	Func<(long, DateTime)> getLastAppendedRecord,
	ILoggerFactory logFactory,

 ... (clipped 6 lines)

Solution Walkthrough:

Before:

public class UserIndexEngine(ILoggerFactory loggerFactory) {
    private readonly ILogger _log;
    private readonly UserIndexEngineSubscription _subscription;

    public UserIndexEngine(...) {
        _log = loggerFactory.CreateLogger();
        _subscription = new UserIndexEngineSubscription(..., loggerFactory, ...);
    }
    // ...
}

public class UserIndexEngineSubscription(..., ILoggerFactory logFactory, ...) {
    private readonly ILogger _log = logFactory.CreateLogger();
    // ...
}

After:

// Assuming UserIndexEngineSubscription is registered in DI container
public class UserIndexEngine(ILogger log, UserIndexEngineSubscription subscription) {
    private readonly ILogger _log = log;
    private readonly UserIndexEngineSubscription _subscription = subscription;

    // ...
}

public class UserIndexEngineSubscription(..., ILogger log, ...) {
    private readonly ILogger _log = log;
    // ...
}

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a suboptimal dependency injection pattern where ILoggerFactory is used instead of ILogger, impacting multiple new classes and affecting design quality and testability.


[general] Permit uppercase identifiers

✅ Permit uppercase identifiers

Add RegexOptions.IgnoreCase to the GeneratedRegex attribute to allow uppercase characters in index and column identifiers.

src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexSql.cs [40-41]

-[GeneratedRegex("^[a-z][a-z0-9_-]*$", RegexOptions.Compiled)]
+[GeneratedRegex("^[a-z][a-z0-9_-]*$", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
 private static partial Regex ValidationRegex();

Suggestion importance[1-10]: 4

__

Why: This is a reasonable suggestion to make the identifier validation case-insensitive, which could improve flexibility, but it's a minor enhancement and not a bug fix.



                     PR 5425 (2025-12-22)                    
[possible issue] Handle null selector result correctly

✅ Handle null selector result correctly

Modify the CanHandleEvent method to treat a null or undefined result from a selector expression as a signal to skip indexing the event, similar to the skip value.

src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexProcessor.cs [163-172]

 			var fieldValue = _evaluator.Select(_fieldSelectorExpression);
-			if (fieldValue == JsValue.Null)
-				return true;
+			if (fieldValue.IsNull() || fieldValue.IsUndefined())
+				return _fieldSelectorExpression is null;
 
 			if (_skip.Equals(fieldValue))
 				return false;
 
 			field = (TField)TField.ParseFrom(fieldValue);
 
 			return true;

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a logical flaw where an event with a null selector result is indexed. The proposed change to skip such events makes the indexing behavior more intuitive and robust.



                     PR 5422 (2025-12-18)                    
[high-level] Refactor JavaScript object creation

✅ Refactor JavaScript object creation

Instead of manually building a JavaScript object using a hierarchy of C# classes that inherit from ObjectInstance, define a simple C# record or class that matches the desired JavaScript structure. Then, use Jint's JsValue.FromObject to automatically convert the C# object to a JsValue, simplifying the code and improving maintainability.

Examples:

src/KurrentDB.SecondaryIndexing/Indexes/User/JavaScript/RecordObject.cs [12-77]

internal sealed class RecordObject : JsObject {
	private readonly PositionObject _position;
	private readonly SchemaInfoObject _schemaInfo;

	public RecordObject(Engine engine, JsonParser parser) : base(engine, parser) {
		_position = new PositionObject(engine, parser);
		SetReadOnlyProperty("position", _position);

		_schemaInfo = new SchemaInfoObject(engine, parser);
		SetReadOnlyProperty("schemaInfo", _schemaInfo);

 ... (clipped 56 lines)

src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexProcessor.cs [157-166]

			_jsRecord.MapFrom(resolvedEvent, ++_sequenceId);

			if (_filter is not null) {
				var passesFilter = _filter.Call(_jsRecord).AsBoolean();
				if (!passesFilter)
					return false;
			}

			if (_fieldSelector is not null) {
				var fieldJsValue = _fieldSelector.Call(_jsRecord);

Solution Walkthrough:

Before:

// KurrentDB.SecondaryIndexing/Indexes/User/UserIndexProcessor.cs
class UserIndexProcessor {
    private readonly RecordObject _jsRecord;

    public UserIndexProcessor(...) {
        _jsRecord = new RecordObject(_engine, parser);
    }

    private bool CanHandleEvent(ResolvedEvent resolvedEvent, out TField? field) {
        _jsRecord.MapFrom(resolvedEvent, ++_sequenceId);
        var passesFilter = _filter.Call(_jsRecord).AsBoolean();
        // ...
    }
}

// KurrentDB.SecondaryIndexing/Indexes/User/JavaScript/RecordObject.cs
class RecordObject : JsObject { // Inherits from Jint's ObjectInstance
    public RecordObject(...) {
        _position = new PositionObject(engine, parser);
        SetReadOnlyProperty("position", _position); // Manual property setting
        // ...
    }

    public void MapFrom(ResolvedEvent resolvedEvent, ulong sequenceId) {
        RecordId = $"{resolvedEvent.OriginalEvent.EventId}"; // Manual mapping
        // ...
        _position.MapFrom(resolvedEvent, sequenceId);
    }
}

After:

// Define simple C# records to represent the JS object structure
record JsApiPosition(string streamId, ulong streamRevision, ulong logPosition);
record JsApiSchemaInfo(string subject, string type);
record JsApiRecord(
    string recordId,
    JsApiPosition position,
    JsApiSchemaInfo schemaInfo,
    object value, // Can be lazily evaluated
    object headers
    // ... other properties
);

// KurrentDB.SecondaryIndexing/Indexes/User/UserIndexProcessor.cs
class UserIndexProcessor {
    private bool CanHandleEvent(ResolvedEvent resolvedEvent, out TField? field) {
        // Create a simple C# object
        var recordPoco = MapToPoco(resolvedEvent, ++_sequenceId);

        // Let Jint handle the conversion to a JS object
        var jsRecord = Jint.Native.JsValue.FromObject(_engine, recordPoco);

        var passesFilter = _filter.Call(jsRecord).AsBoolean();
        // ...
    }
}

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a complex, custom implementation for creating JavaScript objects and proposes a much simpler, more idiomatic approach using Jint's built-in object mapping, which would significantly reduce code complexity and improve maintainability.



                     PR 5421 (2025-12-18)                    
[general] Reintroduce lazy initialization for connection pools

✅ Reintroduce lazy initialization for connection pools

Reintroduce lazy initialization for the DuckDBConnectionPool to avoid creating it for every connection, restoring the previous performance-optimized behavior.

src/KurrentDB/KestrelHelpers.cs [34-41]

 	public static void UseDuckDbConnectionPoolPerConnection(this ListenOptions listenOptions) {
 		listenOptions.Use(next => async connectionContext => {
 			var poolFactory = listenOptions.ApplicationServices.GetRequiredService<DuckDBConnectionPoolLifetime>();
-			using var pool = poolFactory.CreatePool();
-			connectionContext.Items[nameof(DuckDBConnectionPool)] = pool;
-			await next(connectionContext);
+			var lazyPool = new Lazy<DuckDBConnectionPool>(poolFactory.CreatePool);
+			connectionContext.Items[nameof(Lazy<DuckDBConnectionPool>)] = lazyPool;
+
+			try {
+				await next(connectionContext);
+			} finally {
+				if (lazyPool.IsValueCreated) {
+					lazyPool.Value.Dispose();
+				}
+			}
 		});
 	}

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a performance regression where a connection pool is created eagerly for every connection and proposes reintroducing lazy initialization, which is a significant performance improvement.


[general] Update extension method for lazy pool

✅ Update extension method for lazy pool

Update the GetDuckDbConnectionPool extension method to retrieve the Lazy and return its Value, aligning with the lazy initialization pattern.

src/KurrentDB.Core/Services/Transport/Http/HttpContextExtensions.cs [16-25]

 	[CanBeNull]
 	public static DuckDBConnectionPool GetDuckDbConnectionPool(this HttpContext httpContext) {
 		var connectionItemsFeature = httpContext.Features.Get<IConnectionItemsFeature>();
 
 		if (connectionItemsFeature is null ||
-			!connectionItemsFeature.Items.TryGetValue(nameof(DuckDBConnectionPool), out var item))
+		    !connectionItemsFeature.Items.TryGetValue(nameof(Lazy<DuckDBConnectionPool>), out var item) ||
+		    item is not Lazy<DuckDBConnectionPool> lazyPool)
 			return null;
 
-		return item as DuckDBConnectionPool;
+		return lazyPool.Value;
 	}

Suggestion importance[1-10]: 8

__

Why: This suggestion is a necessary follow-up to reintroducing lazy initialization for the connection pool, ensuring the retrieval logic correctly handles the Lazy wrapper to access the pool instance.



                     PR 5409 (2025-12-16)                    
[possible issue] Fix incorrect configuration option skipping

✅ Fix incorrect configuration option skipping

Fix a bug by removing an else block that incorrectly skips processing scalar configuration options if their top-level value is missing in a provider.

src/KurrentDB.Core/Configuration/ClusterVNodeOptions.Framework.cs [104-125]

 				if (!provider.TryGet(option.Value.Key, out var value) && !isDefault) {
 					// Handle options that have been configured as arrays (GossipSeed is currently the only one
 					// where this is possible)
 					if (option.Value.OptionSchema.Value<string>("type") is "array") {
 						var parentPath = option.Value.Key;
 						var childValues = new List<string>();
 
 						foreach (var childKey in provider.GetChildKeys([], parentPath)) {
 							var absoluteChildKey = parentPath + ":" + childKey;
 							if (provider.TryGet(absoluteChildKey, out var childValue) && childValue is not null) {
 								childValues.Add(childValue);
 								sourceDisplayName = GetSourceDisplayName(absoluteChildKey, provider);
 							}
 						}
 
-						value = string.Join(", ", childValues);
 						if (childValues.Count is 0)
 							continue; // no child values. skip
+
+						value = string.Join(", ", childValues);
 					} else {
-						continue; // no value and it is an array so don't check for children. skip.
+						continue; // no value for this option in this provider.
 					}
 				}

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where scalar options would be skipped if their top-level key is missing, preventing them from being loaded from other providers.



                     PR 5405 (2025-12-12)                    
[general] Simplify null or whitespace string handling

✅ Simplify null or whitespace string handling

In TryPrettifyJson, simplify the initial check to return string.Empty for any null or whitespace input string, ensuring consistent behavior.

src/KurrentDB/Components/Query/Query.razor [176-186]

 static string TryPrettifyJson(string json) {
-	if (string.IsNullOrWhiteSpace(json)) return json ?? string.Empty;
+	if (string.IsNullOrWhiteSpace(json)) return string.Empty;
 	try {
 		using var doc = JsonDocument.Parse(json);
 		return JsonSerializer.Serialize(doc.RootElement, Indent);
 	}
 	catch (JsonException) {
 		// Not valid JSON; return original (or decide to throw)
 		return json;
 	}
 }

Suggestion importance[1-10]: 4

__

Why: The suggestion improves code clarity and consistency by simplifying the handling of null or whitespace strings, which is a good practice for maintainability.



                     PR 5391 (2025-12-07)                    
[high-level] System memory calculation logic is flawed

✅ System memory calculation logic is flawed

The current method for calculating available system memory is flawed because it incorrectly uses process-specific memory metrics (Process.WorkingSet64) instead of system-wide ones. It is recommended to either make the memory limit a configurable setting or to rely on DuckDB's own default memory management.

Examples:

src/KurrentDB.Core/DuckDB/DuckDBConnectionPoolLifetime.cs [56-63]

		(double Total, double Used) CalculateRam() {
			var process = Process.GetCurrentProcess();
			var processRam = process.WorkingSet64;
			var totalRam = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
			var totalGb = (double)totalRam / 1024 / 1024 / 1024;
			var processGb = (double)processRam / 1024 / 1024 / 1024;
			return (totalGb, processGb);
		}

Solution Walkthrough:

Before:

(double Total, double Used) CalculateRam() {
    var process = Process.GetCurrentProcess();
    var processRam = process.WorkingSet64; // This is only this process's memory
    var totalRam = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
    // ...
    return (totalGb, processGb);
}

// in constructor...
var (total, used) = CalculateRam();
var availableRam = total - used; // Incorrectly calculates "available" RAM
var duckDbRam = (int)Math.Round(availableRam / 2);
_pool = new ConnectionPoolWithFunctions($"Data Source={path};memory_limit={duckDbRam}GB", ...);

After:

// Suggested Alternative: Make the memory limit configurable
// (This requires adding a property to TFChunkDbConfig)

public DuckDBConnectionPoolLifetime(TFChunkDbConfig config, ...) {
    var path = ...;
    var connectionString = $"Data Source={path}";

    // Check for a configured memory limit
    if (config.MemoryLimitGB.HasValue) {
        connectionString += $";memory_limit={config.MemoryLimitGB.Value}GB";
    }
    // If not configured, DuckDB will use its own default (typically 80% of system RAM)

    _pool = new ConnectionPoolWithFunctions(connectionString, ...);
}

Suggestion importance[1-10]: 10

__

Why: The suggestion correctly identifies a critical flaw where process-specific memory (Process.WorkingSet64) is incorrectly used to calculate system-wide available RAM, leading to a dangerously high and incorrect memory limit for DuckDB.



                     PR 5387 (2025-12-01)                    
[possible issue] Fix a potential race condition

✅ Fix a potential race condition

To prevent a potential race condition in the Subscribe method, assign _cts to a local variable at the start and use that variable for the null check and subsequent accesses.

src/KurrentDB.SecondaryIndexing/Subscriptions/SecondaryIndexSubscription.cs [29-50]

 public void Subscribe() {
-	if (_cts == null) {
+	var cts = _cts;
+	if (cts == null) {
 		log.LogWarning("Subscription already terminated");
 		return;
 	}
 	var position = indexProcessor.GetLastPosition();
 	var startFrom = position == TFPos.Invalid ? Position.Start : Position.FromInt64(position.CommitPosition, position.PreparePosition);
 	log.LogInformation("Starting indexing subscription from {StartFrom}", startFrom);
 
 	_subscription = new(
 		bus: publisher,
 		expiryStrategy: DefaultExpiryStrategy.Instance,
 		checkpoint: startFrom,
 		resolveLinks: false,
 		user: SystemAccounts.System,
 		requiresLeader: false,
 		catchUpBufferSize: options.CommitBatchSize * 2,
-		cancellationToken: _cts!.Token
+		cancellationToken: cts.Token
 	);
 
-	_processingTask = ProcessEvents(_cts.Token);
+	_processingTask = ProcessEvents(cts.Token);
 }

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential race condition where _cts could be nullified after the check, and proposes the standard thread-safe pattern to fix it.



                     PR 5373 (2025-11-19)                    
[general] Improve exception message with context

✅ Improve exception message with context

Improve the exception message when an index is not found by including the indexName to provide better context for debugging.

src/KurrentDB/Components/Query/QueryService.cs [36-43]

 case "index":
 	var indexName = tokens[1];
 	var exists = tryGetUserIndexTableDetails(indexName, out var tableName, out var tableFunctionName, out var hasFields);
 	if (!exists)
-		throw new("Index does not exist");
+		throw new($"Index '{indexName}' does not exist.");
 
 	cte = string.Format(UserIndexCteTemplate, cteName, $"\"{tableName}\"", $"\"{tableFunctionName}\"", hasFields ? ", field" : string.Empty);
 	break;

Suggestion importance[1-10]: 4

__

Why: The suggestion improves the exception message by including the indexName, which enhances debuggability, but it is a minor quality improvement rather than a functional change.



Clone this wiki locally