Skip to content

AgentOS 2026.8.6

Choose a tag to compare

@andreapn andreapn released this 06 Aug 05:15
· 550 commits to main since this release
20e1cf7

A cron release. Schedules can run scripts now — without paying for a model turn on every tick — and the surface that tells you what a job actually did was rebuilt around that, because a script job is the shape that exposed how much of it was broken.

Added

  • A cron job can run a script instead of a model turn. Every job that could do something went through the model chain, so a deterministic task — poll an endpoint, check a threshold, diff a feed — paid for a full turn on every tick. A 5-minute job is 288 turns a day to run a script that prints one line when something is wrong. Two shapes, one idea: let the script decide whether anything is worth reporting.

    • --job-kind script — the script is the job. Its stdout is delivered verbatim, empty stdout is a silent tick, and a non-zero exit delivers the error and fails the job, so a broken watchdog cannot be mistaken for a quiet one. make_script_run_handler takes a delivery chain and nothing else, so there is no argument or payload that can route it into a provider call.
    • --job-kind agent_turn --script — the script runs first as a collector, and its stdout is prepended to the prompt as a ## Script output block. A tick where it prints nothing, or ends with {"wakeAgent": false}, skips the turn entirely — before the session is touched, so a gated tick leaves no session row, no transcript line, and no model call behind.

    Scripts resolve inside ~/.agentos/scripts/; absolute paths, ~, .., and symlinks out of it are refused. Arguments are exec'd as argv and never handed to a shell. Scheduling one requires an interactive CLI or Web caller — the in-agent cron tool refuses it from a chat channel, matching the existing gate on tool_policy.elevated. (Refs #219)

  • New bundled skill cron-watchers. A script job is only as useful as the script, and the three everyone writes first are the same three: an RSS/Atom feed, a JSON endpoint, and a GitHub repo. Each ships as a small standalone script following the contract the scheduler expects — print what is new, print nothing when nothing is new, exit non-zero on a real failure. Deduplication state lives in ~/.agentos/state/cron-watchers/<name>.json, outside the scripts directory, so that directory stays read-only in practice. The first run reports nothing by default: a watcher that has never run cannot tell which of the thirty items on the page are new. (Refs #219)

  • Read a run's output in full. agentos cron output <job-id> [--run <run-id>] and the cron.runOutput RPC. The in-agent cron tool gains action="runs", bounded so one answer cannot eat the context window — "what did the watcher report?" now has an answer the model can look up rather than one it can invent.

  • senior-unilp-manager can open the pool a position lives in. lp_write.py create-pool initializes hook-less Uniswap v4 pools. initializePool is a plain PositionManager function, not a modifyLiquidities action, and the transaction carries no unlockData, no deadline and no token movement — so nothing about the plan/simulate/confirm/send path moves. Guards, in the order a run meets them: hooks is pinned to address(0) and there is no flag to change it; a dynamic fee is refused; fee and tickSpacing are range-checked locally so the failure is a sentence rather than an unnamed custom-error selector; a fee/tick-spacing pair outside the vanilla tiers needs --allow-odd-tier, because pools only searches those tiers and the pool would otherwise be invisible to discovery; an already-initialized pool prints its poolId and current tick and exits without planning; and revalidate re-reads slot0 just before sending. The starting price is the one parameter that can never be corrected, so the table prints tick, sqrtPriceX96, and the price in both directions under a banner saying so. selftest goes 695 → 716 assertions.

  • tick --json --alert-only lets a ratchet cron stay quiet. A ratchet tick wired as a script job delivered the full result of every mandate on every run, so a healthy ratchet posted a block of JSON saying nothing happened, every ten minutes, until nobody read it any more. A tick that found nothing still prints the whole payload — the run history keeps it, so a quiet tick stays auditable — but ends on {"wakeAgent": false}, which has_actionable_output() already reads as "no news": the run succeeds and nothing is delivered. A tick that fired, adopted a landed fire, halted, was rejected, expired, or built a plan on a dry run is delivered as usual. NEEDS_ATTENTION is tested apart from the action and deliberately alerts on every tick — it is a terminal state, so a halted mandate reports the same plain noop as a healthy one while it waits for a person, and filtering on the action alone would silence the one alarm that must never go quiet. The flag is opt-in and only touches --json. (Closes #234)

  • lp_read.py price --tokens is documented (SKILL.md §6b), with the rule to use it rather than deriving a price from a pool. Its absence was not theoretical: a session derived a token's price from a zero-TVL dust pool, was off by 3×, and that number would have become the permanent starting tick of a new pool. (Refs #228)

Fixed

  • A bad cron delivery target is rejected when you save it, not ten minutes later. A script job was repointed at Telegram through the Web UI; the value to hand was the session key agent:main:telegram:direct:…, the Recipient field was free text, and nothing objected. validate_channel_target now refuses an id beginning with a session-key prefix (agent:, cron:, webchat:, session:) for any channel, and requires an integer or @username for Telegram — a rejection suggests the id that would have worked. The guard is a prefix whitelist, not a colon count, because a real MS Teams conversation id contains colons. cron.add and cron.update then ask the adapter itself via TelegramChannel.probe_target; only a definite "no" blocks the save, since a missing capability, a timeout, or a transport error is not evidence about the chat id.

  • The chats we already know are offered. New channels.deliveryTargets RPC lists each Telegram channel's paired DMs and configured group chats, and the Web UI renders Recipient as a dropdown when the channel has known targets, with Enter manually… for a group chat outside group_chat_ids. Channels with no pairing store keep the text input.

  • The reason a delivery failed is on the run record. DeliveryReport.channel_detail turns one line of "delivery failed" into "Cron job 'x' delivery to telegram failed: Bad Request: chat not found" in agentos cron runs. An exception escaping the channel leg reports its type instead of vanishing into asyncio.gather.

  • structlog events reach ~/.agentos/logs/debug.log. Half this codebase logs through logging.getLogger and half through structlog, and only the first half reached the file — the missing half included every delivery.* warning, which is why the reason above existed only in the terminal the gateway was started in. The gateway tees structlog events into stdlib logging while file logging is on, and restores the previous processor chain when it is turned off.

  • A run's output is stored whole. clamp_run_output replaces the scattered [:500] slices that truncated a script job's stdout on the way into the database; preview_summary is what the delivery layer and the run list get. The Web UI's expanded row fetches the full text lazily and falls back to the preview when the fetch fails, with a min(60vh, 32rem) panel instead of 18rem.

  • A script job's output no longer vanishes. Several skips in the delivery chain encode "the run already wrote this into the session, so mirroring it would duplicate it". That holds for an agent turn, which appends its own reply to the transcript it ran in. A script job has no turn: nothing writes its stdout anywhere unless the chain does. A job with sessionTarget=current from webchat was reported "delivered" without a single byte being written, and a job bound to a chat whose run is that chat was skipped on origin == session_key — both ran green and left the operator with an empty conversation. A cron add --script from the CLI genuinely has nowhere to write, and now reports no_session_target rather than a bare skipped, so an empty chat is legible instead of looking like a quiet run.

  • The "→ Chat" button no longer leads to "Could not load chat history." Each run reports chatAvailable and the button is hidden when it is false — script jobs never create a session, and isolated agent sessions are reaped after 24h, so the key alone was never proof a chat exists. chat.history answers an empty transcript for a missing cron session instead of raising.

  • The session reaper is paged. list_sessions() returns the 100 most recently updated sessions — precisely the ones that are not expired — so expired isolated cron sessions were never reaped on a busy store.

  • A cron tool refusal reaches the model. Cron raises plain ToolError in ~30 places with field-naming messages that envelope.py discarded, because it exposes a message only for SafeToolUserMessage subclasses. A call carrying tool_policy.elevated on a script job came back as "received an invalid argument", and the seven retries after it — which dropped the required schedule field — all came back as "failed with an internal error". Nothing was scheduled and no log line recorded why. Cron's refusals are SafeToolError now (scoped to cron; the ~230 other sites have not been audited for what they contain), and job_kind='script' + tool_policy.elevated is rejected up front, naming the field and pointing at agent_turn. (Refs #228)

  • A quoted script path is unwrapped before it is stored. Asking an agent in the Web chat to schedule a script job gets you script='"watch-memory.sh"' often enough that it is the first thing that happens — the job saved cleanly, then failed on its first tick against a path with quote characters in the middle of it. A path whose first and last characters are the same quote is never a real file name. (Refs #219)

  • /reset clears the visible conversation on web and CLI. sessions.reset keeps the session key and only rotates session_id, so chat.history returns empty afterwards — but both clients left the conversation on screen, which reads as "nothing happened". The Web UI clears on session.epoch_changed, which is broadcast from (and only from) the reset path, so one hook covers the typed /reset, the slash menu, the SessionChip button, and a reset issued by another connected client. The CLI gains ChatApplication.clear_screen(); native scrollback writes \x1b[3J too, so the cleared turns cannot be scrolled back to.

  • Two senior-unilp-manager doc commands were unrunnable. python3 <S>/ratchet.py reads as a redirect from a file named S in a shell, and the cron examples cannot use $S at all, since a cron job runs in a fresh isolated session or as a bare file. Both now spell out {baseDir}/scripts/ratchet.py. (Refs #228)

Changed

  • The cron surfaces say "no LLM" instead of "no model", and agentos cron runs grows Delivery and Output columns.

Full changelog: v2026.8.5...v2026.8.6