Architecture

For people reading or changing the code: layers, request path, data model, and how the SQLite limitations are handled.

Desktop shell (Tauri 2) window · tray · updater · sidecar Browser (dev mode) http://localhost:5174 React 19 frontend pages / components / Zustand / TanStack Query SSE rendering · send queue · panels i18n (zh / en) · light and dark themes ASP.NET Core API REST (ApiResponse / PagedResult) + SSE /scalar/v1 reference · /api/health probe Domain services (Hetu.Core) notes / chat / workspace / knowledge / graph / Wiki / memories repository interfaces · task coordinator · agent loop Model providers OpenAI-compatible · Anthropic API keys encrypted at rest MCP servers stdio · JSON-RPC 2.0 · tools/list + tools/call Git and CLIs worktrees · gh / glab · real PTY External search web search (toggle) SQLite + sqlite-vec (default) PostgreSQL + pgvector
Overall shape: one backend process and one database; the frontend only talks to the API over HTTP/SSE, while models and MCP are outbound dependencies.

Layers and dependency direction

Hetu.Api             controllers, SSE streams, background workers, DI wiring
   |  (depends on Core and Infrastructure interfaces)
Hetu.Core            entities, domain services, repository interfaces   <- no EF Core / ASP.NET impl
   ^
Hetu.Infrastructure  EF Core implementations, AI providers, MCP, sqlite-vec, files and processes
Hetu.Shared          DTOs, enums, constants (the frontend/backend contract)

The dependency direction is Api → Core ← Infrastructure: Core defines the rules and interfaces, Infrastructure holds the concrete implementations (database, model calls, processes, filesystem).

Hetu.Api controllers · SSE · workers Hetu.Core entities · services · repository interfaces Hetu.Infrastructure EF Core · AI providers · MCP · sqlite-vec Hetu.Shared DTOs / enums / constants (contract) 1 Controller: validate, then delegate 2 Service: business rules and orchestration 3 Repository: SQL (paging, aggregation, projection) 4 Long work: handed to the task coordinator
Layers and where a typical request lands: controllers validate and delegate, business rules live in services, SQL in repositories, and long work goes to the task coordinator.

Path of a request

  1. The frontend (React + TanStack Query) calls /api/* through a shared axios instance with response interceptors.
  2. Controllers validate input and delegate; business logic lives in Hetu.Core/Services.
  3. Services reach the database through repositories exposed by IUnitOfWork (EF Core), using AsNoTracking and projections for reads.
  4. Long-running work (indexing, extraction, Wiki, kanban execution) goes to IBackgroundTaskCoordinator, which writes a TaskItem row and lets a worker execute it.
  5. Chat requests stream over SSE: the controller writes events back and the frontend renders per event type.

Data model conventions

ConventionDetails
Base classEntities derive from BaseEntity: Id (Guid), CreatedAt, UpdatedAt
TimeAlways DateTimeOffset in UTC; SQLite stores it as fixed-width text (yyyy-MM-dd HH:mm:ss.fffffff+00:00)
Soft deleteIsDeleted (plus DeletedAt) with a global query filter; use IgnoreQueryFilters() explicitly when deleted rows are needed
MigrationsCode First, with separate migration projects for SQLite and PostgreSQL (Hetu.Infrastructure.PostgresMigrations)

Tables by domain

DomainTablesKey columns / notes
NotesNotes · Notebooks · Tags · NoteTags · NoteVersions · ShareLinksNotes carry Title/Content/NotebookId/IsPinned/IsFavorite/IsDeleted/DeletedAt; versions link by NoteId and keep the newest 20 by CreatedAt; share links store ShareCode/ExpiresAt/ViewCount
KnowledgeKnowledgeItems · NoteChunks · NoteChunkEmbeddings · vec_chunk_embeddingsItems: Type/Title/Content/SourceUrl/FileName/NoteId/IsDeleted; chunks: ChunkIndex/ChunkMethod/Content; embeddings metadata: Model/Dimensions/UpdatedAt, vectors in the vec0 virtual table keyed by rowid
Graph / WikiGraphEntities · GraphRelations · WikiDocuments · WikiGenerationJobsEntities: Name/Type/Description; relations: SourceId/TargetId/RelationType; Wiki documents: SetId/ProjectId/SortOrder/Title/Content, jobs keep stage and progress
MemoriesMemories · MemoryEmbeddings · vec_memory_embeddingsMemories: Content/Category/Importance/Scope/AccessCount/LastAccessedAt; Dream uses the last two to decide decay and forgetting
WorkspaceWorkProjects · WorkSessions · WorkMessages · WorkCodeChunks · WorkCheckpointsSessions: ProjectId/Title/Branch/WorktreePath/PermissionMode; messages: SessionId/Type/Content (tool output stored separately from prose); code chunk vectors are independent of note vectors
ChatChatGroups · ChatTopics · ChatMessagesTopics: GroupId/ModelId/SystemPrompt; messages read in ascending CreatedAt
Tasks and automationTaskItems · KanbanTasks · KanbanTaskRuns · KanbanTaskRunSteps · Workflows · WorkflowRuns · ScheduledTasksBackground tasks: TaskType/EntityId/Status/StartedAt/CompletedAt/ErrorMessage; runs and steps are separate rows so each execution can be replayed
Models and usageAiProviders · AiModels · LlmUsageLogs · AppSettingsModels: ProviderId/ModelId/Purpose/IsDefault; usage: Source/RefId/ModelId/InputTokens/CompressedTokens/OutputTokens/CachedTokens/LatencyMs; settings are key-value
OtherInboxNotifications · ManagedProjects · McpServers · Skills · PromptPresetsMCP servers: Type(stdio/sse)/ConnectionConfig(JSON)/IsEnabled; inbox merges repeated events by category key with an occurrence count

Repository surface

MethodPurpose
GetByIdAsync / GetAllAsync / FindAsyncStandard reads (read-only paths use AsNoTracking)
CountAsyncSQL COUNT without materializing entities
SelectAsyncProjection of selected columns only
CountByAsyncSQL GROUP BY counts returned as a dictionary
GetPagedByDateAsyncTime-ordered paging: ORDER BY + OFFSET/FETCH elsewhere, but on SQLite it projects Id + sort key, orders in memory and fetches the page by primary key
PruneAsyncKeeps the newest N rows and deletes the rest with one DELETE

Where the code lives

ConcernFile
App wiring / middlewaresrc/Hetu.Api/Program.cs (DI, response caching, health probe, Scalar)
SSE entry pointssrc/Hetu.Api/Controllers/WorkStreamController.cs · ChatMessagesController.cs
Agent loopsrc/Hetu.Core/Services/AgentLoopService.cs (steering, checkpoints, approvals)
Repositoriessrc/Hetu.Infrastructure/Repositories/ (EfRepository provides paging / aggregation / prune primitives)
DbContext and migrationssrc/Hetu.Infrastructure/Data/ and src/Hetu.Infrastructure.PostgresMigrations/
Git / worktrees / PRsrc/Hetu.Api/Services/WorkGitService.cs · src/Hetu.Core/Services/Work/WorkWorktreeService.cs
Compression pipelinesrc/Hetu.Core/Services/CompressionPipelineService.cs
Desktop shellshell/hetu-desktop/src/backend.rs (sidecar lifecycle) · src-tauri/nsis-hooks.nsh (install hook)
Frontend routing and servicesfrontend/src/App.tsx · frontend/src/services/ · frontend/src/stores/

The SQLite DateTimeOffset limitation

  • EF Core's SQLite provider cannot order or aggregate DateTimeOffset in SQL: ORDER BY throws NotSupportedException, MAX() fails to translate, and comparisons such as x.CreatedAt > since do not translate either.
  • Workarounds already in the repository: paging via GetPagedByDateAsync, grouped counts via CountByAsync, timestamp aggregates by projecting two columns and taking the maximum in memory, and “recent N days” checks by projecting timestamps and filtering in memory.
  • Never put OrderBy(x => x.CreatedAt) on an IQueryable — it fails at runtime. PostgreSQL is unaffected.

Performance practice

CaseApproach
List endpointsPaging and ordering pushed to the database; count first, then fetch the page
Status aggregationAggregated in SQL (GROUP BY / DISTINCT / EXISTS) instead of loading whole tables
Polled endpointsShort [ResponseCache] on read-only status endpoints; the frontend only polls while tasks are running
Bulk writes and cleanupSet-based operations such as ExecuteDeleteAsync instead of loading entities one by one
Frontend renderingDerived state instead of synchronous setState inside effects; useMemo for unstable array identities

Agent loop

  1. The request carries permission mode, tool allowlist, search / knowledge / memory toggles and reasoning effort.
  2. Each loop iteration first drains “steering”: instructions injected while streaming are merged into the context here.
  3. The model returns tool calls; the permission mode decides direct execution or an approval request, and results are fed back.
  4. Progress events stream over SSE: body deltas, thinking, tool calls and results, checkpoints, notices.
  5. Queued messages fire after the turn ends; the persisted session row is created by the first message.

Quality rules

  • Backend dotnet build and frontend npx eslint . must be warning-free, and no suppression is allowed (#pragma warning disable / SuppressMessage / eslint-disable / @ts-ignore).
  • Dependency vulnerabilities are fixed by upgrading the package (pinning transitive dependencies to patched versions), not by ignoring the advisory.
  • README and site documentation are bilingual and must be updated together; screenshots live in docs/screenshots/ with stable names.