Additional
Performance & Stability Enhancements
Synced from github.com/CoWork-OS/CoWork-OS/docs
This document covers performance optimizations and stability fixes applied to CoWork OS to address resource issues observed during multitask runs (collaborative team reviews, parallel agent execution).
Problem Statement
During live multitask review runs, the following resource issues were observed:
- Renderer memory: grew from ~1 GB to 7.4 GB RSS
- Renderer CPU: stayed at 100% after task completion
- MCP server processes: 27 leaked processes (~2.4 GB)
- Synthesis prompts: ballooned to 244k+ characters
- Concurrent verification: up to 3 duplicate
tsc --noEmitprocesses (~3 GB combined) - React render loops: "Maximum update depth exceeded" caused cascading re-renders
- SQLite lock contention: "database is locked" errors under concurrent writes
- Phase tracking: UI showed "Thinking..." during active agent execution
Renderer Memory & CPU
Event Array Capping
| Constant | Value | File |
|---|---|---|
MAX_RENDERER_TASK_EVENTS | 600 | src/renderer/App.tsx |
MAX_RENDERER_CHILD_EVENTS | 300 | src/renderer/App.tsx |
The renderer caps task events in memory using capTaskEvents(), which prioritizes structural events (approvals, plan steps, errors) over noise events (progress updates, streaming). Child event arrays from sub-agents are capped at a lower threshold since they're secondary context.
Child Event Polling Stop
When all child tasks reach terminal status (completed, failed, cancelled), the 12-second polling interval for child historical events is cleared. Previously the interval ran indefinitely, generating IPC traffic and re-renders for finished work.
File: src/renderer/App.tsx — poll timer callback checks tasksRef.current for non-terminal children.
Stale Task Reconciliation Guard
The 4-second reconciliation interval that re-fetches task state from the database is skipped entirely when the selected task is already terminal. This prevents unnecessary IPC round-trips and state updates for completed tasks.
File: src/renderer/App.tsx — early return using isTerminalTaskStatus().
Map Ref Cleanup
Growing Map and Set refs (latestAttentionEventByTaskIdRef, taskLastEventTimestampRef, completionToastNotifiedPathsRef) are pruned whenever the task list changes, removing entries for task IDs no longer in the sidebar.
File: src/renderer/App.tsx — cleanup effect keyed on tasks.
React Render Loop Fix
Root cause: loadTasks was a plain async function recreated every render, passed as onTasksChanged to MainContent. The team run event effect included onTasksChanged in its dependency array, causing infinite re-subscription cycles.
Fixes:
- Wrapped
loadTasksinuseCallback([], [])— it only uses stable references (state setters, refs, constants) - Added
onTasksChangedRefinMainContentto decouple the event subscription from prop identity - Guarded
setWelcomeTaskSuggestions([])to skip when already empty
Files: src/renderer/App.tsx, src/renderer/components/MainContent.tsx
Single-Event Merge Fast Path
mergeTaskEventsByIdentity() now has a fast path for incoming.length === 1 (the common streaming case). Instead of building a full Map and sorting, it finds-or-appends the single event and skips the sort when the event is already in order.
File: src/renderer/utils/task-event-stream.ts
Timeline Virtualization
| Constant | Value | Purpose |
|---|---|---|
VIRTUALIZE_THRESHOLD | 50 | Switch to virtual rendering above this event count |
MAX_SHOW_ALL_EVENTS | 200 | Cap for "Show All" mode |
WINDOW_SIZE | 6 | Default visible events in windowed mode |
Virtualization is now always enabled when events exceed 50, regardless of feature flags. "Show All" is capped at the last 200 events.
File: src/renderer/components/timeline/SemanticTimeline.tsx
MCP Server Lifecycle
Connection Reference Counting
MCPClientManager tracks which executors use which MCP server connections via connectionRefCounts: Map<string, Set<string>> (serverId → executorIds). Servers connected at startup are tagged as initialServerIds and are never auto-disconnected.
acquireForExecutor(executorId, serverId)— registers usagereleaseForExecutor(executorId)— removes all references; disconnects servers with zero remaining references (excluding initial servers)
Release is called from:
ToolRegistry.cleanup()— when a task's tool registry is cleaned updaemon.cleanupOldExecutors()— when cached executors are evicteddaemon.completeTask()— immediately for sub-agent tasks (they won't receive follow-ups)
Files: src/electron/mcp/client/MCPClientManager.ts, src/electron/agent/tools/registry.ts, src/electron/agent/daemon.ts
Synthesis Prompt Optimization
Token-Budgeted Truncation
| Constant | Value |
|---|---|
MAX_SYNTHESIS_PROMPT_CHARS | 100,000 |
groupAndCompactThoughts() groups agent thoughts by agent name and applies proportional per-agent truncation when total content exceeds the budget. Applied in all three synthesis prompt builders (buildSynthesisPrompt, buildMultiLlmSynthesisPrompt, buildCouncilSynthesisPrompt).
Synthesis Step Bounds
Synthesis tasks are configured with maxTurns: 3 and explicit instructions: "Produce your synthesis in a SINGLE response. Do NOT create sub-tasks or use planning tools."
Synthesis Retry with Compact Prompt
When a synthesis item fails (e.g., provider prompt-too-long), the orchestrator retries once per team run by re-invoking transitionToSynthesizePhase(). The synthesisRetried Set prevents infinite retry loops.
File: src/electron/agents/AgentTeamOrchestrator.ts
Executor & Cache Management
Tightened Executor Cache
| Constant | Previous | Current |
|---|---|---|
MAX_CACHED_EXECUTORS | 10 | 2 |
EXECUTOR_CACHE_TTL_MS | 30 min | 3 min |
| Cleanup interval | 5 min | 2 min |
Completed task executors are evicted faster, freeing memory from LLM conversation history, tool registries, and associated state.
File: src/electron/agent/daemon.ts
Workspace Verification Deduplication
Multiple executors running in the same workspace often trigger identical verification commands (e.g., tsc --noEmit, npm run build). The daemon now deduplicates these:
verificationLocks: Map<string, { promise, startedAt }>keyed byworkspace::command- If the same command was started within 120 seconds, the existing promise is returned
- Locks are cleaned up 5 seconds after completion
The executor's runStepExternalVerification routes through daemon.runWorkspaceVerification() when available.
Files: src/electron/agent/daemon.ts, src/electron/agent/executor.ts
Database Stability
WAL Mode + Busy Timeout
PRAGMA journal_mode = WAL
PRAGMA busy_timeout = 5000
WAL (Write-Ahead Logging) enables concurrent reads during writes — critical for multitask operations where the daemon, executors, and renderer all access the database simultaneously. busy_timeout = 5000 gives SQLite a 5-second retry window instead of immediately failing with "database is locked".
File: src/electron/database/schema.ts
Automatic Maintenance
Daily database maintenance (deferred 60 seconds after startup):
pruneOldEvents(90)— deletes events for terminal tasks older than 90 daysvacuumIfNeeded(500)— runs VACUUM when freelist exceeds 500 MB
Files: src/electron/database/repositories.ts, src/electron/agent/daemon.ts
Search Scope Optimization
Grep Directory Exclusions
Added to skipDirs: release, .cowork, out, .cache, .parcel-cache, .turbo
These directories contain build artifacts and worktrees that bloat search scope during code review tasks.
Grep Output Capping
| Constant | Value |
|---|---|
MAX_GREP_OUTPUT_BYTES | 50,000 |
When serialized grep matches exceed 50 KB, matches are removed from the end until within budget. The truncated flag is set so the agent knows results were capped.
Timeline Payload Sanitizer
| Constant | Value |
|---|---|
MAX_TIMELINE_STRING_CHARS | 60,000 |
MAX_TIMELINE_SANITIZE_DEPTH | 12 |
All timeline events are sanitized before IPC transmission. Base64 images are replaced with metadata stubs. Strings exceeding 60k characters are truncated with a notice.
Files: src/electron/agent/tools/grep-tools.ts, src/electron/agent/timeline-payload-sanitizer.ts
Load-Aware Trigger Gating
Event triggers are skipped when active task count >= 4, preventing automated triggers from spawning additional work during heavy load.
Files: src/electron/triggers/EventTriggerService.ts, src/electron/triggers/types.ts
Read-Only Review Safety
Git State Snapshot
When a task is classified as read-only review (via descriptionHasReadOnlyIntent), the executor captures git diff --cached --stat and git diff --stat at task start and stores it as reviewDiffSnapshot.
Tool Restrictions
Read-only review tasks automatically restrict system interaction tools: group:system, take_screenshot, type_text, screenshot, click, double_click, move_mouse, drag, scroll, keypress. This prevents review agents from accidentally modifying the workspace.
File: src/electron/agent/executor.ts
Team Run Phase Tracking
Added "execute" phase to AgentTeamRunPhase:
dispatch → execute → synthesize → complete
Previously, the phase stayed at "think" during the entire child task execution, showing "Thinking..." in the UI. Now it transitions to "execute" when items are spawned, and the CollaborativeSummaryPanel shows "Agents are executing..." for the execute phase and "Planning..." for the dispatch phase.
Files: src/shared/types.ts, src/electron/agents/AgentTeamOrchestrator.ts, src/renderer/components/CollaborativeSummaryPanel.tsx
Files Modified
| File | Changes |
|---|---|
src/renderer/App.tsx | Event capping, polling stop, reconciliation guard, Map cleanup, loadTasks stabilization |
src/renderer/components/MainContent.tsx | onTasksChangedRef, welcome suggestions guard |
src/renderer/components/CollaborativeSummaryPanel.tsx | Phase display labels |
src/renderer/components/timeline/SemanticTimeline.tsx | Virtualization enforcement, Show All cap |
src/renderer/utils/task-event-stream.ts | Single-event merge fast path |
src/electron/agent/daemon.ts | Executor cache, verification dedup, MCP cleanup, DB maintenance |
src/electron/agent/executor.ts | Review snapshot, tool restrictions, verification routing |
src/electron/agent/tools/grep-tools.ts | Skip dirs, output capping |
src/electron/agent/tools/registry.ts | MCP release in cleanup |
src/electron/agent/timeline-payload-sanitizer.ts | String truncation, image omission |
src/electron/agents/AgentTeamOrchestrator.ts | Synthesis budget, retry, bounds, execute phase |
src/electron/database/schema.ts | WAL mode, busy timeout |
src/electron/database/repositories.ts | Event pruning, vacuum |
src/electron/mcp/client/MCPClientManager.ts | Connection ref counting |
src/electron/triggers/EventTriggerService.ts | Load-aware gating |
src/electron/triggers/types.ts | getActiveTaskCount dep |
src/shared/types.ts | Execute phase type |
Verification
npm run type-check # Must pass (pre-existing errors in TerminalTabsDock/i18n are unrelated)
npm run test # 6007 passed, 2 pre-existing failures unrelated to these changes
Runtime verification during a multitask run:
- Renderer RSS should stay under 2 GB
- MCP server count should return to baseline after child tasks complete
- Renderer CPU should drop to near 0% within 30 seconds of task completion
- No concurrent duplicate
tsc --noEmitprocesses - Task spinner must disappear within 5 seconds of collaborative task completion
CollaborativeSummaryPanelshould show "Agents are executing..." during runs, not "Thinking..."