Skip to content

feat(client): rewrite as a unified, session-scoped rssCloud/WebSub test harness#39

Merged
andrewshell merged 3 commits into
mainfrom
feat/client-session-harness
Jul 1, 2026
Merged

feat(client): rewrite as a unified, session-scoped rssCloud/WebSub test harness#39
andrewshell merged 3 commits into
mainfrom
feat/client-session-harness

Conversation

@andrewshell

@andrewshell andrewshell commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces the two-box, dev-only UI in apps/client with a unified control (rssCloud REST/XML-RPC/WebSub), async JSON actions under /s/:sessionId/actions/* (no page navigation), feed-URL discovery, and a live combined incoming+outgoing traffic log via @andrewshell/socklog over a per-session WebSocket.
  • Makes the harness safe to run as a public utility instead of a local-only dev tool: session-scoped state (no cross-user leakage), an SSRF-guarded outbound fetch reusing @rsscloud/core's createSafeFetch, idle-404 + GC eviction for abandoned sessions (suspended while a live socklog connection is attached, so an overnight-monitoring tab never goes dark), and structural enforcement that /actions/ping//actions/publish can only ever target this harness's own feed.
  • Adds dotenv + Docker support (apps/client/Dockerfile, .env.example, a paired service in examples/dockge/compose.yaml).
  • Reuses apps/server's stylesheet; the control box is laid out as labeled fieldsets, and the socklog panel grows with content instead of a fixed height.

Test plan

  • pnpm --filter @rsscloud/client-app run test — 73 passing
  • pnpm run lint / pnpm run test:unit / pnpm run typecheck — clean across all 6 workspace packages
  • Manual verification in a real browser against a real running hub: session redirect, protocol switching, Subscribe/Ping/Publish/Discover, subscriber-mode button hiding, live traffic log
  • docker build + docker run smoke test of apps/client/Dockerfile
  • Verified idle-404/GC-eviction suspension while a socklog socket is connected, via a real WebSocket client

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Added a session-based client test harness with per-session protocol controls, WebSocket live log streaming, and session-scoped callbacks.
    • Introduced feed discovery helpers to detect RSSCloud and WebSub support from a feed URL.
  • Bug Fixes
    • Improved session lifecycle behavior (idle expiry, garbage collection, and isolation) and hardened request verification flows.
    • Strengthened outbound request security with SSRF protections and configurable CIDR allow-listing.
  • Documentation
    • Refreshed the client README, environment template, and Docker/Compose run guidance.
  • Tests
    • Expanded automated coverage for discovery, SSRF guarding, sessions, and protocol actions.
  • Chores
    • Updated linting for browser code and refined docker build context handling.

…st harness

Replaces the two-box, dev-only UI with one unified control (rssCloud
REST/XML-RPC/WebSub), async JSON actions under /s/:sessionId/actions/*
(no page navigation), feed-URL discovery, and a live combined
incoming+outgoing traffic log via @andrewshell/socklog over a
per-session WebSocket. Enables testing live/third-party hubs, not just
a local one.

Because it's now meant to run as a public utility rather than a local
dev-only tool:
- every outbound call goes through an SSRF-guarded fetch (reusing
  @rsscloud/core's createSafeFetch), refusing loopback/private
  targets by default
- state is session-scoped (Map<sessionId, SessionState>) instead of
  global, so concurrent users' logs/feeds/subscriptions never cross
- callback/feed routes 404 once a session's been idle past
  SESSION_CALLBACK_IDLE_MS, and idle sessions are fully evicted after
  SESSION_GC_IDLE_MS - both suspended while a live socklog connection
  is attached, so a page left open overnight monitoring a feed never
  goes dark or gets GC'd
- /actions/ping and /actions/publish structurally accept only a
  feedName, never a feedUrl, so this harness can never be made to
  ping/publish a feed it doesn't itself serve
- adds dotenv + Docker support (apps/client/Dockerfile, .env.example,
  a paired service in examples/dockge/compose.yaml)

The UI reuses apps/server's stylesheet and the control box is laid
out as labeled fieldsets; the socklog panel has no fixed height and
grows with content, matching apps/server's own log page.

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

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 24f6eafd-cb61-4a37-aafa-1b5fcbf40cb6

📥 Commits

Reviewing files that changed from the base of the PR and between 0905561 and 46f6042.

📒 Files selected for processing (3)
  • apps/client/lib/session-store.js
  • apps/client/lib/session-store.test.js
  • eslint.config.js

📝 Walkthrough

Walkthrough

The client app is refactored into a session-scoped test harness with external config, guarded outbound fetch, feed discovery helpers, per-session state and log streaming, session-routed protocol actions, browser UI updates, and matching Docker, compose, and README changes.

Changes

Client Test Harness Refactor

Layer / File(s) Summary
Configuration and entrypoint
apps/client/.env.example, apps/client/config.js, apps/client/index.js, apps/client/package.json, apps/client/config.test.js
Adds environment-driven client config, a new Node entrypoint, updated package scripts, and validation for numeric config parsing.
Guarded fetch and discovery
apps/client/lib/guarded-fetch.js, apps/client/lib/discover.js, apps/client/lib/index.js, apps/client/lib/*.test.js
Adds SSRF-guarded fetch plus feed discovery helpers, exports them from lib/index.js, and tests XML parsing, fetch handling, and loopback blocking/allow-listing.
Session store and sockets
apps/client/lib/session-store.js, apps/client/session-sockets.js, apps/client/lib/*.test.js, apps/client/session-sockets.test.js
Adds per-session idle tracking, request logs, socket sets, log replay/broadcast behavior, and tests for lifecycle, sweep, and WebSocket streaming.
Session routes and actions
apps/client/client.js, apps/client/client.hub-config.test.js, apps/client/client.test.js
Refactors the app into a session-scoped factory with session-gated routes, WebSub/rssCloud actions, outbound logging, and RSS/cloud config rendering.
Client UI
apps/client/public/app.js, apps/client/public/css/style.css
Adds browser-side action posting, protocol toggles, discovery-driven overrides, subscriber-mode handling, and page styling.
Docker and docs
apps/client/Dockerfile, examples/dockge/compose.yaml, .dockerignore, apps/client/README.md
Updates the client build/runtime image, compose example, Docker ignore rules, and harness documentation.

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

Poem

A rabbit hops through sessions bright,
With logs that stream and fetches light. 🐇
Feeds are found, and hubs reply,
While sockets hum and pages fly.
The warren’s tidy, quick, and new—
Hop on through, the code is true.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a client rewrite into a unified, session-scoped rssCloud/WebSub test harness.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/client-session-harness

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (4)
apps/client/session-sockets.js (1)

39-56: 📐 Maintainability & Code Quality | 🔵 Trivial

Log truncation logic lives outside the module that owns the data structure.

broadcast() directly mutates session.requestLog (unshift/pop with MAX_LOG_ENTRIES), even though the array and its lifecycle are otherwise fully owned by session-store.js. Any future caller that needs to append to the log without going through broadcast (or a change to the cap) has to know about this hidden coupling. Consider exposing an appendLog(id, entry) method on the session store that owns the truncation policy, and have broadcast call it.

wss.on('connection', ...) is the canonical way listeners are notified after handleUpgrade

🤖 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 `@apps/client/session-sockets.js` around lines 39 - 56, Move the request log
mutation and truncation policy out of broadcast() and into the session store
that owns the session state. Add an appendLog(sessionId, entry) method on the
session-store.js API to handle unshift/pop and MAX_LOG_ENTRIES management, then
update broadcast() to call that method before sending the websocket message.
Keep the existing ws.send flow in broadcast(), but remove direct
session.requestLog manipulation so log lifecycle is centralized and reusable.
apps/client/lib/discover.js (1)

45-48: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Hardcoded atom:link prefix misses non-standard namespace aliases.

Detection of WebSub hub links in RSS relies on the literal tag name atom:link, which only matches feeds using exactly that namespace prefix. Feeds declaring the Atom namespace under a different prefix (e.g. <a:link rel="hub">) will silently report webSub: null. Low risk for a discovery/UX helper, but worth a comment noting the assumption.

🤖 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 `@apps/client/lib/discover.js` around lines 45 - 48, The WebSub hub lookup in
discover.js is hardcoded to the atom:link prefix, so feeds using a different
namespace alias will be missed. Update the hub-link detection around
channel?.['atom:link'] and findHubLink to either handle namespace aliases more
generically or add an explicit comment documenting that this helper only
supports the atom:link prefix assumption.
apps/client/public/app.js (1)

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

postAction failures are silently swallowed.

None of the click handlers catch rejections from postAction. A network error, timeout, or non-JSON error response (res.json() throwing on an HTML error page) results in an unhandled promise rejection with zero user-visible feedback — the button just appears to do nothing.

♻️ Proposed fix
 async function postAction(action, fields) {
-    const res = await fetch(`/s/${sessionId}/actions/${action}`, {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify(fields)
-    });
-    return res.json();
+    try {
+        const res = await fetch(`/s/${sessionId}/actions/${action}`, {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify(fields)
+        });
+        return await res.json();
+    } catch (err) {
+        console.error(`Action "${action}" failed:`, err);
+        return { error: err.message };
+    }
 }

Also applies to: 81-130

🤖 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 `@apps/client/public/app.js` around lines 21 - 28, The postAction flow
currently lets fetch/res.json failures escape, so click handlers end up with
unhandled rejections and no user feedback. Update postAction to catch
network/parse errors and surface a clear failure state or message, then make the
relevant click handlers that call postAction (including the ones in the affected
action handlers) handle rejected promises and notify the user instead of
silently doing nothing.
apps/client/client.test.js (1)

7-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test that actions refresh callback liveness.

These tests only age a session from creation time. Add a case that advances time, performs an outbound action, advances less than sessionCallbackIdleMs, and verifies /notify still returns 200; that would catch missing touchOutgoing() wiring.

🤖 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 `@apps/client/client.test.js` around lines 7 - 25, Add a regression test in
client.test.js to cover callback liveness refresh after outbound activity: after
creating the session with createApp/createSessionStore, advance currentTime,
perform an outbound action that should invoke touchOutgoing(), then advance time
by less than sessionCallbackIdleMs and verify GET /s/:id/notify still returns
200. Use the existing helpers and route patterns around createSessionStore,
createApp, and the /s/idle-session/notify flow so this catches missing
touchOutgoing() wiring.
🤖 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 `@apps/client/client.js`:
- Around line 447-452: The WebSub session state update in the
subscribe/unsubscribe flow is happening too early, before the hub request
finishes successfully. In the `subscribe()` and `unsubscribe()` callback logic
inside `client.js`, move the `req.session.webSubSecrets[feedUrl]` set/delete so
it only runs after the outbound hub call succeeds, and leave the session
unchanged if the call throws. Use the existing `secret`, `feedUrl`, and
`req.session.webSubSecrets` handling in those branches to locate and adjust the
mutation points.
- Around line 357-370: Refresh the session idle clock in outbound action
handlers so `requireLiveSession` does not treat active sessions as idle. Update
the `/actions/discover` route and the other outbound request paths to call
`sessionStore.touchOutgoing()` when sending a `direction: 'outgoing', phase:
'request'` event, and prefer `broadcastOutgoingRequest(...)` for those entries
so the touch happens consistently.
- Around line 711-722: The rssCloud feed generation in renderCloudFeed is
hard-coding the cloud endpoint to localhost:5337, which causes deployments to
advertise the wrong hub. Update the cloud config passed from client.js to derive
domain, port, and path from the configured hub URL (config.hubServerUrl /
hubUrl) instead of fixed values, while keeping registerProcedure and protocol
unchanged. Use the existing feed setup near renderCloudFeed and the hubUrl value
as the source of truth so the emitted <cloud> element matches the configured
rssCloud hub.
- Around line 443-447: The WebSub subscribe request logging in logAndRespond
currently records the user-provided secret in the live traffic log and
session.requestLog. Update the logging path around websub-subscribe so the
actual hub request payload still includes secret, but the logged/redacted body
replaces that field before it is persisted or displayed. Use the existing
logAndRespond flow and the websub-subscribe call site to keep the network
request unchanged while redacting only the logged entry.

In `@apps/client/config.js`:
- Around line 8-12: The getNumericConfig helper currently returns NaN for
malformed numeric env vars, which can silently break downstream uses like
setInterval, app.listen, and timeout checks. Update getNumericConfig to validate
the parsed value and fail loudly when process.env[key] is present but not a
valid number, rather than returning NaN; keep the defaultValue behavior only
when the env var is unset. Use the existing getNumericConfig function in
config.js to centralize this validation.

In `@apps/client/Dockerfile`:
- Around line 24-38: The runtime stage in the client Dockerfile still starts as
root; update the final image setup to switch to a dedicated non-root user before
the CMD runs. Add or reuse an unprivileged user in the runtime stage, make sure
the copied app files are readable/executable by that user, and set the active
user for the final image near the runtime WORKDIR/EXPOSE/CMD block so the
container launches under that account.

In `@examples/dockge/compose.yaml`:
- Around line 56-81: The CLIENT_FETCH_ALLOW_CIDRS setting in the client Compose
service is too broad and weakens the SSRF guard by allowing the full
172.16.0.0/12 private range. Narrow the allowlist in the client service so it
only covers the specific Docker subnet needed to reach the sibling rsscloud
container, and keep the rest of the compose environment unchanged; update the
client block in compose.yaml accordingly.

---

Nitpick comments:
In `@apps/client/client.test.js`:
- Around line 7-25: Add a regression test in client.test.js to cover callback
liveness refresh after outbound activity: after creating the session with
createApp/createSessionStore, advance currentTime, perform an outbound action
that should invoke touchOutgoing(), then advance time by less than
sessionCallbackIdleMs and verify GET /s/:id/notify still returns 200. Use the
existing helpers and route patterns around createSessionStore, createApp, and
the /s/idle-session/notify flow so this catches missing touchOutgoing() wiring.

In `@apps/client/lib/discover.js`:
- Around line 45-48: The WebSub hub lookup in discover.js is hardcoded to the
atom:link prefix, so feeds using a different namespace alias will be missed.
Update the hub-link detection around channel?.['atom:link'] and findHubLink to
either handle namespace aliases more generically or add an explicit comment
documenting that this helper only supports the atom:link prefix assumption.

In `@apps/client/public/app.js`:
- Around line 21-28: The postAction flow currently lets fetch/res.json failures
escape, so click handlers end up with unhandled rejections and no user feedback.
Update postAction to catch network/parse errors and surface a clear failure
state or message, then make the relevant click handlers that call postAction
(including the ones in the affected action handlers) handle rejected promises
and notify the user instead of silently doing nothing.

In `@apps/client/session-sockets.js`:
- Around line 39-56: Move the request log mutation and truncation policy out of
broadcast() and into the session store that owns the session state. Add an
appendLog(sessionId, entry) method on the session-store.js API to handle
unshift/pop and MAX_LOG_ENTRIES management, then update broadcast() to call that
method before sending the websocket message. Keep the existing ws.send flow in
broadcast(), but remove direct session.requestLog manipulation so log lifecycle
is centralized and reusable.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 020b507c-1575-4e72-95d5-9543abaf2c05

📥 Commits

Reviewing files that changed from the base of the PR and between 66eb163 and 5d77296.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • apps/client/.env.example
  • apps/client/Dockerfile
  • apps/client/README.md
  • apps/client/client.js
  • apps/client/client.test.js
  • apps/client/config.js
  • apps/client/index.js
  • apps/client/lib/discover.js
  • apps/client/lib/discover.test.js
  • apps/client/lib/guarded-fetch.js
  • apps/client/lib/guarded-fetch.test.js
  • apps/client/lib/index.js
  • apps/client/lib/session-store.js
  • apps/client/lib/session-store.test.js
  • apps/client/package.json
  • apps/client/public/app.js
  • apps/client/public/css/style.css
  • apps/client/session-sockets.js
  • apps/client/session-sockets.test.js
  • examples/dockge/compose.yaml

Comment thread apps/client/client.js
Comment thread apps/client/client.js
Comment thread apps/client/client.js
Comment thread apps/client/client.js
Comment thread apps/client/config.js Outdated
Comment thread apps/client/Dockerfile
Comment thread examples/dockge/compose.yaml
- webSubSecrets is only mutated after the hub subscribe/unsubscribe
  call actually succeeds, not before — a failed call no longer stores
  a secret for a subscription that was never accepted, or drops a
  still-valid one
- wire sessionStore.touchOutgoing() into every outbound action via a
  new broadcastOutgoingRequest() helper — it was never being called,
  so a session's idle clock never advanced past creation regardless of
  activity, silently defeating the "active session stays live" intent
- derive the served test feed's <cloud> domain/port from
  config.hubServerUrl instead of hard-coded localhost:5337, so a
  deployment against any other hub advertises the right one
- redact the WebSub secret in the broadcast/logged copy of a subscribe
  request; the real value still reaches the hub unchanged
- config.js's getNumericConfig now fails loudly on a malformed (but
  present) numeric env var instead of silently producing NaN
- apps/client/Dockerfile now runs as the non-root `node` user
- narrow examples/dockge/compose.yaml's CLIENT_FETCH_ALLOW_CIDRS from
  Docker's whole default /12 range to a dedicated /24 subnet
- fix .dockerignore's `.env` pattern (found while validating the Dockerfile
  change) to `**/.env` — the bare form only matches at the build-context
  root, not at any depth like .gitignore, so a real local .env could be
  baked into an image built from either app's Dockerfile
- lib/session-store.js: add appendLog() so session-sockets.js's
  broadcast() no longer reaches into session.requestLog directly
- lib/discover.js: document the atom: namespace-prefix assumption in
  hub-link detection
- public/app.js: postAction() catches network/parse failures and
  surfaces them instead of leaving an unhandled rejection

Verified with new/updated tests (82 passing), a real docker build+run
(confirmed non-root, confirmed .env no longer present in the image),
and a live browser pass confirming the secret redaction end-to-end.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/client/lib/session-store.js (1)

47-75: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Sessions with a live socket can never be evicted — unbounded memory growth risk.

isIdle and sweep both treat any session with sockets.size > 0 as permanently active, with no fallback hard TTL. For a public-facing harness, a client that opens (and keeps open) WebSocket connections to many session IDs can pin those sessions in memory indefinitely — sweep(sessionGcIdleMs) will never reclaim them, regardless of createdAt. This is an available DoS vector for memory exhaustion now that the harness is meant for public use rather than local-only dev.

Consider adding a hard ceiling independent of socket state (e.g. evict regardless of live sockets once now() - session.createdAt exceeds some absolute max age), and/or a cap on total tracked sessions.

💡 Illustrative approach: hard age ceiling overriding the socket exemption
-    function isIdle(id, idleMs) {
+    function isIdle(id, idleMs, hardMaxAgeMs) {
         const session = sessions.get(id);
         if (!session) {
             return true;
         }
+        if (hardMaxAgeMs && now() - session.createdAt > hardMaxAgeMs) {
+            return true;
+        }
         if (session.sockets.size > 0) {
             return false;
         }
         return now() - session.lastOutgoingAt > idleMs;
     }

-    function sweep(maxIdleMs) {
+    function sweep(maxIdleMs, hardMaxAgeMs) {
         let evicted = 0;
         for (const [id, session] of sessions) {
-            if (session.sockets.size > 0) {
+            const hardExpired = hardMaxAgeMs && now() - session.createdAt > hardMaxAgeMs;
+            if (session.sockets.size > 0 && !hardExpired) {
                 continue;
             }
-            if (now() - session.lastOutgoingAt > maxIdleMs) {
+            if (hardExpired || now() - session.lastOutgoingAt > maxIdleMs) {
                 sessions.delete(id);
                 evicted += 1;
             }
         }
         return evicted;
     }
🤖 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 `@apps/client/lib/session-store.js` around lines 47 - 75, The session eviction
logic in isIdle and sweep currently exempts any session with sockets.size > 0
forever, which can pin sessions in memory indefinitely. Update the session GC
path to enforce an absolute max age using session.createdAt so old sessions can
be evicted even if sockets remain open, and consider adding a global session cap
if needed. Keep the change localized to the isIdle/sweep logic in
session-store.js and preserve the existing idle-based behavior for normally
inactive sessions.
🤖 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 `@apps/client/public/app.js`:
- Line 35: The `alert` call in `app.js` is being flagged by ESLint because
browser globals are not enabled. Update the client ESLint configuration to
include browser environment globals such as `window`, `alert`, and `fetch` (or
otherwise declare the browser env) so `no-undef` no longer reports `alert` as
undefined.

---

Outside diff comments:
In `@apps/client/lib/session-store.js`:
- Around line 47-75: The session eviction logic in isIdle and sweep currently
exempts any session with sockets.size > 0 forever, which can pin sessions in
memory indefinitely. Update the session GC path to enforce an absolute max age
using session.createdAt so old sessions can be evicted even if sockets remain
open, and consider adding a global session cap if needed. Keep the change
localized to the isIdle/sweep logic in session-store.js and preserve the
existing idle-based behavior for normally inactive sessions.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 3ac3f6cb-86d6-4273-94ee-92e4cd1cc146

📥 Commits

Reviewing files that changed from the base of the PR and between 5d77296 and 0905561.

📒 Files selected for processing (13)
  • .dockerignore
  • apps/client/Dockerfile
  • apps/client/client.hub-config.test.js
  • apps/client/client.js
  • apps/client/client.test.js
  • apps/client/config.js
  • apps/client/config.test.js
  • apps/client/lib/discover.js
  • apps/client/lib/session-store.js
  • apps/client/lib/session-store.test.js
  • apps/client/public/app.js
  • apps/client/session-sockets.js
  • examples/dockge/compose.yaml
🚧 Files skipped from review as they are similar to previous changes (6)
  • examples/dockge/compose.yaml
  • apps/client/config.js
  • apps/client/Dockerfile
  • apps/client/lib/discover.js
  • apps/client/client.js
  • apps/client/client.test.js

Comment thread apps/client/public/app.js
…globals

- lib/session-store.js: isIdle/sweep exempted any session with a live
  socket from idle/GC checks unconditionally, with no ceiling — on a
  public deployment, simply holding the socklog WebSocket open forever
  would pin that session in memory indefinitely. Added a
  MAX_SESSION_AGE_MS (7 days) ceiling tied to session.createdAt; past
  that, a held-open socket falls back to the normal lastOutgoingAt
  idle rules instead of exempting the session outright, and sweep()
  once again terminates any lingering socket on eviction.
- eslint.config.js: scope browser globals (window, document, alert,
  etc.) to apps/client/public/**/*.js instead of leaving the shared
  Node-context config to flag them — an editor's live ESLint
  integration applies the one repo-wide config regardless of what the
  npm lint script's glob covers, so this was real editor noise on
  public/app.js even though CI wasn't affected.

Verified with new/updated tests (85 passing in apps/client) and a
forced (non-cached) full monorepo lint/test/typecheck run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@andrewshell andrewshell merged commit 8b7eb73 into main Jul 1, 2026
5 checks passed
@andrewshell andrewshell deleted the feat/client-session-harness branch July 1, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant