Two selects that never wrote anything, and a queue

The approval card in Auto mode and the missing /effort were one bug. Both
selects hung their hx-patch on an empty sibling form reached by form="…",
and htmx binds a trigger to the annotated element: change fires on the
select and bubbles to its ancestors, which a sibling is not. The live rows
read agent_mode=manual and params_json={} while the browser showed Auto and
Effort: high. policy.py was never involved.

The verb moves onto the control; the empty form stays as value scoping,
which is the half of the CLAUDE.md note that was right. conftest gains
control_named so a test asserts the element carrying the name carries the
verb, rather than asserting the markup that was there throughout.

The composer's highlight was a third instance of the same carelessness in
CSS: .tok-mention is written for the transcript and scoped to nothing, so
the mirror painted its token in accent-coloured monospace over the
textarea's own text. Scoped under .msg; the mirror restates transparency
and font rather than inheriting them, and bleeds by box-shadow.

/effort is now offered before the first prompt and _new_chat reads it.
/index re-walks the project directory on demand, file_write drops the
listing it just invalidated, and the index ladder falls through to SFTP on
a host that refuses exec instead of returning nothing.

A second message during a reply is queued rather than starting a second
concurrent generation: a real Message row with queued set, so it survives a
restart and can be withdrawn. _drain hands one on at the end of a reply,
_inject takes one in at a tool-round boundary so an agent can be steered
mid-task. Stop leaves the queue undelivered. The terminal's Auto toggle
becomes off/copy/send, and send posts straight to the chat without touching
the composer.

@ now offers notes, skills, this chat's attachments and a URL to fetch; a
knowledge base attaches as a reference rather than a copy. copy_document
carries provenance, which was the one attach path that dropped it.

Also fixes an unrelated live bug: the round loop compared against the
global MAX_ROUNDS of 3 while sizing itself from the agent budget of 40, so
agent replies stopped after three rounds and reported forty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-02 19:49:34 +02:00
parent 0bee366488
commit 8a3a225fea
31 changed files with 2131 additions and 81 deletions
+75
View File
@@ -254,3 +254,78 @@ def test_somebody_elses_connection_is_not_browsable(
# 404 and not 403: whether that connection exists is not this endpoint's to
# reveal to somebody who does not own it.
assert client.get(f"/api/agents/{profile.id}/browse").status_code == 404
# --- Reading the project directory again -------------------------------------
def _agent_chat(db, profile):
from lembas.db.models import KIND_AGENT, Chat, Connection, Model
connection = Connection(name="c", base_url="http://127.0.0.1:1", api_key_encrypted="")
db.add(connection)
db.commit()
db.add(Model(connection_id=connection.id, model_id="m", capabilities_json={"tools": True}))
db.commit()
chat = Chat(
user_id=profile.owner_id,
model_id="m",
connection_id=connection.id,
kind=KIND_AGENT,
ssh_profile_id=profile.id,
project_dir=profile.default_dir,
)
db.add(chat)
db.commit()
return chat
def test_reindexing_walks_the_tree_again(client: TestClient, db, registered, served_tree):
"""The listing is built only when a reply starts and then held for five
minutes, so anything done in the terminal panel is invisible to it until
then. This is the way to say "look again"."""
from lembas.services.agent import index as index_service
profile = _profile(db, served_tree)
chat = _agent_chat(db, profile)
response = client.post(f"/api/chats/{chat.id}/index")
assert response.status_code == 200, response.text
body = response.json()
assert body["ok"] is True
assert body["files"] >= 2 # README.md and src/
assert index_service.cached(profile.id, served_tree["root"]) is not None
def test_reindexing_a_plain_chat_says_there_is_nothing_to_read(
client: TestClient, db, registered, served_tree
):
from lembas.db.models import Chat, Connection, Model
connection = Connection(name="c", base_url="http://127.0.0.1:1", api_key_encrypted="")
db.add(connection)
db.commit()
db.add(Model(connection_id=connection.id, model_id="m"))
db.commit()
chat = Chat(user_id=_profile(db, served_tree).owner_id, model_id="m",
connection_id=connection.id)
db.add(chat)
db.commit()
assert client.post(f"/api/chats/{chat.id}/index").status_code == 409
def test_reindexing_somebody_elses_chat_is_not_possible(
client: TestClient, db, registered, served_tree
):
profile = _profile(db, served_tree)
chat = _agent_chat(db, profile)
client.post("/auth/logout", follow_redirects=False)
client.post(
"/auth/register",
data={"name": "Sam", "email": "sam@shire.test", "password": "potatoes-po-ta-toes"},
follow_redirects=False,
)
assert client.post(f"/api/chats/{chat.id}/index").status_code == 404