Data groups: a provider's models read only their own group's data

Every connection is in a data group. Its models are handed, and can find,
only that group's memories, notes, skills, knowledge, reports and
personality -- by search and by id. A chat stays in the group it was
started in: switching its model, the endpoint fallback, the crowd, friends,
bases and the @ menu all stay inside it, and a chat whose model has moved
is refused rather than sent. A group may name its own embedder and image
reviewer. data.manage lets a person make personal groups, remap
connections for themselves and move their own records.

Also: a search no longer mixes two embedders of the same width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 16:07:55 +00:00
co-authored by Claude Opus 5.5
parent e65ea90fe6
commit 9970bb43c6
61 changed files with 3595 additions and 200 deletions
+43
View File
@@ -261,6 +261,10 @@ def authored_sideways() -> tuple[str, ...]:
return tuple(selectors)
# The chat `build_client` seeds, so a run can name `/chat/<SHOOT_CHAT>`.
SHOOT_CHAT = "5" * 32
def build_client():
import lembas.config as config_mod
@@ -293,6 +297,45 @@ def build_client():
for name in ("gemma4-moe", "qwen3-coder"):
db.add(Model(connection_id=connection.id, model_id=name, display_name=name))
# A second data group with a hosted model in it, a note in each group and a
# chat with a fixed id (`SHOOT_CHAT`). Without them every grouped screen --
# the chips, the Data tab, the models named under the picker as being in
# another group -- is rendered with nothing in it, which is the "a screen the
# test never renders is unchecked" lesson again.
from lembas.db.models import Chat, DataGroup, Note, User
with session_scope() as db:
db.add(DataGroup(id="hosted", name="Hosted providers"))
hosted = Connection(
name="hosted",
base_url="http://127.0.0.1:2",
api_key_encrypted="",
data_group_id="hosted",
)
db.add(hosted)
db.flush()
db.add(
Model(
connection_id=hosted.id,
model_id="deepseek-flash",
display_name="DeepSeek Flash",
)
)
owner = db.query(User).first()
db.add(Note(owner_id=owner.id, title="A note at home", body="x", data_group_id="default"))
db.add(Note(owner_id=owner.id, title="A hosted note", body="x", data_group_id="hosted"))
local = db.query(Connection).filter_by(name="local").first()
db.add(
Chat(
id=SHOOT_CHAT,
user_id=owner.id,
model_id="gemma4-moe",
connection_id=local.id,
data_group_id="default",
title="A chat to measure",
)
)
# 🚨 The suggestion cards are seeded by the startup hook, and `TestClient(app)`
# runs a lifespan only inside a `with` block -- so every shot of the new-chat
# screen ever taken by this script was of a page with its cards missing. That