Worker Service Overhead Analysis
Date: 2025-11-06 File:src/services/worker-service.ts
Total Lines: 1173
Overall Assessment: This file has accumulated unnecessary complexity, artificial delays, and defensive programming patterns that actively harm performance. Many patterns were likely added “just in case” without real-world justification.
Executive Summary
High Severity Issues (Score 8-10):- Line 942: Polling loop with 100ms delay instead of event-driven architecture (Score: 10/10)
- Lines 338-365: Spinner debounce with 1.5s artificial delay (Score: 9/10)
- Lines 204-234: Database reopening on every getOrCreateSession call (Score: 8/10)
- Lines 33-70: Unnecessary Claude path caching for rare operation (Score: 6/10)
- Lines 694-711: Redundant database reopening in handleInit (Score: 7/10)
- Lines 728-741: Fire-and-forget Chroma sync with verbose error handling (Score: 5/10)
- Line 28: Magic number MESSAGE_POLL_INTERVAL_MS without justification (Score: 4/10)
- Lines 303-321: Over-engineered SSE client cleanup (Score: 4/10)
Line-by-Line Analysis
Lines 1-30: Setup and Constants
Lines 22-24: Version reading from package.jsonLines 33-70: Claude Path Caching
- YAGNI Violation: This function is called exactly once per worker startup (line 846 in runSDKAgent)
- Premature Optimization: Caching saves ~5ms on an operation that happens once per worker lifetime
- Added Complexity: 37 lines of code including module-level state for negligible benefit
- False Economy: The worker runs for hours/days. Saving 5ms on startup is meaningless.
Lines 103-110: WorkerService State
app,sessions,chromaSync,sseClients: Good - necessary stateisProcessing: Questionable (Score 5/10) - Do we really need to track this globally? Can’t we derive it fromsessions.size > 0orsessions.values().some(s => s.pendingMessages.length > 0)?spinnerStopTimer: Bad (Score 7/10) - Exists solely to support artificial debouncing (see lines 338-365)
Lines 145-178: Service Startup
Lines 145-153: HTTP server startupLines 200-236: getOrCreateSession - THE KILLER
-
Database Reopening: Opens database at line 204, closes at line 234. This happens on:
- First call to
/sessions/:id/init(line 691) - First call to
/sessions/:id/observations(line 762) - First call to
/sessions/:id/summarize(line 789)
- First call to
-
Redundant Database Access: The database is ALREADY opened in
handleInitat line 695 to callsetWorkerPort(). So we have:- Line 695:
const db = new SessionStore()in handleInit - Line 696:
db.setWorkerPort() - Line 697-711: More queries on the same database
- Line 711:
db.close() - Line 691:
this.getOrCreateSession()is called - Line 204: Opens database AGAIN inside getOrCreateSession
- Line 234: Closes it
- Line 695:
- Error Handler Opens Database: Line 228 opens a NEW database connection in the error handler. If runSDKAgent fails, we open the database AGAIN just to mark it failed, then close it. This is defensive programming for ghosts - if the worker is crashing, do we really care about marking it failed?
- Pass the already-open database connection to getOrCreateSession
- Or at minimum, reuse the connection from the calling context
- The error handler should either crash hard or mark failed WITHOUT reopening the database
Lines 263-292: SSE Stream Setup
Lines 297-322: SSE Broadcast and Cleanup
- Two-Pass Cleanup: Creates a temporary array of failed clients, then iterates again to remove them. Why not just remove them in the first loop?
- Unnecessary Logging: Do we really need to log every time a client disconnects? The
handleSSEStreamalready logs disconnects (line 290). This is duplicate logging.
Lines 338-365: Spinner Debounce - ARTIFICIAL DELAY
- Artificial Delay: 1.5 SECONDS (1500ms) of artificial delay before stopping the spinner. This is pure overhead added for no reason.
- Why Was This Added?: Probably someone thought “the UI flickers when the spinner stops/starts rapidly.” SO FUCKING WHAT? That’s a UI rendering problem, not a worker service problem. Fix it in the UI with CSS transitions or debouncing on the CLIENT side.
- Double-Check Pattern: Checks if queues are empty, waits 1.5s, then checks AGAIN. This is defensive programming for ghosts. If the queue is empty, it’s empty. We’re not protecting against race conditions here - we’re just wasting time.
-
Polling Instead of Events: This function is called from
handleAgentMessage(line 1145) after processing every single response. Instead of reacting to the actual completion of work, we’re polling state and debouncing. -
State Management Overhead: Requires
spinnerStopTimerfield (line 109), timer cleanup logic, null checks, etc.
Lines 370-411: Stats Endpoint
- Redundant existsSync Check: The database path is guaranteed to exist if SessionStore initialized successfully. If it doesn’t exist, SessionStore would have crashed on startup. This is defensive programming for ghosts.
- Three Separate Queries: Could be combined into a single query with UNION or multiple SELECT columns, but this is minor.
Lines 507-555: GET /api/observations
-
Duplicate Parameter Arrays:
paramsandcountParamsare maintained separately even though they contain the same values (just the project filter). This is error-prone and verbose. -
Two Queries Instead of One: We run a COUNT query and a SELECT query. For small datasets, this is fine, but for large datasets, the COUNT query can be expensive. The
hasMoreflag could be computed by fetchinglimit + 1rows and checking if we got more thanlimit.
handleGetSummaries (line 557) and handleGetPrompts (line 618). Copy-paste code smell.
Estimated Savings: Remove COUNT queries (which can be expensive on large tables), simplify parameter handling.
Lines 685-752: POST /sessions/:sessionDbId/init - DATABASE REOPENING HELL
-
Two Database Opens in Same Function:
- Line 691:
getOrCreateSession()opens DB internally (line 204) - Line 695: Opens DB AGAIN for
setWorkerPort() - Line 711: Closes DB
- Line 691:
-
Redundant Data Fetching:
getOrCreateSession()already fetches session data from the database (line 205). Then we query AGAIN for the user prompt (line 698). -
Tight Coupling:
getOrCreateSession()hides database access, making it unclear that we’re opening the database twice.
- Open database ONCE at the start of handleInit
- Pass the open database to getOrCreateSession
- Fetch all needed data in a single transaction
- Close database at the end
Lines 728-741: Chroma Sync with Verbose Error Handling
- Inconsistent Error Handling: The comment says “crash on failure” but then we catch the error and continue. Which is it?
-
Redundant Comment: The code says
.catch(err => { /* continue */ })and the comment says “Don’t crash - SQLite has the data”. The code is self-documenting. - Fire-and-Forget: If we’re going to fire-and-forget, why bother with verbose error handling? Either care about failures (and retry/alert) or don’t (and just log).
Lines 758-779: POST /sessions/:sessionDbId/observations
-
Database Opens for No Reason:
getOrCreateSession()opens the database (line 204), but we don’t actually need any data from the database here. We just need to get or create the in-memory session object. - Hot Path Performance: This endpoint is called for every single tool execution. If you run 100 tool calls in a session, this opens/closes the database 100 times unnecessarily.
- Separate “get existing session” from “create session from database”
- Only open database if creating a new session
- For existing sessions, just push to the queue
Lines 914-1005: createMessageGenerator - THE POLLING HORROR
-
Infinite Polling Loop: Lines 936-944 implement a busy-wait polling loop that checks
pendingMessages.lengthevery 100ms. This is the single dumbest pattern in the entire file. - Event-Driven Alternative: We have a fucking queue! When something is added to the queue, NOTIFY THE CONSUMER. Use an EventEmitter, a Promise, a Condition Variable, ANYTHING but polling.
- Wasted CPU: Every 100ms, this loop wakes up, checks if the queue is empty, and goes back to sleep. For a worker that runs for hours, this is thousands of unnecessary wake-ups.
- Latency: When an observation is queued (line 770), it sits in the queue for up to 100ms before being processed. This adds 0-100ms of artificial latency to every single observation.
- Battery Impact: On laptops, constant polling prevents CPU from entering deep sleep states, draining battery.
- Remove 100ms polling interval (eliminate 0-100ms latency per observation)
- Reduce CPU wake-ups from ~10/second to 0 when idle
- Improve battery life on laptops
- Make the system feel more responsive
Lines 1011-1146: handleAgentMessage - Database Reopening and Chroma Spam
- Database Reopening: Opens database (line 1030), stores all observations, closes database (line 1142). This is called for every SDK response. For a session with 10 observations, this opens/closes the database 10+ times.
- Verbose Chroma Error Handling: Lines 1057-1076 and 1114-1133 have identical verbose error handling for Chroma sync failures. This is copy-paste code smell.
- Success Logging Spam: Line 1066 and 1123 log success for EVERY Chroma sync. For a session with 100 observations, this logs 100 success messages. Why? Who reads these?
-
Debounce Call: Line 1145 calls
checkAndStopSpinner(), triggering the 1.5s artificial delay.
- Reuse database connection across multiple calls
- Simplify Chroma error handling (fire-and-forget means swallow errors)
- Remove success logging (or make it debug-level)
- Remove debounce delay
Summary of Patterns
1. Database Reopening Anti-Pattern
Occurrences: Lines 200-236, 685-752, 758-779, 1011-1146 Impact: Opens/closes database 4-100+ times per session instead of reusing connections Fix: Pass open database connections between functions, use transactions, connection pooling2. Polling Instead of Events
Occurrences: Line 942 (100ms polling loop) Impact: 0-100ms latency per observation, wasted CPU cycles, battery drain Fix: Use EventEmitter or async queue with await/notify pattern3. Artificial Delays
Occurrences: Line 363 (1.5s spinner debounce), line 942 (100ms poll interval) Impact: 1.5s delay before spinner stops, 0-100ms delay per observation Fix: Remove debouncing, use event-driven patterns4. Premature Optimization
Occurrences: Lines 33-70 (Claude path caching) Impact: 37 lines of code to save 5ms on a one-time operation Fix: Remove caching, inline the function5. Defensive Programming for Ghosts
Occurrences: Line 382 (existsSync check), lines 228-231 (error handler reopens DB), lines 728-741 (verbose error handling) Impact: Code complexity without real benefit Fix: Fail fast, trust invariants, simplify error handling6. Copy-Paste Code
Occurrences: handleGetObservations, handleGetSummaries, handleGetPrompts (nearly identical) Impact: Maintenance burden, inconsistency risk Fix: Extract common pagination logic into helper functionRecommendations
Immediate Wins (Low Effort, High Impact)
-
Remove Spinner Debounce (Lines 338-365)
- Effort: 5 minutes
- Impact: Eliminate 1.5s artificial delay
- Score: 9/10 stupidity
-
Replace Polling with Events (Line 942)
- Effort: 30 minutes
- Impact: Eliminate 0-100ms latency per observation, reduce CPU usage
- Score: 10/10 stupidity
-
Remove Claude Path Caching (Lines 33-70)
- Effort: 5 minutes
- Impact: Remove 37 lines of unnecessary code
- Score: 6/10 stupidity
Medium Wins (Moderate Effort, Good Impact)
-
Fix Database Reopening in Hot Path (Lines 758-779)
- Effort: 1 hour
- Impact: Eliminate 99+ database cycles per session
- Score: 6/10 stupidity
-
Simplify Chroma Error Handling (Lines 728-741, 1057-1076, 1114-1133)
- Effort: 15 minutes
- Impact: Remove 50+ lines of verbose error handling
- Score: 5/10 stupidity
-
Simplify SSE Broadcast (Lines 297-322)
- Effort: 5 minutes
- Impact: Remove 10 lines, eliminate two-pass cleanup
- Score: 4/10 stupidity
Long-Term Improvements (High Effort, Architectural)
-
Database Connection Pooling
- Effort: 4 hours
- Impact: Reuse connections across requests, eliminate all open/close overhead
- Score: 8/10 stupidity (current approach)
-
Extract Pagination Helper
- Effort: 1 hour
- Impact: DRY up handleGetObservations/Summaries/Prompts
- Score: 5/10 stupidity
Estimated Performance Impact
Current Hot Path (1 observation):- HTTP request arrives: 0ms
- getOrCreateSession opens/closes DB: 1-5ms
- Queue message: 0ms
- Poll interval: 0-100ms (average 50ms)
- SDK processing: variable
- handleAgentMessage opens/closes DB: 1-5ms
- Chroma sync (async): N/A
- checkAndStopSpinner debounce: 1500ms
- Total artificial overhead: 1502-1610ms (1.5-1.6 seconds)
- HTTP request arrives: 0ms
- Get existing session (no DB): 0ms
- Queue message + notify: 0ms
- SDK processing: variable
- Store in DB (connection pool): 0.1-0.5ms
- Chroma sync (async): N/A
- Stop spinner (no debounce): 0ms
- Total artificial overhead: 0.1-0.5ms
Conclusion
This file has accumulated significant technical debt in the form of:- Artificial delays (1.5s debounce, 100ms polling)
- Database reopening anti-pattern (4-100+ opens per session)
- Polling instead of events (busy-wait loop)
- Premature optimization (caching rare operations)
- Defensive programming (protecting against non-existent failures)
- Remove spinner debounce (9/10 stupidity)
- Replace polling with events (10/10 stupidity)
- Fix database reopening in hot path (6-8/10 stupidity)

