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
+43
View File
@@ -375,6 +375,43 @@ def store_text(
return attachment
def copy_attachment(
db: DBSession, *, user_id: str, chat_id: str | None, attachment: Attachment
) -> Attachment:
"""Duplicate something already sent, so it can ride along with a new message.
A copy and not a second reference to one row: an attachment belongs to the
message it was sent with, and sharing one between two would make deleting
either of them a question rather than an answer.
"""
stored_name = ""
source = attachments_dir() / attachment.stored_name if attachment.stored_name else None
if source is not None and source.exists():
stored_name = f"{secrets.token_hex(16)}{Path(source.name).suffix}"
(attachments_dir() / stored_name).write_bytes(source.read_bytes())
copy = Attachment(
user_id=user_id,
chat_id=chat_id,
filename=attachment.filename,
stored_name=stored_name,
media_type=attachment.media_type,
size_bytes=attachment.size_bytes,
kind=attachment.kind,
width=attachment.width,
height=attachment.height,
extracted_text=attachment.extracted_text,
pages=attachment.pages,
truncated=attachment.truncated,
extraction_error=attachment.extraction_error,
source_path=attachment.source_path,
source_label=attachment.source_label,
)
db.add(copy)
db.commit()
return copy
def copy_document(
db: DBSession, *, user_id: str, chat_id: str | None, document
) -> Attachment:
@@ -407,6 +444,12 @@ def copy_document(
pages=document.pages,
truncated=document.truncated,
extraction_error=document.extraction_error,
# Where it came from, for the same reason a project file carries it: a
# model handed four documents cannot tell which is which, and cannot
# name one back when asked to work on it. This was the one attach path
# that dropped provenance.
source_path=(document.title or "")[:1000],
source_label=(document.base.name if document.base else "Knowledge")[:200],
)
db.add(attachment)
db.commit()