aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 13:27:58 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 13:27:58 +0200
commit0185271050ff66d7ce14aa83a990035fc62e73e4 (patch)
tree7095368181d272c1cf53fdb0870203ed4cb72445
parent185a4ed7a605a8b22ab51beb417c155e85c811ef (diff)
downloaddesktop-assistant-0185271050ff66d7ce14aa83a990035fc62e73e4.tar.gz
desktop-assistant-0185271050ff66d7ce14aa83a990035fc62e73e4.zip
Docs: desktop-assistant is a text daemon now
README and AGENTS.md describe the socket protocol and the history instead of the voice pipeline; the voice-era findings stay, marked as such, since a return to voice starts from them and from the voice-v1 tag. The overlay spec is marked superseded, the daemon spec records what changed while planning, and the example tools lose their spoken announcements. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
-rw-r--r--AGENTS.md100
-rw-r--r--README.md102
-rw-r--r--docs/superpowers/specs/2026-09-26-overlay-design.md4
-rw-r--r--docs/superpowers/specs/2026-10-16-text-daemon-design.md16
-rw-r--r--examples/config.toml30
-rw-r--r--examples/tools/finance.toml2
-rw-r--r--examples/tools/mail.toml2
-rw-r--r--examples/tools/search.toml2
-rw-r--r--examples/tools/weather.toml2
9 files changed, 128 insertions, 132 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 4aef988..45d6e31 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,42 +1,49 @@
# desktop-assistant
-A local desktop voice assistant: speak, get a spoken answer, nothing leaves
-the machine. Everything runs on already-packaged local tools; this repo is
-the glue between them.
+A local text assistant daemon: questions in and streamed replies out on a
+Unix socket, answered by the model already loaded in llama-server, with
+read-only tools and a SQLite history. Nothing leaves the machine except
+what an enabled tool fetches.
-Status: **first working loop** in `assistant.py` (2026-09-25). Mic button
-on/off is push-to-talk, whisper large-v3-turbo on the Arc transcribes,
-the resident llama-server model answers, Kokoro speaks it. Button-off to
-first spoken word ~2 s. See "Findings" and "Next steps".
+Status (2026-10-16): **text daemon**. The front end is the quickshell
+canvas (`desktop/assistant/` in the quickshell repo, SUPER+Home). Spec:
+`docs/superpowers/specs/2026-10-16-text-daemon-design.md`.
-Tools (2026-09-26, phase 1 of 2): the model can call read-only tools
-(time, calendar, weather, search) defined as shell-command TOML files in
+The voice version is the tag `voice-v1`: push-to-talk on the mic's mute
+button, whisper large-v3-turbo, Kokoro. The sections from "Hardware"
+through "Kokoro-82M" describe that version and its measurements. They are
+kept because a return to voice starts from them.
+
+Tools (2026-09-26): read-only shell-command TOML files in
`~/.config/desktop-assistant/tools/`; `config.toml` there overrides the
-built-in settings. Examples in `examples/`. STT, LLM and TTS stay local;
-only the weather and search tools reach the network, and only when a
-question needs them. Spec: `docs/superpowers/specs/2026-09-26-tools-and-config-design.md`.
+built-in settings. Examples in `examples/`. Spec:
+`docs/superpowers/specs/2026-09-26-tools-and-config-design.md`.
-Run: `python3 assistant.py` (`--test` for the self-check), or `./assistant.sh restart`
-to replace a running one (and its overlay) detached from the terminal.
+Run: `python3 assistant.py` (`--test` for the self-check), or
+`./assistant.sh restart` to replace a running one detached from the
+terminal.
---
## Pipeline
```
-mic button ──> arecord ──> whisper-server (STT) ──> llama-server (LLM) ──> Kokoro (TTS) ──> aplay
+socket (ask) ──> llama-server (LLM, tools) ──> socket (think, token, tool, done)
+ └─> history.db (SQLite, FTS5)
```
-| Stage | Package (installed) | Binary | Notes |
-|-------|---------------------|--------|-------|
-| STT | `whisper.cpp` 1.9.4 | `whisper-server` (also `whisper-stream`, `whisper-cli`) | SDL2 mic capture, ffmpeg decoding, Vulkan + OpenVINO backends |
-| LLM | `llama.cpp-vulkan` 0.5.0 | `llama-server` | already running as a system service, see below |
-| TTS | `python3-kokoro-onnx` 0.6.1 | (Python module) | uses system `python3-onnxruntime` and `espeak-ng`, no `espeakng-loader` |
+- **Concurrency:** one reader thread per client, one reply thread per
+ question. One lock guards the conversation, the turn counter and the
+ database. `say()` checks the turn under it before each emit, and stop
+ bumps the turn under it, so a stopped reply cannot emit after its `done`.
+- **One daemon:** `serve()` probe-connects first and refuses to start over a socket that answers; `main()` removes the socket only if it served it.
+- **Commands** are type-checked before they run (ids must be `int`: SQLite would match `"1"` to row 1 while the Python comparison with the current id would not) and run under a try, so a failing command answers `error` instead of killing the client's reader.
+- **Client sockets** have a 2 s timeout, not non-blocking mode: a large
+ `hello` would not fit a non-blocking send and would drop the client.
+- **Search** quotes the query as one FTS5 phrase with a prefix star. The
+ self-check proves typed FTS syntax (`"`, `*`, `NOT`, `OR`, `)`) is
+ matched as text and raises nothing.
-The three packages are in the `my-slackbuilds` repo
-(`../slackbuilds-superrepo/my-slackbuilds/`). Package-level changes (build
-options, backends, bumps) happen there, under that repo's `AGENTS.md`, not
-here. `openvino` 2026.4.0 is also packaged there, solely to feed whisper.cpp.
## Hardware
@@ -106,6 +113,26 @@ not import from it.
## Findings
+### Text daemon, live check (2026-10-16)
+
+Model Gemma4-12B-qat-mtp, ctx 32768, preset reasoning-budget 4096.
+
+- Thinking on: `think` events arrive (1017 deltas for the symlink question;
+ the first after 2.8 s). Reasoning took about 21 s, so the first token came
+ at 23.8 s for a two-line question. This is why `thinking` became a config
+ key (default true).
+- "What time is it?": no tool call; the date stamp in the prompt answered
+ it. First token 4.9 s.
+- "How many unread mails do I have?": tool round in order `tool` mail
+ done=false, `state` tool, `tool` mail done=true (0.12 s later), `state`
+ thinking, tokens; first token 6.7 s.
+- ctx used went 1166, 1247, 1275, 1603 across the three questions and the
+ tool round.
+- `list` returned the one conversation, `search` "symlink" returned it with
+ a `**symlink**` snippet.
+- History dir 700; `history.db`, `-wal` and `-shm` 600.
+- SIGTERM logged `exit: SystemExit 0` and removed the socket.
+
### Models (2026-09-25)
Everything under `/data/voice-models/` (outside the repo, kept apart from
@@ -645,22 +672,17 @@ kokoro-onnx GitHub release `model-files-v1.0`.
## Next steps
-Steps 1-5 of the original plan are done (models, backend comparison, live
-mic, TTS, glue); push-to-talk came for free from the mic button. Open, in
-no particular order:
-
-- **Latency:** STT ~1.2 s is the biggest share; the double encoder pass
- (detect + transcribe) is the target if it matters.
+- **Canvas:** the quickshell side, `desktop/assistant/`, its own plan in
+ that repo.
+- **Local memory:** `docs/superpowers/plans/2026-09-30-local-memory.md`
+ still applies; its tasks 1-4 target this daemon, the voice-specific
+ bits of task 4 aside.
+- **llamachat ports**, if the canvas replaces llamachat for good:
+ attachments, skills, a model picker, model-written titles.
- **Phase 2 tools:** tools that write or modify (events, messages,
- desktop), own spec, after phase 1 has been used for a while.
-- OpenVINO in the whisper.cpp package: runs on CPU only here (no Intel
- compute-runtime), decision goes to `my-slackbuilds`.
-- **Wake word:** `hey_jarvis` works in quiet rooms, fails under loud
- music (3/8); no echo canceller in pipewire. Pick an option from
- "Wake word trial" before building anything.
-
-Open questions: whether an audio-capable LLM makes whisper unnecessary.
-License: GPLv2 only (chosen 2026-09-25).
+ desktop), with a review step, own spec.
+- **Voice:** start from `voice-v1`. The open items from that era (STT
+ latency, wake word, OpenVINO) are in the sections above.
## Package-side leftovers (tracked in my-slackbuilds, not here)
diff --git a/README.md b/README.md
index c6d6f2f..61c2aa8 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,73 @@
# desktop-assistant
-A local push-to-talk voice assistant. Unmute the microphone, speak, mute it:
-the assistant transcribes what you said, asks a local LLM, and speaks the
-answer. Italian and English. Speech, transcripts and the model stay on the
-machine; only tools you enable (weather, web search) reach the network.
+A local text assistant daemon. Questions arrive on a Unix socket, the
+model already loaded in a local llama-server answers, calling read-only
+tools when a question needs them, and the reply streams back token by
+token. Every conversation is kept in a local SQLite history with
+full-text search. The model and the history stay on the machine; only
+tools you enable (weather, web search) reach the network.
-```
-mic button ──> arecord ──> whisper-server (STT) ──> llama-server (LLM) ──> Kokoro (TTS) ──> aplay
-```
+The front end is the assistant canvas of the quickshell desktop
+(`desktop/assistant/` in the quickshell repo, on SUPER+Home). Anything
+that speaks JSON lines on a Unix socket can drive it.
-## How it works
-
-- **Push-to-talk** is the microphone's own mute button: the script listens to
- the ALSA `Mic Capture Switch` (`amixer events`). Switch on starts a
- recording, switch off ends it.
-- **Speech to text**: whisper.cpp's `whisper-server` with the
- `large-v3-turbo` model on the GPU (Vulkan). The script starts it if it is
- not already running and stops it on exit.
-- **LLM**: an OpenAI-compatible `llama-server` on `localhost:8181`. The
- script uses whichever model the server already has loaded, so it never
- swaps models under other clients.
-- **Text to speech**: Kokoro-82M through `kokoro-onnx`, one voice per
- language, chosen per sentence from the language of the reply. Speech
- starts after the first sentence, while the rest is still being generated.
-- **Calls**: while the desktop's presentation mode is on
- (`statusctl presentation get` returns `1`), button presses are ignored, so
- a call can use the microphone.
+The voice version (push-to-talk, whisper, Kokoro) is the tag `voice-v1`.
## Requirements
-- whisper.cpp (`whisper-server`) and a whisper ggml model
-- a running `llama-server` with an OpenAI-compatible API
-- Python 3 with `onnxruntime`, `numpy` and `kokoro-onnx`
-- `espeak-ng`, ALSA utils (`amixer`, `arecord`, `aplay`), `curl`
-- `jq` for the weather and search example tools, `python3-dateutil` for the calendar one
-- a USB microphone with a hardware mute button exposed as an ALSA switch
-
-Paths, the card name, ports and voices can be changed in `config.toml`, see
-"Configuration and tools" below.
+- a running `llama-server` with an OpenAI-compatible API, router mode
+- Python 3 (stdlib only; SQLite with FTS5)
+- `curl` and `jq` for the weather and search example tools,
+ `python3-dateutil` for the calendar one
## Usage
```sh
python3 assistant.py # run
python3 assistant.py --test # self-check
-./assistant.sh start # detached, with the overlay
+./assistant.sh start # detached
./assistant.sh stop
-./assistant.sh restart # stop a running one, start again
+./assistant.sh restart
./assistant.sh status
```
+The daemon uses whichever model the router has loaded, so it never swaps
+a model out under another client.
+
+Only one daemon serves the socket: a second one refuses to start while the
+first answers.
+
+## Protocol
+
+JSON lines both ways on `$XDG_RUNTIME_DIR/desktop-assistant.sock` (mode
+0600). Every connected client sees every event.
+
+In: `ask {text}`, `stop`, `new`, `list`, `open {id}`, `search {q}`,
+`delete {id}`.
+
+Out: `hello` on connect (state, model, context use, the current
+conversation and any reply in flight), then `user`, `think`, `token`,
+`tool`, `done`, `state`, `ctx`, `error`, and the answers `conversations`,
+`conversation` and `results`. The full table is in
+`docs/superpowers/specs/2026-10-16-text-daemon-design.md`.
+
+Ids are integers; a malformed command or argument answers `error` and the
+connection stays open.
+
+Try it by hand:
+
+```sh
+socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/desktop-assistant.sock
+{"type":"ask","text":"What time is it?"}
+```
+
+## History
+
+`~/.local/share/desktop-assistant/history.db` (`$XDG_DATA_HOME`
+honoured), directory 0700, file 0600. The model is sent the last 10 turns
+of the open conversation. Tool results are stored as a placeholder, not
+their output. Reasoning is shown live and not stored.
+
## Configuration and tools
Everything is optional. Settings and tools live in
@@ -71,8 +89,6 @@ cp -r examples/tools ~/.config/desktop-assistant/
description = "What the tool does, for the model"
command = ["script.sh", "{param}"] # looked up in tools/ first, then PATH
timeout = 10
-announce.it = ["Cerco un attimo."] # optional, said while it runs
-announce.en = ["Let me look that up."]
[params.param]
type = "string"
@@ -99,18 +115,6 @@ The example tools only read:
full markdown recap opened in `MD_EDITOR` (default `xdg-open`); `CHANGELOG`
and `PKGDIR` in `[env]` override the mirror path and package database
-## Overlay
-
-The assistant broadcasts its state, context use, the running tool and the
-audio levels as JSON lines on `$XDG_RUNTIME_DIR/desktop-assistant.sock`
-(mode 0600). The overlay is the `assistant/` component of the quickshell
-repo: a circle with the name, `used / max` context and the tool, pulsing
-dots while it transcribes or thinks, and a ring of ripples following your
-voice and the reply. It fades out after the reply. Name, position, delay
-and colours are the `[overlay]` table in `config.toml`. With no overlay
-running the assistant works the same. A press ignored in presentation or
-gaming mode sends a normal-urgency notification instead.
-
## License
Copyright (C) 2026 Danilo M. <danix@danix.xyz>
diff --git a/docs/superpowers/specs/2026-09-26-overlay-design.md b/docs/superpowers/specs/2026-09-26-overlay-design.md
index e45819f..3c4aef3 100644
--- a/docs/superpowers/specs/2026-09-26-overlay-design.md
+++ b/docs/superpowers/specs/2026-09-26-overlay-design.md
@@ -1,6 +1,8 @@
# Assistant overlay: design
-Date: 2026-09-26. Status: approved in brainstorming, not implemented.
+Date: 2026-09-26. Status: **superseded** by
+`2026-10-16-text-daemon-design.md`; the overlay and the voice pipeline it
+drew were removed (tag `voice-v1`).
## Goal
diff --git a/docs/superpowers/specs/2026-10-16-text-daemon-design.md b/docs/superpowers/specs/2026-10-16-text-daemon-design.md
index 30f202b..40a73c8 100644
--- a/docs/superpowers/specs/2026-10-16-text-daemon-design.md
+++ b/docs/superpowers/specs/2026-10-16-text-daemon-design.md
@@ -54,7 +54,7 @@ merging, `--test`, `assistant.sh`.
- **Thinking.** `chat_template_kwargs.enable_thinking` becomes true.
`delta.reasoning_content` is streamed as its own `think` event, so the
canvas can collapse it. Reasoning is not stored and not sent back to the
- model.
+ model. It is the config key `thinking` (default true): measured live, reasoning added about 21 s before the first token of a two-line answer.
- **System prompt.** The default is rewritten for text: markdown is
allowed, there is no instruction to answer in short spoken sentences.
- **Input.** The mic loop is replaced by a reader thread per client. Each
@@ -71,6 +71,7 @@ merging, `--test`, `assistant.sh`.
JSON lines both ways on the existing socket. Every connected client sees
every outbound event, so a canvas reopened mid-reply catches up.
+A client stalled for 2 s on a send is dropped.
Inbound:
@@ -84,6 +85,8 @@ Inbound:
| `{"type":"search","q":"..."}` | answers `results` |
| `{"type":"delete","id":N}` | deletes N, answers `conversations`; deleting the current one acts as `new` |
+Ids in `open` and `delete` must be integers; a bad id, a non-string `text` or `q`, or a command that raises answers `error` (`bad id`, `bad text`, `bad query`, `<type> failed`) and leaves the conversation untouched.
+
An `ask` while a reply is streaming stops the old reply first. An unknown
or malformed line is logged and answered with `error`; it never closes the
connection.
@@ -92,21 +95,20 @@ Outbound:
| Event | Fields |
| --- | --- |
-| `hello` | `state`, `model`, `ctx_used`, `ctx_max`, `id`, `turns` (the current conversation) |
+| `hello` | `state`, `model`, `ctx_used`, `ctx_max`, `id`, `turns` (the current conversation), `pending` (`{user, reply, think, tools}` of a reply in flight, or null) |
| `state` | `state`: `idle`, `thinking`, `tool` (with `tool`) |
| `user` | `text`: the question, echoed so every client draws it |
| `think` | `delta` |
| `token` | `delta` |
| `tool` | `name`, `done` (false when it starts, true when it returns) |
-| `done` | `id`: the conversation the turn was saved in |
+| `done` | `id`: the conversation the turn was saved in; `stopped`: true for a reply that was stopped |
| `error` | `message` |
| `ctx` | `used`, `max` |
| `conversations` | `items`: `[{id, title, updated}]`, newest first |
| `conversation` | `id`, `turns`: `[{user, reply, tools}]` |
| `results` | `items`: `[{id, title, snippet}]`, best first |
-`hello` is sent on connect. A reply that was stopped emits `done` with
-whatever text it had, and that partial turn is not saved.
+`hello` is sent on connect. A stopped reply gets a `done` with `stopped: true` from whatever stopped it, and that partial turn is not saved.
## Storage
@@ -142,12 +144,12 @@ delete trigger cleans the index.
- `open` loads the last 10 turns' `messages` as the model history and all
turns' `user`, `reply`, `tools` for the canvas. Tool results come back as
the stored placeholder.
-- `search` wraps the user's text as an FTS5 phrase (double quotes doubled
- inside), so FTS syntax in the query cannot break or widen it. Results are
+- `search` wraps the user's text as an FTS5 phrase (double quotes doubled inside) with a prefix star, so FTS syntax in the query cannot break or widen it and a half-typed word still matches. Snippets mark matches with `**`. Results are
ranked by `bm25` and grouped to one row per conversation, with
`snippet()` from its best turn.
- One connection guarded by a lock: the reply threads and the client
reader threads all go through it. Only the daemon touches the file.
+- An index on `turns(conv, seq)`; `db_open` chmods the directory and file even when they already exist.
## assistant.sh
diff --git a/examples/config.toml b/examples/config.toml
index 19e1e8b..94bc1e6 100644
--- a/examples/config.toml
+++ b/examples/config.toml
@@ -3,37 +3,11 @@
# shown are the built-in defaults. An unknown key stops the assistant
# with its name, so a typo cannot fall back to a default in silence.
-# card = "Microphone" # ALSA card id of the mic
# llm = "http://localhost:8181/v1" # llama-server, OpenAI API
-# system = "You are a voice assistant. ..."
+# system = "You are a desktop assistant. ..."
# tool_rounds = 3 # tool round trips per question
# temperature = 0.6 # per request; unset = the model preset's
-
-# [whisper]
-# model = "/data/voice-models/whisper/ggml-large-v3-turbo-q5_0.bin"
-# device = 0 # -dev index, GPU devices only
-# port = 8182
-
-# [kokoro]
-# model = "/data/voice-models/kokoro/kokoro-v1.0.onnx"
-# voices = "/data/voice-models/kokoro/voices-v1.0.bin"
-
-# Style name = weight; a blend replaces the default blend whole.
-# [voice.it]
-# lang = "it"
-# blend = { if_sara = 0.8, af_bella = 0.2 }
-# [voice.en]
-# lang = "en-us"
-# blend = { af_heart = 1.0 }
-
-# The quickshell overlay (quickshell repo, assistant/). udt palette names
-# for the colours.
-# [overlay]
-# name = "Assistant"
-# position = "center" # center, top, bottom, top-left, top-right, bottom-left, bottom-right
-# hide_delay = 3.0 # seconds idle before it fades out
-# user_color = "accent"
-# assistant_color = "green"
+# thinking = true # false skips the model's reasoning (faster first word)
# Added to every tool's environment. Personal values live here, not in
# the tool files.
diff --git a/examples/tools/finance.toml b/examples/tools/finance.toml
index 1cfdc4a..8f3677f 100644
--- a/examples/tools/finance.toml
+++ b/examples/tools/finance.toml
@@ -1,8 +1,6 @@
description = "The user's money, from Firefly III: account balances, this month's budgets, or recent spending. Read-only."
command = ["finance.sh", "{what}", "{since}", "{account}"]
timeout = 20
-announce.it = ["Controllo i conti."]
-announce.en = ["Checking your accounts."]
[params.what]
type = "string"
diff --git a/examples/tools/mail.toml b/examples/tools/mail.toml
index 2f1a9d8..b94ef2b 100644
--- a/examples/tools/mail.toml
+++ b/examples/tools/mail.toml
@@ -1,8 +1,6 @@
description = "The user's email: how many unread messages and who they are from. Read-only."
command = ["mail.sh", "{query}"]
timeout = 15
-announce.it = ["Controllo la posta."]
-announce.en = ["Checking your mail."]
[params.query]
type = "string"
diff --git a/examples/tools/search.toml b/examples/tools/search.toml
index 2c6919e..7443658 100644
--- a/examples/tools/search.toml
+++ b/examples/tools/search.toml
@@ -1,8 +1,6 @@
description = "Search the web. Use it for facts, news, distances, prices, anything you are not sure about, instead of guessing."
command = ["search.sh", "{query}"]
timeout = 10
-announce.it = ["Cerco un attimo.", "Vediamo cosa trovo.", "Controllo."]
-announce.en = ["Let me look that up.", "Let me see what I can find.", "Checking."]
[params.query]
type = "string"
diff --git a/examples/tools/weather.toml b/examples/tools/weather.toml
index ddf3f8e..d9a2e8c 100644
--- a/examples/tools/weather.toml
+++ b/examples/tools/weather.toml
@@ -1,8 +1,6 @@
description = "Weather now, or the forecast for today or tomorrow. Leave place empty for the user's home."
command = ["weather.sh", "{place}", "{when}"]
timeout = 12
-announce.it = ["Controllo il meteo.", "Vediamo che tempo fa."]
-announce.en = ["Checking the weather.", "Let me check the weather."]
[params.place]
type = "string"