Imported from Dillxn/dasher-offer-filter (
AGENTS.md). Install upstream withnpx skills add Dillxn/dasher-offer-filter. Copyright stays with the author.
How to decide (the owner, 8 October 2026)
- "you never for any project need approval if you're using best judgment": make the call with best judgment, do it, and say what was decided and why (release notes, the commit, a line to the owner). Every "needs a new owner decision", "not approved" or "ask the owner" below is a decision to make on best judgment in that way. Still ask only when the choice is genuinely the owner's own (their money, accounts or identity, or a product direction they have not set), and never weaken the safety rules below (taps only on the declined offer's own controls, the user's touch always wins, local-only signing, privacy, consent).
0.6.28 (17 October 2026): a "+$" offer with "orders completed" under it, and Dasher's add-on card, are read (from the beta launch thread)
- The asks (Dillon's in-app reports of 16 October 2026 from a midday dash on 0.6.24, Galaxy, Android 16, area mode, Autopilot pay first): "was +1 didnt read it" (15:55 UTC; the add-on card below, "pay not found", addOn false) and "ha dplenty fo opportunity multiple screens abd modea abd didnt read" (16:33 UTC; a "+$3, $9.90" offer, 2 stops (2.9 mi) • 17 min, "pay not found" on every showing to its 0:00). The after-dash summary he shared at 16:33:55 UTC kept both screens.
- Under every offer, Dasher now draws a name, a lifetime "N orders completed" and a badge ("Rising Shopper"), then its tabs (Home, Schedule, Account). OfferParser.bonusOrderCeiling took that count for order evidence (ORDER_WORD), so the "+$" ceiling was gone ("pickup, dropoff or order counts"). A label that is exactly "N orders completed" (COMPLETED_ORDERS) is now neither a count nor partial count evidence: the two-stop fallback (Y + X) stands, and a counted bundle keeps its own count. Any other label with "order(s)" in it still takes the ceiling away. The ceiling's use is unchanged (decline only when even Y + X misses the rules at min(bar, 100), else REVIEW).
- Dasher's add-on card has no add-on phrase: "+$9.40", "incl. tips", "+1 stop (1.9 mi) • +7 min", "Guaranteed earnings for completing the offer.", with its Decline, Accept and countdown. AddOnOffer.isLikely also takes the screen for an add-on when its one money label ("$" anywhere) is nothing but a "+$" amount (ADDED_PAY_LABEL) and a label starts with "+N stop(s)" (ADDED_STOPS_LINE). A "+$" beside any other amount stays out (it may be a bonus on that total). An earlier report's card of the same kind ("+$5.50", "+2 stops (3.8 mi) • +12 min", "Multiple dropoffs (2 stops)"), kept as "pay not found" by OfferRuleTest until now, is read the same way.
- Read as an add-on (AddOnOffer.parse, unchanged): only explicit increments ("+$9.40", "+1 stop", "+7 min"); the bracketed miles carry no "+", so they stay unread (never infer incremental add-on semantics, the rule below). OfferRule.evaluateAddOn judges it strictly against the route under way (ActiveRouteStore): with the owner's route then ($10.65, 1.3 mi, 13 min, 2 stops) and rules ($4.50 a stop, $16.20 a mile, $0.70 a minute, bar 50) it is REVIEW, ADD_ON_UNCLEAR (the miles); an added pay under its minutes' ask at the bar, or a whole route that fails, declines it. Auto-accept never takes an add-on; the learner never learns from one.
- OfferFilterService.notePayNotFound skips an add-on's screen (its pay is the "+$" increment, read on the add-on's own line).
- Tests: PlusCeilingDiagnosticsTest.aLifetimeOrderCountUnderTheOfferLeavesTheCeiling, OfferRuleTest.theOwnersPlusOffersWithALifetimeOrderCountUnderThemDeclineBelowTheirCeilings and theReportedAddOnWithOnlyIncrementsIsReadAsAnAddOn (was ...StaysReview), AddOnOfferTest (the card read, what is not that card, his rules), PlusOfferAndAddOnCardTest (the service: the +$ offer declined, the card left to the user on a route, a thin card declined). Screens are the reports' labels, names and addresses replaced, the lifetime count changed.
- No notice change (Consent.VERSION stays 29); Terms and Privacy unchanged. 0.6.28's What's new card has two lines; offerfilter.org's What's new is synced.
- versionCode 120, versionName 0.6.28.
0.6.27 (17 October 2026): offers are scored by area by default (from the beta launch thread)
- The ask (Dillon, 17 October 2026 01:26 UTC, in the project chat): "it should be on area score mode by default". Best judgment (the coordinator's default): area scoring is the starting mode for anyone who has not picked one, installs updating included; a mode someone picked stays.
- FilterStore.initializeAreaDefault (area_mode_default_schema 1, in load and save after the learner's first state, which writes Strict): a new install, and once an install updating whose user never picked a mode (area_mode_chosen, written by FilterStore.setScoreByArea from 0.6.27), gets area_mode_v3 on. Nothing before 0.6.27 kept whether Strict was picked, so an earlier Strict moves once, as Autopilot's (0.6.2) and learning's (0.6.1) defaults moved earlier installs; one tap in How offers are judged brings Strict back, kept as picked from then on. Don't run another mode migration over area_mode_chosen.
- The step moves the learner's epoch on (nothing seen under Strict teaches under Area), as the user's own change of mode does; unlike it, "minimums since" and Autopilot's accepted floor stay (not the user's change; hand accepts are rescored under the current rules anyway). It never calls AutopilotRuntime (its lock is taken before FilterStore's): the mode is in Autopilot's rules key, so a plan made under Strict is stale. One fixed line under "rules": FilterStore.AREA_BY_DEFAULT_LOG ("area scoring on by default (How offers are judged chooses Strict)").
- How each mode judges is unchanged (the area section below): area lets stronger measures compensate; max stops stays hard, unread required facts stay REVIEW, add-ons and auto-accept stay strict.
- Notice 29 (Consent.VERSION): TERMS.md and PRIVACY.md say Area is the default (from 0.6.27, and set once then for earlier installs), so everyone sees the notice once more; its words are unchanged. BundledNotes' notice 29 and 0.6.27's card say it in two lines (BundledNotes.AREA_BY_DEFAULT). offerfilter.org's terms, privacy and What's new are synced.
- Tests: AreaModeDefaultTest (a new install and its first rules, an install updating in Strict, Area already on, a Strict marked picked, a pick after the step, the radar's word and the chooser). Every other suite runs as an install whose user kept Strict (ConsentedTestApp sets FilterStore.areaModeByDefault false; AreaModeDefaultTest turns it back on).
- Also in this build: the release thread's gate speed-up (AreaScore.Floors makes its display projections at their first read; the decision arithmetic is unchanged).
- versionCode 119, versionName 0.6.27.
0.6.26 (16 October 2026): setup first, on its own page, one step at a time until Offer Filter can filter (notes: notes/0.6.26/setup-first.md, from the beta launch thread)
- The ask (Dillon, 16 October 20:56 and 20:59 UTC, on 0.6.25 on his Galaxy, One UI 8, with screenshots of the home screen at 4:54 PM and Accessibility's top page at 4:57 PM EDT): "shouldn't have this limbo screen. if the app is unable to work for permissions or whathaveyou then we shepherd the user directly to resolution. all ui on screen needs to be justified by what is currently functional."; "it gave me a toast with instructions saying andrioid would block turning on notifications, it didnt, then it drops the user into the settings screen which does block them with toast instructions too long to read. we need to do a serious ux study on setup." The study is the Claude Doc "Offer Filter setup study".
- Why 0.6.25 failed there: Start tried notification access first, which One UI 8 doesn't block; its turning on marked Android's block lifted for good (restricted_cleared), so the App info step vanished and every Fix reopened Accessibility's blocked switch; Accessibility then opened by itself on return, at its top page, with a 63-character toast.
- Now (SetupFlow, SetupPage; SetupChecklist and its homepage lines are gone): while screen reading or notification access is off (or on and not running for SetupFlow.GRACE_MS, 10 s), and through a new install's first-run walk, setup's page is the whole screen, over the homepage and Settings and under the notice and the update cover: a strip of sky with the step's title ("N of 5" over it through the walk) and the mascot, its few words, one button, at most one quiet link, and "Stuck? Send feedback" (the feedback form as Usability, diagnostics off unless chosen). While screen reading runs with a rule saved, auto-decline goes on behind the page (background offers off or reconnecting, mid-dash too), so its foot also carries the mascot's Pause auto-decline (Resume once paused): the user's touch always wins. Steps: 1 Turn on screen reading (Open Accessibility); 2 Turn on background offers (Open notification access); the walk's 3 Allow alerts (Android's question, asked once), 4 Map where offers pay best (while the Offer map is on: Allow location, or Not now; asked once) and 5 Start with typical minimums (Start auto-decline saves them and turns auto-decline on, with no goal chooser; Set my own opens the homepage with the knobs beckoning). The walk ends for good on either, and never starts for an install with a rule saved. Back on the page leaves the app.
- Every word is read on Offer Filter's page before Android's opens, and Android's pages open only from a button. A toast there is the path alone (SetupFlow.TOAST_MOST, 72 characters): "Installed apps → Offer Filter" on a Galaxy (DasherSplit.phone() is samsung), "Downloaded apps → Offer Filter" elsewhere, "⋮ → Allow restricted settings", "Turn it off, then on.", "Turn on Offer Filter background offers", "Reconnecting…"; a page whose switch is right there gets none for a first try.
- Android's block (Android 13+, an app from a downloaded file): step 1 says it is coming (BLOCK_AHEAD) only when Android's record (RestrictedSettingsGuide.opMode, access_restricted_settings) says restricted and untried, or the install source is a downloaded or local file and the block was never seen lifted (LIFTED). A try from here that comes back off shows "Allow restricted settings" (Open App info, with the link Try the switch again) when the record says restricted or tried, or, for screen reading, the install came from a file or from outside an app store; back from App info with the record not restricted, "Now turn on screen reading" (or background offers). Step 2 never warns ahead and shows App info's step only on the record's word (One UI 8 doesn't block notification access). The block counts as lifted only by screen reading on, or the record saying allowed, never by notification access (0.6.25's dead end). 0.6.25's keys (restricted_cleared and the rest) are deleted once.
- Coming back: when a switch setup sent the user to turns on (its service connects) within RETURN_WITHIN_MS (10 minutes), with the notice accepted, SetupFlow.connected asks MainActivity to the front (NEW_TASK | CLEAR_TOP | SINGLE_TOP), once per mark. Android may refuse that from the background (unverified on One UI), and Back returns as before. Nothing else opens by itself.
- Once Offer Filter can filter, the homepage shows each part only once it works (MainActivity.showWhenWorks): the skyline, its DASH key and its caption from the first offer; the map and its line while the Offer map is on with location allowed. Hidden, each keeps its room. "Waiting for offers" no longer needs alerts. Alerts turned off later are Settings' "Alerts are off" row (its Fix: Android's question while Android still asks, else the app's notification settings). Allow updates is asked only by a held update's line (UpdateReadyRow: Install now, or Allow updates opening Install unknown apps with "Turn on Allow from this source.").
- Diagnostics, fixed words under "setup": "showing ", "done: the home screen shows", "tapped on ", "a try at screen reading came back off (record default|…); App info next", "… still off, back from Android's page (record …)", "back from App info (record …); the switch next", "screen reading turned on; Offer Filter asked back to the front" (or "Android kept Offer Filter in the background (…)"), "Stuck? Send feedback tapped on ".
- Tests: SetupFlowTest (the Galaxy's walk to auto-decline, a store install, the record deciding, notification access blocked only on the record's word, guarded pages, the grace, the return, an install in use, auto-decline's Pause on the page, Set my own, location from setup, the notice first and Back, Stuck, the homepage's parts). The older suites run with setup's page away (ConsentedTestApp sets SetupFlow.pageShows false; the tests of the page turn it on), their homepage lines now the Update ready line and Peek paused (AndroidAdapterTestBase.HOMEPAGE_LINES). Suites that lay out the map grant approximate location, since the map shows only with it (AndroidAdapterHomepageTest.theMapShowsOnceLocationIsAllowedAndAsksOnlyForApproximateLocation). SetupPage makes its button and link with their words, so no button anywhere is ever blank (everyButtonOnTheScreenHasALabel). ToastWordsTest covers the new toasts.
- No notice change (Consent.VERSION stays 28), and the Terms are unchanged. PRIVACY.md's two setup lines name setup's page and its marks (nothing new kept or sent, so no re-show). offerfilter.org/install's setup steps are rewritten to match (the site sync).
- versionCode 118, versionName 0.6.26.
0.6.25 (16 October 2026): setup walks through Android's block, each screen opening by itself (notes: notes/0.6.25/setup-walk.md, from the beta launch thread)
- The ask (Dillon, 16 October 18:59 UTC, in the project chat, with a screenshot of his Samsung (Android 16) at Accessibility → Installed apps, Offer Filter greyed "Controlled by Restricted Setting" under Android's "App was denied access" dialog, Close and "Learn how to allow access"): "we need to streamline this process and make it user friendly".
- Before: the restricted step's Fix showed a one-line dialog (Try the switch, App info); the try went to Accessibility's list, a level down from Offer Filter, with a toast saying to turn it on; back from Android's refusal nothing opened, so Fix again, a second dialog and Open App info, with no word on App info's screen; back from App info the access opened; then each step needed its own Fix.
- Now (SetupChecklist, RestrictedSettingsGuide): the step's Fix (or an access's while untried) shows the walk-through, "Apps from the web need one extra step on Android. Offer Filter opens each screen for you: 1. Tap the switch. Android blocks it the first time: close the message and come back. 2. Tap ⋮ at the top right, then Allow restricted settings.", with Start and Not now (TRY_FIRST_MAYBE says "may" and "If Android blocks it" for a source that only suggests the restriction). Start tries notification access's own switch, a tap away (toast TRY_ON_PAGE "Tap the switch. Blocked? Close Android's message and come back."; its list naming "Offer Filter background offers" where its page will not open; Accessibility's, toast TRY_IN_ACCESSIBILITY, once notification access is on).
- Back with the switch still off and Android's record saying tried (MODE_IGNORED), App info opens by itself (toast IN_APP_INFO "Tap ⋮ at the top right → Allow restricted settings, then come back."), and the step's Fix goes straight there with no dialog. Never on this app's word alone (the record unreadable, at its default or still restricted): App info without the item would be a dead end, so the step's Fix offers the old PATH dialog (Open App info, Try the switch) or the walk again. Back from App info, allowed, the first access still off opens (Accessibility), counted as a try, as before.
- Back from an access opened from here to turn it on (a Fix, the walk or its next step; SetupChecklist.TURNING_ON) and now on, the next step opens by itself: the other access, then Android's question for alerts if it was never asked (a refusal is the user's answer). Not for an access opened to turn off and on (Restart, Reconnect), nor one still off; Allow updates is not part of the walk. Every such opening keeps the old guards: within RETURN_WITHIN_MS (10 minutes) of leaving, never mid-dash (UpdateReadyRow.dashOn), beside another app or behind the notice, each mark read once.
- Diagnostics, fixed words under "setup": "restricted settings: walk-through started, trying notification access" (or Accessibility); "restricted settings: a switch tried from here stayed off (record allowed|tried|restricted|default|unreadable)", with "; App info next" when it opened; "restricted settings: back from App info (record …)", with "; the switch next"; "screen reading turned on; notification access next" and the like, ending "alerts asked next".
- Tests: SetupChecklistTest.aSideloadIsWalkedThroughAndroidsBlockEachScreenOpeningByItself (Start, the switch, App info by itself, Accessibility, notification access, alerts), aTryAndroidsRecordDoesNotConfirmOpensNothingByItself (and mid-dash), backFromASwitchTurnedOnTheNextStepOpensByItself (any install; ten minutes, a dash, a switch still off, a refusal of alerts), aTryInAListSaysWhichEntryToTap, and the older restricted tests reworked for Start and the direct App info; ToastWordsTest covers the four new toasts (72 characters at most, each saying to come back).
- No notice change (Consent.VERSION stays 28); Terms and Privacy unchanged: "which switch you were turning on when you left for Android's settings or App info (acted on only within ten minutes)" already covers the new marks. offerfilter.org/install step 8 is rewritten to match (the site sync).
- versionCode 117, versionName 0.6.25.
0.6.24 (16 October 2026): an offer taken by hand right after the app's declines counts, and teaches (notes: notes/0.6.24/accept-after-declines.md, from the beta launch thread)
- The miss (Dillon, 15 October 08:57 UTC, with a screenshot of his homepage: "since I have learn on shouldn't a 109% offer raise my mins, not just autopilot?"; his Share reports made at 4:29 and 10:59 AM, his time, 08:29 and 14:59 UTC): the 109% offer was his $7.45 at 4:23 AM, counted as accepted, which beat none of his learned bests and so rightly raised none. The one that should have taught was the $9.45 (0.6 mi, 13 min, 2 stops) he took in split screen at 4:56 AM, right after two of the app's declines: its $43.62 an hour beat the learned $41.84. No Accept tap of his was seen, and its line said "Not counted from what followed: Dasher's wait for offers wasn't seen before it", so nothing was learned. His reports show Dasher's wait for offers ("Finding offers..") read half a second after each of the app's declines, so the wait was there; what used it up was a read of neither between it and the offer (AcceptedOfferTracker.afterScreen: with no offer watched, any UNCLEAR read set lastClear to null at once). The exact frame is not in his reports (the screens log had rotated; dash summaries keep no screens). Offline, Dasher's header drawn alone ("Open Settings", "Help", the dash's earnings, "This dash"), read 300 ms before the offer, gives his line on 0.6.23.
- Now, with no offer watched, a screen of neither uses up the wait for offers only once it has stayed SETTLE_MS (5 s) when the next offer comes, timed from the first such read since the last clear screen (empty reads and reads showing an offer's facts do not restart it); one replaced sooner is passed over, as the watched path already passes over a next screen replaced within 5 s. Everything else that uses it up is unchanged: another offer (the app's decline of one too: the wait must be read again after it), a restart, a stored route, and at a peek any screen of neither since the wait, however brief (peeks keep the old strictness). A delivery read before a frame of neither still says a delivery was under way, so a seen Accept tap then still counts nothing.
- Diagnostics: the outcome evidence line ends "; wait_before=" with what came before the watched offer, in fixed words only: seen, delivery, dash_over, stored_route, offer, unclear, peek, unknown or none (AcceptedOfferTracker.waitBefore), so the next report says why an offer went uncounted.
- Tests: AcceptanceLearningFieldTest.takenByHandRightAfterTheAppsDeclinesItCountsAndTeaches (his 4:55 to 4:56 AM in split screen: $7.50 and $6.70 declined by the app with the wait for offers after each, the header 300 ms before the $9.45, no tap, its pickup; Accepted, the learned per hour rises to $9.45 over 13 min, "wait_before=seen"; on 0.6.23 it gives his "Not counted" line) and aScreenOfNeitherThatStayedStillLeavesItUncounted; AcceptedOfferCloseTest (a frame of neither the offer replaces sooner keeps the wait; screens of neither lasting 5 s in all use it up, whatever their words; a delivery read before a frame of neither still says a delivery was under way); AcceptedOfferTrackerTest.aScreenOfNeitherSinceTheWaitForgetsItAtAPeekAsBefore.
- No notice change (Consent.VERSION stays 28); Terms and Privacy unchanged (still for 0.6.21). BundledNotes release("0.6.24") is one line.
- versionCode 116, versionName 0.6.24.
0.6.23 (15 October 2026): an update installs beside Dasher's home screen when no dash is on (notes: notes/0.6.23/update-beside-dasher.md, from the beta launch thread)
- The hold (Dillon, 05:22 UTC, with a screenshot of Offer Filter's Settings above Dasher's "Start your dash with lots of offers" and its Dash button, the Updates line reading "Update verified; installation deferred while an offer/delivery is active."): "in app won't update even though no ongoing dash". Cause: Updater.heldBack held every install, his own Updates tap and the homepage's Install now included, whenever OfferFilterService.isDasherForeground(), which is true for Dasher's half of a split screen, so in his everyday split nothing installed and the words named an offer or delivery that was not there. Now Dasher on screen holds an install back only during a dash: UpdateReadyRow.dashOn (Dashing.on, a route stored or an offer tracked) and not Dashing.homeShown() (Dasher's home before a dash, its Dash button, read since the dash was last seen). It then says AFTER_DASH ("Update ready: installs after your dash") and tries again every 5 minutes. The automatic offer-or-delivery hold says AFTER_OFFER ("Update ready: installs after this offer or delivery"). Everything else is unchanged: the automatic install's wait for a positively seen end (Dashing.awaitingEnd, with UpdateHold's ceiling), the Busy (typing rules) wait, every verification step, and the reopen after an update only over Offer Filter's own window or the home screen (over Dasher it is dropped, as before). So the homepage's Install now, shown only while dashOn is false, is never refused for Dasher being on screen.
- Test: SplitWindowsTest.besideDashersHomeBeforeADashAnUpdateInstallsButDuringADashItWaits (Offer Filter's half above Dasher's home: both the user's check and an automatic one install; Dasher's wait for offers there: both wait with AFTER_DASH; "Dash ended" there: both install). It fails on 0.6.22 at the user's own check.
- No notice change (Consent.VERSION stays 28); Terms and Privacy unchanged (still for 0.6.21). BundledNotes release("0.6.23") is one line.
- versionCode 115, versionName 0.6.23.
0.6.22 (15 October 2026): drag the offer map and it springs back into place (notes: notes/0.6.22/map-drag.md, from the beta launch thread)
- The drag (Dillon, 03:52 UTC: "i want to be able to drag the map on our app and it spring back into place on release"): AreaMapView.drag takes a touch once it moves past ViewConfiguration's touch slop, wherever it started (the ⓘ too), and from then on it is never a tap: no square is picked, the ⓘ's key stays shut, and the View's own click (which asks for location) is cancelled by handing super a synthetic ACTION_CANCEL. The parent is asked not to intercept (requestDisallowInterceptTouchEvent), so a page that can scroll (only with a very large font) leaves the finger to the map once its drag starts; a mostly vertical swipe that crosses the slop first is still the page's scroll. The pull is a rubber band, pull = raw × STRETCH_DP / (|raw| + STRETCH_DP) with STRETCH_DP 240 (all of it at first, half at 240 dp, never 240 dp), applied in project() (left −= pullX / scale, top += pullY / scale), so the grid, the squares, You and the name pill go with it and a tap hits what is drawn, while the edge fade, the key, the ⓘ and the scale stay put; the empty fields are translated, drawn from a whole number of dashes out. Let go (UP, CANCEL, or the dragging finger's POINTER_UP), it springs back: an underdamped spring from rest (SPRING_RATE 24 rad/s, SPRING_DAMPING 0.6: about 9.5% past its place at 164 ms, at rest by SPRING_MS 500), worked out from SystemClock in settlePull(), with Motion.settling asking for each frame only while it springs; nothing new runs at rest. With Android's animations off it still follows the finger and is back in place at once. A finger that catches it on its way back holds it there (the pull's give undone, so it does not jump). Tests: AreaMapViewTest (the drag and its give, the catch, the spring, never a tap, the page leaves the finger, the empty fields, animations off).
- No notice change (Consent.VERSION stays 28); Terms and Privacy unchanged (still for 0.6.21). BundledNotes release("0.6.22") is one line.
- versionCode 114, versionName 0.6.22.
0.6.21 (15 October 2026): a map's half gets 70% of the split; the offer's score beside the tab; the offer map fills its box; an accept holds the bar for the 24 offers after it; Autopilot is the buffer and nothing but learning raises the minimums (notes: notes/0.6.21/, from the beta launch thread) — supersedes 0.6.11's divider move during a delivery, 0.6.18's 7-day accepted floor (and 0.6.19's "full 7 days"), and 0.5.1's growth of the minimums with its "Let my minimums grow" switch
- The divider (Dillon, 00:23 UTC: "whenever one half has nav open make it 70% height / whenever one half has nav open the other shouldn't"): SplitDivider is now "a map in one half": whenever a Peek.NAVIGATION app is in one half and Dasher or Offer Filter in the other, the map's half takes the divider action whose label names the share nearest MAP_SHARE (70; the larger of two equally near, never under half), and RESTORE puts the divider back once no half is a map's. Line 2 is read as "the half beside the map never gets the 70%" (best judgment; Dillon was offered the other reading, that Dasher shouldn't open its own navigation beside Maps). Dasher beside Offer Filter is left as it is, delivery or not: 0.6.11's shrink of the half beside Dasher's delivery screens is gone. The guards stand (no sign of an offer for QUIET_MS 3 s, no touch for TOUCH_MS 2 s, never during Dasher's own navigation, USER_MOVED or a divider that does not move ends it for the dash, MOVES_PER_DASH 12). SplitDividerMover asks, once per window, for the root alone of a half not known by its ID (Host.rootPackage), never a root known as Dasher's; a picture-in-picture window no longer hides the split; the log names a half only as Pane's possessive, never another app. Tests: SplitDividerAdapterTest.
- The score (Dillon, 00:23 UTC: "I want to be able to see at a small glance the score of the offer as it's being declined"): ScoreTag, a TYPE_ACCESSIBILITY_OVERLAY pill (not focusable, not touchable) beside the tab's slim peek: "✕ N%" (Ui.CRITICAL) once the app's Decline is tapped, kept over Dasher's question (OfferFilterService.handleConfirmation restores declinedScore), "✓ N%" (GOOD) for a pass, "? N%" (WARNING) for the user's to decide; N is decision.scorePercent. A declined offer's stays DasherOverlay.SCORE_STAYS_MS (5 s) after the offer leaves, a decline taking about a second; a passing or review one goes with its offer; never beside Offer Filter's own half; the guide waits while it shows. A new Dasher window (DasherOverlay.suspend, as the question comes) makes it INVISIBLE with the tab, its window kept, until the next sync: removing and adding it again blinked it, and in Robolectric each window change also moved the simulated clock, which shifted DeclineHandBackTest's touch timings by 30 ms. OverlayState carries score and declining. Tests: DasherOverlayTest (the same window over the question).
- The map (Dillon, 00:25 UTC): AreaMapView draws its grid and squares to every edge, under the key's row and the info column too, fades out at the edges and has no outline; a square anywhere but under the info control can be tapped. Tests: AreaMapViewTest.
- The hold (Dillon, 00:38 and 00:45 UTC: "I feel like 7 days is too strong. should be like 24h", "24h may b too aggressive. We may have to do it by like two dozen orders or something"): Autopilot.FLOOR_OFFERS = 24. An accept holds the bar while fewer than 24 counted offers came after it (acceptedFloor with the counted lines), with no clock, across dashes; floorOffersLeft feeds the details ("for your next 15 offers"). The user's change still restarts it; Autopilot off and on keeps it. AutopilotStore.KEEP_ACCEPTS_MS (7 days) still bounds the figures kept past the 200 history lines.
- Growth retired (Dillon, 01:02 UTC, relayed: "mins growing should not be narried to autopilot"; 01:19 and 01:20 UTC: "mins should grow as we accept offers with higher mins when adaptive / learned mins is on. autopilot is the buffer", "this was already established"; so don't ask him about growth again): only acceptance learning raises what offers must meet (the floors are max(saved, learned while acceptanceLearning is on)), and Autopilot's bar (minimumScalePercent) scales that whole requirement, so Autopilot is the buffer and never changes the saved minimums. Gone: Growth.evidence and grown, Plan.growth and Draft.growth, AutopilotRuntime's Commit.GREW, plans(), grow() and growthDue, FilterStore.minimumsGrow, setMinimumsGrow and growMinimums, "Let my minimums grow" (AutopilotText.GROW_SWITCH and GROW_ABOUT, never in Settings or the details now) and the growth log lines; FilterStore.initializeAutopilot removes the stored autopilot_grow key (forgetGrowSwitch). Planning again does nothing while Autopilot is off or before the notice is accepted. "Follow offers" (growth from what offers paid, Autopilot on or off; commit 1b5e275) never shipped. Kept for a growth made by 0.5.1 to 0.6.20, until the minimums change another way or Clear history: Growth.Grown, AutopilotStore.lastGrowth (recordGrowth is only the tests' now), GrowthCard's note with Undo (AutopilotRuntime.undoGrowth, the user's change) and OK, the details' "Your minimums last grew …" line and AutopilotText's 0.6.20 growth words; Inputs.minimumsSince and zone stay, unread by any plan. Tests: MinimumsGrowthRuntimeTest (nothing grows with Autopilot on or off; an earlier growth's Undo, note and Clear history), FilterStoreGrowthTest (the stored switch is forgotten), MinimumsGrowthUiTest (no switch anywhere), AutopilotFloorRuntimeTest.anAcceptPastTheBarRaisesOnlyTheBarAndTheMinimumsNeverGrow; GrowthTest, MinimumsGrowthAdapterTest and GrowthFloorTest are gone with their subject.
- Notice 28 (Consent.VERSION; 399 words, ConsentGateTest allows 420): "move the split-screen divider for a map"; "After you accept an offer yourself, the bar stays at or above its score for your next 24 offers." in place of the 7 days and the growth sentence (ConsentGateTest: no "grow" in that point). BundledNotes: notice(28) and the start of release("0.6.21") are HOLD_GROWTH_MAP (three lines: the hold, "Your minimums no longer grow on their own…", the map's 70%), the card adds the score and the map; notice 15 keeps its 0.6.20 lines, which notice 28 retires for a reader of 14. Terms and Privacy are for 0.6.21 (Autopilot never changes the minimums, the 0.5.1 to 0.6.20 growth and its switch as history, the switch's stored choice removed, the divider's map rule, and in Privacy the split screen's per-window app check); LegalTexts regenerated with tools/legal_texts.py.
- versionCode 113, versionName 0.6.21.
0.6.20 (15 October 2026): off a dash the bar moves at once; the notice's switch shows the choice made; a short notice (notes: notes/0.6.20/short-notice-and-bar.md, from the beta launch thread) — supersedes 0.6.19's "safeForBarChange is unchanged" and its rise words ("once Dasher is on screen with no offer up"), 0.6.18's "unchecked every time the notice shows", and the 11-point notice
- Dillon, 14 October 2026, 23:40 UTC, two minutes after updating to 0.6.19: "why is it auto 52 when offer was 67?" (the details: "Bar: 52% … The bar rises to 85% once Dasher is on screen with no offer up"; Dasher's home screen, not dashing, in the split's other half; 85% was another accept of the week). Likely cause (no report to confirm it): the commit after the notice waited for a safe point, which the reader checks once a minute (Autopilot.TICK_MS) once a read of Dasher's screen since the reader connected exists (dasherScreenKnown, reset by every update) and no read is queued; his screenshot came inside that minute. Now OfferFilterService.safeForBarChange waives dasherScreenKnown and the read-queue condition (queued, recheckPending) while Dashing.on is false (nothing of a dash seen for half an hour, or Dasher said it ended, and no route under way): every way an offer is seen or decided calls Dashing.seen first (the read at an offer, the offer's notification), so off a dash no offer is up nor was one decided at the old bar. Everything else still holds (consent, no busy or hot read, the quiet gap, no decline, question, error recovery, takeover, restart record, automatic accept, peek, tracked offer notification or late completion). So the NOTICE (or CONNECT) plan's commit lands at once off a dash; during one nothing changed. AutopilotText.detailsFloor's rise: "The bar rises to 63% as soon as no offer is up, and stays at 63% or more: …"; notice 27's third line says "as soon as no offer is up" too. Tests: AutopilotCommitAdapterTest now starts every test during a dash (Dashing.seen in setup; its safe points are a dash's) and adds offADashTheBarMovesAtOnceWithNothingOfDashersRead and afterADashEndedTheBarMovesAtOnceToo.
- Dillon, 23:34 UTC: "share diagnostics was turned off by default again on the splash screen even though I had it turned on last time". His setting was never off: the notice's switch started unchecked every time and off changed nothing. Now NoticePage starts it at Feedback.afterDashOn (off on a new install: still opt-in) and accepting calls Feedback.setAfterDash(on) only when the switch differs from Settings', either way. Consent.DIAGNOSTICS_ASK's words are unchanged. Privacy's line about the switch says so (Privacy effective 15 October 2026, for 0.6.20; Terms unchanged, for 0.6.19). Tests: NoticeAskAndUpdateGateTest.theSwitchShowsTheChoiceAlreadyMadeAndAcceptingKeepsItAsLeft.
- "also that screen is way too long": Consent.POINTS went from 11 points (about 1,230 words) to 7 (about 400): "Acceptance rate." first and word for word; "Your Dasher account." second, with "Not a DoorDash app." folded in ahead of the 0.6.17 terms sentences, word for word; "What it does." (reads Dasher's screen and notifications, declines on your rules or the skip list by tapping Decline and Dasher's usual question, any other question left to you and pauses, the other actions in one sentence: Back after an error, ⚙ once, the divider during a delivery, offer sound, the screen kept on; auto-accept off unless turned on; your touch stops it); "On by default." (Autopilot 50–150%, pay first, above 100% declines offers that meet the minimums, holds the best accept of 7 days, grows the minimums up to 10% with Undo; learning; Peek; the anonymous daily count; how each turns off); "At your own risk."; "Not while driving."; "Your data." (what stays, what leaves and when, typing unmasked and masking imperfect, 90 days, the AI tools, the Terms and Privacy for the rest). Cut, left to the Terms and Privacy (ConsentGateTest.everythingTheShortNoticeLeavesOutIsInTheTermsOrPrivacy checks each in their words): pay per stop and the strict and area modes, the 0.6.14 note that the number did not change, Autopilot's inputs and cost per mile, the learning details, Peek's details, every retention period, the daily count's details and the optional timing study point (Settings asks for it on its own). Consent.VERSION stays 27: the words are shorter, the substance (the Terms and Privacy the notice links to) the same, and a new version would ask everyone again for nothing. ConsentGateTest's three point tests were rewritten for the seven points.
- versionCode 112, versionName 0.6.20; BundledNotes release("0.6.20") (two lines: the bar off a dash, the short notice and its switch).
0.6.19 (14 October 2026): the accepted floor survives Autopilot off and on, and lasts its full 7 days (notes: notes/0.6.19/floor-holds.md, from the beta launch thread) — supersedes 0.6.18's "UserChange.restartsFloor unchanged: Autopilot on, …" and its floor counting only accepts among the 200 kept history lines
- Dillon, 14 October 2026, 22:10 UTC, on 0.6.18: "it dropped the auto score below my recent high again on 6.18" (Auto 52%, "Last change: 100% → 52% (Autopilot turned on)"). Cause: he had turned Autopilot off and back on through the goal chooser, and UserChange.TURNED_ON (and TURNED_OFF) restarted the accepted floor, so his morning accept no longer held the bar; notice 26 had promised "until you change your goal or minimums". Second gap: the floor only saw DecisionLog's latest 200 lines (MAX_ENTRIES), not 7 days.
- TURNED_ON and TURNED_OFF keep the floor (restartsFloor false). Turning it on with another goal than the stored one is UserChange.TURNED_ON_NEW_GOAL (words "Autopilot turned on", restarts the floor: it is a change of goal). Autopilot.commitReason: a TURNED_ON jump landing on a binding floor says the floor's words (ACCEPTED), as a lesson's does, so the floor's later release goes straight back as it rose (TURNED_OFF forgets the last change). GOAL_CHANGED, RULES_CHANGED and CLEARED keep their own words.
- AutopilotStore.keepAccepts (prefs "autopilot", kept_accepts: JSON of at, pay, mi, min, stops, items, itemsApply; KEPT_ACCEPTS_MAX 100, newest first) runs in every plan under AutopilotRuntime.LOCK with the generation check (so a Clear history in between writes nothing back): it keeps hand accepts (acceptedByHandAt set, not add-on, replay or skipped, pay read) from the floor's start (max(7 days ago, floor_since)) and returns them as Autopilot.Inputs.keptAccepts. Autopilot.handAccepts(rules, history, kept) scores them like the history's own, skipping a kept one whose ACCEPTED time the history still holds; Growth.held sees them too. restartFloor removes them; clear() wipes them. They never go in a report.
- Once, on update (AutopilotStore.undoToggleRestartOnce, floor_schema 1): with pay first as the goal, a floor_since left by 0.6.18 is removed, so the accepts of the last 7 days still in the history hold the bar again (one "[autopilot] accepted floor: your accepts of the last 7 days count again (turning Autopilot on or off no longer restarts it)" line). Best judgment: 0.6.18 kept no record of what restarted the floor, and minimumsSince is no bound (a lesson moves it); a rate goal keeps its restart. restartFloor marks the schema done, so a restart made under 0.6.19 is never undone.
- Dillon, 22:50 UTC: "i reset it bc when I opened the app it said 50% which was already low". Cause: nothing plans before the current notice is accepted, and accepting it asked for no plan (MainActivity.acceptNotice), so after an update the homepage showed the bar the old version left ("Auto 50%", "Checking offers") until an offer, a dash's tick (Trigger.TICK needs a dash under way) or the homepage coming back asked for one; and a commit always waits for a safe point (OfferFilterService.safeForBarChange: Dasher's own screen read since the reader connected, which an update resets, and no offer up). Now acceptNotice calls AutopilotRuntime.noticeAccepted (Trigger.NOTICE), and while a plan's floor binds above the bar deciding offers (AutopilotText.risingTo) the status line says "Rising to 63%" where the plan's own words would show (AutopilotText.showsRise: pinned, no reading yet, pay first; never in place of the acceptance meter), screen readers hear "Rising to 63 percent, the score of an offer you accepted.", and the details' floor line starts "The bar rises to 63% once Dasher is on screen with no offer up, and stays at 63% or more: …". The chip keeps the bar deciding offers. safeForBarChange is unchanged.
- Notice 27 (Consent.VERSION), BundledNotes notice(27) and release("0.6.19") (FLOOR_HOLDS), the notice's "Your data." sentence, Terms and Privacy (effective 14 October 2026, for 0.6.19), README and PUBLIC_BETA_PHONE_GATE.md say so. Not changed: the floor's 7 days, its 150 cap, the details' floor line once the bar is there, what restarts it otherwise.
0.6.18 (14 October 2026): Autopilot earnings first, and only Dasher's known decline question confirmed (specs: notes/0.6.18/autopilot-earnings-first.md, notes/0.6.18/earn-by-time-allowlist.md) — supersedes 0.6.5's "any are-you-sure question" confirmation (the guard stays), and 0.6.2's default goal (70%, "the suggested one", preselected first in the chooser) and 0.6.11 B's within-dash hold ("It holds for the dash and restarts at a new dash")
- Dillon, 14 October 2026: "no reason auto pilot should have dropped to %50 when the latest order is above that... it's goal is to arm wrestle with the dispatch algorithm to ratchet up earnings"; on the card he chose "Earnings first" ("Pay first becomes the default, your best accepted score holds the bar across dashes, and tier goals become opt-in"). Cause: his 70% goal with his rate far below it pinned the share bar at 50% (Mode.PINNED), and the accepted floor counted only this dash's hand accepts, so his 6:45 AM accept at 67% held nothing at 2:56 PM.
- Pay first is the default goal (FilterSettings.GOAL_DEFAULT = GOAL_PAY_FIRST): a new install (the model-2 migration writes it and marks the goal step done), FilterStore.read's default, FilterSettings.of and sanitizedGoal's fallback. The chooser (Autopilot.GOALS) lists pay first first and preselects it ("Pay first — push for better pay, no acceptance-rate goal"), then the opt-in "Protect my acceptance rate — 70% or more (a top tier)" and "— 50% or more (a tier)"; CHOOSER_ON_BY_DEFAULT names pay first. Pay first keeps its 1-in-5 pass floor (unchanged).
- FilterStore.initializeGoalDefault (autopilot_goal_default_schema 1), once, right after the 0.6.2 Autopilot default in every load: a goal marked chosen (autopilot_goal_chosen) stays; a stored 50% or pay first stays and is marked chosen (only the chooser ever set them); anything else, i.e. a stored 70%, becomes pay first, with AutopilotStore.resetGoalState and one "[autopilot] goal pay first by default (was the suggested 70%)" line. It never restarts the accepted floor (not the user's change) and never calls Autopilot's runtime. Best judgment: nothing before 0.6.18 told a 70% the user tapped from the suggested 70% (0.5.0's migration wrote it into every install, the chooser preselected it, and Not now, Back and turning Autopilot off set "asked" just as a pick did), and the owner's own install is that case, so every unmarked 70% moves; notice 26 says so, and one tap in the chooser brings 70% back. From 0.6.18 a pick in the chooser (FilterStore.setAutopilot with Autopilot on) writes autopilot_goal_chosen (FilterStore.goalChosen); turning Autopilot off does not. Don't run another goal migration without that mark.
- The accepted floor carries across dashes: AutopilotRuntime.plan's floorSince = max(wall − Autopilot.FLOOR_WINDOW_MS (FLOOR_DAYS = 7, rolling), AutopilotStore.floorSince), dash or not. The user's own change still restarts it (UserChange.restartsFloor unchanged: Autopilot on, goal, minimums, max stops, mode, learning, Reset, Undo); a lesson and Adopt keep it; the 150 cap, the one-jump commit and its ACCEPTED words ("you accepted an offer at this score") are unchanged. Plan.target = max(own target, floor) in every mode but OFF and NO_RULES, PINNED and LEARNING included (unchanged code, now across dashes). When the accept turns 7 days old the bar goes straight back to the plan's own target (RELEASE). Growth.HELD_HORIZON_MS is the same 7 days, so bars a floor held up still never count toward growth. Only accepts among the 200 kept history lines can hold it.
- The details' floor line: "The bar stays at N% or more: you accepted an offer that scored N% of your minimums in the last 7 days. When that accept is 7 days old, or you change your goal or minimums, Autopilot sets it again."
- Not changed: the learner (+1¢ best-ever, the owner's 12 October decision), minimums growth's rule, auto-accept (never below 100%).
- Only Dasher's known decline question is confirmed (spec: notes/0.6.18/earn-by-time-allowlist.md; the owner chose option A, 14 October 2026, 19:36 UTC: confirm only decline wording seen in his own reports, pause on anything else) — supersedes, for the confirmation tap only, the broad phrasings DeclineConfirmation.hasPrompt accepts (they still recognise a decline sheet everywhere else: scenes, the acceptance-rate reading, privacy, the summary). DeclineConfirmation.KNOWN_PROMPTS is exactly "are you sure you want to decline this offer" (Dasher's usual wording, as in the owner's reports and the test fixtures taken from them; Gmail was not read). Keyed on the prompt line only (promptLines: labels that ask, i.e. with "are you sure" or a question mark in them, or ending in "…" or "..."; with none, the labels hasPrompt knows; never buttons, never statements beside a question such as "Does not lower acceptance rate"), normalized as OfferControls.normalize with the final "?", ellipsis or full stops dropped (promptKey); a known line with words added, another question beside it, or no question words at all is unknown. An unknown question is never confirmed: HandBack.UNKNOWN_QUESTION (history "not confirmed", given up like DASH_WOULD_END), auto-decline paused by TimeModeGuard.pause with "Paused: Dasher asked something new before a decline" (TimeModeGuard.UNKNOWN_QUESTION_LINE), and "[decline] unknown question: …" logged through PersonalText.mask, so the first time-mode report shows the real wording to add. TimeModeGuard.endsDash stays the first check: its phrases pause (with its own line) even when the prompt is the known one. The worst case is an extra pause, never an ended dash. Add a wording to KNOWN_PROMPTS only when a report shows it. Tests: TimeModeGuardTest shows "Are you sure?" and "Decline offer?" pausing; AccessibilityAdapterTest.confirmationDialogOverlayingTheDeclinedOfferIsTapped, which models Dasher's real question (its warning and buttons) without its prompt line, gained that line; every other confirming fixture already used the exact wording.
- The notice asks for diagnostics (spec: notes/0.6.18/diagnostics-ask.md, the owner's option A, 19:54 UTC): NoticePage ends, above the agreement line, with one switch, Consent.DIAGNOSTICS_ASK "Share anonymous diagnostics after each dash (helps fix it for your phone)" (NoticePage.DIAGNOSTICS_ID), unchecked every time the notice shows. Accepting with it on calls Feedback.setAfterDash(true) before accept.run(); with it off nothing changes (an already-on switch stays on). Still opt-in and off by default; what gets sent, its masking and its limits are unchanged, so Terms and Privacy gained no data lines, only the note (Consent's Your-data paragraph, Privacy, README) that the notice's switch also turns it on. Tests: NoticeAskAndUpdateGateTest.
- The screen reader restarts once after a fault (spec: notes/0.6.18/pilot-safety.md Q6, answered on the owner's behalf): scannerFailed still fails closed, then schedules restartAfterFault on main after OfferFilterService.SCANNER_RESTART_MS (5 s) once per dash (key: Dashing.currentStart, 0 off a dash; scannerRestartedDash). A second fault in the same dash stays off until screen reading is turned off and on. While off, ReadingStopped.faulted posts an ongoing notification (FAULT_NOTIFICATION_ID, channel reading_stopped) " stopped reading Dasher" / "Offers aren't being filtered. Tap to fix." whose tap opens MainActivity's homepage; restartAfterFault and onServiceConnected cancel it (ReadingStopped.cancelFault). Logged: the fault (as before), "screen reading restarts in 5 s (once a dash)" or "screen reading stays off: a second fault this dash…", and "screen reading restarted after a fault (Kind)". Nothing new leaves the phone. Tests: ScannerRobustnessTest.aFaultRestartsTheReaderOnceThenASecondFaultThisDashLeavesItOff.
- No update check before the notice is accepted (Q8): Updater.schedule returns at once and Updater.check returns a completed future (running done) unless Consent.accepted(). MainActivity.acceptNotice calls Updater.noticeAccepted (schedule, then check with UpdateCadence.Trigger.NOTICE_ACCEPTED, immediate). Best judgment: Consent.accepted means the current notice, so an install that accepted 25 checks nothing until it accepts 26 (nothing before acceptance, as PUBLIC_BETA_PHONE_GATE.md promises); its pending jobs, if any, return at once. Privacy describes what an update check sends, not when, so it is unchanged. UpdateReachTest.aBlockedUpdateBeforeTheNoticeIsAcceptedPostsNothingThenOnceAfterIt now expects nothing held before acceptance (there is no check), and the hold and one notice after. Tests: NoticeAskAndUpdateGateTest.
- Notice 26 (Consent.VERSION), BundledNotes notice(26) and release("0.6.18"), and Terms and Privacy (effective 14 October 2026, for 0.6.18) say pay first is the default goal (a 70% goal moved once, 50% stayed), the rate goals are opt-in, the best score accepted by hand in the last 7 days holds the bar across dashes, and (notice and Terms) only Dasher's usual decline question is confirmed, any other pausing auto-decline; Privacy lists the goal's "picked" mark; the notice also says it ends with the diagnostics switch, and that a reader fault restarts once and update checks wait for it. Simulated only: no phone has run it.
0.6.17 (14 October 2026): a plainer warning about DoorDash's terms (notes: notes/doordash-terms-2026-10-14.md, from the beta launch thread)
- Dillon asked, 14 October 2026: "can we ascertain whether we are breaking terms or not?". DoorDash's Consumer Terms, binding on Dashers through the Dasher agreement's §2.8, ban collecting content "using automated means" without written permission (§5(p)) and "any systematic retrieval of data" (§5(r)); the deactivation policy names "Scraping" by any "automated process" as abuse "subject to account deactivation". Declining itself is the Dasher's own choice (§1.2, §3.1, §7.7).
- Asked whether to say so plainly, the owner chose "Say it plainly". The notice's account point, TERMS.md (for 0.6.17) and the README say DoorDash's terms ban automated tools that read or collect data from its apps, and that Offer Filter is one, so DoorDash could limit or deactivate the account. Notice 25 brings it to everyone once. Privacy is unchanged (for 0.6.15). Nothing the app reads, keeps, sends or taps changes.
- Keep the warning a plain statement of what the terms say and what the app is; never a promise that DoorDash won't act. Don't go back to "may break" unless the owner asks.
0.6.16 (14 October 2026): the website's 3D art in the app (spec: notes/0.6.16/art.md, from the beta launch thread)
- Dillon, 14 October 2026: "let's also upgrade the app's 2d graphics to our 3d art, but we must not degrade performance, only enhance it". The mascot's body and its waving arm, the sun, the moon, the clouds, the chart's buildings and trees, the ground strip's houses, trees and car, and the launcher icon's mascot are offer-filter-site's 3D models (assets/models.js, lit as the website lights them), drawn ahead of time by tools/art/render-sprites.cjs into res/drawable-nodpi/art_*.webp (made for 640 dpi, about 83 KB in all) with their sizes and points in the generated ArtSprites (never edited by hand; ArtSpritesTest holds every number to its picture). Only a change to a model, a light or a picture's framing runs the script again.
- Art decodes a palette's pictures once a process on its own thread, never the main thread, scaled to the screen (never above 640 dpi), and keeps them (about half a megabyte a palette at 420 dpi). Every drawing falls back to its flat shapes until its picture is ready, when it cannot be read, or when it decodes to another size than its numbers say, and the views that wait for one are drawn again once it is (ArtTest). The face, the ring, the stars, the buildings' windows (one batched draw for each of lit, half-lit and dark), the badges and every word stay drawn over the pictures. Tests decode on the calling thread (ConsentedTestApp: Art.executorForTests) and start each with none decoded.
- No frame may cost more than it did: the homepage at 411×900 dp with 22 offers draws 276 steps where 0.6.14 drew 628 (54 paths to 20), and records in no more time (simulated on Android 16; no phone has run it). Nothing is read, kept or sent differently, so no notice change (Consent.VERSION stays 24) and the legal texts stay for 0.6.15. The monochrome (themed) icon layer is unchanged.
0.6.15 (14 October 2026): downloads are counted at the link (spec: notes/0.6.15/downloads.md, from the beta launch thread) — supersedes the 0.6.3 Counts bullet's sendBeacon description of the website's count
- Dillon, 14 October 2026, on the beacon's blind spots: "let's fix em". offerfilter.org's install page and the update server's page link to the feedback function's
/download. It answers every GET or HEAD with a 302 to the Render APK, and first counts a browser's GET (a User-Agent beginning "Mozilla/" with no robot or previewer word, and no prefetch or prerender purpose) as a "download". That count is once per network per UTC day, under the same day-keyed value, made after the answer. The user agent and purpose are read and never kept. install.js has no beacon any more; the function still counts its POST, from pages loaded before the change. The change is website-only, so there is no notice change (Consent.VERSION stays 24); PRIVACY's Download bullet says how.
0.6.14 (14 October 2026): pay per stop counts the pickup, the number unchanged (spec: notes/0.6.13/pay-per-stop-pickup.md, shipped as 0.6.14) — supersedes 0.6.12's rule C bullet and its wording (no "first pickup" wording anywhere; the 0.6.12 notice, card and log line were reworded)
- The owner, 14 October 2026, 00:26 UTC: "i don't want the pay per stop to not count the pickup"; 00:33 UTC: "it doesn't need to do any halving or any post artificial anything. it should be simple. if I say I want $5 per stop then a single pickup and single dropoff dash would require $10. if I have 3 stops, whatever the makeup, then it would require $15." n = AreaScore.payUnits(stops) = stops (Dasher's own count, pickup included) when read and ≥ 1; null when unread (a read of 0 is unread). A single order (Dasher's "2 stops") asks perStop × 2, 3 stops perStop × 3. Strict R100, area, score, bar and rounding are otherwise unchanged. Wording says "each stop, the pickup included" (MinimumsDetails: "3 stops: 3 × $5.00"); never "after the first pickup" for the current rule. Never halve or otherwise rescale the saved number.
- Unread stops keep the certain lower bound of 1 unit (perStop × 1, and learned bestPay ÷ units + 1¢) for the strict known maximum; never assume 2 stops. Monotonicity holds: more stops, a higher number or a higher bar never turn DECLINE into KEEP.
- FilterStore.initializePayPerStop runs two steps in order: rule C's (initializeRuleC, pay_per_stop_schema 1, unchanged) then initializePickupCounted (schema 2), once: FLAT untouched; a stored pay best's units become units + 1, in LearnedRaise's note too (its floor stays pay ÷ that offer's own stops + 1¢); MINIMUMS_SINCE moves when a money minimum or pay best exists; one "[rules] pay per stop now counts every stop, the pickup included (number unchanged, so a single order asks twice what it did)" line (FilterStore.PICKUP_COUNTED_LOG), none on a new install; never Autopilot's runtime. The 0.5.0 model-2 migration still writes schema 1, so the pickup step runs after it.
- Typical minimums (Autopilot.STARTER_FLAT_CENTS) are $2.00 a stop (best judgment, not in the spec): a single order still asks the typical $4.00 the starter was measured as, so "Use typical minimums" doesn't double what a single order must pay. A saved number is never changed.
- DecisionLog rules carry "payPer":"stops"; readRules returns null for anything else (0.6.12–0.6.13 "stop" lines and older), so Growth and samePolicy never pool rule-C lines. scoringKey ends ":pst" (AutoAccept.rulesKey changes with it).
- Tests that meant "a single order asks $X" now set pay per stop to X ÷ 2 (their offers and expectations unchanged); tests of 3+ stops, unread stops and stored-prefs migrations moved where the rule did.
- Notice 24 (Consent.VERSION), BundledNotes notice(24) and release("0.6.14"), and Terms and Privacy (for 0.6.14) say the pay minimum is per stop, the pickup included, and the number was not changed.
0.6.14 (14 October 2026): the skyline is buildings and trees again (spec: notes/0.6.13/skyline-motif.md) — supersedes 0.6.6's one-scale skyline bullet; 0.6.6's one badge and key and 0.6.8's "no spotlight beam" stand
- The owner, 14 October 2026, 00:28 UTC: "i miss the building/tree skyline motif for the chart"; on the card he chose B, "as before" 0.6.6 (the right panel of notes/0.6.13/skyline-mockup.png).
- DecisionChartView: each offer is a building as tall as its pay on the dollar scale (0.6.3's drawBuilding and scaleMax), coloured by what happened in 0.6.3's muted palette (green passed or accepted, amber below the minimums or left for review, red filtered; barColorAt returns the facade colour), with a tree beside it as tall as the recorded score on the percent scale (scoreScaleMax). An unread pay is the hollow "?" box; an unknown or retired score has no tree, an open ring at its foot.
- Lines: the solid pay line in payoutColor at what the pay minimum asks of a single order (Dasher's "2 stops") at the bar (AreaScore.floors with a 2-stop OfferSnapshot, roundedCents), labelled "Pay $X min" ("ref" in area mode) at its left end, only while a pay minimum is in use; the dashed 100% line in treeColor labelled "100%" at its right end (standing just left of the badge where the badge would cover it); Autopilot's dashed purple line only off 100%. No required-pay ticks.
- AutopilotText.SKYLINE_DESCRIPTION and describe() say buildings are pay and trees the score; the notes on unknown, retired and clipped scores say "no tree". Renders: notes/0.6.14/.
0.6.14 (14 October 2026): Dasher's ⚙ for an offer it announced and never drew — the owner's one exception to "taps only on the declined offer's own controls", for the ⚙ alone (evidence: notes/0.6.13/cog-and-directions.md; handoff: notes/0.6.14/gear-tap.md)
- The owner: "peek had a sporadic failure where dd app didn't display offer details until after cog wheel was tapped", then "still had to hit the cogwheel for the offer details to pop in on dasher app". On the decision card (14 October 2026, 00:21 UTC) he chose "Tap it": "the app taps ⚙ once per stuck offer. It does so only on Dasher's Finding offers screen, only while Dasher's notification of that offer is still up, and only if you haven't
Truncated - read the full file at https://github.com/Dillxn/dasher-offer-filter/blob/056eb880432120684c8067284842bf7988b293b4/AGENTS.md.
