When we shipped the OS kernel in v0.48, CortexPrism crossed an architectural threshold — it became an operating system for AI agents, not just a collection of features. But the codebase hadn't caught up to the architecture. A single router file had grown to 6,075 lines. The UI assembler was 17,740 lines. The agent loop was 1,605 lines with every pipeline stage in one function. New contributors couldn't find where to make changes. Cross-cutting concerns were tangled in monolithic files.
v0.49.0 fixes that at the structural level. This is the "make it right" release — the one where we pay down the technical debt that accumulates during rapid feature development.
The Package Architecture
We reorganized the codebase into 6 coarse Deno workspace packages, each with a clear domain boundary:
packages/
├── core/ — @cortex/core: config, database, i18n, utils, plugins (41 files)
├── gate/ — @cortex/gate: security, sandbox, virtual filesystem (29 files)
├── ai/ — @cortex/ai: agent, tools, memory, LLM, pipeline, skills (166 files)
├── server/ — @cortex/server: server, channels, A2A, MCP, voice, codegraph (222 files)
├── infra/ — @cortex/infra: processes, scheduler, observability, triggers (43 files)
└── cli/ — @cortex/cli: CLI commands, TUI framework (92 files)
The dependency graph is strict and acyclic:
@cortex/core ← @cortex/gate ← @cortex/ai ← @cortex/server ← @cortex/cli
↖ ↗
@cortex/infra
This means core has zero internal dependencies — it's the foundation. gate depends only on core. ai depends on core and gate. server depends on core and ai. cli is the composition root, depending on everything. infra sits between ai and cli, depending on core and ai.
Each package exposes a contracts/ directory with pure TypeScript interfaces that define its public API. These 41 interfaces have zero runtime dependencies — they're purely type-level contracts. For example, @cortex/ai/contracts/tools.ts defines ITool, IToolRegistry, IToolContext, IToolResult, and 8 other types. Any package that depends on @cortex/ai sees only these interfaces, not the implementation details in src/.
A boundary enforcement script (scripts/check-boundaries.ts) validates that cross-package imports only reference contracts/ directories. If someone accidentally imports from packages/ai/src/ instead of packages/ai/contracts/, CI will catch it. This is the same pattern that operating systems use — a stable syscall interface that implementations must satisfy but callers only see.
What Happened to the Monoliths?
Three files had grown well past the point of maintainability. Here's exactly what happened to each:
The Router (src/server/router.ts, 6,075 lines) was a single file where every REST endpoint lived — auth, agents, memory, sessions, tools, config, codegraph, MCP, channels, workflows, OS health, debug settings, etc. Each section was separated by // ── Auth ── style comments. The mechanical split produced 62 files in src/server/routes/, one per API area — routes/auth.ts, routes/agents.ts, routes/memory.ts, and so on. Each exports RouteHandler[] — an array of { method, pattern, handler } tuples. new-router.ts iterates a flat publicRoutes/protectedRoutes table with the auth guard between them. Adding a new endpoint is now: create a new route file, export handlers, add to the table. No more scrolling through 6,000 lines to find the right // ── comment.
The UI (src/server/ui.ts, 17,740 lines) was a single template literal that assembled the entire SPA — CSS, 41 HTML page templates, 25 JavaScript blocks, shared utilities — all in one file. The split produced 74 files under src/server/ui/: css.ts (embedded CSS), 41 files in pages/ (one per page — pages/chat.ts, pages/memory.ts, pages/codegraph.ts, etc.), 25 files in js/ (concatenated JavaScript modules — js/01_core.ts, js/02_router.ts, js/03_websocket.ts, etc.), and shared utilities in shared/. mod.ts assembles all pieces via string concatenation into a single <script> block, preserving the global variable scope that the existing JS modules depend on (ws, sessionId, currentPage, etc.). The assembly preserves the exact same output — just modularly composed.
The Agent Loop (src/agent/loop.ts, 1,605 lines) contained 11 pipeline stages interleaved in a single function. The split produced 11 files under src/agent/stages/ (setup, history, assessment, prompt-builder, model-selector, llm-stream, tool-executor), 3 under post/ (response, background, cleanup), and 3 under helpers/ (nanoid, preferences, strip-tool-calls). The orchestrator in loop.ts is now 81 lines:
export async function agentTurn(opts: IAgentTurnOptions): Promise<IAgentTurnResult> {
const ctx = createTurnContext(opts);
await setupStage(ctx);
await historyStage(ctx);
await assessmentStage(ctx);
await promptBuilderStage(ctx);
await modelSelectorStage(ctx);
await llmStreamStage(ctx);
await toolExecutorStage(ctx);
await postResponseStage(ctx);
backgroundStage(ctx); // fire-and-forget
await cleanupStage(ctx);
return ctx.result;
}
Each stage receives a TurnContext with typed access to session, agent, tools, memory, and pipeline state. Stages can mutate the context but can't see each other's internals. The pipeline hooks system registers before/after handlers on any stage.
Interactive Memory Graph
While the modularization was the main architectural work, we also shipped two user-facing features. The Memory > Graph tab now renders an interactive D3 force-directed graph. Previously it showed a static card list of entities. Now you see colored nodes (blue for concepts, green for code symbols, purple for domains) connected by typed edges. Hover for details. Click to explore. The graph makes memory tangible — you can see what your agent has learned and how concepts relate.
Integrated Terminal
The Terminal tab in the editor panel now runs a real shell. Before v0.49.0, it was a static "not connected" placeholder. Now it spawns bash or PowerShell via WebSocket with full stdin/stdout/stderr piping. It's a full xterm.js terminal emulator — not a simulation. Ctrl+C sends SIGINT to the running process. Ctrl+D sends EOF. Session state (working directory, environment variables) persists between commands and across tab switches.
The Bug Backlog
Every major architectural change surfaces issues in existing code. The modularization process (especially the UI split) uncovered 53 JavaScript functions that were silently dropped during extraction. Template literal escaping in the terminal feature broke 11 strings. But beyond the extraction artifacts, the deeper win was finding and fixing 5 modules that were written but never actually wired up:
-
preference-learner.ts (260 lines): A full preference learning system with confidence tracking and pattern extraction. Never imported by any file. The agent loop had its own separate regex-based implementation. Now wired into both detectAndPersistPreference() and the prompt enrichment pipeline.
-
glossary.ts: A term definition and lookup system that was entirely in-memory — all terms lost on restart. Now DB-persisted via semantic_memory with proper async loading.
-
cross-agent-context.ts: Wrote shared context to a shared_context table that had no corresponding migration. All writes silently failed. Migration 039 created the table with proper schema and indexes.
-
context-bridge.ts: A cross-session context bridge that was defined but never called. Now wired into the agent loop's prompt enrichment pipeline alongside memory injection and preferences.
-
privacy.ts: Retention enforcement that was defined but had zero callers. Expired entries were never actually purged. Now wired into daily consolidation.
These aren't just bug fixes — they're features that were partially built, tested, and then accidentally left disconnected. The modularization process forced us to trace every import chain, which is how we found them.
Memory heuristics had a subtle ordering bug: boostImportanceFromAccess() reset access_count to 0, then slowDecayForFrequentAccess() checked access_count >= 5 — which always failed because the count was already zero. Episodic last_accessed was never updated (only semantic). Daily consolidation only re-scored semantic decay, leaving episodic memories to accumulate indefinitely. Qdrant's upsert was using the wrong HTTP method (PUT instead of POST) and missing the /upsert path suffix. Pinecone's API version was 8 months stale.
What's Next
With the modular architecture in place, the codebase is positioned for three areas of development:
-
Distributed agent swarms. The kernel's process tree and resource accounting were designed to extend across machines. With clean package boundaries, adding cross-instance coordination becomes an implementation detail of @cortex/infra.
-
WebAssembly tool plugins. With the @cortex/gate contracts defining capability-based access control, WASM plugins get stronger isolation guarantees — a plugin that only needs stdout shouldn't have filesystem access at all.
-
Multi-user collaboration. Shared workspaces with per-user agent configs. The @cortex/server contracts define channels and sessions in a way that naturally extends to multi-tenant routing.
Get Started
CortexPrism runs on macOS, Linux, and Windows as a single Deno binary. No Docker required, no Python, no node_modules.
# Install
curl -fsSL https://cortexprism.io/install.sh | bash
# Setup and start
cortex setup
cortex serve
# Open http://localhost:3000
Already running? Upgrade in place:
cortex self update
The project is Apache 2.0 licensed, fully open source, and has zero telemetry. Everything runs on your hardware.
GitHub: github.com/CortexPrism/cortex
Changelog: CHANGELOG.md
Built with Deno. 6 packages. Clean contracts. Zero telemetry.