Repository navigation
Releases: CarlDog/wobblebot
Release list
v2.0.13 — withdrawal submission safety
Withdrawal submission safety
The Harvester reserves a withdrawal claim and its rolling-cap charge in SQLite before contacting Kraken. Concurrent execution attempts cannot submit the same proposal twice. A timeout or interrupted audit leaves an unverified claim that blocks further bot withdrawals until an operator reconciles it. Historical failed rows are treated as unverified during migration, and the dashboard shows the hold.
Upgrade and recovery
Back up the harvest and operator databases using SQLite's online backup operation before migration. Reconcile withdrawal claims and approved execute commands against Kraken Funding before restarting the Harvester. The local claim-... and historical failed-... identifiers are not Kraken reference IDs. If reverting to v2.0.12 while any claim is unresolved, keep the Harvester stopped. See the withdrawal incident runbook and changelog.
The source release and GHCR image do not deploy the file-based NAS stack. No real withdrawal was performed during verification.
WobbleBot 2.0.12
WobbleBot 2.0.12 closes the silent fill-loss path found on 2026-09-18 (ADR-046): a fill Kraken had confirmed, whose trade rows the same tick's TradesHistory snapshot did not yet list, was committed as a closed order with zero trade rows and never re-examined. One of eight engine fills in the evidenced 09-09 to 09-18 window was lost this way; the daily reconcile reported it for eight days until a hand backfill.
- A confirmed fill whose trade rows do not cover its
filled_amountis pending, never final.save_fillrefuses the shape; the order is closed and apending_fill_tradesmarker written in one transaction, so the counter-order still fires exactly once. - The engine sweeps markers every tick using the order's own trade list (
QueryOrders trades=true, thenQueryTrades; verified read-only against the live account), records rows as they arrive, and clears the marker once the fill is covered. Bound: 120 lookups that ran and left the fill uncovered, or 30 minutes; a give-up keeps the marker, logs ERROR, pages "Fill recorded without its trade rows", and is re-raised at every later boot until backfilled. - A restart resumes any sweep the previous process left; the cancel path and the boot reconciler use the same resolution. The ADR-038 fee-drift page now fires on a recovery tick as well as a fill tick.
live.dbgains one additive table, created on the first boot; no settings or key change. Upgrade and rollback notes:docs/release/v2.0.12-upgrade.md.
Review gate: five dimension reviewers, a sixth on the fix round, the completeness critic, and a read of that fix (16 + 12 findings, every code finding fixed and pinned); 22 of 22 mutants caught; 4,300 tests passed at the tip. Details in PR #152 and the roadmap receipt.
Deploy is the operator's two steps: this tag publishes the image; the IMAGE_TAG bump on the file-based stack applies it.
WobbleBot 2.0.11
WobbleBot 2.0.11 packages the final stabilization fixes for the 2.0.x closeout.
- Read-only health queries now drain failed SQLite connections before the event loop can close. Three deterministic regression cases cover the paths missed by the earlier storage-adapter repair.
- Counter-order exposure checks use the proposed order's actual notional, including partial fills and restart recovery.
- Advisor roles can use two explicit fallback targets, with persisted attempt history, shared spend caps and a final heuristic when all configured cloud routes fail.
- Native Ollama Cloud is available as a distinct provider using
OLLAMA_API_KEY. Provider support is separate from model qualification; no Ollama candidate is selected in the current NAS configuration. - Settings are grouped by purpose and ownership. Maintenance archives use unique export names so retries preserve existing exports.
Keep the operator's current settings and prompts. The generic example is a template, not a replacement for the configured NAS routes. This release widens the LLM ledger's provider constraint; tagged 2.0.9 cannot read new Ollama Cloud rows. Retain compatible source-pinned rollback images and verified backups. See the upgrade and acceptance runbook.
Validation: Windows Python 3.13.14 passed 4,229 tests. The packaged Linux Python 3.14.7 candidate passed 4,223 tests with six absent-private-file skips, plus 10 offline integration tests. Formatting, lint, type checks and mandatory upgrade gates passed. Publication CI is separately linked to this tag.
This release supersedes the unpublished v2.0.10 candidate. Its tag test failure exposed the reader race and blocked publication; that tag remains intact. No 2.0.10 NAS deployment occurred.
Release publication does not close the phase. NAS deployment and the authorized eight-hour observation campaign have separate receipts in the roadmap; formal acceptance and 2.1 development remain gated.
v2.0.9 — failed SQLite-open shutdown repair
Fixes C5-R1: failed SQLite storage opens now wait for their worker to terminate before returning the database error. Cleanup is bounded and preserves the original error cause. Read-only enforcement, successful opens and trading/transfer boundaries are unchanged.
The repair includes deterministic real-worker regressions, independent review and four caught mutations. Windows and installed Linux suites, static checks and supported aiosqlite lifecycle checks passed for the repair. Final release CI and image identity are recorded in the roadmap after publication.
For operators upgrading from 2.0.7, this also includes the 2.0.8 stabilization changes. Follow the upgrade/rollback handoff and 2.0.x limitations. Publication and deployment do not constitute formal phase acceptance.
See the repair evidence and current roadmap receipts.
2.0.8 — stabilization and operator reliability
Fixed
- Starved grids now explain the last layout's placements, refusal reasons,
cost-basis deferrals and retry cadence on the status card, including configured
symbols without prior orders or fills. Pause/offside and current open orders
suppress contradictory starvation claims; tick counters never imply elapsed age. - Persisted starvation diagnostics degrade together on corrupt data and carry a
separate freshness marker so a prior-version writer cannot refresh stale details
after rollback. Existing pause/offside state survives migration and readback. - Malformed pending-command rows now raise a sanitized
StorageErrorfrom both
readers. The live loop keeps ticking and reports blocked command dispatch until
the row is repaired; approvals are neither discarded nor dispatched partially.
Error tracebacks omit the malformed payload. - Operator TTL-expirer and heartbeat-monitor failures now log ERROR and propagate
to supervision; cancellation and normal shutdown retain distinct INFO messages. - Discord confirmation refusals now contain connection, DNS and timeout failures
while keeping the approval gate closed and the shared confirmation unchanged. - Missing or whitespace-only Discord tokens now stop the operator with exit 2
before background tasks start. Recalibration also returns exit 2 for missing
reader credentials while retaining exit 1 for an exchange balance-read failure. - The imported package version now agrees with distribution metadata and the
release manifest. The import smoke test checks their agreement instead of
repeating a stale version literal. - HTTP healthchecks now close error responses before returning unhealthy, fixing
an owned-response leak exposed by Python 3.14's resource warnings.
Maintenance
- The closeout backlog now reconciles historical candidates, issue dispositions
and gated next-phase work. Release limitations and accepted scoped standards exceptions
are explicit; the roadmap remains the only phase-status ledger. - Dependabot groups only the reviewed dev-only minor/patch updates for fleet
routing. Runtime packages and major upgrades retain individual review.
Upgrade and rollback handoff · Remaining 2.0.x limitations
Publication is separate from production deployment and formal phase acceptance.
Publication evidence records the passing final gates and verified GHCR image. The first tag test attempt reported an aiosqlite worker callback after event-loop closure; one unchanged retry passed. C5-R1 remains unresolved: reproduce and resolve the shutdown behavior, or obtain explicit operator acceptance before deployment. A passing retry does not establish a repair. See the updated deployment limitations.
2.0.7 — a dead background task is now a crash, not a zombie
A reliability release. One incident, one root cause, one class of bug — found by watching the 2.0.6 soak, not by a test.
The incident
A ~21-minute NAS upstream-connectivity outage killed name resolution for every container on 2026-09-05, 11:53:45–12:14:13 UTC. Pi-hole was not at fault: it was up 8 days and logged Connection error against 1.0.0.1, 208.67.222.222 and 8.8.8.8 simultaneously.
cli/observe, cli/news and cli/live failed soft and recovered. cli/operator did not — its notification forwarder died at 11:57:20 and stayed dead for 10h11m while the process stayed alive and its Discord transport reconnected at 12:26. Discord notification forwarding and the heartbeat-alert monitor for every other daemon were dark for that window. Trading was unaffected.
The container reported unhealthy 591 consecutive times and nothing acted on it, because restart: unless-stopped fires on process exit, not on healthchecks.
Root cause
aiohttp.ClientConnectorDNSError is not a discord.DiscordException — its MRO ends ... → ClientError → OSError. The Kraken adapter wraps its HTTP library's base error (except httpx.HTTPError); the Discord adapter wrapped only discord.py's domain error. That single asymmetry is why four daemons survived and one died.
The raw error passed straight through a correct, pre-existing per-row handler, out of the poll loop, out of a bare finally that logged it at INFO, and into a Task nobody awaited — where, because the Task stayed referenced by a local, even Python's "Task exception was never retrieved" warning never fired.
Fixed
- Seven adapter catch sites widened, not six. The seventh is
start()'s login handler, which the first draft of the plan missed — a boot during an outage would still have leaked raw aiohttp into the gateway task. - A loop that DIES logs ERROR; only a loop that STOPS stays INFO.
- Five background tasks supervised, not four. The omitted one was the Discord gateway — the worst zombie of the set, because
except DiscordTransportErrorguardedawait stop_event.wait(), so the module's ownexit_code = 1was already unreachable for exactly the failure it was written to catch. - The shutdown cancel loop no longer aborts on the first corpse.
cli/webhad the same narrow-catch bug with a worse consequence — an escape skippedsafe_shutdownentirely. Deliberately no exit-on-death there.cli/operator's exit codes are documented for the first time.
Process
The plan was reviewed before implementation: 51 agents, 5 dimensions, 15 findings, 7 surviving three-lens refutation, and a completeness critic that overturned a kill the panel made twice. It found the plan wrong in five of its six layers — including a prescription that raised TypeError on the repo's own interpreter while the correct pattern sat 976 lines above it in the same file.
All eight behaviours are mutation-verified: reverted in turn, guarding tests required to go red, tree asserted byte-identical after each restore. 8/8 caught. 3924 tests pass.
Known gap, stated rather than closed
The healthcheck still has no actor. This release converts task-death into a process exit that unless-stopped already handles; it does nothing for the "wedged-but-alive" class. Deferred to the reserved 2.1.0 phase, along with the fact that the heartbeat monitor watching all six daemons lives inside the one that died.
Full detail in CHANGELOG.md and roadmap receipt 17.
v2.0.6 — the audit's own findings, fixed
A correctness release with no new features. Every item is either a defect found by auditing 2.0.5, or a claim in the codebase that turned out to be false when someone measured it instead of reading it.
Numbered 2.0.6 rather than 2.1.0: that name stays reserved for the deployment & lifecycle-integrity phase.
Operator-facing fixes
- All sixteen CLI entry points emitted a raw traceback for a directory
--configor a malformedsettings.yml.IsADirectoryErrorandPermissionErrorareOSError, neither isFileNotFoundError; andyaml.YAMLErroris not aValueError. The contract now lives in one place. - The capital report told the operator a sellable position could not be sold. Its ordermin check reads
available— correctly, for "can a NEW sell be placed" — while the sentence described the whole position. DOGE held 293.64 with 262.10 committed to resting orders against an ordermin of 50, and paged daily as unsellable. Total and available are now two questions at two severities. cli/live's documented exit codes were incomplete in both places that stated them, including the docstring--helprenders verbatim.- Four
docs/implementation/guides still declared the system did not exist. Superseded in place — real documents link to them.
Guards that reported green while checking nothing
- The repo-wide logging guard scanned zero files from any working directory but the repo root.
- The offside popover branch BTC and ETH actually render in production was pinned only at DTO level, on a different attribute than the template gates on.
- The schema-drift strict mode was armed only by an untracked
.envand went silently dead the day that file was deleted. Wiring it into CI was measured and deliberately not done — theenv:line would have been inert. - A key defined twice in
.envwas invisible to the drift tests, which collapse keys into aset.
Process
Gated by the ratified pre-deploy adversarial review: 11 dimensions, 72 agents, 20 findings, 7 surviving three-lens refutation, verdict nothing blocks.
It found a real defect in this release's own capital fix — the informational line sat below an early return and was built and discarded in exactly the case it was written for, so a false warning had been replaced by silence — plus four false claims in comments and docstrings, each disproved by executing both revisions rather than by argument. All fixed before the tag.
Full detail in CHANGELOG.md and roadmap item 16.
v2.0.5 — Operator legibility
Operator legibility
Three slices, one theme: a dashboard or a log that states something confidently wrong is worse than one that says nothing. Every item either removes a false claim or replaces silence with a true one.
The offside popover states wall-clock truth
It said "Parked N ticks since cli/live last started", and that number resets on every deploy. The captured evidence:
2026-09-03T23:01:23Z BTC/USD still offside at 81190.1; parked (720 consecutive ticks)
71 minutes into that daemon. At the configured 5s tick the dashboard rendered "about 1h 0m" for a symbol parked since 19 August — roughly 380x short. The engine now records when an offside episode began and restores it across restarts.
An episode whose start nothing observed says exactly that, rather than guessing. BTC and ETH ship in that state and will read "unknown" until they re-enter their bands, which is 11-15% away. That is the honest answer by design, not a failure.
A starved symbol says what is blocking it, and stops repeating
A symbol that cannot place at its anchor re-emitted the same refusal on every retry forever — 985 lines a day, measured on the live container, not estimated. Those per-level lines now drop to debug while starved, replaced by one entry warning naming the binding cap and an hourly summary that reports what binds now.
A queued recovery counter that cannot place announces itself once, with the order and the reason. Warning on every attempt would be ~14,800 lines a day, since that path retries every tick outside the back-off.
Two latent bugs surfaced on the way: the periodic still-starved summary had been unreachable since the day it was written (it counted ticks against 240 while the retry gate counted them against 60), and resuming a paused symbol left its starved clock frozen.
An untraded symbol's card can be hidden
An eye toggle on any symbol outside live.symbols collapses its card into an "N hidden" summary with one-click reveal. Symbols the engine does trade cannot be hidden — their cards carry pause, resume and re-anchor.
A hidden symbol that still has resting orders is named and tinted rather than disappearing: hiding is limited to symbols cli/live never ticks and shutdown never cancels, so an order resting on one is capital nothing is managing.
The Discord fast path says why it declined
It logged only its hits, so an inert fast path was indistinguishable from the mis-parse it exists to prevent. It now reports a classified reason, announces at startup whether it is armed, and the embed footer credits whichever parser actually decided.
Schema
engine_state.offside_since (an ALTER) and a new hidden_symbols table, both additive.
Rollback is safe — 2.0.4 reads engine_state with an explicit column list and never touches hidden_symbols, verified against the tag's own SQL. One asymmetry, in the CHANGELOG: 2.0.4's upsert omits offside_since, so a rollback freezes it and a later roll-forward can render a stale date. Bounded to symbols that flap in and out of band; BTC and ETH carry NULL and are immune.
Process
A six-dimension adversarial review ran against the merged state, since the per-group reviews covered the groups but not their interactions or the deploy. Verdict: nothing blocks. Its corrections are in this release — including one of my own overclaims, and six comment contradictions in a release cut for legibility.
Full changelog: v2.0.4...v2.0.5
v2.0.4 — seven findings from an adversarial review of the same day's releases
Patch release over 2.0.3, cut 2026-09-03.
No configuration schema change. Upgrading from 2.0.3 is an IMAGE_TAG bump to 2.0.4; nothing else moves.
An adversarial review of the code 2.0.2 and 2.0.3 shipped earlier the same day raised 17 findings across eight dimensions; 7 survived three independent refuters. Three were live in production. Every fix below is pinned by a test proven to fail when the behavior is reverted.
Fixed
- A re-anchor can no longer be queued for a symbol the engine does not trade. Symbol cards render for any asset with a held balance, a wider set than
live.symbols, so the per-card anchor button put an untraded coin one click from a real grid layout: the engine would place orders it never ticks, the cost-basis guard would pass its sells unguarded for want of a basis, and clean shutdown would leave them live on the exchange. The re-anchor route now refuses a symbol outsidelive.symbols, with the card gate as defense in depth. An unknown trading set falls open, so an unwired deployment is unchanged. - The Discord fast path defers instead of refusing. Its symbol pattern matches any short word, so
cancel orders on all, the phrasing the help catalog and the operator prompt both advertise, was rejected with the false reason "ALL is not in the active symbol set" while the model that parses it correctly never saw the message.pause everythingfailed the same way. A matched verb whose symbol does not resolve now falls through to the model, andcancel_open_ordersgained an explicit all-symbols grammar. - The offside popover no longer states two falsehoods. It claimed the engine "parks with no orders" while the same card listed the symbol's live ladder below it; going offside suppresses new placement and counters but never cancels standing orders, and an operator who believed the claim would re-anchor and destroy a buy ladder waiting for the retrace. It also rendered an in-memory tick counter as a wall-clock duration, so a symbol parked for weeks read as minutes old after any restart.
Changed
- The re-anchor icon now comes from the same set as its neighbours. It was a hand-drawn outline glyph beside two filled Material icons; it is now Material's own
anchor. Attribution for Material Design Icons (Apache-2.0) added toNOTICE.md, which had none, and the stale Lucide entry corrected.
Tests
Three tests shipped in 2.0.3 passed against deliberately broken code and now fail when it breaks: the dead-man's-switch deadline warning had no coverage at all, the popover's band assertions were position-independent, and the new alert field was only ever asserted as None. All twelve behaviors changed in this release were re-verified by a mutation harness.
Full detail in CHANGELOG.md under [2.0.4] and in docs/planning/roadmap.md item 13.
v2.0.3 — offside badge hover popover + dead-man's-switch deadline logging
Patch release over 2.0.2, cut 2026-09-03.
No configuration schema change. Upgrading from 2.0.2 is an IMAGE_TAG bump to 2.0.3; nothing else moves. settings.yml, .env, prompts, and the databases are untouched.
Added
- Offside badge hover popover. The OFFSIDE badge on a symbol card keeps its label; hovering or focusing it now opens a plain-language explanation: which side of the band price is on and against which level, the anchor and spacing that define the band, how long, and the two exits (price returns, or re-anchor). The band is rebuilt from the grid state through the engine's own level math, never from open orders, so a parked symbol with an empty book still gets the full sentence; without a grid state or price it degrades to the duration. Reachable by keyboard and by tap.
- Dead-man's-switch streak logging names the deadline. The first failed reset of a streak logs, at WARNING, where Kraken's last confirmed auto-cancel deadline sits relative to now, and the 3-strike alert carries that timestamp in its context. This is the fact the 2026-09-03 post-mortem could not recover.
Full detail in CHANGELOG.md under [2.0.3] and in docs/planning/roadmap.md.