The crash is the spec
Most agent demos assume the process stays alive. A real local gateway cannot. The machine sleeps, the terminal closes, the provider stalls, and the process dies. If the record is only in memory, the next start cannot tell whether a tool ran, whether it was about to run, or whether the user ever saw the result.
DreenMind writes the user message, the assistant message, and the tool-call row before executing the tool. That ordering is the design. A crash can leave an interrupted call, but it cannot leave an action with no record. On startup, the gateway closes an interrupted tool call with a synthetic error instead of replaying it. Replaying an external effect is not recovery; it is a second effect.
The database is not a log file bolted onto the runtime. It is the source of truth. The model context is a projection of the record. This is slower than keeping everything in a slice, and it is the only way to make a credible claim about what happened after a failure.
The remaining gap is honest: the turn is recoverable, but the external effect may not be. If a tool wrote a file and the process died before recording the result, the gateway can report an interrupted call; it cannot know the file’s final state. The design reduces ambiguity. It does not abolish it.
The gateway owns the state
It is tempting to let every client open the SQLite file. The CLI can read it, the bot can write it, and the desktop app can do both. That works until two writers disagree. DreenMind chooses one writer: the gateway. The CLI is an HTTP client. Telegram is an adapter. Neither owns the database.
This choice makes the failure model simpler. There is one place to enforce schema changes, one place to check integrity, and one place to decide whether a turn is valid. It also makes the CLI less magical: if the gateway is down, commands fail. They do not queue work in a local file and hope a later process picks it up.
FTS5 is derived state. It can be rebuilt without pretending the conversation was recreated. That distinction matters during repair. The messages are the record; the index is a convenience.
An approval is a capability
An approval is not a thumbs-up emoji. It is a narrow, single-use capability to perform a specific action. DreenMind scopes the request to the tool and arguments, expires it after 15 minutes, and waits at most 60 seconds for a decision. The decision is recorded with the actor and the outcome.
The capability model changes the UI. A Telegram button is a convenient lie if it does not carry the exact scope. The gateway exposes approval decisions through the CLI and HTTP API, where the operator can see the request. Telegram remains a message channel, not an authority channel.
Single-use matters because a model can ask twice. If the first approval covered “write this file,” the second request must be a new decision. The gateway does not infer consent from a previous turn.
The trade-off is friction. The operator has to look. That friction is the point: sensitive tools are not supposed to feel automatic.
Exact hosts, not good intentions
A web fetch tool is an SSRF primitive unless it is constrained. DreenMind uses an exact-host allowlist. A suffix match is not enough; a wildcard is not enough; “block localhost” is not enough. The host must be named. DNS is revalidated, private and link-local addresses are blocked, and redirects are not a path around the list.
The tool also bounds the response. It checks content size, treats the body as untrusted data, and does not let the page’s text become instructions. The model can read the result; it cannot promote it into policy.
This is deliberately less useful than a general browser. A general browser would need a much larger threat model, including JavaScript, cookies, downloads, and user sessions. DreenMind does not pretend to have that model.
The right question is not “can it fetch this URL?” It is “who decided this host was allowed, and what happens when the host changes?” The allowlist is the answer, and it is intentionally small.
What DreenMind refuses to do
DreenMind refuses streaming, embeddings, images, audio, semantic memory, teams, group chats, plugin loading, MCP, shell execution, and a hosted control plane. Some of those are future work. Some are permanent non-goals. The website should not blur the difference.
The refusal is a test strategy. Every added edge needs authentication, a failure model, a data boundary, and test evidence. A feature that cannot answer those questions is not ready, no matter how impressive the demo looks.
The current tree is a working local gateway. The blog explains the decisions behind it, not a release cadence.
Status labels are part of the product
“Implemented,” “tested,” “verified,” “partial,” “planned,” and “unverified” are not decoration. They describe evidence. A provider adapter tested against a fake server is tested, but its live behavior is unverified. A Docker contract tested without an escape suite is partial. A checksummed draft artifact is not a signed release.
Labels prevent the most common failure in developer marketing: turning a code path into a promise. They also make the next step obvious. If an edge is unverified, the work is a live test with a named environment, not a redesign.
The site uses the same labels as the repository. If a claim cannot be labeled, it should not be made.