September 8, 2026
4 pull requests merged across 2 repos
Offline-Protocol/offline-protocol-sdk
Cuts v0.25.0. Nothing here changes behaviour: it is the version bump, the
release notes, and three corrections found while auditing the range.
Minor rather than patch, decided on public surface rather than on the
commit types. offline-protocol-transport loses ReticulumConfig, both its
constructors-adjacent helpers, ReticulumTransportBuilder and two constants,
and the TypeScript PresenceSource and SecurityWarningCode unions widen by
one and two members. The .udl and types.ts diffs were checked with comment
lines stripped rather than argued from semver intuition.
The release itself is four things: Reticulum attaches over
contract v1 so a device on it can receive for
the first time, the inert Reticulum configuration is deleted, the wire
specification becomes implementable from its own text and gains seven
conformance vector files, and two iOS BLE defects that cost battery and blocked
discovery are fixed.
Related issues
Refs #420, #421, #422, #427, #429, #430.
Type of change
-
chore— build, tooling, or dependency changes
Checklist
-
cargo fmt --all -- --checkpasses -
cargo clippy --workspace --locked -- -D warningspasses -
cargo test --workspacepasses -
cargo-denyis satisfied (advisories ok, bans ok, licenses ok, sources ok) - Commits follow Conventional Commits
- Docs /
CHANGELOG.mdupdated where relevant - No new
unsafein core crates - UDL unchanged in this PR, so no binding regeneration; the generated
bindings embed no version string, which is why a bump does not touch them
Also run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps, the
three no_std targets (core, sealed, leaf on thumbv8m.main-none-eabihf,
zero diagnostics each), scripts/check-license-consistency.sh, and
npx tsc --noEmit in bindings/react-native.
Version files
[workspace.package].version and the ten internal dependencies in
[workspace.dependencies]; the four more declared locally in
offline-protocol-sealed and offline-protocol-leaf, where inheritance is
deliberately not used (ADR 0020 / 0022) and
local_dep_versions_match_the_workspace_table is the guard; Cargo.lock plus
the two tool-workspace lockfiles CI builds with --locked;
bindings/python/pyproject.toml; the npm package and its lockfile; and the
three THIRD-PARTY-NOTICES.md copies, which list our own crates by version and
so go stale on every bump. All eleven crates are published at 0.24.1 on the
sparse index and 0.25.0 is free.
Three corrections that are not version bumps
1. A wider reading of the stale-figure class. The wire-spec entry said the
signing-domain registry now lists five live domains and that six vector
files pin the spec. #427 landed later in the same range, added
offline-gateway-addr-v1 and a seventh vector file, and made both wrong.
Neither number reads as a measurement, so the usual "re-measure quoted figures"
pass does not catch it. The rule that does: any cardinal describing a registry,
table or file set that another PR in the same range also adds to. Both entries
are corrected, and the vector-coverage sentence now names the gateway proof.
**2. docs/changelog/0.22.md has shipped eight broken links since its own
archive.** Archiving moves a section two directories down, so ](docs/…) has to
become ](../…); that rewrite was missed at the 0.22.0 cut and nothing in CI
resolves a markdown link, so it survived three releases. Fixed, and
CONTRIBUTING.md's archive paragraph now says to validate the whole tree at
each cut and why "silently" is literal.
3. Cargo.toml said "all nine move together". The internal-dependency table
has been ten since 0.24.0.
Breaking changes
For Rust crate consumers only: ReticulumConfig and its four fields,
ReticulumTransport::with_config / config() / should_reconnect /
increment_reconnect_attempts, ReticulumTransportBuilder, and
RETICULUM_CONNECTION_TIMEOUT_SECS / RETICULUM_MAX_PAYLOAD_SIZE are gone.
ReticulumTransport::new is the one constructor left, and there is nothing to
carry across because nothing read those values. No binding surface,
configuration key or wire byte follows them out; the React Native
transports.reticulum section is untouched.
TypeScript applications that exhaustively switch on PresenceSource or
SecurityWarningCode get a compile error from the widened unions.
Migration is §19, which now carries this release's label.
Notes for reviewers
The changelog diff is large because the section was rebuilt, not appended to.
Two things worth checking rather than skimming:
- The release header's direction. Reticulum is off by default
(reticulum_enabled: false in TransportConfig::default()), so the "a device
could never receive anything" headline is scoped to deployments that enabled
it and pointed at a daemon. The header says so. It also states the reverse
case, which is the one that can surprise an operator: a daemon speaking the
pre-contract shape now yields a transport that connects and never becomes
available, because the attach times out after ten seconds
(GatewayAttachPolicy.ATTACH_TIMEOUT). This direction-checking is deliberate
after v0.24.1, where the first draft of the release note said the opposite of
what the engine default made true.
- The archive move is provably lossless. The 0.24.1 section was re-extracted
from git show HEAD:CHANGELOG.md, the link rewrite applied, and the result
diffed against what landed in docs/changelog/0.24.md: identical except the
single ./docs/UPGRADING.md → ../UPGRADING.md line.
[Unreleased] had also accumulated three ### Fixed and three ### Added
headings from per-PR appends. All ten entries are preserved, regrouped into one
heading each under Added / Changed / Removed / Fixed / Documentation, matching
0.23.0 and 0.24.0. The three (#NNN) references are dropped for the same
reason: 0.20.0 had fifteen, and the four releases since carry one between them.
Not done here: adding a markdown link checker to CI. It is out of scope for
a release cut, but correction 2 above went unnoticed for three releases, so it
is a genuine gap worth a follow-up.
After merge
Tag v0.25.0 on main; pushing the tag is what publishes. release.yml's
version-gate runs ahead of both publish jobs and will refuse a tag whose
number disagrees with [workspace.package].version or pyproject.toml, which
is the one point in the release where re-tagging is still safe.
bahdotsh/wrkflw
TODO item 4 (P3, refactor) from the follow-up list off the PR #116 review. Fourth and last item in that series, after #119, #127 and #128.
What the item asked for, and why this PR does something else
The item was written against a pre-#119 tree. It asked to delete the ad-hoc trim_logs_to_cap() calls in check_diff_filter_results, rewrite the stale comment block above them, and route ~7 hand-rolled Local::now().format("%H:%M:%S") sites through add_timestamped_log.
Auditing that against current main:
- The trim calls and the stale comment are already gone. #119 removed all six ad-hoc calls and the "Ad-hoc
self.logs.push(...)" comment. The single surviving call lives inmark_logs_for_update, which is the choke point every log mutation routes through — it belongs there. - Only three of the ~7 timestamp sites survived. One is
add_timestamped_logitself. The other two are inprocess_execution_result, and they write toWorkflowExecution.logs— not to the app log buffer.
WorkflowExecution.logs has five writers and zero readers. Every view that touches execution_details renders .jobs, .start_time or .progress; none of them read .logs.
Doing what the item literally said would have been a regression. Both arms of process_execution_result already report their outcome through wrkflw_logging, and get_combined_logs merges that store into the app buffer without deduplicating. Routing those two pushes through add_timestamped_log would have rendered each outcome twice — precisely the dual logging #128 removed — and would have failed the dual_logged_events_do_not_also_land_in_the_app_buffer test that #128 added.
So the dead field goes instead.
Changes
- Delete
WorkflowExecution.logs(crates/ui/src/models/mod.rs). - Delete its five writers: three struct constructors (two in
app/state.rs, one inhandlers/workflow.rs) and the twoexecution_details.logs.push(...)calls inprocess_execution_result. - Regenerate
INDEX.md.
Net 12 deletions, zero insertions of logic. Exactly one [HH:MM:SS] format site now remains in state.rs, inside add_timestamped_log.
Behaviour
No user-visible change. The deleted field was never rendered, and the workflow outcome lines it duplicated continue to reach the Logs and Execution tabs through the global store.
Verification
All four CI commands pass with zero failures:
cargo fmt --check
cargo clippy --workspace --all-features -- -D warnings
cargo check --no-default-features --workspace
cargo test --workspace --all-features
Full workspace suite green, including all 68 tests in wrkflw-ui. No test changes were needed — the existing dual-logging test keeps guarding the app-side behaviour.
Follow-up
JobExecution.logs is dead in the same way: two writers, no readers. Left out of this PR to keep it scoped to what item 4 named, and because one of its writers carries real per-job executor output that deserves a check before deletion. Filed as item 5.
TODO item 3, follow-up from the review of PR #116.
The problem
The TUI keeps two log buffers: the app-local logs vec and the global wrkflw_logging store. get_combined_logs concatenates them with no deduplication, so any code path reporting an event to both sinks rendered it twice in the Logs and Execution tabs.
The ticket named one such message, "Executing workflow". Auditing the crate turned up 24 dual-logged sites, not one:
| File | Sites |
|---|---|
crates/ui/src/app/state.rs | 12 |
crates/ui/src/app/mod.rs | 6 |
crates/ui/src/handlers/workflow.rs | 6 |
Every workflow execution, trigger, and reset was being double-reported.
The fix
At each site the app-side push is deleted and the wrkflw_logging call kept. This is the same resolution PR #116 applied to the Docker and Podman warnings, and it is not an arbitrary choice: the global store stamps its own [HH:MM:SS] prefix and a level glyph, so the surviving copy is the one that renders with a real timestamp and the correct badge.
add_timestamped_log now documents the one-message-one-sink rule so this does not creep back in.
Badges improve as a side effect
LogBadge::classify checks the level glyph in the same tier as the level keyword, so five lines stop misreporting their severity:
Workflow 'x' failed: ...was INFO, now ERROR- the "no workflow selected" lines were INFO, now WARN
Cannot trigger workflow in Success statewas misbadged SUCCESS on the strength of the word in the state name, now WARN
One wording change
At the "Cannot trigger workflow in {state} state" site in app/mod.rs, the workflow name existed only in the app-side copy. The global warning now carries it, and the "Press Shift+R" hint moved to the same sink so it stays adjacent to the warning it explains rather than rendering several lines away.
Tests
executing_workflow_is_logged_once_across_both_sinksasserts the message never reaches the app buffer and appears exactly once in the merged view. It keys off a unique workflow name because the global store is process-wide and never cleared.dual_logged_events_do_not_also_land_in_the_app_bufferpins the app-buffer delta for five more actions.reset_workflow_statuslegitimately keeps one app-only trace line.- The badge fixture in
models/mod.rsgains glyph-prefixed shapes covering the classification this change relies on.
Both new tests were confirmed to fail when the duplicate push is reintroduced.
Verification
cargo fmt --check, cargo clippy --workspace --all-features -- -D warnings, and cargo test --workspace --all-features all pass locally.
Note on the second commit
chore: regenerate INDEX.md is a large diff and almost none of it belongs to this change. The committed index dates from April, against 158 files; the tree is now at 202. CLAUDE.md asks for a regeneration after code changes, so it is included, but kept as a separate commit so it can be dropped without touching the fix.
Follow-up to #116 and #119. Three log lines on the diff-filter path still rendered ??:??:?? in the Execution/Logs tabs:
- the two per-warning / per-parse-error sub-items in
check_diff_filter_results - the "Diff filter: evaluating triggers" line in
spawn_evaluation
Why they were left behind
process_log_entry extracts the timestamp from the [HH:MM:SS] prefix and then trims the content that follows. The sub-items encoded their nesting as two leading spaces, so timestamping them would have flattened them against the summary line they hang under. #116 kept the indentation and dropped the timestamp.
That's a false choice. A printable glyph survives the trim where whitespace cannot, so the nesting and the timestamp can coexist — the same trick the curl: recipe lines already use.
Changes
The timestamp fix
- New
NESTEDsymbol (↳, U+21B3) incrates/logging/src/symbols.rs, alongside the other TUI-only glyphs. - The two sub-item sites now call
add_timestamped_logand mark nesting with the glyph instead of two spaces. - The "evaluating triggers" line now calls
add_timestamped_log. It had no indentation problem, it was simply missed.
Closing the door behind it
add_log is now private. It was pub with exactly one caller — add_timestamped_log itself — and all three rounds of this bug (#116, #119, and this PR) came from a site that reached past the timestamped helper to get to it. Private makes the next attempt a compile error instead of a cosmetic bug nobody notices until a screenshot.
The filter half of the bug
Restoring the timestamps made these lines readable but not yet reachable: LogFilterLevel::matches and the badge classifier in process_log_entry kept independent keyword lists. The classifier matched lowercase warning, the filter only the uppercase WARN badge word — so a warning sub-item was badged WARN and then hidden by the Warning filter that names it.
Both now classify through a single LogBadge type, so the badge the user sees and the filter that keeps the line can no longer disagree. Two visible consequences, both corrections:
- Lines badged
INFOare now shown by the INFO filter. Previously a line had to literally containINFOor the●glyph, so most routine output was badgedINFOand hidden by the INFO filter. - A line is shown by exactly one filter, the one matching its badge. Previously a line containing both
errorandsuccesswas badgedERRORbut surfaced under Success too.
LogBadge::Plain keeps the dim styling the default branch had, distinct from the INFO accent used for active work, so the panes look the same as before.
Unblocking CI
The Clippy job was already failing on main — current stable clippy flags useless_borrows_in_formatting, for_kv_map, and question_mark in wrkflw-runtime and wrkflw-executor, neither of which this branch otherwise touches. CI pins dtolnay/rust-toolchain@stable with no rust-toolchain.toml, so this appeared without any code change. Six mechanical fixes are folded in here so the pipeline can go green.
Testing
check_diff_filter_results_timestamps_nested_sub_itemsdrives the real handler and asserts every line in the burst parses to a real timestamp, that sub-items carry the glyph after the renderer's trim, and that summary lines are not marked nested.spawn_evaluation_logs_timestamped_linecovers the evaluation-start line throughtoggle_diff_filter.leading_whitespace_is_trimmed_but_a_nesting_glyph_survivespins the parser behaviour that motivates the glyph, so the reasoning does not have to live only in a comment.every_filter_shows_the_lines_it_badgeswalks a representative set of real log lines and asserts each is shown by exactly one filter, the one named after its badge. This is the invariantLogBadgeexists to hold.diff_filter_warning_lines_survive_the_warning_filterties that invariant back to the real handler: the warning sub-item must pass the Warning filter, the parse-error sub-item the Error filter.
cargo fmt --all --check, cargo clippy --workspace --all-features -- -D warnings, and cargo test --workspace --all-features are all clean locally.