⚠️ macOS notarization is still pending for this release.The
.dmgbelow is signed with our Developer ID, but Apple has not returned
its notarization ticket yet, so macOS blocks it on first launch. We publish the
release rather than hold it back.
This asset is replaced with the notarized one as soon as the ticket arrives
— usually within a few hours. Re-download it then and none of the steps below
apply.Two ways to install v0.16.0 with nothing extra to do:
- Already running Lucidos? Take the in-app update. It does not go through
Gatekeeper and is unaffected by notarization.- Prefer the terminal?
curl -fsSL https://lucidos.dev/install.sh | shIf you want the
.dmgright now, macOS needs you to approve the app once
by hand (Sequoia removed the old Control-click shortcut):
- Open the
.dmgand drag Lucidos to Applications.- Launch it. macOS says it "could not verify" the app — click Done.
- Open System Settings → Privacy & Security, scroll down to Security,
and click Open Anyway. Do this within an hour of step 2; it asks for
your administrator password.- Launch Lucidos again and click Open Anyway in the second dialog.
You only do this once. Subsequent launches are normal.
v0.16.0 — 2026-07-29
Added
-
Provider-native web search —
web_searchnow resolves over the configured provider set instead of a single hardcoded backend. Adds aWebSearchProvidertrait, a fallback chain, and three backends (Anthropic, OpenAI, Vertex/Gemini grounding), each pinned to a model its own provider actually serves. Prompt and result formatting are shared across backends, andmax_usesis decoupled from the chain. (ADR 0023) -
Per-model context windows — every builtin model now declares a verified context window, editable in Settings, and that value drives the context-trim budget instead of a guess derived from the model id. The reported token total counts tool schemas, and truncation is surfaced honestly as trimmed. Bare Claude rows stay on the prefix map since they send no 1M beta header.
-
GPT-5.6 (Sol / Terra / Luna) — added to the chat model picker and the Codex coding-agent
/modelpicker, with themaxreasoning tier enabled for the family. Max effort is validated server-side and filtered by the selected model in the picker. -
Claude Opus 5 — added to both the chat and Claude Code pickers and set as the default model (via
cc-settings.json). -
Built-in model-provider proxies —
vertex,openai,openrouter,anthropic, andlocalare available as proxy targets out of the box, so an app can call a model provider without hand-registering it inapis.json. Bearer credentials get the Anthropic OAuth beta header injected automatically. -
Packaged system-knowhow —
system-knowhow/ships as the 7th runtime resource and is resolved at runtime viaLUCIDOS_SYSTEM_KNOWHOW_DIR, so a packaged install has the same authoritative reference docs as a source checkout. -
Codex parity work — Codex sessions stream reasoning summaries, load
CLAUDE.md, validate the effort tier, surface plan-tool progress over the app-server protocol, and map slash commands to playbook files. -
Chat threads auto-resume after a version switch — a chat or trigger thread interrupted by Switch to new version now resumes on the next boot, matching what coding-agent threads already did, instead of leaving a device-attributed abort and a manual Continue button. A dedicated boot pass handles them (the switch teardown always lands a terminal event, so the orphan sweep structurally can't see them), and the shared cause gate now requires an engine-shutdown cause so a user Stop is never mistaken for a switch.
-
Stranded-Apply visibility — a frontend Apply that can't reach the served dist is now surfaced instead of failing silently, and the engine warns once when the dist it's serving is pinned to a coding-agent worktree.
-
Zero-file Apply refusal — Apply is refused server-side on a change whose branch diff has gone empty, and such a change is reconciled (without discarding its siblings) rather than left blocking the panel.
-
Compose-clear announcements — every clear of the compose box is announced to other devices, whether it came from an ordinary send or from answering a question card.
-
App Store Connect API key notarization — the release build prefers an ASC API key over an Apple ID app-specific password when one is configured, so notarization runs cleanly headless.
-
One source of truth for the release version, enforced —
RELEASEis the only place the version is written by hand; everything else derives from it (the build reads it, the release flow rewritesinstall.sh's baked constant, the site publisher pins the landing page's download links at publish time). A new offline suite scans the tree for a version literal nothing keeps in sync, pins both halves of theinstall.shmechanism so deleting the substitution fails immediately rather than at the next bump, and flags prose that announces which release line the project is on. Historical narration stays exempt — the changelog, plans, and ADRs are correct precisely because they don't move. Phase A runs the suite against the worktree after the bump, so a stale literal fails the release instead of reaching the mirror. -
Resumable notarization — the release build submits with
--no-waitand writes a resume handle (submission id, DMG path + sha256, source commit, submit time) to disk before it starts polling, so losing the waiting process costs a poll instead of a full rebuild. A build-grade run auto-resumes when a handle matches the DMG on disk,--adopt-submission <uuid>picks up a submission that's already in flight, stapling is idempotent, and the handle is cleared once staging succeeds. Backed by a pure, offline-testable state library (scripts/lib/release_notarize.sh) with atomic writes and a checksum gate that refuses to resume against a changed DMG. -
A re-fold on unchanged bytes no longer burns a notarization submission — one release spent 16 Apple submissions in two days, several of them on byte-identical compiled input, because any movement on
mainforced a rebuild + resign + resubmit. The build now fingerprints only what actually reaches the bundle — the seven compiled-input paths (crates,Cargo.lock,Cargo.toml,packages,package.json,package-lock.json,system-knowhow) — and Phase A consults that gate before rebuilding, reusing the already-notarized DMG when every hash matches. A docs-, plans-, orscripts/-only re-fold is now free. The fingerprint deliberately excludes the driver scripts themselves (a one-word comment edit inbuild-dmg.shwas enough to flip a coarser hash and re-break the very case this fixes); a separate recipe fingerprint tracks those, and both are recorded in the staging manifest. Also fixes a concurrent-poller miscount where a resume's own subshell was reported as a second poller. -
Shellcheck gate on every shell script —
scripts/lint-shell.sh+make lint-shell, wired intolintsomake checkcovers it. Discovery isgit ls-files '*.sh'rather than a hand-written path list, so a script added in any directory is gated the day it's committed. Fails closed on all three ways it could be disarmed (no shellcheck on PATH, empty discovery, a silently-skipped file). The sweep that brought the tree clean fixed the findings at the source rather than suppressing them — theSC2155masking bugs below were found by it. -
A release no longer waits on Apple — notarization verdicts have taken anywhere from ten minutes to fourteen-plus hours, and a wedged notary used to hold the whole release.
--defer-notarizationstages the DMG unstapled (recorded asnotarized: falsein the staging manifest) and publishes with a "notarization pending" banner on the release body, so the in-app update, the headless tarball andcurl … | sh— none of which touch Gatekeeper — ship immediately, while the download button deliberately stays on the previous notarized DMG. Against a submission already in flight it stages without polling at all, which rescues a Phase A stuck on a slow verdict with no rebuild.--attach-notarizedthen staples, swaps the published asset in place, removes the banner, and fires the clean-machine DMG gate against the published tag. (ADR 0027)
Changed
- The release candidate is the published artifact — the rc branch is now a pre-stripped, validated tree that gets promoted to
mainrather than rebuilt at publish time, so the commit CI gates is byte-identical to the commit that ships. Adoption requires a parentless candidate (a candidate carrying ancestry would push every reachable object while the private-data scan only inspects the tip tree), and the promote step re-asserts that at the irreversible push. The exclusion list, theWORKSPACES.mdstub, and the fail-closed private-data scan are now single-sourced. (ADR 0024) bash_output(wait_secs)actually blocks — the drain now waits the full requested window (server-side) rather than returning immediately, and reports a real elapsed clock. A user message cuts the block short so their follow-up isn't stuck behind it.- Background tasks report their true exit status — the completion summary no longer masks a failing pipeline stage;
pipefailsemantics are documented (rightmost failing stage wins),128+signumexit codes are decoded, and a trigger script killed by a signal is named as such. - Accent palette —
--accent-yellowretheming to a muted sand, with the "notable state" role split off into its own--accent-notabletoken. Dead--initiator-*tokens dropped. - Client build id — the frontend shows the client's own build id instead of the engine's frozen CalVer, and launch binaries are published per build variant rather than through cargo's shared uplift path. (ADR 0022)
- Dev stack refuses worktree pinning — a dev stack can no longer be launched from a coding-agent worktree, in either the shell path or the gateway; the opt-out no longer buys a machine-global gateway. (ADR 0021)
- Picker tooltips — native
titletooltips replaced with the shareddata-tooltipsystem, keeping the status dot's accessible name intact. - Rules layout — the monolithic scripts rule split into
dev-runtimeandbuild-release, with path-scoped frontmatter so rules load conditionally; env-var reference moved out ofCLAUDE.mdinto a lazy-loaded skill.
Fixed
- The packaged macOS app could not talk to its own backend — upgrading to Tauri 2.11 broke every webview→Rust IPC call in the shipped bundle, and with it native notifications, window dragging, and the durable device id. The packaged window loads over the gateway rather than
tauri://localhost, which Tauri treats as a remote origin; 2.11 tightened the ACL so a remote origin gets no permissions unless a capability names it explicitly. Every command failed the ACL check, the startup health probe never completed, and the reload watchdog re-loaded the WKWebView every 60 seconds forever — a loop that could not possibly help, since the origin's permissions don't change on reload. Fixed with an app-level ACL manifest (allow-app-ipc) granting the gateway origin its permissions, a watchdog that backs off when reloading demonstrably isn't the cure, per-command IPC health tracking, and an honest report to the engine log when the bridge is down. (ADR 0028) - New installs get persistent notification banners — macOS assigns every newly-authorized app the Banners alert style, which auto-dismisses after about five seconds, so a Lucidos notification could vanish before it was read. The bundle now declares
NSUserNotificationAlertStyle = alert, the same first-launch default request Chrome and iMovie ship, so a fresh install starts on the sticky Alerts style. It is only a default: once macOS has created the app's Notification Center entry, the user's own choice in System Settings wins permanently. - A cron slot no longer double-fires after a restart — catch-up re-ran a slot that had already run, and the recorded run time is now resolved in Rust rather than in SQL.
- The repositories picker no longer sticks on a cancelled read — an aborted fetch was latched as a permanent error state instead of a transient one.
- Codex can write to the workspace again — the sandbox denied
EPERMondata/because the writable root was derived from the git common dir rather than the actual workspace data directory. The root is now canonicalized, and adatasymlink may relocate the sandbox hole but never widen it. - An auto-resume after a version switch no longer wedges the thread — a confirmed resume attach was being classified as stale, and a failed continuation left no recovery arm. Losing the spawn race to a live session no longer settles it.
- The real published front door is tested in CI — a job now runs the exact advertised command (
curl -fsSL https://lucidos.dev/install.sh | sh) against the live origin rather than a checkout, with the origin parameterised so an RC can be gated the same way. The installer also rejects a soft-404 HTML payload before sourcing a fetched helper lib, instead of executing a web page. - Autocorrect on prose text fields — a JSX
autocorrect="off"inverts to on; the shared stamp now owns the attribute. - The focused pane paints through the reclaimed macOS title-bar band instead of stopping short of it.
- Rust toolchain pinned (1.94.1) so a "clean" build stops drifting between machines.
- The
curl … | shinstall one-liner works again — the bakedLUCIDOS_DEFAULT_VERSIONfallback ininstall.shhad been stranded at0.14.0whileRELEASEmoved on, and it is precisely the value a piped install resolves (no checkout ⇒ no adjacentRELEASEfile to read). Since v0.14.0 predates the headless tarballs, the advertised one-liner was not installing an old version — it was 404ing. The release flow now rewrites that constant in the same step that bumpsRELEASE, failing loudly if the substitution doesn't match, and a guard test asserts the two can never diverge by hand. - …and it works on Linux too — a real clean-machine test (fresh
ubuntu:22.04, the exact command the README and landing page advertise) found two more blockers, neither visible to CI, which runs the installer from a checkout and so never takes the piped-dash path. On Debian/Ubuntu/bin/shis dash, so the bash re-exec guard re-fetched the script fromLUCIDOS_INSTALL_URLand exec'd that — discarding the copy the user piped and re-resolving its own baked version, so piping lucidos.dev's 0.15.0 installer actually installed 0.14.0. The guard now pins the resolved version across the re-exec (an explicitLUCIDOS_VERSIONstill wins) and the constant moved above the guard so both branches read the same value. - A wedged coding-agent thread no longer needs an engine restart to clear — membership in the session table was treated as liveness by every reader, but only the run loop ever set
process_exited. A run future dropped rather than completed (cancelled caller, aborted task) left the entry behind with that flag false and its receiver gone. One such phantom fooled three readers at once: worktree cleanup logged "live agent session active" forever, the chat fast path sent follow-ups into a dead channel, and the resume guard refused every follow-up with "A coding agent is already running for this thread". Liveness is now derived (!process_exited && !msg_tx.is_closed()), and a drop guard marks the session dead on every abnormal exit path. - Apply's merge session survives the browser disconnecting — Tier 2 awaited a whole coding-agent merge inline, so the session's lifetime was the caller's HTTP future. iOS Safari dropping the connection 72 s into a conflict resolution killed the merge mid-tool. Tier 2 now detaches through the same guarded spawn Tier 1 and Tier 3 already use and answers immediately with a conflict result. Hardening review caught two consequences of the detach and fixed both: the orphan-sibling reconcile (which stops a stale pending change on another branch from blocking Archive) now runs inside the spawned task, gated on the change actually reaching
appliedso a failed or handed-off merge can't discard a newer sibling's work; and the drop guard no longer claims an abort for an entry it doesn't own. - An interrupted thread keeps its red status dot — an interrupted Lucidos Agent thread kept its
failedstatus; an interrupted Claude Code / Codex thread silently lost it. Both channels emit the same abort, so the abort was never the problem — what lands after it was. The restart teardown emits the boundary abort while the subprocess is still alive, and the duplicate-terminal suppressor doesn't stop the activity stream, so tool results still arriving milliseconds later (~13 ms in the observed trace) hit the "bump back to running" arm and overwrote the verdict. The chat mirror of the same bug: the shutdown sweep emitted an abort with no request id, so the loop's own cancel couldn't be paired with it and the phantom cancel walked the red dot back to idle —thread_summaries.statusis last-write-wins, which the old docstring's "abort takes precedence" claim only held for the exchange label. - A test run can no longer kill the machine's live dev engine — the host-pid kill guard read caller-owned state in both its arms, so any caller could switch it off.
ports_test.shdoes exactly that as part of being a well-behaved sandboxed test (it unsets the host/frontend pid vars and pointsHOMEat a temp dir), which left the live engine invisible to the guard, matching*lucidos-engine*on cmdline, and reachable by the stale-port reclaim. It died twice on 2026-07-28. Two additive arms were added — the guard gains reasons to refuse, never reasons to permit — and the test suite is now structurally unable to signal the host. (ADR 0025) - Silently-swallowed command failures in the workspace scripts — nine
SC2155instances oflocal x="$(cmd)", where the exit status islocal's rather than the command's, so a failingdate/ container lookup / database-URL resolution passed silently. Split into declare-then-assign. Three globals with zero readers anywhere in the repo were deleted rather than suppressed. CONTRIBUTING.mddescribes how contributing here actually works — the guide read like a conventional upstream repo, so a contributor had no way to know this repository is a published mirror:mainis one parentless commit force-pushed per release, successive releases share no history, and a PR is imported (squashed onto the previous tag with aCo-authored-by:trailer, then closed with a link to the release that carries it) rather than merged. It now says so up front, including what to do with a fork whose ancestor no longer exists, and notes that CI never runs on PRs so contributors should report what they ran locally. Two documents separately announced that Lucidos was "currently on the 0.9.x line" — a claim nobody re-reads at release time; both now point at the newest tag.- A first-run install no longer races the PostgreSQL init server — a clean-machine install could die at "Creating shared PostgreSQL database" with
psql: connection to server on socket … failed, seconds after the readiness probe printedready!. The pgvector image's entrypoint runsinitdbagainst a temporary server and stops it before starting the real one; a singlepg_isreadyover the unix socket is answered by that temporary server, so the probe could succeed inside the window and the next command would find the socket gone. A warm volume skipsinitdbentirely, which is why only a genuinely clean machine ever hit it. The probe now runs over TCP (the init server doesn't listen on TCP at all) and requires three consecutive successes, resetting if the server stops answering. - A clean tree no longer aborts the release — the private-data guard read
git grep's status with a bareout=$(…); rc=$?. The release script runs underset -Eeuo pipefailwith an ERR trap that exits, and-Epropagates that trap into the command substitution — so rc=1, which is the guard's clean "no matches" case, killed the subshell before the status was ever classified, and a spotless tree surfaced as "the denylist could not load". The guard was inverted: the cleaner the tree, the more reliably the release aborted. The status is now read in a condition context, where neither errexit nor the trap sees it. The existing suite ran under plainset -u, which is exactly why it stayed green while the release failed; new cases re-run the guard in a child shell wearing the release script's own flags, and pin that the trap must keep reaching subshells (bash does not fire a parent's ERR trap for a failing( … )block, and every build step is one). - Private data no longer leaks into the system prompt — browser-login domains and the home directory are kept out of the chat system prompt, home-rooted paths are abbreviated in LLM-visible tool output and in coding-agent folder-resolution errors, and the release guard's private-data denylist moved out of tracked source (fails closed on an unterminated block or a grep error, and keeps git's stderr out of the hit list). The tracked heuristic is now shape-only and names nobody: contributor names moved into a separate exceptions list that denies each name outright and enumerates its legitimate attribution sites — strictly stronger than the old pattern, which had been walking straight past a bare personal GitHub org in CI config.
- Compose drafts — a draft whose text was already submitted is dropped rather than resurrected; an answer never consumes an image-bearing draft; a draft the server still holds is never superseded; only a server report proves what the server holds; the pending flag is held until the last write settles.
- Agent vision — explicitly-requested and injected images stay in the model's vision instead of being stripped by the context trimmer, with a bound on how many stay pinned.
- Background-task drain internals — closes the lost-wakeup window in the
bash_outputinjection wake, makes the finish wake durable for every concurrent waiter, reserves the pending-injection count before sending, scopes the injection-drain decrement to its own registration, reports the real wait when a task is evicted mid-drain, and accounts for buffer-cap loss in the truncation marker. - Version status — an older on-disk binary (engine or gateway) is no longer read as a new version; an abbreviated same-commit id counts as not-older; the self-heal give-up is announced once instead of every tick; the frontend announces a new version only when one exists.
- Changes / Apply — the engine rebuild starts on every apply path, never fires for a non-Lucidos-source change, and the post-apply refresh runs only for the accepted
ChangeApplied(not a suppressed duplicate), with the decision logged. - Thread type survives a continuation —
ContinuationStartedno longer relabels a chat thread as a coding-agent thread, threads already flipped are repaired on boot, and the channel gate is documented. A resumed trigger thread now continues on its own channel instead of having its source rewritten tochat, resolved through a shared channel decoder that also accepts the legacy alias. - Question-parked threads survive restart — preserved across every abort path, resumed on their originating channel when answered, with no stale restart reminder posted on resume.
- Question dividers — an answered question divider re-anchors below a child-completion card, and only an unresolved divider is exempted from the child-completion redirect.
- Orphaned Thinking marker — a Thinking marker left stranded when a child-thread completion takes the turn is now dropped, with the Thinking-only finalize folded into the shared pending-step resolution rather than living as a special case.
- Notifications — the app-icon badge is re-asserted rather than diffed; the badge and unread list are single-sourced; the unread set reconciles after a cold-start mark-read; toasts stop gluing the notification title onto a structured body's first line.
- Credentialed proxy —
Sec-Fetch-Siteis authoritative in the guard (superseding the revertedx-forwarded-hostreconstruction), and the gateway strips inboundx-forwarded-host. - Email — network phases are bounded (IMAP 120s ops, SMTP 80s send under a 120s client budget) so a send surfaces the real error instead of a 10s client timeout; duplicate sends are guarded; the confirm modal remounts when a different draft replaces the open form.
- Stop & queued messages — Stop returns queued messages to compose instead of re-running them, awaits in-flight queued-message removal (trash-then-Stop race), and excludes mid-trash messages from the queue clear.
- Mobile / iOS — welcome suggestions stay tappable while composing, the wider header reserve is scoped to the brand row,
viewportIsMobileself-corrects so an iOS PWA can't strand the desktop layout, and a stale entry bundle no longer wedges boot. - Coding-agent recovery — conflict resolution survives a stray-kill auto-recovery with a session-branch-keyed hand-off; an app coding-agent resume reuses its surviving branch instead of failing on
-b; the restart auto-resume race is closed by subscribing before backfill, with an orphaned-continuation startup safety net. - Boot — the embedding model loads in the background and never blocks boot, with the memory rebuild and extraction guarded against the empty-embedder window.
- Todo panel — strikethrough applies to the item text rather than the row chrome, and abandoned items aren't struck through at all.
- SDK bundle resolves from the checkout instead of a fixed hop count above the binary.
- LLM serialization — tool results are ordered before user text in the OpenAI-compatible serializer; Gemini grounding is pinned to the global endpoint.
- Intent sub-loops surface their narrative text to the parent thread.
- Security & dependencies —
serde_with3.17.0 → 3.21.0 (GHSA-7gcf-g7xr-8hxj) and an npm audit fix for the postcss path-traversal advisory (GHSA-r28c-9q8g-f849). - CI & tests — repaired the docs strict build and the Linux/aarch64-darwin release-tarball jobs; guaranteed the gitignored
VERSIONon allbuild.rspaths; the e2e database is rebuilt from zero on every run; 19 clippy lints cleared so the warnings-as-errors gate passes; build-lock tests no longer flake on a forked child's inherited fd; the channel decoder is pinned to its serde representation; every project's failure artifacts are kept for the whole browser run; the e2e worktree prune stays reachable on every cleanup path and the pre-kill hook reports its real exit code. The release scripts gained 66 new offline assertions covering the notarize resume handle and the credential-resolution path, plus 45 more for the compiled-input fingerprint gate, the re-fold reuse decision, and the staple-time DMG hash pin.
Removed
- The card-less chat-redesign sandbox page (
chat-redesign.html). - Dead
--initiator-*theme tokens.