Two controls that did nothing, and instructions worth reading

**Switching mode mid-reply did nothing.** The mode was snapshotted when the
reply began, so changing to Auto during a long agent reply went on asking about
every call until the next turn. The same snapshot held the chat's allow list,
which means "Always allow this" was accepted, written to the row, and then
ignored for the rest of the reply that had just asked about it -- the same bug,
in the quieter place nobody reported.

`agent/session.py:refresh` re-reads exactly those two, between rounds and never
within one. A round's calls are authorised together, so a switch must not
retroactively approve what is already queued -- which is the property the
reply-long snapshot was protecting by accident, and the reason this is not
simply moved into `_authorise`. It mutates in place, because `as_approved`
copies field references and a replacement would leave the round's approved copy
pointing at the old context.

**The composer's highlighting stayed behind after sending.** htmx fires
afterSwap and afterSettle *before* afterRequest, and the composer empties itself
from `hx-on::after-request` -- so every repaint ran while the box still held the
message. It repaints on afterRequest and on `reset` as well now, deferred a
frame: a form's reset event fires before its fields are actually cleared, so
reading the value in the same turn paints the text that is about to vanish.
Driven under a DOM stub reproducing htmx's real ordering, and confirmed to fail
without the fix.

**plan_update, audited.** It never said to mark a task `doing`, so the plan only
ever showed work already finished, which is the opposite of "what somebody reads
to see where you are". It never said several changes fit in one call, so a model
spends a round per task. And `done` now means checked rather than written.

**New: core.engineering**, an agent-chat fragment about conduct rather than
about any language -- run what you write, find the project's own build and test
commands rather than guessing, read before editing, change one thing at a time,
read the error instead of guessing at a fix, do not broaden an except to make
output clean, and say what you did not check. Every line is about the gap
between having written something and knowing it works, which is the gap a model
closes by asserting.

That pushed the shipped harness to within 1,300 characters of its ceiling, where
crossing it silently severs the project's own AGENTS.md. The ceiling is 20,000
and the test pins a margin as well as a fit -- the headroom is also where an
administrator's own wording goes, and an override is usually longer than the
default it replaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-04 12:36:50 +02:00
parent 35b85a9cda
commit 5984d90fb0
8 changed files with 243 additions and 15 deletions
+83
View File
@@ -233,3 +233,86 @@ def test_an_unrecognised_mode_is_ignored(client: TestClient, db, registered, wan
db.refresh(chat)
assert chat.agent_mode == policy.MODE_EDIT
# --- Changing it while a reply is running ---------------------------------------
def test_the_mode_is_re_read_between_rounds(client: TestClient, db, registered):
"""The reported bug. The mode was snapshotted for the whole reply, so
switching to Auto during a long agent reply went on asking about every
call -- which looks exactly like a control that does not work, because for
that reply it was one.
Between rounds and not within one: a round's calls are authorised together,
and switching must not retroactively approve what is already queued.
"""
from lembas.db.models import User
from lembas.services.agent import session as agent_session
chat = _agent_chat(db, mode=policy.MODE_MANUAL)
user = db.scalars(select(User)).first()
agent = agent_session.resolve(db, chat, user)
assert agent.mode == policy.MODE_MANUAL
client.patch(f"/api/chats/{chat.id}", data={"agent_mode": policy.MODE_AUTO})
# The route wrote through its own session; this one still holds the row it
# loaded. `refresh` opens a fresh session in the real path, so this is the
# test catching up rather than the behaviour under test.
db.expire_all()
agent_session.refresh(db, agent)
assert agent.mode == policy.MODE_AUTO
def test_always_allow_reaches_the_reply_that_asked(client: TestClient, db, registered):
"""The same bug, in the place nobody reported because it is quieter: the
verdict was accepted, written to the row, and then ignored for the rest of
the reply that had just asked about it."""
from lembas.db.models import User
from lembas.services.agent import session as agent_session
chat = _agent_chat(db, mode=policy.MODE_MANUAL)
user = db.scalars(select(User)).first()
agent = agent_session.resolve(db, chat, user)
assert "pytest" not in agent.allow
chat.scope_json = {**(chat.scope_json or {}), "allow": ["pytest"]}
db.commit()
agent_session.refresh(db, agent)
assert "pytest" in agent.allow
def test_refreshing_keeps_what_this_reply_has_read(client: TestClient, db, registered):
"""`read_paths` is what `file_edit` checks before applying a patch, and it
is a fact about this reply rather than about the row. Mutating in place is
what keeps it -- and keeps the approved copy of a round, which holds field
references rather than a copy."""
from lembas.db.models import User
from lembas.services.agent import session as agent_session
chat = _agent_chat(db, mode=policy.MODE_MANUAL)
user = db.scalars(select(User)).first()
agent = agent_session.resolve(db, chat, user)
agent.read_paths.add("/project/main.py")
approved = agent.as_approved()
agent_session.refresh(db, agent)
assert "/project/main.py" in agent.read_paths
assert "/project/main.py" in approved.read_paths
def test_an_unknown_mode_on_the_row_refreshes_to_manual(client: TestClient, db, registered):
"""A row that predates a rename has to fail towards asking, here as much as
in `resolve`."""
from lembas.db.models import User
from lembas.services.agent import session as agent_session
chat = _agent_chat(db, mode=policy.MODE_AUTO)
user = db.scalars(select(User)).first()
agent = agent_session.resolve(db, chat, user)
chat.agent_mode = "reckless"
db.commit()
agent_session.refresh(db, agent)
assert agent.mode == policy.MODE_MANUAL
+11
View File
@@ -477,3 +477,14 @@ def test_the_shipped_defaults_fit_under_the_ceiling(db, owner):
assert not out.endswith(""), f"the preamble was truncated at {len(out):,} characters"
assert "A" * 100 in out, "the project's own instructions were cut off entirely"
assert "L" * 100 in out, "the project listing was cut off"
# Fitting is not enough. It fitted with 1,300 characters to spare once, and
# a ceiling that close to the content is one the next fragment crosses --
# silently, and by cutting the tail, which is the project's own AGENTS.md.
# The margin is also what an administrator's own wording goes in: an
# override is usually longer than the default it replaces.
room = harness.MAX_HARNESS_CHARS - len(out)
assert room >= harness.MAX_HARNESS_CHARS * harness.HARNESS_MARGIN, (
f"only {room:,} characters of headroom left under "
f"{harness.MAX_HARNESS_CHARS:,}; raise the ceiling or shorten a fragment"
)