diff --git a/CLAUDE.md b/CLAUDE.md index 5b80b4e..b04829d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `