Brings feat/1014-ios-app-review up to date with dev for PR #1049.
Conflicts: the app name in TESTFLIGHT_GUIDE.md and tools/ios-signing/README.md
keeps "Offband MeshCore" (#1050); channels_screen.dart takes dev's channel
menu (new radio sound tile) and re-applies the #1075 scroll wrapper.
testflight.yml is dispatch-only again, as the push trigger was only for the
owner's TestFlight pass (#1047). REVIEW-NOTES encryption line and PLAN
finding 14 updated to match what App Store Connect shows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two findings from the owner's field report: a Channels screen full of
replacement characters, and a channel he had silenced chirping again.
An erased record is not an empty one. Erased flash reads back 0xFF and
Channel.isEmpty tests for 0x00, so a slot the radio had LOST was adopted
as a channel, rendered with U+FFFD, and then persisted over the client's
own good cached list. One missing validation gate, two symptoms.
- Channel.isErased / isUnusable: an all-0xFF record, or a name that did
not decode, is never adopted.
- A sync that sees an erased slot KEEPS the cached list, reusing the
fail-closed branch #1062 already had for unconfirmed holes.
- restoreChannelsToRadio() writes the cache back to the original slots
with their PSKs, behind a confirmed banner action.
Radio notify was keyed by slot, not identity. #544 landed after #1061 and
keyed its in-memory map by slot index; the slot-identity handler cleared
the slot's messages but not its notify mode, so a channel that moved
slots handed its mode to the new occupant and lost its own.
- _channelNotifyModes is keyed by PSK hex, matching channelNotifyModesByRadio
(#848) and the PSK-keyed history (#194).
- A reply whose slot does not resolve to a confirmed channel is dropped.
- setChannelNotify takes a Channel and resolves its CURRENT slot at send
time, so a stale handle cannot address the wrong one; unresolvable is a
visible refusal, not a silent write.
Both guards proved against planted defects: forcing isErased false turns
5 tests red including the cache overwrite, and restoring the slot-keyed
map yields Expected: null, Actual: RadioChannelNotify.off.
flutter analyze clean, 1885 tests pass. New strings English-only with
untranslated.json regenerated.
Task: meshcore-open-75d
Agent: CalmBay
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gemini gate (gemini-2.5-pro) on the pre-push stack, four findings:
- Held channel message frames are now keyed per radio: verifying the
unbounded-growth finding exposed that frames held from radio X would
have flushed into radio Y's conversations after a radio switch. A
parked radio's frames flush when that radio next completes a sync. No
cap is added: held frames are popped messages, dropping one is
guaranteed loss, and growth is traffic-bounded (<=172 bytes/frame).
- _loadChannelMessages merges messages that arrived during the disk
read instead of clobbering the list, so the slot-identity reload
cannot hide a message the held-frame flush just filed (disk was never
at risk: saves merge by identity, #343).
- sendChannelMessage refuses the write for a slot this connection has
not confirmed (the persisted copy would fall back to the legacy
slot-index key and be orphaned), and the chat screen surfaces a
persistent error at every send path instead of greying anything.
New English UI string; untranslated.json regenerated (#1063).
Three integration tests cover the fixes (#1064); with the guards
behaviorally disabled exactly those three fail (+3 -3). Full suite
1807 passed / 1 skipped. OUTCOMES.md records the gate and dispositions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The owner waived the pre-PR hardware gate for feature 1061 (no way to
reproduce on demand); validation moves to live testing after merge.
Merges still require the explicit per-PR grant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The plan of record (four approved parts, the Epic B re-scope) and the
outcomes: what each Epic shipped, the slot-level ingress gap the
integration tests exposed, the positive and negative test evidence, and
what stays open until the owner's hardware pass.
Issue: #1064
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The advert is requested from the device-info handler. On Android that is
genuinely after MTU negotiation, because requestMtu(185) is awaited before
service discovery and before device-info is asked for; elsewhere the platform
reports the negotiated value through the MTU stream, so the request could in
principle precede Dart observing it. It does not matter in practice, since the
link-layer negotiation has already happened, today's advert fits inside even
the 20-byte floor, and TAG_CONTINUE covers overflow.
Flagged rather than left to be discovered, and checked in the connect path
rather than asserted from the contract.
Also notes that the Windows .exe is only the runner shell, so its timestamp
does not move when Dart changes. The payload is data/app.so, which is what was
verified and re-delivered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Corrects this report. It previously said the rotation hazard could not be
fixed on feat/523-display-indicators because that branch was checked out in
another worktree. That was over-respecting a lock: git refusing to commit to a
branch checked out elsewhere is checkout bookkeeping, not a permission, and
ownership had already been transferred. Removing a worktree frees the ref and
does not delete the branch.
The 523 worktree was verified completely clean before removal, so nothing of
ChartreuseGate's was at risk, and the branch move was confirmed a genuine
fast-forward with merge-base --is-ancestor before relying on it. No history
rewritten.
The sweep that followed found a SECOND carrier I had missed: test/combined-1054-523,
a test-build branch, which is exactly the kind someone flashes from. Finding it
is the argument for sweeping rather than fixing the branch you were told about.
git branch --contains gives the complete carrier set; all three now hold
0x1A/0x1B, and the bad commit was never pushed to any remote.
#1057 factory reset merged in so the build carries every unmerged client
feature and that test branch is fully superseded, verified as a file superset.
Re-checked after that merge that it had not resurrected the pair.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both artifacts come from the combined branch so one install exercises the
advert, channel notify, rotation and all of #523 together.
The APK signature is verified rather than assumed: apksigner reports
CN=Benjamin Wiechel. A worktree without key.properties silently produces a
debug-signed APK, so it was copied in first.
C:\Apps\Offband_MeshCore was deliberately NOT overwritten. The copy there is
from Nov 2 and is what he has been testing; this build is unreviewed and
carries another session's feature merged in, so replacing his working copy
while he slept is not a call to make for him. It went to a sibling folder.
Also names the four things worth a human look, led by the err-5 fallback on a
radio that predates the advert, since that is the one path I could not verify
without his hardware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three additions to the outcomes report, all evidence rather than narrative.
The facts I read from the firmware tree instead of from Agent Mail, since
taking numbers from mail is what caused the collision: the mode encoding
(Unset 0, All 1, Mentions 2, Off 3), the reason the collision was silent
rather than refused ('stored < kChanNotifyCount' and nothing else, so every
rotation value 0 to 3 is a valid notify mode), and the wire numbers.
The FIRMWARE_VER_CODE audit, naming the only two gates that read it with their
lines, both of which are the exception he ratified under #1066 and both
untouched.
And the one instruction I could not satisfy: he asked for the err-5 fallback to
be tested against current firmware. Every branch of it is unit-tested, but
verifying it on a real radio is bench work on his hardware, and the fake-radio
harness is still only a PLAN on dev, so there is no software radio to drive it
against. Said plainly as a handoff rather than left to look complete.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three decisions are his: push/PR, whether feat/523-display-indicators gets
rebased onto the rotation fix, and whether the CHAN_NOTIFY gating precedence I
shipped stands if firmware rules otherwise.
Records what I could not finish and why, rather than reporting the night as
clean: the silent-corruption path still exists on ChartreuseGate's branch,
which is checked out in their live worktree and so cannot be committed to from
here. The fix is on a derived branch instead so their work is neither blocked
nor overwritten.
Also records a repair I made to his machine. Every Flutter Android build on
this box was failing, flutter doctor included, from a clean tree with none of
my code involved: Android Studio updated and left AndroidStudio2025.3.1 as a
half-removed cache directory with no .home, and Flutter's install scan chokes
on it. I created that one file. Additive, reversible, and named in the doc so
he can undo it.
And the guard that lied: the first offset-87 test compared only named
capability getters, so a leak into a caps bit with no getter yet moved nothing
it could see, and it PASSED with a planted defect in place. Strengthened to
compare the raw caps byte, re-planted, went red. Recorded because a green bar
on an unproven guard is the failure mode, not the test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plan of record for Feature #1053, owner approved 2026-11-03. Tagged releases go to Play closed testing or straight to production by dispatch, and closed testing promotes to production. Test builds never upload, so no version codes are spent on them.
Two Gemini 2.5 Pro passes applied after approval: a tighter keyless-identity condition, a provenance attestation on the AAB, a tag-on-main check, and idempotent, non-destructive track updates. Section 12 records each finding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gemini gate (gemini-2.5-pro) on the pre-push stack, four findings:
- Held channel message frames are now keyed per radio: verifying the
unbounded-growth finding exposed that frames held from radio X would
have flushed into radio Y's conversations after a radio switch. A
parked radio's frames flush when that radio next completes a sync. No
cap is added: held frames are popped messages, dropping one is
guaranteed loss, and growth is traffic-bounded (<=172 bytes/frame).
- _loadChannelMessages merges messages that arrived during the disk
read instead of clobbering the list, so the slot-identity reload
cannot hide a message the held-frame flush just filed (disk was never
at risk: saves merge by identity, #343).
- sendChannelMessage refuses the write for a slot this connection has
not confirmed (the persisted copy would fall back to the legacy
slot-index key and be orphaned), and the chat screen surfaces a
persistent error at every send path instead of greying anything.
New English UI string; untranslated.json regenerated (#1063).
Three integration tests cover the fixes (#1064); with the guards
behaviorally disabled exactly those three fail (+3 -3). Full suite
1807 passed / 1 skipped. OUTCOMES.md records the gate and dispositions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The owner waived the pre-PR hardware gate for feature 1061 (no way to
reproduce on demand); validation moves to live testing after merge.
Merges still require the explicit per-PR grant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The plan of record (four approved parts, the Epic B re-scope) and the
outcomes: what each Epic shipped, the slot-level ingress gap the
integration tests exposed, the positive and negative test evidence, and
what stays open until the owner's hardware pass.
Issue: #1064
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apple stores the owner's standard-encryption answer as false with no declaration, on all three answered builds, so the key restates that answer and later builds should skip the question. Corrects finding 14, which said false would contradict it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set to true with no compliance code, validation failed with 90592 in run 41179059212. No code exists, because no document is required with France excluded, and false would contradict the answer given to Apple. The owner answers one question per build instead.
Epic #1014, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The published privacy policy lists it, and the notes should match.
Epic #1014, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision for the first App Store submission. Guideline 2.5.19 bars hidden features, and a personal sponsorship link is an unclear fit. With no unlock on iOS, CoreScope is unreachable there, so the privacy label becomes Data Not Collected, and the notes no longer need the disclosure. Other platforms are unchanged.
Epic #1014, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The display name now matches the App Store record. The notes and listing state what block really does, including hiding channel senders, and say plainly that there is no report button. Verdicts are on #1014.
Epic #1014, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Within Apple's field limits as read today, with sizes from Apple's current specification.
Epic #1014, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discloses the Experimental section, and covers user-generated content, the donation link and encryption.
Epic #1014, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CoreScope is declared per the plan's choice 2, and every other host is read against Apple's definition of collect.
Epic #1014, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 41171670352 validated and uploaded 1.7.0 (41171670352), so the trigger has done its job. testflight.yml is back to manual dispatch only. The plan records what that run proved.
Epic #1013, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The assertion stayed green with the flag off, and run 41171197215 listed sqlite3.framework and llamadart.framework, so the plan's hypothesis was wrong. The flag is gone, the assertion still guards every build, and it is proven able to fail against an empty app. The plan now says so, and records the run ID as the build number. Negative steps discard their output so their error lines do not annotate a passing job.
Epic #1013, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the Environment rule that would refuse the first signed run, adds IPA signature and altool validation checks, aligns the privacy goal with the CoreScope choice, and marks rounds 1 to 3 done. Verdicts are on #958.
Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finding 6 and owner round 2 now use app.offband.meshcore.ios, after Apple refused the original ID.
Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four Epics: Windows signing toolchain proven on a Mac runner, signed IPA and TestFlight lane, App Review package, Integration Testing. Every finding cites the research on #958.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds the owner-approved parity design (docs/initiatives/standalone/975-settings-restructure/tones-screen-parity-mockups.html) against meshcore-firmware 3e788e2a:
- #986: the six event tones (DM, Channel, Room, New contact, Ack, Sent) on Buzzer and tones, one shared picker with a play button per row (0x0F/0x10/0x11). DM is one tone for every direct message.
- #970: all eight custom slots listed; the editor reads the stored tune back (0x12), checks typing with a port of firmware's rtttlProblem(), and saves in transport-sized chunks (0x0A), restarting once if displaced.
- #971: Radio tone in the channel sheet and chat menu, Default naming what it plays.
- #987: Screen (text sizes, Screen off, time zone with a phone-matched suggestion) and Battery (full point, set here, back to auto) under Node Settings, gated on caps2 0x40.
Caps2 0x10 is now TUNES (slots, channel tone, event tones, preview). Also corrects the mockup's zone count: 40 zones plus not set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved 2026-10-25 with four decisions: Screen and Battery under Node Settings, a separate play button, no way back to radio default, and the Menus and lists / Messages labels. Built against meshcore-firmware 3e788e2a. Covers #970, #971, #986, #987.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command is 0xC5 sub-codes 0x0D/0x0E with caps byte 2 0x20, not a new 0xC7. Power-save availability comes from ps_ok in every GPS reply. The radio raises a sleep interval of 1 to 299 s to 300. The buzzer-only examples are now boards that actually lack a button; the T1000-E has both. Facts confirmed by AmberForge on Agent Mail 2026-10-24 and verified on firmware-base.
Agent: MistyLynx (session 6c0d5797)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved 2026-10-23. Six numbered state cards drawn against the real SettingsShell widgets, with behavior bullets as the build contract and a rationale section, following the firmware GUI mockup convention. Self-contained: inline SVG icons, system fonts, no network references.
Covers GPS off, on, Power save backed off, a build without Power save, the Settings menu before and after, and the button and buzzer split. Serves #969 as well as epic #975.
Agent: MistyLynx (session 6c0d5797)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner asked why there were no UI mock-ups to review. There was no good
answer: this Epic's card was built and installed on both his machines without
him ever seeing the design, and the plan had no step requiring otherwise.
So the plan now has B0: mock-ups first, owner-approved before any of it is
built, as a committed HTML file beside the plan in the format his firmware
initiative already uses. epic-b-storage-mockups.html is that file: eleven
numbered variants with real Material 3 tokens, a 360-wide pane, example values
from his own install, and the behaviour to build as bullets under each.
Owner-approved 2026-10-23.
The confirmation (B1a, variants 8 to 10). It states how many copies, roughly
how much space, how many messages are NOT affected, and that it cannot be
undone, with the count on the action button so the scope is the last thing
read. Cancel, a tap outside and back all decline. Nothing eligible opens no
dialog at all, because a dialog that only says zero is not worth opening.
That needed a preview pass: verify every eligible key and total it without
deleting, so the numbers are real rather than estimated. Verification now runs
twice for a confirmed clear, once to preview and once under the key lock. That
is the cost of saying what will go before it goes. approximateBytes is named
for what it is: the sum of blob lengths, not what VACUUM returns, because page
rounding moves the real figure; the dialog says "about" and the result reports
what actually changed.
Writing the preview found a real bug, and it was about to bite him. The
"skip anything stamped this pass" guard exists so installing an update never
deletes a blob on sight, and it was being applied to the button as well. Every
install that migrated before #962 has an empty __msgrows_migrated_at, so the
first press stamped everything and cleared NOTHING. His install is exactly
that: 47 migrated keys, none stamped. He would have pressed the button on a
70.9 MB database with 16.6 MB of reclaimable blobs and been told "nothing to
clear".
Fixed as eligibleByAge, one static rule both preview and run share, so the
dialog cannot promise what the sweep will not do. An explicit press includes
newly stamped keys; the automatic sweep still refuses them. Three tests on the
rule directly, plus one proving the automatic path refuses while the press
clears.
Label stays "Clear Stale History Now" rather than "Compact", owner's call and
the better argument: the name generalises to clearing stale history for other
reasons later, such as #963's retention limit.
14 widget tests and 34 storage tests. Full suite 1,502 pass, 1 skipped.
analyze clean, format clean.
Task: meshcore-open-gan
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
965 Epic A the timestamp and sweep, 966 Epic B the manual action, 967 Epic C
Integration Testing. All three are sub-issues of #962, with Citadel tasks
meshcore-open-5vt, -gan and -2jd chained A to B to C.
This is a follow-up only because of a sequencing mistake on my part: the plan
merged with epic_chain empty, and the grant validator requires a non-empty
list while the mint path indexes its first and last entries, so the plan as
merged could not have minted anything. The Epic issues need to exist before a
plan PR merges, which is how #853 did it. Noted in section 5 so the next plan
does not repeat it.
No other term changes: budget 3, owner verification, 168-hour expiry, chain
true, no flash.
Task: meshcore-open-8xj
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner, 2026-10-22: "X = 30, sweep once per launch, timestamps in separate
blob, button reports refusals, agreed." All four as recommended.
Section 9 becomes a Decisions table carrying the reason for each, so the
trade-offs are on the record rather than only the outcome.
Every X placeholder is now the literal 30 days: at a glance, scope, A3, the
can't-tell row for a key with no timestamp, the acceptance bar and done-when.
A plan that goes to build with placeholders in it invites someone to pick a
number themselves.
Three things sharpened while propagating them:
- A3 states the once-per-launch wiring, since the trigger is a real decision
and not an implementation detail.
- B1 states that the manual action REPORTS refused keys in its result, not
just in the log, which is what decision 4 actually asks for. It previously
only said it shared the sweep's refusal path.
- The acceptance bar tests the boundary at 29 days, not just the middle of
each range.
Noted that 30 days is a named constant, so changing it later is one edit and
the tests can set it directly rather than manipulating clocks.
Task: meshcore-open-8xj
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The owner asked whether VACUUM would wipe out non-migrated blobs. It cannot:
it rebuilds the file to reclaim pages the deletes already freed and never
removes a row that still exists. Measured on a copy rather than asserted.
deleted on purpose : 40 migrated history blobs
kept : 291 keys, of which 98 UNMIGRATED history
missing after VACUUM : 0
byte-changed after VACUUM : 0
unexpectedly present : 0
message rows : 28,389 -> 28,389
file : 74,350,592 -> 54,927,360
integrity_check : ok
Every kept blob was hashed before and after and came through identical.
The consequence is worth more than the reassurance, so it is now diagnosis
row 11: ALL the risk is in the DELETE selection and none of it is in VACUUM.
VACUUM makes a wrong selection permanent but never selects anything itself,
which is why the safety budget sits on the per-key re-verify and the
key-pattern restriction.
Added to the acceptance bar as a test: every kept blob byte-identical after
VACUUM, unmigrated history keys asserted present by name, row count unchanged.
Task: meshcore-open-8xj
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The #917 plan deferred this as a separate owner-gated step. The owner opened
that gate on 2026-10-22 and specified the design: a per-key time marker, an
automatic clear X time after a key migrates, and a manual "Clear Stale History
Now" action.
Ten diagnosis rows, each measured rather than asserted. The two that shape the
design:
- DELETE frees NO disk space. 74,350,592 bytes before deleting all 40 migrated
blobs on a copy of the owner's database, 74,350,592 after. VACUUM is what
shrinks the file: 54,919,168 bytes, -18.5 MiB in 0.6s, integrity ok, all
28,386 rows intact.
- Migration is lazy and per key. His install had 40 keys migrated and 98 not,
because those radios had not been reconnected, so an install-wide clock
would delete nothing for a long time and then a lot at once.
Also records what the retained blob does and does not protect, because
"keeping a backup" is the wrong mental model: it is a frozen snapshot of the
pre-migration history, and everything that arrived since has been single-copy
from the instant it landed, blob or no blob.
Three Epics, merge budget 3, owner verification per Epic because this deletes
data. Five can't-tell branches, four of which stop; the one that proceeds is
a failed VACUUM, which affects file size only and follows deletes that were
each individually verified.
Four choices for the owner, with recommendations: X = 30 days, sweep once per
launch, timestamps in a separate blob rather than reshaping the hot marker,
and the manual action reports keys it refused rather than only logging them.
epic_chain is deliberately empty: the Epic issues do not exist until this plan
merges, and it is filled in before the grant is minted.
Task: meshcore-open-8xj
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4b8f5731 corrected the plan's downgrade claim but explained it with a drift
version check that does not exist. drift compares the stored user_version
against schemaVersion for inequality and routes a downgrade into onUpgrade;
the pre-#917 build simply had no migration getter, so it got the default that
throws.
The conclusion is unchanged and firmer: every shipped build lacks that
strategy, so nothing done now can let one open a schema-2 file.
Also points at the measurements and the per-decision recommendations now on
the issue, and at the test that pins the behaviour.
Task: meshcore-open-3e1
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan's Epic B outcome said a downgrade to an older build "still finds its
history where it looks for it". True of the data, false in practice, and the
owner hit it: drift refuses to open a database whose user_version is newer
than the binary's schemaVersion, so a pre-#917 build cannot open a migrated
database at all. It never reaches the blobs it would have read.
Correcting it rather than leaving a wrong statement in a merged plan of
record, because the consequence is a release one: once a user launches a
schema-2 build they cannot go back to an earlier release, so a Play rollback
or a halted staged rollout does not help anyone who already launched it. The
real fallback is an older build PLUS a pre-migration database copy, which
loses everything received since.
#943 carries the open decisions. This commit only fixes the claim.
Citadel: meshcore-open-gq7
Refs #943, #917, #920
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner chose A on all three, 2026-10-20. Canon says a plan is rewritten so
the new design IS the plan, rather than leaving answered choices sitting as
options, so section 9 is now a Decisions table and nothing in the document
still asks a question.
1. The legacy untagged data is DELETED, not orphaned: the 2,041
path_history_<contact> keys and the global delivery_observations. Both
regenerate, and here the orphaned data IS the performance problem, which
is why this differs from #754's orphan choice.
2. Path history MOVES into drift rather than gaining a prefix in place, one
migration instead of two.
3. #158 and #190 stay in the Feature as Epic D.
Propagated to the places the decisions change behaviour, not just section 9:
B2 and B3 now state the legacy delete and gate it behind the per-radio
readback, and the two can't-tell rows in section 6 name decision 1 instead
of pointing at an open choice.
grant-terms re-validated: zero errors, chain [936-940], budget 5.
Citadel: meshcore-open-rmr
Refs #853
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#853 had an audit in its issue body and nothing else: no plan doc, no merged
PLAN.md, no grant. That is why #754 and #471 sat written-but-unmerged for
four days with nowhere to go, and why its other children were filed as three
separate bugs against one file.
Written from the canonical FEATURE-PLAN template. Five Epics (#936 to #940),
ordered base-capability first, Integration Testing last per canon.
Three things the research changed from #853's own audit, which was taken at
a69cb998 and has moved since:
- `pending_messages` (#855) is DEAD CODE. Its three methods have no callers
anywhere in lib/ or test/ and the key is absent from the owner's live
prefs. The answer is deletion, not scoping. Three separate bug PRs would
not have caught that.
- #854 and #924 are the same store seen from two directions, not-per-radio
and unbounded-at-2,041-keys. One change fixes both, and because 86% of the
prefs file is that store, it also cuts most of #923's write cost as a side
effect.
- #190's named root cause was already addressed by #194 and #472, so the
plan carries a verdict task rather than a fix, with evidence either way.
Epic A exists because of what tonight taught: #853's acceptance says every
store has "a verified scoping verdict" and the only way to check that today
is to re-read an audit by hand. A1 makes it a test that fails when a surface
is unclassified or a new global key appears. Same lesson as #927, where a
comment saying a flag must never reach dev did not stop it reaching dev.
grant-terms validated against the real parser: five epics, merge budget 5,
owner verification, 168-hour expiry with its basis. Zero errors.
Three owner choices in section 9, each with a recommendation and reasoning,
all three about what happens to existing untagged data or where it lives.
Citadel: meshcore-open-rmr
Refs #853, #854, #855, #856, #507, #158, #190, #923, #924
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The numbers, and the two places the implementation deliberately differs from
what this plan said: the blob is never deleted rather than deleted after a
verified readback, and a row holds the per-message JSON rather than a column
per field. Both are narrower or stricter than the plan, and a plan that does
not say where it was departed from is not a record.
Citadel: meshcore-open-i8t
Refs #920, #917
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan gated the smaller stores on measurement. The measurement came back
"no", so the plan says so, with the numbers and with the two findings that
fell out of taking it (#923, #924). A gate that resolves silently is not a
gate.
Citadel: meshcore-open-xxc
Refs #919, #917
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first version was invented YAML in an untagged fence. grant.py requires a single fence tagged grant-terms containing JSON with keys epic_chain, chain, verification, merge, and optionally flash/expires_after_hours.
epic_chain is a list of distinct ISSUE NUMBERS, so the Epics had to exist first: 919 (A, isolate offload), 920 (B, normalise to rows), 921 (C, integration testing). Verified with grant.parse_terms against the file: zero errors.
Refs #334
Agent: TealGlen (session c61ce804)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured diagnosis: StoredBlobs holds one JSON blob per key; saving one message decodes, rebuilds and re-encodes the whole channel history. 134 ms per message at 9230 records against 0.002 ms for an append, so 67000x more work than needed, scaling ~14 ms per 1k records.
Epic chain: A moves the whole read-modify-write into one Isolate.run (relief, no data risk); B normalises to one row per message (removes the cost); C integration-tests both on hardware.
Carries the cannot-tell branches table and grant terms with the basis for every budget. Also records that #335 did not fix this and that drift already runs SQL off the UI isolate.
Refs #334
Agent: TealGlen (session c61ce804)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Promise table, signal precedence, marker requirements for the firmware D1 decision, known fail-closed limitations.
Agent: GentleGlacier (session f0c92a36)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>