Skip to content

Releases: theselfruleorg/polyrob

POLYROB v1.0.2

Choose a tag to compare

@themontreal themontreal released this 17 Sep 19:02
183042e

[1.0.2] — 2026-09-18

Added

  • X OAuth 2.0 user token: encrypted store + auto-refresh (tools/x_oauth2.py).
    The X Chat DM read (where every inbound DM now lands) needs a user-context
    OAuth2 token that X expires two hours after mint; the tree read ONE static
    env value and never refreshed it, so a hand-minted token proved the rail once
    and then inbound went dark. Every consumer (surfaces/x/client.py,
    tools/twitter_tool.py, the polyrob x/gateway presence checks, the CLI
    twitter gate) now resolves through resolve_access_token: store → refresh
    within 5 min of expiry (refresh token ROTATED and persisted before the old one
    is dropped; a failed refresh keeps the old pair) → env seed
    (TWITTER_OAUTH2_ACCESS_TOKEN + new TWITTER_OAUTH2_REFRESH_TOKEN, stored
    once) → static env override. The DM client retries a 401 exactly once on a
    refreshed token; the twitter tool rebuilds its DM + Chat clients before every
    DM read/send. polyrob x-account oauth-login (PKCE with a local callback),
    oauth-import (hidden prompts), oauth-status, oauth-refresh. New flags
    TWITTER_OAUTH2_CLIENT_ID / _CLIENT_SECRET / _REFRESH_TOKEN.

Changed

  • The per-transaction wallet ceiling is the owner's, from chat (owner ruling
    2026-09-18, after the second time an approved raise did nothing).
    budget.wallet_per_tx_usd is now an owner-override preference: an approved
    value replaces the AGENT_WALLET_MAX_PER_TX_USD default in either direction,
    clamped to the daily cap (core/wallet/config.py::effective_max_per_tx_usd).
    The daily cap (budget.wallet_daily_usd over WALLET_DAILY_CAP_USD) stays
    min-merged and env-only — it is the operator's hard envelope, so a single
    transaction can never exceed what a day may lose and no raise made from chat
    moves the maximum daily loss. Prod 2026-09-17: the owner approved
    wallet_per_tx_usd = 220, the pref sat on disk, the guard kept reading the
    env's $120, and the agent asked the owner to edit polyrob.env and restart.

Fixed

  • The wallet caps apply LIVE. PolicyGate copied both caps at construction
    (process start), so budget.wallet_per_tx_usd / budget.wallet_daily_usd
    documented and shown as applies: live — took effect only at the next
    restart, in EITHER direction (an owner tightening from chat was just as
    inert). WalletConfig.cap_resolver (live_caps_resolver) is consulted on
    every check() and by the cap properties; a hand-built config keeps the
    frozen values; a raising resolver keeps what the gate had.

  • The approval pre-hook read the raw env ceiling, not the owner's.
    spend_lane.autonomous_ceiling_usd() read DEFI_AUTONOMOUS_MAX_USD while
    tx_guard step 9 read the pref-resolved budget.defi_autonomous_usd, so the
    two halves of one lane disagreed: the owner approved a $300 autonomous
    ceiling, tx_guard honoured it, and the hook still demanded a tap for every
    live swap over the env's $5 — three taps in one afternoon for trades the
    owner had said may run unattended, and an unattended cron buyback that could
    never run. The hook now delegates to tx_guard.autonomous_max_usd (fail-open
    to the env value, never wider).

  • Approving an ask a CRON run raised re-arms the job instead of waking the
    dead run.
    The ask had no goal to re-arm, so resume-on-grant self-woke the
    finished cron session — a forged turn the money guard refuses — and the agent
    told the owner "trigger it from your seat" for a trade the owner had just
    approved (prod 2026-09-17 16:14). Autonomous sessions now remember their cron
    job (autonomy_marker.cron_job_for_session, threaded through
    run_task_to_outcome(cron_job_id=)), the ask carries cron_job_id, and an
    approval pulls that job's next_run_at to now (core/cron_rearm.py, the one
    UPDATE both CronJobStore.run_now and the owner queue use) so the next tick —
    a genuine cron turn — redeems the grant. No wake. A running job is left alone
    and the owner is told its next run redeems the grant.

  • A guarded proposal that could never take effect is refused, not queued.
    For a min-merged key with the env set, a value above the env value resolved
    to the env value after the tap — the owner saw "tap to approve", tapped, and
    nothing moved. propose_pref_change now refuses up front and names the exact
    env line (WALLET_DAILY_CAP_USD=… in the service env file) that would.

  • FileTokenStore writes follow the shared-data identity convention
    (<writer>:polyrob-data 0660 under a group-writable data dir; an existing
    file keeps its mode + group). It wrote 0600 in the writer's primary group,
    so the OAuth2 pair the root CLI imported was unreadable by polyrob-agent
    — the unit that needed it — and an agent-written refresh would have been
    unreadable by the owner's CLI the same way.

  • FileTokenStore no longer treats an UNREADABLE file as a CORRUPT one. On
    prod the polyrob-email unit (a non-root service identity) got EACCES on
    the root-owned 0600 X token store that polyrob.service had just written,
    and the corruption branch renamed the file aside — deleting the agent's
    freshly imported OAuth2 pair from under the process that owned it. An
    OSError on read now logs and yields an empty in-memory store for THAT
    process only; the file is left where it is.

  • The browser rail under wallet custody (proposal 049). A custody process
    refused local Chromium — correctly — but never reached the isolated browser
    it was told to use: BrowserManager passed the BrowserConfig into
    Browser's BotConfig slot, so BROWSER_CDP_URL/BROWSER_WSS_URL were
    dropped (prod 2026-09-17: the service ran, the env was set, 4,517 refusals in
    24 h, and the agent told the owner "no remote browser configured"). Over CDP,
    every session then attached to the service's default PERSISTENT context — no
    storage_state (the X login silently dropped), no service-worker block, one
    cookie jar shared by every session and tenant. Both fixed: the endpoint
    reaches the browser, and a remote browser always gets a fresh context.

  • Owner-ceremony launches (x-account capture-session/signup, the pfp still)
    now consult the one launch policy: refused under custody, scrubbed env,
    sandbox on. tests/test_browser_launch_ratchet.py pins the launch sites.

Added

  • core/security/browser_rail.py: the ONE answer to "can this process drive a
    browser, and through what" — none (custody) → install, configured, unreachable (<reason>) → check the service, remote cdp ok (<version>).
    Read by the launch refusal, the step loop (which now skips the per-step page
    observation and warns once instead of three ERRORs per step), the
    <tool-catalog> (gated:custody-no-browser for browser/x_browser/
    dapp_browser), the status snapshot's identity section (browser: line;
    WARN only when a configured endpoint fails) and polyrob doctor.
  • polyrob browser install|update|status|render: the isolated browser
    service as a boundary — dedicated UID, Chromium sandbox on via an AppArmor
    userns profile (Ubuntu 24.04 restricts unprivileged user namespaces; the
    earlier unit's --no-sandbox was a wrong diagnosis of that), a UID-keyed nft
    egress chain (no loopback / RFC1918 / link-local / metadata from the browser),
    Chromium for the venv's Playwright pin. deployment/polyrob-browser.service,
    polyrob-browser-egress.service, hardening/polyrob-browser-egress.sh and
    hardening/apparmor/polyrob-browser are the CLI's rendered output, pinned by
    a test. scripts/deploy_prod.sh prints the browser revision beside the pin
    and updates on drift. --mode server --listen <private-ip> on a SECOND host
    writes a Playwright-server unit instead (token in a root-only env file,
    private bind enforced) and prints the agent's BROWSER_WSS_URL — the shape
    that removes the shared kernel. Guide: docs/guide/self-hosting.md.

Fixed

  • The REPL painted 5–10 blank rows above the prompt for every provider error
    and never showed the error text: core/logging.py gave the file handler
    and the console handler ONE ComfyFormatter, whose 0.5 s duplicate filter
    saw the console's copy of each record as a repeat of the file's and
    returned "" — the StreamHandler still wrote the newline, and every such
    write erased and redrew the prompt. Duplicate suppression is now a
    per-handler logging.Filter (a formatter cannot drop a record), and the
    REPL console renders one compact ✗ component: message line per error;
    a re-wrapped exception (one 401 logged five layers of the same text) is
    collapsed to its first line on the console only — bot.log keeps every
    layer.
  • Gemini: a text part was dropped and logged as Gemini function_call missing name at ERROR. A proto Part answers hasattr(part, "function_call") True for every oneof member, so a plain text part hit the
    nameless-call branch and continued — every text-only Gemini reply became
    "Model output has empty action list", the model was pushed into a filler
    step, and the REPL showed a second bubble ("Я жду твоего ответа"). Presence
    is now asked via "function_call" in part.
  • A session the terminal created (REPL, one-shot polyrob run) is a live
    surface: it renders every reply from the feed but bound no router, so
    maybe_deliver_autonomous_send pushed each chat reply through the owner
    delivery rail — dedup, the hourly rate limit and the daily cap included.
    Past the cap send_message told the model its answer was "NOT delivered",
    the model re-sent an apology (another duplicate bubble) and the turn closed
    failed. core.surfaces.binding.bind_terminal_surface marks such a
    session; --resume keeps the durable rail (2026-08-28 incident).
  • The bounded planning turn (ALLOWED_REASONING_TURNS) no longer logs two
    ERROR lines ("violates the agent contract") before the caller grants it.

Changed

  • done(text) no longe...
Read more

POLYROB v1.0.1

Choose a tag to compare

@themontreal themontreal released this 17 Sep 14:15
f08f300

Added

  • self-deploy skill: an agent-facing bootstrap for a FRESH instance —
    assess (model, wallet + funding per chain, email, X, autonomy grants, tool
    catalog), provision what a lever exists for (own inbox, X account, standing
    work), bundle the human-only asks into ONE message (API keys, funding
    addresses read from a tool, env flags, a CAPTCHA), verify by a READ, and
    report one readiness table. Until now polyrob init was the human's wizard
    and the setup interview covered only the owner contract.
  • twitter_poll_results(tweet_id): reads the options, votes, shares,
    voting_status and end time of a poll the agent posted. The tool could post
    a poll (poll_options) but no read asked for attachments.poll_ids, so the
    agent could ask the community and never learn the answer. The
    x-engagement skill now teaches the post → record id → read → aggregate loop
    and pins poll answers as DATA.
  • X message reads distinguish legacy Direct Messages from encrypted X Chat,
    support OAuth 2.0 PKCE user tokens, and can verify/decrypt Chat event history
    with the official Chat XDK and account keys instead of misreporting ciphertext
    or a legacy-only page as an empty inbox.
  • The identity status section now reports the agent's reachable identities:
    email: (the address it sends AS, or none → remedy) and x: (API rail
    configured / PARTIAL with the missing key names / none, plus the handle and
    whether writes are armed). Env PRESENCE only, never a value.
  • A liquidity status section on every seat (status snapshot, polyrob wallet overview, Telegram /wallet overview, console GET /api/webgate/liquidity)
    lists the treasury's Uniswap v3 positions with their fees; on-chain
    enumeration is opt-in and owner-only. /lp is listed in the money verb
    group on every chat surface.
  • launchpad_status names the graduated pool (PoolKey + pool id) once a Pons
    V2 curve has graduated, and states the fee reality: the LP fee is 0, the
    hook collects, creator income is claimed via the escrow, and the launch
    locker position cannot be withdrawn.
  • X_SIGNUP_HANDLE / X_SIGNUP_DISCLOSURE: the @handle x_signup_start
    requests and the automation-disclosure bio it writes.

Changed

  • Post-1.0 alignment sweep: one home per shared rule instead of hand-carried
    copies. core.event_log owns the telemetry db resolution and a fail-open
    emit() (the recap reader used to ignore TELEMETRY_EVENT_LOG_PATH and
    create the db on read); every owner/admin CLI verb resolves its data home
    through cli/_admin_home.py (the deployed-home rule now also covers
    apps, surface, cron, goals, journey, and owner pending's goal
    board, which came from a third resolver); the sub-agent and owner-pause
    refusals every money verb states live in core.wallet.authority; the
    publish/app-service owner-turn clauses in core.security.owner_turn; the
    ship rail's orchestrator/approval/workspace helpers in tools/ship_common.py;
    the goal board's status/kind vocabulary in core.goal_vocab; the /help
    group order is read from core.verbs; the copy-layer engine is shared by
    core.copy and webview.copy; the Telegram update dedup is the core
    IdempotencyStore over its existing table. Twelve open-coded boolean
    truth sets now parse through core.env, pinned shrink-only by
    tests/test_bool_env_parse_ratchet.py.
  • Social-agent skills and toolsets now route public discovery, native X account
    reads, encrypted Chat, browser inbox fallback, and approval-gated writes through
    the capabilities that actually implement each operation.
  • REPL slash commands parse quoted arguments (/steer "two words" is one
    argument); an unbalanced quote is refused with the remedy. Free-text verbs
    (/learn, /persona) keep the raw line. /export names the formats the
    REPL supports and points at polyrob session export for the rest; /logs
    prints the log directory instead of a CLI verb that does not exist.
  • The approval awaiting event names the provider that will decide.
  • lp_collect / lp_remove set their receipt minimum from a simulated
    collect, not the position's fee-growth estimate (core rounding leaves the
    estimate several raw units high, which made a correct collect refuse).
    Every position receipt also asserts the fungible legs. LP legs are no longer
    written into the token-keyed position book: adding them double-counted
    tokens already held, and removing them on withdrawal erased unrelated
    holdings. Position receipts stay in the NFT and transaction telemetry until
    an LP-specific basis exists.

Fixed

  • Scheduled agent runs now propagate wall-clock cancellation and are recorded as
    incomplete unless the agent actually calls done(). The default cron budget is
    ten minutes, preventing slow provider calls from silently turning unfinished
    treasury rails into successful ticks.
  • Provider health distinguishes a sentinel on the serving provider from one on an
    unused fallback. A credit-limited fallback is shown as a warning and explicitly
    does not claim to block the live provider.
  • Live incremental streams claim the turn's reply latch after becoming visible,
    preventing done() bookkeeping from producing a second user-facing response.
  • Browser management accepts operator-configured CDP and WebSocket endpoints, so
    hardened custody deployments can keep Chromium outside the signing service.
  • The owner's spend ceilings are read from the same home the preference
    writers use. tx_guard, the wallet config fallback and the Telegram
    /wallet autonomous verb resolved the preference store through the
    process home (an empty tree under a service account), so an approved
    budget.defi_autonomous_usd was recorded and never read and the guard kept
    refusing at the env default. Those seats may no longer call the process
    home (ratcheted).
  • X self-registration: XPageDriver.set_handle_and_profile was a pass stub
    while the docs claimed the disclosure was written into the bio — no handle
    and no disclosure were ever applied. It now edits /settings/screen_name and
    /settings/profile best-effort and RETURNS what it applied; the signup
    result carries requested_handle/handle_applied/bio_applied and the tool
    says plainly when the disclosure is NOT on the profile yet. The flow refuses
    up front with the remedy when the agent has no email or no inbox client
    (it used to fill an EMPTY email and pause several steps later on "no code
    arrived"), provisions the AgentMail inbox idempotently before opening a
    browser, and the default disclosure names the instance instead of the
    owner's internal tenant id (operated by local).
  • x_browser stays explicit-grant-only: it is high_impact +
    delegate_blocked, so it is not on the CLI optional-registrar table and is
    reached by explicit tool_ids only.
  • A per-profile daemon (polyrob profile create --service) no longer loads
    the primary instance's environment first: any key the profile did not
    override leaked through (its Telegram token → a 409 fight, its TWITTER_*
    keys → the profile posted AS the primary, its owner ids and money flags). A
    profile daemon reads its own environment files only.
  • defi_trade.register_agent / set_agent_uri referenced os.environ with
    no module-level import (latent NameError).
  • The coding and git tools resolve their confined root per TENANT, not the
    anonymous bucket, on a multi-tenant server.
  • The twitter extra now carries chatxdk, which the encrypted X Chat reads
    need; a base install without the extra still imports.

Deployment

  • The signing service owns the wallet state directory while wallet artifacts
    remain group-readable, allowing the durable submission journal to be created on
    the first broadcast without weakening the service-identity boundary.

What's Changed

Full Changelog: v1.0.0...v1.0.1

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 16 Sep 20:38
2171e5d

What's Changed

Full Changelog: v0.13.0...v1.0.0

POLYROB v0.13.0

Choose a tag to compare

@themontreal themontreal released this 08 Sep 10:36
6d920b5

[0.13.0] — 2026-09-08

Two-week release audit — money, autonomy and app-service hardening (2026-09-08)

A full audit of the two weeks since 0.12.0 produced 6 critical and 13 high findings;
every one is fixed with a regression test.

  • Solana swap guard sees the whole transaction (core/wallet/solana_tx_inspect.py): a
    Token-2022 transfer no longer bypasses the balance-delta check, the guard reads the
    full instruction set instead of the first transfer, and it REFUSES what it cannot
    observe rather than passing it.
  • A Solana swap reports whether it actually landed — a submitted-but-unconfirmed
    signature is no longer recorded as a completed trade.
  • A real x402 payment is no longer swallowed by the settlement watcher while one of
    our own swaps moves the same amount through the treasury in the same window.
  • polyrob autonomy pause writes to the resolved data home, not the process CWD —
    the CLI could previously report a verified pause that the running agent never saw.
  • /pause stops the message tool (the dominant autonomous send path) and the
    wallet daily spend cap + replay guard now hold ACROSS processes, not per-process.
  • The cold-start requeue no longer rips a goal from a live claim — the boot sweep
    takes the same CAS guard the dispatcher does.
  • A goal run that produced every deliverable is no longer failed for a missing
    done(), and an unanswered owner approval no longer counts as a cron job's failure.
  • A goal says WHICH declared tool never loaded, and why, instead of failing opaquely.
  • The stream seeder takes a lock so a manual run cannot double-seed a money leg;
    planner stall/backoff bookkeeping is tenant-scoped.
  • The agent recognises a sub-addressed copy of its own address (me+tag@…) as itself,
    closing a self-correspondent binding loop.
  • Sandbox container uid can edit host-written files, not merely enter their directories.
  • The Alchemy key cannot reach the log from the DeFi index provider.
  • App service (032) hardening: approval binds to the approved CONFIG (not just the
    slug), app egress admits only public addresses and is re-asserted every supervisor tick,
    and the tested-tree digest describes exactly the tree that ships.

Telemetry — every external write records its effect (2026-09-08)

  • wallet_spend rows carry the tenant. They were written tenantless, so the unified
    ledger read a confident $0.00 over real spend.
  • Cron delivery goes through the gated twitter / email ACTIONS, not the raw tool
    helpers — so a scheduled post is rate-limited, approval-gated, cooldown-checked and
    recorded like every other send.
  • /pause social actually stops autonomous posting. Seven of the pause scopes had
    zero consumers; the social scope is now enforced at the send path.
  • Shrink-only ratchet (tests/test_external_write_telemetry_ratchet.py) pinning every
    external writer that still records no effect telemetry, so the list can only get smaller.

Refactors (2026-09-08)

Behaviour-preserving extractions that bring three oversized modules back under the size
ratchet: the controller's document-authoring actions, TaskAgent's public session-control
verbs, and the LLM manager's read-only client/model inventory each move to their own mixin.

032 — durable app service (2026-09-07)

An agent-built app now runs as
its own hardened container on the box, survives session end / agent restart / reboot,
and is reachable at https://<slug>.<APP_SERVICE_BASE_DOMAIN>; the owner approves the
address once and /halt stops it. Off by default (APP_SERVICE_ENABLED); ONE bundle
turns it on: AGENT_BUILDER_MODE=ship (+ APP_SERVICE_BASE_DOMAIN).

  • Registry (core/app_service/registry.py, app_services.db): tenant-keyed rows,
    address-sticky approval, CAS claims, caps counters; a cross-tenant slug rejects.
  • Tool app_service (deploy/stop/list_apps/logs): the publish gate shape,
    the 031 pause predicate, workspace confinement, ship==tested, caps, secret-shaped env
    refused. The pending row is the owner ask (the owner-queue provider denies an
    autonomous goal-run turn with no ask — the turn the ship-software stream uses).
  • Supervisor (polyrob apps supervise, deployment/polyrob-apps.service): snapshot
    of the tested tree, per-app bridge + nft egress deny, docker run -d with the ONE
    hardening list (core/container_hardening.py, lifted from the sandbox backend),
    health, a two-substitution nginx stanza, artifacts.url stamped on go-live (021),
    breaker, logs; pause edge tears live containers down and resume redeploys.
  • Owner seats: polyrob apps …, Telegram /apps …, REPL /apps, the console Apps
    page, the status snapshot apps section (pending approvals lead as CRIT).
  • AGENT_BUILDER_MODE=off|build|ship: the fifth named default bundle — build =
    PUBLISH_ENABLED + GITHUB_TOOL_ENABLED and publish in the goal toolset; ship =
    • APP_SERVICE_ENABLED/APP_SERVICE_ALLOW_PUBLIC and app_service in the goal
      toolset; clamps to build without domain + cert; never money/host/secrets.
  • Serving side (owner-run once): scripts/setup_apps_vhost.sh — wildcard cert via
    manual DNS-01, the nginx include dir, the unit. deploy_prod.sh restarts it with the
    family. Guides: docs/guide/deployment-postures.md, docs/guide/owner-controls.md.

Publishing & app-deployment evaluation — Wave 1 (2026-09-06)

A production audit found 336 agent artifacts, 0 with a URL, and the built
publish/hf_deploy/github rails all switched off. Wave 1 stops the ship loop from satisfying itself. Wave 0
(enable the built static rail on the box) is an owner step; Wave 2 is proposal 032.

  • ship-software stream (data/streams/streams.yaml): the grant now carries
    publish, shell, process (the goal body already told the agent to use the process
    tool the grant omitted); the success criterion demands a URL the owner can open — or
    the exact reason it cannot be — and states that a loopback curl alone does not satisfy
    it. The manifest re-syncs prose and criteria on the next hourly seed.
  • Build-command timeouts. The binding 60 s on prod was the controller's per-action
    default cap (shell/code_execution had no row), not the tools' own ceilings.
    TimeoutConfig.TOOL_TIMEOUTS gains shell/code_execution (330 s;
    SHELL_TIMEOUT_SECONDS/CODE_EXEC_TIMEOUT_SECONDS), and ONE foreground ceiling
    SHELL_MAX_TIMEOUT_SEC (default 300, tools/code_exec/limits.py) now governs both
    shell_run (120 when the agent omits timeout) and the dev-mode run_code cap.
  • Unreachable deliverables are named. A completion notice whose deliverables carry
    no published URL ends with one line saying so — naming PUBLISH_ENABLED when the
    rail is not registered (agents/task/goals/deliverables.py::reachability_note).

031 — stop everything by prompting the agent (2026-09-03)

The owner's "stop" (text or
voice, any words) now stops every autonomous activity in seconds, survives restarts, and
every seat reports the verified state; "resume" reverses it.

  • One pause record + one predicate + a ratchet. core/autonomy_control.py owns
    <data>/AUTONOMY_PAUSE.json (scopes, optional expiry, atomic, fail-closed); every
    autonomous starter calls allows(kind) (tests/test_autonomy_control_ratchet.py). The
    three legacy sentinels (AUTONOMY_HALT, TREASURY_ENTRY_PAUSE, STREAM_SEEDING_PAUSE)
    are read-only facets of it — a touch still works, nothing writes them any more.
  • Deterministic owner stop gate on Telegram (text or voice, before any queue, model
    call or tool) and in the REPL: "stop" / "stop everything" / "autonomy off" pauses
    everything from the text alone; "resume"/"continue" lifts a pause; a scoped "stop
    trading" goes to the agent unless the model is credit-dead or the session is busy, in
    which case everything is paused as the safe default. The directive is recorded into
    the session as ALREADY APPLIED so a later drain can never re-apply a stale stop.
  • autonomy_control agent action (owner-only; forged, leaf and non-owner turns
    refused; a correspondent-tainted session cannot reach it) so "stop trading
    for 6 hours" becomes state, not a row cancellation. The security prompt tells the agent
    to call it FIRST on any stop/pause/resume ask.
  • /pause [scope…] [for 6h] /resume [scope…] on Telegram, the REPL and the web
    console (/api/webgate/pause|resume); polyrob autonomy pause|halt|resume|status;
    /halt and polyrob owner halt|pause-entries|pause-streams kept as aliases. One
    renderer pair produces the verified confirmation text on every seat.
  • In-flight work is cancelled on the paused edge: goal runs return to ready with no
    failure increment, the running cron job returns to scheduled, background delegations
    of autonomous sessions are cancelled; loops stay armed (resume needs no restart); the
    a 3 s pause watcher reconciles a pause written by another process (CLI, console,
    script, touched file) the same way; the cold-start requeue stays unconditional (a
    running row after a restart is a lie; dispatch is gated).
  • Status leads with the pause line on every seat (⏸ PAUSED (…) since … by … via …
    or ▶ RUNNING — n goal run(s), n cron run(s), loops alive x/y); a pause_violation
    CRITICAL health item names autonomous activity recorded after a pause. A missing
    cron/goal store no longer blanks the loops section.
  • Out-of-process actors honour the record: ops_alert.py suppresses non---critical
    alerts (durably logged) while oversight/all is paused; the maint/intel watchdogs
    neither nudge nor relaunch; dev_inject.sh appends only; the loop prompts run a
    read-only "paused tick".
  • Fixes the pause needed to hold: the agent's own email can never be seeded as a
    correspondent (its sent copy re-ran a finished treasury goal on ...
Read more

POLYROB v0.12.0

Choose a tag to compare

@themontreal themontreal released this 21 Aug 08:39
41552b6

Fixed — session eviction killed the shared Twitter/X and MCP tools (13h prod outage)

  • Session teardown no longer destroys process-wide tool singletons. Idle
    session eviction (orchestrator.cleanup(full_cleanup=True)) called every
    controller tool's private _cleanup() — including container singletons — so
    one session's eviction nulled TwitterTool.client / MCPTool.server_manager
    for the whole process, and the skipped cleanup() bookkeeping left
    is_initialized True, so the re-init gate never fired again (dead until
    restart; 2026-08-21, ~13h41m of failed X reads/writes plus the "anysite
    unavailable" symptom). Teardown now skips container/browser-manager-owned
    instances and releases session-owned tools via the public cleanup() only;
    a new ratchet test forbids cross-object _cleanup() calls repo-wide.
  • Twitter tool lifecycle made honest. The credential check iterated the
    already-filtered config dict, so a deploy with missing credentials reported
    the tool enabled; it now checks the expected key set. _cleanup() clears the
    lifecycle flags itself; _check_ready() verifies the live client (a dead
    client now returns a named cause instead of an untyped tweepy
    AttributeError that reads like a credentials problem); _ensure_initialized
    self-heals a dead client and marks success instead of rebuilding the client
    (and burning a live get_me call) on every search/get_user/get_tweets; two
    dead init paths (_initialize_client, _lazy_init, ~70 lines, zero callers)
    removed.

Added — named profiles: isolated, shareable bot identities

  • Profiles. polyrob profile create <name> makes a fully isolated home
    under ~/.polyrob/profiles/<name>/ — its own .env, characters, skills,
    identity docs, memory, goals/cron state, and sessions. Select one with
    polyrob -P <name> / POLYROB_PROFILE, pin a folder to one with
    polyrob profile adopt (writes ./.polyrob/profile), or make one sticky
    with polyrob profile use. An explicit -P overrides an exported
    POLYROB_HOME; a pin/sticky never does (one-shot mismatch warning instead),
    so servers with an explicit POLYROB_DATA_DIR are untouched. With no
    selection anywhere, behavior is byte-identical legacy mode.
  • Manage: profile list/show/path/rename/delete/alias; create writes a
    ~/.local/bin/<name> wrapper by default so <name> run "…" works as a
    command. polyrob doctor and the REPL's /profile print the active profile
    and both homes; polyrob init --profile <name> writes identity keys into
    the profile's .env (provider keys and the default model stay global).
  • Share: profile export/import (tar.gz backup — credentials excluded,
    secret-shaped strings force-scrubbed, traversal-guarded) and
    profile install <git-url|dir> [#ref] / update / info (the
    polyrob.profile.yaml distribution format). On update, distribution-owned
    paths (characters/skills/cron/mcp.json/soul.md) are replaced; config.yaml
    is preserved unless --force-config; .env, auth.json, wallet material
    and the whole data/ tree are never touched — a distribution ships a soul,
    never someone else's memories.
  • Daemons: one process per profile — every surface command honours -P,
    and deployment/polyrob@.service runs polyrob@<profile> units with the
    homes set explicitly.
  • Guards: file tools refuse to touch another profile's home
    (POLYROB_ALLOW_CROSS_PROFILE=1 to bypass deliberately); a process that
    reaches the runtime without profile resolution while a sticky profile is set
    warns loudly that it would write into the default home.

Changed — the package now ships a NEUTRAL identity (behavior change for every install)

  • A fresh install is POLYROB, not a specific person's bot. The framework
    used to hardcode the maintainer's own character (rob.character.json, with
    its bio and lore) as the default persona for every install on earth, and
    DEFAULT_INSTANCE_ID was "rob". The package now ships one neutral
    polyrob.character.json; rob.character.json and trump.character.json
    left the package (a specific bot's character is data, dropped into
    <data_dir>/characters/ or a profile — not framework code). The default
    instance id is now "polyrob".
  • Escape hatches (existing installs): set PERSONALITY_DEFAULT_CHARACTER
    and/or drop your character file in <data_dir>/characters/; pin your
    instance id with POLYROB_INSTANCE_ID. A configured character name that no
    longer resolves falls back to the neutral persona with a one-shot warning —
    never a hard failure.
  • Identity docs migrate automatically. On the default instance id, a
    one-time copy-not-move migration duplicates identity/rob/
    identity/polyrob/ in the data home (marker-gated, fail-open, source kept),
    so existing SELF/owner docs don't vanish behind the renamed directory. A
    deploy that pins POLYROB_INSTANCE_ID=rob explicitly is untouched.

Fixed — the neutral identity holds everywhere (alignment sweep)

  • Every user-facing surface now speaks as the configured instance, never a
    hardcoded bot name.
    The Telegram /help said "ROB commands" on every
    deployment; the LLM-outage notice, the soul init scaffold, /v1/models'
    owned_by, and /health's service name carried the old name; the avatar
    generator's default seed was a person's name (now POLYROB, and pfp --seed
    defaults to the instance name, matching its own help); the dev env template
    pointed at a character file that no longer ships. All resolve the instance id
    or the neutral default now.
  • polyrob update on a box with the per-profile unit template silently
    no-oped.
    The manual systemd steps swept the bare polyrob@.service
    TEMPLATE into one && chain; systemctl stop on a bare template is invalid,
    so the chain aborted before git pull. Templates are now skipped; live
    polyrob@<name> instances are still included.
  • One character-directory precedence. Data home > profile home > the
    shipped use-case personas (researcher/coder/analyst/writer/ops) >
    the packaged neutral set — one implementation, used by the CharacterManager,
    the persona resolver, and /persona (which now unions all tiers; the old
    cwd-relative lookup missed a profile's characters entirely).
  • Identity-card noise on a fresh install: the banner no longer prints
    polyrob · instance polyrob, and /session / /self label an auto-derived
    owner instead of repeating the same name three times.

Fixed — the agent's own work kept disappearing

  • The daily workspace GC had two owners; the read-only console was one of
    them.
    polyrob-webview.service builds its own TaskAgent, and
    TaskAgent.initialize() unconditionally spawned _periodic_workspace_cleanup
    — so a monitoring console ran a destructive rmtree over the agent's data
    once a day. TaskAgent now takes owns_workspace_gc (default True, so the
    agent/API processes are unchanged) and the webview passes False.
  • Deploys shipped code to processes nobody restarted. deploy_prod.sh
    restarted polyrob.service (and polyrob-email.service) but never the
    webview, which runs from the same /opt/polyrob tree. A webview process
    started 2026-08-05 therefore never picked up the 2026-08-18 project-root
    guard and kept deleting the project directory every day at 06:26 UTC for two
    weeks while .deployed_sha reported the fix as live. Both deploy scripts now
    restart every sibling unit, on the success and the rollback path.
  • filesystem.read_file corrupted every file it read. A whole-file read ran
    through _clean_text, which strips each line and collapses horizontal
    whitespace — so reading a .py or .yml returned content whose indentation
    was gone, and the agent then edited from the corrupted copy. The write path
    was fixed for this in F9; the read path was missed. Reads and writes now
    round-trip. (offset/limit and char_offset reads were never affected.)

Fixed — x402 could not price its own server

  • A 402 challenge carried in the response BODY is now parsed. The client
    read only the PAYMENT-REQUIRED header, but the x402 spec puts the payment
    requirements in the body and POLYROB's own middleware emits exactly that
    (nothing in the codebase sets that header). x402_quote therefore reported
    every body-carrying server — our own gated A2A and /v1 routes included — as
    "not a paid resource". Header challenges are unchanged; the body is a
    fallback, and a present-but-broken challenge still fails closed. Not a spend
    hole: x402_fetch already authorized the gate at max_amount_usd when the
    quote came back None.
  • x402_quote no longer requires a wallet. Pricing costs $0; refusing it
    when AGENT_WALLET_ENABLED was off left invoice-only deployments unable to
    see what anything charges. A null result now says so honestly instead of
    implying "free".

Added — x402 discovery

  • x402_probe and x402_sweep (tools/x402/discovery.py): probe one
    endpoint or many, read-only, and score payability 0–5 (answered / 402 /
    parseable challenge / price disclosed / full asset+network+payTo
    routing) with the reasons a score fell short. Handles POST-only paywalls
    (JSON-RPC, A2A) and every accepts shape seen in the wild. Never sends a
    payment header, needs no wallet, bounded to 50 targets at 8 concurrent, and
    every agent-supplied URL goes through the same SSRF validator web_fetch
    uses. All challenge decoding delegates to the one client-side parser.

Added — unattended treasury trading

  • Tiered on-chain spend lane (DEFI_TIERED_SPEND_LANE, default OFF) — a
    goal-dispatched run can trade within the per-tx autonomous ceiling without the
    owner-queue tap. A narrowed turn-origin bar (DEFI_AUTONOMOUS_TURN_TRADING,
    default OFF) lets ONLY a goal/cron-dispatched MAIN-agent turn reach the lane;
    a leaf/su...
Read more

POLYROB v0.11.0

Choose a tag to compare

@themontreal themontreal released this 19 Aug 09:03
f8ab17f

[0.11.0] — 2026-08-19

Added — multi-chain DeFi

  • Chain registry SSOT (tools/defi/chains.py) — Ethereum and Base as
    money-capable chains, Robinhood Chain as data-only (capability decided by
    on-chain evidence, not vibes); the tool door checks chain capability while
    the transaction guard keeps its own RPC pin. Per-chain portfolio views, a
    provider-id price filter, and a registry-driven chain gate on every money
    verb. Gas is sized from the simulation's gasUsed (a fixed 120k limit
    would out-of-gas a real swap).

Fixed — first-run install & CLI UX (proposal 027, clean-room verified)

  • A plain pip install polyrob can actually run tasks. Module-scope
    playwright imports sat on the task-agent import chain, so a core install
    (no [browser] extra) died with a bare "Task package not available" —
    including the wheel published on PyPI. Browser modules are now import-safe;
    the hard failure moves to launch time with the pip remedy.
  • The wheel ships migrations/ (it was omitted — two causes: missing
    __init__.py and missing from packages.find), and the CLI container now
    runs boot migrations, so polyrob run can no longer hit no such column
    after an upgrade. polyrob update's pip/pipx steps state the auto-migrate
    contract instead of prescribing a command that could not work.
  • Honest exits + actionable failures: polyrob run exits 1 when the
    session fails and prints one remedy line (auth / billing / pip extra); a
    bad key halts on the FIRST attempt (the retry classifier's bare "rate"
    substring matched "geneRATE", so every provider error retried with
    backoff); mid-run tracebacks squelch to one line unless --verbose;
    serve/dashboard/telegram/whatsapp preflight their extras BEFORE
    side effects instead of crashing with raw tracebacks.
  • One credential path: a single-provider connect prompt in polyrob init
    and the inline wizard (was six sequential prompts accepting fake keys
    silently), one no-key remedy grammar everywhere (polyrob auth add first),
    config set <secret> writes global scope by default (--project opts
    out), a rejected live-probe key offers removal, OAuth seats are usable
    after connect under local mode (LLM_AUTH_STORE_ENABLED local default ON),
    zero-key resolution returns nothing instead of inventing gemini, and new
    /auth + /doctor REPL slashes.
  • Directory hygiene: read-only commands (doctor, --help) no longer
    write .polyrob/logs/ into the CWD (log dirs create lazily on first
    write); the REPL creates its session dir only after the key gate; running
    from $HOME no longer mixes the data home into ~/.polyrob; init writes
    .gitignore only inside git work trees; telemetry defaults off.
  • Interface: grouped --help (Start here / Surfaces / … instead of 40+
    flat rows), aliases collapse onto their canonical row, --json on
    doctor / model list / auth status, frozen-flag INERT disagreements
    surface in plain doctor, kb export = the knowledge-vault export.
  • Docs truth pass: one canonical quickstart (the pipx playwright step now
    targets polyrob's venv), POLYROB_LOCAL vs AUTONOMY_ENABLED untangled,
    CLI memory-backend default corrected, polyrob auth documented, the six
    shipped OAuth seats acknowledged, install.sh adopted + history-safe.
  • Guard rails: new tests/install/ clean-room suite (wheel packaging,
    extras import matrix, exit codes, no-pollution) + a bare-venv wheel gate in
    the release process.

Added

  • Artifact ledger (core/artifacts.py) — one durable row per file the agent
    produces (producer, path, sha256, size, kind, published url), written at the
    single filesystem write choke point and attributed to a goal by the dispatcher
    BEFORE any exit branch, so a run that failed on max_steps keeps the evidence
    it produced. verify() returns ok/changed/missing/unknown, which lets
    the new artifact acceptance check tell "never produced" from "produced then
    deleted". Tenant scoping is structural. New sidecar artifacts.db.

  • Ship rail (core/publish.py, tools/publish/, PUBLISH_ENABLED, default
    OFF)
    — the agent can put a built page at a real URL: publish /
    publish_list / unpublish. A FIRST publish of a NEW slug is gated by a real
    approving provider (same resolution as hf_deploy, including the
    auto_notifyowner_queue remap); the same slug then iterates unattended.
    The approval gate is enforced by the FILESYSTEM LAYOUT — an unapproved
    publication waits under <PUBLISH_ROOT>/.pending/, which is not a valid slug —
    so the web server needs no application logic. Refused for leaf/sub-agent and
    forged (self-wake / delegation-result) turns, confined to the session
    workspace, credential files refused. Serving side:
    deployment/nginx/polyrob-publish.conf + scripts/setup_publish_vhost.sh
    (owner-run) serve /<slug>/ statically and proxy /api/<slug>/ to the dev
    container's loopback port. New sidecar publications.db.

  • artifact acceptance check{"type":"artifact","name"|"id",…} resolves
    through the ledger instead of a workspace-relative path, so a wipe or a
    relative-path mismatch can no longer masquerade as "never produced".

  • Retry continuity — the goal run task now also carries what earlier attempts
    PRODUCED (verified against the ledger), not only what failed.

  • Agent mail by default (AgentMail provider) — the agent can now have its
    OWN email address with one env var. EMAIL_PROVIDER (auto|smtp|
    agentmail) selects the transport behind the unchanged email tool/surface:
    with AGENTMAIL_API_KEY set, the agent idempotently provisions a managed
    inbox (api.agentmail.to) on first run — no SMTP/IMAP setup — and sends/
    receives from it (address persisted to <data_home>/agent_mail.json, minted
    RFC Message-IDs + a thread map keep correspondent reply-routing exact). New
    identity primitive core/instance.py::resolve_agent_email (sender identity,
    distinct from POLYROB_OWNER_EMAIL); POLYROB_AGENT_EMAIL overrides. The
    legacy GMAIL_* smtp path is byte-identical; receive rides a new
    MailFetcher seam (surfaces/email/fetchers.py).

  • X (x.com) browser rail (tools/x_browser/, X_BROWSER_ENABLED, default
    OFF)
    — the agent can register its own X account and post from it through a
    real browser on a durable, encrypted login. New x_browser tool with
    dedicated, approval-gated verbs x_post / x_login_check / x_signup_start
    (high_impact + delegate_blocked; x_post owner-approval-gated,
    x_signup_start always owner-queued). Signup is a deterministic state machine
    that pulls the verification code from the agent's own inbox, stores a generated
    password encrypted before typing it, writes an automation disclosure to the
    bio, and escalates every CAPTCHA / phone check / unknown page to the owner
    (SignupPaused → notice + goal-board ask; headed waits for an in-window solve,
    headless pauses with a resume command). Browser login persistence via
    BrowserContextConfig.storage_state. CLI polyrob x-account capture-session / status / signup [--resume]. No CAPTCHA solving, no
    anti-detection changes, one account per instance.

Changed

  • Owner asks and approvals ride a lane the daily cap cannot drop.
    _CRITICAL_SOURCES now covers approval, payment_approval and
    goal_blocked; push_owner_message takes a source, so a blocked-goal
    escalation stops sharing the chatter source. Prod delivered 1 of 196 owner
    notices in 8 days, six of the suppressed being owner APPROVAL requests.
  • An ask closes when the goal it blocks no longer needs the owner — new
    ASK_OBSOLETE status (deliberately distinct from fulfilled, which claims the
    owner acted), released by record_success/cancel.
  • A provider pin is a preference, not a death pact
    core.runtime_config.resolve_live_provider re-routes a durable cron pin or a
    goal pin whose provider is credit-dead, and goal dispatch pauses only when
    NOTHING can serve rather than when the default provider is dead.
  • An objective may be standing, but not infiniteOBJECTIVE_GOAL_BUDGET
    (default 25 live children) refuses a further child and names the alternative;
    the planner sees each objective's spend.
  • A published deliverable is reported by its URL instead of "attached" or
    "server-only: ".
  • .env.example and friends are writable again. The .env* credential glob
    swallowed env TEMPLATES, blocking the agent from producing a deploy package.
    Scoped to is_credential_file only — is_secret_path (ingestion into model
    context) still refuses them.

Fixed

  • The DeFi money verbs joined the approval lane and the taint gate. Three
    trading verbs shipped on NO approval lane and outside the correspondent-taint
    gate's name layer — an approval-mode deployment could reach them without the
    owner's OK. Every money verb now rides the same approval + taint + cap
    ladder, enforced by an end-to-end real-guard suite.

  • Usage is billed to the SERVING provider, not the model's vendor — a
    Kimi model served through OpenRouter was attributed (and priced) as
    Moonshot; and a flat-rate subscription seat no longer fabricates per-token
    spend in the aux + display paths.

  • The daily workspace cleanup deleted the agent's home. Under
    POLYROB_PROJECT_DIR every session's workspace IS the shared project root, so
    cleanup_old_workspaces called shutil.rmtree on it once per old session
    ("removed 198/202 old workspaces", 2026-08-16/17) — destroying a week of
    artifacts and breaking the goal acceptance checks, which then failed
    "file not found" on evidence a previous round had really written. A per-session
    workspace stays collectable; a shared project root is not scratch.

  • Provider-outage resilience wave (2026-08-16 log review) — the 08-13..16
    z.ai/OpenRouter double outage groun...

Read more

POLYROB v0.10.0

Choose a tag to compare

@themontreal themontreal released this 11 Aug 09:22
648b607

[0.10.0] — 2026-08-11

Two new capability surfaces — user-declared LLM providers and on-chain token
operations — on top of a large correctness and honesty pass across the agent
loop, billing, security gates, and the provider/credential UX.

Added

  • On-chain token sight (defi_data, DEFI_DATA_ENABLED, default off): a
    read-only tool giving the agent eyes on Base — token_resolve (ranked
    candidate contracts for a ticker), token_info (on-chain identity + price +
    liquidity + a safety screen), price, portfolio (own holdings, USD-valued)
    and contract_read (raw eth_call). No signer is constructed and nothing is
    broadcast, so it cannot move value. Two rules run through the whole tier:
    an address is the only identitytoken_info/price/contract_read
    reject a ticker outright, and token_resolve returns every candidate and
    never picks, because binding a symbol to a contract is the primary injection
    surface for an agent that reads web pages; and unknown is never zero — an
    unreadable price renders unknown, an unreachable safety screen renders
    UNSCREENED (never "safe"), a balance that failed to read is listed
    separately as unknown, and a partial portfolio scan says outright which
    addresses it looked at and that anything outside that set is invisible. Only
    high-confidence prices enter a portfolio total, so an attacker-seeded pool
    cannot inflate the headline figure an owner reads. Token metadata is pinned
    for verified tokens and frozen on first sight otherwise, with any later
    divergence surfaced as metadata_changed rather than accepted. Results are
    untrusted-wrapped (a token's name/symbol are chosen by whoever deployed
    the contract), and portfolio alone is gated while a session is
    correspondent-tainted. An ALCHEMY_API_KEY upgrades holdings enumeration
    from an honest partial scan to a complete index; it is never required.
  • On-chain transfers behind a transaction guard (defi_trade,
    DEFI_TRADE_ENABLED, default off):
    the first verb that can move real
    funds — transfer, with dry_run=true by default. Every call routes through
    a single choke point (core/wallet/tx_guard.py) that simulates the
    transaction, measures its observed asset and allowance deltas, and asserts
    them against a declared intent — refusing on any disagreement or any probe
    failure.
    The bound is not "we only wrote safe verbs", which stops being a
    security property the moment the agent supplies its own calldata. Nine
    ordered, fail-closed gates run before anything is signed: owner kill-switch,
    turn origin (a forged, self-wake, delegation-result, delegated-leaf or
    autonomous turn can never reach a money verb, and an unprovable origin
    refuses), structural checks, an RPC-trust gate that refuses to arm on the
    shared public endpoint, simulation trustworthiness, the delta assertion (an
    undeclared allowance grant refuses outright — a hidden approve is the one
    effect a USD cap cannot bound, because the drain happens in a later
    transaction), pricing (an unpriceable outflow refuses), the per-transaction
    ceiling plus rolling daily caps and replay guard, and finally the approval
    lane: above DEFI_AUTONOMOUS_MAX_USD (default $25) the call returns
    lane=owner_queue and does not execute. Result rendering is honest by
    construction — a refusal says NOT SENT, a reverted receipt says the transfer
    did not happen but gas was spent, and a receipt that never arrived says
    BROADCAST BUT NOT CONFIRMED and warns against blind retry. The signing
    perimeter is deliberately narrow: transaction signing refuses a transaction
    with no chainId (EIP-155 replay exposure), and typed-data signing is kept
    off the money path entirely, since a signed permit is not a transaction and
    would bypass simulation, deltas, caps and audit. defi_trade is classified
    money + high-impact + delegation-blocked, so it is explicit-grant-only and
    the agent cannot self-serve it; it is ANDed with — never a replacement for —
    the existing wallet caps, kill-switch and owner_queue approval lane.
  • Declarative LLM provider registry (proposal 024 P0,
    LLM_PROVIDER_REGISTRY, default on):
    one ProviderSpec table
    (modules/llm/provider_spec.py) now describes every LLM provider — identity,
    credential shape, transport, base URL, capabilities — and the thirteen
    historical hand-maintained provider lists (profiles, PROVIDER_CONFIG,
    schema-generator routing, native-tools list, key→provider detection, fallback
    hierarchy, the OpenAI-compat model map, …) are derivations of it. Users can
    declare NEW providers with zero code in ~/.polyrob/providers.yaml
    (LLM_CUSTOM_PROVIDERS): any OpenAI-compatible endpoint (Ollama, LM Studio,
    vLLM, llama.cpp, LiteLLM, Groq, Together, Fireworks, corporate gateways) or
    Anthropic-compatible endpoint (z.ai GLM Coding Plan, incl. bearer auth) —
    served by two generic spec-parameterized clients. Declared models join the
    model registry (ownership routing through the OpenAI-compat surface and
    check_provider_model); the polyrob model picker UI joins in the L1.5
    surface wave. Byte-identical
    with the flag off or no user file (pinned by a dual-mode characterization
    suite + a provider-list ratchet test); providers.yaml is treated as a
    credential-equivalent file (denied to agent file tools — it redirects the
    agent's inference endpoint), and auth.json stores are name-denied
    everywhere in preparation for 024 L1.
  • LLM credential layer (proposal 024 L1, LLM_AUTH_STORE_ENABLED, default
    off):
    core/llm_auth/ — a durable credential store at
    ~/.polyrob/auth.json (0600 O_EXCL create, cross-process flock, atomic
    replace), the single resolve_credential oracle (env key → OAuth entry →
    consent-tagged borrowed entry → no-credential sentinel), credential
    health (ok/rate_limited/exhausted, last_status_at-reconciled so
    a lost exhaustion marker can never resurrect a spent subscription), and an
    auth error taxonomy (relogin_required vs throttle). Fail-closed tenancy:
    the store only serves on a single-owner POLYROB_LOCAL deployment. The
    credential layer is agent-unreachable by construction (pinned by tests), and
    both secret scrubbers now redact JWT-shaped OAuth tokens via one shared
    pattern. OAuth connect flows (L2) and surface un-blinding (L1.5) are the
    next phases.

Changed

  • Provider and credential UX is honest end-to-end. A 15-finding evaluation
    of provider setup and usage flows closed two outright blockers and thirteen
    smaller lies. polyrob serve used to import a repo-root module that is in no
    wheel, so every installed polyrob serve died with a ModuleNotFoundError
    instead of the intended no-key refusal — the server entry point now lives in
    the package and gates before importing it. Keyless-by-design providers (a
    local Ollama, the documented rail) were excluded from the gating oracles, so
    a keyless box was refused even when explicitly asked for it; they are now
    first-class. Beyond that: polyrob doctor and polyrob model derive status
    from one vocabulary (present / malformed / missing / no key needed)
    instead of contradicting themselves between a table and its footer; a
    rejected providers.yaml row is queryable state in doctor rather than a
    transient log warning; polyrob init no longer prompts for the API key of a
    keyless provider; polyrob run -p <unknown> prints the known-provider list
    instead of a traceback; GET /v1/models derives from the spec registry so
    declared providers are discoverable by OpenAI SDK clients; error messages
    name the session's actual provider (a -p zai-coding failure no longer
    reports "provider openrouter failed" or "from AnthropicCompatClient"); and
    remedies point at polyrob doctor / init / config set / providers.yaml
    rather than a repo-relative env path that does not exist on an installed box.
    POLYROB_<PROVIDER>_MODEL is now a registered catalog row, and the
    credential-surface write refusal moved into the config oracle so any remote
    surface inherits it.
  • Repo-wide duplication and dead-code sweep. A verified audit removed
    several thousand lines of unreachable surface — a retired second billing path
    that re-implemented the markup math, an unused A2A client, dead LLM client
    and manager methods (including a third "which provider owns this model"
    implementation), dead database/memory modules and ~20 zero-caller methods,
    orphaned telemetry models, and a 346-line duplicate Markdown formatter — and
    consolidated the survivors onto single sources of truth: one bool/float env
    parser battery, one secret-scrub sequence, one x402 database/telemetry
    helper, one payments network table, one persistent-backend cache, one
    surface-command envelope behind the seven polyrob <surface> commands, one
    sidecar-DB path resolver, and one FTS query builder. Behaviour-preserving
    except where the duplication was itself the bug (see Fixed/Security).

Fixed

  • Credit death is fail-fast again. The billing block's deliberate
    InsufficientCreditsError was being absorbed by the generic exception
    boundaries guarding the native-tools → structured-output → plain-call →
    manual-parse chain, so each absorbed raise bought another billable provider
    call: one out-of-credits step could make up to four paid calls before the
    credit sentinel saw it. A timeout at the outer boundary was likewise answered
    by starting another full-timeout call.
  • A stray "billing" or "402" in an application exception no longer halts the
    session.
    Both branches that stop the agent classified any exception by bare
    substring match on its message, so a database error on the shipped
    billing_failures table, or a 500 on a path containing /402, killed the
    run with "PERMANENT ERROR — check API configuration" and pointed the operator
    at their API...
Read more

POLYROB v0.9.0

Choose a tag to compare

@themontreal themontreal released this 25 Jul 09:55
835e201

Reliability, autonomy, and honesty hardening across the agent loop, plus two new
interoperability surfaces (an inbound MCP server and dependency-ordered goals).

Added

  • Per-session spend budget (RUN_BUDGET_USD, default 0 = off): set a dollar
    ceiling and a run halts honestly the moment its summed real provider cost reaches
    the cap — reported as a stopped run with a budget marker, never a fabricated
    "completed". The cap counts real provider cost (not the marked-up user price);
    sub-agents ride the parent's budget. Surfaced in the agent's environment block so
    the model can pace itself, and delivered honestly over chat and Telegram.
  • Inbound MCP server surface (MCP_SERVE_ENABLED, default off): polyrob can now
    act as an MCP server, so an MCP client (Claude Desktop, Cursor) can connect to it
    as a tool provider over POST /mcp (JSON-RPC-over-POST). v1 is read-only and
    exposes five tenant-scoped tools — rob_usage_summary, rob_goals_list,
    rob_goal_show, rob_conversations, rob_pending_approvals — authenticated with
    the same X-API-KEY / bearer-JWT / x402 policy as the A2A surface. This is the
    inbound counterpart to the existing outbound MCP client (MCP_ENABLED).
  • Query-based tool discovery (tool_search / tool_describe): the agent can now
    search every tool the deployment knows about by keyword — built-in tools and the
    tools behind connected MCP servers (dozens-to-hundreds, previously reachable only
    through the single mcp tool) — and get full detail (parameters, capability
    dimensions, honest load/gate status, and how to invoke) for any one of them. Both
    actions are read-only, deterministic (no LLM/embeddings), and reuse the same honest
    status the <tool-catalog> renders — money tools are searchable but never shown as
    self-serve-loadable, and delegated sub-agents see the same structured refusals. Rides
    the existing progressive-disclosure gate (TOOL_PROGRESSIVE_DISCLOSURE, on under
    POLYROB_LOCAL).
  • Dependency-ordered goals: the durable goal board now supports dependency edges.
    The agent's goal_create tool accepts depends_on: [<goal_id>, …], and a goal
    with unmet prerequisites waits until they complete before it becomes eligible to
    run. Block reasons are now typed (provider_outage / needs_input / dep_failed),
    and a goal blocked by a transient provider outage self-heals after
    GOAL_BLOCKED_PROVIDER_RETRY_MIN (default 30 min) instead of aging out like a
    stuck goal.
  • SSH code-execution backend (CODE_EXEC_BACKEND=ssh): run run_code on a remote
    host over your system ssh (CODE_EXEC_SSH_HOST / _USER / _PORT / _KEY). It
    is honestly reported as not a sandbox by default — a generic remote host runs
    agent code with the SSH user's full privileges — so a server refuses it unless you
    attest the host is hardened/disposable with CODE_EXEC_SSH_SANDBOXED=true.
  • OAuth for outbound MCP connections (MCP_OAUTH_ENABLED, default off): SSE/HTTP
    MCP servers whose config declares an auth: {provider: generic_oauth2, …} block get
    an injected Authorization header, with token minting/refresh persisted
    (Fernet-encrypted) and a single automatic retry on a 401. Forward-looking
    scaffolding; no shipped server declares auth yet.
  • Security & trust-model guide page (docs/guide/security-model.md):
    one honest, consolidated answer to "what actually stops the agent from doing
    something bad?" — which gates are in-process heuristics vs. the OS/container
    boundary, where code runs unsandboxed today, and recommendations by deployment
    shape.
  • AUTONOMY_ENABLED master switch: one owner-legible flag that turns the
    self-directed autonomy loops on. Default off for a new local install; on
    automatically under AUTONOMY_MODE=autonomous or AUTONOMY_POSTURE
    owner-visible/full. First run prints a one-time posture notice (autonomy on/off +
    where data and config live), and polyrob doctor gains an autonomy: line
    alongside the data dir and active config file.

Changed

  • Autonomy is now OFF by default for new local installs (default change): the local
    CLI profile still enables the interactive tools (coding, git, knowledge base, memory,
    project-context); the self-directed loops (self-wake, goal board + planner, curator,
    background-review, episodic continuity, self-editing) now require AUTONOMY_ENABLED=true
    (or AUTONOMY_MODE=autonomous / an AUTONOMY_POSTURE). A first run no longer silently
    starts a background agent that schedules goals and rewrites its own skills — it prints
    the active posture and where data/config live instead. Multi-tenant server behavior is
    unchanged. Check state anytime with polyrob doctor or /autonomy.
  • Dead-target delivery hygiene, now on by default (DEAD_TARGET_REGISTRY): the
    agent stops burning outbound sends on provably-dead targets (a chat you've been
    blocked from, or a deleted conversation) and automatically revives the target the
    next time it hears from it. Only definitively-classified failures are suppressed;
    ambiguous errors are unaffected.
  • Anti-injection framing on context compaction, now on by default
    (COMPACTION_PROMPT_GUARD):
    the summarizer prompt and the prior-summary block it
    rebuilds are framed so adversarial text captured in a long conversation can't hijack
    the compaction step.
  • Reason-specific outage notices: the owner-facing "all providers are down" notice
    (rides LLM_OUTAGE_NOTICE) now distinguishes the cause — out of credits vs. an auth
    failure vs. every provider exhausted — instead of one generic message.
  • Coding tool tolerates near-miss edits: str_replace now falls back through a
    small ladder of whitespace-tolerant matches (blank-line edges, interior spacing) when
    an exact match fails, and reports which rung matched so the edit stays auditable.
  • Cross-session search paging and ranking: session_search supports
    before_id pagination under newest-first sort, and de-prioritizes automation
    (goal/cron) sessions in results so human conversations rank first.
  • Background delegations survive a restart: a detached (background=true)
    delegation that completed while the process was down is now delivered back into its
    session after restart, rather than being silently lost.

Fixed

  • Chain-aware LLM error classification: a single structured error taxonomy
    (core/error_classifier.py) now drives the loop's fatal-vs-retry and
    billing-vs-transient decisions by walking the full exception chain, so a billing
    failure wrapped inside another error is no longer misread as a generic failure.
  • Goal board correctness under contention: atomic claim/completion is
    compare-and-swap guarded against double-processing, dependency cycles are rejected at
    creation, and a raced dependency edge is repaired on the next tick.
  • Telemetry write no longer errors on first use: the per-session LLM-usage log
    handles a missing directory on its very first write instead of failing.
  • Per-session turn serialization: inbound messages that resolve to the same session
    through different address aliases are now serialized on the resolved session id, so
    two near-simultaneous arrivals can't race.
  • Invoice listing paginates correctly: a status-filtered invoice list now applies the
    filter in SQL before the row limit, so filtered results beyond the first page are no
    longer dropped.

Security

  • Log redaction gaps closed: the secret-scrubbing filter is now attached to the two
    log surfaces that were bypassing it, and a hole where secrets in a marker-less shape
    (e.g. a bare token value) slipped past the marker gate is fixed. Real log calls are
    now covered.
  • No raw exception text echoed to callers: the A2A JSON-RPC internal-error response
    and the inbound MCP-server error paths no longer echo raw exception strings (which can
    carry internal detail) — they return a generic message and log the detail server-side.
  • Credit-death detection is precise: the sentinel that recognizes an unrecoverable
    "out of funds" 402 now matches the code on a word boundary, so an unrelated number
    that merely contains 402 no longer trips it.
  • Inbound MCP server fails closed on an unresolved caller: a tools/call with no
    resolvable principal is refused rather than served.
  • Coding tool's type-checker no longer inherits secrets: the LSP diagnostics
    subprocess (pyright/tsc) now runs with a scrubbed environment allowlist, so provider
    keys and other secrets in the process environment are never exposed to it.
  • Correspondent-tainted sessions can't read the financial ledger: the read-only
    accounting / x402_invoices verbs are now name-gated with the money verbs, so a
    session tainted by a third-party correspondent can no longer read treasury balances,
    income, or invoice history.

What's Changed

Full Changelog: v0.8.1...v0.9.0

POLYROB v0.8.1

Choose a tag to compare

@themontreal themontreal released this 21 Jul 09:50
8054b24

[0.8.1] — 2026-07-21

2026-07-20 — Reliability & honesty fixes (live battle-test hardening)

  • Financial-language honesty: an unpaid fetch / x402 attempt now explicitly
    states it did NOT pay (the proximate cause of a fabricated "payment sent" claim),
    and the agent is steered to x402_quote instead of a rejected max_amount_usd=0.
  • Owner-delivery priority lanes: the user-delivery rail is now priority-ordered
    so a credit-death / halt notice can no longer be starved behind ordinary chatter
    under the flat FIFO send cap.
  • Credit-death sentinel reachability: the fatal-halt branch the sentinel needs
    was unreachable — it now walks the exception chain to recover the 402's billing
    text, and re-trip notices no longer self-dedupe (each trip carries its timestamp).
  • Message tool: results acknowledge attached media (killing a retry-to-BLOCKED
    loop), carry real content evidence + the true error for the completion judge, and
    resolve owner as a target alias before access-tier resolution.
  • Code execution (docker): the sandbox workspace bind-mount is writable by the
    forced uid, and chmod recurses into pre-existing subdirs.
  • Filesystem tool: write_file/append_file coerce dict/list content to JSON
    instead of erroring.
  • Surfaces / Twitter: a goal's owner-notify message is no longer literally
    addressed to the bot's own handle; Twitter media_paths resolve against the real
    session workspace; and the agent is steered away from posting debug-scratch text
    to the live account.

2026-07-20 — Dynamic tool rig S3+S4: mcp un-exclusion + create-time narrowing removed

  • mcp registers in the CLI/headless container (S3 tail; browser was un-excluded
    by the maint loop @14381f62): MCPTool.__init__ is config parsing only, so it left
    _CLI_INCOMPATIBLE; registration is gated by explicit MCP_ENABLED, the
    autonomous-mode capability default, or local server files (_cli_extra_gate).
    Missing gateway secrets (MCP_GATEWAY_TOKEN/ANYSITE_JWT) now fail loudly at
    load/connect time — an owner ask — never a silent "not found in container".
  • goal_create stops narrowing (S4): under TOOL_PROGRESSIVE_DISCLOSURE, an
    inference-only goal no longer writes keyword-guessed payload.tools (which
    short-circuited dispatch's wide default_goal_tools()); dispatch-time inference
    remains as a widening hint, explicit tools + baseline union unchanged, flag off =
    byte-identical. Seeding doctrine ("omit payload.tools unless deliberately
    narrowing or granting a money verb") stamped into scripts/seed_goal.py.

2026-07-19 — Dynamic tool rig S1+S2: honest <tool-catalog> + self-serve load_tool (progressive tool disclosure)

  • Every session can now SEE the whole tool universe and self-serve what it needs
    (owner directive: the static, silently-narrowed toolset was the rigidity behind
    the "17 steps researching around a missing browser" failure). Gated
    TOOL_PROGRESSIVE_DISCLOSURE (default OFF; ON under POLYROB_LOCAL).
  • S1 <tool-catalog> foundation block (tools/tool_disclosure.py, pinned as a
    TOOL_CATALOG-origin control message like skills): one line per known tool with
    an HONEST status — loaded, loadable — load_tool("<id>"), or gated:<reason>
    with the remedy channel (money explicit-grant-only / leaf-blocked /
    unavailable-on-this-deploy naming the missing config). Pure render over the
    existing SSOTs (tools/descriptors.py + core/tool_capabilities.py + the
    container); the system prompt stays byte-stable/cacheable.
  • S2 load_tool(tool_id) action: materializes a loadable tool mid-session
    through the SAME load_tools_from_container path session creation uses — its
    schemas appear on the next step (registry cache self-busts). Money tools are
    NEVER loadable (explicit owner/goal grant only); delegate-blocked ids refused
    for leaf/sub-agent turns (honors the DELEGATE_BLOCKED_TOOLS env override);
    correspondent-taint/posture/approval gates unchanged — loading registers
    schemas, it grants no execution rights. Refusals are STRUCTURED
    (gated:<reason> + remedy), killing the silent-drop failure mode.
  • S3 (lazy construction for _CLI_INCOMPATIBLE heavy tools, browser first) and
    S4 (goal_create stops writing narrow inferred payload.tools) shipped
    alongside — see the S3+S4 entry above.

2026-07-19 — Deliverable reachability: completions attach their files, console deep links, /kb + /files (proposal 021)

  • Goal/cron completion pushes now carry their deliverables. The owner push is
    built from the run's artifact registry (agents/task/goals/deliverables.py):
    files attach to the Telegram message as documents/photos (screened + capped),
    everything else is listed honestly as server-only: <path> (<reason>) — never
    again a bare filename the owner can't open. Attaching is gated
    DELIVERABLES_ATTACH_ENABLED (ON under POLYROB_LOCAL), capped by
    DELIVERABLES_ATTACH_MAX_MB (10) / DELIVERABLES_ATTACH_MAX_FILES (3).
  • One shared attach-eligibility seam (core/surfaces/attachments.py):
    workspace confinement (relocated from the message tool), per-file size cap,
    secret-shaped-filename refusal, bounded prompt-injection threat scan
    (fail-closed on scanner error). The message tool's media_paths now rides
    the same screen; the delivery rail (deliver_user_message) and the
    out-of-band TelegramBotSink gained media transport (per-entry fail-open —
    a media fault never takes the text down).
  • Console deep links (WEBVIEW_PUBLIC_URL): completions append the
    owner-auth webview /session/<id> link; the daily digest appends a console
    line. Unset ⇒ byte-identical.
  • Webview browses the agent's ACTUAL workspace root: with
    POLYROB_PROJECT_DIR set the agent runs pm() in project-root mode but the
    webview's own process didn't — its file browser showed the EMPTY per-session
    dir while artifacts sat in the project dir. Startup now applies the same
    mode, single-tenant postures only.
  • Telegram /kb <query> and /files [n] owner verbs — the phone-first
    owner's read path into the knowledge base and the artifact registry
    (previously CLI-only; "ingested into KB" was write-only theatre from chat).
  • Goal-run prompt teaches attachment: report a produced file to the owner
    WITH message(media_paths=[...]); oversized/refused files get the full
    server path instead.
  • Same-day review fix wave (two independent review passes):
    layering-ratchet repair (threat scanner dependency-injected out of core),
    content-level secret refusal via core/secret_scrub (text AND binary heads),
    write-attribution widened to all ledger output kinds + unattributed files
    listed (never dropped), attached lines carry the absolute server path (text-only
    re-deliveries stay reachable) + attachments attrs on user_delivery events,
    MESSAGE_MEDIA_MAX_MB (45) decouples the explicit message-tool cap from the
    10 MB auto-attach cap, cross-tenant media guard in _notify_owner_done,
    webview file endpoints refuse credential-shaped files, sink caption truncation,
    /files episode window scaling.

2026-07-19 — Avatar pipeline: one-time random setup, native headless renderer, voice surfaced

  • Avatar setup is now a ONE-TIME flow: draft → randomize → keep.
    pfp generate mints a RANDOM DRAFT identity (fresh shuffle variant → new face +
    voice per instance) instead of silently freezing the committed stock face every
    install; pfp randomize [face|voice] re-rolls the draft (everything / face-only /
    voice-only, studio shuffle semantics); pfp keep (or pfp pick's save) accepts it
    and locks the identity PERMANENTLY. A kept identity cannot be changed by any verb —
    modules/pfp/store.py raises PfpLockedError on any identity-changing write
    (pixels-only re-render of the same identity stays allowed); pfp push requires a
    kept identity; a pre-lock-era pfp.json is treated as kept. REPL: /pfp generate /
    /pfp randomize [face|voice] / /pfp keep. --stock reproduces the committed
    identity, --seed/--variant pin a specific roll, --config keeps the
    frozen-blob path.
  • Native headless renderer (modules/pfp/still.py): the Mindprint dot pass
    ported to Pillow/numpy over the parity-tested field port. Render chain is now
    Chromium (exact engine) → native mesh renderer (same face, no browser) →
    committed reference (STOCK identity only). A randomized identity can no longer
    be silently replaced by the reference PNG's pixels.
  • Setup lets you SEE and HEAR the identity on both surfaces. CLI/REPL: every
    setup step renders the face inline (truecolor TTY) and the new
    polyrob pfp say [text] / /pfp say speaks the voice signature through the
    native TTS engine (modules/pfp/voice.py — macOS say with timbre→clear-voice
    mapping + semitone pitch shift, espeak-ng/espeak, Windows SAPI SSML;
    fail-open with web pointers when no engine exists). Web: the webview /identity
    page now runs the full setup — live face, DRAFT/KEPT state, 🔊 hear-voice
    (browser speechSynthesis, studio timbre mapping), and draft-only
    re-roll/keep controls over POST /api/pfp/{generate,randomize,keep}
    (403 read-only; store-enforced lock contract). Config-shuffle helpers moved to
    modules/pfp/identity.py (CLI re-exports them unchanged).
  • pfp pick freezes into the instance identity home (renders png + meta that
    the webview /identity page, invoice cards, and pfp push actually read) instead
    of writing the repo's avatar/config/rob.json (read-only under pip installs;
    changes never propagated without a manual generate --force). --out exports
    the chosen config JSON.
  • Generate/randomize now report the full identity + next steps (face traits,
    the voice signature, and view/re-roll/push/console pointers) instead of a bare
    PNG path; /pfp status shows traits + voice too.
  • **pfp push --discord (fla...
Read more

POLYROB v0.8.0

Choose a tag to compare

@themontreal themontreal released this 18 Jul 18:49
71023de

Cumulative release: public consumers jump 0.5.1 → 0.8.0 directly. The 0.6.0 and
0.7.0 sections below the fold in CHANGELOG.md
were cut in-tree but never published — everything in them ships here too.

2026-07-18 — Proposal wave 010A/012/015/016/019-cap: outage honesty + delivery-cap starvation + acceptance gap

  • LLM_OUTAGE_NOTICE (default ON, 015 #2): an owner chat turn that dies on
    total LLM-provider exhaustion (the live OpenRouter-402 shape) now gets one
    static, LLM-free ⚠️ notice over the originating surface (30-min per-chat
    cooldown, fail-open, never for goal/cron runs) instead of pure silence.
  • llm_provider_exhausted failure marker (015 #3): dispatcher failure
    classification now distinguishes a provider outage from a genuine
    refusal/no-op in goals.last_failure_error; intel_scorecard.py surfaces
    it as a dedicated red flag.
  • Honest episode stats on failure (012 #1): all dispatcher
    failure-classification paths thread the real RunOutcome
    steps/spend/artifacts into finalize_episode (previously always 0/0,
    corrupting noop_ratio and every consumer of episodes.outcome).
  • Self-evolution notifier batching + durable capped record (019-cap #1+#2):
    maybe_notify_owner_pending fingerprints the pending set and re-notifies
    only on change (it had burned 29/30 daily proactive-delivery slots,
    starving the daily digest); a capped delivery now writes a durable
    owner_notice instead of dropping content irrecoverably.
  • file_contains acceptance check (016 #1+#2): the check type the goal
    planner kept inventing now exists (workspace-relative, bounded read,
    all/any modes); planner + goal_create prompts state the exact closed
    type set.
  • EMAIL_AUTONOMY_RUNTIME (default OFF, 010 A): the email process no
    longer runs the goal/cron autonomy runtime, eliminating the coin-flip
    claim of telegram-outbound goals by a process that structurally cannot
    send them.
  • preferences explain UX: field-level schema descriptions + a
    self-correcting missing-key error (a goal run had burned its retries
    passing text= to explain).

2026-07-19 — 019 revalidation fix wave (adversarial 3-reviewer pass over P0–P5)

  • Critical (OpenAI batch tools): the P5 request-builder extraction left a
    stale formatted_messages reference in _generate_with_tools's debug log —
    an unconditional NameError (f-strings evaluate eagerly) that broke EVERY
    OpenAI native tool call on the default (non-streaming) path. Fixed +
    regression test that drives the real batch method over a fake SDK.
  • Telegram: an act_on_inbound raise (e.g. create_session on exhausted
    credits) unwound past all cleanup — leaking the progress tracker in the
    module registry forever, orphaning the ⚙️ Working… bubble, and giving the
    user silence. The dispatch is now wrapped: tracker closed, bubble deleted,
    error breadcrumb sent.
  • CLI pairing: a printed start line could be left unclosed when
    _should_show_tool flipped mid-flight (synchronous delegate_task sets
    last_step_sub_agent=True before its own completion). A PAIRED completion
    now always prints its result line.
  • RunActivity: eviction is now least-recently-UPDATED (was FIFO-by-first-
    insertion — a long-lived busy session could be evicted by 512 newcomers);
    the snapshot fold now runs AFTER the feed write succeeds, honoring the
    documented "never disagrees with the feed" invariant.
  • Token-streaming brain guard hardened: fenced ```json starts now
    suppress live deltas too, and a TRAILING brain-state block after prose mutes
    the live stream at the "current_state" marker (remainder rides the final
    chunk whole, where the downstream brain scrub works). Content reconstruction
    stays exact; new tests for both shapes.
  • Webview /pending: the auto-refresh no longer dies permanently after the
    first "Show full" click (visibility tracked separately from the fetch cache).
  • Sub-agent mirror: subagent_started now emits inside the try that
    guarantees its paired finished, so a cancellation while queued for a slot
    can't strand the parent phase at delegating.
  • Deps: openai>=1.26.0 (floor for stream_options); anthropic floor
    already adequate (messages.stream predates it).
  • Also fixed a foreign test's process-wide env leak
    (test_email_autonomy_gate.py drove the real _run_email, whose
    CORRESPONDENT_ACCESS_ENABLED setdefault flipped 6 unrelated telegram
    routing tests to DENIED in full-suite runs). Full suite: 8399 passed / 0
    failed.

2026-07-18 — Live run-state observability P5 (proposal 019): true token streaming

  • LLM_TOKEN_STREAMING (default OFF): when ON and the provider client
    implements the new astream_agent_response (Anthropic + OpenAI),
    LLMClientAdapter.astream yields REAL per-token deltas instead of the
    legacy one-blob chunk — the CLI ResponseBox / webview stream_chunk /
    Telegram partials fill as the model writes. OFF = byte-identical legacy.
  • Safety of the stream: deltas run through a per-call
    StreamingThinkScrubber (a <think> block split across delta boundaries
    never leaks); a completion starting with { (brain-state JSON) suppresses
    live deltas entirely so raw JSON never streams to the user (final chunk
    then carries the whole content — exact legacy shape). Tool calls, usage
    metadata, and the per-call provider-response billing id ride the final
    chunk, so token accounting and billing dedup are unchanged.
  • Provider plumbing: the batch request builders were extracted
    (_build_tool_api_params / _build_tool_request_params) so streaming
    issues byte-identical requests; Anthropic streams SDK text_delta events
    then parses get_final_message() with the same block parser; OpenAI uses
    stream=True + stream_options.include_usage with by-index tool-call
    fragment assembly. A pre-first-chunk failure falls back to single-chunk;
    mid-stream failures propagate (a silent fallback would double the text).
  • The agent loop, stream_output funnel, and both stream consumers were
    already N-chunk-safe — no changes there. Not yet live-smoke-tested against
    a real provider (no key on the dev box); flag stays OFF until the owner
    flips it.

2026-07-18 — Live run-state observability P4 (proposal 019): machine surfaces

  • A2A: an approval wait now streams as A2A's NATIVE input-required task
    state (back to working on resolution) with the action name in the status
    message; tasks/get responses carry metadata.current_activity (the same
    RunActivity snapshot as the session-status API).
  • OpenAI-compat: stream: true stays buffered (P5 is the token-streaming
    upgrade) but the agent turn now runs concurrently with the SSE body,
    emitting spec-legal : keep-alive comment frames every ~15s so long turns
    no longer hit client/proxy idle timeouts; a failure after headers surfaces
    as an error chunk + [DONE] instead of a dead socket. Documented honestly
    in docs/guide/api.md.
  • Proposal 019 status → IMPLEMENTED (P0–P4); P5 (true token streaming)
    remains deferred pending separate owner approval.

2026-07-18 — Live run-state observability P3 (proposal 019): webview truthfulness

  • Per-session state banner: a page-level banner (visible on every tab)
    driven by live feed events — ⏸ Awaiting your approval: <action> with
    inline Approve/Deny (reusing the webgate pending actions; hidden on a
    read-only console, ambiguity falls back to /pending), ↻ retrying,
    📦 compacting. Cleared by the matching resolution/progress events.
  • First-class feed cards for every 019 kind (tool_started shows
    "running…" the moment a tool dispatches; approval/retry/compaction/
    sub-agent/delegation render as compact one-liners instead of raw-JSON
    generic cards).
  • /pending + /autonomy auto-refresh (5s/10s, visibility-gated;
    pending skips refresh mid-action or while a body is expanded) — a newly
    blocked approval or a goal that starts running shows without a manual
    reload.
  • Session-list activity badge: [● tool: navigate] / [⏸ awaiting_approval] etc. from the in-process RunActivity snapshot
    (honest absence when another process owns the session).

2026-07-18 — Live run-state observability P2 (proposal 019): Telegram progress

  • Live progress bubble (TELEGRAM_PROGRESS_EDITS, default ON; per-owner
    pref progress.telegram): the static ⚙️ Working… Telegram bubble becomes a
    feed-driven live status line — ⚙️ step 3 · → navigate · 2 tools · 45s · $0.02 — edited in place at most once per 2.5s. Wait states override
    immediately: ⏸ Waiting for your approval — /pending, ↻ rate_limit — retrying in 8s, 📦 Compacting context…; a still-blocked approval gets ONE
    reminder edit after 10 min (never a new message). Built on a new
    surface-agnostic TurnProgressTracker
    (agents/task/telemetry/live_progress.py) + a multi-subscriber feed-callback
    seam (ProductTelemetry.add_feed_subscriber — the CLI's single
    _on_feed_entry slot is no longer the only consumer, so the gateway's
    one-process surfaces can't clobber each other).
  • Autonomous run START notice (AUTONOMY_START_NOTICE, ON under
    AUTONOMY_POSTURE=full/autonomous, else OFF): ▶ goal started: <title> /
    ▶ cron run started: <task> pushed via the one owner-delivery rail
    (dedup + caps) at dispatch time — the owner no longer learns of autonomous
    runs only at completion or in the daily digest. Digest and $0 gated ticks
    never notify.
  • Deferred within P2: the email finalized-turn summary footer (needs
    RunOutcome→OutboundMessage plumbing; email stays buffered and unchanged).

2026-07-18 — Live run-state observability P1 (proposal 019): full vocabulary + snapshot

  • Vocabulary completed (same RUN_EVENTS_ENABLED gate, fail-open):
    compaction_started/finished ...
Read more