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:
@@ -20,7 +20,7 @@ lembas info # paths + counts, useful when confused
|
||||
lembas secret-key # generate LEMBAS_SECRET_KEY
|
||||
lembas create-admin # create or promote an admin
|
||||
|
||||
pytest # 1408 tests, ~87s
|
||||
pytest # 1412 tests, ~87s
|
||||
# PLAN.md tracks what is and is not built
|
||||
ruff check . # lint (line length 100)
|
||||
python scripts/build_artwork.py # regenerate artwork (SVG + PWA icons;
|
||||
@@ -332,13 +332,27 @@ along with the reply, and a reload starts the turn afresh -- the model asks
|
||||
again. That is consistent with "a restart abandons replies in flight", but it
|
||||
means an approval is not a durable record of consent.
|
||||
|
||||
**The mode and the allow list are re-read between rounds, not once per reply.**
|
||||
Both are things a person changes *while watching a reply*, and both were
|
||||
snapshotted when it began -- so switching to Auto during a long agent reply went
|
||||
on asking about every call, and "Always allow this" was accepted, written to the
|
||||
row and then ignored for the rest of the reply that had just asked. Both look
|
||||
exactly like a control that does not work, because for that reply they were.
|
||||
`agent/session.py:refresh` re-reads the two, and only those two: everything else
|
||||
is fixed for the life of the chat or is an instance setting nobody edits
|
||||
mid-reply. Between rounds and never within one -- a round's calls are authorised
|
||||
together, so switching must not retroactively approve what is already queued,
|
||||
which is the property the old snapshot was protecting by accident. It mutates
|
||||
in place because `as_approved` copies field *references*: a replacement would
|
||||
leave this round's approved copy pointing at the old context.
|
||||
|
||||
**A chat's kind and connection are fixed at creation; only the mode moves.**
|
||||
`Chat.kind`, `ssh_profile_id` and `project_dir` are chosen on the new-chat screen
|
||||
and refused by `update_chat` thereafter with a 409 — a transcript whose earlier
|
||||
turns ran somewhere else is not one conversation. `agent_mode` is the exception
|
||||
and changes freely: it decides what gets asked about, not what the conversation
|
||||
is. The mode is read **once per reply**, so switching to Auto mid-reply cannot
|
||||
retroactively approve what is already queued.
|
||||
is. It is read **once per round** — see the note above for why that is not once
|
||||
per reply, and why it is not per call either.
|
||||
|
||||
**The mode is enforced in the loop, never in the prompt.** `_authorise` consults
|
||||
`agent/policy.py:decide()` server-side, keyed on each `ToolDef.risk`. A model is
|
||||
@@ -417,6 +431,14 @@ prompt as the whole reply's. It is recomputed per round now, and
|
||||
prompt is **summed** across rounds because it was paid for each time, while what
|
||||
the reply *occupies* is the last round's prompt plus what was written.
|
||||
|
||||
**A harness that fits is not the same as one with room.** The shipped set had
|
||||
grown to within 1,300 characters of the 16,000 ceiling, and crossing it is
|
||||
silent: `assemble` cuts the *tail*, which by fragment order is the project's own
|
||||
AGENTS.md. It is 20,000 now, and `tests/test_harness.py` pins a **margin**
|
||||
(`HARNESS_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 rather than shorter.
|
||||
|
||||
**`MAX_HARNESS_CHARS` has to be larger than the budgets the same code grants.**
|
||||
It was 8000. The fragments alone are about 7,900 characters for an agent chat,
|
||||
and `index_chars` (2,000) and `instructions_chars` (4,000) are granted on top,
|
||||
@@ -718,6 +740,14 @@ one `input` event catches it in a second, and caught two more on the same run:
|
||||
choosing a command from the menu left `/help` sitting in the box, and Tab did
|
||||
not complete. Anything touching these files gets driven before it is committed.
|
||||
|
||||
**htmx fires afterSwap and afterSettle before afterRequest.** The composer
|
||||
empties itself from `hx-on::after-request`, and the mirror repainted on the
|
||||
first two -- so every repaint ran while the box still held the message, and the
|
||||
highlighting sat over an empty field until the next keystroke. It repaints on
|
||||
`htmx:afterRequest` and on `reset` as well now, both 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.
|
||||
|
||||
**Two things must be sized the same or the composer's highlighting slides off.**
|
||||
A `<textarea>` cannot style its own contents, so `.composer__mirror` sits behind
|
||||
it holding the same text with every character transparent, contributing nothing
|
||||
|
||||
Reference in New Issue
Block a user