A time in no particular zone, and a preview missing what it previews

The first audit pass: everything from 0.8.1 to 0.9.8 read as a whole rather
than one feature at a time, starting with what a model is actually told.

Four of these had shipped as correct. The date line carried a timezone
variable that resolves to nothing until somebody chooses one -- so every
default account was told times were "in  unless they say otherwise", while
two comments asserted the line disappeared instead. The prompt preview
built its variables without a chat, which is what eleven fragments are
gated on, so the whole agent surface was absent from it whatever was
ticked. Plan mode was instructed to keep its plan current with a tool that
mode withdraws. And knowledge_get returned a document whole where every
sibling reader caps and says so, its description promising exactly that.

The subagent guidance was wrong in both directions at once: it denied a
documented parameter and named seven of twenty-three allowed commands.
Both halves are pinned by tests against the real list and the real schema
now, because prose and a constant drift the moment one is edited alone.

docs/notes/audit-0.9.md carries the findings that are not fixed here, with
why -- the ones whose fix would change what a feature does are the user's
call, not this pass's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 09:13:54 +02:00
co-authored by Claude Opus 5
parent 0ce8026bd2
commit e970f10cca
13 changed files with 521 additions and 51 deletions
+42 -2
View File
@@ -188,6 +188,21 @@ RISKS = (RISK_READ, RISK_WRITE, RISK_EXECUTE, RISK_ASK)
# single page. Cut with the model told so, rather than refused.
MAX_FETCH_CHARS = 20_000
# The same bound for a knowledge document, and it was missing. `knowledge_get`
# returned `extracted_text` whole while every sibling reader capped and said so
# -- `fetch` above, `file_read`, the memories block, the skill index, the
# project listing. `MAX_EXTRACTED_CHARS` is 120_000 by default and an
# administrator can raise it, so one call on a long PDF filled an ordinary
# window with nothing anywhere reporting that it had.
#
# Larger than a fetched page on purpose. Somebody put this document in the
# library deliberately and named it in a search; a page the model followed a
# link to is a guess. Cut with the model told so rather than refused, which is
# what `fetch` and `file_read` both do -- a reader that fails on exactly the
# documents worth reading is worse than one that hands back the first part and
# says there was more.
MAX_DOCUMENT_CHARS = 40_000
@dataclass
class ToolContext:
@@ -471,6 +486,14 @@ async def _run_knowledge_get(context: ToolContext, args: dict[str, Any]) -> Tool
"results": [{"title": document.title, "id": document.id}],
}
body = document.extracted_text or document.extraction_error or "(no text)"
if len(body) > MAX_DOCUMENT_CHARS:
body = (
f"{body[:MAX_DOCUMENT_CHARS]}\n\n"
f"[Cut off here. This document is {len(document.extracted_text or ''):,} "
f"characters and the first {MAX_DOCUMENT_CHARS:,} are above. Search it "
"with knowledge_search to find the part you need.]"
)
event["truncated"] = True
return ToolOutcome(f"{document.title}\n\n{body}", event)
@@ -912,8 +935,21 @@ async def _run_ask_user(context: ToolContext, args: dict[str, Any]) -> ToolOutco
session-free snapshot that deliberately holds no way to reach one. Getting
here means some other path called `run_tool` directly, and saying so is
better than returning an empty answer the model would treat as a reply.
It read `args["question"]`, singular, against a schema that declares
`questions` and a list -- so the event it built always carried an empty
`query`, and the card showed a refusal with no sign of what had been asked.
Harmless only because this path is unreachable, which is exactly why nothing
caught it: schema drift on a branch no test exercises. Tolerant of the same
spellings `generation._questions_in` accepts, rather than importing it,
which would be a circular import for one field on a dead path.
"""
question = str(args.get("question") or "").strip()
asked: Any = args.get("questions") or args.get("question") or ""
if isinstance(asked, list):
asked = asked[0] if asked else ""
if isinstance(asked, dict):
asked = asked.get("question") or ""
question = str(asked).strip()
return ToolOutcome(
"That question could not be put to anyone, so it has gone unanswered. "
"Carry on without it, or say what you need.",
@@ -997,7 +1033,11 @@ REGISTRY: dict[str, ToolDef] = {
ToolDef(
name="knowledge_get",
family=FAMILY_KNOWLEDGE,
description="Read one knowledge document in full, by the id a search returned.",
description=(
"Read one knowledge document, by the id a search returned. A "
"long one is cut off at the end rather than refused, and you "
"are told when that happened."
),
parameters=_object({"id": _STRING}, ["id"]),
run=_run_knowledge_get,
),