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(emergency prune + LLM compaction),
retry_wait(all five backoff sleeps in the step-error handler, with
reason/delay/attempt/provider),subagent_started/finished(mirrored into
the PARENT session's feed with goal preview + duration), and
delegation_dispatched/completed(background delegation lifecycle — the
dispatch-to-terminal invisibility gap). Provider failover events
(provider_failure/provider_fallback_success) gained CLI +/activity
renderings (they reached only the webview before). - CLI: sub-agent + delegation + provider-failover lines render in the
default view; compaction/retry are bar-visible states (✱ compacting (llm),
✱ retry (rate_limit) 8s) with trace lines under/verbose— all via the
EventSpec registry seam. RunActivitysnapshot: a per-session phase machine (idle | thinking | tool | awaiting_approval | compacting | retrying | delegating | done)
derived at the ONE feed choke point (never at emit sites), exposed as
current_activity(phase/detail/seconds_in_state/step/call_id) on
GET /api/task/sessions/{id};nullfor unknown/remote sessions.polyrob session tail <id> --follow: the command's docstring finally
tells the truth — keeps streaming new feed events live (ordered,
dependency-free seq-file poll), so a second terminal can watch any running
session, including goal/cron runs.
2026-07-18 — Live run-state observability P0 (proposal 019): no more dead air
- Span/wait feed events (gated
RUN_EVENTS_ENABLED, default ON, fail-open):
tool_startedfires the moment a tool is DISPATCHED (multi_act),
llm_startedthe moment an LLM call begins, and
awaiting_approval/approval_resolvedbracket the approval-provider wait —
so a long tool, LLM latency, or a blocked approval is visible live instead of
silent. Tool spans join start→completion via a newcall_idon both events
(LLM tool-call id, else a synthesized per-batch id). - CLI: the
→ name(args)line now prints at dispatch time (paired
completion prints only the✓/✗result line; unpaired completions keep the
legacy two-line form byte-identically). The status bar's tool segment became
a live current-activity segment with a ticking clock (→navigate 43s,
✱ thinking 8s,⏸ approval: send_email /pending); a blocked approval also
prints a full-width notice (never muted by/quiet).polyrob run's live
activity line shows the in-flight tool. - Loud degradation: when TelemetryManager init fails, the orchestrator's
no-op fallback now (a) swallows ANY capture method (__getattr__— no more
AttributeError for newer captures), (b) pushes a visible error line through
the CLI feed callback ("live activity unavailable"), and (c) sets
telemetry_degraded.polyrob doctorgained a live-activity pipeline check. - Webview:
/activitysummarize() branches for all four kinds (per-session
view renders them automatically); a no-dark-kinds contract test pins CLI +
webview + formatter coverage for every run-event kind.
2026-07-18 — Config control plane (proposal 018, P0–P5)
- Honest
/configpanel: unconfigured keys show the real built-in default
(flags-catalog + posture-aware) instead of a wall ofNone (default);
advisory keys (style.*) are labeled; every enforced pref key is
ratchet-tested to have a real enforcement-site consumer. - 4 dead pref keys wired:
goals.notify_on_done,autonomy.self_wake,
autonomy.background_review,outbound.max_new_recipients_per_daynow
actually enforce (tighten-only merges preserved). digest.quiet_hoursenforced: proactive sends inside the window are
durably held and released at window-end (5-min autonomy-runtime ticker);
interactive replies unaffected.core/config_service.py: one describe/explain/search/set control plane
over prefs + the ~409-flag catalog; provenance chains
(git config --show-originstyle); secrets never readable back; writes
route to the existing stores only.- CLI:
/config explain KEY,/config search QUERY, full argument
completion (subcommands/keys/enum values), and bare/configopens an
interactive settings picker (reuses the /model ReplPicker; Enter seeds a
ready-to-sendsetcommand, bools pre-toggled). - Webview:
GET/PATCH /api/webgate/config*— search/explain/set for both
namespaces; env-flag writes gated to local/own_ops owner postures. - Agent self-awareness:
<environment>block shows the CLAMPED autonomy
mode + both axes + the loaded-tool list;agent_statusgainsmode=; the
preferencesaction gains read-onlyexplain;self_envhard-denies
core/config_policy/source. - Hardening:
core/env.float_env; import-frozen numeric flags no longer
crash the process on a straynonevalue; raw numeric-parse ratchet (65,
shrink-only).
Structural F-2: god-file split — webview read-services + ratchets (2026-07-17)
- Changed: the webview console now sources a new session's default tools from
the/api/task/capabilitiesdefault_toolspayload (the
agents/task/tool_defaults.pySSOT) instead of the hardcoded
['browser','filesystem']inchat.js; the static list is kept only as a
last-resort fallback if the endpoint is unreachable, so session creation never
breaks (R-6). - Changed (refactor, no behavior change): the four pure feed-reading handlers
api_agents(multi-agent roster),api_services,api_taskandapi_skills
moved out ofwebview/server.pyinto shared read-services in the agents tier
(agents/task/telemetry/agent_graph.py::build_session_agents,
feed_reads.py::build_session_{services,task,skills}) so the console, CLI and
HTTP API can reuse them; the routes are thin wrappers, logic copied verbatim,
with 20 new characterization tests.webview/server.py4316 → 3758. - Added:
tests/test_file_size_ratchet.py— a shrink-only line-count ceiling
for the five F-2 god-files +policy.py; new behaviour must go in a new module,
not grow these, and a split lowers its row.
Structural F-3: fresh-eyes final sweep (2026-07-17)
- Removed: dead code —
SubAgentManager.get_file_lock/get_api_limiter+
their class dicts (zero callers anywhere); the deprecated
SessionOrchestrator.get_workspace_dir()async shim (zero production callers;
the syncworkspace_dirproperty is the one accessor); two zero-importer
re-export shims (agents/task/flag_defaults.py,surfaces/email/seed.py). - Fixed: the PydanticDeprecatedSince20 warning cluster — core/config.py's 7
inertField(env=)→alias=(loading was already by field-name matching;
byte-identical), 10 v1@validator→@field_validator
(tools/mcp/config.py, api/mcp_models.py; parity verified),class Config→
ConfigDict(modules/llm/adapters.py, api/a2a/agent_card.py), deprecated
json_encodersdropped (api/models.py, modules/memory/models.py).
BotConfig()now constructs with zero deprecation warnings. - Changed: the four
_int_envre-implementations now delegate to the ONE
parsercore.env.int_env(verified behaviorally identical first). - Adjudicated (no code change): the H-MEM "~650 dead LOC" claim retired
(subsystem is production-wired); the P4 async-initialize refactor is
won't-fix (AGENTS.md updated); theagents/task/constants.pymass shim-flip
is won't-fix (hybrid module, 78 production importers of task-tier symbols);
thecore/config_policy/policy.py9-submodule split is scoped and deferred
to its own session; prod sidecar relocation VERIFIED live (db_relocated
2026-07-17 14:19Z) — the legacy read-both fallback is removable one release
later.
Structural F-1: rate limiters consolidated onto core/rate_limit.py (2026-07-17)
- Changed: the six in-process rate-limiter forks (three algorithms) are now
configured instances of ONE canonical module,core/rate_limit.py:
SlidingWindowLimiter(MCP exec —tools/mcp/rate_limit.pyis a back-compat
shim; user MCP admin; the public x402 invoice throttle; the webview
connection/event throttles;RateLimitManager's internals),TokenBucket
(moved fromcore/surfaces/rate_bucket.py, now a re-export shim; the api
middleware burst gate), andFixedWindowCounter(the api middleware's
minute/hour windows). Decision semantics are pinned by characterization tests
written FIRST against the legacy implementations (27 tests across the three
previously untested forks + RateLimitManager) and pass unchanged after the
consolidation.surfaces/telegram/rate_limit.pystays separate by design (a
RetryAfter penalty tracker, not a request-budget limiter). - Fixed: the webview per-IP connection tracker no longer grows one key per
client IP forever — it now shares the same bounded-LRU key space
(max_keys=5000) the per-session event limiter already had (E5/WS-4
precedent; rate-limit semantics for active keys unchanged). - Added:
tests/test_rate_limiter_ratchet.py— a shrink-only allowlist scan
that fails on any NEW limiter-shaped definition outside the canonical module.
Structural remainder R-2: DB locations are honest (2026-07-17)
- Fixed:
DB_PATHis real. It was a decoy — config anchored it, created its
parent directory, andpolyrob updatesnapshots trusted it, but the app always
opened the hardcoded<data_dir>/database/bot.db. The default now matches
reality (data/database/bot.db) anddatabase_managerhonors an explicit
DB_PATHbehind a refuse-to-guess guard: if the real database still sits at
the derived location and the configured path doesn't exist, startup raises
with the exact move instructions instead of silently opening a fresh empty DB. - Fixed:
telemetry_events.dband the opt-inmessages.dbmirror moved to
the data-home axis (core.runtime_paths.sidecar_db_path, read-both/write-new).
They previously lived under the SESSION artifact tree (<data_home>/sessions/
on prod-shaped installs) while the backup manifest expected<data_home>/<name>
— sopolyrob updatesnapshots silently missed the live files. Snapshots also
capture the legacy files explicitly until relocation. - Added: a one-shot, clobber-proof boot relocation
(core/sidecar_relocate.py) moves an existing legacy file to the data home on
the first telemetry touch per process, audited via the newdb_relocated
telemetry event kind. Fail-open — any error keeps the read-both fallback.
Structural remainder R-4: core/security promotion + layering inversions (2026-07-17)
- Added:
core/security/— the tier-0 home for the security primitives:
secret_guard(secret/credential path detection),untrusted_wrap
(prompt-injection DATA framing), andforged_turns(forged-turn kind
constants). The oldagents/task/agent/core/paths remain as re-export
shims; tools/controller importers now use the core home. Importing these
modules pulls zero upper-tier code (pinned by tests). - Changed:
core/surfaces/inbound_webhook.pyno longer imports the surface
tier —core/surfaces/act.pyownsInboundResult+ an actor-registration
seam;surfaces/telegram/harness.pyregisters the shared dispatch at import. - Changed:
modules/x402/middleware.pyno longer importsapi.auth_state—
api/app.pyinstalls the auth-state writer at mount
(install_auth_state_writer). - Added: a 5-tier import-boundary ratchet (
tests/test_layering_ratchet.py)
seeded with the 126 existing upward edges, shrink-only; the core→agents
allowlist tightened 35→34.
Structural remainder R-1: one canonical .env precedence (2026-07-17)
- Added:
core.paths.env_file_candidates()— the single source of truth for
which.envfiles configure the process and in which precedence order.
load_env,/config check, the CLI first-run guard, and thepolyrob update
snapshot all derive from it now (layering behavior unchanged). - Changed:
polyrob updatesnapshots additionally capture the legacy
~/.rob/.envtransition fallback (it can still hold live keys via the
read-only fallback layer), so a rollback restores the whole user env state.
Money ledger: two statements, never summed (2026-07-16)
The daily digest and the accounting//status views used to merge the owner's LLM/API
bill into the agent's own wallet spend before computing one "net" figure — on
2026-07-16 this told the owner "earned $0.00, spent
agent's wallet sat untouched at $10 USDC (the $2.47 was 100% API cost, none of it the
agent's own spend). The ledger now shows two statements that are never added together:
- Treasury — the agent's own money (USDC): income, spend, pending invoices, and
net = income − spend. Runtime/API cost never enters this figure. - Runtime cost — the owner's money (compute): window + lifetime spend and call
counts. It has no "net" — there is nothing to net compute cost against.
"Earned" is retired in favor of income/spend, and the old merged fields are gone with
no fallback — a caller still reading the merged figure will error instead of silently
showing a wrong number again. A balance is only ever shown when the provider actually
exposes one; unknown now renders as omitted, never a misleading $0.00.
- Removed: the autonomy budget gate —
AUTONOMY_BUDGET_USD,
AUTONOMY_BUDGET_WINDOW_DAYS,BUDGET_AWARE_AUTONOMY, and the
budget.autonomy_daily_usdpreference/onboarding prompt. It was a $10/day rate
ceiling that can't protect a finite balance — the agent was under budget every single
day while the provider balance ran to zero — and it gated on the merged figure above,
so an x402 wallet payment could eat into the agent's compute budget. Every
wallet-spend cap (WALLET_DAILY_CAP_USD, venue caps, payment approval mode,
correspondent-taint, x402 invoice caps) is untouched. Nothing now throttles burn rate
on its own — the provider's 402 and the credit sentinel are the backstop. - Fixed: the credit-death sentinel could only trip from a cron or goal run, so an
interactive chat that hit a real provider 402 — the one place the owner would
actually notice — never latched it. There is now one universal trip site reached
from every run path, interactive included. - Fixed: a failed Telegram-bound run used to go silent — the "already delivered
live" skip assumed the run had succeeded. A failed run now always tells the owner.
Structural-wave verification & completion pass (2026-07-16)
Cleanup-and-completion over the WS-1..WS-7 wave; zero behavior change except
where marked.
- refactor(core): the data-home resolver trio is ONE rule — verified
POLYROB_PROJECT_DIRnever changed the data-home value (workspace placement only), so
core.bootstrap._resolve_cli_data_homeandcore.runtime_config.get_data_rootnow
delegate tocore.runtime_paths.resolve_data_home(three-way parity test added). - refactor(core):
core/tool_catalog.py's second hand-classification folded into the
WS-2 capability module —TOOL_PERMISSIONSlives beside the capability rows and the
catalog risk tiers are DERIVED (high = external-write permission, medium = high_impact
without one), memberships parity-pinned.VALID_TOOL_IDS(skill_manager) is now derived
from the capability table (verified set-equal first); the T12 vocabulary test, made
tautological by that derivation, now checks gate ids against the independent
registry-side vocabulary. - refactor(config): WS-1 phases 3–4 landed — the 32 core-adjacent consumer files
(tools/, cron/, modules/) import fromcore.config_policydirectly (only the five
shim-tail-symbol imports remain onagents/task/constants); theflag_defaultsbridge
moved tocore/config_policy/flag_defaults.py(old path re-exports); all 15 core lazy
config_policy imports promoted to top level with their fail-open/fail-closed guards kept
on the calls; 12 never-referenced underscore re-exports + a deadimport logging
trimmed from the shim; stale pre-wave comments corrected across core/tools/modules. - fix(paths): cli/commands' remaining 22
or "data"fallbacks (a latent CWD write
when no container/config is present) now route throughdata_dir_or_home(); the path
ratchet's single-quote blind spot closed (5 hiddenaction_registration.pysites fixed,
patterns extended); 13 ratchet baseline rows deleted,handlers.py7→2. - docs(flags):
DELEGATE_BLOCKED_TOOLScatalog row fixed (11 → 15 ids; derivation +
live anchors noted); flags catalog + user-guide refs regenerated. AGENTS.md now
describes the derived gate sets andcore/tool_capabilities.py. - fix(tests): the 4 order-dependent failures are gone — TWO root causes: (1) tests
building a CLI container imported the dev box operator's REAL env files
(~/.polyrob/.envkeys, legacy~/.rob/.envprovider pins,config/.env.production
backfill) intoos.environ; the suite now disables the backfill session-wide and a
narrow per-test guard restores the operator-var set (provider pins, owner binding, API
keys, and the frozen-security flagspolyrob initapplies in-process) — deliberately
NOT a home redirect, which shadowed tests that isolate viaPath.home.
(2)test_ledger_error_fails_closedasserted outside its broken-ledger patch and
depended on a fresh-process ledger failure; the assertion moved inside the patch.
Revalidation fixes — pre-existing main failures (2026-07-16)
Found by the full-suite revalidation pass after the WS wave; each verified pre-existing
at 370843bd before fixing.
- fix(llm):
modules/llm/adapters.pyimported the four provider client modules at top
level, so EVERY entry-point import (eachpolyrobCLI invocation, every uvicorn worker
boot) eagerly loaded the anthropic + openai + google.generativeai SDKs. Now
TYPE_CHECKING-only (the classes were annotation-only there);cli.polyrobimports zero
heavy SDKs andtest_import_layeringis green. - fix(docs): recovered 10 lost plan/review docs from git history (referenced by
committed docs but only ever present in the shared working tree); the three never
committed anywhere are grandfathered with an audit trail.test_doc_consistencygreen. - fix(tests): the nginx deploy guards read the retired
deploy_unified.shtombstone;
they now encode the same invariants against the live deploy surface
(deploy_webview.shinstalls the ownops vhost; no live script installs the demoted
proxynginx.conf;/opt/polyrobanchor). - Known-not-fixed (documented): 4–5 order-dependent test failures (
test_identity,
test_budget_gate,test_goal_dispatcherchild-tools,chat_resolver_parity,
protected_config_guard) — all pass in isolation; root cause isload_envimporting
the dev box's real~/.polyrob/.envintoos.environmid-suite. Needs a suite-wide
env-sandbox fixture (own change, own blast radius).
Structural upgrade WS-2..WS-7 slices (2026-07-16)
Same-day continuation of the WS-1 wave; every item ratchet- or parity-tested.
- WS-2 (tool capabilities): ONE per-tool capability table (
core/tool_capabilities.py;
orthogonal dimensionsmoney/high_impact/delegate_blocked/exec/
readable_while_tainted).MONEY_TOOLS,DELEGATE_BLOCKED_TOOLS(env override kept) and
HIGH_IMPACT_TOOL_IDSare now derivations, byte-identical memberships parity-pinned;
register_optional_toolrefuses an unclassified tool, so a new tool can never silently
skip every gate. Verb-level sets stay hand-curated at their gates (T12 keeps them in sync). - WS-3 (paths):
core/runtime_paths.pygainsdata_dir_or_home()/
goals_db_path()/cron_db_path(); ~25 sites that fell back to a relative"data"
(a latent CWD/install-tree write) now resolve the data home, incl.skill_usage's
repo-root anchor, both browser screenshot fallbacks (+ session-id cleaning, also in the
trace filename), and 9 operator scripts' hardcodeddata/*.db//var/lib/polyrob/*
argparse defaults. New ratchettests/test_path_ratchet.pyfreezes the remaining
constructions per-file, shrink-only. Deferred with notes:bot.db/messages.db/
telemetry_events.dblocation moves (need a data migration) and the.env-candidates
helper. Tenant-dir conventions documented (core/instance.py::self_tier_root) — two
deliberate grammars, one per path axis, not to be unified on disk. - WS-4 (rate limiting): two real leaks fixed —
api.middleware.RateLimiter's
_cleanup_old_bucketswas never called (per-user dict grew for the process lifetime on a
network-facing surface; now amortized-swept), and the canonical
core/surfaces/rate_bucket.TokenBucketnow prunes fully-refilled idle keys
(exact-semantics eviction). Full 6-fork consolidation deferred: it changes throttling
shape (token bucket vs sliding window) and three forks have no characterization tests. - WS-5 (layering edges):
cli/gitignore.py→core/gitignore.py(shim kept), killing
core→cli;core/initialization.py's dead top-levelagents.personalityimports deleted
(layering-ratchet allowlist tightened 37→35). - WS-7 (SSOT tail):
api/openai_compat/model_map.pyresolves a bare registered model
slug via registry membership (grok/glm/kimi no longer misroute to the env default);
scripts/seed_goal.pyand the telegram owner-interactive toolset now source from named
TOOLSETSentries (earn,owner_interactive).
WS-1 — config-layer relocation: core↔agents.task cycle broken (2026-07-16)
Deep structural wave following T1–T12.
- refactor(core): relocated the cross-cutting autonomy/mode/posture/payment-policy cluster +
AutonomyConfig(≈1230 lines) fromagents/task/constants.pyinto the new core-tier package
core/config_policy/(policy.py).agents/task/constants.pyre-exports every public and
externally-referenced private symbol unchanged, so all ~126 importers are byte-compatible; new
code should import fromcore.config_policy. Added areset_autonomy_mode_warnings()test seam. - refactor(core): flipped all 15
core/ → agents.task.constantsback-edges to
core.config_policy, soimport core.config_policypulls zeroagents.*modules — the
core ↔ agents.task.constantscycle is one-directional (agents → core) at last. - test(core): added
tests/test_layering_ratchet.py— banscore/imports of
agents.task.constantsand enforces that the remainingcore→agents.*edges (WS-1 phases 3–4 +
WS-5 targets) may only shrink.
Structural cleanup wave T1–T12 (2026-07-16)
Twelve fixes from the 2026-07-16 four-way structural audit (duplication / path handling /
layering / sources-of-truth).
- T1 (data-loss fix): 10 sidecar DBs (
slack/signal/discord/xdedup,wa_window,
group_allowlist,conversations,outbox,surface_state,deployed_apps) registered in
core/db_manifest.py—polyrob updatebackup/rollback silently skipped them. Grep-based
completeness contract test added. - T2:
/capabilitiesno longer advertises the deprecatedx-ai/grok-4.1-fast— default
model now comes fromllm_client_registry.get_default_model(env-overridable). - T3:
credit_sentinel's fallback path resolution followsresolve_data_home(dropped its
uniqueDATA_ROOTprecedence — the spend/halt gate could latch in the wrong tree). - T4: the fail-CLOSED identity-scan write gate is ONE base-class implementation for all
three identity-doc writers (self/contract/owner) — was copy-pasted ×3 (security-drift hazard). - T5:
VALID_TOOL_IDScovers all registrable tools (shell,process,self_env,
hf_deploy,github,x402_pay,alchemy,collablandwere rejected as invalid);
registry-parity contract test added. - T6: one canonical telegram recipient resolver
(user_delivery.resolve_telegram_recipient); cron delivery delegates and its no-sink case now
leaves a durableowner_noticeinstead of a silent drop. - T7: deleted dead
modules/database/connection_pool.py(zero importers, divergent PRAGMAs). - T8:
core/activity_evidence.py— one ledger/episodes evidence layer shared by the owner
digest andpolyrob recap(numbers can no longer diverge). - T9:
core/event_kinds.py— SSOT for all 33 durable event-logkindstrings + a producer
contract test; activity feed / spend rollup / digest consume the constants. - T10: a bare
PathManager()routes throughresolve_session_data_root(closes the RC-1
"two session trees" landmine —DATA_ROOT-only default). - T11: runtime logs resolve to
<data_home>/logs(newPOLYROB_LOG_DIRoverride) instead
of the install tree; packaged/read-only installs can log. - T12: cross-consistency contract tests for the six dangerous-tool gate sets (money ⊆
delegate-blocked, correspondent-gate coverage incl. namespaced trade verbs, gate ids ⊆ tool
vocabulary, verb-substring sync).
AUTONOMY_MODE — single-owner capable-by-default master switch (2026-07-16)
Proposal 013 (owner directive 2026-07-15): the recurring "session has no web_fetch/twitter",
"planner: REAL BLOCKER tool unavailable", and "can't approve emails to addresses we don't know"
stalls were all one disease — the framework is deny-by-default and treats missing-permission as
a hard wall. One master switch, AUTONOMY_MODE=supervised|autonomous (never "yolo"/"unleashed"
in code, flags, or docs), makes a genuinely single-owner instance capable-by-default without
touching money-spend, host access, or secrets. supervised (default/unset) is byte-identical to
pre-013 behavior; autonomous is only effective on a single-owner deployment (POLYROB_LOCAL +
a bound owner principal via POLYROB_OWNER_USER_ID/_TELEGRAM_ID/_EMAIL) — otherwise it
clamps back to supervised with a one-time WARN, so a multi-tenant server can never drift into
it.
- Capability-flag groups default ON under effective autonomous mode
(_mode_capability_default):TWITTER_ENABLED,MCP_ENABLED,GROUP_CHAT_ENABLED,
EMAIL_SURFACE_ENABLED,X402_INVOICE_ENABLED(receive-side only),
MESSAGE_AUTONOMOUS_ALLOWLISTED,CORRESPONDENT_ACCESS_ENABLED,
CORRESPONDENT_REPLY_ENABLED— wired at every consumer seam (core/config.py's MCP gate,
modules/eip8004/registration.py,core/surfaces/access.py's group gate,
modules/x402/invoicing.py+core/autonomy_runtime.pyfor the invoice tool/settlement
watcher pair), plusCORRESPONDENT_REQUIRE_APPROVALinverted (defaults OFF under autonomous).
An explicit per-flag env always wins. - Autonomous toolset —
AUTONOMOUS_MODE_TOOLS(never money-spend/host) is granted to a bare
session, the goal dispatcher's default toolset, the planner's session-tools (+web_fetch), and
the Telegram interactive toolset, all gated onfull_autonomy_enabled();VALID_TOOL_IDS
gained the vocabulary needed to express the grant. - Two-lane approvals — a new
auto_notifyprovider (allow +tool_auto_approvedaudit event- post-hoc owner notification — "act-and-report") becomes the default under autonomous mode for
an unset/auto/interactive_cliAPPROVAL_PROVIDER. A fixed always-owner-queued lane
(_ALWAYS_GATED_VERBS: the fourself_env_*verbs,mcp_install, plus the aspirational
self_modify/tool_manage) never moves toauto_notifyregardless of mode.hf_deploy's
first-publish mapsauto_notify → owner_queue(a public HF Space is not something to
act-and-report after the fact).
- post-hoc owner notification — "act-and-report") becomes the default under autonomous mode for
- Outbound policy ladder —
OUTBOUND_POLICY(open|domains|allowlist|off, default
allowlist,openunder autonomous mode) replaces the per-address ACL with a policy+cap model
(resolve_outbound_policy, fail-closed), enforced at the send gates (cap → seed → send →
record → report),OUTBOUND_DAILY_SEND_CAP(default 30) as the first live reader of
outbound_count_surface_since, and a first-contact report that fires only for open-tier sends.
Deviation from the original plan:outbound.domainsmerges via a newnarrow_listkind
(the pref can only intersect a non-empty operatorOUTBOUND_DOMAINSenv, or define the set
from scratch when the env is empty) — the plan's specifiedunionmerge would have let a
tenant pref widen past an operator-set domain allowlist, inverting its polarity;
narrow_listis the corrected, allowlist-safe behavior. - Receive-side auto payments, spend stays gated —
PAYMENT_APPROVAL_MODEdefaults toauto
under autonomous mode, but only forPAYMENT_RECEIVE_APPROVAL_TOOLS = ("x402_request",).
Deliberate hard line: the four live-trade spend verbs
(hyperliquid_place_limit_order/_market_order,polymarket_place_limit_order/
_market_order) keepowner_queuepre-approval under both modes, including an explicit
auto— trading is never act-and-report, closing a gap the initial cut left open (013 T7
review). - Tool-availability transparency —
TOOL_AVAILABILITY_HINT(default ON) injects a
<tool-availability>prompt block (GATED_TOOL_REGISTRY) disclosing every
known-but-not-loaded tool with its gate + remedy, so a missing capability is always named
instead of guessed at or used as an excuse; the goal planner's "TOOL GROUND TRUTH" block reuses
the same registry. - Artifact-existence stamping — goal/planner prompts now stamp titles/bodies/acceptance
criteria/past-failure text with[present, N bytes]/[MISSING on disk]against what's
actually on the workspace (containment-safe, symlink/traversal-safe boundary lookbehind), plus
a planner escalate-once instruction after repeated identical blockers. /config+ visibility — a Telegram/configcommand (guarded-set → owner-approval
proposal),autonomy_mode_display()surfaced in/status,polyrob owner show, and
polyrob doctor, matching pending-review parity in the webview, and an autonomy/prefs section
inpolyrob config show.
Money-spend, host access (AGENT_COMPUTE_POSTURE), and secrets are untouched by this mode in
either direction — see docs/CONFIGURATION.md's AUTONOMY_MODE section for the full flag
table. Rollout to prod (T12) is owner-gated and not part of this wave.
Capability completion — exec everywhere it should be, agent knows where it lives (2026-07-16)
Proposal 014 (from an incident investigation): closes the
session-entry toolset gaps 013 left, makes the posture≥1 dev sandbox Node-capable, and
gives the agent an in-context answer to "where do I live". Everything is gated — a
deployment with nothing set is byte-identical.
default_session_tools()SSOT (agents/task/tool_defaults.py) — the three drifting
['browser','filesystem','task']literals intask_agent_lite.pynow route through one
helper; under effectiveAUTONOMY_MODE=autonomousa bare session gets the ambient
autonomous grant (never money-spend/compute — those are structurally absent).- Telegram interactive toolset is mode- and posture-aware
(surfaces/telegram/interactive_tools.py) — the owner chat under autonomous mode gets
the fullAUTONOMOUS_MODE_TOOLSgrant (keepinggoal/cronjob), plus
code_execution/shell/codingatAGENT_COMPUTE_POSTURE>=1via the new
with_compute_tools()SSOT (the goal dispatcher now shares it).INTERACTIVE_TOOL_IDS
still always wins; supervised default unchanged. CODE_EXEC_DEV_IMAGE(defaultnikolaik/python-nodejs:python3.11-nodejs20) — the
posture≥1 persistent dev container defaults to a python+node image so npm/npx toolchains
work; the confined ephemeral sandbox keepspython:3.12-slim; explicit
CODE_EXEC_DOCKER_IMAGEwins everywhere.run_code(packages=)honors the effective sandbox network — the gate now probes
DockerBackend.effective_setup_network()instead of the raw env, so a dev container
that auto-bridged (env unset) is no longer wrongly refused pip installs; explicit
CODE_EXEC_NETWORK=nonestill refuses.- Dev-mode exec timeout ceiling is 120s (was silently 30s) — aligns the backend clamp
with the shell tool's foreground contract; explicitCODE_EXEC_MAX_TIMEOUT_SECwins;
confined default stays 30s. <environment>foundation block (agents/task/agent/core/env_context.py, flag
ENV_CONTEXT_BLOCKdefault ON) — instance, platform, data dir, absolute workspace path
with explicit persistence semantics, posture/mode axes, and a host-executable probe,
pinned after runtime identity. Emits only underPOLYROB_LOCALor effective
AUTONOMY_MODE=autonomous; multi-tenant server sessions unchanged.
Wallet / crypto security hardening wave (2026-07-15)
A 7-way security + UX review of the wallet/x402/trading stack (2026-07-15)
followed by a same-day fix wave: 2 Critical and all
14 High findings closed, plus most Medium/Low.
- C1 — pay-side fund-drain closed: the x402 payment gate now authorizes at the
tool-callmax_amount_usd(not the advisory quote) AND re-checksPolicyGate
against the REAL challenge amount before signing; the reserve is held across the
whole check→pay→record span. - C2 — wallet CLI reads the right env:
polyrob wallet/set-capload the local
env before reading the wallet (no more acting on a phantom empty wallet). - Owner kill-switch exists (H5/H6):
polyrob owner halt/resume— a structural
halt enforced insidePolicyGate.check, invoice minting, renewals, and live-trade
gates; the halt probe fails CLOSED. - Turn-origin money gates (H10/H11): live orders and NAMESPACED crypto trade
verbs are blocked from forged/autonomous/correspondent-tainted turns; every
mutating trade verb is origin- and halt-gated. - Custody hardening (H1–H3): the wallet derivation scheme is pinned alongside the
seed (a legacy wallet can never be silently re-derived),resolve_schemefails
fast on corrupt meta, wallet policy files are credential-guarded, and the audit
sink is tamper-evident. - Approval integrity (H4): one approval = exactly one execution; a forged
approval probe fails closed. - Settlement/invoicing hardening (H7–H9 + M-class): shared-DB and
settlement-watcher races closed; snapshots now capture the wallet dir and deny
renamed env copies (M1/M2); unpriceable/non-finite order values fail closed (M10). - Owner-facing money truth (H12–H14): the agent's money self-knowledge corrected,
polyrob financeworks standalone, wallet view/export fail friendly on a bad
seed, andpolyrob doctorverifies the wallet actually works before reporting
"on".
Onboarding finalization — wallet, avatar, identity (2026-07-14)
Closes out the onboarding-finalization wave: the agent wallet is now a one-command,
portable, exportable thing instead of a bare env var, and the avatar/setup surfaces
catch up to it.
polyrob wallet init— generates a fresh 24-word BIP-39 mnemonic (shown once) or
imports one (--from-mnemonic) or a legacy raw seed (--from-seed); writes
AGENT_WALLET_ENABLED/AGENT_WALLET_MASTER_SEEDto~/.polyrob/.env(chmod 600) and
offers to pointX402_PAYMENT_RECIPIENTat the new treasury address so earnings settle
somewhere spendable. Testnet prints faucet guidance; mainnet prints USDC-on-Base
guidance.polyrob wallet export [--venue]— TTY-only, typed-EXPORTconfirmation reveal of
the mnemonic (bip44) or per-venue0x-hex private keys; never agent-callable.- Versioned key derivation — a wallet's scheme (
legacyPBKDF2 orbip44BIP-44,
m/44'/60'/0'/0/{treasury,x402,polymarket,hyperliquid}) is recorded write-once in
data/wallet/meta.jsonbywallet init/import; a pre-existing wallet with no meta file
is legacy FOREVER — addresses never change. New wallets getbip44(mnemonic imports
cleanly into MetaMask/Rabby).AGENT_WALLET_DERIVATIONis a recovery-hatch env override
for a corrupted/missing meta file only. /pfpREPL command (alias/avatar;status|generate [force]|show) — the avatar
stays fully optional (nothing auto-generates it); this makes generating/inspecting it
discoverable without leaving the chat REPL.polyrob initbridges from the inline key wizard — after the first-run key prompt
saves a usable key, it now offers "Finish full setup now (model, persona, autonomy —
~1 min)?" and runsinit --skip-keyson accept instead of leaving the operator with a
bare key and nothing else configured;initalso gained an optional agent-wallet
opt-in step (default No) and a "Next steps" block (wallet / avatar / surfaces / identity
/ doctor).polyrob doctorsetup-completeness lines — wallet/avatar/surfaces/SOUL status,
gateway-gate-accurate (a flag-on-but-uncredentialed surface reads as configured-but-
incomplete, not silently "off").ui.show_avatarpreference — a per-owner toggle for whether the webview identity
page renders the avatar./model set-defaultSSOT fix (G11) — now keepsDEFAULT_PROVIDER/DEFAULT_MODEL
env pins in lockstep with the CLI preference store, instead of drifting apart.polyrob soul init(O10) — scaffolds the operator-authored SOUL identity docs
(identity/identity.md+identity/operating.md) so authoring the richer identity
layer has a discoverable onboarding path instead of requiring hand-authored files.polyrob-user-guideskill v2 — addsreferences/wallet-and-identity.mdand
regeneratesreferences/configuration.mdfrom the currentdocs/CONFIGURATION.md
(also absorbs theAGENT_WALLET_DERIVATIONrow andMESSAGE_AUTONOMOUS_ALLOWLISTED
from a concurrent change).- Docs —
docs/CONFIGURATION.mdgains theAGENT_WALLET_DERIVATIONrow;
docs/guide/payments.mdgains "Create the wallet in one command",
"Portability, backup & export" (with the snapshot-contains-seed caveat), and
"Migrating to a new install" sections;docs/guide/getting-started.mddocuments the
inline first-run key wizard + its full-setup bridge, fixes the stale ASCII-box
"Example Session" banner to the real two-line banner, and completes the config-layers
table (config/.env.*, legacy~/.rob/.env).
Update/infra/onboarding hardening — Wave 3 (2026-07-14)
Completes Wave 3 of the 2026-07-14 review (all remaining P2s except the owner-action
secret rotation).
polyrob gatewaylaunches every surface (H2) — Discord/Slack/Signal/X now start
under the gateway when their flags are on (previously silently ignored); an enabled
surface with missing credentials is WARNED about and skipped.SurfaceConfiggains
the four flag helpers; stale gateway caveats removed from the migration guide.- Inline-schema == migration-HEAD contract (U4) — a new CI test builds a fresh DB
through the real component creators, stamps at HEAD via the real boot path, and
requires every shipped migration'sverify()to pass. It immediately caught a real
drift:billing_failures(v1_3_0) had no inline creator — fresh installs never
created it and billing-failure records silently failed to insert (now mirrored into
AuthTables). The dead legacy schema initializers inconnection.py(home of the
singularschema_versiontable) and the orphanscripts/migrate_*one-offs are
deleted (U11). - Doctor env checks (U10/O6) — Python ≥3.11 floor,
[server]-extra presence,
Playwright chromium probe, and a DB-schema-vs-code line (also printed by
polyrob update). backup_database.sh(U7) — now a WAL-safe all-DB snapshot via the update
engine's Online-Backup path (was:cpof a live WAL DB at a path that no longer
exists), restorable viapolyrob update --rollback.- In-use guard portable (U8) — the process scan is
/proc→ psutil →ps, so
macOS--apply/--rollbackno longer bypasses the guard silently. - Prod venv rebuild procedure (D12) — DEPLOYMENT.md documents the
requirements.txt-first rebuild (barepip install -e .loses the extras-only
tweepy/eth-account that run on prod); a guard test pins both in requirements.txt. - Init polish (O2/O3/O4) — consistent 1/6..6/6 wizard numbering; the closing
"no usable key" check reads every env layerpolyrob runhonors; DeepSeek's key
prompt says it can't bootstrap alone. - Pairing approvable (O5) —
polyrob owner pair {pending,approve,revoke}ships;
core/pairing.pyno longer documents a phantom command. - Portal workflow self-gating (D11) — skips deploy (green) when Cloudflare
secrets are unconfigured, for whenever Actions is re-enabled.
Update / infra / migration-guide / onboarding fix wave (2026-07-14)
Implements Waves 1–2 (+ selected Wave-3 hardening) of a 2026-07-14 internal
review — the connective
tissue around the good engines: the migration runner, deploy paths, updater safety net,
first-run toolset, and the flagship docs.
- Migration runner survives self-recording migrations (U1, P0) —
migrations.migrate upgradeno longer double-records the schema version (IntegrityError→ exit 1),
which deterministically rolled back anypolyrob update --applycontaining a
self-recording migration. The upgrade loop is extracted to a testable
apply_pending_migrations()with recording guarded byis_version_applied. polyrob initno longer degrades the first run (O1, P0) —resolve_toolset ("default")now resolves to the true dynamic default (web_fetch + coding/anysite
additions), identical to an unsetPOLYROB_AGENT_TOOLSET; accepting the wizard
default used to silently dropweb_fetch, breaking the documented first task.- Deploy paths migrate the DB and the docs tell the truth (D1/D2/D3, P0/P1) —
deploy_unified.sh(destructive, dead api+webgate shape) is retired to a hard-exit
tombstone (legacy body preserved atdeployment/legacy/);scripts/deploy_prod.sh/
deploy_from_local.shnow runmigrations.migrate upgradebefore restart;
start_autonomyschedulesrun_boot_migrationsso every posture (telegram/REPL/
email/gateway) self-heals schema like the API lifespan;polyrob-email.serviceis
committed and restarted by both deploy scripts. AGENTS.md/DEPLOYMENT.md/
deployment/README.mdrewritten around the real headless shape (guard tests keep
them honest). - Updater rollback restores what it promises (U2/U9, P1) — snapshots carry a
scope(full|db_only); bare--rollbackprefers the newest FULL snapshot
instead of the DB-only pre-migrate one;--applytakes ONE snapshot (migrate_guarded
reuses it); same-second snapshot dirs no longer clobber. Systemd manual steps are
posture-aware (detectpolyrob*units +daemon-reload; no more phantom
polyrob-api), and the stale "automated apply not wired yet" messaging now
advertises--apply(U3/U6). - Migration guide un-staled (H1/H2, P1) — Discord/Slack/Signal/X marked
shipped (with honest validation status), compute-posture ladder replaces "not
supported", learning-loop claim fixed, and everypolyrob gatewaymention carries
the "doesn't launch Discord/Slack/Signal/X yet" caveat.tests/test_doc_consistency.py
guards the platform claims against re-diverging. - Provenance process fix (H4, P1) — the lost cross-agent-parity design record
was reconstructed; the 7 still-recoverable referenced
plan docs are committed; a contract test now requires any internal plan/review doc
referenced from a committed doc to be committed itself (21 already-lost files
grandfathered by name). - Onboarding docs (O8/O9/O10, P1/P2) — getting-started.md gains an "Updating"
section; instances.md corrects the SOUL doc location (flatidentity/*.md, NOT the
nested per-user dir — files placed there never loaded) and adds a SOUL authoring
guide. - Hardening (Wave-3 picks) — trajectory capture runs off-loop
(asyncio.to_thread); bulk datagen export reaches legacydata/auto/*/sessions/*
sessions;deploy_webview.shtakes its target from~/.polyrob/ops.envinstead of
a hardcoded host; stale ops-script headers corrected (D7/D9/D10/H5/H6).
Built-in ecommerce / payments finalization (2026-07-14)
The four separate money organs (x402 receive middleware, agent pay-side wallet, agent
invoicing, platform credits) are finalized into one coherent, owner-legible built-in
ecommerce capability: the agent can quote, invoice (text + branded QR image), get paid
(USDC, auto-detected), meter, deliver, and account for itself. Landed as 17 reviewed
tasks; all new behavior is behind default-OFF flags (a deployment that enables none
is byte-identical to before). Full reference: docs/guide/payments.md.
-
Truth & safety (P0). Metering now persists on a headless single-owner deploy —
an owneruser_profilesrow is seeded at startup (ensure_owner_profile), closing the
FK failure that made spend read a false$0. The autonomy budget gate and cron ticks
fail closed (a ledger error orautonomy_halted()holds dispatch, not runs). Pay-side
hardening: the wallet PolicyGate runs unconditionally, payment asset is pinned to the
canonical USDC for the configured network (defeats a decimals-spoof cap inflation),
network binding is fail-closed (V1 names + CAIP-2),success=falsesettlements are
treated as unpaid, and the kill-switch probe fails closed. Billing correctness: one
cost entry point (cache-write surcharge preserved), a realusage_records.request_id
column keyed on the provider response id for reachable retry dedup. -
Invoicing as a product. Branded QR invoice cards (
modules/pfp/cards.py, pure
Pillow +qrcode+ a shipped OFL font;INVOICE_CARD_ENABLED,INVOICE_QR_STYLE);
an outbound media leg (Telegram photos, email attachments,message(media_paths),
workspace-confined); free-formpayer_contact("billed to"); approval modes
(PAYMENT_APPROVAL_MODE=approvevia a durable, remotely-approvableowner_queue
provider with Telegramtap-verbs |autowithin-caps); non-payment expiry
escalation. -
Facilitator-free settlement. On-chain USDC settlement detection
(X402_SETTLE_ONCHAIN_DETECT): the watcher scans treasury transfers, matches by exact
atomic amount oldest-first, and settles — with atransaction_hashpartial-unique
index + CAS against double-settle, amount-jitter for same-amount disambiguation, and a
payment_unmatchedowner notice. A payment-aware cron wake-gate leg. -
Watchtower subscriptions (
SUBSCRIPTIONS_ENABLED,WATCHTOWER_PRICE_USD= $10/mo):
prepaid periods + renewal invoices on the settlement-watcher tick (atomic idempotent
apply_settlement, cronsubscription_lapsedgate,polyrob owner sub list/cancel). -
Metering→invoice bridge (
USAGE_INVOICE_BRIDGE_ENABLED): a tenant-scoped
usage_rollup+usage_summaryaction drafts an invoice from measured cost (never
auto-sends). -
ERC-8004 payment-backed reputation (
EIP8004_PAYMENT_FEEDBACK): a settled invoice
offers the payer aProofOfPayment-backed verified-purchase feedback authorization
(settled + treasury-toAddress + txHash replay guard + agent-id binding; local
simulation, not on-chain yet). -
Machine-payer middleware fixes: exact
(method,path)route gating (free reads no
longer paywalled), a shared 402 challenge,has_other_auth-gated 503 on a missing
facilitator, and an un-spoofable rate limit on the public invoice endpoints
(get_trusted_client_iptrusted-proxy resolution;X402_PUBLIC_RATE_*). Removed the
deadx402_access_logtable and the discontinued Google-Charts QR URL.
CLI candy polish wave (2026-07-14)
A pure visual/UX polish of the rob terminal REPL — the current implementation,
made its best self (no renderer changes, no new rendering abstractions):
- Slash commands highlight live while typing (known command / prefix / unknown /
args each styled distinctly); the completion menu is now actually visible — a
stock prompt_toolkit palette with per-command descriptions, opening while a
/commandis typed and on Tab. - The hint line under the input is context-aware: mid-turn it shows
^C stop,
typing a known/commandshows that command's usage, idle shows the key hints
plus one gentle rotating tip (and it stays quiet while the/modelpicker is open). - Every functional view (
/goals,/subagents,/todos,/pending,
/autonomy,/status,/usage,/tools,/toolset,/persona,/sessions,
/history,/session,/telemetry,/finance,/journey,/skills,
/cron,/config,/kb,/mcp,/learn,/self,/approve,/context)
now shares ONE visual grammar: a 2-space gutter, one table style, aligned
label/value rows, one empty-state phrasing with actionable hints, and one
status-glyph vocabulary (●/✓/✗/○/⚠from the theme) — the per-view
emoji vocab (🟢🟡✅🔴⬜⚪⏱️) is retired. - Sub-agent steps render on a quiet tree-prefixed lane (
└ researcher · step 3). - Status bar: ctx% turns yellow at 80% and red at 90%; the in-flight verb rotates
on long turns (cooking… → thinking… → …); the previously-dark autonomy line
(goals/cron counts) is now populated by a slow fail-open background poll. - New shared modules:
cli/ui/candy.py(plain-string view grammar helpers),
cli/ui/slash_highlight.py,cli/ui/hints.py,cli/ui/autonomy_poll.py;
glyph/style vocabulary consolidated incli/ui/theme.py. No new env flags.
Owner-UX Phase 4 — surface parity: recap core, Telegram owner verbs, Preferences page (2026-07-14)
Closes out the owner-UX usability wave's Phase 4 (surface parity): the CLI/REPL,
every chat surface (Telegram — and everything sharing its dispatch: WhatsApp,
Discord, Slack, Signal, X, Email), and the web Console now expose the same
read-only situational-awareness verbs and the same typed-preference control
surface, instead of each surface having absorbed a different slice of the
recent agent waves.
- Surface-neutral recap core (
core/recap.py) — the episodes/events/
skills/ledger assembly behind/journeywas extracted out of the CLI
rendering layer into one pure, dependency-injectedbuild_recap, so any
surface reuses the exact same data-gathering instead of re-implementing it.
Hardened this pass:_parse_windownow rejects a window that parses to
something unusable — non-finite (nan/inf, e.g."1e400d"overflowing
toinf, or"nand"parsing asfloat("nan")because the trailing char
happens to be thedsuffix) or absurdly large (> ~10 years, e.g. a
30-digit day count) — with the same friendlyValueErroras a malformed
label, rather than relying onint(nan)coincidentally raising somewhere
downstream. Telegram's/recap [window]exposes the raw label to chat
input, so this is a real hardening, not just belt-and-suspenders. - Telegram owner verbs (shared by every surface on the same dispatch):
/status(bound-session state, goal counts, next cron run, cost over the
trailing 24h),/recap [window](alias/journey),/goals(board
summary),/prefs(read-only effective preferences) — owner-gated by the
resolved principal (the local-CLI bypass is never honored on a network
surface), tenant-scoped, and backed by the SAME primitivespolyrob owner
and the REPL slash commands use. - Webview Preferences page (
/preferences+ GET/PATCH
/api/webgate/preferences) is real: schema-driven from
core.prefs.PREF_SCHEMA, safe keys apply on write, guarded keys need an
explicitconfirm:true(409 without it),WEBVIEW_READ_ONLYblocks all
writes. A same-wave review caught and fixed a confirm-bypass in the parallel
T3 commit: a truthy non-booleanconfirm(the string"false", or1) was
being accepted as confirmation — the PATCH handler now requires a literal
JSONtrueand 400s on a malformed/non-dict body or a missingvalue. - Hardening:
surfaces/telegram/harness.py—_status_reply/
_goals_replynow share the caller's already-openGoalBoardinstead of
each opening a second connection to the samegoals.db;/status's
"Cost today" line is relabeled "Cost (24h)" (it's a rolling window, not a
calendar-day figure). - Docs:
docs/guide/architecture.mdgains a "Chat-surface owner
commands" table documenting/status//recap//goals//prefs(plus the
pre-existing/pending//approve//asks//allowverbs) for every chat
surface;console.md's Preferences section andcli.md's REPL command
table were already accurate for this wave.
What's Changed
- release: POLYROB 0.8.0 by @themontreal in #5
Full Changelog: v0.5.1...v0.8.0