Skip to content

fix: viewer model dropdown, SSE cleanup, OpenRouter URL, zh-TW mode, alias fix, restart guard, FTS5 fallback, httpcore dep#2069

Closed
thedotmack wants to merge 2 commits intomainfrom
bugfix/viewer-misc-unlabeled-bugs
Closed

fix: viewer model dropdown, SSE cleanup, OpenRouter URL, zh-TW mode, alias fix, restart guard, FTS5 fallback, httpcore dep#2069
thedotmack wants to merge 2 commits intomainfrom
bugfix/viewer-misc-unlabeled-bugs

Conversation

@thedotmack
Copy link
Copy Markdown
Owner

Summary

Test plan

  • Verify viewer shows correct model and preserves precise ID on save
  • Verify SSE clients are cleaned up on disconnect
  • Verify OpenRouter URL is configurable
  • Verify zh-TW mode produces Traditional Chinese
  • Verify claude-mem alias uses current version after upgrade
  • Verify restart guard uses time-windowed counting
  • Verify text search falls back to FTS5 when Chroma disabled
  • Verify chroma-mcp has httpcore dependency

Closes #1958, closes #1959, closes #1960, closes #1961, closes #2054, closes #2053, closes #2048, closes #2046

🤖 Generated with Claude Code

…alias fix, restart guard, FTS5 fallback, httpcore dep

- #1958: Map precise model IDs in viewer settings dropdown, preserve on save
- #1959: Clean up dead SSE clients on disconnect/error, periodic 30s sweep
- #1960: Make OpenRouter base URL configurable via CLAUDE_MEM_OPENROUTER_BASE_URL
- #1961: Add Traditional Chinese (zh-TW) mode file with proper translations
- #2054: Use version-agnostic alias path, always update existing aliases
- #2053: Replace flat restart counter with time-windowed counter (60s window, cap 10, 5m decay)
- #2048: Fall back to FTS5 full-text search when Chroma disabled for text queries
- #2046: Add httpcore and httpx to chroma-mcp uvx dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Warning

Rate limit exceeded

@thedotmack has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 3 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 3 minutes and 3 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: be126ca7-5719-4851-97fd-a062e17efea8

📥 Commits

Reviewing files that changed from the base of the PR and between beea789 and b9f6046.

📒 Files selected for processing (12)
  • plugin/modes/code--zh-tw.json
  • plugin/scripts/smart-install.js
  • src/services/sync/ChromaMcpManager.ts
  • src/services/worker-service.ts
  • src/services/worker-types.ts
  • src/services/worker/OpenRouterAgent.ts
  • src/services/worker/RestartGuard.ts
  • src/services/worker/SSEBroadcaster.ts
  • src/services/worker/SearchManager.ts
  • src/services/worker/SessionManager.ts
  • src/services/worker/http/routes/SessionRoutes.ts
  • src/ui/viewer/components/ContextSettingsModal.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/viewer-misc-unlabeled-bugs

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

@claude
Copy link
Copy Markdown

claude Bot commented Apr 18, 2026

Code Review

This PR bundles 8 bug fixes. Overall the approach is solid, but there are a few issues worth addressing before merge.


Overview

  • RestartGuard replaces a fragile flat counter with a time-windowed approach — good design
  • SSEBroadcaster adds proactive dead-client cleanup — correct and well-structured
  • FTS5 fallback when Chroma is disabled is a meaningful UX improvement
  • OpenRouter URL and zh-TW mode are clean, minimal additions
  • The viewer model dropdown and alias path fixes are needed but have bugs noted below

Issues

🔴 Bug — ContextSettingsModal.tsx: Custom model option corrupts settings

The custom model <option> has value="custom", not the actual model ID:

return <option value="custom">custom: {model}</option>;

If a user with a custom model ID (e.g. my-custom-model) triggers onChange, it saves the string "custom" to CLAUDE_MEM_MODEL, permanently overwriting their real model ID. The value should be the model ID itself:

return <option value={model}>custom: {model}</option>;

🟡 Dead code — RestartGuard.ts: applyRestartDecay is never called

applyRestartDecay and RESTART_DECAY_MS are defined but wired up nowhere. Either hook it into the worker's processing loop or remove it — as-is it's misleading documentation that suggests decay is active when it isn't.

🟡 Dynamic imports in hot path — worker-service.ts / SessionRoutes.ts

const { recordRestartAndCheckAllowed, resetRestartCounter } = await import('./worker/RestartGuard.js');

These imports are inside functions called on every session completion/crash. Bun/Node caches them after the first load so there's no real perf hit, but the pattern is unconventional for a module that's always needed. A top-level import would be cleaner and immediately shows the dependency.

🟡 SSEBroadcaster.dispose() not wired to shutdown

The new dispose() method is well-written but nothing in this PR calls it. If WorkerService doesn't call it on SIGTERM/SIGINT, the setInterval will prevent clean shutdown and accumulate on restarts. Worth verifying the shutdown path calls this.sseBroadcaster.dispose().

🟡 Shell alias quoting in smart-install.js

The generated alias embeds $() subshell expansion inside single quotes:

alias claude-mem='bun "$(ls -d ~/.claude/plugins/.../worker-service.cjs ... | sort -V | tail -1 || echo "...")"'

This works in bash (single quotes in an alias body are re-evaluated on execution), but it's fragile — zsh and fish behave differently, and the nested double-quoting inside $() is hard to read. A named function in .bashrc/.zshrc would be more portable and easier to update:

function claude-mem() { bun "$(ls ...)" "$@"; }

Minor Notes

  • ChromaMcpManager.ts: httpx is added alongside httpcore but isn't mentioned in the PR description or issue [BUG] chroma-mcp: No module named 'httpcore' on all query/add operations #2046. Intentional? Worth noting.
  • ContextSettingsModal.tsx: The knownModels array is duplicated in two IIFEs. Extracting it to a const above the JSX would reduce duplication and improve readability.
  • RestartGuard.ts: consecutiveRestarts is still incremented alongside restartTimestamps for "legacy logging." If it's not driving any logic, it adds noise. Consider removing or clarifying in a comment why both are maintained.
  • worker-types.ts: The consecutiveRestarts field JSDoc still says "Track consecutive restart attempts" without acknowledging that the real guard is now restartTimestamps. Minor docs drift.

What's Good

  • RestartGuard.ts is well-factored — the 60s window + 10-cap is much more robust than a flat counter for long-running legitimate sessions
  • SSEBroadcaster cleanup logic is thorough: handles close, error, periodic sweep, and inline broadcast cleanup
  • SearchManager.ts FTS5 fallback correctly only shows the "install Chroma" message when FTS5 also returns nothing — good UX
  • OpenRouterAgent.ts configurable URL follows the existing env → settings → default priority pattern consistently
  • zh-TW mode mirrors the structure of other mode files cleanly

The critical custom-model-dropdown bug (#🔴 above) should be fixed before merge. The other items are improvements but not blockers.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR bundles eight bug fixes: precise model IDs in the viewer dropdown, SSE dead-client cleanup, configurable OpenRouter base URL, Traditional Chinese mode, version-agnostic CLI alias, time-windowed restart guard, FTS5 fallback when Chroma is disabled, and httpcore/httpx added to chroma-mcp uvx deps. Most changes are clean and well-scoped.

  • P1 – ContextSettingsModal: The <option value=\"custom\"> has the literal string \"custom\" as its value; selecting it via onChange overwrites a user's custom model ID (e.g. an OpenRouter model string) with the word \"custom\", breaking API calls. The option value should be the actual model ID.

Confidence Score: 4/5

Safe to merge after fixing the custom-model ID overwrite in ContextSettingsModal

One P1 defect in ContextSettingsModal would silently corrupt a user's custom model setting when they interact with the dropdown. All other findings are P2 style/cleanup items. Everything else in the PR looks correct.

src/ui/viewer/components/ContextSettingsModal.tsx (custom option value bug)

Important Files Changed

Filename Overview
src/ui/viewer/components/ContextSettingsModal.tsx Model dropdown updated to show precise model IDs; custom option uses value="custom" which overwrites real model IDs on re-selection (P1 bug)
src/services/worker/RestartGuard.ts New module replacing flat restart counter with time-windowed counting (60s / cap 10); applyRestartDecay is defined but not connected to any callsite
src/services/worker/SSEBroadcaster.ts Adds periodic dead-client cleanup interval and error-event listener; dispose() method is not wired to shutdown, leaving the interval running
src/services/worker/SearchManager.ts Falls back to FTS5 full-text search when ChromaDB is unavailable; chromaFailed flag only set if FTS5 also returns nothing — sensible behaviour
src/services/worker/OpenRouterAgent.ts Adds CLAUDE_MEM_OPENROUTER_BASE_URL override for the API endpoint, threaded cleanly through config and fetch call
plugin/scripts/smart-install.js Switches to a version-agnostic runtime alias using shell command substitution; updates existing aliases rather than skipping when one already exists
src/services/sync/ChromaMcpManager.ts Adds httpcore and httpx as explicit --with dependencies to uvx chroma-mcp invocations for both http and local modes
src/services/worker-types.ts Adds restartTimestamps: number[] field to ActiveSession interface for time-windowed restart tracking
src/services/worker-service.ts Wires RestartGuard into pending-work restart path using dynamic imports; static imports would be cleaner
src/services/worker/http/routes/SessionRoutes.ts Replaces inline restart counter logic with recordRestartAndCheckAllowed / resetRestartCounter from RestartGuard
src/services/worker/SessionManager.ts Initialises restartTimestamps: [] on new session objects alongside existing consecutiveRestarts
plugin/modes/code--zh-tw.json New Traditional Chinese (zh-TW) mode file with correct 繁體中文 language requirement prompts

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Session Generator Exit] --> B{pendingCount > 0?}
    B -- Yes --> C[recordRestartAndCheckAllowed]
    C -- allowed --> D[Restart Generator]
    C -- blocked --> E[terminateSession / abort]
    B -- No --> F[resetRestartCounter]
    F --> G[Remove Session]

    H[SSEBroadcaster] --> I{Client connects}
    I --> J[Add to sseClients Set]
    J --> K[Listen: close / error → removeClient]
    H --> L[setInterval 30s → cleanupDeadClients]
    H --> M[broadcast → inline dead-client prune]

    N[SearchManager text query] --> O{ChromaDB initialised?}
    O -- Yes --> P[Semantic search]
    O -- No --> Q[FTS5 fallback]
    Q --> R{Results found?}
    R -- Yes --> S[Return FTS5 results]
    R -- No --> T[chromaFailed = true → show install hint]
Loading

Comments Outside Diff (2)

  1. src/ui/viewer/components/ContextSettingsModal.tsx, line 380-390 (link)

    P1 Custom option overwrites model ID with the literal string "custom"

    The <option value="custom"> has the string "custom" as its value, not the actual custom model ID. If a user with a custom model (e.g. an OpenRouter model string) switches to haiku and then selects the custom option to revert, onChange fires updateSetting('CLAUDE_MEM_MODEL', 'custom') — replacing their real model ID with the literal word "custom", which is an invalid model identifier and will break API calls.

    The custom option should either be disabled (since it's informational) or use the actual model ID as its value:

    With this fix, the value computed above (which already maps unknown models to 'custom') would also need to map to the actual model string instead of 'custom', but using the real model ID as both the option's value and the computed select value keeps everything consistent.

    Fix in Claude Code

  2. src/services/worker-service.ts, line 763-775 (link)

    P2 Dynamic import of RestartGuard on every restart event

    RestartGuard functions are imported with await import(...) inside the hot-path event handler. While module caching makes subsequent imports cheap, static top-level imports would be clearer, avoid async overhead on first load, and catch missing-module errors at startup rather than at restart time.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix: viewer model dropdown, SSE cleanup,..." | Re-trigger Greptile

Comment on lines 16 to +24
private sseClients: Set<SSEClient> = new Set();
private cleanupInterval: ReturnType<typeof setInterval> | null = null;

constructor() {
// Periodic cleanup of dead/disconnected SSE clients every 30 seconds
this.cleanupInterval = setInterval(() => {
this.cleanupDeadClients();
}, 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.

P2 dispose() is never called — interval leaks on shutdown

The setInterval started in the constructor runs every 30 s for the lifetime of the process. The new dispose() method clears it, but there is no call to dispose() anywhere in the diff (not in worker-service.ts, not in WorkerService's shutdown/cleanup path). The live setInterval will prevent Node/Bun from exiting cleanly during graceful shutdown.

Wire dispose() into the existing graceful-shutdown sequence, e.g.:

// in WorkerService shutdown
this.sseBroadcaster.dispose();

Fix in Claude Code

Comment on lines +68 to +92
*/
export function resetRestartCounter(session: ActiveSession): void {
session.consecutiveRestarts = 0;
session.restartTimestamps = [];
}

/**
* Apply time decay: if enough time has passed since the last restart,
* clear the restart history. Call this periodically during successful processing.
*/
export function applyRestartDecay(session: ActiveSession): void {
if (!session.restartTimestamps || session.restartTimestamps.length === 0) return;

const now = Date.now();
const mostRecentRestart = Math.max(...session.restartTimestamps);

if (now - mostRecentRestart > RESTART_DECAY_MS) {
logger.debug('SYSTEM', 'Restart counter decayed after sustained success', {
sessionId: session.sessionDbId,
previousRestarts: session.restartTimestamps.length,
decayMs: RESTART_DECAY_MS
});
resetRestartCounter(session);
}
}
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.

P2 applyRestartDecay is defined but never called

applyRestartDecay is exported but nothing in this PR (or anywhere in the diff) calls it. The docstring says "call this periodically during successful processing," but no callsite wires it up. The 5-minute decay won't happen; timestamps age out only via the 60 s window filter. Consider either connecting it to the message-processing loop or removing it until it is needed.

Fix in Claude Code

…ssful processing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Apr 18, 2026

Code Review — PR #2069

This is a solid multi-fix PR. The individual changes are focused and well-scoped. A few issues worth addressing before merge, plus some minor suggestions.


Overview

Eight independent fixes bundled together: SSE leak cleanup, configurable OpenRouter URL, zh-TW language mode, version-agnostic alias, time-windowed restart guard, FTS5 fallback, httpcore dependency, and viewer model dropdown.


Issues

1. ContextSettingsModal.tsx — custom model option is selectable and will corrupt settings

<option value="custom">custom: {model}</option>

value="custom" means if a user clicks this dropdown item, updateSetting('CLAUDE_MEM_MODEL', 'custom') fires and saves the literal string "custom" as the model ID. The intent is read-only display of the current unknown model, but nothing prevents selection. Fix: add disabled to the option, or restructure the select to only show the live model value as an input field when it's custom.

2. RestartGuard.ts — off-by-one in the cap check

if (restartsInWindow > MAX_WINDOWED_RESTARTS) {  // blocks at 11, not 10

The PR description says "cap 10" but > 10 allows 11 restarts before blocking. Should be >= MAX_WINDOWED_RESTARTS to match the stated intent.

3. smart-install.js — PowerShell update regex is fragile

const updated = existingContent.replace(/function claude-mem \{[^\n]*\}\n?/, functionDef);

[^\n]* only matches single-line function bodies. If a user ever manually expanded the function to multi-line, the regex silently fails to update it. The old behavior (skip-if-exists) was at least safe. Consider matching function claude-mem as an anchor and replacing through the closing } with a multiline match, or logging a warning when the regex produces no substitution (updated === existingContent).


Minor Suggestions

4. RestartGuard.ts — redundant legacy counter

session.consecutiveRestarts is still incremented inside recordRestartAndCheckAllowed but is never used for guard logic anymore — only the timestamp array is. This creates two sources of truth. Either remove the legacy increment or add a note that it's kept only for logging.

5. worker-service.ts / SessionRoutes.ts — repeated dynamic imports

const { recordRestartAndCheckAllowed, resetRestartCounter } = await import('./worker/RestartGuard.js');

This pattern appears 4+ times in hot paths (session exit handlers). There's no meaningful code-splitting benefit here; these should be static imports at the top of each file. Dynamic imports inside callbacks also obscure the module graph.

6. ContextSettingsModal.tsx — IIFE inside JSX attribute

The value={(() => { ... })()} pattern is non-idiomatic React and harder to read. Extract the mapping logic to a named function or a const selectedModelValue = ... in the render body above the return.


What Looks Good

  • SSEBroadcaster.ts: The error event handler + periodic cleanupDeadClients + inline cleanup during broadcast + dispose() is thorough. The layered defense is appropriate for a long-lived process.
  • RestartGuard.ts: Extracting restart logic into its own module is the right call — eliminates the duplicated MAX_CONSECUTIVE_RESTARTS = 3 constants that lived in two separate route handlers.
  • SearchManager.ts: Graceful FTS5 fallback when Chroma is absent is a good UX improvement. The condition to still set chromaFailed = true when FTS5 also returns nothing correctly preserves the install-suggestion message.
  • OpenRouterAgent.ts: env > settings > default precedence for the base URL is the correct pattern.
  • ChromaMcpManager.ts: Adding both httpcore and httpx to both the HTTP and persistent client args is correct.
  • zh-TW mode: Placeholder strings are consistent with the existing locale pattern.

Summary

The SSE, FTS5 fallback, RestartGuard, and OpenRouter fixes are all clean. The three issues above — the selectable custom model option (data corruption risk), the off-by-one in RestartGuard, and the fragile PowerShell regex — are worth fixing before merge. The rest are style/cleanup suggestions.

@thedotmack
Copy link
Copy Markdown
Owner Author

Closing to start fresh from main — will redo fixes isolated in Docker container.

@thedotmack thedotmack closed this Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment