Releases: theselfruleorg/polyrob
Release list
POLYROB v1.0.2
[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, thepolyrob x/gateway presence checks, the CLI
twitter gate) now resolves throughresolve_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+ newTWITTER_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_usdis now an owner-override preference: an approved
value replaces theAGENT_WALLET_MAX_PER_TX_USDdefault in either direction,
clamped to the daily cap (core/wallet/config.py::effective_max_per_tx_usd).
The daily cap (budget.wallet_daily_usdoverWALLET_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 editpolyrob.envand restart.
Fixed
-
The wallet caps apply LIVE.
PolicyGatecopied both caps at construction
(process start), sobudget.wallet_per_tx_usd/budget.wallet_daily_usd—
documented and shown asapplies: 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
everycheck()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()readDEFI_AUTONOMOUS_MAX_USDwhile
tx_guardstep 9 read the pref-resolvedbudget.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 totx_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 carriescron_job_id, and an
approval pulls that job'snext_run_atto now (core/cron_rearm.py, the one
UPDATE bothCronJobStore.run_nowand 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_changenow refuses up front and names the exact
env line (WALLET_DAILY_CAP_USD=…in the service env file) that would. -
FileTokenStorewrites follow the shared-data identity convention
(<writer>:polyrob-data 0660under a group-writable data dir; an existing
file keeps its mode + group). It wrote0600in the writer's primary group,
so the OAuth2 pair the root CLI imported was unreadable bypolyrob-agent
— the unit that needed it — and an agent-written refresh would have been
unreadable by the owner's CLI the same way. -
FileTokenStoreno longer treats an UNREADABLE file as a CORRUPT one. On
prod thepolyrob-emailunit (a non-root service identity) gotEACCESon
the root-owned0600X token store thatpolyrob.servicehad 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
OSErroron 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:BrowserManagerpassed theBrowserConfiginto
Browser's BotConfig slot, soBROWSER_CDP_URL/BROWSER_WSS_URLwere
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.pypins 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-browserforbrowser/x_browser/
dapp_browser), the status snapshot'sidentitysection (browser:line;
WARN only when a configured endpoint fails) andpolyrob doctor.polyrob browser install|update|status|render: the isolated browser
service as a boundary — dedicated UID, Chromium sandbox on via an AppArmor
usernsprofile (Ubuntu 24.04 restricts unprivileged user namespaces; the
earlier unit's--no-sandboxwas 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.shand
hardening/apparmor/polyrob-browserare the CLI's rendered output, pinned by
a test.scripts/deploy_prod.shprints 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'sBROWSER_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.pygave the file handler
and the console handler ONEComfyFormatter, whose 0.5 s duplicate filter
saw the console's copy of each record as a repeat of the file's and
returned""— theStreamHandlerstill wrote the newline, and every such
write erased and redrew the prompt. Duplicate suppression is now a
per-handlerlogging.Filter(a formatter cannot drop a record), and the
REPL console renders one compact✗ component: messageline 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.logkeeps every
layer. - Gemini: a text part was dropped and logged as
Gemini function_call missing nameat ERROR. A protoPartanswershasattr(part, "function_call")True for every oneof member, so a plain text part hit the
nameless-call branch andcontinued — 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_sendpushed each chat reply through the owner
delivery rail — dedup, the hourly rate limit and the daily cap included.
Past the capsend_messagetold 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_surfacemarks such a
session;--resumekeeps 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...
POLYROB v1.0.1
Added
self-deployskill: 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 nowpolyrob initwas the human's wizard
and the setup interview covered only the owner contract.twitter_poll_results(tweet_id): reads the options, votes, shares,
voting_statusand end time of a poll the agent posted. The tool could post
a poll (poll_options) but no read asked forattachments.poll_ids, so the
agent could ask the community and never learn the answer. The
x-engagementskill 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
identitystatus section now reports the agent's reachable identities:
email:(the address it sends AS, ornone → remedy) andx:(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
liquiditystatus section on every seat (status snapshot,polyrob wallet overview, Telegram/wallet overview, consoleGET /api/webgate/liquidity)
lists the treasury's Uniswap v3 positions with their fees; on-chain
enumeration is opt-in and owner-only./lpis listed in the money verb
group on every chat surface. launchpad_statusnames 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 @handlex_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_logowns the telemetry db resolution and a fail-open
emit()(the recap reader used to ignoreTELEMETRY_EVENT_LOG_PATHand
create the db on read); every owner/admin CLI verb resolves its data home
throughcli/_admin_home.py(the deployed-home rule now also covers
apps,surface,cron,goals,journey, andowner pending's goal
board, which came from a third resolver); the sub-agent and owner-pause
refusals every money verb states live incore.wallet.authority; the
publish/app-service owner-turn clauses incore.security.owner_turn; the
ship rail's orchestrator/approval/workspace helpers intools/ship_common.py;
the goal board's status/kind vocabulary incore.goal_vocab; the/help
group order is read fromcore.verbs; the copy-layer engine is shared by
core.copyandwebview.copy; the Telegram update dedup is the core
IdempotencyStoreover its existing table. Twelve open-coded boolean
truth sets now parse throughcore.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./exportnames the formats the
REPL supports and points atpolyrob session exportfor the rest;/logs
prints the log directory instead of a CLI verb that does not exist. - The approval
awaitingevent names the provider that will decide. lp_collect/lp_removeset 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 callsdone(). 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,
preventingdone()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 autonomousverb resolved the preference store through the
process home (an empty tree under a service account), so an approved
budget.defi_autonomous_usdwas 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_profilewas apassstub
while the docs claimed the disclosure was written into the bio — no handle
and no disclosure were ever applied. It now edits/settings/screen_nameand
/settings/profilebest-effort and RETURNS what it applied; the signup
result carriesrequested_handle/handle_applied/bio_appliedand 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_browserstays explicit-grant-only: it ishigh_impact+
delegate_blocked, so it is not on the CLI optional-registrar table and is
reached by explicittool_idsonly.- 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, itsTWITTER_*
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_urireferencedos.environwith
no module-level import (latentNameError).- The coding and git tools resolve their confined root per TENANT, not the
anonymous bucket, on a multi-tenant server. - The
twitterextra now carrieschatxdk, 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
- release: POLYROB 1.0.1 by @themontreal in #17
Full Changelog: v1.0.0...v1.0.1
v1.0.0
POLYROB v0.13.0
[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 pausewrites to the resolved data home, not the process CWD —
the CLI could previously report a verified pause that the running agent never saw./pausestops themessagetool (the dominant autonomous send path) and the
walletdaily 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_spendrows carry the tenant. They were written tenantless, so the unified
ledger read a confident$0.00over real spend.- Cron delivery goes through the gated
twitter/emailACTIONS, not the raw tool
helpers — so a scheduled post is rate-limited, approval-gated, cooldown-checked and
recorded like every other send. /pause socialactually 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): thepublishgate 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 theship-softwarestream uses). - Supervisor (
polyrob apps supervise,deployment/polyrob-apps.service): snapshot
of the tested tree, per-app bridge + nft egress deny,docker run -dwith the ONE
hardening list (core/container_hardening.py, lifted from the sandbox backend),
health, a two-substitution nginx stanza,artifacts.urlstamped 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 snapshotappssection (pending approvals lead as CRIT). AGENT_BUILDER_MODE=off|build|ship: the fifth named default bundle —build=
PUBLISH_ENABLED+GITHUB_TOOL_ENABLEDandpublishin the goal toolset;ship=APP_SERVICE_ENABLED/APP_SERVICE_ALLOW_PUBLICandapp_servicein the goal
toolset; clamps tobuildwithout 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.shrestarts 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-softwarestream (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
defaultcap (shell/code_executionhad no row), not the tools' own ceilings.
TimeoutConfig.TOOL_TIMEOUTSgainsshell/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 omitstimeout) and the dev-moderun_codecap. - Unreachable deliverables are named. A completion notice whose deliverables carry
no published URL ends with one line saying so — namingPUBLISH_ENABLEDwhen 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.pyowns
<data>/AUTONOMY_PAUSE.json(scopes, optional expiry, atomic, fail-closed); every
autonomous starter callsallows(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 — atouchstill 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_controlagent 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;
/haltandpolyrob owner halt|pause-entries|pause-streamskept 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
readywith no
failure increment, the running cron job returns toscheduled, 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
runningrow 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); apause_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.pysuppresses non---critical
alerts (durably logged) whileoversight/allis paused; the maint/intel watchdogs
neither nudge nor relaunch;dev_inject.shappends 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 ...
POLYROB v0.12.0
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 nulledTwitterTool.client/MCPTool.server_manager
for the whole process, and the skippedcleanup()bookkeeping left
is_initializedTrue, 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 publiccleanup()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
AttributeErrorthat reads like a credentials problem);_ensure_initialized
self-heals a dead client and marks success instead of rebuilding the client
(and burning a liveget_mecall) 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
withpolyrob profile use. An explicit-Poverrides an exported
POLYROB_HOME; a pin/sticky never does (one-shot mismatch warning instead),
so servers with an explicitPOLYROB_DATA_DIRare untouched. With no
selection anywhere, behavior is byte-identical legacy mode. - Manage:
profile list/show/path/rename/delete/alias;createwrites a
~/.local/bin/<name>wrapper by default so<name> run "…"works as a
command.polyrob doctorand the REPL's/profileprint 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.yamldistribution 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 wholedata/tree are never touched — a distribution ships a soul,
never someone else's memories. - Daemons: one process per profile — every surface command honours
-P,
anddeployment/polyrob@.servicerunspolyrob@<profile>units with the
homes set explicitly. - Guards: file tools refuse to touch another profile's home
(POLYROB_ALLOW_CROSS_PROFILE=1to 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_IDwas"rob". The package now ships one neutral
polyrob.character.json;rob.character.jsonandtrump.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 withPOLYROB_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 duplicatesidentity/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 pinsPOLYROB_INSTANCE_ID=robexplicitly 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/helpsaid "ROB commands" on every
deployment; the LLM-outage notice, thesoul initscaffold,/v1/models'
owned_by, and/health's service name carried the old name; the avatar
generator's default seed was a person's name (nowPOLYROB, andpfp --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 updateon a box with the per-profile unit template silently
no-oped. The manual systemd steps swept the barepolyrob@.service
TEMPLATE into one&&chain;systemctl stopon a bare template is invalid,
so the chain aborted beforegit 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//selflabel 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.servicebuilds its ownTaskAgent, and
TaskAgent.initialize()unconditionally spawned_periodic_workspace_cleanup
— so a monitoring console ran a destructivermtreeover the agent's data
once a day.TaskAgentnow takesowns_workspace_gc(defaultTrue, so the
agent/API processes are unchanged) and the webview passesFalse. - Deploys shipped code to processes nobody restarted.
deploy_prod.sh
restartedpolyrob.service(andpolyrob-email.service) but never the
webview, which runs from the same/opt/polyrobtree. 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_shareported the fix as live. Both deploy scripts now
restart every sibling unit, on the success and the rollback path. filesystem.read_filecorrupted every file it read. A whole-file read ran
through_clean_text, which strips each line and collapses horizontal
whitespace — so reading a.pyor.ymlreturned 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/limitandchar_offsetreads 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 thePAYMENT-REQUIREDheader, 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_quotetherefore reported
every body-carrying server — our own gated A2A and/v1routes 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_fetchalready authorized the gate atmax_amount_usdwhen the
quote came backNone. x402_quoteno longer requires a wallet. Pricing costs $0; refusing it
whenAGENT_WALLET_ENABLEDwas 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_probeandx402_sweep(tools/x402/discovery.py): probe one
endpoint or many, read-only, and score payability 0–5 (answered / 402 /
parseable challenge / price disclosed / fullasset+network+payTo
routing) with the reasons a score fell short. Handles POST-only paywalls
(JSON-RPC, A2A) and everyacceptsshape 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 validatorweb_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...
POLYROB v0.11.0
[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'sgasUsed(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 polyrobcan 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__.pyand missing frompackages.find), and the CLI container now
runs boot migrations, sopolyrob runcan no longer hitno 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 runexits 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/whatsapppreflight 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 addfirst),
config set <secret>writes global scope by default (--projectopts
out), a rejected live-probe key offers removal, OAuth seats are usable
after connect under local mode (LLM_AUTH_STORE_ENABLEDlocal default ON),
zero-key resolution returns nothing instead of inventinggemini, and new
/auth+/doctorREPL 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$HOMEno longer mixes the data home into~/.polyrob;initwrites
.gitignoreonly inside git work trees; telemetry defaults off. - Interface: grouped
--help(Start here / Surfaces / … instead of 40+
flat rows), aliases collapse onto their canonical row,--jsonon
doctor/model list/auth status, frozen-flag INERT disagreements
surface in plaindoctor,kb export= the knowledge-vault export. - Docs truth pass: one canonical quickstart (the pipx playwright step now
targets polyrob's venv),POLYROB_LOCALvsAUTONOMY_ENABLEDuntangled,
CLI memory-backend default corrected,polyrob authdocumented, the six
shipped OAuth seats acknowledged,install.shadopted + 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 onmax_stepskeeps the evidence
it produced.verify()returnsok/changed/missing/unknown, which lets
the newartifactacceptance check tell "never produced" from "produced then
deleted". Tenant scoping is structural. New sidecarartifacts.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 ashf_deploy, including the
auto_notify→owner_queueremap); 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 sidecarpublications.db. -
artifactacceptance 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 unchangedemailtool/surface:
withAGENTMAIL_API_KEYset, 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 primitivecore/instance.py::resolve_agent_email(sender identity,
distinct fromPOLYROB_OWNER_EMAIL);POLYROB_AGENT_EMAILoverrides. The
legacy GMAIL_* smtp path is byte-identical; receive rides a new
MailFetcherseam (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. Newx_browsertool with
dedicated, approval-gated verbsx_post/x_login_check/x_signup_start
(high_impact+delegate_blocked;x_postowner-approval-gated,
x_signup_startalways 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. CLIpolyrob 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_SOURCESnow coversapproval,payment_approvaland
goal_blocked;push_owner_messagetakes asource, 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_OBSOLETEstatus (deliberately distinct fromfulfilled, which claims the
owner acted), released byrecord_success/cancel. - A provider pin is a preference, not a death pact —
core.runtime_config.resolve_live_providerre-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 infinite —
OBJECTIVE_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.exampleand friends are writable again. The.env*credential glob
swallowed env TEMPLATES, blocking the agent from producing a deploy package.
Scoped tois_credential_fileonly —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_DIRevery session's workspace IS the shared project root, so
cleanup_old_workspacescalledshutil.rmtreeon 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...
POLYROB v0.10.0
[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)
andcontract_read(raweth_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 identity —token_info/price/contract_read
reject a ticker outright, andtoken_resolvereturns 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 rendersunknown, 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 asmetadata_changedrather than accepted. Results are
untrusted-wrapped (a token'sname/symbolare chosen by whoever deployed
the contract), andportfolioalone is gated while a session is
correspondent-tainted. AnALCHEMY_API_KEYupgrades 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, withdry_run=trueby 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: aboveDEFI_AUTONOMOUS_MAX_USD(default$25) the call returns
lane=owner_queueand 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 nochainId(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_tradeis 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 andowner_queueapproval lane. - Declarative LLM provider registry (proposal 024 P0,
LLM_PROVIDER_REGISTRY, default on): oneProviderSpectable
(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); thepolyrob modelpicker 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.yamlis treated as a
credential-equivalent file (denied to agent file tools — it redirects the
agent's inference endpoint), andauth.jsonstores 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(0600O_EXCLcreate, cross-processflock, atomic
replace), the singleresolve_credentialoracle (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_requiredvs throttle). Fail-closed tenancy:
the store only serves on a single-ownerPOLYROB_LOCALdeployment. 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 serveused to import a repo-root module that is in no
wheel, so every installedpolyrob servedied with aModuleNotFoundError
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 doctorandpolyrob modelderive status
from one vocabulary (present/malformed/missing/no key needed)
instead of contradicting themselves between a table and its footer; a
rejectedproviders.yamlrow is queryable state indoctorrather than a
transient log warning;polyrob initno 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/modelsderives from the spec registry so
declared providers are discoverable by OpenAI SDK clients; error messages
name the session's actual provider (a-p zai-codingfailure no longer
reports "provider openrouter failed" or "from AnthropicCompatClient"); and
remedies point atpolyrob doctor/init/config set/providers.yaml
rather than a repo-relative env path that does not exist on an installed box.
POLYROB_<PROVIDER>_MODELis 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 sevenpolyrob <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
InsufficientCreditsErrorwas 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_failurestable, or a 500 on a path containing/402, killed the
run with "PERMANENT ERROR — check API configuration" and pointed the operator
at their API...
POLYROB v0.9.0
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, default0= 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 overPOST /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 sameX-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 singlemcptool) — 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'sgoal_createtool acceptsdepends_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): runrun_codeon a remote
host over your systemssh(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 withCODE_EXEC_SSH_SANDBOXED=true. - OAuth for outbound MCP connections (
MCP_OAUTH_ENABLED, default off): SSE/HTTP
MCP servers whose config declares anauth: {provider: generic_oauth2, …}block get
an injectedAuthorizationheader, with token minting/refresh persisted
(Fernet-encrypted) and a single automatic retry on a 401. Forward-looking
scaffolding; no shipped server declaresauthyet. - 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_ENABLEDmaster switch: one owner-legible flag that turns the
self-directed autonomy loops on. Default off for a new local install; on
automatically underAUTONOMY_MODE=autonomousorAUTONOMY_POSTURE
owner-visible/full. First run prints a one-time posture notice (autonomy on/off +
where data and config live), andpolyrob doctorgains anautonomy: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 requireAUTONOMY_ENABLED=true
(orAUTONOMY_MODE=autonomous/ anAUTONOMY_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 withpolyrob doctoror/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
(ridesLLM_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_replacenow 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_searchsupports
before_idpagination 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"402now matches the code on a word boundary, so an unrelated number
that merely contains402no longer trips it. - Inbound MCP server fails closed on an unresolved caller: a
tools/callwith 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_invoicesverbs 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
- release: POLYROB 0.9.0 by @themontreal in #9
Full Changelog: v0.8.1...v0.9.0
POLYROB v0.8.1
[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 tox402_quoteinstead of a rejectedmax_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
resolveowneras a target alias before access-tier resolution. - Code execution (docker): the sandbox workspace bind-mount is writable by the
forced uid, andchmodrecurses into pre-existing subdirs. - Filesystem tool:
write_file/append_filecoerce 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; Twittermedia_pathsresolve 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
mcpregisters 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 explicitMCP_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_createstops narrowing (S4): underTOOL_PROGRESSIVE_DISCLOSURE, an
inference-only goal no longer writes keyword-guessedpayload.tools(which
short-circuited dispatch's widedefault_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 intoscripts/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 underPOLYROB_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>"), orgated:<reason>
with the remedy channel (moneyexplicit-grant-only /leaf-blocked/
unavailable-on-this-deploynaming 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 aloadabletool mid-session
through the SAMEload_tools_from_containerpath 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 theDELEGATE_BLOCKED_TOOLSenv 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_INCOMPATIBLEheavy tools, browser first) and
S4 (goal_createstops writing narrow inferredpayload.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 asserver-only: <path> (<reason>)— never
again a bare filename the owner can't open. Attaching is gated
DELIVERABLES_ATTACH_ENABLED(ON underPOLYROB_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 themessagetool), per-file size cap,
secret-shaped-filename refusal, bounded prompt-injection threat scan
(fail-closed on scanner error). Themessagetool'smedia_pathsnow rides
the same screen; the delivery rail (deliver_user_message) and the
out-of-bandTelegramBotSinkgained 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_DIRset 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
WITHmessage(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 viacore/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) +attachmentsattrs onuser_deliveryevents,
MESSAGE_MEDIA_MAX_MB(45) decouples the explicitmessage-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,
/filesepisode 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 generatemints 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(orpfp pick's save) accepts it
and locks the identity PERMANENTLY. A kept identity cannot be changed by any verb —
modules/pfp/store.pyraisesPfpLockedErroron any identity-changing write
(pixels-only re-render of the same identity stays allowed);pfp pushrequires a
kept identity; a pre-lock-erapfp.jsonis treated as kept. REPL:/pfp generate/
/pfp randomize [face|voice]//pfp keep.--stockreproduces the committed
identity,--seed/--variantpin a specific roll,--configkeeps 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 sayspeaks the voice signature through the
native TTS engine (modules/pfp/voice.py— macOSsaywith 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 overPOST /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 pickfreezes into the instance identity home (renders png + meta that
the webview /identity page, invoice cards, andpfp pushactually read) instead
of writing the repo'savatar/config/rob.json(read-only under pip installs;
changes never propagated without a manualgenerate --force).--outexports
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 statusshows traits + voice too. - **
pfp push --discord(fla...
POLYROB v0.8.0
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_exhaustedfailure marker (015 #3): dispatcher failure
classification now distinguishes a provider outage from a genuine
refusal/no-op ingoals.last_failure_error;intel_scorecard.pysurfaces
it as a dedicated red flag.- Honest episode stats on failure (012 #1): all dispatcher
failure-classification paths thread the realRunOutcome
steps/spend/artifacts intofinalize_episode(previously always 0/0,
corruptingnoop_ratioand every consumer ofepisodes.outcome). - Self-evolution notifier batching + durable capped record (019-cap #1+#2):
maybe_notify_owner_pendingfingerprints the pending set and re-notifies
only on change (it had burned 29/30 daily proactive-delivery slots,
starving the daily digest); acappeddelivery now writes a durable
owner_noticeinstead of dropping content irrecoverably. file_containsacceptance check (016 #1+#2): the check type the goal
planner kept inventing now exists (workspace-relative, bounded read,
all/any modes); planner +goal_createprompts 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.preferencesexplain UX: field-level schema descriptions + a
self-correcting missing-keyerror (a goal run had burned its retries
passingtext=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
staleformatted_messagesreference in_generate_with_tools's debug log —
an unconditionalNameError(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_inboundraise (e.g.create_sessionon 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_toolflipped mid-flight (synchronousdelegate_tasksets
last_step_sub_agent=Truebefore 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
```jsonstarts 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_startednow emits inside the try that
guarantees its pairedfinished, so a cancellation while queued for a slot
can't strand the parent phase atdelegating. - Deps:
openai>=1.26.0(floor forstream_options); anthropic floor
already adequate (messages.streampredates it). - Also fixed a foreign test's process-wide env leak
(test_email_autonomy_gate.pydrove the real_run_email, whose
CORRESPONDENT_ACCESS_ENABLEDsetdefault 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 newastream_agent_response(Anthropic + OpenAI),
LLMClientAdapter.astreamyields REAL per-token deltas instead of the
legacy one-blob chunk — the CLI ResponseBox / webviewstream_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 SDKtext_deltaevents
then parsesget_final_message()with the same block parser; OpenAI uses
stream=True+stream_options.include_usagewith 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_outputfunnel, 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-requiredtask
state (back toworkingon resolution) with the action name in the status
message;tasks/getresponses carrymetadata.current_activity(the same
RunActivity snapshot as the session-status API). - OpenAI-compat:
stream: truestays buffered (P5 is the token-streaming
upgrade) but the agent turn now runs concurrently with the SSE body,
emitting spec-legal: keep-alivecomment 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
indocs/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_startedshows
"running…" the moment a tool dispatches; approval/retry/compaction/
sub-agent/delegation render as compact one-liners instead of raw-JSON
generic cards). /pending+/autonomyauto-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
prefprogress.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-agnosticTurnProgressTracker
(agents/task/telemetry/live_progress.py) + a multi-subscriber feed-callback
seam (ProductTelemetry.add_feed_subscriber— the CLI's single
_on_feed_entryslot 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_ENABLEDgate, fail-open):
compaction_started/finished...