Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,25 @@ The reference implementation runs on a Mac Studio M1 in our living room at Lake
- **Private family chat** with native iOS, Android, macOS, Windows and web apps (Matrix + Element X).
- **Photo library and mobile backup** that replaces iCloud Photos for the whole family (Immich).
- **Document archive with OCR** that you photograph from your phone and the local AI files for you (Paperless-ngx).
- **Your family wiki**: an Obsidian-compatible second brain, generated from the documents, notes, and voice memos you file (memory).
- **Local AI engine** on Apple Metal GPU: voice transcription, text-to-speech, document classification (oMLX + Whisper + Piper).
- **A bot runtime in chat** that automates the small stuff: filing receipts, transcribing voice memos, status reports.
- **One CLI to operate it all**: `./stack up <thing>` and it is running.

<p align="center">
<video src="https://github.com/user-attachments/assets/9242ab58-2c0f-4fc2-bcc9-e7ce618caca8" width="280" controls muted></video>
</p>

<p align="center"><em>The archivist files and finds documents from your phone, in chat</em></p>
<table align="center">
<tr>
<td align="center" width="38%">
<video src="https://github.com/user-attachments/assets/9242ab58-2c0f-4fc2-bcc9-e7ce618caca8" controls muted></video>
</td>
<td align="center" width="62%">
<img src="docs/assets/memory-wiki-answer.png" alt="Homer asks in the family chat whether driving under the influence of Duff beer is covered by the car insurance. The archivist answers no, citing the policy documents.">
</td>
</tr>
<tr>
<td align="center"><em>The archivist files and finds documents from your phone, in chat</em></td>
<td align="center"><em>Ask the wiki a question in chat, get an answer with sources</em></td>
</tr>
</table>


Everything runs on your Mac. Nothing leaves your network unless you tell it to.
Expand Down
10 changes: 9 additions & 1 deletion docs/admin-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ famstack uses the 420xx range to avoid conflicts with anything you might already
| 42040 | code (Forgejo) |
| 42050 | chatai (Open WebUI) |
| 42060 | ai (oMLX) |
| 42061 | ai (LM Studio, optional GGUF backend) |
| 42062 | ai (Whisper) |
| 42063 | ai (Piper TTS) |
| 42070 | memory (family wiki) |

If a port is taken, stop the offender or switch to `domain` mode (see [stack-reference.md](stack-reference.md)).

Expand Down Expand Up @@ -425,7 +428,7 @@ The persistent layer of the household brain: a curated knowledge vault, rendered

Setup seeds the vault with three things: the classification ontology (the topics and document types the archivist files against), household facts, and a hand-curated correspondents layer with aliases, so "Springfield Insurance" and "Springfield Ins. Co." resolve to the same page instead of becoming duplicates.

The wiki rebuilds itself when the vault changes: file a document in chat, refresh the page. Edits happen in Forgejo (every wiki page links to its source), so the commit log doubles as the household's learning history.
The wiki maintains itself. A curator sidecar (`stack-memory-curator`) watches the vault: when new filings settle it regenerates the pages of the family members involved plus the home page (a couple of LLM calls, a few minutes after the burst), and once a night it rebuilds everything — topic pages, cross-references, the lot — while the GPU has nothing better to do. `./stack memory wiki` stays available as the manual trigger, and `[memory]` in `stack.toml` holds the knobs (see [Configuration](#configuration)). Edits happen in Forgejo (every wiki page links to its source), so the commit log doubles as the household's learning history.

| | |
|---|---|
Expand Down Expand Up @@ -465,6 +468,11 @@ schedule = "0 0 3 * * *" # Watchtower nightly image updates
[ai]
default = "mlx-community/Qwen3.5-9B-MLX-4bit" # change to match your RAM
language = "en" # "de" for German voice/transcription

[memory]
wiki_auto_rebuild = true # curator refreshes member pages after filings
wiki_rebuild_quiet_secs = 180 # how long the vault must stay quiet first
wiki_nightly = "03:30" # nightly full rebuild, local time ("" disables)
```

Key things to know:
Expand Down
6 changes: 6 additions & 0 deletions docs/agent/ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ If a precondition is missing, `./stack` prints exactly what to do. Don't improvi

**Output contract:** every command returns JSON when piped or when `--json` is passed. Force human output with `--pretty`. Exit code 0 == success.

**Applying config/compose changes to a running stacklet:** `stack up <id>` is the apply command — it re-renders `.env` and `compose up` creates new services and recreates changed ones, leaving the rest running. `restart` (= `down` + `up`) also works but takes the whole stacklet down first. Plain `docker compose restart` does neither: it won't re-render env or create new services.

## Invariants

- `stack up` is **always safe to re-run.** Failures leave the system in a valid intermediate state; re-running picks up where it failed.
Expand Down Expand Up @@ -82,6 +84,7 @@ Refuse without explicit, scoped human approval:
| 42050 | chatai | Open WebUI |
| 42060 | ai | oMLX |
| 42062 | ai | Whisper |
| 42070 | memory | Family wiki (Quartz) |

Port collisions: do not silently rebind. Surface them. The user's fix is "stop the offender" or switch to domain mode.

Expand All @@ -99,6 +102,9 @@ Port collisions: do not silently rebind. Surface them. The user's fix is "stop t
| LLM OOM / very slow | RAM tier | Edit `[ai] default` to smaller model, then `./stack setup ai`. |
| Disk full | `./stack host` | Likely the photo library. Move `data_dir` to external SSD. |
| Element warns "browser not supported" | n/a | Click Continue. The check is outdated; Element works in every modern browser. |
| Wiki pages stale after filings | `docker logs stack-memory-curator` | Curator debounces (~3 min quiet) before rebuilding; topic pages wait for the nightly sweep. Manual override: `./stack memory wiki`. |
| Wiki shows old content entirely | `docker logs stack-memory-curator` | The curator owns the vault `git pull` (the wiki container is a read-only view). Curator down/stuck → vault and wiki go stale together. |
| Curator logs "waiting for vault" | `./stack status` → memory, code | Vault not cloned yet — the memory install hooks own the initial clone. |

For symptoms not on this table: `./stack logs <id>` + `./stack errors`, paste output to the user. **Do not invent fixes.**

Expand Down
Binary file added docs/assets/memory-wiki-answer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions docs/cleanup-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
Items land here with a reason. Surface them when adjacent code is touched
(see `docs/agent/dev.md`, Pre-1.0 conventions).

## Wiki freshness follow-ups (curator shipped 2026-06-11)

The curator sidecar ships the first two freshness tiers: debounced
incremental rebuilds (persons + home) and the nightly full sweep.
Design notes that survive it, for whoever touches this next:

- **Realtime is NOT a requirement.** The mirror is realtime; the wiki
is a derived view. The nightly sweep makes the incremental person
mapping merely *helpful*, never load-bearing — worst case for a
mapping miss is "stale until tonight". Don't grow the incremental
heuristics; grow the deriver instead.
- **Page update strategy — design when it's time, but the tension is
known (2026-06-11):** full regeneration resamples page quality (a
good page can regress on the next sweep); evolving the existing
page accumulates errors that self-cite (the "Bartley [5]" finding).
Most promising middle: a fact-checking pass — "page + sources, fix
what the sources don't support, touch nothing else" — anchored to
ground truth while preserving good prose. Likely CLI shape then:
`wiki` = update/check, `wiki rebuild` = fresh full generation.
- **famstacker `wiki` command** (Server Room chat trigger) is the
missing third tier: "CLI commands are the primitives, the bot runs
or offers them". Needs the famstack API to allow the command and an
ack-then-report shape for the multi-minute run.
- `{"cmd": "notify"}` for the famstack API (containers → Server Room
via `stack messages send`) lives in the git stash
("wiki auto-rebuild: curator sidecar + API notify") — platform
piece, ship it with whichever consumer arrives first; the curator's
completion notice is a natural one.
- Rejected runtime homes, don't re-litigate: host daemons (no launchd
surface), quartz container (node image; "the wiki never writes"),
bot-runner service concept (one consumer), bot-runner image reuse
(the curator uses 2 of its 10 deps; slim image won).

## CI release gate (post-0.3)

## CI release gate (post-0.3)

The pre-tag gate in `docs/agent/dev.md` is manual; v0.3.0-beta.1 shipped
Expand Down
13 changes: 10 additions & 3 deletions lib/stack/ai/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,20 +208,27 @@ def _full_role(self, role: str) -> str:
async def complete(self, role: str, prompt: str, *,
images: "list | None" = None,
json_mode: bool = False,
model_override: str | None = None) -> str:
model_override: str | None = None,
temperature: float | None = None) -> str:
"""Run a single chat completion and return the response text.

``role`` resolves to a concrete model via `resolve_model`. Pass
``images`` (objects with ``.data``/``.mime``) for a multimodal
call, and ``json_mode=True`` to ask for a JSON object back. SDK
errors are translated to the typed LLM errors above.
call, and ``json_mode=True`` to ask for a JSON object back.
``temperature`` pins sampling when output stability matters
(the wiki generator regenerates pages and wants the same
evidence to yield the same page); None keeps the server
default. SDK errors are translated to the typed LLM errors
above.
"""
model = model_override or resolve_model(self._full_role(role))
content = prompt if not images else _content_parts(prompt, images)

kwargs: dict = {}
if json_mode:
kwargs["response_format"] = {"type": "json_object"}
if temperature is not None:
kwargs["temperature"] = temperature

try:
resp = await self._client.chat.completions.create(
Expand Down
5 changes: 5 additions & 0 deletions lib/stack/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ def _build_template_vars(self) -> dict:
# Default "family" fits famstack; deskstack or non-family
# deployments override to "office", "household", a surname, etc.
"shared_bucket": self._cfg("core", "shared_bucket", "family"),
# Wiki freshness (the memory stacklet's curator sidecar).
# Bool/int from stack.toml render as env-friendly strings.
"wiki_auto_rebuild": str(self._cfg("memory", "wiki_auto_rebuild", True)).lower(),
"wiki_rebuild_quiet_secs": str(self._cfg("memory", "wiki_rebuild_quiet_secs", 180)),
"wiki_nightly": self._cfg("memory", "wiki_nightly", "03:30"),
# AI service URLs — host-side for CLI, docker-side for containers
"ai_openai_url": ai_openai_url,
"ai_openai_url_docker": ai_openai_url.replace("://localhost", "://host.docker.internal"),
Expand Down
4 changes: 2 additions & 2 deletions stacklets/memory/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ RUN git clone --depth 1 --branch "${QUARTZ_TAG}" https://github.com/jackyzha0/qu
COPY quartz/quartz.config.ts ./quartz.config.ts
COPY quartz/quartz.layout.ts ./quartz.layout.ts

# The entrypoint keeps the vault in sync (background git pull loop) and
# then serves it with Quartz. See quartz/entrypoint.sh.
# The entrypoint serves the vault with Quartz — a pure view. The
# curator sidecar owns the git pull. See quartz/entrypoint.sh.
COPY quartz/entrypoint.sh /usr/local/bin/wiki-entrypoint.sh
RUN chmod +x /usr/local/bin/wiki-entrypoint.sh

Expand Down
Loading
Loading