Skip to content

AFTER UPDATE: fix(generation): centralize connection admission and provider fallback - #4423

Closed
Gunterlie wants to merge 7 commits into
stagingfrom
stack/1-admission
Closed

Gunterlie wants to merge 7 commits into
stagingfrom
stack/1-admission

Conversation

@Gunterlie

@Gunterlie Gunterlie commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

Why

NoodleR posts and images generate in the background, on the same API connection the user is actively chatting on. Without a gate, a scheduled creator post fires mid-roleplay and the user queues behind their own bot, or burns rate limit they wanted for themselves.

This adds a shared admission service so background work yields to foreground work: it is admitted only when nothing is running in the foreground on that connection and the last foreground request finished at least 30s ago. Foreground work never asks permission.

Admission failures are typed distinctly (isConnectionAdmissionFailure) so the fallback chain can tell "connection busy" apart from "provider failed" — otherwise a busy connection would look like an outage and trigger a pointless failover to the backup model.

Relationship to #4503

Standalone against staging — not a stack. This PR is the connection-admission
work only, and it stands on its own.

The rest of what was originally split across stack/2-docs and stack/3-server
now lives in #4503 (NoodleR Slice 8f), which is also based on staging and is
reviewed independently.

#4503's branch does contain this PR's commits, because the Slice 8f code is
built on the admission service. Merge this PR first — once it lands, these
changes drop out of #4503's diff and what remains there is the Slice 8f work.

Notes

  • No linked issue exists for this work; one should be opened before this is taken out of draft.
  • states is a module-level map keyed per connection id and per image base URL, and is never pruned. Bounded by configured connections, so not a leak that matters in practice, but it is an unmarked deliberate call.

Test plan

  • pnpm check
  • pnpm regression:providers
  • Manually verify in the app: start a long foreground generation, confirm a scheduled NoodleR post does not fire until 30s after it finishes
  • Manually verify a genuinely failing primary provider still falls back to the backup model (admission errors must not suppress real failover)

Summary by CodeRabbit

  • New Features

    • Added connection-aware controls for language model, embedding, and image-generation requests.
    • Prevented conflicting background requests during active foreground work.
    • Improved handling of shared image-generation endpoints and interrupted streaming requests.
  • Bug Fixes

    • Prevented unnecessary fallback retries after connection admission failures.
    • Ensured connection activity is released after completed or interrupted requests.
  • Tests

    • Added coverage for connection exclusivity, fallback behavior, stream abandonment, and background request handling.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds per-connection admission control for provider operations. Provider creation now receives connection IDs across routes and services. Image and fallback generation handle admission failures explicitly. Regression coverage validates admission, cleanup, and rejection behavior.

Changes

Connection admission

Layer / File(s) Summary
Admission state and provider wrapper
packages/server/src/services/generation/connection-admission.ts
Adds foreground and background admission tracking, lifecycle errors, reset helpers, and provider wrappers for chat and embedding operations.
Provider fallback and registry integration
packages/server/src/services/llm/provider-registry.ts, packages/server/src/services/llm/connection-fallback-provider.ts
Wraps connection-bound providers, preserves admission failures, and closes abandoned primary streams.
Image generation admission
packages/server/src/services/image/image-generation.ts
Adds request admission modes, endpoint-based admission keys, wraps physical image requests, and skips fallback for admission failures.
Connection ID propagation
packages/server/src/routes/*.routes.ts, packages/server/src/routes/generate/dry-run-route.ts, packages/server/src/services/{capability-packages,memory-recall-embedding,professor-mari,spotify,turn-games}/*
Passes selected connection IDs to LLM and embedding provider creation.
Admission regression coverage
scripts/regressions/provider-compat.regression.ts
Tests connection isolation, stream cleanup, endpoint keys, background accounting, and rejection propagation.

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

Sequence Diagram(s)

sequenceDiagram
  participant RouteOrService
  participant createLLMProvider
  participant ConnectionAdmissionProvider
  participant LLMProvider
  RouteOrService->>createLLMProvider: provide connection ID
  createLLMProvider->>ConnectionAdmissionProvider: wrap configured provider
  ConnectionAdmissionProvider->>LLMProvider: admit and execute operation
  LLMProvider-->>ConnectionAdmissionProvider: return result or error
  ConnectionAdmissionProvider-->>RouteOrService: return admitted result
Loading

Possibly related PRs

Suggested labels: bugfix, refactor

Suggested reviewers: spicymarinara

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the centralization of connection admission and provider fallback, which matches the main changes.
Description check ✅ Passed The description explains the problem, implementation, relationship to related work, known limitation, and planned validation, but it does not follow the repository template fully.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stack/1-admission

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the server label Aug 1, 2026
@Gunterlie Gunterlie changed the title AFTER UPDATE: fix(generation): centralize connection admission and provider fallback TRY YOUR LUCK: fix(generation): centralize connection admission and provider fallback Aug 1, 2026
@Gunterlie Gunterlie changed the title TRY YOUR LUCK: fix(generation): centralize connection admission and provider fallback AFTER UPDATE: fix(generation): centralize connection admission and provider fallback Aug 1, 2026
@Gunterlie
Gunterlie marked this pull request as ready for review August 2, 2026 20:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

Inline comments:
In `@packages/server/src/services/image/image-generation.ts`:
- Around line 123-124: Update generateImage and ImageGenRequest to carry the
selected primary connectionId/admission key, and use that key for admission on
both primary and fallback image-generation attempts instead of only
normalizedBaseUrl. When switching to the fallback provider, replace it with the
image-specific fallback connection id so each request is keyed by its actual
connection row.

In `@packages/server/src/services/llm/connection-fallback-provider.ts`:
- Around line 165-171: Update the finally cleanup around
generation.return(undefined) in the fallback provider to suppress any rejection
from the cleanup call, ensuring it cannot replace an error from
generation.next(). Preserve the cleanup attempt for abandoned consumers while
allowing the subsequent catch to inspect and handle the original provider error.

In `@packages/server/src/services/llm/provider-registry.ts`:
- Around line 165-166: The admission wrapper currently has competing owners,
causing duplicate wrapping and preventing background-mode requests. In
packages/server/src/services/llm/provider-registry.ts lines 165-166, choose one
ownership model by either passing an admission mode alongside connectionId or
removing the implicit withConnectionAdmissionProvider wrapping, and verify
background callers do not receive an implicit foreground wrapper. In
packages/server/src/services/llm/connection-fallback-provider.ts lines 207-233,
apply the fallback rule to the primary path by requiring an unwrapped primary or
detecting an existing ConnectionAdmissionProvider for primaryConnectionId before
wrapping, preserving the requested mode and preventing duplicate background
admission.

In `@scripts/regressions/provider-compat.regression.ts`:
- Around line 769-816: Add post-run release assertions to the two scenarios:
after `primaryRun` rejects, verify
`tryBackgroundConnection("primary-connection", ...)` succeeds, and after
`fallbackRun` resolves, verify the equivalent for `"fallback-connection"`. Reuse
Scenario 3’s `BACKGROUND_CONNECTION_IDLE_MS` time offset and release each
acquired admission.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3699ee69-6318-4abc-9263-171a8072c91d

📥 Commits

Reviewing files that changed from the base of the PR and between e3c7592 and e028342.

📒 Files selected for processing (14)
  • packages/server/src/routes/connections.routes.ts
  • packages/server/src/routes/encounter.routes.ts
  • packages/server/src/routes/generate/dry-run-route.ts
  • packages/server/src/routes/lorebooks.routes.ts
  • packages/server/src/services/capability-packages/capability-language-model.service.ts
  • packages/server/src/services/generation/connection-admission.ts
  • packages/server/src/services/image/image-generation.ts
  • packages/server/src/services/llm/connection-fallback-provider.ts
  • packages/server/src/services/llm/provider-registry.ts
  • packages/server/src/services/memory-recall-embedding.ts
  • packages/server/src/services/professor-mari/workspace-agent.service.ts
  • packages/server/src/services/spotify/dj-mari-playlist.service.ts
  • packages/server/src/services/turn-games/turn-game-bot-runner.service.ts
  • scripts/regressions/provider-compat.regression.ts

Comment thread packages/server/src/services/image/image-generation.ts
Comment thread packages/server/src/services/llm/connection-fallback-provider.ts
Comment thread packages/server/src/services/llm/provider-registry.ts
Comment thread scripts/regressions/provider-compat.regression.ts
@Gunterlie

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Thanks — worked through all four. Two were valid and are fixed in 72aa681; two I am skipping, with reasoning below.

Fixed — connection-fallback-provider.ts teardown masking the provider error

Correct, and it undercut the fix it was part of. The finally awaited generation.return(undefined) directly, so a teardown rejection would propagate in place of the error from generation.next() — exactly the error the catch inspects via isAbortFailure / isConnectionAdmissionFailure to decide whether to fall back. Now .catch(() => undefined), so cleanup still runs for abandoned consumers but cannot displace the original failure.

Fixed — post-run release assertions in provider-compat.regression.ts

Added to both scenarios, reusing the BACKGROUND_CONNECTION_IDLE_MS offset and releasing each acquired admission. Mutation-checked rather than assumed: stubbing out attempt.release() in finalizeConnectionAttempt makes the lane exit 1 on must release its foreground slot, and it returns to 0 when restored. They cover the failed-call and completed-call paths that the abandoned-stream case did not.

Skipping — threading connectionId into image admission

The fallback concern does not apply: the fallback path re-enters generateImage recursively at line 309 with fallback.baseUrl, so it is already admitted under its own endpoint key, not the primary's. admissionMode rides along in the ...request spread.

On keying generally, endpoint-over-connection-id is deliberate and documented at lines 279-284: most image callers have no connection row in scope, so keying on connectionId would leave that foreground work unregistered and let background preparation start underneath it — strictly worse than the current conservative behavior. Two connections sharing a base URL share a key, which over-serializes background work slightly and never affects foreground. The cross-keyspace gap with text work is already flagged in the ponytail: comment there.

Skipping — competing admission-wrapper ownership

I could not reproduce the premise on this branch. No call site both passes connectionId to createLLMProvider and feeds that provider to withConnectionFallbackProvider as primary. The nine sites that gained connectionId (connections.routes.ts x3, encounter.routes.ts x3, lorebooks.routes.ts, dry-run-route.ts, memory-recall-embedding.ts, workspace-agent.service.ts, dj-mari-playlist.service.ts, turn-game-bot-runner.service.ts) all consume the provider directly. The primary: sites for the fallback wrapper are a disjoint set and pass no connectionId. So no double wrapping occurs today.

The background half is likewise not reachable: grep finds no production caller constructing { kind: "background" } — that path exists for the scheduler work landing later in the stack and is currently exercised only by regressions, which wrap explicitly rather than going through createLLMProvider.

Worth noting the latent footgun is real if a future caller does both, though the failure is mild for foreground (symmetric double increment/decrement, so no leak — just background held off while active). A nested background wrap would throw BackgroundConnectionBusyError on the inner attempt, which would be a genuine bug. Happy to add the "detect an existing ConnectionAdmissionProvider for this connectionId before wrapping" guard if you would rather have it in now, but on this branch it guards a case no caller can currently reach, so I left it out.

Validation on 72aa681: pnpm check 0, pnpm regression:providers 0.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Gunterlie Thanks for the detailed follow-up.

The two fixed items address the reported failure modes. Suppressing teardown rejection preserves the provider error for fallback classification. The added release assertions cover both failed and completed calls.

I accept the two skipped items for this PR.

Image generation re-enters generateImage with fallback.baseUrl. The fallback therefore uses its own endpoint admission key. Endpoint keying also preserves foreground accounting where no connection row is available.

The current provider call graph does not double-wrap an admission provider. The nested background-wrapper case is a future integration risk only. A guard can be added when production background callers or combined fallback/provider construction are introduced.

The reported validation is sufficient for these findings.

🐇 ✅


✏️ Learnings added
Learnt from: Gunterlie
URL: https://github.com/Pasta-Devs/Marinara-Engine/pull/4423

Timestamp: 2026-08-02T21:42:47.927Z
Learning: In `packages/server/src/services/image/image-generation.ts`, image connection admission deliberately uses an endpoint key instead of a connection ID. Many image callers have no connection row available, so endpoint keying ensures foreground image work is registered. Fallback image generation recursively calls `generateImage` with `fallback.baseUrl`, which gives fallback work its own endpoint admission key. This may conservatively serialize separate connections that share a base URL, and image admission currently has a documented cross-keyspace gap with text admission.

Learnt from: Gunterlie
URL: https://github.com/Pasta-Devs/Marinara-Engine/pull/4423

Timestamp: 2026-08-02T21:42:47.927Z
Learning: In PR `#4423`, current production call sites that pass `connectionId` to `createLLMProvider` consume the provider directly and are disjoint from `withConnectionFallbackProvider` primary-provider call sites. No production caller currently constructs background connection admission. A nested `ConnectionAdmissionProvider` can become a defect for background admission if a future caller both passes a connection ID to `createLLMProvider` and wraps that provider with fallback admission.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/server/src/services/llm/connection-fallback-provider.ts (1)

169-172: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Log cleanup failures without masking the primary error.

At Line 172, .catch(() => undefined) drops every cleanup error. A rejected generation.return(undefined) can indicate incomplete provider or connection-admission cleanup. If cleanup fails, log the error with logger.warn and continue without rethrowing. This preserves the primary error for fallback decisions and keeps the failure observable.

Proposed fix
-        await generation.return(undefined).catch(() => undefined);
+        try {
+          await generation.return(undefined);
+        } catch (cleanupError) {
+          logger.warn(
+            cleanupError,
+            "[%s-fallback] Failed to close the primary generation during cleanup",
+            this.category,
+          );
+        }

As per coding guidelines, server failures must use the shared Pino logger, use logger.warn for non-fatal issues, and pass the error object first.

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

In `@packages/server/src/services/llm/connection-fallback-provider.ts` around
lines 169 - 172, Update the cleanup call in the fallback provider flow around
generation.return to catch the rejection as an error, log it with the shared
logger.warn passing the error object first, and then continue without rethrowing
so the primary provider error remains available for fallback decisions.

Source: Coding guidelines

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

Duplicate comments:
In `@packages/server/src/services/llm/connection-fallback-provider.ts`:
- Around line 169-172: Update the cleanup call in the fallback provider flow
around generation.return to catch the rejection as an error, log it with the
shared logger.warn passing the error object first, and then continue without
rethrowing so the primary provider error remains available for fallback
decisions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5d13dfef-02ba-4638-a113-7129eea81861

📥 Commits

Reviewing files that changed from the base of the PR and between e028342 and 72aa681.

📒 Files selected for processing (2)
  • packages/server/src/services/llm/connection-fallback-provider.ts
  • scripts/regressions/provider-compat.regression.ts

@Gunterlie

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Valid — fixed in 1e3ce0f.

The previous .catch(() => undefined) stopped the teardown failure from displacing the provider error, but discarded it entirely, so a generator that failed to close left no trace at all. Now:

await generation.return(undefined).catch((closeError: unknown) => {
  logger.warn(closeError, "[%s-fallback] Failed to close the primary generation stream", this.category);
});

Error object first and a Pino format specifier for this.category, per the repo logging convention. Still no rethrow, so the catch below continues to see the original provider error when deciding whether to fall back.

pnpm check 0, pnpm regression:providers 0.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Gunterlie The fix addresses the observability gap.

logger.warn records a primary-stream teardown failure. The handler does not rethrow, so the original provider error remains available to the fallback decision logic.

The reported validation covers the updated behavior.

🐇 ✅

You are interacting with an AI system.

@Gunterlie

Copy link
Copy Markdown
Collaborator Author

@Xelvanis Thanks for the review — all three findings were real, and F1 in particular was a good catch. Fixed in b647ac8, and the branch is now merged up with staging as of 8cce9fe.

F1 — RunPod admission conflating endpoints

Confirmed exactly as described: buildRunPodUrl composes baseUrl + endpointId, so two RunPod connections sharing api.runpod.ai/v2 with different endpoint IDs landed on one admission key. Foreground work on one could delay background or fallback work on an entirely idle other, plus the 30s cooldown.

Added imageAdmissionKey(normalizedBaseUrl, imageEndpointId), which appends the trimmed endpoint id when there is one and returns the bare URL otherwise — so backends whose base URL is the physical target (ComfyUI, A1111) keep their existing key and behavior. Your counterevidence note on that case is what shaped the signature. The recursive fallback call re-derives the key from fallback.imageEndpointId, so fallback attempts are isolated too.

On your account-wide uncertainty: I kept the repository contract you identified — admission is per physical endpoint. If RunPod rate limits turn out to be account-wide, that is a separate policy layer and should not be smuggled in through the admission key.

F2 — wrapper replacing capability diagnostics

Confirmed at capability-language-model.service.ts:24. Added unwrapConnectionAdmissionProvider and used it there, so the concrete backend class is reported again.

Scoped deliberately to the admission decorator. ConnectionDefaultProvider can mask the name the same way when a connection has custom parameters, but that predates this PR, so I left it rather than widen the diff. Worth a separate issue if the diagnostic value matters to you.

F3 — dead admission surface

Confirmed — the only references to withForegroundConnection and kind: "none" anywhere were stale dist/ artifacts, no source consumers on this head or downstream in the stack. Deleted both and the two no-op branches. beginForegroundConnection stays, it is live. Also corrected the module header comment, which still claimed image work keys on base URL alone.

Validation

pnpm check 0, pnpm regression:providers 0, pnpm regression:noodle 0, run after the staging merge. Your ajv blocker did not reproduce here; the lane runs clean on a fresh --frozen-lockfile install.

Coverage for F1 is on the key derivation — same-base/different-id, trimming, and the bare-URL case — and it is mutation-checked: collapsing the key back to the base URL makes the lane exit 1. Your residual-risk note stands that nothing drives two live RunPod endpoints over HTTP end to end; that would need a two-endpoint fake server and I did not add one.

Deferred item noted — background admission from Noodle owners and conn.id from the Agent Suite rewrite arrive in stack/3-server, not this layer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@packages/server/src/services/image/image-generation.ts`:
- Around line 302-304: Update the background image-generation entry points,
including runImageGenerationRequest with queue false and illustrated background
variants, to pass admissionMode { kind: "background" } plus the appropriate
queue/cooldown hook into generateImage; ensure generateImage’s admission path
receives these values instead of defaulting to foreground admission.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2b5aaa0c-9468-42b6-814b-2c8df84d4f15

📥 Commits

Reviewing files that changed from the base of the PR and between b647ac8 and 5bc8524.

📒 Files selected for processing (2)
  • packages/server/src/services/image/image-generation.ts
  • scripts/regressions/provider-compat.regression.ts

Comment thread packages/server/src/services/image/image-generation.ts
Extracts connection admission accounting into a shared service so image
generation, the provider registry, and the fallback provider all route
through one place instead of each tracking slots independently.

Split out as the base layer of a stack; the NoodleR work builds on it.
ConnectionFallbackProvider drains the primary with a manual loop rather than
yield*, so a consumer that stopped reading mid-stream never propagated the early
return to the primary generator. With an admission wrapper in place that left the
connection's foreground slot held for the lifetime of the process, silently
locking out all background generation on that connection.

Covered by a provider-compat case that fails without the fix.
The finally that closes the primary generator awaited generation.return()
directly, so a rejection from teardown replaced the provider error the catch
below inspects to decide whether to fall back. Swallow it.

Also assert both existing admission scenarios release their foreground slot,
covering the failed-call and completed-call paths.
…ping them

The cleanup catch discarded the rejection silently, so a generator that failed
to close left no trace. Log it with the shared logger and continue, keeping the
provider error available for the fallback decision.
…agnostics concrete

RunPod connections share one API base URL and select the physical endpoint with a
separate id, so keying admission on the base URL alone let two independent endpoints
contend for one slot. Key on the endpoint instead.

The admission decorator also became the class reported through the capability
language-model host, so every configured provider surfaced as
ConnectionAdmissionProvider in package diagnostics. Unwrap before reading the name.

Drops withForegroundConnection and the none mode, which had no consumers.
imageAdmissionKey appended any imageEndpointId regardless of backend, so a
stale id left on an imported, copied, or API-created ComfyUI/A1111 connection
split one physical endpoint into separate admission slots — letting background
work bypass foreground admission and its cooldown. Key on the resolved source
so only runpod_comfyui, whose base URL does not identify the endpoint, carries
the id.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

Inline comments:
In `@packages/server/src/services/generation/connection-admission.ts`:
- Around line 9-16: Prune idle entries from the states map within the foreground
and background release paths. When a state has foregroundActive === 0,
backgroundActive === false, and lastForegroundFinishedAt is older than
BACKGROUND_CONNECTION_IDLE_MS, delete its corresponding key; preserve state
entries while either activity remains active or the idle window has not elapsed.
- Around line 161-171: Update BaseLLMProvider.chatComplete() to close the
AsyncGenerator returned by provider.chat() by calling await
gen.return(undefined) in a finally block around its manual gen.next() loop,
ensuring ConnectionAdmissionProvider.chat() executes finalizeConnectionAttempt.
Preserve the existing iteration and result handling.
- Around line 122-134: The completed-outcome path in finalizeConnectionAttempt
must not promote late finalizer errors into a stream failure after usable tokens
have been emitted. Adjust the coordination between
ConnectionAdmissionProvider.chat, finalizeConnectionAttempt, and
ConnectionFallbackProvider.chat so completed finalization errors are retained
for non-streaming calls but logged and suppressed for already-emitting streams,
preserving normal propagation before output is delivered.

In `@packages/server/src/services/llm/connection-fallback-provider.ts`:
- Around line 222-236: Prevent fallback retries from reusing the one-shot
ConnectionAdmissionMode.beforeAttempt hook. In
packages/server/src/services/llm/connection-fallback-provider.ts lines 222-236,
derive a separate fallback mode without beforeAttempt (or claim the hook once
and pass plain background mode) before calling withConnectionAdmissionProvider;
in packages/server/src/services/image/image-generation.ts lines 296-309,
override admissionMode in the recursive fallback request spread with a mode that
omits beforeAttempt, while preserving the existing fallback override.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b21fc3b6-5eea-468d-9ee6-03e3214af6a5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bc8524 and f727e13.

📒 Files selected for processing (14)
  • packages/server/src/routes/connections.routes.ts
  • packages/server/src/routes/encounter.routes.ts
  • packages/server/src/routes/generate/dry-run-route.ts
  • packages/server/src/routes/lorebooks.routes.ts
  • packages/server/src/services/capability-packages/capability-language-model.service.ts
  • packages/server/src/services/generation/connection-admission.ts
  • packages/server/src/services/image/image-generation.ts
  • packages/server/src/services/llm/connection-fallback-provider.ts
  • packages/server/src/services/llm/provider-registry.ts
  • packages/server/src/services/memory-recall-embedding.ts
  • packages/server/src/services/professor-mari/workspace-agent.service.ts
  • packages/server/src/services/spotify/dj-mari-playlist.service.ts
  • packages/server/src/services/turn-games/turn-game-bot-runner.service.ts
  • scripts/regressions/provider-compat.regression.ts

Comment on lines +9 to +16
type ConnectionState = {
foregroundActive: number;
backgroundActive: boolean;
lastForegroundFinishedAt: number;
};

const states = new Map<string, ConnectionState>();
export const BACKGROUND_CONNECTION_IDLE_MS = 30_000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

states grows without bound and is never pruned.

states keeps one entry per connection id and per image admission key forever. Entries stay after the connection is deleted or the endpoint changes. The entry is small, so this is a slow leak rather than an outage, but nothing removes idle entries.

Consider deleting the state when foregroundActive === 0, backgroundActive === false, and the idle window has elapsed. Do this inside the release paths.

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

In `@packages/server/src/services/generation/connection-admission.ts` around lines
9 - 16, Prune idle entries from the states map within the foreground and
background release paths. When a state has foregroundActive === 0,
backgroundActive === false, and lastForegroundFinishedAt is older than
BACKGROUND_CONNECTION_IDLE_MS, delete its corresponding key; preserve state
entries while either activity remains active or the idle window has not elapsed.

Comment on lines +122 to +134
async function finalizeConnectionAttempt(
attempt: { release: () => void; finalize?: ConnectionAttemptFinalizer },
outcome: ConnectionAttemptOutcome,
): Promise<void> {
try {
await attempt.finalize?.(outcome);
} catch (error) {
if (outcome === "completed") throw new ConnectionAttemptFinalizationError(error);
logger.error(error, "[connection-admission] Attempt accounting failed after a failed provider call");
} finally {
attempt.release();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect background finalizers passed as beforeAttempt to confirm what a failed finalize means for streamed output.
set -euo pipefail

rg -n -C 12 'beforeAttempt' --glob '*.ts' packages/server/src scripts

Repository: Pasta-Devs/Marinara-Engine

Length of output: 9339


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- connection-admission relevant section ---\n'
sed -n '70,190p' packages/server/src/services/generation/connection-admission.ts | cat -n

printf '\n--- ConnectionFallbackProvider relevant references ---\n'
rg -n -C 8 'finalizeConnectionAttempt|ChatCompletion|chat\(|chatComplete|ConnectionAttemptFinalizationError|chatComplete' packages/server/src/services/llm/connection-fallback-provider.ts packages/server/src/services/generation/connection-admission.ts

printf '\n--- all finalizeConnectionAttempt usages ---\n'
rg -n -C 8 'finalizeConnectionAttempt' packages/server/src

Repository: Pasta-Devs/Marinara-Engine

Length of output: 255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- connection-admission relevant section ---'
sed -n '70,190p' packages/server/src/services/generation/connection-admission.ts | cat -n

printf '%s\n' ''
printf '%s\n' '--- ConnectionFallbackProvider relevant references ---'
rg -n -C 8 'finalizeConnectionAttempt|ChatCompletion|chat\(|chatComplete|ConnectionAttemptFinalizationError|chatComplete' packages/server/src/services/llm/connection-fallback-provider.ts packages/server/src/services/generation/connection-admission.ts

printf '%s\n$' ''
printf '%s\n' '--- all finalizeConnectionAttempt usages ---'
rg -n -C 8 'finalizeConnectionAttempt' packages/server/src

Repository: Pasta-Devs/Marinara-Engine

Length of output: 29615


Avoid promoting late finalizer failures to stream failure after output is emitted.

ConnectionAdmissionProvider.chat marks the streaming call completed after yield* this.provider.chat(...) returns, then finalizeConnectionAttempt can later throw ConnectionAttemptFinalizationError. ConnectionFallbackProvider.chat includes that admission failure in its catch and rethrows it when emittedUsableOutput is true, so the consumer reports a failed generation for a token stream that already delivered content. Keep the throw for non-streaming paths only, or log and suppress it when tokens were already delivered.

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

In `@packages/server/src/services/generation/connection-admission.ts` around lines
122 - 134, The completed-outcome path in finalizeConnectionAttempt must not
promote late finalizer errors into a stream failure after usable tokens have
been emitted. Adjust the coordination between ConnectionAdmissionProvider.chat,
finalizeConnectionAttempt, and ConnectionFallbackProvider.chat so completed
finalization errors are retained for non-streaming calls but logged and
suppressed for already-emitting streams, preserving normal propagation before
output is delivered.

Comment on lines +161 to +171
async *chat(messages: ChatMessage[], options: ChatOptions): AsyncGenerator<string, LLMUsage | void, unknown> {
const attempt = await beginConnectionAttempt(this.connectionId, this.mode);
let outcome: ConnectionAttemptOutcome = "failed";
try {
const result = yield* this.provider.chat(messages, options);
outcome = "completed";
return result;
} finally {
await finalizeConnectionAttempt(attempt, outcome);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find manual iteration of provider chat generators that may skip generator close.
set -euo pipefail

echo "--- manual .next() iteration over chat generators ---"
rg -n -C 10 --glob '*.ts' '\.chat\s*\(' packages/server/src | rg -n -C 6 '\.next\s*\(' || true

echo "--- explicit generator close calls ---"
rg -n -C 4 --glob '*.ts' '\.return\s*\(\s*undefined\s*\)' packages/server/src || true

echo "--- for await consumers of chat ---"
rg -n -C 4 --glob '*.ts' 'for await' packages/server/src | rg -n 'chat' || true

Repository: Pasta-Devs/Marinara-Engine

Length of output: 5412


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- locate relevant files ---"
fd -a 'connection-admission|connection-fallback-provider|base-provider|game\.routes|connections\.routes|providers' packages/server/src | sed 's#^\./##'

echo "--- connection-admission outline and relevant section ---"
ast-grep outline packages/server/src/services/generation/connection-admission.ts --view compact || true
sed -n '1,230p' packages/server/src/services/generation/connection-admission.ts

echo "--- base-provider chat generation outline/search around generateContentWithStream ---"
ast-grep outline packages/server/src/services/llm/base-provider.ts --view compact || true
sed -n '620,760p' packages/server/src/services/llm/base-provider.ts

Repository: Pasta-Devs/Marinara-Engine

Length of output: 13692


Close provider.chat(...) generators when calling chatComplete.

BaseLLMProvider.chatComplete() calls gen.next() in a loop but does not call return() on the AsyncGenerator, so the ConnectionAdmissionProvider.chat() finally block never runs. Manual callers must close an await gen.next() loop with await gen.return(undefined) in finally; for await closes the generator automatically.

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

In `@packages/server/src/services/generation/connection-admission.ts` around lines
161 - 171, Update BaseLLMProvider.chatComplete() to close the AsyncGenerator
returned by provider.chat() by calling await gen.return(undefined) in a finally
block around its manual gen.next() loop, ensuring
ConnectionAdmissionProvider.chat() executes finalizeConnectionAttempt. Preserve
the existing iteration and result handling.

Comment on lines +222 to 236
const fallback = withConnectionAdmissionProvider(
createLLMProvider(
fallbackConnection.provider,
fallbackBaseUrl,
fallbackConnection.apiKey,
fallbackConnection.maxContext,
fallbackConnection.openrouterProvider,
fallbackConnection.maxTokensOverride,
isEnabled(fallbackConnection.claudeFastMode),
isEnabled(fallbackConnection.treatAsLocalEndpoint),
fallbackConnection.defaultParameters,
),
fallbackConnection.id,
admissionMode,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

ConnectionAdmissionMode carries a one-shot beforeAttempt hook, but both fallback paths reuse the same mode object for the retry. In background mode beforeAttempt returns a finalizer for one attempt. Each fallback path passes the identical mode object to the retry, so a single logical generation invokes beforeAttempt twice and produces two finalizers: the primary attempt finalizes as "failed" and the fallback attempt finalizes as "completed". Attempt accounting then double-counts the generation.

  • packages/server/src/services/llm/connection-fallback-provider.ts#L222-L236: derive a separate mode for the fallback wrapper instead of reusing the admissionMode reference already applied to primary at Line 213. Either strip beforeAttempt for the fallback wrap, or make the hook claim once per withConnectionFallbackProvider invocation and hand the retry a plain { kind: "background" } mode.
  • packages/server/src/services/image/image-generation.ts#L296-L309: the recursive fallback call at Line 326 spreads ...request and carries admissionMode through unchanged. Override admissionMode in that spread with a mode that has no beforeAttempt, matching the way fallback: undefined is already overridden.
📍 Affects 2 files
  • packages/server/src/services/llm/connection-fallback-provider.ts#L222-L236 (this comment)
  • packages/server/src/services/image/image-generation.ts#L296-L309
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/services/llm/connection-fallback-provider.ts` around
lines 222 - 236, Prevent fallback retries from reusing the one-shot
ConnectionAdmissionMode.beforeAttempt hook. In
packages/server/src/services/llm/connection-fallback-provider.ts lines 222-236,
derive a separate fallback mode without beforeAttempt (or claim the hook once
and pass plain background mode) before calling withConnectionAdmissionProvider;
in packages/server/src/services/image/image-generation.ts lines 296-309,
override admissionMode in the recursive fallback request spread with a mode that
omits beforeAttempt, while preserving the existing fallback override.

@Gunterlie

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #4514, which is the same work re-cut as a fresh branch off staging with the outstanding review findings fixed (one-shot beforeAttempt no longer re-claimed on fallback, finalizer errors no longer kill a completed stream, chatComplete now closes the generator on every exit). Not a stack any more: #4514 and #4515 are two independent PRs against staging.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant