# Release v2.5.0

Released: 2026-05-20

## Added

- **Network Exposure dashboard widget.** Shows how PortOS is exposed on the network — HTTP vs HTTPS, bind address (loopback / all-interfaces / specific), cert mode (Tailscale-trusted hostname vs self-signed), the loopback HTTP mirror port when HTTPS is on, and whether the current browser origin allows mic/voice. A red banner appears when PortOS is bound on all interfaces over plain HTTP. Seeded into the default "Everything" and "Ops" dashboard layouts.

- **CoS task form: pick a reviewer for the Review Loop.** A compact dropdown next to the Review Loop checkbox lets you pick the post-PR reviewer (Copilot / Claude / Gemini / Codex; default Copilot). Non-Copilot choices skip the Copilot pre-request and tell the follow-up agent to drive an iterative CLI-based review against the PR diff instead. Agent cards show the selected reviewer when it isn't the default.

- **Deep-linkable image preview modals.** Every page hosting an image gallery (Media History, Media Collections, Image Gen, Video Gen, Universe Builder variations + cast, Comic Script Stage, Nouns Stage) now drives its lightbox via a `?preview=<filename>` URL parameter — so a previewed image is shareable and survives reload. The browser back button closes the modal; prev/next navigation keeps history clean.

- **Auto-clean after image generation — per-provider opt-in.** New checkboxes in Settings → Image Gen turn auto-cleaning on independently for the Codex, Local, and External providers. When on, every generated image is denoised and stripped of C2PA provenance metadata before it shows up in the gallery — what you see is already cleaned, no extra click. Clean failures fall back to the original image and never break the generation.

- **Per-render "Auto-clean this render" checkbox on the Image Gen form.** A checkbox under the Generate button lets you override the saved auto-clean default for a single render; the label flips to "(overrides saved default)" when it differs from the saved value. Quietly fixes a latent bug along the way: the saved auto-clean toggle previously had no effect for Codex or Local renders — only External worked. All three providers now honor the saved setting.

- **Lightbox: original ↔ cleaned variant toggle.** When the previewed image has a manually-cleaned sibling (or is itself a cleaned sibling), a "View: Original | Cleaned" segmented control appears at the top of the lightbox details panel so you can flip between variants without closing the modal. The toggle is also a deep-linkable URL change. Auto-cleaned images replace the original in place, so the toggle correctly doesn't appear for them.

- **Manual clean auto-files into the source's collections.** Cleaning an image via the lightbox Clean button used to leave the cleaned copy out of every collection the source belonged to. The cleaned image is now automatically added to every collection that contained the source, so you don't have to re-add it everywhere.

- **Per-bucket exporter hash cache.** Subscription re-exports of large series are dramatically faster — assets are no longer re-hashed on every export. A 200MB re-export drops from roughly 200MB of disk reads to a single index load plus one stat per file.

## Changed

- **Honest SynthID copy in the Image Cleaner.** The Clean tooltip, the auto-clean settings descriptions, the per-render Image Gen checkbox, the Image Clean dev page, the success toast, and the palette keywords now all explicitly say the cleaner removes C2PA provenance metadata but does **not** defeat Google's SynthID watermark. The prior copy ("strips C2PA + reduces AI artifacts") was technically accurate but implicitly over-promised against SynthID-bearing renders (gpt-image / Imagen / Gemini). The Image Clean dev page now links to openai.com/synthid for verification.

- **Image cleaning collapsed to a single button.** The light/aggressive split is gone — "light" mode proved unreliable in practice (visible C2PA-adjacent noise remained). The lightbox Clean affordance is one button with a clear tooltip; the Image Clean dev page dropped its level picker. Existing cleaned files on disk round-trip unchanged.

- **First-run no longer silently flips port 5555 from HTTP to self-signed HTTPS.** PortOS used to auto-generate a self-signed cert on every start when Tailscale was unavailable, which broke the documented `http://localhost:5555` URL and forced a browser click-through. Fresh installs without Tailscale now stay on plain HTTP at the documented URL. The setup banner is cert-aware — when HTTPS is provisioned, it prints the loopback HTTP mirror (`http://localhost:5553`) and the Tailscale hostname instead of the now-broken localhost URL.

- **Universe Builder cast preview now matches Media History exactly.** Clicking a cast/canon image used to open a stripped-down modal showing the character description instead of the prompt, with missing controls (no negative prompt, no notes, no Remix / Send to Video / Clean / Favorite). Canon clicks now open the same full-featured lightbox as the rest of the app, hydrated from the gallery sidecar so prompt, negative prompt, model, seed, and resolution all show up. The character descriptor falls back as a label only for legacy renders with no sidecar.

- **Canon entries inline-edit their description.** The main description on each canon card (characters / places / objects) was previously a static paragraph with no edit affordance. Unlocked rows now render a buffered textarea — type, blur, save. Pre-migration characters that used the older `description` field migrate to the new field on first save. The pipeline series view stays read-only on purpose.

- **Universe Canon — Render All per kind, compact single-kind view, deduped lock indicator.** A new "Render all (N)" button in each canon kind's toolbar fires reference renders in parallel for every entry that has a description but no image. When the canon section is already scoped to a single kind (deep-linked via `?kind=places` or a sidebar entry), the redundant inner card chrome is dropped so the page no longer shows a nested card-in-card. The redundant "🔒 LOCKED" chip on locked entries was removed because the lock toggle button on the right already conveys that state.

- **[med-sharing-server-services-sharing-version-js] Cross-peer share imports salvage older universe canon instead of silently dropping it.** Share-bucket imports from pre-B.4 PortOS installs used to lose any series-side canon (characters, places, objects) silently because the post-B.4 series sanitizer strips those fields. The importer now folds that legacy canon into the linked universe (creating a placeholder universe for orphaned series) so the data survives the migration.

- **[med-ai-dispatch-server-lib-tuipromptrunner-js-198] TUI prompt-runner buffer raised from 1MB to 8MB; overflow is now visible.** Realistic full-context LLM replies (~600KB UTF-8 from a 200K-token window plus screen chrome) no longer head-truncate mid-token. When the buffer is exceeded, a one-time warning fires and the run record now carries an `outputTruncated` flag so the Runs page can flag responses where the start may have been lost.

- **[agent-tui-spawn-truncation-warn-parity] Agent TUI spawner surfaces output-buffer overflow.** Long-running CoS agents whose failure tail was previously buried behind banner-redraw chatter (and silently dropped on truncation) now record an overflow flag on the agent record with a one-time warning. Distinguishes "long run with overflow" from "clean short run" in error analysis.

- **[med-ai-dispatch-server-services-digital-twin] TUI providers now work for Digital Twin AI features.** Settings → Digital Twin previously hung or returned banner garbage when the active provider was a TUI (claude-code, etc.) — the dispatcher was piping stdin to a process expecting a PTY. Twin import, enrichment, testing, and analysis now flow through the shared runner with proper TUI handshaking.

- **[med-ai-dispatch-server-services-aidetect-js-242] TUI providers now work for Settings → Detect App.** The "Unknown provider type" error path is gone; TUI dispatch goes through the shared runner with the analyzed directory as the working directory, and JSON parsing is more forgiving of TUI banner output.

- **[cleanplateprompt-migrate-to-rich-canon-fragments] Clean-plate prompts include era and weather.** Composing a clean-plate (background-only) image prompt used to drop the `era` and `weather` fields from place canon. The full descriptor now flows through, matching how storyboard and comic-panel renders already handle it.

- **[med-pipeline-server-routes-pipeline-js-1416-1452] Cover render no longer overwrites the cover script when the request omits it.** Triggering a cover render previously rewrote the persisted script field on every render, racing against any in-flight blur-save of the script. Now: an absent script field preserves the saved script, an explicit empty string clears it, and present text writes through.

- **[med-pipeline-server-services-pipeline-arcplanner-2] Arc-planner no longer guesses wrong on ambiguous season pairings.** When an arc-resolve LLM call left more than one season unmatched on each side, the positional-pairing fallback used to silently invent season-id mappings — child issues landed under the wrong season instead of the visible "ungrouped" bucket. The fallback now only fires for the unambiguous 1-on-1 case; multi-side ambiguity correctly routes orphans to ungrouped and logs the cardinality.

- **AI Toolkit warm-up errors return 503 with a typed code.** Requests that hit the runner, prompt service, or provider service during toolkit warm-up used to surface as untyped 500s with string-matched messages. They now return `503 AI_TOOLKIT_NOT_INITIALIZED` so clients can distinguish "service still initializing" from genuine errors.

- **Mic-availability hint recognizes the full loopback range.** The secure-context predicate that decides whether mic / voice features are available now matches the entire `127.0.0.0/8` block and the bracketed IPv6 `[::1]` form, matching how browsers actually decide a context is "secure."

- **Retire `DONE.md`.** The audit trail for completed PLAN.md items is now `git log` + `.changelog/` — `/claim` removes the picked item from PLAN.md outright and appends to `NEXT.md` instead of archiving to DONE.md. Existing repos with a DONE.md on disk are left untouched. The slashdo submodule was bumped to v2.18.0 to match.

## Fixed

- **`update.sh` / `update.ps1` start PortOS when it wasn't already running.** Both updaters tear down PM2 early to clear stale fork paths, then previously called `pm2 restart` to bring the app back — which doesn't reliably restart processes that aren't currently managed. Running the updater while PortOS was stopped ended an "update complete" cycle with no server running and the browser auto-open timing out against a dead port. The restart step now boots a fresh daemon and starts the apps, matching the normal start flow.

- **[med-migration-scripts-migrations-014-attribute] Migration 014 no longer writes `'unknown'` as a phantom annotation author.** On a fresh install, the migration runner used to fire before local-identity initialization, so legacy annotations got stamped with the literal string `'unknown'` — and from then on, those notes couldn't be re-saved or exported (the annotation pipeline refuses to write under `'unknown'`). The migration now lazy-creates the identity inline, and the annotation reader heals any pre-existing `'unknown'` bucket by re-keying it to the real local instance id.

- **HF token banner no longer surfaces in Codex / External mode.** Picking a gated FLUX model and then switching the backend chip to Codex used to leave the "paste your HuggingFace token" banner stuck over the Codex form, even though Codex never touches HuggingFace. The gate now requires Local mode first.

- **[med-pipeline-server-services-pipeline-arcplanner] Arc-planner warns when an LLM emits duplicate season ids.** Two seasons sharing an id were silently dropped on persist (last-write-wins by id) while child issues kept their season pointers — leaving issues attached to the surviving season under the colliding id, which was rarely what an arc-resolve was supposed to do. A warning now flags the offending ids so the responsible run is grep-able. Behavior is preserved; the warning is the new signal.

- **v2.1.0 review residue — small-fixes bundle.** Eleven small fixes batched: Universe Builder render registration moves before the enqueue loop so the worker can't race past run registration; volume PDF sort puts null-arcPosition issues at the end instead of interleaving via the number fallback; series PATCH warns when legacy canon fields are stripped so stale-tab POSTs are observable; prompt runner cwd guard treats empty-string overrides as absent; loop runner no longer misclassifies a successful `onComplete` throw as `iteration:error`; sharing migration defers the orphan-series universe-link write until after the canon patch lands so a crash window can't leave the series stripped and the universe empty; importer warns when arc-extract returns empty summary + logline before falling back; ANSI stripper handles `\x1B\x1B[31m`-style chunks that previously leaked an orphan escape byte.

- **Sharing / annotation-merge hardening.** Five sharing fixes: outgoing annotation manifests honor per-bucket display-name overrides (previously all buckets got the global name); peer payloads with missing or invalid timestamps are dropped on import instead of winning last-write-wins via a `now` fallback; future-skewed peer timestamps are clamped to local-now so one bad clock can't ratchet every subsequent merge; peer payloads with an unknown instance id are refused on import to match the outgoing-path guard; and exporting annotations to a bucket no longer writes an empty manifest into every auto-merge bucket on a fresh install with no annotations.

## Removed

- **`CODEX5.5_REVIEW.md`** at the repo root — the concrete bugs from that review have all shipped, and the strategic items live in PLAN.md.

## Full Changelog

**Full Diff**: https://github.com/atomantic/PortOS/compare/v2.4.0...v2.5.0
