Skip to content

feat(session): add session keeping with sticky load balancing and threaded audit logs - #602

Merged
SantiagoDePolonia merged 11 commits into
mainfrom
feat/session-keeping
Jul 28, 2026
Merged

feat(session): add session keeping with sticky load balancing and threaded audit logs#602
SantiagoDePolonia merged 11 commits into
mainfrom
feat/session-keeping

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What

Requests from one client session (one conversation / one agent task) are now recognized and used in two places:

  1. Sticky load balancing — a load-balanced virtual model routes every request of a session to the target that served it first, keeping provider prompt caches warm and model behavior consistent mid-conversation. On by default; disable per redirect with session_affinity: false (YAML/env/admin API/editor checkbox).
  2. Threaded Audit Logs — the Audit Logs page gets a "Group by session" toggle (on by default): one row per session showing its latest request with a count badge, an expander on the left unfolds the older requests indented with tree connector lines. Every unfolded row keeps full expansion, panes, and the Interactions drawer.

How a session is identified

First match wins, zero client configuration needed:

  1. Headers (built-in registry, source-verified per tool): X-Session-Id (OpenCode, Kilo Code), X-Claude-Code-Session-Id (Claude Code, documented for gateways), Session-Id/Session_id (Codex CLI, Roo Code), X-Litellm-Session-Id, Helicone-Session-Id, Agent-Session-Id (Goose).
  2. Body fields: Anthropic metadata.user_id (both Claude Code formats parsed), session_id, litellm_session_id, prompt_cache_key (Zed), /v1/responses conversation.
  3. Auto-detection: chat/responses requests are grouped by their conversation opening (model + system + tools + messages through the first user turn), so agents that just resend history (Aider, Cline, Continue) still group.

Config: SESSION_KEEPING_ENABLED / SESSION_AUTO_DETECT / SESSION_BUILTIN_RULES / numbered SESSION_HEADER_<N> (+_TRANSFORM), or the session: YAML block. Credential headers are rejected as sources. Non-UUID ids are scoped by user path so weak client ids cannot collide across tenants.

Provider-specific / operational notes

  • Sticky pins live per instance (like rate-limit counters and the realtime call registry): a restart or another replica just re-pins; 6h idle TTL, 10k cap.
  • A pinned target that becomes unavailable or rate-saturated is re-picked by the strategy and the session re-pins; the all-saturated first-target 429 fallback never pins.
  • session_id is a new indexed column on audit_logs (idempotent ALTER migration on SQLite/PostgreSQL, index on MongoDB) and a new tri-state session_affinity column on virtual_models.
  • New admin endpoint GET /admin/audit/sessions (threads: latest entry + count + span; filters apply to entries before grouping; sessionless entries are singleton threads); GET /admin/audit/log gains a session_id filter that works without date params.
  • Deferred (documented): /v1/responses previous_response_id chain-following, failover-sweep re-pinning, do-not-pass stripping for session headers.

Docs

New docs/features/session-keeping.mdx, updates to virtual-models docs, .env.template, config.example.yaml, CLAUDE.md config reference; swagger regenerated.

Testing

  • Unit/integration: detection precedence + both metadata.user_id formats + auto-detect stability; sticky semantics (both strategies, re-pin, no-pin-on-saturation, TTL, prune, eviction); audit column roundtrip + sessions listing verified live against SQLite, PostgreSQL, and MongoDB; stream-entry whitelist guard; admin handler + routes; config env/yaml merge. Dashboard: 376 node tests (grouped merge, live folding, gating, flat-mode regression), svelte-check clean, dist rebuilt.
  • Live smoke test: header sessions grouped (count 3), auto-detected conversation chained across turns, Claude Code metadata UUID extracted, threaded dashboard UI verified with screenshots (collapsed + expanded with connector lines).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added session keeping to group related requests for sticky virtual-model routing using detected session ids.
    • Added session_affinity support for virtual models in the dashboard and admin/API (configurable to return to stateless balancing).
    • Audit logs now store session_id, allow exact filtering, and support a threaded “group by session” view via a new /admin/audit/sessions endpoint.
  • Bug Fixes
    • Improved audit retrieval and pagination normalization to avoid empty/incorrect limit edge cases.
  • Documentation
    • Updated configuration and feature docs for session keeping/session affinity and the new audit session threading behavior.
  • Tests
    • Added test coverage for session detection, sticky routing, and session-thread audit grouping.

…eaded audit logs

Detect which client session each request belongs to and use it in two places:
load-balanced virtual models route a session to the target that served it
first (session_affinity, default on, disable per redirect), and the Audit Logs
page groups requests into session threads (Group by session, default on, with
expandable children and tree connectors).

Session identity: explicit headers from a built-in registry of known tools
(Claude Code, Codex CLI, OpenCode, Kilo Code, Goose, LiteLLM/Helicone
conventions), then body signals (Anthropic metadata.user_id in both Claude
Code formats, session_id, litellm_session_id, prompt_cache_key, responses
conversation), then content auto-detection hashing the conversation opening.
Configurable via SESSION_KEEPING_ENABLED / SESSION_AUTO_DETECT /
SESSION_BUILTIN_RULES / SESSION_HEADER_<N> or the session: YAML block; zero
config works. Non-UUID ids are scoped by user path.

Audit entries persist an indexed session_id across SQLite/PostgreSQL/MongoDB
(and the live preview events); GET /admin/audit/sessions lists threads
(latest entry + count + span, sessionless entries as singletons) and
GET /admin/audit/log?session_id=... fetches one thread. Sticky pins are
per-instance with a 6h idle TTL, are never taken on the all-saturated 429
fallback, and re-pin when a pinned target becomes unavailable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 19:54

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Session keeping spans configuration, request detection, virtual-model affinity, audit persistence and APIs, and dashboard session-thread grouping. Requests receive detected session IDs, routing can pin them to targets, and audit logs can be retrieved as grouped threads.

Changes

Session keeping

Layer / File(s) Summary
Session detection and propagation
config/*, internal/session/*, internal/server/*, internal/core/context.go
Adds configurable session detection, request-context propagation, body handling, and middleware wiring.
Audit persistence and APIs
internal/auditlog/*, internal/admin/*, docs/openapi.json, cmd/gomodel/docs/docs.go
Persists session_id, groups entries for SQL and MongoDB, and adds filtering and session-thread endpoints.
Virtual-model affinity
internal/virtualmodels/*, config/virtualmodels.go, web/dashboard/src/pages/models/*
Adds configurable sticky target selection, persistence, TTL handling, capacity-aware repinning, and editor support.
Dashboard session threads
web/dashboard/src/pages/audit-logs/*, web/dashboard/tests/*
Adds grouped retrieval, expandable threads, lazy child loading, live merging, and related tests.
Documentation and schemas
CLAUDE.md, docs/features/*, docs/docs.json, config/config.example.yaml
Documents session detection, affinity behavior, audit grouping, and configuration options.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SessionCapture
  participant Detector
  participant VirtualModelService
  participant AuditStore
  participant Dashboard
  Client->>SessionCapture: model request
  SessionCapture->>Detector: detect session id
  Detector-->>SessionCapture: session id
  SessionCapture->>VirtualModelService: resolve with session id
  VirtualModelService-->>Client: selected target
  SessionCapture->>AuditStore: persist session_id
  Dashboard->>AuditStore: request grouped sessions
  AuditStore-->>Dashboard: session heads and latest entries
  Dashboard->>AuditStore: request session children
  AuditStore-->>Dashboard: grouped audit entries
Loading

Possibly related PRs

Suggested reviewers: copilot

Poem

A rabbit hops through sessions bright,
Pins each request to targets right.
Audit threads now softly grow,
With carrots queued in rows below.
Sticky paths and logs align—
A tidy burrow, hop by line.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.22% 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
Title check ✅ Passed The title clearly summarizes the main change: session keeping with sticky load balancing and threaded audit logs.
Description check ✅ Passed The description is comprehensive and covers the change, behavior, config, docs, and testing, though it does not use the exact template headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/session-keeping

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.

@mintlify

mintlify Bot commented Jul 27, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Jul 27, 2026, 7:54 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; authentication establishes the effective user path before session detection, and normal and streaming audit persistence recover the resulting session identity from the final request context.

T-Rex T-Rex Logs

What T-Rex did

  • The reviewer compared the before-state and after-state audit dashboards to confirm the UI changes, including grouping by default, a head badge showing 3 items, and the expanded older-requests view with connector styling.
  • The reviewer verified that an older session still supports its details interaction in the expanded view.
  • The reviewer inspected the browser network logs to confirm requests to /admin/audit/sessions, /admin/audit/log, and /admin/audit/detail were issued during navigation.
  • The reviewer reviewed the artifact set (videos, images, and logs) to corroborate the UI and network findings.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

sequenceDiagram
  participant Client
  participant Auth
  participant Session as SessionCapture
  participant Router as Virtual Model Router
  participant Audit
  Client->>Auth: Model request and credentials
  Auth->>Auth: Establish effective user path
  Auth->>Session: Authenticated request
  Session->>Session: Detect and scope session ID
  Session->>Router: Request with session context
  Router->>Router: Reuse or establish target pin
  Router-->>Audit: Final request outcome
  Audit->>Audit: Persist session-scoped entry
Loading

Reviews (2): Last reviewed commit: "chore: merge main into session keeping" | Re-trigger Greptile

Comment thread internal/server/http.go Outdated

@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 `@internal/auditlog/reader_sessions_sql.go`:
- Around line 20-66: The GetSessions session-grouping query needs index support
for its partitioning and per-session ordering. Inspect the audit_logs schema and
migrations for the existing session_id index, then add or update a composite
index covering auditThreadKey (session_id), timestamp, and id in the appropriate
schema/migration location; preserve the current filtering, ranking, and
pagination behavior.

In `@internal/auditlog/session_id_test.go`:
- Around line 120-128: Add grouped-read SessionID filtering assertions in
internal/auditlog/session_id_test.go:120-128 and
internal/auditlog/reader_sessions_mongodb_test.go:61-68. In each GetSessions
test, query SessionID "sess-a" and verify exactly one thread, the matching total
count, and its latest entry; preserve the existing status-filter assertions and
use the backend-specific test setup.

In `@internal/session/detect.go`:
- Around line 99-109: Replace the ambiguous string concatenation in
scopeSessionID with collision-resistant hash-based scoping that incorporates
userPath and id as distinct values, reusing the existing sha256 and hex imports.
Preserve the current behavior for empty userPath and UUID ids, which must
continue returning id unchanged.

In `@web/dashboard/src/pages/audit-logs/auditList.svelte.js`:
- Around line 218-243: Update the stale-result branch in the audit session fetch
flow to remove liveLogs.auditThreadChildren[sessionId] before returning. Reuse
the same placeholder cleanup behavior as the catch path, while keeping stale
handling silent and allowing toggleThread to retry the fetch.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 92f94991-4e5a-4482-9b07-b83c32b7fedb

📥 Commits

Reviewing files that changed from the base of the PR and between 7e974d6 and 3f8fe96.

⛔ Files ignored due to path filters (4)
  • internal/admin/dashboard/static/dist/assets/index-CJ1Lm7IE.css is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/assets/index-D19L8xXa.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/assets/index-DU1ycplF.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (66)
  • .env.template
  • CLAUDE.md
  • cmd/gomodel/docs/docs.go
  • config/config.example.yaml
  • config/config.go
  • config/session.go
  • config/session_test.go
  • config/virtualmodels.go
  • docs/docs.json
  • docs/features/session-keeping.mdx
  • docs/features/virtual-models.mdx
  • docs/openapi.json
  • internal/admin/handler.go
  • internal/admin/handler_audit.go
  • internal/admin/handler_audit_sessions_test.go
  • internal/admin/handler_test.go
  • internal/admin/handler_virtualmodels.go
  • internal/admin/routes.go
  • internal/admin/routes_test.go
  • internal/app/app.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/middleware.go
  • internal/auditlog/reader.go
  • internal/auditlog/reader_mongodb.go
  • internal/auditlog/reader_sessions_mongodb.go
  • internal/auditlog/reader_sessions_mongodb_test.go
  • internal/auditlog/reader_sessions_sql.go
  • internal/auditlog/reader_sql.go
  • internal/auditlog/session_id_test.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_sql.go
  • internal/auditlog/stream_wrapper.go
  • internal/core/context.go
  • internal/live/broker.go
  • internal/server/http.go
  • internal/server/internal_chat_completion_executor.go
  • internal/server/session.go
  • internal/server/session_test.go
  • internal/session/detect.go
  • internal/session/detect_test.go
  • internal/session/factory.go
  • internal/session/factory_test.go
  • internal/session/session.go
  • internal/virtualmodels/balancer.go
  • internal/virtualmodels/config.go
  • internal/virtualmodels/resolve.go
  • internal/virtualmodels/service.go
  • internal/virtualmodels/snapshot.go
  • internal/virtualmodels/sticky.go
  • internal/virtualmodels/sticky_test.go
  • internal/virtualmodels/store_sql.go
  • internal/virtualmodels/types.go
  • web/dashboard/src/pages/audit-logs/AuditEntryRow.svelte
  • web/dashboard/src/pages/audit-logs/AuditEntrySummary.svelte
  • web/dashboard/src/pages/audit-logs/AuditFilters.svelte
  • web/dashboard/src/pages/audit-logs/AuditLogsPage.svelte
  • web/dashboard/src/pages/audit-logs/AuditThreadGroup.svelte
  • web/dashboard/src/pages/audit-logs/audit-logic.js
  • web/dashboard/src/pages/audit-logs/auditList.svelte.js
  • web/dashboard/src/pages/audit-logs/live-logs-logic.js
  • web/dashboard/src/pages/audit-logs/liveLogs.svelte.js
  • web/dashboard/src/pages/models/VirtualModelEditor.svelte
  • web/dashboard/src/pages/models/virtualModels.svelte.js
  • web/dashboard/src/pages/models/virtualModelsLogic.js
  • web/dashboard/tests/audit-list.test.js
  • web/dashboard/tests/live-logs.test.js

Comment thread internal/auditlog/reader_sessions_sql.go
Comment thread internal/auditlog/session_id_test.go Outdated
Comment thread internal/session/detect.go Outdated
Comment thread web/dashboard/src/pages/audit-logs/auditList.svelte.js
…rage

- Move SessionCapture after authentication so weak/auto session ids are
  scoped by the effective user path (a managed key's bound path), closing a
  cross-tenant thread/pin collision; audit entries pick the id up in the
  post-handler re-read, live previews carry it from the terminal events.
- Scope weak ids by hashing user path and id as distinct values instead of
  ambiguous "path|id" concatenation.
- Drop the thread-children loading placeholder on stale fetches so the
  spinner cannot persist forever.
- Make the session_id index composite with timestamp (SQL and MongoDB) to
  back thread-detail ordering and the grouping query.
- Cover the SessionID filter on grouped reads in both backends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 20:25

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

🤖 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 `@internal/auditlog/reader_sessions_mongodb_test.go`:
- Around line 69-79: The filter scenarios in the GetSessions test should be
table-driven rather than hard-coded separately. Refactor the existing
status-code and SessionID cases into a test-case table with subtests, reusing
the shared setup and assertion flow while preserving each case’s query
parameters and expected results.

In `@internal/auditlog/store_sql.go`:
- Around line 113-115: Rename the composite index declaration in the audit log
schema to a new name, such as idx_audit_session_timestamp, instead of reusing
idx_audit_session_id. Keep the indexed columns ordered as (session_id,
timestamp) so existing deployments create the required composite index.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9e232c87-0275-44db-bee8-657fefe2dbc4

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8fe96 and b907b35.

⛔ Files ignored due to path filters (2)
  • internal/admin/dashboard/static/dist/assets/index-CSnAP_4I.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (10)
  • internal/auditlog/middleware.go
  • internal/auditlog/reader_sessions_mongodb_test.go
  • internal/auditlog/session_id_test.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_sql.go
  • internal/server/http.go
  • internal/server/session.go
  • internal/session/detect.go
  • internal/session/detect_test.go
  • web/dashboard/src/pages/audit-logs/auditList.svelte.js

Comment thread internal/auditlog/reader_sessions_mongodb_test.go Outdated
Comment thread internal/auditlog/store_sql.go Outdated
…rive filter tests

- Retain 128 bits of the scoping/auto hash (sum[:16]) for practical
  collision resistance.
- Recreate the session index as idx_audit_session_timestamp and drop the
  single-column predecessor, so databases that created it get the
  composite (verified against an existing SQLite file).
- Fold the GetSessions filter scenarios into shared table-driven subtests
  run by both the SQL and MongoDB suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 20:34

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…hole-session expansion, outage-safe pinning

Four fixes for session-keeping behavior gaps introduced or exposed by moving
session detection after authentication:

- CreateStreamEntry now takes the request context and stamps the session id
  onto the persisted stream copy — the copy is made mid-handler, before the
  audit middleware's post-handler enrichment, so every streamed request was
  losing its session id.
- The live dashboard re-folds a row into its on-screen thread when a later
  event delivers the session id (audit.started always precedes detection now,
  so rows arrive sessionless and previously stayed separate heads until a
  refetch).
- A session_id audit query without explicit date parameters is now unbounded
  instead of silently applying the default trailing window, so expanding a
  thread shows the whole session even when browsing a historical range.
- Sticky affinity is keyed to the redirect's configured targets rather than
  the currently supported ones: with a single target momentarily available
  (provider outage, startup) the session still pins it, so a recovering
  target cannot move an active conversation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 21:02

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

Follow-up review round addressed in 76171be:

  • [P1] Streamed audits lost session ids — real regression from moving session detection post-auth: the stream copy is created mid-handler, before the middleware's post-handler enrichment. CreateStreamEntry now takes the request context and stamps the id itself, so no call site can miss it (regression test added).
  • [P2] Live rows now re-fold when a later event delivers the session id — necessary since audit.started always precedes detection now; without it, live grouping only converged on refetch.
  • [P2] Whole-session thread expansion — a session_id query without explicit date params was silently getting the default trailing 30-day window; it is now unbounded (explicit dates still apply), matching the documented contract.
  • [P2] Outage-safe pinning — affinity is keyed to configured targets (len(entry.targets) > 1) instead of currently-supported ones, so a session pins its sole viable target during a partial outage and a recovering target can't move an active conversation. The saturated-fallback no-pin rule is unchanged.

🤖 Generated with Claude Code

@mintlify

mintlify Bot commented Jul 27, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟡 Building Jul 27, 2026, 7:54 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/dashboard/src/pages/audit-logs/live-logs-logic.js (1)

127-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared id/request_id matcher.

The fold/regroup logic (lines 138-336) is correct and well-tested. Separately, the id/request_id equality check is duplicated across four sites in this file (mergeLiveAuditEntry, mergeLiveAuditChild, removeLiveAuditThreadChild, removeLiveAuditEntry). A shared helper would keep matching semantics from silently diverging as more thread-grouping call sites are added.

♻️ Proposed helper extraction
+function matchesLiveKey(entry, id, requestID) {
+    return (!!id && String(entry.id || '').trim() === id) ||
+        (!!requestID && String(entry.request_id || '').trim() === requestID);
+}

Then replace each inline check, e.g.:

-            const index = currentEntries.findIndex((entry) => {
-                return String(entry.id || '').trim() === key ||
-                    (incoming.request_id && String(entry.request_id || '').trim() === String(incoming.request_id).trim());
-            });
+            const index = currentEntries.findIndex((entry) => matchesLiveKey(entry, key, String(incoming.request_id || '').trim()));

Also applies to: 218-221, 313-317, 428-431

🤖 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 `@web/dashboard/src/pages/audit-logs/live-logs-logic.js` around lines 127 -
130, Extract the duplicated id/request_id comparison into a shared matcher
helper in live-logs-logic.js, preserving the current trimming, string
conversion, and incoming request_id guard semantics. Replace the inline checks
in mergeLiveAuditEntry, mergeLiveAuditChild, removeLiveAuditThreadChild, and
removeLiveAuditEntry with calls to that helper.
🤖 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.

Outside diff comments:
In `@web/dashboard/src/pages/audit-logs/live-logs-logic.js`:
- Around line 127-130: Extract the duplicated id/request_id comparison into a
shared matcher helper in live-logs-logic.js, preserving the current trimming,
string conversion, and incoming request_id guard semantics. Replace the inline
checks in mergeLiveAuditEntry, mergeLiveAuditChild, removeLiveAuditThreadChild,
and removeLiveAuditEntry with calls to that helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: daaa594f-cbd4-49b8-876b-9d17b308eecc

📥 Commits

Reviewing files that changed from the base of the PR and between 20759c2 and 76171be.

⛔ Files ignored due to path filters (2)
  • internal/admin/dashboard/static/dist/assets/index-DbIJkqkC.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (12)
  • internal/admin/handler_audit.go
  • internal/admin/handler_audit_sessions_test.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/session_id_test.go
  • internal/auditlog/stream_entry_request_fields_test.go
  • internal/auditlog/stream_wrapper.go
  • internal/server/passthrough_support.go
  • internal/server/translated_inference_service.go
  • internal/virtualmodels/balancer.go
  • internal/virtualmodels/sticky_test.go
  • web/dashboard/src/pages/audit-logs/live-logs-logic.js
  • web/dashboard/tests/live-logs.test.js

…ity-complete stream copies, recency-aware re-fold

- Sticky assignment (lookup, strategy choice, pin) now happens in one
  critical section via stickySessions.resolve, so concurrent first requests
  of a session — common for agents firing parallel calls — agree on a single
  target instead of racing to overwrite each other's pins (race test added).
- Session detection materializes chat/responses bodies through the shared
  requestBodyBytes stage when ingress capture skipped them (over 64 KiB or
  chunked), so body signals and content detection work for exactly the large
  agent conversations where affinity matters most; bodies beyond the 1 MiB
  audit bound fall back to header signals.
- CreateStreamEntry finalizes the full context-derived identity through
  EnrichLogEntryWithRequestContext instead of hand-picking the session id,
  which also fixes managed-key labels being absent from streamed audit
  records.
- The live re-fold keeps the newest request as thread head when completions
  arrive out of order, matching what a refresh would show.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 21:31

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

Third review round addressed in fe605c0:

  • [P1] Atomic affinity assignmentstickySessions.resolve now covers lookup, strategy choice, and pin in one critical section, so concurrent first requests of a session agree on one target (16-goroutine race test added; suite passes under -race).
  • [P1] Large/chunked-body detectionSessionCapture runs the shared requestBodyBytes materialization for chat/responses requests whose body ingress capture skipped (>64 KiB or unknown length), so body signals and content detection work for large agent conversations; handlers reuse the same buffered read, and bodies beyond the 1 MiB audit bound fall back to header signals (documented).
  • [P2] Recency-aware re-fold — when completions arrive out of order, the newest request stays thread head (matching what a refresh shows) and the older one becomes the child.
  • Stream-copy identityCreateStreamEntry now finalizes the full context identity via the existing EnrichLogEntryWithRequestContext instead of hand-picking the session id; this also fixes managed-key labels missing from streamed audit records, and there is now exactly one function defining post-auth request metadata used by both streaming and non-streaming paths.

On the architecture suggestions: the atomic-resolve seam matches the proposed AffinityResolver shape without introducing the interface yet (per the repo's no-premature-abstraction rule — it's the natural cut point if Redis-backed affinity ever lands); the shared body-materialization stage now runs before detection as suggested. RequestIdentity, server-authoritative thread events, and scoping UUID ids (with a separate display value) are noted as follow-ups rather than folded into this PR.

🤖 Generated with Claude Code

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

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

Inline comments:
In `@docs/features/session-keeping.mdx`:
- Around line 50-51: Update the body-based signals documentation in
session-keeping to describe the server body_size_limit configuration, including
its default of 10M, rather than stating a fixed 1 MiB limit. Clarify that bodies
exceeding this configured limit are excluded from the captured snapshot used for
body and automatic session detection, while preserving the note about
header-signal fallback.

In `@internal/virtualmodels/balancer.go`:
- Around line 84-91: Restructure the balancing flow around
stickySessions.resolve so strategy selection and choose/pick execution occur
before acquiring the sticky-session lock, keeping catalog reads from
targetCost/LookupModel outside the critical section. Keep only the existing
sticky pin lookup and assignment atomic under the lock, while preserving
candidate validation through poolTarget and the current fallback behavior.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b40cff92-3442-4be2-83b0-c43b13d3d2c6

📥 Commits

Reviewing files that changed from the base of the PR and between 76171be and fe605c0.

⛔ Files ignored due to path filters (2)
  • internal/admin/dashboard/static/dist/assets/index-Z0MNC1F2.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (10)
  • docs/features/session-keeping.mdx
  • internal/auditlog/session_id_test.go
  • internal/auditlog/stream_wrapper.go
  • internal/server/session.go
  • internal/server/session_test.go
  • internal/virtualmodels/balancer.go
  • internal/virtualmodels/sticky.go
  • internal/virtualmodels/sticky_test.go
  • web/dashboard/src/pages/audit-logs/live-logs-logic.js
  • web/dashboard/tests/live-logs.test.js

Comment thread docs/features/session-keeping.mdx
Comment thread internal/virtualmodels/balancer.go
Copilot AI review requested due to automatic review settings July 27, 2026 22:19
Copilot AI review requested due to automatic review settings July 27, 2026 22:30

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

Final review-comment cleanup in 47e3d71: extracted the shared audit id/request_id matcher noted in the outside-diff review, and moved sticky strategy/catalog selection outside the session mutex while preserving atomic first-pin behavior. Full Go tests, targeted race tests, all dashboard tests, Svelte checks, and commit hooks pass.

Copilot AI review requested due to automatic review settings July 27, 2026 22:36

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

@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 `@internal/virtualmodels/balancer.go`:
- Around line 79-105: The saturated-fallback path in
internal/virtualmodels/balancer.go lines 79-105 must return pool[0].selector
directly when saturatedFallback is true, avoiding s.sticky.resolve and
preserving session pins; retain normal strategy selection for non-saturated
routing. In internal/virtualmodels/sticky.go lines 43-61, update lookup so only
expired entries are deleted; treat a live pin that is not currently viable as a
miss without removing it, leaving repinning to the caller.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6327f626-1dcc-4cb9-8030-cc267373b213

📥 Commits

Reviewing files that changed from the base of the PR and between 76171be and 2823773.

⛔ Files ignored due to path filters (3)
  • internal/admin/dashboard/static/dist/assets/index-BDVc7dxb.css is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/assets/index-oXFoUAuu.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (12)
  • docs/features/session-keeping.mdx
  • internal/auditlog/session_id_test.go
  • internal/auditlog/stream_wrapper.go
  • internal/server/session.go
  • internal/server/session_test.go
  • internal/session/detect.go
  • internal/session/detect_test.go
  • internal/virtualmodels/balancer.go
  • internal/virtualmodels/sticky.go
  • internal/virtualmodels/sticky_test.go
  • web/dashboard/src/pages/audit-logs/live-logs-logic.js
  • web/dashboard/tests/live-logs.test.js

Comment thread internal/virtualmodels/balancer.go Outdated
Copilot AI review requested due to automatic review settings July 28, 2026 07:40

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/dashboard/src/pages/audit-logs/live-logs-logic.js (2)

187-191: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Propagate late usage updates to thread children.

A demoted audit entry can now be merged as a child here, but applyLiveUsageToAudit only searches auditLog.entries. If its usage event arrives afterward, the child never receives usage enrichment. Search and update auditThreadChildren as well.

🤖 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 `@web/dashboard/src/pages/audit-logs/live-logs-logic.js` around lines 187 -
191, Update the live usage handling around applyLiveUsageToAudit so usage events
also search auditThreadChildren, not only auditLog.entries. Ensure matching
child entries receive the same usage enrichment, including children created
through mergeLiveAuditChild, while preserving the existing top-level entry
behavior.

437-443: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve a session thread when its head is removed.

At Line 437, removing a grouped head with session_count > 1 drops the only visible thread row while its remaining children stay cached but unreachable. Promote a loaded child or reload the grouped list; decrement the thread count instead of deleting the thread outright.

🤖 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 `@web/dashboard/src/pages/audit-logs/live-logs-logic.js` around lines 437 -
443, Update the removal logic around matchesLiveAuditKey and
removeLiveAuditThreadChild so deleting a grouped session head with session_count
> 1 preserves the visible thread: promote an already-loaded child or reload the
grouped list, decrementing the thread count rather than removing the session
entirely. Keep the current full-removal behavior when no child remains.
🤖 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.

Outside diff comments:
In `@web/dashboard/src/pages/audit-logs/live-logs-logic.js`:
- Around line 187-191: Update the live usage handling around
applyLiveUsageToAudit so usage events also search auditThreadChildren, not only
auditLog.entries. Ensure matching child entries receive the same usage
enrichment, including children created through mergeLiveAuditChild, while
preserving the existing top-level entry behavior.
- Around line 437-443: Update the removal logic around matchesLiveAuditKey and
removeLiveAuditThreadChild so deleting a grouped session head with session_count
> 1 preserves the visible thread: promote an already-loaded child or reload the
grouped list, decrementing the thread count rather than removing the session
entirely. Keep the current full-removal behavior when no child remains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 21ac7bf4-762f-4e46-8195-a30c09177cfd

📥 Commits

Reviewing files that changed from the base of the PR and between 2823773 and 0f9a5f2.

⛔ Files ignored due to path filters (2)
  • internal/admin/dashboard/static/dist/assets/index-UYgiC4ZC.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (16)
  • cmd/gomodel/docs/docs.go
  • config/config.example.yaml
  • config/config.go
  • docs/openapi.json
  • internal/admin/handler.go
  • internal/admin/handler_audit.go
  • internal/admin/handler_audit_sessions_test.go
  • internal/admin/handler_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/reader.go
  • internal/auditlog/store_sql.go
  • internal/server/http.go
  • internal/virtualmodels/balancer.go
  • internal/virtualmodels/sticky.go
  • internal/virtualmodels/sticky_test.go
  • web/dashboard/src/pages/audit-logs/live-logs-logic.js

Copilot AI review requested due to automatic review settings July 28, 2026 09:24

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@SantiagoDePolonia
SantiagoDePolonia merged commit 953d600 into main Jul 28, 2026
18 of 19 checks passed

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/dashboard/src/pages/audit-logs/live-logs-logic.js (1)

143-157: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

audit.detail fallback insert skips session folding, unlike the general branch.

The general audit.* path explicitly tries foldLiveAuditIntoThread before falling back to a plain insert when auditGroupSessions is on (lines 193-201), but the audit.detail branch's fallback (154-157) does not. If a detail event arrives for a request that's no longer in auditLog.entries (e.g. scrolled past the auditLog.limit window) nor in any loaded auditThreadChildren list, while another head for the same session_id is still on-screen, this path inserts a second head for that session and double-increments auditLog.total, breaking the single-head-per-session invariant the rest of this file maintains.

🐛 Proposed fix: mirror the general branch's fold-before-insert step
                 const child = this.mergeLiveAuditChild(incoming, patch);
                 if (child) {
                     this.notifyLiveConversation(child);
                     return child;
                 }
                 if (!this.auditLiveInsertAllowed()) return;
+                if (this.auditGroupSessions) {
+                    const folded = this.foldLiveAuditIntoThread(patch);
+                    if (folded) {
+                        this.notifyLiveConversation(folded);
+                        return folded;
+                    }
+                }
                 this.auditLog.entries = [this.mergeLiveAuditUsagePatch(patch), ...currentEntries].slice(0, this.auditLog.limit || 25);
                 this.auditLog.total = Number(this.auditLog.total || 0) + 1;
                 return this.auditLog.entries[0];
🤖 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 `@web/dashboard/src/pages/audit-logs/live-logs-logic.js` around lines 143 -
157, Update the audit.detail fallback in the live audit merge flow to attempt
session folding via foldLiveAuditIntoThread before inserting a new entry,
matching the general audit.* branch when auditGroupSessions is enabled. Use the
folded result when available; otherwise preserve the existing insert and
total-increment behavior, maintaining one head per session.
🤖 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 `@web/dashboard/tests/live-logs.test.js`:
- Around line 967-978: Add a grouped-mode regression test for an audit.detail
event whose request is absent from auditLog.entries and auditThreadChildren
while another head with the same session_id is visible. Assert the
fallback-inserted entry folds into the existing thread rather than creating a
second head, preserving the existing head’s session_id and grouped structure.

---

Outside diff comments:
In `@web/dashboard/src/pages/audit-logs/live-logs-logic.js`:
- Around line 143-157: Update the audit.detail fallback in the live audit merge
flow to attempt session folding via foldLiveAuditIntoThread before inserting a
new entry, matching the general audit.* branch when auditGroupSessions is
enabled. Use the folded result when available; otherwise preserve the existing
insert and total-increment behavior, maintaining one head per session.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eb9219aa-c88d-480f-bcc7-c9880d0852f0

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9a5f2 and 3f00d6b.

⛔ Files ignored due to path filters (2)
  • internal/admin/dashboard/static/dist/assets/index-Gd_0GZP5.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (2)
  • web/dashboard/src/pages/audit-logs/live-logs-logic.js
  • web/dashboard/tests/live-logs.test.js

Comment on lines +967 to +978
test("audit.removed reloads an unexpanded grouped thread when its head disappears", () => {
const app = createLiveLogsApp({ auditGroupSessions: true });
app.auditLog.entries = [{ id: "head", session_id: "s-a", session_count: 3 }];
app.auditLog.total = 1;

app.removeLiveAuditEntry({ id: "head" });

assert.deepEqual(app.auditLog.entries, []);
assert.equal(app.auditLog.total, 1);
assert.equal(app.fetchAuditCalls, 1);
});

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 | 🔵 Trivial | ⚡ Quick win

Add regression coverage for audit.detail fallback-insert folding.

None of the grouped-mode tests cover an audit.detail event arriving for a request absent from both auditLog.entries and auditThreadChildren while another head for the same session_id is on-screen — the scenario affected by the fold-skip gap flagged in live-logs-logic.js (lines 143-157). Consider adding a case similar to this test that asserts the entry folds into the existing thread instead of creating a second head with session_id: "s-a".

🤖 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 `@web/dashboard/tests/live-logs.test.js` around lines 967 - 978, Add a
grouped-mode regression test for an audit.detail event whose request is absent
from auditLog.entries and auditThreadChildren while another head with the same
session_id is visible. Assert the fallback-inserted entry folds into the
existing thread rather than creating a second head, preserving the existing
head’s session_id and grouped structure.

SantiagoDePolonia added a commit that referenced this pull request Jul 29, 2026
…615)

* fix(auditlog): cast audit thread key to text for legacy uuid schemas

PostgreSQL databases created before the unified schema (#586) keep their
original uuid audit_logs.id column - CREATE TABLE IF NOT EXISTS never
retypes it. The session thread key added in #602 coalesces id with the
text session_id column, so every GET /admin/audit/sessions call on such
a database failed with SQLSTATE 42804 (COALESCE types text and uuid
cannot be matched), leaving the Audit Logs page empty in its default
group-by-session view.

Cast the id to text inside the thread key; the cast is valid on both
backends and a no-op on SQLite. Covered by a regression test that
recreates the exact pre-unification PostgreSQL DDL and runs GetSessions
against it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(auditlog): pin per-thread latest entry in legacy-schema test

Table-drive the session summaries and assert Latest.ID for the grouped
thread too, so a regression in the per-thread recency ranking on the
uuid id column cannot slip past the count check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants