fix/1140-apple-app-names
main
dev
feat/1109-macos-signing
feat/2069-indicator-tags
fix/1128-channel-key-history
feat/1129-sponsor-prompt
fix/1118-factory-reset-delete-on-send
fix/1122-notify-logging
fix/1896-client-slot0-public
feat/1054-factory-reset
fix/1111-osm-identity
feat/523-land-indicators
feat/1108-macos-artifact
feat/1014-ios-app-review
feat/1013-ios-signed-lane
feat/1068-radio-channel-notify
docs/1091-epic-chain
docs/1053-play-automation
fix/1065-vercode-floors
feat/1012-ios-signing-toolchain
docs/958-ios-appstore-plan
fix/1033-scan-location-message
feat/693-offline-app-settings
fix/1009-release-linux-2404
release/1.7.0
epic/870-console-gating
feat/759-fake-radio-integration
feat/758-fake-radio-ci
feat/757-fake-radio-behaviors
feat/756-fake-radio-core
epic/870-remote-cli-permission
feat/842-sync-is-truth
feat/975-settings-restructure
feat/966-clear-stale-history
feat/965-stale-blob-sweep
docs/962-epic-chain
docs/962-reclamation-plan
fix/943-952-storage-migration
feat/940-isolation-integration
feat/939-scope-key-lifecycle
feat/938-blocked-per-radio
feat/937-storage-service-migration
feat/936-scoping-harness
docs/853-isolation-plan
fix/754-discovery-per-radio
feat/920-message-rows
chore/904-docs-proof
feat/919-blob-isolate
fix/927-ci-spike-flag
feat/917-storage-save-path
chore/904-docs-only-ci
docs/857-interop-plan
feat/844-per-radio-notify
fix/812-land-814-815-on-dev
epic/816-integration
epic/814-discover-regions
epic/813-region-protocol
epic/815-channel-scope
docs/812-region-scoping-plan
release/1.6.0
fix/793-log-composer-budget
feat/703-forced-resync
docs/755-plan-flash-fix
docs/755-fake-radio-plan
docs/750-contact-naming-plan
feat/702-lossless-serial
feat/668-contact-sync-guard
feat/610-contact-card-receive
fix/717-linux-web-mtu
feat/638-top-toast
fix/589-late-reply-log-only
fix/663-triage-internal-actors
fix/652-release-prerelease-flag
feat/619-contact-identity-uri
fix/581-ack-adoption
epic/568-export-import
fix/580-caplog-partial
release/1.5.0-beta.1
task/501-deviceui-logging
chore/542-assets2
chore/542-assets
docs/519-claudemd-web
release/1.4.0
feat/483-button-buzzer-ui
fix/497-mention-verbatim
fix/279-send-width
docs/478-docs-home-research
release/1.3.0
fix/443-battery-optimization-warning
fix/447-close-labeler
fix/445-windows-sqlite3-native-assets
feat/429-companion-sync
fix/395-dm-channel-send-wedge
feat/427-localize-log-share
feat/393-app-log-share
fix/share-icon-android
fix/389-back-nav
feat/385-storage-health
release/1.2.2
feat/367-store-consolidation
chore/365-cut-1.2.1
fix/363-pin-db-path
feat/351-contact-settings-menu
feat/349-window-geometry
feat/355-migration-merge
integration/290-306-335
release/1.1.2-rc.3-allplatforms
fix/309-path-len-decode
chore/298-path-logging
fix/285-ingest-timelog
fix/252-block-outgoing-dm
feat/64-observer-config
feat/batch1-ux
chore/offband-rebrand
v1.8.0
v1.7.0
v1.6.0
v1.5.0-beta.1
v1.4.0
v1.3.0
v1.2.2
v1.2.1
v1.2.0
v1.1.2-rc.3-allplatforms
v1.1.2-rc.3
v1.1.2-rc.2
v1.0.0
observer-g2-rc3
observer-g2-rc4
observer-g2-rc5
observer-g2-rc6
observer-g2-rc7
observer-g2-rc8
observer-g2-rc9
preview-1.8.0-1
v1.1.0-rc.1
v1.1.0-rc.2
v1.1.0-rc.3
v1.1.2-beta.1
v1.1.2-beta.2
v1.1.2-beta.3
v1.1.2-ble-test
v1.1.2-queuediag
v1.1.2-rc.1
v1.1.2-rc.1-b45-nearbyrep
v1.1.2-rc.1-b46-pathhash
v1.1.2-rc.1-b47-tracewidth
v1.1.2-rc.1-b48-repeatertrace
v1.1.2-rc.1-b49-maptrace
v1.1.2-rc.1-b50-tracefix
v1.1.2-rc.1-b51-originstrip
v1.1.2-rc.1-b52-channelqr
v1.1.2-rc.1-b53-viarepeater
v1.1.2-rc.1-b54-rxfed
v1.1.2-rc.1-b55-topology
v1.1.2-rc.1-b57-routeux
v1.1.2-rc.1-b58-toporeadout
v1.1.2-rc.3-b59-pathlen
${ noResults }
6 Commits (43ec4d2fa3aaa36d8ea786d145268b8a2bf3f070)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
1b38c674e4 |
feat(#965): per-key migration timestamps and the stale-blob sweep
Epic A of #962. The engine only; the manual action is #966. A1 wrote the diagnosis as failing tests first, and they were red for the right reason: no sweep existed and six BlobStore methods did not. Also lands a control test asserting that DELETE alone does not shrink the database, which passes today and must keep passing, since it is the entire reason a VACUUM step exists. A2, the timestamps. A second blob, __msgrows_migrated_at, keyed by store key. Deliberately NOT a reshape of __msgrows_migrated: that marker is read on every save and every load through a cache, so changing its shape would put a migration of its own on the hottest storage path. Owner decision 2026-10-22. An install that migrated before this ships has no timestamp, so backfillMigratedAt stamps it on FIRST SIGHT and the sweep skips anything it stamped this pass: updating the app can never delete a blob on the spot, and every key gets the full 30 days measured from when we first knew about it. A3, the sweep. Eligibility is age plus a fresh verification, never the marker alone, which is the difference between "we recorded that this migrated" and "we just confirmed it". missingFromRows returns null for "could not tell" and an empty list for "verified nothing missing", and those are deliberately different values: collapsing them is how the only copy of an unreadable history gets deleted. A4, one VACUUM, only when something was deleted. A failure there is logged and tolerated: the deletes already happened and were each verified, so it costs space and not data, and the next sweep retries it. The comparison rule now lives in ONE place, _absentFrom, used by both the migration gate and the sweep. The gate still decodes its own blob, because a second compute over multiple megabytes on the migration path would cost more than the duplication saves, so the rule is shared even though the decode is not. Wired into main.dart's existing post-frame callback, beside the #906 exit-info collector, for the same reason: storage hygiene must never be on the path to a usable screen. The catchError sits at that fire-and-forget call site rather than inside run(), so #966's button still gets a failure it can report. scoping_inventory gained the new key, because the #853 gate caught it exactly as designed. Its verdict is globalOnPurpose for the same reason as the marker: it is keyed BY store keys that already carry the radio. 15 tests, the plan's whole acceptance bar: the 29-day and 30-day boundaries; one missing row refuses AND the same key clears once its rows are complete, which is what proves the gate discriminates rather than never deleting; an undecodable blob refuses; an unstamped key is stamped not deleted; an unmigrated key is untouched; contacts, discovered_contacts and path_history_ are asserted non-candidates BY NAME; a DM key sweeps through the DM converter; and every kept blob is byte-identical across the sweep and VACUUM with the row count unchanged. A5, verification against a copy of the owner's real database through the production sweep: blob keys: 470, migrated history keys aged: 47 cleared 40, refused 0, 17367040 bytes returned (16.56 MiB) kept keys missing: 0, byte-changed: 0 integrity_check: ok, rows 29949 -> 29949 47 aged and 40 cleared because the other 7 migrated through the empty-blob path and never had a blob to reclaim. Full suite 1,469 pass, 1 skipped, up from 1,454. analyze clean, format clean. Task: meshcore-open-5vt Agent Mail: TealGlen Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
3 weeks ago |
|
|
5f79c6e1db |
fix(#943,#952): act on the Gemini gate findings
Two of five findings were real.
Log the downgrade refusal BEFORE the path lookup and the throw. onUpgrade runs
inside drift's beforeOpen, so it fires on whichever isolate issued the first
query, and whoever catches the exception decides what the user sees. A
downgrade that only stopped background work, with nothing in the log, would be
the quiet failure this branch exists to replace. The log line now lands first,
so the record exists however the exception is handled.
Strengthen the recovery bench. It asserted row COUNTS, so a converter
producing the right number of rows with wrong contents would have passed.
It now also checks that every identified blob record has a row carrying its
identity, and that no payload is empty, both derived from the raw blob rather
than from the converter under test so the check is not circular. Records with
no messageId are not identity-checked, because reproducing the `x:` fallback
key here would make it circular again; the count assertion still covers them.
Verified on a copy of the owner's database with the trap reconstructed (the
6,308-message channel removed from the marker plus one planted row the blob
does not contain): the key migrates, and all 6,308 identities are present.
Then verified the new assertion can FAIL, by planting an empty payload the
migration does not overwrite:
Expected: empty
Actual: WhereIterable<String>:['{}']
Three findings were rejected, with reasons in the PR body: the duplicate
merge-key collapse is pre-existing and unchanged by this diff, the claimed
migration/blob-save race cannot occur because of the re-check inside the
per-key lock, and the path_provider call is bounded and already proven.
Task: meshcore-open-lkp
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
3 weeks ago |
|
|
e692562765 |
test(#952): prove the stuck keys recover against a real database
The unit tests pin the invariant on fixtures. This runs the real migration
code over a copy of an actual database and finds the stuck keys by shape,
without using the code under test: a key that has rows and is absent from the
migrated marker.
On the owner's install, all 5 such keys migrate, 0 still refused:
keys with rows: 41, marked migrated: 36, rows but unmarked: 5
MIGRATED blob=237 rows 238 -> 238 ...psk_43636021
MIGRATED blob=5646 rows 5648 -> 5649 ...psk_8b3387e9
MIGRATED blob=1578 rows 1585 -> 1585 ...psk_9cd8fcf2
MIGRATED blob=6308 rows 6313 -> 6313 ...psk_e3c26491
MIGRATED blob=16 rows 16 -> 16 messages_d549bbfbd060dae1...
psk_8b3387e9 is the one worth reading twice: 5,648 rows became 5,649. That key
held 3 rows the blob does not have AND one blob record that had never reached
a row. The old gate saw unequal totals and refused, so that record stayed
unreachable from rows indefinitely; the superset check inserts it and then
verifies it is there.
Asserts no key loses records, every migrated key ends up marked, and every
retained blob is still present, so the run is non-destructive beyond the copy
it works on. Reports and skips when BENCH_DB is absent, and says explicitly
that a database with no stuck keys is not a pass.
Task: meshcore-open-lkp
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
3 weeks ago |
|
|
c4e28b653d |
task(#937): B4 Epic B verification, rehearsed on a real prefs file
Rehearsed the legacy purge against a copy of the owner's live prefs, 2,149 keys and 4,277,260 bytes: purged 2,095 path-history keys, global observation list: yes legacy keys left: 0 prefs keys remaining: 53 this radio's scoped history: intact That last line is the point of the rehearsal. The purge removes the unscoped legacy keys and leaves the scoped history alone, which is only safe because the two live in different backends: new keys in drift, legacy in prefs. **The harness reports its own timings as NOT the real cost, loudly.** It measured a 4 ms worst UI block, and that number is worthless: the SharedPreferences test mock is an in-memory map and does not do the whole-file re-encode and rewrite every real `remove` performs on Windows. A time threshold here would pass no matter how slow production was, so there is deliberately no time assertion. The real figures come from simulating the plugin's actual behaviour against the same file: 12.7 s total, mean 6.1 ms, 32 ms for the first removal falling under 1 ms by the last as the file collapses 4.28 MB to 45 KB. That is why the purge yields every 20 removals. Flagging it rather than quietly reporting 4 ms, because a harness that measures the mock instead of the mechanism is the third false-green this session would have produced: the #919 heartbeat that read 0 ms, the #920 readback gate that compared a converter to itself, and this. **The 45 KB endpoint also means #923 is largely neutralised in practice.** Every remaining prefs write drops from ~32 ms to well under 1 ms, because 86% of the file is gone. The mechanism is untouched and #923 stays open, as the plan says, but its blast radius is now small. **Suite:** 1,414 pass, analyze clean, format clean. Epic B is complete: #855 deleted, #854 and #856 scoped per radio, #924 resolved, and the inventory's known-holes set is empty. Citadel: meshcore-open-sgu Refs #937, #854, #855, #856, #923, #924, #853 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
3 weeks ago |
|
|
dbe52164da |
task(#920): B4 Epic B verification against the owner's real database
Ran the migration over a copy of the live 41.17 MiB database, 138 message history keys: keys migrated : 138 of 138 records in blobs : 64,883 rows stored : 64,883 elapsed : 4,382 ms Exact parity, no refusals. Every blob is byte-identical afterwards, checked by length and content hash per key, which is the promise that makes this recoverable. Per-message save cost against history length, median of 20: 500 messages 0.124 ms 5,000 0.126 ms 50,000 0.133 ms A hundred times the history for 1.07x the cost. The Feature's "done when" was that receiving a message costs the same at 50,000 messages as at 50, and that is now true. For contrast, the blob path blocked the UI for 381 ms at 9,230 messages. One-time migration cost for the worst key, 5.38 MiB and 9,230 messages, with the heartbeat harness from #919: UI blocked 64 ms wall 447 ms (30 heartbeats) So the first open of the biggest channel after this ships costs a 64 ms hitch on this desktop, once, and the UI stays live through the rest. That number needed a correction before it could be reported. The first run measured 166 ms, because the harness opened the database with a plain `NativeDatabase`, which executes SQL on the calling isolate. Production uses `drift_flutter`'s `driftDatabase()`, which hosts the connection in a background isolate, so the harness was charging the UI isolate for work that does not happen there. Switched to `NativeDatabase.createInBackground`, which is the shape that ships: 64 ms. The parity test refuses to invent data: without `--dart-define=BENCH_DB` it prints that it measured nothing rather than synthesising a database, and it copies the file it is given rather than writing to it. Citadel: meshcore-open-i8t Refs #920, #917, #334 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
3 weeks ago |
|
|
d791c58fea |
task(#919): A5 Epic A verification: isolate gate + block-time benchmark
Two things the Epic needed and did not have: a gate that fails if the work
comes back to the UI isolate, and a number for how much it was blocking.
The gate is structural, not timed. The merge callback reports the isolate it
ran on, and the test asserts that is not "main". `compute` names the worker
it spawns after the callback, so the recorded name also says which worker.
A wall-clock threshold would have been flaky on CI and would not say why it
passed.
The benchmark runs a 1 ms heartbeat on the main isolate and reports the worst
gap between beats, which is the UI freeze an ANR is. It is named `_bench`,
not `_test`, so a bare `flutter test` skips it: it needs a local copy of a
real database and the numbers are machine-dependent.
Measured against the owner's real largest channel, 5,650,194 chars (5.39
MiB), 9,230 messages, on this desktop:
UI blocked wall heartbeats
1 save, inline (pre-919) 381 ms 381 ms 1
1 save, off-thread 44 ms 297 ms 174
5 saves, inline 1,385 ms 1,385 ms 1
5 saves, off-thread 66 ms 1,496 ms 964
8.7x less blocking on one save, 21x on a burst of five. Wall time barely
moves, which is the plan's point: the work is not cheaper, it is elsewhere.
Inline, UI-blocked equals wall and one heartbeat lands in 1.4 seconds; the
UI isolate was gone for the whole operation. Off-thread it beats 964 times
through the same burst.
The first measured support for the burst claim, which until now was
arithmetic. Still desktop, not a phone: that is the owner's device check.
The heartbeat needed a fix before it could be trusted. A periodic timer
cannot fire while the isolate is blocked, so cancelling it the moment the
action returns discards the tick that would have shown the gap, and the
first run of this benchmark duly reported the inline path blocking 0 ms.
Verified on a bare 200 ms busy-wait: no trailing yield gives fires=0,
worst=0 ms; with a yield it gives 200 ms. It now yields before cancelling
and reports the heartbeat count so a dead instrument cannot read as a clean
result.
Citadel: meshcore-open-xxc
Refs #919, #917, #334
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
3 weeks ago |