# DS4Server implementation plan Bundle/application identifier: `DS4Server.rfc1437.de` The GUI is a Rust/Iced reimplementation of the user-facing model server and coding-agent workflows in `../ds4`. The existing engine remains the behavioral reference: `ds4.c`/`ds4.h` define model and session behavior, `ds4_server.c` defines the compatible API, and `ds4_agent.c` defines agent sessions, tools, and turn orchestration. ## Current status | Area | Status | Available now | Still open | | --- | --- | --- | --- | | App shell | Implemented | Native multi-window Iced app, Codex-inspired layout, native Application/Edit/Window menus, Preferences panel, and Model Manager window | Functional chat content and native app packaging | | Projects and sessions | Implemented for metadata | Native folder picker, project-name dialog, create/select/delete projects and sessions | Transcripts, KV payloads, rename/archive, and model binding | | Persistence | Implemented for metadata and preferences | SQLite in Application Support, Diesel ORM, embedded migrations, constrained defaults and CRUD tests | Messages, downloaded-artifact metadata, and session-runtime migrations | | Model runtime | Model intake implemented | Typed DS4 preferences, managed downloads, mmap-backed GGUF v3 loading, complete DS4 shape/quantization validation, DeepSeek/GLM tokenization, and prompt rendering | Metal inference, sessions/KV state, lifecycle coordination, and idle unloading | | Local endpoint | Not started | Nothing listening | OpenAI-compatible HTTP and streaming surfaces | | Agent | UI shell only | Empty conversation pane and composer placeholder | Messages, generation, tools, approvals, compaction, and cancellation | | A2UI | Future extension | bDS2 provides the reference structured render-tool contract | Native inline surfaces for local chat after core chat and tools are stable | | Dev brain | Not started | No vault access | Obsidian vault selection, durable access, and agent knowledge/memory tools | | Release | Not started | Development binary builds and tests | `.app` packaging, signing, entitlements, and notarization | The application currently manages durable project/session metadata and model preferences. Its separate Model Manager can download, fully validate, and delete main and DSpark GGUF artifacts in managed Application Support storage without blocking the UI. The engine can mmap a configured model, validate its metadata and complete tensor layout, load its tokenizer, and render a prompt; the app does not yet acquire that engine for inference, serve HTTP, save chat messages, or run an agent. ## Phase 0 — project and session shell Status: **implemented**. - Show projects in a sidebar with their sessions nested underneath. - Create projects with a native macOS folder picker and a project-name dialog. - Create, select, and delete projects and sessions. - Use a Codex-inspired two-pane layout with embedded, action-specific SVG icons. - Persist projects and sessions in SQLite under `~/Library/Application Support/DS4Server.rfc1437.de/`, using Diesel ORM queries and embedded Diesel migrations. - Never delete workspace contents when removing app metadata. Exit criterion: restart the app and see the same project/session tree. ## Phase 1 — model library and on-demand loading Status: **partially implemented**. The typed DS4 preference contract, shared effective-settings builder, background download workflow, and model-intake boundary are implemented; Metal inference, session state, and idle lifecycle coordination remain open. 1. **Implemented:** the typed model/runtime preference contract described below is complete before chat, the HTTP endpoint, or the engine lifecycle. 2. **Partially implemented:** the mmap-backed GGUF loader, DeepSeek/GLM tokenizer, prompt renderer, and model `open`/`close` ownership boundary are complete. The Metal session API modeled on `ds4.h` remains: `create_session`, `sync`, `sample`, and KV save/load. 3. **Implemented:** retain the tested model restrictions from DwarfStar. Main and DSpark artifacts reject invalid GGUF versions, catalog mismatches, and incompatible metadata, tensor shapes, offsets, or quantization before promotion and again before model use. 4. Reuse the existing `.metal` kernels and preserve mmap-backed resident loading plus the explicit SSD-streaming path. Keep Objective-C only at the Metal interop boundary. 5. Keep one engine resident at most, matching DwarfStar's instance-lock and memory assumptions. 6. Move engine work off the UI thread and support cooperative cancellation at the safe session boundaries already defined by `ds4_session_sync`. ### DS4 KV-cache port and shared session core KV-cache behavior is part of the DS4 engine contract, not a new application cache to redesign. Port the existing `ds4_session` and `ds4_kvstore` behavior to Rust while preserving its semantics and on-disk compatibility: - A live engine session owns one mutable inference timeline: exact rendered tokens, logits, and the DS4-specific KV graph state. Callers provide the full token prefix and the Rust port of `ds4_session_sync` must retain the longest compatible prefix, evaluate only its suffix, or safely rewind, invalidate, and rebuild exactly when DS4 does. - Carry over DS4's safe cancellation/checkpoint boundaries, common-prefix matching, canonical rewrite and replay, snapshots, payload staging, save/load, strip-to-transcript behavior, and atomic replacement. The engine continues to own the opaque KV payload format; UI, HTTP, and persistence code must not reinterpret tensor state. - Port `ds4_kvstore` policy rather than replacing it with a generic cache: exact token and rendered-text prefix lookup, cold and continued aligned checkpoints, model/quantization/context/payload-ABI compatibility checks, tool-call replay metadata, disk-budget enforcement, hit tracking, and eviction behavior. - Use one process-wide Rust session registry and KV checkpoint store for both local chat and the HTTP server. A remote request bound to an application session resolves to the same logical DS4 session as the GUI and mutations are serialized so two callers never write one live timeline concurrently. Stateless HTTP requests still participate in the same compatible-prefix checkpoint pool, so a prefix produced locally can be reused remotely and vice versa. - Store that shared checkpoint pool under Application Support in `kv-cache/`, using the DS4 key/header/payload rules. Session rows hold only an optional reference to their current checkpoint; the large opaque payload is not copied into separate GUI and HTTP caches. - Sharing does not merge unrelated conversations or expose one session's transcript to another. It shares the DS4 implementation, checkpoint pool, and explicitly bound logical session; each unrelated live conversation keeps its own mutable session state. - Engine unload must first leave every reusable live timeline in a valid persisted checkpoint. Reload restores a compatible checkpoint immediately; if model identity, quantization, context size, payload ABI, or rendered history is incompatible, rebuild from the transcript and replace the stale checkpoint only after the new state is valid. ### Lazy lifecycle - Start the application and the local HTTP endpoint with no model loaded. - Acquire the configured model only when generation starts, whether initiated by a local chat or an OpenAI-compatible endpoint request. Concurrent first requests share the same load instead of opening the model more than once. - Expose `Unloaded → Downloading → Loading → Ready → Unloading` plus failure states and progress in the GUI. A request that triggered loading waits for the shared load and receives a clear error if download or initialization fails. - Treat active local generations, endpoint requests, prefills, and tool turns as model activity. Start the idle timer only when all activity has stopped. - Unload the engine after the configured idle timeout. New work cancels a pending unload; work arriving after unload transparently loads the model again. Persisted transcripts and KV files remain available when model memory is released. - Changing the selected model prevents new work from using the old model and unloads it as soon as current work reaches a safe boundary. ### Preferences foundation (implemented) - Provide a Preferences panel from the sidebar and the standard macOS `Command-,` shortcut. - Let the user choose the active model. Default to `deepseek-v4-flash`. - Let the user enable or disable DSpark for the selected model. Disable the control for models that do not support DSpark. - Let the user configure the inactivity timeout; default to 10 minutes. - Persist preferences in the application SQLite database. Changing preferences never loads a model by itself. ### DS4 model and generation preferences (implemented) The Preferences panel must expose every persistent model, runtime, and generation parameter accepted by the `ds4` command line. `../ds4/ds4_cli.c`, `ds4.h`, and the shared GPU/distributed parsers are the source of truth; use the same defaults, ranges, units, dependencies, and model-family overrides rather than inventing application-specific behavior. - **Model and capacity:** selected main GGUF (`--model`), optional MTP/DSpark support GGUF (`--mtp`), context tokens (`--ctx`), maximum generated tokens (`--tokens`), and system prompt (`--system`). Managed catalog selections map to their installed files instead of requiring users to type paths. - **Sampling and reasoning:** temperature (`--temp`), top-p, min-p, optional seed, and one reasoning mode representing `--think`, `--think-max`, or `--nothink`. Preserve DS4's explicit-vs-default distinction because GLM applies model-family sampling defaults only when the user has not overridden them. - **Execution:** the macOS app uses Metal exclusively; omit the unnecessary CPU backend and backend selector. Persist CPU helper threads for host-side work, GPU power percentage, prefill chunk, exact/quality kernels, and warm weights. - **Speculative decoding:** MTP draft-token count and margin, GLM MTP, GLM MTP timing, DSpark enablement, DSpark confidence threshold, and DSpark strict target-only decode. Enabling a dependent control must enable or require its support artifact exactly as the CLI does. - **SSD streaming:** enablement, cold start, expert count or GiB cache budget, fully resident GLM layers, and explicit expert preload count. Retain the distinction between unset/automatic and an explicit zero where DS4 does. - **GPU placement:** per-device VRAM budgets, device indices, and CUDA tensor parallelism when available. Hide or disable these controls in a Metal-only or CPU-only build rather than accepting values the engine cannot honor. - **Directional steering:** direction-vector file, FFN scale, and attention scale, including the CLI rule that a file with no explicit scale defaults the FFN scale to 1. - **Advanced diagnostics that change model execution:** simulated used memory and routed expert profile output. Keep these in an Advanced section with clear diagnostic labels, but persist and pass them through like their CLI equivalents. Prompt sources, raw one-shot mode, inspect/dump/perplexity/imatrix/self-test actions, and distributed coordinator/worker network topology are CLI workflows, not persistent model parameters; they belong to their corresponding chat, diagnostic, or deployment surfaces rather than Preferences. Any future DS4 CLI option must be classified explicitly as either a preference or one of these non-preference actions. Implement this before chat as one typed settings path shared by the local GUI, the lazy engine lifecycle, and HTTP defaults: - Persist unset/automatic values separately from explicit values and provide a Reset to DS4 defaults action. Validate before saving with the same accepted ranges and cross-field rules as the CLI. - Mark settings as load-time or turn-time. A load-time change unloads/reloads at the next safe boundary; sampling, seed, token budget, and reasoning changes apply to the next turn without duplicating the engine. - Local chat uses these values directly. HTTP requests use them as defaults and may override only request-scoped generation fields supported by the API; both paths still resolve through the same effective-settings builder. - Add a focused parity test that inventories every option branch in the DS4 CLI model/runtime, sampling, and steering groups and fails until each option is mapped to a typed preference or explicitly classified as a non-preference action. Exit criterion: save, restart, and recover every supported DS4 model/runtime and generation value; show the same effective defaults and validation as the CLI; and construct one engine configuration plus one turn configuration without chat or HTTP code re-parsing preference fields. ### Targeted downloads and storage - Preferences only selects the model/runtime configuration. A separate Model Manager window, reachable from the native Window menu, lists every managed main and DSpark artifact with its on-disk size and state, and provides Download/Resume, Validate, Stop, and confirmed Delete actions. - A dedicated worker thread performs HTTP and verification work so the Model Manager and the rest of the application stay interactive. - Use an in-process Rust HTTP client with Rustls rather than invoking a system downloader. Stream response chunks directly into a `.part` file and use a Rust SHA-256 implementation for verification. - Report aggregate bytes present, total bytes, bytes missing, percentage, smoothed transfer rate, current artifact/verification phase, and estimated time remaining. Keep the detailed display in Model Manager and a persistent status bar with a Stop action while the transfer runs. - During SHA-256 verification, show a dedicated progress bar based on bytes read and hashed rather than the already-complete file size. Update the bytes verified, verification throughput, percentage, and estimated time remaining throughout the pass so large models never appear stalled at 100%. - Stop is non-destructive: close the active transfer and retain its `.part` file. Starting the download again in the same process or after quitting and relaunching must derive progress from that file and issue an HTTP range request from its exact byte length. If the server ignores the range request, safely restart that artifact from byte zero rather than appending corrupt data. - Promote a `.part` file only after its exact size and SHA-256 are verified. A stopped verification keeps the complete partial file so the next run can verify and promote it without downloading it again. - Store managed model artifacts under `~/Library/Application Support/DS4Server.rfc1437.de/models//` and mmap/load them from there. Do not place managed models in the app bundle or a project directory. - Download only the artifact explicitly requested in Model Manager; never prefetch other catalog models or optional artifacts. - Remove checksum-invalid partial files without touching a valid model. Keep partial files after cancellation, application exit, network failure, or an interrupted verification so they remain restart-resumable. - Delete model artifacts only through an explicit, confirmed Model Manager action; changing Preferences must never remove files. Exit criterion: select a supported DeepSeek V4 or GLM 5.2 GGUF, load it without blocking the UI only when a chat begins, run a deterministic prompt, unload it after the configured idle timeout, and pass the matching `../ds4` token-output fixture. Repeat through the local endpoint and verify both paths share the same engine lifecycle. ## Phase 2 — OpenAI-compatible local endpoint Status: **open; not started**. 1. Add a localhost-only HTTP service whose lifecycle is independent of the engine. It can listen while the model is unloaded and acquires the configured model only for inference. Start and stop it from the GUI; never expose a LAN listener without an explicit setting. 2. Implement the DwarfStar compatibility surface in this order: - `GET /v1/models` - `POST /v1/chat/completions` - `POST /v1/responses` - `POST /v1/completions` `GET /v1/models` reports the configured model without forcing it to load. 3. Support streaming SSE, reasoning output, usage accounting, cancellation, sampling parameters, tool schemas, and tool choice. 4. Port exact sampled tool-call replay and deterministic canonicalization from `ds4_server.c` so a client's normalized JSON does not destroy KV-prefix reuse. 5. Route API conversations through the shared session registry and KV store defined in Phase 1. Responses `conversation` bindings and any other persistent request identity map to the same application session used by the GUI; stateless requests use shared DS4 prefix lookup. Persist/restore only through the engine-owned serialization format. Add resident batching only after single-session and cross-surface correctness are established. 6. Display endpoint address, model, active requests, token rates, and errors in the GUI. Exit criterion: the upstream DwarfStar server tests pass against the app for non-streaming and streaming Chat Completions and Responses calls, including a multi-turn tool call. A compatible prefix created by local chat must register a remote cache hit, a remote checkpoint must be reusable by local chat, and a GUI/HTTP race on one bound session must serialize without corrupting its token or KV frontier. ## Phase 3 — durable agent sessions Status: **partially open**. Project/session metadata rows exist; transcripts, model binding, and KV payload persistence are not implemented. Extend each metadata-only session with transcript and shared-checkpoint metadata: ```text Application Support/DS4Server.rfc1437.de/ data.sqlite3 models// main.gguf dspark.gguf # only when enabled kv-cache/ .kv ``` - Add migrated message/transcript tables and an optional current-checkpoint key to SQLite. Keep large engine-owned KV payloads only in the process-wide Phase 1 store, written with atomic replacement; do not introduce separate agent-only or HTTP-only cache formats. - Record model identity, context size, rendered token history, title, timestamps, and working directory. - Restore compatible KV immediately. If KV is absent or incompatible, rebuild from the transcript and show prefill progress. - Add strip/archive behavior equivalent to the native agent: retain the transcript and clear its checkpoint reference. Remove the cache file only when it is not reusable by another live/session prefix; otherwise normal DS4 disk-budget eviction reclaims it. - Version formats before the first release and add migrations only when a real format change exists. Exit criterion: quit during normal use, relaunch, select a session, and resume the same conversation without prefill when its KV payload is compatible. ## Phase 4 — agent chat and tools Status: **UI shell only**. The conversation area, composer, and icons are visual placeholders with no message or agent runtime behind them. 1. Build the classic chat surface: transcript, reasoning disclosure, tool-call cards, composer, queued input, stop button, prefill progress, and generation statistics. Local turns must call the same session registry and KV path used by the HTTP endpoint. 2. Port the native agent turn loop from `ds4_agent.c`, including model-specific system prompts and DeepSeek DSML/GLM tool-call parsing. 3. Implement the local tool set with the project directory as its boundary: - shell command execution - file read and continuation - file write - anchored edit - text/file search - opt-in Dev Brain knowledge lookup and memory storage 4. Stream assistant tokens and partial tool calls into Iced while inference and tools run on workers. Preserve cooperative interruption and a valid KV prefix at every stop point. 5. Require confirmation for commands or writes outside the project boundary, destructive actions, and network tools. Add web search/page visiting only after the local tool loop is stable. 6. Port context compaction and system-prompt reinjection after ordinary multi-turn operation is correct. ### Native macOS menus (required with chat) The application must install and maintain a complete native macOS menu bar; the application-name menu alone is not sufficient. Use platform menu roles, ordering, selectors, and standard key equivalents rather than drawing menus inside the Iced window. - Provide the standard **Application**, **File**, **Edit**, **View**, **Window**, and **Help** menus. Include the normal macOS Application actions (About, Settings, Services, Hide, Hide Others, Show All, and Quit) and Window actions (Minimize, Zoom, Bring All to Front) with native behavior. - Make File actions operate on real application concepts: New Session, Add/Open Project, and Close Window. Make View actions cover actual available views, including sidebar visibility and Enter Full Screen. Help opens the local help or documentation surface. Do not add permanently inert placeholder items. - The Edit menu is part of the chat acceptance criteria. Undo, Redo, Cut, Copy, Paste, Paste and Match Style where supported, Delete, and Select All must act on the current first responder. They must work in the chat composer and every other editable text field; Copy and Select All must also work for selectable transcript text. - Prefer native first-responder selectors for native controls. Where an Iced widget owns focus, bridge the same menu command into that focused widget so menu clicks and `Command-Z`, `Shift-Command-Z`, `Command-X`, `Command-C`, `Command-V`, and `Command-A` behave identically. - Validate menu enabled state whenever focus or application state changes: editing commands reflect the focused selection/clipboard and session/project commands reflect whether their action is currently possible. Menu actions must use the same application messages as toolbar/sidebar actions instead of duplicating business logic. - Preserve conventional macOS labels, grouping, keyboard shortcuts, and accessibility metadata. Keep platform-specific menu integration behind the macOS application shell so the chat and session core remain platform-neutral. Exit criterion: with the composer focused, every standard Edit command works from both the menu and its keyboard shortcut, including undo/redo across multiple edits; transcript text can be copied; and Application, File, View, Window, and Help expose functional, correctly enabled native actions. ### Dev Brain — Obsidian knowledge and memory - Add an optional Dev Brain setting in Preferences. Use the native macOS folder picker to select an existing Obsidian vault, allow the vault to be changed or disconnected, and persist its canonical path plus a security-scoped bookmark in SQLite so sandboxed builds can regain access after restart. - Keep the vault global to the application rather than tied to one project or session. The selected vault remains in its original location; never copy it into Application Support or a project directory. - Add one local `dev_brain` agent tool with the minimum useful operations: search Markdown notes, read a selected note, and create or append a memory note. Return vault-relative paths with results so the agent can cite where knowledge came from. - Restrict every operation to the explicitly selected vault. Resolve and check paths before access, ignore `.obsidian` and hidden files, and do not provide deletion or arbitrary overwrite operations initially. - Let the agent retrieve relevant global knowledge during a turn and record durable conclusions after a turn. Vault access is opt-in, does not preload the model, and must not block sessions when no vault is configured or available. - Reuse the existing local file/search implementation underneath the new vault boundary instead of building a second filesystem stack. Exit criterion: create a project session, ask the model to inspect and edit a small fixture repository, review each tool result in the GUI, restart, and continue from the persisted session. With Dev Brain enabled, store a memory in one session and retrieve it with a cited vault-relative note path from a session in another project after restarting the app. ## Phase 5 — A2UI rich local-chat surfaces Status: **future extension; not part of the core milestone**. Start only after ordinary local chat, structured tool calls, transcript persistence, and cancellation are stable. Use the A2UI implementation in `../bDS2` as the behavioral reference, especially its render-tool schemas, typed surface normalization, inline message placement, chart geometry, and interaction state. - Expose typed `render_card`, `render_chart`, `render_form`, `render_list`, `render_metric`, `render_mindmap`, `render_table`, and `render_tabs` tools to the local agent. Keep these tools presentation-only; they return structured data and do not perform project mutations themselves. - Support the bDS2 chart set initially: bar, stacked bar, line, area, pie, donut, and heatmap. Render charts and mind maps with native Iced primitives and SVG geometry inside the assistant message rather than adding a web view. - Treat tool arguments as untrusted data. Validate type, nesting depth, row and series counts, numeric values, labels, action names, and payload size before rendering. Never accept model-authored HTML, JavaScript, styles, file URLs, or arbitrary commands. - Attach each validated surface to the assistant message/tool call that created it and persist the versioned structured payload with the transcript. A restarted session must reconstruct the same surface without asking the model to emit it again. - Build a surface only after its tool arguments are complete and valid while allowing ordinary assistant text to continue streaming. Malformed or unknown render payloads fall back to a bounded JSON/text representation and must not fail the surrounding chat turn. - Preserve local interaction state for forms and tabs. Dispatch form submits and card actions through a small allowlist of application actions; any action that mutates files, settings, or external state still follows the normal approval path. - Provide keyboard navigation, readable labels, theme-aware colors, and a text or tabular fallback for chart data so the information remains accessible and copyable. - Keep A2UI local-chat-only initially. Do not extend the OpenAI-compatible HTTP surface or invent a general plugin/component protocol until a real client requires it. Exit criterion: ask the local agent to present fixture data as each supported surface type; verify a chart and nested tab surface render correctly, an interactive form returns validated values, malformed payloads degrade safely, and all surfaces survive application restart with their message history. ## Verification and packaging Status: **partially implemented**. Formatting, Clippy, build, and unit-test commit gates are documented in `AGENTS.md`; database migration/CRUD constraints and the preferences shortcut have unit coverage. Model fixtures, integration tests, and release packaging remain open. - Keep unit coverage narrow: persistence round trips, format compatibility, DS4 KV prefix/rewrite/store/load parity, cross-surface session binding, request parsing, and tool-boundary checks. - Reuse `../ds4` fixtures for prompt rendering, sampling, server streaming, and KV correctness; run live Metal tests only when a supported GGUF is available. - Preserve the DwarfStar/llama.cpp notices required by `../ds4/LICENSE` for reused or adapted source and kernels. - Produce a signed `.app` with the exact identifier above, then add hardened runtime, entitlements, notarization, and update delivery as release work. ## Recommended next milestones 1. Implement the single-engine lifecycle and local generation path: lazy load, shared concurrent acquisition, activity tracking, and idle unload. 2. Put the OpenAI-compatible endpoint on that same engine lifecycle so local chats and HTTP requests cannot load duplicate engines. 3. Add transcript/KV persistence, then connect the existing conversation UI and finally port the agent tools. 4. Add the opt-in Dev Brain tool after local file/search boundaries and agent approvals are stable. 5. Add bDS2-style A2UI render tools and native inline surfaces after the core local chat/tool loop is complete.