Skip to content

POLYROB v0.8.0

Choose a tag to compare

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Vocabulary completed (same RUN_EVENTS_ENABLED gate, fail-open):
    compaction_started/finished (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.
  • RunActivity snapshot: 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}; null for 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_started fires the moment a tool is DISPATCHED (multi_act),
    llm_started the moment an LLM call begins, and
    awaiting_approval/approval_resolved bracket 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 new call_id on 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 doctor gained a live-activity pipeline check.
  • Webview: /activity summarize() 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 /config panel: unconfigured keys show the real built-in default
    (flags-catalog + posture-aware) instead of a wall of None (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_day now
    actually enforce (tighten-only merges preserved).
  • digest.quiet_hours enforced: 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-origin style); 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 /config opens an
    interactive settings picker (reuses the /model ReplPicker; Enter seeds a
    ready-to-send set command, 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_status gains mode=; the
    preferences action gains read-only explain; self_env hard-denies
    core/config_policy/ source.
  • Hardening: core/env.float_env; import-frozen numeric flags no longer
    crash the process on a stray none value; 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/capabilities default_tools payload (the
    agents/task/tool_defaults.py SSOT) instead of the hardcoded
    ['browser','filesystem'] in chat.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_task and api_skills
    moved out of webview/server.py into 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.py 4316 → 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 sync workspace_dir property 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
    inert Field(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_encoders dropped (api/models.py, modules/memory/models.py).
    BotConfig() now constructs with zero deprecation warnings.
  • Changed: the four _int_env re-implementations now delegate to the ONE
    parser core.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); the agents/task/constants.py mass shim-flip
    is won't-fix (hybrid module, 78 production importers of task-tier symbols);
    the core/config_policy/policy.py 9-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.py is a back-compat
    shim; user MCP admin; the public x402 invoice throttle; the webview
    connection/event throttles; RateLimitManager's internals), TokenBucket
    (moved from core/surfaces/rate_bucket.py, now a re-export shim; the api
    middleware burst gate), and FixedWindowCounter (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.py stays 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_PATH is real. It was a decoy — config anchored it, created its
    parent directory, and polyrob update snapshots trusted it, but the app always
    opened the hardcoded <data_dir>/database/bot.db. The default now matches
    reality (data/database/bot.db) and database_manager honors an explicit
    DB_PATH behind 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.db and the opt-in messages.db mirror 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>
    — so polyrob update snapshots 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 new db_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), and forged_turns (forged-turn kind
    constants). The old agents/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.py no longer imports the surface
    tier — core/surfaces/act.py owns InboundResult + an actor-registration
    seam; surfaces/telegram/harness.py registers the shared dispatch at import.
  • Changed: modules/x402/middleware.py no longer imports api.auth_state
    api/app.py installs 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 .env files configure the process and in which precedence order.
    load_env, /config check, the CLI first-run guard, and the polyrob update
    snapshot all derive from it now (layering behavior unchanged).
  • Changed: polyrob update snapshots additionally capture the legacy
    ~/.rob/.env transition 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 $2.47, net $-2.47" while the
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_usd preference/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_DIR never changed the data-home value (workspace placement only), so
    core.bootstrap._resolve_cli_data_home and core.runtime_config.get_data_root now
    delegate to core.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_PERMISSIONS lives 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 from core.config_policy directly (only the five
    shim-tail-symbol imports remain on agents/task/constants); the flag_defaults bridge
    moved to core/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 dead import 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 through data_dir_or_home(); the path
    ratchet's single-quote blind spot closed (5 hidden action_registration.py sites fixed,
    patterns extended); 13 ratchet baseline rows deleted, handlers.py 7→2.
  • docs(flags): DELEGATE_BLOCKED_TOOLS catalog row fixed (11 → 15 ids; derivation +
    live anchors noted); flags catalog + user-guide refs regenerated. AGENTS.md now
    describes the derived gate sets and core/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/.env keys, legacy ~/.rob/.env provider pins, config/.env.production
    backfill) into os.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 flags polyrob init applies in-process) — deliberately
    NOT a home redirect, which shadowed tests that isolate via Path.home.
    (2) test_ledger_error_fails_closed asserted 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.py imported the four provider client modules at top
    level, so EVERY entry-point import (each polyrob CLI invocation, every uvicorn worker
    boot) eagerly loaded the anthropic + openai + google.generativeai SDKs. Now
    TYPE_CHECKING-only (the classes were annotation-only there); cli.polyrob imports zero
    heavy SDKs and test_import_layering is 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_consistency green.
  • fix(tests): the nginx deploy guards read the retired deploy_unified.sh tombstone;
    they now encode the same invariants against the live deploy surface
    (deploy_webview.sh installs the ownops vhost; no live script installs the demoted
    proxy nginx.conf; /opt/polyrob anchor).
  • Known-not-fixed (documented): 4–5 order-dependent test failures (test_identity,
    test_budget_gate, test_goal_dispatcher child-tools, chat_resolver_parity,
    protected_config_guard) — all pass in isolation; root cause is load_env importing
    the dev box's real ~/.polyrob/.env into os.environ mid-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 dimensions money/high_impact/delegate_blocked/exec/
    readable_while_tainted). MONEY_TOOLS, DELEGATE_BLOCKED_TOOLS (env override kept) and
    HIGH_IMPACT_TOOL_IDS are now derivations, byte-identical memberships parity-pinned;
    register_optional_tool refuses 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.py gains data_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' hardcoded data/*.db / /var/lib/polyrob/*
    argparse defaults. New ratchet tests/test_path_ratchet.py freezes the remaining
    constructions per-file, shrink-only. Deferred with notes: bot.db/messages.db/
    telemetry_events.db location 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_buckets was 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.TokenBucket now 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.pycore/gitignore.py (shim kept), killing
    core→cli; core/initialization.py's dead top-level agents.personality imports deleted
    (layering-ratchet allowlist tightened 37→35).
  • WS-7 (SSOT tail): api/openai_compat/model_map.py resolves a bare registered model
    slug via registry membership (grok/glm/kimi no longer misroute to the env default);
    scripts/seed_goal.py and the telegram owner-interactive toolset now source from named
    TOOLSETS entries (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) from agents/task/constants.py into the new core-tier package
    core/config_policy/ (policy.py). agents/task/constants.py re-exports every public and
    externally-referenced private symbol unchanged, so all ~126 importers are byte-compatible; new
    code should import from core.config_policy. Added a reset_autonomy_mode_warnings() test seam.
  • refactor(core): flipped all 15 core/ → agents.task.constants back-edges to
    core.config_policy, so import core.config_policy pulls zero agents.* modules — the
    core ↔ agents.task.constants cycle is one-directional (agents → core) at last.
  • test(core): added tests/test_layering_ratchet.py — bans core/ imports of
    agents.task.constants and enforces that the remaining core→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/x dedup, wa_window,
    group_allowlist, conversations, outbox, surface_state, deployed_apps) registered in
    core/db_manifest.pypolyrob update backup/rollback silently skipped them. Grep-based
    completeness contract test added.
  • T2: /capabilities no longer advertises the deprecated x-ai/grok-4.1-fast — default
    model now comes from llm_client_registry.get_default_model (env-overridable).
  • T3: credit_sentinel's fallback path resolution follows resolve_data_home (dropped its
    unique DATA_ROOT precedence — 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_IDS covers all registrable tools (shell, process, self_env,
    hf_deploy, github, x402_pay, alchemy, collabland were 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 durable owner_notice instead 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 and polyrob recap (numbers can no longer diverge).
  • T9: core/event_kinds.py — SSOT for all 33 durable event-log kind strings + a producer
    contract test; activity feed / spend rollup / digest consume the constants.
  • T10: a bare PathManager() routes through resolve_session_data_root (closes the RC-1
    "two session trees" landmine — DATA_ROOT-only default).
  • T11: runtime logs resolve to <data_home>/logs (new POLYROB_LOG_DIR override) 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.py for the invoice tool/settlement
    watcher pair), plus CORRESPONDENT_REQUIRE_APPROVAL inverted (defaults OFF under autonomous).
    An explicit per-flag env always wins.
  • Autonomous toolsetAUTONOMOUS_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 on full_autonomy_enabled(); VALID_TOOL_IDS
    gained the vocabulary needed to express the grant.
  • Two-lane approvals — a new auto_notify provider (allow + tool_auto_approved audit event
    • post-hoc owner notification — "act-and-report") becomes the default under autonomous mode for
      an unset/auto/interactive_cli APPROVAL_PROVIDER. A fixed always-owner-queued lane
      (_ALWAYS_GATED_VERBS: the four self_env_* verbs, mcp_install, plus the aspirational
      self_modify/tool_manage) never moves to auto_notify regardless of mode. hf_deploy's
      first-publish maps auto_notify → owner_queue (a public HF Space is not something to
      act-and-report after the fact).
  • Outbound policy ladderOUTBOUND_POLICY (open|domains|allowlist|off, default
    allowlist, open under 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.domains merges via a new narrow_list kind
    (the pref can only intersect a non-empty operator OUTBOUND_DOMAINS env, or define the set
    from scratch when the env is empty) — the plan's specified union merge would have let a
    tenant pref widen past an operator-set domain allowlist, inverting its polarity;
    narrow_list is the corrected, allowlist-safe behavior.
  • Receive-side auto payments, spend stays gatedPAYMENT_APPROVAL_MODE defaults to auto
    under autonomous mode, but only for PAYMENT_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) keep owner_queue pre-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 transparencyTOOL_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 /config command (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
    in polyrob 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 in task_agent_lite.py now route through one
    helper; under effective AUTONOMY_MODE=autonomous a 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 full AUTONOMOUS_MODE_TOOLS grant (keeping goal/cronjob), plus
    code_execution/shell/coding at AGENT_COMPUTE_POSTURE>=1 via 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 (default nikolaik/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 keeps python:3.12-slim; explicit
    CODE_EXEC_DOCKER_IMAGE wins 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=none still refuses.
  • Dev-mode exec timeout ceiling is 120s (was silently 30s) — aligns the backend clamp
    with the shell tool's foreground contract; explicit CODE_EXEC_MAX_TIMEOUT_SEC wins;
    confined default stays 30s.
  • <environment> foundation block (agents/task/agent/core/env_context.py, flag
    ENV_CONTEXT_BLOCK default 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 under POLYROB_LOCAL or 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-call max_amount_usd (not the advisory quote) AND re-checks PolicyGate
    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-cap load 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 inside PolicyGate.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_scheme fails
    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 finance works standalone, wallet view/export fail friendly on a bad
    seed, and polyrob doctor verifies 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_SEED to ~/.polyrob/.env (chmod 600) and
    offers to point X402_PAYMENT_RECIPIENT at 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-EXPORT confirmation reveal of
    the mnemonic (bip44) or per-venue 0x-hex private keys; never agent-callable.
  • Versioned key derivation — a wallet's scheme (legacy PBKDF2 or bip44 BIP-44,
    m/44'/60'/0'/0/{treasury,x402,polymarket,hyperliquid}) is recorded write-once in
    data/wallet/meta.json by wallet init/import; a pre-existing wallet with no meta file
    is legacy FOREVER — addresses never change. New wallets get bip44 (mnemonic imports
    cleanly into MetaMask/Rabby). AGENT_WALLET_DERIVATION is a recovery-hatch env override
    for a corrupted/missing meta file only.
  • /pfp REPL 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 init bridges 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 runs init --skip-keys on accept instead of leaving the operator with a
    bare key and nothing else configured; init also gained an optional agent-wallet
    opt-in step (default No) and a "Next steps" block (wallet / avatar / surfaces / identity
    / doctor).
  • polyrob doctor setup-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_avatar preference — a per-owner toggle for whether the webview identity
    page renders the avatar.
  • /model set-default SSOT fix (G11) — now keeps DEFAULT_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-guide skill v2 — adds references/wallet-and-identity.md and
    regenerates references/configuration.md from the current docs/CONFIGURATION.md
    (also absorbs the AGENT_WALLET_DERIVATION row and MESSAGE_AUTONOMOUS_ALLOWLISTED
    from a concurrent change).
  • Docsdocs/CONFIGURATION.md gains the AGENT_WALLET_DERIVATION row;
    docs/guide/payments.md gains "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.md documents 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 gateway launches 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. SurfaceConfig gains
    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's verify() 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 in connection.py (home of the
    singular schema_version table) and the orphan scripts/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: cp of a live WAL DB at a path that no longer
    exists), restorable via polyrob update --rollback.
  • In-use guard portable (U8) — the process scan is /proc → psutil → ps, so
    macOS --apply/--rollback no longer bypasses the guard silently.
  • Prod venv rebuild procedure (D12) — DEPLOYMENT.md documents the
    requirements.txt-first rebuild (bare pip 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 layer polyrob run honors; DeepSeek's key
    prompt says it can't bootstrap alone.
  • Pairing approvable (O5)polyrob owner pair {pending,approve,revoke} ships;
    core/pairing.py no 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 upgrade no longer double-records the schema version (IntegrityError → exit 1),
    which deterministically rolled back any polyrob update --apply containing a
    self-recording migration. The upgrade loop is extracted to a testable
    apply_pending_migrations() with recording guarded by is_version_applied.
  • polyrob init no 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 unset POLYROB_AGENT_TOOLSET; accepting the wizard
    default used to silently drop web_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 at deployment/legacy/); scripts/deploy_prod.sh /
    deploy_from_local.sh now run migrations.migrate upgrade before restart;
    start_autonomy schedules run_boot_migrations so every posture (telegram/REPL/
    email/gateway) self-heals schema like the API lifespan; polyrob-email.service is
    committed and restarted by both deploy scripts. AGENTS.md/DEPLOYMENT.md/
    deployment/README.md rewritten 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 --rollback prefers the newest FULL snapshot
    instead of the DB-only pre-migrate one; --apply takes ONE snapshot (migrate_guarded
    reuses it); same-second snapshot dirs no longer clobber. Systemd manual steps are
    posture-aware (detect polyrob* 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 every polyrob gateway mention 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 (flat identity/*.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 legacy data/auto/*/sessions/*
    sessions; deploy_webview.sh takes its target from ~/.polyrob/ops.env instead 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 owner user_profiles row 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 or autonomy_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=false settlements are
    treated as unpaid, and the kill-switch probe fails closed. Billing correctness: one
    cost entry point (cache-write surcharge preserved), a real usage_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-form payer_contact ("billed to"); approval modes
    (PAYMENT_APPROVAL_MODE = approve via a durable, remotely-approvable owner_queue
    provider with Telegram tap- verbs | auto within-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 a transaction_hash partial-unique
    index + CAS against double-settle, amount-jitter for same-amount disambiguation, and a
    payment_unmatched owner 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, cron subscription_lapsed gate, polyrob owner sub list/cancel).
  • Metering→invoice bridge (USAGE_INVOICE_BRIDGE_ENABLED): a tenant-scoped
    usage_rollup + usage_summary action drafts an invoice from measured cost (never
    auto-sends).
  • ERC-8004 payment-backed reputation (EIP8004_PAYMENT_FEEDBACK): a settled invoice
    offers the payer a ProofOfPayment-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_ip trusted-proxy resolution; X402_PUBLIC_RATE_*). Removed the
    dead x402_access_log table 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
    /command is typed and on Tab.
  • The hint line under the input is context-aware: mid-turn it shows ^C stop,
    typing a known /command shows that command's usage, idle shows the key hints
    plus one gentle rotating tip (and it stays quiet while the /model picker 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 in cli/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 /journey was extracted out of the CLI
    rendering layer into one pure, dependency-injected build_recap, so any
    surface reuses the exact same data-gathering instead of re-implementing it.
    Hardened this pass: _parse_window now rejects a window that parses to
    something unusable — non-finite (nan/inf, e.g. "1e400d" overflowing
    to inf, or "nand" parsing as float("nan") because the trailing char
    happens to be the d suffix) or absurdly large (> ~10 years, e.g. a
    30-digit day count) — with the same friendly ValueError as a malformed
    label, rather than relying on int(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 primitives polyrob 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
    explicit confirm:true (409 without it), WEBVIEW_READ_ONLY blocks all
    writes. A same-wave review caught and fixed a confirm-bypass in the parallel
    T3 commit: a truthy non-boolean confirm (the string "false", or 1) was
    being accepted as confirmation — the PATCH handler now requires a literal
    JSON true and 400s on a malformed/non-dict body or a missing value.
  • Hardening: surfaces/telegram/harness.py_status_reply/
    _goals_reply now share the caller's already-open GoalBoard instead of
    each opening a second connection to the same goals.db; /status's
    "Cost today" line is relabeled "Cost (24h)" (it's a rolling window, not a
    calendar-day figure).
  • Docs: docs/guide/architecture.md gains a "Chat-surface owner
    commands" table documenting /status//recap//goals//prefs (plus the
    pre-existing /pending//approve//asks//allow verbs) for every chat
    surface; console.md's Preferences section and cli.md's REPL command
    table were already accurate for this wave.

What's Changed

Full Changelog: v0.5.1...v0.8.0