diff --git a/.dockerignore b/.dockerignore index 218f5d29..e417facb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,4 @@ __pycache__ *.pyo tests/ .env* +/AGENTS.md diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7da4f1..bc7f455c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ ### Fixed +- **A restart can no longer turn a suppressed `[SILENT]` control message into a phantom recovered turn on the `/goal` path.** The `/api/goal` endpoint now recognizes the exact reserved `[SILENT]` delivery-suppression sentinel and treats it as a successful no-op before any session lookup or goal-state mutation — matching the guard the chat-start endpoint already applies. Previously a wake relay that accidentally POSTed the sentinel to `/goal` could, after a restart, materialize it as a visible recovered user turn. Matching is exact and case-sensitive, so ordinary goals are unaffected. Thanks @webtecnica. (#7019) + +- **A busy subagent tree no longer promotes an active child to a misleading top-level sidebar row.** The session sidebar builds from a bounded oversampled candidate window; after the recency slice, subagent parents that were present in the candidate set could be dropped, leaving their still-running child leaves rendered as if they were their own top-level conversations. Those parents are now re-added after the slice (bounded, cycle-safe, subagent-scoped so WebUI ancestors stay excluded), so the delegated-session tree nests correctly. Thanks @carlotestor. (#7031) + +- **A shared conversation link can no longer fabricate appearance state that overrides an operator's deployment default.** The public share page (`share.html`) now applies the same first-visit appearance guard as the main app bootstrap (and picks up two skin IDs it was missing), so opening a shared link on a fresh browser no longer writes an explicit theme/skin into local storage that would then be treated as a deliberate user choice. Public-share rendering is unchanged. Thanks @webtecnica. (#7030) + +- **Docker: the repository's own agent-instructions file is no longer baked into the runtime image.** A root-anchored `/AGENTS.md` entry was added to `.dockerignore` so the repo-root `AGENTS.md` (maintainer/build guidance, not user content) doesn't get copied into the Docker runtime and injected into the agent's project context. Nested workspace `AGENTS.md` files remain available exactly as before. Thanks @rodboev. (#6853) + - **Docker: the bundled SQLite is upgraded to 3.53.0 to fix a WAL-reset corruption bug (and still erases deleted rows).** The `python:3.12-slim` base image ships SQLite 3.46.1, which is vulnerable to the March 2026 WAL-reset corruption bug, and Debian hasn't backported the fix — so the image now compiles SQLite 3.53.0 from the official amalgamation (SHA-256 pinned and verified before extraction, FTS5/FTS4/R-Tree enabled, build tools purged afterward). The source build is also compiled with `SQLITE_SECURE_DELETE` and asserts `PRAGMA secure_delete = 1` at build time, so deleting a conversation still overwrites its content on disk exactly as the base image did (a from-source build without that flag would have left deleted transcript bytes recoverable in `state.db`). Only affects the Docker image. The source-built library is registered in `ld.so.conf.d` so it loads ahead of the base image's copy on every architecture (including arm64, where `/usr/local/lib` isn't on the default loader path). Thanks @qxxaa. (#6900, #7044) - **Foregrounding a long or busy conversation no longer risks running the browser out of memory.** Switching back to a tab with a very long active chat could make the browser re-materialize the entire transcript's structured data at once — on the largest sessions this ballooned memory usage into the multi-gigabyte range and could OOM-kill the tab or the whole machine. The catch-up load on tab focus is now coalesced, the render window growth is bounded, and full structured-value allocations for off-screen turns are dropped, so foregrounding a long chat stays lightweight. A session-updated notification that arrives while a refresh is already in flight is now latched and reconciled once the refresh finishes (instead of being dropped), and the render cache signature now distinguishes tool-argument order the way the rendered output does (so differently-ordered arguments can't reuse stale cached markup). Thanks @webtecnica. (#7006, #6999) diff --git a/api/agent_sessions.py b/api/agent_sessions.py index 447ea1ef..c0a5078f 100644 --- a/api/agent_sessions.py +++ b/api/agent_sessions.py @@ -517,6 +517,19 @@ def read_importable_agent_session_rows( ``exclude_sources=None``. ``include_sources`` is an additional narrowing filter; callers that want an include-only query should explicitly pass ``exclude_sources=None`` so the default exclusions do not also apply. + + ``limit`` bounds the *recency slice*, not the returned row count. Subagent + rows only render as children when their parent row is in the same payload, + so subagent ancestors of selected rows are re-added afterwards and the + result can exceed ``limit`` by the number of such anchors. Callers must + therefore iterate the result rather than assume ``len(rows) <= limit``. + + That recovery is deliberately bounded by the oversampled candidate set + (``limit * 8`` newest sessions): it re-uses rows the projection already + fetched and never issues an extra query, so an ancestor older than the + oversample stays unresolved and its children render top-level, exactly as + they did before. Widening that window is a ``candidate_limit`` change, not + a change to this walk. """ db_path = Path(db_path) if not db_path.exists(): @@ -766,7 +779,43 @@ def read_importable_agent_session_rows( projected = [row for row in projected if is_cli_session_row_visible(row)] if limit is None: return projected - return projected[:max(0, int(limit))] + selected = projected[:max(0, int(limit))] + + # The recency slice is per-row, but subagent rows are only renderable as + # children: the sidebar nests a child under its parent solely when that + # parent row is present in the same payload. A frozen orchestrator stops + # writing while its leaves keep streaming, so the leaves win the recency + # race and the parent falls outside the window — leaving the leaves to be + # promoted to top-level sidebar rows. Re-add subagent parents that the + # oversampled candidate set already projected (no extra query); webui + # ancestors are left out because that sidebar bucket already has them. + # + # Bounded by construction: ``by_id`` only holds the ``limit * 8`` + # newest candidates, so an ancestor older than that oversample is not + # recovered and its children stay top-level — the pre-existing + # behaviour, narrowed rather than fixed. Resolving those would need an + # unbounded per-row ancestor query on the hot sidebar path; widen + # ``candidate_limit`` instead if the window proves too tight. + # NOTE: this can return more than ``limit`` rows (see docstring). + have = {row.get('id') for row in selected} + by_id = {row.get('id'): row for row in projected if row.get('id')} + pending = list(selected) + while pending: + row = pending.pop() + if str(row.get('raw_source') or row.get('source') or '').strip().lower() != 'subagent': + continue + parent_id = row.get('parent_session_id') + if not parent_id or parent_id in have: + continue + parent = by_id.get(parent_id) + if parent is None: + continue + if str(parent.get('raw_source') or parent.get('source') or '').strip().lower() != 'subagent': + continue + selected.append(parent) + have.add(parent_id) + pending.append(parent) + return selected diff --git a/api/routes.py b/api/routes.py index 842ad3ef..7b22a1ad 100644 --- a/api/routes.py +++ b/api/routes.py @@ -22537,6 +22537,12 @@ def _handle_goal_command(handler, body): require(body, "session_id") except ValueError as e: return bad(handler, str(e)) + if _is_silent_control_message(body.get("args") or body.get("text")): + return j( + handler, + {"status": "suppressed", "reason": "silent_control_message"}, + status=200, + ) if _session_is_subagent_view_only(str(body.get("session_id") or "")): return bad(handler, "Subagent sessions are view-only and cannot run /goal from WebUI", 400) try: diff --git a/static/share.html b/static/share.html index e7dcf832..80be8315 100644 --- a/static/share.html +++ b/static/share.html @@ -6,7 +6,7 @@