Skip to content

AgentOS 2026.8.5

Choose a tag to compare

@github-actions github-actions released this 04 Aug 17:52
· 574 commits to main since this release
f759dd9

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 originSessionKey onto every reminder job while forcing its target to isolated, 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 called append_message, which raises KeyError: Session not found. That surfaced as forward_failed, and because best_effort defaults to off — and its checkbox only renders for channel and webhook delivery, never for the none mode 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 distinct origin_gone delivery status. Both webchat paths are covered — the none-mode mirror, and the mode=ORIGIN + channel=webchat config 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_json with {"profile": "default"} — no such profile exists; _TOOL_PROFILES holds coding, full, memory_only, messaging and minimal — but nothing checked the name until profile_allowlist resolved 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_profile now canonicalises the name or raises listing the ones that exist, and SchedulerOps plus the cron RPC call it before storing, mirroring how elevated is already handled. The read direction is deliberately left tolerant — rows carrying a bad profile already exist, and cron list has 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:59f2 has 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 (the MotionListItem grid item, the <dl> row, and the <dd> flex item), so the card grew past its 1fr track and spilled over the neighbouring column; the overflow: hidden on the card clipped inside the card, but the card box itself had already been widened. All three get min-width: 0 and the value truncates with an ellipsis, keeping the full string in the DOM and on title so 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 into lib/clipboard.ts, so both screens share one implementation.

  • Use and Use in chat buttons on the Skills page. Pinning a run to a skill means starting the prompt with use 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 a Use button next to View details, and the detail dialog gains Use 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 and persistSession strips, 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-manager can run an unattended take-profit ratchet on a one-sided Uniswap v4 position. ratchet.py arms 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 single modifyLiquidities — 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: MandateAuthorization is keyword-only, is never passed by main(), is isinstance-checked, and refuses to construct at all once _ARGV_ENTRY is 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/unilp as a write-ahead log plus a materialized view, fsynced and 0600, with flock on a separate file so os.replace cannot 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 afterRemoveLiquidity or afterAddLiquidity in 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-manager allows one live mandate per position. mandate_id hashes label along 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 existing store.exists() check cannot see that, since the ids differ by construction, so uniqueness is now a scan. Identical terms make arm idempotent (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 excludes originalPrincipalRaw, which is a measurement that moves with the price rather than a term.
  • lp_read.py can answer "which wallet is mine" without becoming able to sign. secp256k1.py is split: account.py holds the curve arithmetic and address derivation and imports nothing that can sign, while secp256k1.py imports it and layers RFC 6979 on top. The dependency runs one way only, so positions with no --owner now resolves the wallet from UNIV4_LP_PRIVATE_KEY while remaining unable to reach a signature — reintroducing signing into account.py is a circular import, not a review comment. lp_write.py address prints the derived address and runs before any chain or RPC client is built.

Full changelog: v2026.8.3...v2026.8.5