# Changelog

What changed, per version, for somebody using or running LLeMbas — not a
restatement of the commit log. If a change fixed something that *looked* like it
worked, that is worth a line: those are the ones nobody would otherwise know to
stop working around.

Newest first. Versions are `__version__` in `src/lembas/__init__.py`, which is
the only place a version is written.

The first tagged release is **1.0.0**. Everything below it shipped as a running
deployment rather than as a release, and is recorded here so the release notes
for 1.0.0 have something to be assembled from.

---

## Unreleased

## 1.0.3

Two Arch-isms in the installer, both of which only a Debian machine could find.
`deploy/lxc-install.sh` had never been executed — it was reviewed and
syntax-checked, which is not the same claim — and running it is what found them.

- Fixed: **`deploy/install.sh` could not create its virtualenv on Debian**, and
  so `deploy/lxc-install.sh` could not finish. It called bare `python`, which is
  Python 3 on Arch — the machine this was written and only ever run on — and
  does not exist on Debian at all unless `python-is-python3` is installed. The
  LXC bootstrap installs `python3`, so the install aborted at the virtualenv
  step with the service user, the bind mount and the clone already made. It now
  calls `python3`, which is right on both.
- Fixed: the service account was created with `--shell /usr/bin/nologin`, which
  is where Arch keeps it and where Debian does not. Nothing invoked it — `sudo -u`
  execs directly and systemd's `User=` never reads a shell — so the account
  worked either way, but it was created pointing at a file that was not there.
  Now `/usr/sbin/nologin`, which is correct on Debian and resolves on Arch too,
  since Arch's `/usr/sbin` is a symlink to `bin`.

## 1.0.2

- **The documentation moved to the [wiki](https://git.houmeres.sk/Houmeres/LLeMbas/wiki).**
  `CLAUDE.md`, `PLAN.md` and `docs/` are gone from the repository: they are
  documentation *about* this project rather than part of it, and a clone should
  carry software. Nothing was lost — the working notes, the roadmap and the eight
  topic notes are all there, with every internal link rewritten, and the README
  now opens onto them. Where a source comment said "see `CLAUDE.md`" it now says
  "see the working notes".
- Entries below this one still name `PLAN.md` and `docs/notes/…`, and are left as
  they were written. A changelog records what happened at the time; rewriting old
  entries to match a later decision makes it a worse record, not a better one.

## 1.0.1

- Fixed: the Updates page showed **"v1.0.0 (reports 1.0.0)"** — two spellings of
  one version, in a note whose whole purpose is to warn that a tag was cut
  before the version bump. `git describe` answers with the tag's name, and tags
  here carry a `v`. Found by cutting the first release, which is the only place
  it could have been.

## 1.0.0

The first release. Every version before it shipped as a running deployment
rather than as a release; this is what those add up to, and the point at which
it is worth somebody else installing.

**What it is.** A self-hosted web interface for OpenAI-compatible endpoints.
Server-rendered, no build step, no CDN, one SQLite file. Point it at whatever
you run — llama.cpp, LM Studio, vLLM, Ollama, OpenRouter, OpenAI — and it works
the same.

### What arrived since 0.8.1

- **Things that happen because time passed.** Say "every Monday at nine" and a
  model sets it up itself, against the same recurrence rule the manual form
  uses. A run can file a **report** you read later, send you a message, or work
  in a chat of its own.
- **News that finds you.** A dot in the sidebar, a count in the tab title while
  you are looking elsewhere, and **web push** so a schedule firing at seven in
  the morning reaches a browser that is shut. Opt-in per device, and the one
  thing here that contacts an outside service — `services/push.py` says so
  plainly and says what it costs.
- **Helpers.** A reply can hand a self-contained piece of work to another model
  that runs on its own and reports back, several at once. A helper cannot ask
  questions, cannot send helpers of its own, changes nothing unless asked, and
  on a machine runs only a fixed list of read-only commands.
- **Drawing.** Point it at a ComfyUI and a model can make images, against
  workflow templates and defaults you set — size, steps, sampler, scheduler,
  checkpoint. It reviews its own result and can try again.
- **Semantic search.** Pick an embedding model and library search fuses keyword
  and meaning, so *"how do I get paid"* finds a document that says *"invoicing"*.
  Choosing none is not a degraded mode: it is byte-for-byte the keyword search
  that was always there, with nothing written and no requests made.
- **Quotas and sharing.** Monthly tokens, concurrent replies, agent wall clock,
  images a day, helpers a reply — resolved by maximum across a person's groups,
  with zero meaning *no limit*. Documents, notes, skills and reports can be
  handed to a group or a person, read-only, with a *Shared with me* filter
  everywhere. And a screen that answers **"what can this account actually do?"**
  by naming where each permission came from.
- **Make it yours.** Name, tagline, logo, favicon and launcher icons; the
  Middle-earth wording is editable data; custom themes defined as a set of
  colours rather than a stylesheet.
- **Install it and update it.** A Dockerfile, a Proxmox container script, and an
  `/admin/updates` page showing what is running, what is available and what
  changed between. The button that applies an update is opt-in and cannot do the
  work itself — it writes a file that a systemd unit picks up, because a web
  application that can restart its own service is one whose worst day is much
  worse.

### The part worth reading

Five audit passes went into this release rather than one, and they found things
that had shipped looking correct. These are the entries somebody stops working
around a bug because of:

- **Every model was told the time in a zone with no name** — on any account that
  had not chosen one, which is every account by default.
- **A helper could write files and run programs on a remote machine,
  unattended, in a mode that promises to change nothing.** `find` was on the
  read-only command list, and `find -fprintf` writes a file.
- **Two ways to get root out of the update helper**, one of which needed no
  compromise at all: root ran a script the unprivileged service account owns,
  and an update fetches that script as that account.
- **Deleting a chat left every file it held on disk** — attachments, generated
  images, all of it, with nothing that would ever look at them again.
- **Folder nesting was fully built, documented in the README, and reachable by
  nothing.** So was moving a chat into a folder.
- **The terminal silently stopped accepting input after a reconnect**, while
  output kept arriving so the panel looked healthy.
- **On the Messages screen, half the keyboard shortcuts did nothing**, because
  two scripts were loaded twice and each toggle ran twice.
- **The prompt preview could not show two thirds of what it previews.**
- **Hints and timestamps failed the contrast minimum in both themes.**

### Where the edges are

Stated because they are the things worth knowing before you rely on it:

- **Nothing executes on the machine LLeMbas runs on.** Agent chats run their
  commands over SSH on a host you choose, and the security of an agent chat is
  the security of that host. There is no sandbox here and that is deliberate —
  `PLAN.md` records the one that was designed and dropped, and why.
- **One worker.** The generation registry, the terminal sessions and the
  schedule ticker are all in-process. Two workers means two tickers and every
  schedule firing twice.
- **A restart abandons replies in flight**, keeping whatever each had.
- **Schema changes are additive.** New tables and columns apply themselves at
  startup; renames and drops are manual. The upgrade path is tested from an
  0.8.1-shaped database with rows in it.
- **Sharing grants reading only.**

2283 tests on Python 3.11, 3.12 and 3.14.

## 0.9.13

**The testing pass.** 2140 tests became 2283, and writing them found four bugs
that no amount of reading had.

- Fixed: **the terminal silently stopped accepting input after a reconnect.**
  Change the connection, or let the shell catch up after falling behind, and
  every keystroke was dropped from then on — while output kept arriving, so the
  panel looked perfectly healthy. It also announced "Disconnected. Close and
  reopen to reconnect." about a shell that had just reconnected successfully.
- Fixed: **on the Messages screen, half the keyboard did nothing.** Two scripts
  were loaded twice there, so `Alt+B`, `Alt+E`, `Alt+T` and `Alt+I` toggled
  their panel twice — which is to say not at all — while `/help` opened two
  dialogs, `/image` posted the message twice, and picking an `@` mention
  attached the file twice.
- Fixed: **pressing the microphone while the permission prompt was up opened a
  recording each time.** Only the last was stopped, so the browser's recording
  indicator stayed on until the tab was closed.
- Fixed: **a skill shared with you took its name out of your own library.**
  Creating your own was refused with "a skill called that already exists. Edit
  it instead" — naming a skill you cannot edit, because sharing grants reading
  only. The model's `skill_create` hit the same dead end. Sharing a curated
  skill with a team is what sharing is *for*.
- Hints and timestamps are readable now. `--ink-faint` failed the accessibility
  contrast minimum in **both** themes — 3.85:1 in Moria, 3.19:1 in Shire, where
  4.5:1 is the bar — so the smallest text on every screen was the hardest to
  read.
- The suite runs on **Python 3.11 and 3.12** as well as 3.14. It had only ever
  run on 3.14, while the Docker image ships 3.12 and the packaging claimed 3.11
  — so the one interpreter most people would actually run was the one nothing
  had tested.
- A `docs/notes/release-checklist.md` for the half of testing a machine cannot
  do: a real endpoint, a real machine, real hardware, a real pair of eyes.

## 0.9.12

**The security pass.** Six findings, all fixed. None is reachable by simply
visiting the site; every one of them is a boundary that was supposed to hold
and did not.

- Fixed: **a helper could write files and run programs on the remote machine,
  unattended, in a mode that promises to change nothing.** A subagent is pinned
  to a fixed list of read-only commands — and `find` was on it. `find -fprintf`
  writes a file, `find -exec` runs a program, `find -delete` removes one, and
  none of them needs a character the shell-metacharacter guard refuses. A page
  the model had just read could have asked for a helper and got an SSH key
  written into `authorized_keys`. Those flags are refused outright now, whatever
  list a command is on.
- Fixed: **an SSH connection could be pointed at `0.0.0.0` and reach the machine
  LLeMbas runs on**, with the "may a connection point here" setting still
  reading *off*. Every other spelling was caught; that one is neither a real
  destination nor a refused one, and connecting to it goes to localhost.
- Fixed, twice, in the update helper — the one place this deliberately crosses a
  privilege boundary: **root ran a script the unprivileged service account
  owns**, and **root sourced a file that account can replace**. Either turns a
  compromise of the web application into root on the host, which is exactly what
  the unprivileged split exists to prevent. The first also meant control of the
  branch was control of root, with no compromise needed at all.
  **If you installed the update helper before this, re-run the installer** —
  the old wiring stays until you do, and the update script now says so loudly
  when it notices.
- Fixed: **browser notification endpoints skipped the guard that stops the
  server being aimed at your own network.** It was the only outbound request in
  the codebase not going through it.
- Fixed: **a chat could be put in another account's folder**, and a folder hands
  its system prompt to the chats inside it — so that read a setting across an
  ownership boundary through a field that looks like a tag.
- Fixed: a `"` typed into the share panel's search box silently stopped every
  checkbox in the panel from doing anything.
- Fixed: **re-running the installer moved the update channel to `stable`** even
  on a host following `edge`. The channel lives in two places — the environment
  file the page reads and the systemd unit the button obeys — and a re-run kept
  the first while rewriting the second, so an install for some unrelated reason
  left the page naming one channel and the button deploying another. It now
  defaults to what the host already follows.

## 0.9.11

- The Updates page no longer runs the **Check the remote** button flush against
  the version and commit above it, where the two read as one block.

## 0.9.10

**The second audit pass: screens that were harder to use than they needed to
be.** Checked by rendering them in a real browser and measuring, not by reading
the CSS.

- Fixed: **the Prompts admin page put its reference material first.** The
  Variables legend and the Preview run to a screen each and sat above the tabs,
  so the editor — the thing the page is for — started two screens down and every
  tab switch had to move the whole page to be any use. On a short tab it could
  not move far enough and left the panel stranded above a screenful of nothing.
  The editor comes first now, the reference after, and the tab bar stays put:
  measured, it moved 385→642px between tabs before and does not move at all now.
  The tab bar also sticks to the top, so a long panel does not scroll it away.
- Fixed: **custom themes were three fixed slots.** A fresh instance opened on
  fifty-seven empty colour boxes under three identical headings, and a fourth
  theme could not be made at all. Now: one block per theme you have, plus one
  blank to add the next, with the colours behind a disclosure — so a theme is a
  name and a starting point until you ask for more. Up to twelve. The page is
  half the height it was.
- Fixed: **deleting a chat left every file it held on disk.** The rows went —
  the message, the attachments, the generated images — and the files they named
  stayed, with nothing that would ever look at them again. Four of the five ways
  a chat can end had this: the delete button, a schedule's task chat, a helper's
  hidden chat, and deleting an account. There is one function that deletes a
  chat now, and it removes the files first.
- Fixed, and it is what made the above invisible: **a file attached before the
  chat existed never learned which chat it belonged to.** Anything picked on the
  new-chat screen kept an empty `chat_id` for the rest of its life. Six things
  filter on that, so for those files the model was not told they were attached,
  the canvas would not open them, and the cleanup could not find them.
- **Folders can be nested, which the README has always claimed.** The route has
  handled it since folders existed — cycle guard, depth limit — and the sidebar
  has always drawn a tree; there was simply no control that could ask for it.
  Moving a folder also respects the depth limit now, which only creating one did.
- The Proxmox container installs the **update helper by default**. A container
  made thirty seconds ago to run one thing is not the shared host the plain
  installer has to be careful about, and an appliance you cannot update without
  a shell is one nobody updates. `INSTALL_UPDATE_HELPER=0` opts out. Docker
  deliberately has no equivalent: updating a container is pulling an image, and
  a helper inside one would need the Docker socket, which is root on the host.
- The starting points on the new-chat screen are four new ones, aimed at
  somebody who has just stood an instance up and wants to know what is behind
  it. Only a fresh install gets them; an instance that has already seeded keeps
  whatever its administrator has made of the list.
- `README.md` describes what this actually is again — schedules, reports,
  helpers, image generation, semantic search, quotas, sharing, branding and the
  updates page were all missing, and two things listed as *planned* had shipped.
  It gained sections on Docker, the Proxmox container and updating.

## 0.9.9

**The first of five audit passes before 1.0.0** — everything that landed between
0.8.1 and 0.9.8 read as a whole rather than one feature at a time. This one is
the main logic, the harness, and every instruction a model is given.

- Fixed: **every model was told the time in a zone with no name.** On any
  account that had not chosen a timezone — which is the default state of every
  account — the date line shipped as "Times the person gives you are in
  unless they say otherwise", on every request. The code claimed in two places
  that the line disappeared instead. It never had.
- Fixed: **the prompt preview could not show most of what it previews.** Eleven
  fragments are gated on things that only exist once there is a real chat, and
  the preview has none — so the whole agent surface, both scheduling fragments
  and the helper warning were missing from it whatever you ticked. Editing
  `tool.agent` and pressing preview showed a system message without `tool.agent`
  in it, and nothing said so. Two new controls come with the fix: what kind of
  chat to preview as, and which agent mode.
- Fixed: **a model in Plan mode was told to use a tool it did not have.**
  `plan_update` is withdrawn in that mode in favour of `plan_submit`, but its
  guidance appeared whenever a plan existed — directly under the line saying
  anything not in your tool list does not exist.
- Fixed: **reading one knowledge document could fill the whole context window.**
  Every other reader caps what it returns and says so; this one returned the
  document whole, and its description said "in full", so it did exactly what it
  claimed. A long PDF is now cut at 40,000 characters with the model told.
- Fixed: **the guidance about helpers on a machine was wrong in both
  directions.** It denied that a helper can write files, which is a documented
  option of the tool beside it, and it named seven of the twenty-three commands
  a helper may run — so a model avoided commands it was allowed to use. Both are
  now checked against the real list and the real schema by tests, because prose
  and a constant drift the moment one is edited alone.
- The tool description for delegating no longer claims a helper gets "the same
  tools". It gets deliberately fewer, and sizing a task against the wrong set is
  how a whole phase gets planned around something that will refuse it.

- The Updates page notices when the update helper on a host was installed for a
  **different channel** than the page follows. It is declared in two places —
  `lembas.env` and the systemd unit — and only the installer writes both, so
  editing one by hand would have left the button deploying something other than
  what the page named, with nothing anywhere saying so.
- Fixed: release notes from a **signed** tag rendered the signature block.
  `_notes_for` stripped the PGP header only, and which header appears depends on
  `gpg.format` — this repository signs with SSH.
- A `CHANGELOG.md`, kept from now on rather than assembled at release time.

## 0.9.8

**Updates follow a channel, not a commit.** `stable` tracks the newest `vX.Y.Z`
tag; `edge` tracks the branch tip. A branch tip is not a release — following one
means deploying whatever was pushed five minutes ago — so stable is the default
for anybody who is not the person writing it.

- The Updates page shows a **version** rather than a commit sha: `1.0.0` at a
  tag, `1.0.0-7-gd4f56d` seven commits past one, and a bare sha only before the
  first release exists.
- Release notes come out of the **annotated tag itself**, so no forge API is
  involved anywhere. That matters: the Gitea API this was checked against
  returns a 500 from a server-side panic on exactly the releases endpoint.
- A tag with a suffix (`v1.1.0-rc1`) is deliberately not a release — git's
  version sort ranks it *above* `v1.1.0`, so accepting one would step a stable
  host onto a candidate.
- Fixed: `deploy/update.sh` stopped silently after `== fetching ==` on any host
  with no release tags — which was every host. Fetched, not reset, not
  restarted, and no error printed.
- Fixed: `install.sh` now refuses an `ssh://` repository URL up front instead of
  letting the clone fail as a service user with no key.

## 0.9.7

**Packaging, and updating without a shell.**

- `/admin/updates`: what is running, what is available, and what changed between.
  A button applies it — answered by an **opt-in** systemd helper, because the
  service runs unprivileged and a web application that can restart its own
  service is one whose worst day is much worse. Without the helper the page says
  so and prints the command.
- `Dockerfile` and `docker-compose.yml`. No secret key, no data and no `.git`
  baked in; loopback only; a TLS proxy expected in front, because a service
  worker and a microphone both require HTTPS or localhost.
- `deploy/lxc-install.sh` creates an unprivileged Proxmox container and runs the
  existing installer inside it.
- `/healthz`, which opens the database rather than only proving the socket is
  listening.

## 0.9.6

**Permissions, quotas and sharing.**

- **"What can this account actually do?"** answered on screen, naming *where*
  each permission came from — admin, the baseline, or a group.
- Users and groups are list-plus-detail, and membership is edited from **one**
  side. It was on both, and a save from either overwrote what the other showed.
- Reading and writing split for notes, memory and skills.
- **Quotas on a group** — monthly tokens, concurrent replies, agent wall clock,
  images a day, helpers a reply. Resolved by maximum across a person's groups,
  with zero meaning *no limit* and winning outright.
- Fixed: **deleting a group or an account left every share naming it behind.**
  `forget_principal` had existed since shares did and was called by nobody.
- Fixed: `library.share` defaulted to off, so sharing shipped documented as done
  and unreachable — the panel only renders for somebody who holds it.
- The share panel is its own action with a search box. It used to be checkboxes
  inside the resource's save form, listing every account on the instance, and a
  tick only took effect if you also saved the resource.
- Reports are shareable, and every listing has a **Shared with me** filter.

## 0.9.5

**Extraction settings, embeddings, and hybrid search.**

- `/admin/extraction`: upload size, image edge, JPEG quality, PDF pages,
  extracted characters, orphan age, extra text extensions.
- An **embedding model** can be chosen from models flagged for it. Library search
  then fuses keyword and semantic ranking, so *"how do I get paid"* finds a
  document that says *"invoicing"*.
- **Choosing none is not a degraded mode**: no rows written, no requests made,
  and byte-for-byte the keyword search that was always there.
- Vectors carry their model and width, and a mismatch is skipped rather than
  scored — comparing two embedding spaces produces a confident wrong answer.
- Indexing happens in the background as records are written, with a rebuild
  button for everything that already existed.

## 0.9.4

**An instance can be somebody else's.**

- Name, tagline, logo, favicon and launcher icons derived from the logo.
- The Middle-earth wording is editable data. Leaving a box alone does not freeze
  it, so a later release can still improve the default.
- **Custom themes** as a set of colours rather than a stylesheet, inheriting
  whichever built-in they start from.
- Global CSS overrides, served as `/branding.css`.

## 0.9.3

**Subagents.** A reply can hand a self-contained piece of work to a helper that
runs on its own and reports back — several at once, so research fans out instead
of queueing.

- A helper cannot ask questions, cannot send helpers of its own, writes nothing
  unless the call asked and the chat's mode allowed it, and on a machine runs
  only a fixed list of read-only commands — in **every** mode, including Auto.
- Fixed, and it was live in scheduled runs too: an unattended chat that hit an
  approval built a card nobody could see and sat on it for fifteen minutes.

## 0.9.2

**Image generation defaults an administrator can actually set** — steps, cfg,
size, sampler, scheduler, denoise, negative prompt, checkpoint, batch. There were
none: one hard-coded set from the SD1.5 era, and prose in a box as the only way
to change it.

- The samplers and schedulers ComfyUI had been reporting all along are now the
  pickers; nothing had ever read them.
- The tool's own schema restates the instance's defaults, instead of telling the
  model "Default 512" beside an instance that draws at 1024.

## 0.9.1

**Everything that arrives is announced, not only chat replies.** A scheduled run
that filed a report used to light a dot in a corner and say nothing.

- A count in the tab title while you are looking elsewhere.
- **Web push**, so a schedule firing at seven in the morning reaches a browser
  that is shut. Opt-in per device. It is the one thing here that contacts an
  outside service, and `services/push.py` says so plainly.

## 0.9.0

**A model can schedule things.** There was no tool for it — asked to "remind me
every Monday", a model wrote a note and reported that it had scheduled
something, and every screen agreed with it.

- `schedule_create`, `schedule_list`, `schedule_update`, `schedule_cancel`, over
  the same rule normaliser the manual form uses.
- The reply says the resulting timing back in words, which is the only moment
  anybody can check that Monday was understood as Monday.

## 0.8.3

**An SSH connection may not point at this machine unless an administrator says
so.** A profile aimed at `127.0.0.1` walked straight past "nothing runs on the
LLeMbas host" — through a real login, onto the machine holding the database and
the encryption key. Three positions: off, one named port, or anywhere.

## 0.8.2

- Fixed: **opening the canvas before a chat existed swapped the whole site into
  the panel.** `hx-get=""` is not "fetch nothing" — htmx looks for the attribute,
  not the value, so the empty one was a real request for the current document.
- Fixed: the Canvas and Terminal buttons appeared where they could not work.
- The bottom edge of the shell is no longer drawn, so the sidebar footer and the
  composer stop meeting a line at two different heights.
- Admin pages scroll in one container; `/admin/prompts` no longer drops you at
  the bottom of a shorter panel.
