# 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 Iced window, Codex-inspired two-pane layout, dark theme, embedded SVG icons, `Command-,` Preferences panel | 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 | Download workflow implemented | Persisted model choice, DSpark toggle, idle timeout, and Rust-native background main/DSpark downloads with progress, ETA, stop, restart-resume, size checks, and SHA-256 verification | Loading, inference, idle unloading, and Metal integration |
| 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 |
| 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, and can download the selected model's main and optional DSpark
GGUF into managed Application Support storage without blocking the UI. It does
not yet load a model, 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**. Preferences and the complete background
download workflow are implemented; loading, inference, and idle unloading
remain open.

1. Port the narrow GGUF loader, tokenizer, prompt renderer, and Metal session
   API behind a small Rust `Engine` surface modeled on `ds4.h`:
   `open`, `close`, `create_session`, `sync`, `sample`, and KV save/load.
2. Retain the tested model restrictions from DwarfStar; reject unsupported
   shapes and quantizations before allocating model state.
3. 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.
4. Keep one engine resident at most, matching DwarfStar's instance-lock and
   memory assumptions.
5. 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 (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.

### Targeted downloads and storage

- The Preferences panel can download the selected model's main GGUF and the
  DSpark support GGUF when enabled. A dedicated worker thread performs HTTP and
  verification work so Preferences 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 Preferences and a persistent
  status bar with a Stop action while the transfer runs.
- 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/<model-id>/` and
  mmap/load them from there. Do not place managed models in the app bundle or a
  project directory.
- Download only the selected model's main artifact. Download its DSpark
  artifact only when DSpark is enabled; 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.
- When the selected model changes or DSpark is disabled, offer removal of the
  now-unused artifact; do not delete large existing downloads without explicit
  confirmation.

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/<model-id>/
    main.gguf
    dspark.gguf       # only when enabled
  kv-cache/
    <ds4-cache-key>.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.

### 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.

## 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.
