# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > Each entry mirrors the corresponding [GitHub release](https://github.com/jyao97/xylocopa/releases) — see those pages for the full prose write-up. This file keeps the same content in Keep-a-Changelog form so it's grep-able from a clone. ## [Unreleased] ## [0.14.0] - 2026-07-24 Conversation branching release. A chat message can now be forked into a new agent that inherits the full conversation — including tool inputs/outputs and file-state context — by truncating and resuming the underlying Claude Code session transcript. Also fixes the localStorage quota exhaustion that silently dropped chat drafts and voice transcripts on long-lived installs. ### Added - **Diverge conversations.** Double-tap a chat bubble → the action menu gains a split-arrows button that forks the conversation at that message into a new agent. The source session JSONL is truncated at the message, re-keyed to a fresh session id, and resumed in a new tmux pane, so the branch replays the original transcript verbatim. On an agent reply the branch keeps that reply and continues after it; on a user message the branch cuts before it and the text prefills the new chat's composer for edit-and-resend. An optional purpose becomes the new agent's name, the Task title (panel attributes inherit from the source task), and the composer prefill. Pre-compact/clear sessions are searched too, so old messages remain forkable. Forked transcripts close with a standard CC interrupt marker, so agent status converges to IDLE through the normal JSONL-derived state machine with no out-of-band writes. (f77eb067, ce487d4e, 53e23579, 9f73fb31, e86f867c, cd7905e0) ### Fixed - **localStorage quota exhaustion ("The quota has been exceeded").** The persisted detail caches (agent briefs, insight suggestions) grew one entry per agent ever seen and eventually filled the origin quota — from then on chat drafts, voice transcripts, and composer prefills were silently dropped. Persisted caches are now capped at the 80 newest entries, and draft/voice/prefill writes go through a quota-safe helper that evicts advisory caches (largest first) and retries; real drafts and auth state are never evicted. (22862189) ## [0.13.0] - 2026-07-24 Model refresh release. Claude Opus 5 (released by Anthropic this month at the same $5/$25 pricing as Opus 4.8) becomes the default model for new tasks and projects, the model picker is slimmed to current tiers, the effort picker regains the missing High level, and the background summarization prompts are retuned. Also restores the frosted-glass treatment on the nav and input bars that never rendered on Chromium. ### Added - **Claude Opus 5 support** (`claude-opus-5`). Wired through the full stack: model picker, backend `VALID_MODELS` validation, pricing table ($5/$25 per MTok), 1M context-window limit for the context pill, and the MCP `task_create` model-selection guidance. (62270a45, 632a66e2) ### Changed - **Default model is now Opus 5** for new tasks and projects (`DEFAULT_MODEL` / `DEFAULT_CLAUDE_MODEL`). Fable 5 still leads the picker visually but stays opt-in due to ~2x cost. Tests now assert the config constant instead of a hardcoded model id. (289d1a4d) - **Model picker slimmed to five options** — Fable 5, Opus 5, Opus 4.6, Sonnet 5, Haiku 4.5. Opus 4.7 and 4.8 are removed from the picker but remain valid on the backend, so existing tasks/projects keep working and their tags keep proper labels via a legacy-label map. (b69ea27d, 6804a79d) - **Effort pickers include High again.** All four selectors (EffortSelector, NewTaskPage, QueueCard, InboxCard) offered only Low/Medium/XHigh/Max; the backend always accepted `high`. (62270a45) - **Summary prompts retuned.** Agent-insight extraction is tighter (max 8 items, one sentence each, drop anything obvious from the code); retry summaries are richer (5-10 bullets, ~1500 chars, with files touched, concrete failure evidence, and ruled-out dead ends) since they are the next attempt's only window into the previous one. (6ea85179) - **OpenAI helper calls read `OPENAI_QUICK_MODEL`** (default `gpt-4o-mini`) instead of overloading `SUMMARY_MODEL`, which now exclusively selects the Claude model for `claude -p` background jobs. (a38b5b9c) ### Fixed - **Frosted glass on nav + input bars renders on Chromium.** The `backdrop-filter` treatment silently never worked; fixed the root cause, tuned glass alpha to .65 light / .68 dark, and added a standalone `/glass-diag.html` device diagnostic. (aab41a04, 579637e8, a8c89e3b) ## [0.12.1] - 2026-07-18 Patch release hardening the v0.12.0 web terminal on mobile and fixing a chat-sync stall introduced by Claude Code's daemon-side auto-compact. ### Fixed - **Chat sync survives daemon-side auto-compact.** Claude Code's daemon can compact a managed agent's conversation by forking it onto a new session id (`--fork-session --resume `) with no agent header in the SessionStart hook; the orchestrator kept tailing the frozen pre-compact JSONL and the agent's chat silently stopped updating. The internal-worker classifier now surfaces the lineage `XY_AGENT_ID` and the resumed session parsed from argv, and when a fork resumes exactly an agent's current session the hook performs the same rotation as the in-process compact path and wakes sync. Verified live: replaying the fork hook rotated the stuck agent and imported the 60 missed messages. (321db643) - **Terminal overlay auto-reconnects.** iOS suspends the WebSocket whenever the PWA is backgrounded, which previously left a stale frozen screen and a manual Reconnect button. Returning to the app now re-attaches immediately (visibilitychange), foreground drops retry up to 3× with backoff, and a 25 s ping keeps NAT/proxy idle timeouts away. Clean exits and errors still require manual reconnect. (5edcbbca) - **No app bleed-through when the soft keyboard opens.** iOS scrolls the page to keep the focused input visible, so shrinking the overlay to `visualViewport.height` alone exposed a strip of the underlying chat page above the keyboard. The terminal pane now pins to the visual viewport (height + `translateY(offsetTop)`, tracking both resize and scroll) inside a full-screen backdrop that never resizes. (23b001d8) ## [0.12.0] - 2026-07-18 Interactive web terminal release. The chat header gains a terminal icon that attaches straight to the agent's live tmux session from any browser — phone included — over a new WebSocket PTY bridge. Also fixes CLI-session discovery misclassifying Claude Code's internal daemon workers as adoptable sessions. ### Added - **Web terminal (tmux attach) in chat.** A terminal icon in the chat header opens a full-screen xterm.js overlay attached to the agent's tmux session via the new `/ws/terminal/{agent_id}` endpoint: a server-side PTY runs a dedicated `tmux attach` client, binary frames carry PTY output, JSON messages carry input/resize/ping. Closing the overlay only detaches that client — the session keeps running. `window-size latest` is set on attach so a phone-sized client doesn't shrink the desktop tmux view. Touch devices get an Esc/Tab/sticky-Ctrl/arrows key bar; visualViewport handling keeps the prompt above the soft keyboard; xterm.js loads lazily (~300 KB chunk) only when the terminal is opened; the overlay respects the iOS safe-area top inset. (87350f55, 905e5ef5, fbbe190a) ### Fixed - **CLI-session discovery no longer flags Claude-internal workers.** Claude Code's daemon architecture (bg-pty hosts, `--fork-session` compact workers, subagents) runs pane-less claude processes whose SessionStart hooks look like a user CLI started outside tmux; they surfaced as adoptable "not in tmux" entries, and adopting one would kill a live internal worker and resume its session in a spurious tmux session. The hook now walks /proc lineage of the process owning the session id (exact argv-token match against a claude binary) — XY_AGENT_ID environment or daemon/bg-pty-host ancestry marks the session internal and skips the entry; `source="fork"` is never adoptable. (f670641d) ### Changed - **Queued/scheduled message editing moved to the composer.** "Modify" on a queued or scheduled bubble loads the text into the bottom input bar instead of an inline bubble textarea, so the text stays visible above the mobile keyboard while editing. (29fa52f1) ## [0.11.0] - 2026-06-10 Interactive web-app preview release. Agents can now hand the user runnable deliverables — data explorers, 3D viewers, local dashboards — as tappable cards in chat, opening fullscreen in a sandboxed iframe with a built-in console drawer. Verified end-to-end with TensorBoard through the port proxy and an agent-built three.js exoplanet viewer on iPhone. ### Added - **Web-app cards in chat.** New `webapp_present` / `webapp_list` MCP tools post `kind="webapp"` card messages and maintain a per-project registry (new `webapps` table) so later agents can find and reuse existing viewers. Cards are tool-driven only; plain-text `.html` paths no longer render run cards. (6c868bb1, bb5433e5) - **Static app serving.** `/api/preview/t/{token}/{project}/{path}` serves project files for preview with a path-embedded, preview-scoped 12 h token, so relative subresources (`./app.js`, `./data.json`) authenticate without cookies. Responses carry a CSP `sandbox` header and `Access-Control-Allow-Origin: *`; served HTML gets an injected script that mirrors `console.*` and uncaught errors to the panel's debug drawer. (35fa749f) - **Localhost port proxy.** `/api/preview/p/{sig}/{project}/{port}/{path}` reverse-proxies registered local services (TensorBoard, dev servers) with a stable HMAC capability prefix, WebSocket relay with subprotocol passthrough, a registered-port allowlist, and the orchestrator's own port denied. Cookie-session apps (wandb local, jupyter) are documented as `url` cards instead — the sandbox is credential-less by design. (6c868bb1, 41bc3c08) - **Sandbox compatibility shims.** Injected into served/proxied HTML: in-memory `localStorage`/`sessionStorage` and no-op `document.cookie` (opaque-origin access throws otherwise), and a `Worker` wrapper that falls back to sync-XHR + blob URL when URL construction is blocked — required for TensorBoard's worker-based chart renderer. (bd2f85cb) - README feature entry with a phone screenshot of the preview panel. (6763dbb5) ### Fixed - Status signals only emit on genuinely-new turns, preventing re-fired side effects on replayed history. (3adba714) - Drift audits exclude intentionally-unimported turns, removing false-positive purge warnings. (89c9e1d6) ### Changed - **Auth: scoped tokens are rejected as session tokens.** `verify_token` now refuses any token carrying a `scope` claim, so a leaked preview URL can never grant API access; `decode_token` returns the verified payload for endpoint-specific checks. (35fa749f) - **CORS: the literal `null` origin is allowed** (sandboxed iframes send it) with `allow_credentials=False`, and vite's own CORS middleware is disabled so `/api` preflights reach the backend instead of being answered with a rejecting allowlist. (bd2f85cb) - Documentation sweep: README rewritten (351 → 213 lines, one home per fact), getting-started (en/zh) tightened, `agent-mcp-tools.md` updated with the probe and webapp domains, stale MCP tool list in ARCHITECTURE.md corrected. (20231530, 745faa4b) - Orchestrator refactors: lookup-or-404 helpers replace 45 boilerplate sites, shared `build_task` extracted to `task_service`, PORT/default-model constants centralized, `_utcnow` deduplicated, dead code removed. (d1b8d59f, 02d5e4c9, 8edd9d36, 9a861d5b, 1ea9fc75) ## [0.10.15] - 2026-05-14 Memory-leak hunt release. Two OOMs on May 13 grew orchestrator RSS to 47.8 GB before the kernel killed the process — which took every tmux'd xylo session down with it. This release plugs five leak vectors found via a parallel multi-agent code audit, adds a `max_memory_restart` safety net so the next leak triggers a pm2 graceful-restart at 8 GB instead of a kernel global-OOM, and ships diagnostic infrastructure (size-bounded forensic log channels, a live `/api/debug/mem-introspect` endpoint, a header-pill memory-pressure indicator) so the next leak leaves a usable trail. ### Memory leak fixes - **`_promote_ack_pending` / `_promote_ack_by_agent` never cleaned in `stop_agent_cleanup` / `error_agent_cleanup`.** The GHOST_DELIVERED instrumentation dict (added 2026-04-30) records ~500B–1KB per promoted message and was explicitly designed to "leave it in the dict so a late hook can still ack it" — but stopping or erroring an agent didn't drop those records. Across many sent messages plus agent churn this accumulated unboundedly. `stop_agent_cleanup` and `error_agent_cleanup` now pop from both dicts, and `_record_promote_for_ack` caps the per-agent queue at 256 entries so even long-running agents that never stop can't grow it forever. While in there, also fixed missing cleanup of `_generation_ids` and `_known_subagents`. (822edfa, b96870b) - **`cc_session_reconcile.reconcile_all` shared one SQLAlchemy session across 500+ agents without flushing the identity map.** Each agent's `reconcile_agent` loads CCSession ORM rows into the shared session; at the 80k-JSONL scale the identity map accumulated tens of thousands of ORM objects pinned until the final commit. Now flushes pending writes and runs `db.expunge_all()` after each agent so memory tracks one agent at a time. Reconcile time grew from a few seconds to ~9 minutes for the 92k-JSONL scan, but RSS stays bounded. (822edfa) - **`claude -p` insight/summary subprocesses had no concurrency cap.** Six call sites in `routers/{agents,projects,tasks}.py` spawned `threading.Thread(daemon=True).start()` per request. When many agents stopped at once each call buffered `proc.communicate()` stdout in memory, and concurrent runs stacked GBs — the ~10 GB sibling python process seen alongside the killed orchestrator at both OOMs. Replaced all six sites with `INSIGHT_EXECUTOR = ThreadPoolExecutor(max_workers=2)` so at most two claude subprocesses run concurrently and the rest queue. (822edfa) - **`display_writer._pre_sent_index` not cleared when an agent was deleted.** Per-agent pre-sent message state stayed in the module dict indefinitely for deleted/permanently-removed agents. `delete_agent()` now pops the entry and discards the agent_id from `_pre_sent_index_ready`. (b96870b) ### Safety net - **pm2 `max_memory_restart: '8G'`.** Steady-state RSS is ~200 MB so 8 GB is well above any normal spike. If a future leak passes that threshold pm2 graceful-restarts the backend instead of letting it grow until the kernel global-OOMs the whole user.slice (which previously took every tmux session down with it). Needs `pm-logrotate` module — installed and configured (50 MB × 4 retained = 200 MB per pm2 log stream). (5d66db3) - **Leak-alert probe wakes the diagnostic chat at 5 GB.** RSS_WATCH posts to `XY_RSS_LEAK_PROBE_URL` (in `.env`, gitignored) once when RSS first crosses 5 GB so the diagnostic chat gets woken in real time, ~3 GB of headroom before pm2's restart kicks in. Single-fire by design; renew via the `probe_create` MCP tool. (c248047) ### Diagnostic infrastructure - **`/api/debug/mem-introspect` endpoint.** Returns `/proc/self/status` (VmRSS/Peak/Anon/File/Threads), `gc.get_count/stats/total_objects`, top 20 object types by count, and sizes of every named in-memory cache we own (`_promote_ack_pending`, `_pre_sent_index`, `_translate_cache`, `_INSIGHT_RUNS`, etc.). `?collect=1` runs `gc.collect()` + `libc.malloc_trim(0)` first so the breakdown reflects retained memory not pending-sweep. Auth-exempt for shell access during an incident. (3b33133) - **Four forensic log channels (internal-only, never reach the UI).** All write to `orchestrator.log` so size-bounded log rotation contains them: - `RSS_WATCH baseline` every 5 min logs the full memory shape (`rss/vm/peak/anon/file MB`) so the next leak leaves a curve to look back at — not just threshold crossings. Plus `RSS_WATCH: crossed N MB` on first cross of each threshold (200/500/1k/2k/5k/10k/20k MB) and `RSS_WATCH: jumped N MB in last minute` if growth exceeds 200 MB/min. (6949c9f) - `cc_session reconcile … took=Xs rss=A→BMB delta=+CMB` every reconcile cycle. (65823d8) - `claude_subproc