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:
@@ -223,12 +223,21 @@ async def build(executor: Executor, project_dir: str) -> ProjectIndex:
|
||||
"""Walk the directory, by whichever means works first."""
|
||||
started = time.monotonic()
|
||||
try:
|
||||
found = None
|
||||
for attempt in (_from_git, _from_find):
|
||||
found = await attempt(executor, project_dir)
|
||||
try:
|
||||
found = await attempt(executor, project_dir)
|
||||
except ExecError as exc:
|
||||
# A rung that cannot run at all is a rung that did not answer,
|
||||
# not the end of the ladder. A host that refuses exec entirely
|
||||
# -- an SFTP-only account, a forced command -- is the exact case
|
||||
# the SFTP rung below exists for, and letting this out skipped
|
||||
# straight past it to an empty listing.
|
||||
log.debug("indexing %s: %s did not run: %s", project_dir, attempt.__name__,
|
||||
exc.message)
|
||||
found = None
|
||||
if found is not None:
|
||||
break
|
||||
else:
|
||||
found = None
|
||||
if found is None:
|
||||
found = await _from_sftp(executor, project_dir)
|
||||
except ExecError as exc:
|
||||
@@ -491,6 +500,17 @@ def forget(profile_id: str) -> int:
|
||||
return len(doomed)
|
||||
|
||||
|
||||
def forget_dir(profile_id: str, project_dir: str) -> None:
|
||||
"""Drop one tree's listing, because something just changed it.
|
||||
|
||||
The TTL exists for drift nobody can see coming. A write through `file_write`
|
||||
is not that: it is this process changing the tree it has just described, and
|
||||
leaving five minutes of a listing that is known to be wrong is worse than
|
||||
having none -- a model reading it concludes the file it created is missing.
|
||||
"""
|
||||
_CACHE.pop((profile_id, project_dir), None)
|
||||
|
||||
|
||||
def clear() -> None:
|
||||
_CACHE.clear()
|
||||
|
||||
@@ -503,5 +523,6 @@ __all__ = [
|
||||
"clear",
|
||||
"ensure",
|
||||
"forget",
|
||||
"forget_dir",
|
||||
"render",
|
||||
]
|
||||
|
||||
@@ -36,6 +36,10 @@ class AgentContext:
|
||||
chat_id: str
|
||||
label: str
|
||||
project_dir: str
|
||||
# The connection's id, carried so a runner can drop the project listing it
|
||||
# has just invalidated. `index` is keyed on the connection and the
|
||||
# directory, not on the chat -- two chats on one tree share a listing.
|
||||
profile_id: str = ""
|
||||
mode: str = policy.MODE_MANUAL
|
||||
allow: tuple[str, ...] = ()
|
||||
deny: tuple[str, ...] = ()
|
||||
@@ -112,6 +116,7 @@ def resolve(db: DBSession, chat: Chat, user: User | None) -> AgentContext | None
|
||||
chat_id=chat.id,
|
||||
label=profile.label,
|
||||
project_dir=chat.project_dir or profile.default_dir or "",
|
||||
profile_id=profile.id,
|
||||
mode=chat.agent_mode if chat.agent_mode in policy.MODES else policy.MODE_MANUAL,
|
||||
allow=tuple(values.get("allow_default") or ()),
|
||||
deny=tuple(values.get("deny_default") or ()),
|
||||
|
||||
@@ -21,7 +21,7 @@ import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from lembas.services.agent import policy
|
||||
from lembas.services.agent import index, policy
|
||||
from lembas.services.agent.base import ExecError, ExecRequest
|
||||
from lembas.services.agent.session import AgentContext
|
||||
from lembas.services.tools import (
|
||||
@@ -198,6 +198,13 @@ async def _run_write(context: ToolContext, args: dict[str, Any]) -> ToolOutcome:
|
||||
exc.message, _event("file_write", agent, path, status="error", error=exc.message)
|
||||
)
|
||||
|
||||
# The tree just changed, and this process is what changed it. The listing's
|
||||
# TTL is for drift nobody can see coming; leaving five more minutes of a
|
||||
# listing known to be wrong makes a model conclude the file it has just
|
||||
# written does not exist.
|
||||
if agent.profile_id:
|
||||
index.forget_dir(agent.profile_id, agent.project_dir)
|
||||
|
||||
return ToolOutcome(
|
||||
f"Wrote {written} bytes to {path}.",
|
||||
_event("file_write", agent, path, status="ok", text=f"{written} bytes"),
|
||||
|
||||
Reference in New Issue
Block a user