diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 16:50:37 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 16:50:37 +0200 |
| commit | 3576ca1c347bc9f02c5e65fc9b90095a4d309365 (patch) | |
| tree | 652afb992fd82229554846140fa6279bd9c7bbe6 | |
| parent | efe6bb69bb22d17501f513a7fc070fbf580ff024 (diff) | |
| download | desktop-assistant-3576ca1c347bc9f02c5e65fc9b90095a4d309365.tar.gz desktop-assistant-3576ca1c347bc9f02c5e65fc9b90095a4d309365.zip | |
From review. A write ended by anything other than its own stop (an ask,
a new conversation, another client's stop) sent only state idle, so the
canvas waited for written or an error forever; interrupt() now sends
error stopped with turn. A refused write carries turn too. An empty reply
is refused instead of saved, a document made of several code blocks is
no longer stripped as one fence, the write sizes its budget on the whole
context since no history goes with it, and its usage no longer moves the
chat's context meter. The file is written as UTF-8 whatever the locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | assistant.py | 40 |
1 files changed, 28 insertions, 12 deletions
diff --git a/assistant.py b/assistant.py index 4107092..9004243 100644 --- a/assistant.py +++ b/assistant.py @@ -522,6 +522,9 @@ def interrupt(): turn += 1 if writing["on"]: # a write has no pending turn to close writing["on"] = False + # Whatever ended it (stop, an ask, a new conversation): the canvas + # waits for an outcome, so it gets one. + emit({"type": "error", "message": "stopped", "turn": True}) emit({"type": "state", "state": "idle"}) conn = inflight["conn"] if conn is not None and conn.sock is not None: @@ -566,7 +569,8 @@ def handle(raw): and "/" not in cmd["name"]) else None) if bad: - emit({"type": "error", "message": bad}) + # A refused write ends the canvas's wait for it, so it carries turn. + emit({"type": "error", "message": bad, **({"turn": True} if kind == "write" else {})}) return try: with lock: @@ -693,7 +697,7 @@ def llm_model(): return model_inputs()[0] -def ask(messages, calls, tools=None, mine=None, think=None): +def ask(messages, calls, tools=None, mine=None, think=None, meter=True): # Yields ("think", text) and ("token", text) as they stream; tool-call # fragments go into calls. mine is the reply's turn: a stop shuts the # connection from the other thread (interrupt), which ends the reading. @@ -715,7 +719,7 @@ def ask(messages, calls, tools=None, mine=None, think=None): return # stopped before the request went out inflight["conn"] = conn try: - yield from stream(conn, u.path, body, calls) + yield from stream(conn, u.path, body, calls, meter) except OSError: if mine is not None and turn != mine: return # the stop hung up under the read @@ -727,7 +731,7 @@ def ask(messages, calls, tools=None, mine=None, think=None): conn.close() -def stream(conn, path, body, calls): +def stream(conn, path, body, calls, meter=True): conn.request("POST", path, json.dumps(body).encode(), {"Content-Type": "application/json"}) r = conn.getresponse() if r.status >= 400: @@ -748,7 +752,7 @@ def stream(conn, path, body, calls): if "error" in chunk: # a failure mid-stream arrives as a chunk err = chunk["error"] raise RuntimeError(err.get("message", str(err)) if isinstance(err, dict) else str(err)) - if chunk.get("usage"): + if chunk.get("usage") and meter: # How full the model's context is: history, tool results and # tool definitions all count. ctx["used"] = chunk["usage"]["prompt_tokens"] @@ -801,7 +805,8 @@ def unfence(text): # Models often wrap a whole document in one code fence; the file wants # what is inside it. lines = text.strip().split("\n") - if len(lines) >= 2 and lines[0].startswith("```") and lines[-1].strip() == "```": + if (len(lines) >= 2 and lines[0].startswith("```") and lines[-1].strip() == "```" + and not any(l.startswith("```") for l in lines[1:-1])): # one fence, not two blocks return "\n".join(lines[1:-1]) + "\n" return text @@ -811,8 +816,8 @@ def write(mine, files, prompt, d, name): # no history in, nothing kept in the conversation, no thinking, no tools. tmp = None try: - free = (ctx["max"] - ctx["used"]) // 2 * 3 if ctx["max"] else 24000 - blocks, refs, _ = attach(files, max(free, 2000)) + # No history goes with a write, so half the whole context is its share. + blocks, refs, _ = attach(files, ctx["max"] // 2 * 3 if ctx["max"] else 24000) model, inputs = model_inputs() images = inputs is None or "image" in inputs if refs and not images: @@ -825,7 +830,8 @@ def write(mine, files, prompt, d, name): body = "\n\n".join(blocks + [prompt]) msgs = [{"role": "user", "content": [{"type": "text", "text": body}] + refs if refs else body}] text, shown = "", 0.0 - for kind, piece in ask(wire(msgs, images), {}, None, mine, think=False): + # meter=False: the chat's context meter is not about this request. + for kind, piece in ask(wire(msgs, images), {}, None, mine, think=False, meter=False): if turn != mine: return if kind == "token": @@ -835,9 +841,11 @@ def write(mine, files, prompt, d, name): emit({"type": "progress", "chars": len(text)}) if turn != mine: # stopped: the hang-up ended the stream return + if not text.strip(): + raise Refused("the model gave an empty reply; nothing written") os.makedirs(d, exist_ok=True) tmp = os.path.join(d, f".write-{os.getpid()}-{mine}.tmp") - with open(tmp, "w") as f: + with open(tmp, "w", encoding="utf-8") as f: f.write(unfence(text)) with lock: if turn != mine: @@ -1439,7 +1447,9 @@ def test(): send(type="write", files=[pdf], prompt="As Markdown.", dir=fd, name="held.md") until("state") send(type="stop") - assert until("state")[-1]["state"] == "idle" + seen = until("state") + assert seen[-1]["state"] == "idle" + assert {"type": "error", "message": "stopped", "turn": True} in seen # an outcome time.sleep(0.3) assert not os.path.exists(os.path.join(fd, "held.md")) assert not [n for n in os.listdir(fd) if n.startswith(".write-")] @@ -1447,7 +1457,13 @@ def test(): for bad in ({"dir": "relative", "name": "x.md"}, {"dir": fd, "name": "a/b.md"}, {"dir": fd, "name": ""}): send(type="write", files=[pdf], prompt="p", **bad) - assert until("error")[-1]["message"] == "bad write", bad + e = until("error")[-1] + assert e["message"] == "bad write" and e["turn"] is True, bad + assert unfence("```\na\n```\nmid\n```\nb\n```") == "```\na\n```\nmid\n```\nb\n```" + script["replies"] = [[{"content": " "}]] + send(type="write", files=[pdf], prompt="p", dir=fd, name="empty.md") + assert "empty reply" in until("error")[-1]["message"] + assert not os.path.exists(os.path.join(fd, "empty.md")) # llama-server down: an error, not a dead thread. CFG["llm"] = "http://127.0.0.1:9/v1" send(type="ask", text="anyone?") |
