| layout | single | ||
|---|---|---|---|
| title | Architecture | ||
| sidebar |
|
||
| toc | true | ||
| toc_label | Contents | ||
| toc_icon | sitemap |
For the strategic principles behind these architectural choices (deep modules, anti-pass-through, generality, hard constraints), see
PHILOSOPHY.md.
This diagram illustrates the flow of data through the application, highlighting the separation between the Renderer (UI), the IPC Bridge, and the specialized in-process services.
{% raw %}
graph LR
classDef frontend fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1
classDef backend fill:#f1f8e9,stroke:#33691e,stroke-width:2px,color:#33691e
classDef core fill:#eceff1,stroke:#455a64,stroke-width:2px,color:#455a64
classDef ai fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c
subgraph UI_Layer["Renderer Process"]
direction TB
AppEntry([App Entry])
GraphView["Knowledge Graph"]
ReduxStore[[Redux Store]]
AppEntry --> ReduxStore
AppEntry --> GraphView
end
subgraph Main_Process["Main Process"]
direction LR
subgraph Services["Core Services"]
Watcher["Smart Folder Watcher"]
ReRanker["Re-Ranking Service"]
Matcher["Folder Matcher"]
end
subgraph Intelligence["AI & Analysis"]
Vision["Llama Vision"]
LLM["Llama Text"]
Embeddings["Vector Embeddings"]
Watcher --> Vision
Watcher --> LLM
ReRanker --> LLM
end
subgraph Data["Persistence"]
Orama[(Orama Vector DB)]
History["Analysis History"]
Embeddings <--> Orama
Vision --> History
end
end
ReduxStore <--> Services
GraphView -.-> Orama
Matcher --> Embeddings
{% endraw %}
The watcher is the proactive heart of the system. Unlike a standard file watcher, it:
- Monitors configured paths for new/modified files.
- Debounces events to ensure files are fully written.
- Dispatches files to LlamaService (Text or Vision) for analysis.
- Automatically generates embeddings and updates Orama Vector DB.
- Triggers notifications and auto-organization based on confidence thresholds.
Search is a two-stage pipeline that spans two services:
- Retrieval (
SearchService/OramaVectorService): Fetches top candidates using hybrid search (vector similarity + BM25 full-text). - Re-Ranking (
ReRankerService, optional): Receives the pre-ranked candidates and uses a lightweight LLM to score each one's relevance to the query (0-10 scale, normalized to 0-1), re-ordering them to bubble up the best matches. Off by default: Chat's Deep mode turns it on; the Search tab and the MCPsearch_documentstool do not.
ReRankerService itself does not perform retrieval -- it operates solely on candidates passed to
it.
This service acts as the bridge between raw AI analysis and your filesystem. It:
- Manages the Orama connection.
- Generates embeddings for file content.
- Matches new files against existing folder "clusters" to suggest destinations.
- Handles deduplication of requests to prevent overloading the local AI.
{% include figure image_path="/assets/showcase/graph.webp" alt="Relate graph" caption="The Relate graph, drawn from the vector index." %}
The Knowledge Graph is a visual representation of your vector database.
- Nodes: Represent files, clusters, or search queries.
- Edges: Represent semantic similarity (distance in vector space).
- Implementation: Built with
React Flowin the renderer, fetching live node data from the main process via IPC.
The DI container that owns the lifecycle of every backend service:
- Registration: Factory-based with three lifetimes --
singleton,transient, andinstance(pre-created objects registered as singletons). - Resolution: Async resolution with in-flight deduplication (prevents duplicate initialization
of the same singleton). Circular dependency detection works for both synchronous
resolve()(via shared resolution stack) andresolveAsync()(viaAsyncLocalStorageper-chain tracking). - Shutdown ordering: Deterministic teardown defined in
SHUTDOWN_ORDER-- watchers first, then AI services (VisionService, LlamaService, OramaVector), then settings last.
Orchestrates the boot sequence with progress reporting to the splash screen:
- Single-instance lock first:
simple-main.jscallsapp.requestSingleInstanceLock()right after its logger is created, which is after dotenv,app.nameand theSTRATOSORT_E2E_USER_DATAoverride (Electron keys the lock to the app name and the userData directory) and before the service, analysis and IPC modules are required, so a duplicate launch exits before the main bundle has loaded. A launch that arrives while this instance is shutting down cannot get a window; thesecond-instancehandler callsrequestRelaunchAfterShutdown()incore/lifecycle.jsinstead, andhandleBeforeQuitlogs the decision before the log streams close and callsapp.relaunch()just beforeapp.exit(), with this instance's arguments minus Jump List task flags so a task such as--open-documentsdoes not run again. A downloaded update that installs on that quit takes priority and suppresses the relaunch. Thesecond-instancelistener is not part of the first-phase listener cleanup, so it keeps recording launches for the whole shutdown. - Bounded pre-window housekeeping: crash-journal recovery, user-data migration, custom-folder
loading and the default
Uncategorizedfolder run before the window is created. Every stat and mkdir of theUncategorizedfolder goes throughprobeFolder()incore/customFolders.js, capped atTIMEOUTS.DEFAULT_FOLDER_PROBE(3 s). A timeout keeps the Documents path (the fs call keeps running, and organize creates a missing target directory on the first move); a real fs error such asEACCESorEPERMfrom Controlled Folder Access also keeps the Documents path, raises thestorage.uncategorized-unavailabledegraded-state warning and is retried on the next launch. The folder is never relocated under userData, which the uninstaller's "remove settings and data" option deletes. A failure to save the default folder is logged and startup continues. The old-log and crash-dump sweeps (sweepOldLogsAndCrashDumps()) are started, not awaited, once the first-paint gate releases; after a startup error they startTIMEOUTS.FIRST_PAINT_GATEafter the degraded-mode window is created, so a launch that keeps failing still bounds its logs and dumps. - Window first:
simple-main.jscreates theBrowserWindowbefore any heavyweight service startup.createWindow.jsshows it onready-to-show(focus follows 150 ms later) or, if that event has not arrived, afterTIMEOUTS.WINDOW_SHOW_FALLBACK(4 s), so a GPU-process hang or a stalled bundle load can never leave the app invisible behind a busy cursor; a failedapp://load shows the missing-bundle page instead. TheWINDOW_CREATION_READYmutex timeout (60 s) force-shows a still-hidden window as a last resort. A GPU process exit before the window's first show also shows it at once (handleGpuProcessGoneincore/gpuConfig.js); a window hidden to the tray stays hidden. - Sticky GPU fallback:
core/gpuConfig.jsreadsuserData/.gpu-fallbackat module load, beforeapp.ready, and disables hardware acceleration while the flag is younger thanGPU_FALLBACK_RETRY_AFTER_MS(3 days, measured from its mtime). The flag is written after three GPU process exits in one session, each within an hour of the one before (GPU_FAILURE_RESET_WINDOWrestarts the count after an hour with no exit), and is never deleted on read.simple-main.jscallsclearGpuFallbackAfterStableRun()TIMEOUTS.GPU_FALLBACK_STABLE_RUN(30 s) after creating the window, next to the.crash-timestampsreset; it deletes the flag only for a hardware-accelerated session with no GPU process exit and a visible window, so a software-rendered session cannot clear it. On Windows and macOS Chromium's GPU rasterization and zero-copy switches are left at their per-driver defaults; Linux keeps forcing both, as earlier releases did (getForcedRasterSwitches()). - First-paint gate (
core/firstPaintGate.js): coordinated startup,runBackgroundSetup()and the semantic search pre-warm (whenFirstPaintGateReleased()) wait until the window has been shown, capped atTIMEOUTS.FIRST_PAINT_GATE(5.1 s, longer thanWINDOW_SHOW_FALLBACKplus the show delay, so a lateready-to-showis not cut off by the gate), so the synchronous stretches of AI bring-up (node-llama-cpp import, GPU probing, the vector store load) run after the first visible frame instead of competing with it on the main-process thread. The document finishing loading does not release it: that fires beforeready-to-show, and the window is shown 100 ms after that. - Last-known-good AI backend:
LlamaServicehandsinitLlamaWithBackend()(utils/llamaBackendSelector.js) a record fromutils/llamaBackendCache.js, stored asuserData/gpu-backend-cache.json. When the record's fingerprint (platform, arch, app version, node-llama-cpp version, GPU type, name and driver version, whichGPUMonitorreads fromnvidia-smior the Windows video controller) and the GPU backendsgetLlamaGpuTypes('supported')reports both match what was saved, and it is under 14 days old, the remembered GPU backend moves to the front of the candidate list before the node-llama-cpp support filter runs. The supported set is probed before the record is read, so a Vulkan record saved while the CUDA runtime was missing is not used once CUDA becomes available. It only reorders: on Windows node-llama-cpp still test-loads the binary, a failed attempt of the remembered backend deletes the record, and a GPU backend that differs from the record is saved (CPU never is).STRATOSORT_LLM_GPU_BACKENDand software or safe mode bypass the record entirely. - Phases (progress reported to splash): Starting (0%) -> Preflight checks (10%) -> Migration
(12%) -> Services -> Models -> App Services -> Ready (100%). See
reportProgress(...)calls inStartupManagerCore.jsfor the authoritative sequence. - Timeout:
runCoordinatedStartupInBackground()insimple-main.jshas two limits. AtTIMEOUTS.COORDINATED_STARTUP_SOFT(45 s) it logs[STARTUP] Coordinated startup still running after soft timeout; continuing, raises thestartup.still-startingwarning and lets startup keep running, because loading a large vector index and the model can outlast it. AtTIMEOUTS.COORDINATED_STARTUP_HARD_CAP(180 s) it aborts throughAbortController; every phase checkssignal.abortedbefore proceeding. A late completion clears bothstartup.still-startingandstartup.coordinated-failureand runs the deferred wiring (finishLateCoordinatedStartup()); a late failure or the hard cap swaps the warning for thestartup.coordinated-failurerestart error.StartupManagerCore's ownstartupTimeoutis set to the same hard cap. - Health monitoring: After startup completes, a 2-minute interval polls service health.
Handler modules registered by registerAllIpc() in src/main/ipc/index.js, organized by domain:
- Top-level:
analysis,analysisHistory,chat,files,knowledge,llama,whisper,organize,semantic,settings,smartFolders,suggestions,system,undoRedo,vectordb,window,downloads,mcp,aiProvider,license. - Files subdirectory:
fileOperationHandlers,fileSelectionHandlers,folderHandlers,shellHandlers, plus helpers such astaggingHandlers,batchOrganizeHandler,destinationPolicy(where a renderer-requested write may land),batchLockManager,batchProgressReporter,batchRollback,batchValidator, andembeddingSync. IpcServiceContext: Groups service dependencies into a single context object, replacing 28-parameter function signatures.ipcWrappers: Composable middleware (withErrorLogging,withValidation,withServiceCheck) and a unifiedcreateHandlerfactory.
In-process GGUF inference via node-llama-cpp. Three model types: text (Llama 3.2 3B default, or Qwen2.5-7B for better quality), vision (config/coordination only, delegates to VisionService), embedding (all-MiniLM-L6-v2 or nomic-embed 768d). GPU auto-detection (CUDA -> Vulkan -> Metal -> CPU), lazy loading via ModelMemoryManager (LRU eviction; budget is the larger of 70% of free RAM capped at 16 GB and 80% of VRAM, with the VRAM share on Apple Silicon capped at total RAM minus 4 GB).
Out of process: a bundled llama-server child reached over local HTTP, registered with
ModelMemoryManager as the vision-runtime consumer and torn down when memory is needed elsewhere.
The per-image timeout is the 300s AI_ANALYSIS_LONG, hard-coded in analysis/imageAnalysis.js
(AppConfig.ai.imageAnalysis.timeout) and applied twice over the same budget — once as the vision
HTTP request timeout and once as the abortable wrapper around it. There is no narrower inner bound
and no setting that shortens it: VISION_REQUEST (90s) is only VisionService's fallback for callers
that omit timeoutMs, which this path never does, and CONFIG_SCHEMA
(shared/config/configSchema.js) deliberately declares no per-image timeout key — it used to carry
a dead one that nothing read. VISION_STARTUP allows 120s for the first load (300s on CPU), with
health check polling.
PDF (unpdf + OCR fallback), DOC (word-extractor; RTF or plain text saved as .doc is read as such),
DOCX (mammoth), XLSX (xlsx-populate), PPTX and ODT/ODS/ODP (officeparser, with ZIP tag-stripping
fallbacks), EPUB (OPF spine order), EML (postal-mime), MSG (@kenjiuno/msgreader, compressed-RTF
bodies via @kenjiuno/decompressrtf), KML/KMZ, Jupyter notebooks, CSV, RTF (tokenizer in
analysis/extractors/rtfText.js, shared with the text extraction worker), plain text. 3D and design
files (analysis/extractors/cadDesignExtractors.js) are read as metadata text; tar/tgz/gz archives
are listed (archiveListing.js); SVG text is read before rasterizing; HEIC/HEIF is decoded through
the bundled FFmpeg when possible (heicDecode.js); Windows icons are decoded with decode-ico. OCR:
Tesseract.js (bundled eng traineddata only) on unpdf-rendered pages, max 3 PDF pages / 10 office
images, 150 DPI, 30 MB input cap. File-size limits (configurable in Advanced mode under Settings →
Performance → Processing limits; the panel is hidden in Simple mode unless a settings search matches
it): 100 MB images, 200 MB documents, 50 MB text; text files over the 50 MB limit are analyzed from
their first 50 MB. The 500k-character cap on extracted text is a hard-coded constant
(MAX_TEXT_LENGTH in analysis/documentExtractors.js), not a setting.
- ModelAccessCoordinator: per-type load queues (mutex), inference queue (1-4 concurrent, 100 max)
- LlamaResilience: per-model-type circuit breaker (5-failure threshold, 300 s recovery; an open
breaker shows in
getHealthStatus().openCircuitsand in Settings → AI Configuration), 3 retries with exponential backoff, GPU->CPU fallback - ModelMemoryManager: LRU eviction, OOM prevention, estimates per model type (refined from the
model file size at load). Out-of-process consumers register as external entries: the Whisper
context (
audio) and the visionllama-serverchild (vision-runtime), which pins itself while a request is in flight. A vision spawn is refused (MODEL_MEMORY_BUDGET_EXCEEDED) when pinned residents leave no room for it. - DegradationManager: system readiness checks, determines recovery actions
Resume support (.partial + Range headers), SHA-256 verification, disk space checks. Auto-downloads
companion files (vision projectors), model catalog in src/shared/modelRegistry.js.
- ParallelEmbeddingService: semaphore-based concurrency (CPU-adaptive, max 10), dynamic adjustment on error rate
src/main/analysis/embeddingQueue/: separate stage queues for files vs folders, batch upsert with individual fallback
Entry: src/renderer/index.js (React 19 + createRoot, prefetches docs path / redact paths /
smart folders / settings).
Phase-based routing via PhaseRenderer.jsx (no React Router): WELCOME -> SETUP -> DISCOVER ->
ORGANIZE -> COMPLETE
Each phase lazy-loaded with React.lazy() + Suspense, wrapped in PhaseErrorBoundary.
| Slice | Responsibility |
|---|---|
uiSlice |
Phase management, loading flags, settings cache, modals |
filesSlice |
File selection, smart folders, organization history, naming |
analysisSlice |
Analysis progress, results (max 5000), embedding state |
systemSlice |
CPU/memory metrics, service health, notifications, paths |
mcpServerSlice |
MCP server lifecycle/status (mcp reducer key) |
aiProviderSlice |
Cloud AI provider config, per-feature routing, budget |
licenseSlice |
License status shown under Settings → License (license) |
Middleware: ipcMiddleware (event queue + Zod validation), persistenceMiddleware (1s
debounce, quota-aware degradation).
React Flow with ELK layout, max 300 nodes.
- 4 node types: file, folder, query, cluster
- 4 edge types: similarity, queryMatch, smartStep, knowledge
- Features: Multi-hop discovery, drag-and-drop, keyboard nav, chat panel, cluster legend, guided tour
- Bridge files: Cross-cluster edges include a small sample of "bridge files" picked by similarity to opposing cluster centroids. This is best-effort and not an exhaustive list.
- Guide intents: "Expand from selection" and "bridges-only" intents are supported; the bridges-only overlay dims non-bridge edges.
- Filter visibility: Active graph filters are surfaced as chips on the graph (top-left overlay) and can be reset from there.
| Directory | Contents |
|---|---|
discover/ |
Drag-drop zone, analysis progress |
organize/ |
Suggestions, virtualized grids |
search/ |
Graph, chat panel, autocomplete |
settings/ |
Model selection, embedding rebuild |
setup/ |
Smart folder management (add/edit modals) |
ui/ |
Design system (Button, Card, Modal, etc.) |
icons/ |
Icon components |
layout/ |
AppShell, ActionBar |
Root-level components include PhaseRenderer, NavigationBar, SettingsPanel, ModelSetupWizard,
Toast system, and error boundaries.
Rate-limited (200 req/s), sanitized, channel-whitelisted. Default invoke timeout is 330s
(IPC_INVOKE_TIMEOUT), with per-channel overrides via src/shared/ipcInvokeTimeout.js (e.g.
downloads triage scans allow up to 900s). Retries use exponential backoff.
To respect local resources, we use a multi-tiered caching strategy:
AnalysisCacheService: Uses LRU caching for expensive AI responses (vision/text analysis).AnalysisHistory: Persisted log of all past analyses to prevent re-processing unchanged files.globalDeduplicator(LLMRequestDeduplicator,src/main/utils/llmOptimization.js): Prevents identical requests (e.g., same image analyzed twice in quick succession) from hitting the LLM.
We maintain separate queues to manage different types of bottlenecks:
- LLM Queue: Limits concurrent heavy AI tasks (via
ModelAccessCoordinator). - DB Queue: Manages read/write locks for Orama.
A review of shallow-looking services examined three candidates for merging or collapsing (LOC figures below are approximate as of 2026-10 and drift with the code):
SettingsService(~1,450 LOC) — KEPT. Custom mutex error, TTL cache, file watcher, atomic-write protocol, backup rotation, schema validation, corruption recovery. Genuine deep module.BatchAnalysisService(~1,200 LOC, 1 caller) — KEPT. Single caller is the IPC entry point, not over-specialization. Internal semaphores, lazy Whisper load, per-modality routing, and progress orchestration earn the module boundary; inlining it intoanalysis.jswould re-bloat the IPC handler file.AnalysisHistoryServiceCore(~1,550 LOC) — KEPT. Already decomposed into 8 cohesive sibling modules inanalysisHistory/. The Core is the deliberate coordinator (circuit breaker, schema validation, immutable-cache cloning, debounced flush). Further splitting would re-create pass-through layers.
No code changes came out of the review. The outcome is recorded here for future contributors.
For information on coding patterns, error handling, and dependency injection, refer to the other documentation files: