The chat's own model answers, then each other member in order, then the order runs
backwards asking each whether it disagrees, ending at the main model, which either
closes or sends them round again. Design and reasoning: LLeMbas.wiki/Crowd-chats.
THE SPEAKER SEAM, WHICH IS ALSO A BUG FIX
`chat_service.speaker_for` makes the *message* name the answering model and the
chat only the default. That closes a live half-wired feature -- `wake_chat` takes a
model override and `schedule/runner` passes one, and it reached the row and never
the request, so a schedule naming another model got the chat's model wearing the
other one's name.
The seam is wider than `build_request`: `{{model_name}}`, the authored prompt's
model layer, `vision` (where a wrong answer makes the endpoint reject the whole
request), the effort vocabulary (which raises inside the model's own chat template,
and whose refusal narrows every Model row sharing the id), `resolve_tools`,
`context_length` -> `_too_big`, and `ToolContext.model_id`. `resolve_endpoint` may
now only write back `chat.connection_id` when the speaker *is* the chat's model.
WHY N CHAINED REPLIES
`Generation` is one reply's state and `_follow` streams per message, so one
generation cannot stream into nine bubbles and `ensure` would not know which of the
nine it was after a restart. A subagent per speaker cannot work either: its answer
comes back as a tool result and tool results are never replayed, so speaker 3 could
not see speaker 2 -- which is the whole point. Chained, exactly one incomplete row
exists at a time, and `tests/test_crowd_chain.py` asserts that at every
observation.
The round lives on `Message.crowd_json`, not on the chat: the row is the authority,
and chat-level state would describe turns a rewind or a restart had removed.
`crowd.next_turn` is pure, so all eight refusals are tested with no endpoint.
THREE RULES, EACH A BUG WRITTEN THE OTHER WAY ROUND
- `if not _advance_crowd(g): _drain(g)` -- advancing must *suppress* draining, or a
queued human turn puts a second incomplete row beside the next speaker's.
- `_advance_crowd` refuses unless the finishing row is the newest, or regenerating
member 2 creates a second member 3 and two chains race down one turn.
- an error skips one speaker and two in a row end the round: the usual failure is a
small member's window overflowing, and `_drain`'s stop-on-error would kill every
crowd at whichever member is smallest.
Each other speaker's turn is relabelled as attributed user content, which is both
how a model can disagree with words it did not write and how the history keeps
alternating. The per-speaker instruction is payload-only -- as a row it could be
dropped from the request by a `created_at` tie, and every later speaker would answer
it. Compaction, titling and the notification are gated to once per turn; `_inject`
is off during a round; the way back gets no tools and a member is treated as
unattended.
Membership stores the model as text with no foreign key: "Test & refresh" deletes
and recreates Model rows, and a cascade would empty the crowd out of every chat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
884 lines
50 KiB
Markdown
884 lines
50 KiB
Markdown
# 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.6.0
|
||
|
||
- **A chat can have a crowd.** Switch it on under Admin → Agents, and each chat's
|
||
settings panel offers the other models. The chat's own model answers first, then
|
||
each of the others in turn; then the order runs **backwards**, each one asked
|
||
whether it disagrees with anything said; and it ends back at the first model,
|
||
which either writes the final answer or sends them round again. Every
|
||
contribution is its own bubble with its own avatar, its own metrics and a chip
|
||
saying which speaker it is and which pass it belongs to.
|
||
|
||
What it costs is stated where you turn it on and again where you pick the
|
||
models, because it is easy to underestimate: one turn is **models × rounds ×
|
||
2 − 1** replies, so four models over two rounds is fifteen. On a single local
|
||
endpoint every change of speaker also loads a different model. Your own warning
|
||
is built into the defaults — larger crowds start going round in circles — so the
|
||
round limit is two, and it is a limit ordinary work will reach rather than a
|
||
runaway backstop.
|
||
|
||
Details worth knowing: each model sees the others' answers **quoted and
|
||
attributed**, never as its own words, so it can actually disagree with them; a
|
||
member you can no longer reach is skipped and said so rather than silently
|
||
dropped; a member whose endpoint fails is skipped, and two failures in a row end
|
||
the round; **Stop ends the round**, not just the model writing at the time; and
|
||
a message typed during a round waits for the round rather than interleaving with
|
||
it. Every sentence a crowd sends is editable under Admin → Prompts.
|
||
|
||
- Fixed: **a schedule that named its own model was ignored.** It was written on
|
||
the reply and never sent, so the bubble showed the model you chose while the
|
||
answer came from the chat's model. The same fix makes the crowd possible: the
|
||
reply itself now says which model is answering, rather than the conversation
|
||
deciding for all of them. Regenerating somebody's turn in a crowd keeps that
|
||
model rather than silently switching to the chat's.
|
||
|
||
## 1.5.0
|
||
|
||
- **A model's personality is now yours, not the instance's.** Each account gets
|
||
its own version of each model's character: a personality is something a model
|
||
works out *with somebody*, so two people talking to the same model are no longer
|
||
talking to the same one, and neither can see the other's. What a model **is** —
|
||
its description and the facts other models are told about it — stays the same
|
||
for everybody, because that is a property of the model rather than of a
|
||
relationship.
|
||
|
||
The box on the model's page is now the **default personality**: the starting
|
||
point somebody has until the model has written its own with them. It is not
|
||
layered underneath theirs afterwards — two personalities at once would
|
||
contradict each other and nobody could tell which was losing. Your own
|
||
personalities, their history, and what each model makes of you are all under
|
||
**Memory** in your settings, and deleting a personality resets it to the
|
||
default rather than removing it.
|
||
|
||
⚠ If you installed 1.4.0 — released and superseded the same day — anything a
|
||
model wrote about you then is sitting in the wrong place and reads as a
|
||
personality rather than as an impression. There is a note in
|
||
`db/migrations.py` with the one statement that moves it; deleting it is just as
|
||
reasonable, since nothing had time to write one worth keeping.
|
||
|
||
- Fixed: **a side panel was wider than a narrow phone and hung off the edge.**
|
||
The canvas, the terminal and the details panel all carried a minimum width of
|
||
384px, which beats the rule that was supposed to cap them at the screen — so on
|
||
a 360px phone they were 24px too wide with their left-hand edge cut off, and on
|
||
a 320px one, 64px. Nothing scrolled sideways, which is why a narrow-width pass
|
||
looking for a horizontal scrollbar never found it: the panels are fixed in
|
||
place, and fixed overflow does not make a page scroll. They are now exactly as
|
||
wide as the screen on a phone, and keep their column on a tablet.
|
||
|
||
The details panel was worse than the other two: it had no cap at all, and its
|
||
width is a *preference* you can drag to 2400px on a desktop. That number was
|
||
arriving verbatim on a phone.
|
||
|
||
- **The Install button now says why it is missing**, instead of not being there.
|
||
Four different things stop a browser installing this and all four looked
|
||
identical; the hint named only the least likely. It now reports whether the page
|
||
is a secure context, what the browser said if the service worker was refused,
|
||
and whether the browser simply never offers it — and names the cause that
|
||
actually bites a self-hosted instance: **a certificate the phone does not
|
||
trust**. A private or self-signed certificate means no service worker, and no
|
||
service worker means no install, however good the rest of it is. Installing the
|
||
CA on the device is the fix, and the app can now tell you that is what is
|
||
wrong.
|
||
|
||
## 1.4.0
|
||
|
||
- **Models can be told about each other.** A model may now be given a list of
|
||
the other models on this instance — their names, the id to refer to one by, and
|
||
what each is for — so that it knows what else is available and what each is
|
||
better at. The list is built per person from the models *they* can reach, so it
|
||
never names one they have no access to.
|
||
|
||
Each model's page has a new **Facts for other models** box for this: parameters,
|
||
quantisation, a benchmark figure, what it is bad at. The existing description is
|
||
used too, so filling in nothing at all still produces a usable list — but note
|
||
that the description is now read by models as well as by people.
|
||
|
||
- **A model can ask another model a question.** New **Ask another model** switch
|
||
on each model's page and a matching permission. The model picks who to ask from
|
||
the list above, writes the question, and gets that model's answer back to use —
|
||
a second opinion from something that is better at the subject, or a check on its
|
||
own reasoning by something that will not make the same mistakes.
|
||
|
||
The model answering sees only the question, not the conversation; it answers as
|
||
itself, and it is told to say so if it thinks the question is wrong. It cannot
|
||
ask anybody anything in turn, and it cannot pass the question on.
|
||
|
||
It shares the **Helpers** switch and allowance on Admin → Agents, because it
|
||
costs the same thing: one reply setting another reply going. On a single local
|
||
endpoint that also means a model swap out and back, so it is not free.
|
||
|
||
- **A model can have a personality of its own, and keep its own read of you.**
|
||
New **Edit its own personality** switch per model. Its character is carried into
|
||
every conversation rather than being an instruction for one, and it is the model
|
||
that writes it — you can seed it, read it, and put any earlier version back from
|
||
the **Personality** card on the model's page. Every version is kept.
|
||
|
||
Separately, each model keeps its own impression of how you work: what you
|
||
expect, how you like being answered, what keeps going wrong between you. Its
|
||
point of view rather than facts about you, which is what a memory is for. It is
|
||
per model and per person — two models may honestly reach different conclusions
|
||
about you, and nobody on a shared instance inherits anybody else's.
|
||
|
||
**You can read and delete all of it**, under Memory in your own settings. That
|
||
is the whole reason a model is allowed to keep one.
|
||
|
||
Two honest limits. A model that has just read a hostile web page can rewrite its
|
||
own character; what stops that being permanent is that every version is kept and
|
||
visible, not that it was prevented — the same position this takes on
|
||
model-written skills. And neither is available to a model running as somebody's
|
||
helper, answering another model's question, or working through a schedule: those
|
||
run on words nobody is watching being written.
|
||
|
||
- Fixed: **the model chosen to review generated images was silently forgotten**
|
||
whenever a connection was refreshed while its endpoint happened not to be
|
||
listing that model. Nothing failed — reviewing fell back to the chat's own
|
||
model, so pictures were being judged by a model you had not chosen, with nothing
|
||
saying so. Existing settings keep working.
|
||
|
||
- Fixed: **editing a message could leave one of the messages below it behind.**
|
||
Only when two were written in the same millionth of a second, which is exactly
|
||
what happens to a question and the reply being started for it — so the orphan
|
||
stayed in the conversation and in everything sent to the model afterwards.
|
||
|
||
## 1.3.2
|
||
|
||
- Fixed: **the model page could not save anything below the reasoning efforts**,
|
||
and had not been able to since 1.3.0. "Save changes" did nothing at all — not
|
||
slowly, not with an error, simply nothing — so the description, the system
|
||
prompt, every capability and tool switch, and the whole availability card
|
||
(enabled, pinned, available to everyone, groups) silently would not take. The
|
||
fields above it, including the display name and the reasoning efforts, saved
|
||
normally, which is what made it look like it worked.
|
||
|
||
Worse, the **Detect from the endpoint** button had stopped detecting. It
|
||
submitted the page as an ordinary save instead — a save carrying only the top
|
||
half of the form, so everything below took its empty default: it would have
|
||
cleared that model's description and system prompt and switched the model off
|
||
with all of its tools disabled. If you pressed it, check that model's page.
|
||
|
||
The cause was one HTML rule: a form inside another form is not allowed, and
|
||
rather than complaining, a browser discards the inner tag and lets the closing
|
||
tag end the *outer* form. Everything after that point was in no form, and a
|
||
button in no form does nothing. Nothing in the markup looks wrong, and no test
|
||
that posts to a route can see it — so the fix comes with one that reads every
|
||
page the way a browser parses it.
|
||
|
||
## 1.3.1
|
||
|
||
- Fixed: **updating to 1.2.0 or later broke every page that lists models**, with
|
||
a 500 and nothing but the error page to show for it. The per-model reasoning
|
||
effort list added in 1.2.0 was the first list-shaped setting this application
|
||
had ever added to a table that already had rows in it, and the code that fills
|
||
in such a column on existing rows could not tell a list from a dictionary — so
|
||
it wrote the wrong kind of empty value into every model, and reading one back
|
||
raised rather than returning nothing.
|
||
|
||
A fresh install was never affected, which is exactly why it was not caught:
|
||
the column is only filled in that way on a database that already existed.
|
||
|
||
This release both stops it happening and **puts right the rows already
|
||
written**, on start, with nothing to run by hand. If your instance is showing
|
||
the error page, updating is the whole fix.
|
||
|
||
## 1.3.0
|
||
|
||
- **A model's reasoning efforts can now be detected rather than known.** There
|
||
is a button on the model's page that asks the endpoint what its chat template
|
||
actually accepts, and ticks those. llama.cpp publishes the loaded model's
|
||
template, and that template is the very thing that rejects an effort it does
|
||
not recognise — so the answer is read from the place that is authoritative
|
||
instead of guessed at, or discovered by a failed reply.
|
||
- Endpoints that do not publish a template — OpenAI, vLLM — say so plainly
|
||
rather than being recorded as accepting nothing.
|
||
|
||
## 1.2.0
|
||
|
||
- Fixed: **choosing a reasoning effort could kill the reply outright**, with a
|
||
Jinja traceback where the answer should have been. Reasoning effort is sent
|
||
two ways, and the second — `chat_template_kwargs` — is rendered into the
|
||
model's own chat template, which does not ignore a value it has never heard
|
||
of: it raises, and the whole request fails. The catch is that the vocabulary
|
||
is **not the same for every model**. gpt-oss takes `low/medium/high`; Bonsai
|
||
takes `low/medium/xhigh` and refuses `high`; OpenAI has added `minimal`,
|
||
`xhigh` and `max` at various points. This application offered the same three
|
||
to everything, so on some models the top setting was one the model would
|
||
throw for.
|
||
- **A model now has its own list of the efforts it accepts**, on its page under
|
||
Models, and the composer's picker and `/effort` offer only those. Tick none
|
||
and the familiar three are used, which is right for nearly everything.
|
||
- **And it corrects itself.** If an endpoint refuses an effort anyway — a model
|
||
swapped underneath a name, a runtime upgraded — that reply is retried once
|
||
without it instead of being lost, and the model's list is narrowed so the
|
||
menu stops offering something that does not work. Where the endpoint says
|
||
what it *does* take, that is what gets stored.
|
||
- `/effort` now reads the levels from the picker rather than from a second copy
|
||
of the list kept in the browser, so the two can no longer disagree about what
|
||
a valid effort is.
|
||
|
||
## 1.1.2
|
||
|
||
Two things a phone found that 1.1.0's phone pass had not.
|
||
|
||
- Fixed: **the administration area could not be navigated on a phone.** Admin
|
||
has a nav of its own rather than the chat sidebar, and 1.1.0 gave every
|
||
sidebar the drawer behaviour — starts closed, slides in — without giving that
|
||
one any of the drawer's furniture. So it sat off-screen with no button to open
|
||
it, no close, and nothing to tap beside it: every administration page was
|
||
reachable and then a dead end. It now opens, closes and dims the page like the
|
||
other one, and a test refuses any future sidebar that cannot be opened.
|
||
- Fixed: **the chat gave nearly a quarter of a phone screen to margins**, so
|
||
anything that could not wrap had to be scrolled to sideways. The thread's side
|
||
padding is halved, and the speaker's avatar moves above the turn instead of
|
||
sitting in a 44px column beside every line of it — a code block gained about
|
||
sixty pixels of readable width.
|
||
- Fixed: **the chat's title was squeezed to nothing.** The row's designated
|
||
shrinker is hidden below a tablet width, so on a phone the controls went rigid
|
||
and asked for 317 pixels of a 390 pixel bar; the heading was not truncated, it
|
||
simply stopped occupying space. The model picker gives now, and on a phone it
|
||
shows its avatar rather than its name — the name is one tap away and the
|
||
title is not.
|
||
- Tick boxes and the smaller buttons are big enough to hit on a phone. A
|
||
checkbox is drawn by the browser at about sixteen pixels whatever the type
|
||
around it, which made it the smallest target in the application by some way,
|
||
and the admin lists are mostly checkboxes.
|
||
- Fixed: **icon buttons could be squashed below their own size.** The sidebar
|
||
toggle measured eighteen pixels across on a phone, under half its target,
|
||
because a full row shrank the button rather than the text beside it.
|
||
|
||
## 1.1.1
|
||
|
||
One bug, and it is the one that made 1.1.0 look broken the moment you updated to
|
||
it. If you saw a stray ✕ beside the logo on a desktop, controls that looked
|
||
half-styled, or a page that would not scroll, this is why — and none of it was
|
||
in the code you were running; it was the code your browser had *not* fetched.
|
||
|
||
- Fixed: **updating showed you the new page drawn with the old stylesheet.**
|
||
Pages are always fetched fresh, while the CSS and JavaScript beside them come
|
||
from the cache the offline support keeps — and that cache was keyed on the
|
||
release while the files inside it were not. For as long as the previous
|
||
release's worker was still in charge, you got 1.1.0's markup over 1.0.x's
|
||
stylesheet: a close button meant for the phone drawer appeared on the desktop
|
||
with nothing to style or place it, and anything else the new layout depended
|
||
on was simply absent. Every asset now carries the release in its address, so
|
||
a new page cannot be handed an old stylesheet whatever the cache holds.
|
||
|
||
It is self-correcting: updating to this version is enough, and no cache needs
|
||
clearing.
|
||
|
||
- The sidebar header is two slots — the name, and a rail on the right for the
|
||
drawer's own controls — instead of a brand with a button appended to it. The
|
||
close button sits in that rail, at the top right where it belongs, and a
|
||
second control added later lands beside it rather than pushing the name
|
||
around.
|
||
|
||
## 1.1.0
|
||
|
||
Mostly about using this on a phone, where it turns out a good deal of it could
|
||
not be used at all.
|
||
|
||
### The sidebar on a phone
|
||
|
||
- Fixed: **the sidebar opened over the page on every phone, and the button that
|
||
closes it was underneath it.** Below a phone width the sidebar is a 280px
|
||
panel laid over the page; nothing ever closed it, and the only control that
|
||
could was in the bar behind it. It now starts closed at that width, slides in
|
||
when you ask for it, dims the page behind it, and closes by tapping beside it,
|
||
by Escape, or by its own button — which is inside the drawer, where you can
|
||
reach it.
|
||
- Fixed: **seven of the eight pages with a sidebar had no way to show or hide it
|
||
at all.** Only the chat page ever had that button. Settings, Messages,
|
||
Reports, Scheduled, Library, Connections and a folder's own page did not —
|
||
which on a phone meant arriving at a page already covered by a panel with
|
||
nothing to do about it. Settings is where the Install and Notifications
|
||
buttons live, so this was also why they were hard to reach.
|
||
- The toggle no longer claims the sidebar is open when it is not, which matters
|
||
to anyone using a screen reader.
|
||
|
||
### Anything you tap
|
||
|
||
- **Every control is now at least 44px on a touch screen**, instead of 36px —
|
||
or 28px for the small ones, which included renaming and deleting a chat, all
|
||
seven actions on a message, and every panel's close button. The dismiss button
|
||
on a notification had no size of its own at all and was about 18 by 7 pixels.
|
||
- Fixed: **renaming or deleting a chat, and copying, editing, regenerating or
|
||
reading aloud a message, were impossible on a phone.** All of them appeared on
|
||
hover, and there is no hover on a phone; tapping the row simply opened it.
|
||
- Fixed: **the settings tabs scrolled sideways with nothing to say so**, hiding
|
||
Appearance, Memory and Security off the right-hand edge of a phone screen.
|
||
There is a fade at the edge now, and a flick lands on a tab.
|
||
- Installed on an iPhone, the page ran underneath the clock and the home
|
||
indicator. It no longer does.
|
||
|
||
### Installing it
|
||
|
||
- The install prompt now offers the richer dialog rather than the terse bar, and
|
||
a long press on the icon offers New chat, Messages and Scheduled.
|
||
- Fixed: **a light-themed instance installed to a phone showed a near-black
|
||
splash screen and then opened parchment**, and every page load flashed dark
|
||
browser chrome before the stylesheet had run. Both follow the theme now.
|
||
- Fixed: **a new version used to take over pages you were reading**, swapping
|
||
the stylesheets under an open tab while it emptied the cache they came from.
|
||
It waits and offers you a reload instead.
|
||
- Fixed: the small mark beside a notification on Android was a solid grey
|
||
square, because the icon it used has no transparency to be cut from.
|
||
- Fixed: notifications silently stopped working for good if the browser ever
|
||
replaced its own subscription, which browsers do.
|
||
- Pages start loading a little sooner, and the two icons a launcher actually
|
||
crops are now kept for offline use.
|
||
|
||
### Things that move
|
||
|
||
- **Every request the application makes now says it is happening**, with a thin
|
||
bar across the top of the window. Nothing did before, so anything slower than
|
||
a few milliseconds looked like a click that had not registered.
|
||
- The thinking indicator turns rather than fading, so a model that is working
|
||
and one that has stopped no longer look alike.
|
||
- Dialogs, the drawer and the panels arrive and leave rather than appearing;
|
||
buttons answer a press; cards lift under the pointer. All of it stops if you
|
||
have asked your system for reduced motion.
|
||
|
||
### Archiving
|
||
|
||
- **A chat can be archived** — out of the list, into a group at the bottom of the
|
||
sidebar, and back again whenever you like. The setting behind this has existed
|
||
and been honoured since folders arrived; nothing had ever been able to switch
|
||
it on.
|
||
|
||
### Smaller things
|
||
|
||
- **Extra headers can be set on a connection.** They were sent with every
|
||
request already and no form could write them, so OpenRouter's attribution
|
||
headers were documented and unreachable.
|
||
- A model is no longer told that it will hear when a background job finishes on
|
||
instances where that notification is switched off.
|
||
- The guidance for asking you a question can now be edited like every other
|
||
piece of the prompt. It was the only one that could not be.
|
||
- Several controls that a screen reader announced as nothing now have names, and
|
||
two lists that claimed to be tab strips now describe themselves honestly.
|
||
- Borders resolve through a token like every other value, so a theme can change
|
||
one. They were a literal `1px` in about ninety places, which was the largest
|
||
patch of hard-coded value left in the stylesheets.
|
||
- `chat.css` may now contain media queries. It was forbidden them, for a good
|
||
reason that had stopped applying: what the ban protected is asserted directly
|
||
now, which is both narrower and stronger.
|
||
|
||
## 1.0.4
|
||
|
||
Six things that looked like they worked. Five of them were found by reading the
|
||
code rather than by anybody reporting them, which is what they have in common:
|
||
none of these fails loudly, and two of them correct themselves if you reload.
|
||
|
||
- Fixed: **a reply lost the model's name and picture the moment it finished.**
|
||
While a reply streams it is attributed correctly; at the instant it lands, the
|
||
frame that replaces the bubble was looking the models up as nobody, and "no
|
||
user" answers "no models" rather than "all models". So a finished reply swapped
|
||
the model's avatar for the plain leaf mark, put the instance's name where the
|
||
model's should be, and grew a raw model id beside it. Reloading the page put it
|
||
all back, which is why this survived a release: it is only ever wrong until you
|
||
look away.
|
||
- Fixed: **a limit on how many replies an account may write at once could be
|
||
stepped over by pressing New chat.** It was enforced when sending into a chat
|
||
that already existed and nowhere else — not on a new chat, not on editing an
|
||
earlier message, not on sending a queued one, and not on regenerating. Four of
|
||
the six ways to start a reply ignored it, including the commonest.
|
||
- Fixed: **a custom theme's confirmations and warnings kept the built-in
|
||
theme's colour behind them.** Setting `success` or `warning` moved the text and
|
||
left the background it sits on, because the faded companion colour was derived
|
||
for three of the five settable colours. Visible on every alert and badge of
|
||
those two kinds, on the "on" state in the permissions list, and on the added
|
||
lines of every diff in an agent chat.
|
||
- Fixed: **on a phone, every page with a sidebar could be scrolled past its own
|
||
bottom into empty background.** The shell was sized to the part of the screen
|
||
you can actually see and the document around it to the part you can see with
|
||
the browser's toolbar retracted; the difference between those is real on a
|
||
phone and nil on a desktop, which is why it was never noticed on one. Reported
|
||
on Settings and true everywhere. A flick that ran off the end of a list now
|
||
stops there as well, instead of dragging the page behind it.
|
||
- Fixed: **the conversation was rendering every assistant message twice on every
|
||
page load** — once into Markdown that nothing read, and once the way it is
|
||
actually shown. The same was true of Messages, for your own turns. Nothing
|
||
looked wrong; a long conversation was simply slower to open than it needed to
|
||
be, every time, along with every rewind and every compaction.
|
||
- Fixed: a test file meant to skip itself on a machine without `setsid` never
|
||
did, because it set its marker twice and the second one replaced the first.
|
||
- Removed: an endpoint serving a message's unrendered Markdown, which nothing
|
||
had ever called — the copy button reads the page it is already on.
|
||
|
||
## 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.
|