Repository navigation
cortex-agent server v2026.9.15
·
2055 commits
to main
since this release
Highlights
- The app installs its own updates, and one click updates both halves. The shell applies an update when it quits, chosen from where the build actually lives rather than from the host distro, and the server's npm prompt — now a dialog, not a chat message — is the only confirmation in the whole system.
- A commission can be started mid-conversation, by either side. The question moved off turn one: the agent can call
cortex_commission_start, or the user can flip the composer's capsule, and a session can be added to a commission from the row that already names it. - Session identity has one owner.
sessions.jsonandconversation-ledger.jsonfold into the registry journal behind a versioned migration, so bindings, headers and turns cannot drift apart any more. - A dead tunnel stops stranding a device. A reverse tunnel whose link dies without a clean close held its remote port for the full TCP retransmission timeout — ~15 minutes of a device being unreachable while everything reported healthy. Both POSIX and Windows now reclaim the port.
- PI can see the pictures its tools return. Every image an MCP tool handed a PI session was collapsed to a sentence saying an image existed, with the image thrown away.
- The phone composer fits on a phone again. The toolbar wanted ~670px of screen and was truncating the engine chip to make room; it needs 388px now.
Updates
- Made the app shell install updates by itself, by default, when it quits. The mechanism comes from the install site, never from the OS alone:
Autoreplaces the running copy unattended (NSIS/S /UPDATE /NS, an in-place.appbundle swap, an AppImage self-swap),Elevateddrives apt/dnf/zypper behind one polkit dialog on user-present paths only, andAssistedcopies the file into Downloads and opens it as before. Applying at exit means nothing is interrupted and the new version is there at the next launch. Three consecutive failures flip the plan back toprompt, so a permanently broken site cannot retry quietly forever. - Fixed the updater deciding what kind of install this is by reading
/etc/os-release. A machine's package format says nothing about how this copy was installed, so an extracted AppImage or a self-built binary on a Debian box was reported as adebinstall and the assisted flow produced a second installation beside the one actually running. The answer now comes from where the process lives —$APPIMAGE,dpkg -S/rpm -qfownership, the NSIS uninstall registry entry, the.appbundle layout. - Made the server's npm self-update the single confirmation for both halves. It becomes a dialog in the SPA, and the shell's manifest is capped at the server's version, so the app cannot take a release the server has not taken; a shell re-check now fires on the
restarting → settlededge instead of leaving the app up to a day behind its server. After ten unanswered minutes the same question falls through to the old chat prompt, so Slack, Feishu and headless installs are not stranded behind a dialog nobody has open. First answer wins. - Added a settings card to turn silent updating off, next to the sign-out card in Advanced. It adopts the prefs the shell returns rather than the value that was clicked, and when the shell has already given up after three failures it says so, with a retry that clears the counter.
- Made a server running from a source checkout stop advertising app releases at all (
CORTEX_REPO), matching the npm self-update path: with silent updating on, a dev server must not be able to push a shell onto a connected app. - Added the Android silent-install path — a
PackageInstallersession withsetRequireUserAction(USER_ACTION_NOT_REQUIRED), committed from the background once the last activity stops, degrading to the old confirmed install on every failure path. No Android APK ships with this release (see Downloads), and this code has never been built; the first silent Android update would in any case be the one after the build that declaresUPDATE_PACKAGES_WITHOUT_USER_ACTION.
Commissions
- Made entering commission mode a mid-session transition either side can make. Whether a conversation deserves a contract is usually only visible a few turns in, and the answer was being demanded before turn one. The agent calls
cortex_commission_start— which now does the work (draft directory plus registry flag, idempotent) instead of printing a path someone else had created — or the user flips the composer's commission capsule. Both funnel through one entry point, so the two cannot drift into different states. Leaving reclaims a draft directory nothing was written into; a draft holding text stays on disk. - Fixed commission mode being enforced by which tools a spawn listed, which never worked:
--toolsfilters Claude's built-in set only, so the submit tool was callable from every direct session — which is how one commission came to be created from a session that had never entered the mode. PI could express the exclusion and Claude could not, so the same feature meant two different things per backend. The exclusion is gone on both sides and replaced by a state check where the effect actually is: a submit is refused from a session that is not drafting, one already bound, or one pointing at another session's draft directory. - Fixed the
[Commission]block only ever being delivered on a session's first turn, so a session that entered the mode mid-conversation would never see it. Delivery follows the binding, re-delivers when the binding changes, and is cleared by a real compaction. - Added a one-click way to put another session on a commission, from the two surfaces that already name one: the rail row's hover + and a button on the board beside Complete / Abandon. Only an active commission offers it. The draft's choice also stops silently outliving its draft — every explicit selection disarms it, so a later conversation cannot land on a contract nobody picked for it.
- Fixed the draft composer calling a chosen commission "unnamed" — a label that belongs to a contract not yet named — because it kept only the id and the menu's titles were gone once it closed. The mobile capsule was showing the bare id.
- Tightened the
cortex-commissionskill to load only once a contract has landed; the creation protocol lives in the start tool's return value, so loading the skill during the drill put the wrong half of the procedure in front of the model. settings.commissionEnablednow defaults to on. It stays a switch because it is the kill switch, but a default of off would hide a capability nobody has to opt into any more.
Sessions
- Folded
sessions.jsonandconversation-ledger.jsoninto the session registry journal, which is now the single owner of records, bindings, conversation headers and turns. The journal growspatch/bind/unbind/turnevents beside the existing three; a context-usage tick is one ≤200-byte line instead of a ~736-byte whole-record write. The old repos keep their APIs as façades, so no caller changed. - Added the migration that does it: a versioned step that runs after the file loop, imports bindings, headers and ordered turns in one batch, then renames the sources to
*.pre-2026.9.14.bak. It is idempotent via the rename, crash-safe, and a corrupt source is warned about and left on disk rather than dropped. Dry-run on a copy of real data: 79 bindings, 79 conversations, 283 turns, read back field by field. - Moved the use-count lease out of the store into
domain/sessions/session-use.ts, leaving the store one storage primitive, and deleted the dead store methods it was dragging store→store imports in for. - Fixed
markCommissionBlockDeliveredwriting no event at all, which would have re-delivered the[Commission]block after every restart: the journal's field table is exhaustiveness-checked, but the repo's writer list was a hand-typed array that could silently disagree with it. It is now derived from the reader table, so the two cannot drift. - Pinned the observable behaviour of all three stores in 16 golden characterization tests, built from public read APIs only, before any of this moved.
Remote devices
- Fixed a POSIX reverse tunnel's remote port staying held after the link died in a black hole. The remote sshd blocks writing the session's exit status into nothing, so the forward listener survives a full TCP retransmission timeout (~15 min) and every
ssh -Rin between dies onExitOnForwardFailure— while client-manager still sees a live pid and never restarts anything. On Linux sshd is non-dumpable, sofuser,lsofandss -pall decline to name the owner and the listener has to be found some other way: the session now carries a greppable marker child in place of-N, and a stale tunnel is evicted by killing that marker's parent. The marker watches its parent by pid, so it cannot be reparented to init and leak one process per restart. Verified against a real device with the black hole reproduced on demand. - Fixed the Windows eviction clearing only one listener when several had piled up on a port. It was built on
Get-NetTCPConnection, which reports one row per endpoint however many sockets are bound to it —netstat -anoshowed two where the cmdlet showed one — and the row it returns is the oldest binder, so a run could leave the zombie and kill the live tunnel. It loops now until no sshd holds the port, and the kill is gated on the owner actually being sshd: the old command force-stopped an unrelated listener on a real machine, the new one leaves it alone.
Models and chat
- Fixed a PI session being blind to every image an MCP tool returned.
mapMcpContentcollapsed an image to[Image: mimeType=image/jpeg, base64(56936 chars)]— a sentence saying a picture exists, with the picture thrown away — although PI tool results carry image blocks natively and pi-ai already downgrades them itself when the model has no vision. Image items and image-mime resource blobs now pass through as image blocks (png/jpeg/gif/webp, mime normalized first); an unsupported type, empty data or a non-image blob keeps the old description so nothing is dropped silently. Verified downstream: the same result becomes a realinput_imagefor a vision model and an explicit omission note for a blind one, with no base64 leaking into either request. - Fixed the turn's copy button sitting mid-answer when a running turn's last row was a hint line — a background agent reporting back, a task callback. A system-origin row may now be the row that carries the turn's key; it still offers nothing of its own, and the button it holds copies the assistant's reply.
- Fixed the mobile subagent card having no visible frame in light mode. It was drawn with the desktop tokens, which read on a white chat surface but land 0.015 and 0.006 off the mobile canvas —
#eff1f5on#f5f6f8. It now uses the mobile palette every other mobile card already uses; light-mode contrast goes from ΔL 0.015 to 0.048 and dark mode gets a touch stronger rather than weaker. - Fixed the phone composer toolbar being asked to hold more than it could seat. Browser and commission report as bare 34px icon keys with the name moved into the label, tooltip and menu; the engine chip drops its leading dot and separator in favour of colour, and sheds the thinking level 12× faster than the model name; context usage moves out of the composer and into the chat header, where it belongs — it reports on the session, like the session id and stats beside it.
- Shortened the engine picker's held-back footers, which were full sentences long enough to wrap in the desktop menu and the mobile sheet for what is a footnote. Both languages, desktop and mobile.
- Fixed the recent-notifications card being visibly narrower than the two cards above it in the same panel.
- Fixed a rail row growing 4px when its + appeared on hover, shoving every row below it down the moment the pointer crossed one.
Downloads
Desktop app, built from this tag by CI:
| file | SHA-256 | |
|---|---|---|
| Windows x86_64 | Cortex-2026.9.15-Windows-x86_64-setup.exe |
2c0206812376211c88dc0d63a8ae8a6255e3a54574daa7549bd705fbd6f70d47 |
| macOS universal | Cortex-2026.9.15-macOS-universal.dmg |
c53f503546b3bf654a978a0d0626371e53ff353de9ffee07eab26621e65978fa |
| Linux .deb | Cortex-2026.9.15-Linux-amd64.deb |
7e8d7175c3a5f428ffc03687241af8e7cf04b9ed794fb791a5af315de9581b6d |
| Linux AppImage | Cortex-2026.9.15-Linux-x86_64.AppImage |
75cbc469d45f5247332a1798e27e38c47b13588226efacd5bd4d50ddbb2599a2 |
| Linux .rpm | Cortex-2026.9.15-Linux-x86_64.rpm |
7a012131c94a9f8217abac57fdac7a9e0d6e5365e7424d429d16e4380fa8afe0 |
The Linux bundles are built on Ubuntu 22.04 and need WebKitGTK 4.1 — the .deb declares
libwebkit2gtk-4.1-0, and the AppImage's bundled WebKit needs FreeType 2.11 or newer.
Ubuntu 20.04 and equivalents are below that floor.
No Android APK is attached: the release keystore is not configured on this repo, so the
Android job is skipped rather than producing a differently-signed APK that existing
installs could not upgrade over.