AgentOS 2026.8.5
A cron-reliability and web-UI release. If you created a reminder from the web UI and it has been failing every single run, that is fixed here — and the cause was a session key stamped onto jobs that never asked for one.
Fixed
- A vanished origin chat no longer fails every cron run. The web UI stamps
originSessionKeyonto every reminder job while forcing its target toisolated, and reminder is the default payload kind for a new job — so jobs that never asked to be bound to a session carried one anyway. "New Chat" makes this routine rather than rare: it mints a session key client-side with no RPC and no row, so a reminder created before the first message in that chat points at a session that has never existed. At fire time the delivery chain mirrors the result into that session, and the mirror calledappend_message, which raisesKeyError: Session not found. That surfaced asforward_failed, and becausebest_effortdefaults to off — and its checkbox only renders for channel and webhook delivery, never for thenonemode this path runs under — the run was marked failed with no way to opt out. The mirror is opportunistic now: the run already succeeded in its own isolated session, so a missing origin is "nothing to mirror", not a failure, and it reports a distinctorigin_gonedelivery status. Both webchat paths are covered — thenone-mode mirror, and themode=ORIGIN+channel=webchatconfig the cron tool synthesises from the live ToolContext when the agent schedules something mid-conversation. Genuine channel delivery failures still fail the run, and channel-created jobs (telegram, discord, slack) were never affected: their delivery resolves to the chat, which outlives any session. - An unknown cron tool profile is rejected at write time instead of dying on every firing. A job stored
tool_policy_jsonwith{"profile": "default"}— no such profile exists;_TOOL_PROFILESholds coding, full, memory_only, messaging and minimal — but nothing checked the name untilprofile_allowlistresolved it inside the run. The job stored cleanly and then died ~50 ms in, before the agent turn started, three consecutive times, after which the scheduler auto-paused it; the only trace was a run record the operator had no reason to look at.normalize_tool_profilenow canonicalises the name or raises listing the ones that exist, andSchedulerOpsplus thecronRPC call it before storing, mirroring howelevatedis already handled. The read direction is deliberately left tolerant — rows carrying a bad profile already exist, andcron listhas to render them or the broken job could not be found and deleted. The tool schema now names the valid profiles too; the model that created this job had no way to know them, which is why it guessed. - Cron cards stay inside their grid track. A session key like
agent:main:telegram:direct:1245463966:new:59f2has no break opportunity — a colon is not one, per UAX-14 — so its min-content width equals its full rendered width. Three boxes between the grid track and the text were unable to shrink (theMotionListItemgrid item, the<dl>row, and the<dd>flex item), so the card grew past its1frtrack and spilled over the neighbouring column; theoverflow: hiddenon the card clipped inside the card, but the card box itself had already been widened. All three getmin-width: 0and the value truncates with an ellipsis, keeping the full string in the DOM and ontitleso it stays hoverable and greppable. The prompt row wraps instead (overflow-wrap: anywhere) so an unbroken contract address does not widen the card either.
Added
-
A cron job's ID is visible in the web UI. It was CLI-only despite being the handle every
agentos cron …command takes. It renders as the first meta row on each card, shortened to head+tail, with a copy button for the full UUID. The clipboard helper with the non-secure-context fallback moved out of SessionsPage intolib/clipboard.ts, so both screens share one implementation. -
UseandUse in chatbuttons on the Skills page. Pinning a run to a skill means starting the prompt withuse skill <name>on its own line, which you had to remember and retype by hand while the Skills screen was already showing the exact names. Each installed skill card now carries aUsebutton next toView details, and the detail dialog gainsUse in chat; both navigate to Chat with the composer pre-filled and focused with the caret at the end. Nothing is sent and the current chat session is kept — you write the request underneath and submit it. The prefill travels as a one-shot?prompt=query param that ChatPage reads once on mount andpersistSessionstrips, so a reload or a shared link does not re-inject the text; C0 control characters other than newline are dropped and the value is truncated at 2000 characters. -
senior-unilp-managercan run an unattended take-profit ratchet on a one-sided Uniswap v4 position.ratchet.pyarms a mandate that, at fixed milestones measured against the original principal, exits the position, keeps the converted side as realized profit, and redeploys only the unconverted remainder into a narrower range running from the current price to the original far edge. A fire is a singlemodifyLiquidities— DECREASE → BURN → MINT → TAKE_PAIR, with no SETTLE leg, because the mint always redeploys strictly less of the principal than the decrease just credited and zero of the other currency. Two failure modes disappear as a result rather than being handled: Permit2 is never involved, and there is no window holding loose tokens and no position. The burned NFT is then a boolean witness that the fire landed, which is what makes unattended recovery a lookup instead of a guess. Authorization does not go through--confirm:MandateAuthorizationis keyword-only, is never passed bymain(), isisinstance-checked, and refuses to construct at all once_ARGV_ENTRYis set — so the CLI cannot build one no matter what it is handed. Each fire is re-checked against the pinned chainId, PositionManager, poolId (which is keccak(PoolKey), so the hook is pinned with it), tokenId, signer, recipient, slippage floors, milestone index, the fixed far edge, a re-derived near edge, and a zero cap on the harvested currency. State lives outside the price cache under$AGENTOS_HOME/state/unilpas a write-ahead log plus a materialized view, fsynced and 0600, withflockon a separate file soos.replacecannot swap the inode out from under the lock.Not yet rehearsed on chain. The combined unlock has never been sent against a hooked pool, and a hook may take delta in
afterRemoveLiquidityorafterAddLiquidityin a way that makes TAKE_PAIR revert. SKILL.md marks the dust rehearsal — one range above the price and one below — as required before the first broadcast, with the two-transaction fallback documented behind it.
Changed
senior-unilp-managerallows one live mandate per position.mandate_idhasheslabelalong with the terms, so arming the same position twice under two names produced two ids, two files, and two mandates each intending to burn the same NFT — which happened on a live position. The existingstore.exists()check cannot see that, since the ids differ by construction, so uniqueness is now a scan. Identical terms makearmidempotent (print and exit 0) rather than an error, because the way this arose was a setup step run twice; differing terms are refused and name the field. The comparison is against the mandate's current tokenId, since a fire rolls it onto the position it mints, and excludesoriginalPrincipalRaw, which is a measurement that moves with the price rather than a term.lp_read.pycan answer "which wallet is mine" without becoming able to sign.secp256k1.pyis split:account.pyholds the curve arithmetic and address derivation and imports nothing that can sign, whilesecp256k1.pyimports it and layers RFC 6979 on top. The dependency runs one way only, sopositionswith no--ownernow resolves the wallet fromUNIV4_LP_PRIVATE_KEYwhile remaining unable to reach a signature — reintroducing signing intoaccount.pyis a circular import, not a review comment.lp_write.py addressprints the derived address and runs before any chain or RPC client is built.
Full changelog: v2026.8.3...v2026.8.5