Skip to content

ADFA-514: Update contact dialog with send email button and new email address - #91

Closed
Daniel-ADFA wants to merge 2 commits into
stagefrom
bugfix/ADFA-514
Closed

Daniel-ADFA wants to merge 2 commits into
stagefrom
bugfix/ADFA-514

Conversation

@Daniel-ADFA

Copy link
Copy Markdown
Contributor

Ticket: https://appdevforall.atlassian.net/browse/ADFA-515

Update contact dialog with send email button and new email address

@Daniel-ADFA
Daniel-ADFA marked this pull request as draft April 16, 2025 19:04
fryanpan added a commit that referenced this pull request Jul 30, 2026
Decision input for the release conversation: what a user actually experiences,
the mechanism with file:line, the device evidence, severity and frequency, and
the options with their tradeoffs. Tasks #87-#91. None is fixed.

None of the five is in the README's "Known limitations (v1)" list, and that is the
point: the documented limitations are boundaries the design chose, while these are
cases where the feature behaves worse than its own design intends. The never-stale
invariant holds in all five - the gap is reliability and honesty of feedback, not
speed or correctness.

Ship-blocking for v1: the install-confirm fast-fail (180 s of silence ending in a
message that says the user failed to confirm something never shown to them),
launch-and-retry after a rebaseline reinstall (deterministic today; the fast path
is dead until the user guesses), and the daemon-death reclassification (a one-line
body edit becomes a ~200 s rebaseline in any Room/KSP project, 3/3 on the A56).

Three corrections to the framing this doc started from, all substantiated:

- shrinkDaemonForMemory() does not no-op during a mid-start respawn. Its guard is
  on Building, so in Degraded it runs, and its daemonEpoch++ is a CAUSE of the
  respawn being discarded. A memory trim during a respawn can strand the session
  permanently with only a log.info.
- "Android never delivers PENDING_USER_ACTION to a backgrounded app" is wrong, and
  five places in the tree assert it. The broadcast is explicit to our own manifest
  receiver and is delivered (deferred at worst - our own run-2 findings say so).
  What is lost is the lifecycle-bound EventBus subscriber that owns the dialog,
  plus the Android 10+ background startActivity block. Fixing the platform theory
  points at the wrong repair.
- The Degraded session is not entirely silent - the toolbar icon does go to the
  ATTENTION shape and colour. What is missing is any text, and any feedback that
  the tap was swallowed.

Defect 3 has no device reproduction; the doc says so rather than implying one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkdFG7t9oNJtiayQzUQf89
fryanpan added a commit that referenced this pull request Aug 27, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Aug 29, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Aug 29, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 1, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 1, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 1, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 3, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 4, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Sep 4, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 5, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Sep 5, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 6, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Sep 6, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 6, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 8, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Sep 8, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 8, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 11, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 12, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 12, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 12, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 14, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Sep 14, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 15, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 16, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 16, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Sep 16, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 20, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Sep 20, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Sep 24, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Sep 25, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Oct 1, 2026
…by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A
fryanpan added a commit that referenced this pull request Oct 1, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Oct 1, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Oct 2, 2026
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
fryanpan added a commit that referenced this pull request Oct 2, 2026
…pp (#1716)

* ADFA-4128: qb 04/12 runtime — Inside the proxy app: swaps code, resources and assets into the running process

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W

* ADFA-4128: qb 04 review fixes — reload failure attribution + surfacing

- Stale pendingReloadGeneration mis-blaming later crashes: the backgrounded
  apply now assigns the pending slot too (Generations.pendingAfterApply), and
  BootProbation.generationToBlame refuses a pending value the store has moved
  past. Covered by BootProbationTest.aPendingReloadTheStoreMovedPastIsNotBlamed
  (fails without the fix) and GenerationsTest.aBackgroundedApplyClearsThePendingSlotItAlreadyAcked.
- failReload swallowing every pre-apply failure: the newer-generation guard is
  now a three-way Generations.onReloadFailure — never-applied failures skip the
  rollback/quarantine but still reportCrash + banner; only a failure superseded
  by a newer live generation stays silent. Covered by
  GenerationsTest.aFailureTheStoreNeverAdoptedStillReports.
- Binder-thread setProviders + immediate provider close racing main-thread
  inflation: ResourceStore now performs the field swap, setProviders and the
  close of the replaced provider on the main thread (inline when already there,
  so the boot restore path still lands before first inflation; Looper FIFO keeps
  a posted swap ahead of the posted recreate). Pure threading with no JVM seam —
  justified in swapProvidersOnMain's doc; device-covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W

* ADFA-4128 (4/11): address CodeRabbit review

- F1716-2 heal a half-finished asset merge on the next run
- F1716-5 stop answering a VirtualMachineError with another allocation
- F1716-7 take the asset length from the descriptor already open
- F1716-8 un-commit a resource provider swap that failed to install

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7

* ADFA-4128 (4/11): address Akash's review

Fixes for every finding on the runtime module, plus two changes that came out of
reviewing them.

Crash banner. The copy said "New code crashed", which named the one event this
banner cannot observe: the CRASHED state is set only from failReload, so it is
always the reload machinery that failed, never the user's own code. It also
carried a stack summary it had no room for. It now reads

  Live reload crashed. App is on the last working version.
  For more info, see Build Output in Code on the Go.

and points at the pane where the full text already goes unchanged. At the
narrowest width measured on an A56 at 2x font scale that is five rendered lines,
four from 28 characters up; MAX_BANNER_LINES stays at 6, one line of slack,
because the line a tighter cap drops is the tail of the pointer - which leaves
the reader told to look somewhere without the name of the place.

Crash report. It walks up to three causes and prints each one's frames, not just
its toString. An Android lifecycle crash always arrives wrapped, so the top
frames are ActivityThread's every time and the line naming the developer's bug
sits in the cause; reporting the message alone named the exception without ever
placing it.

markGood retry. lastMarkedGoodGeneration was set before the write was attempted,
so a failed markGood was never retried and its latch blocked every later one for
the process lifetime, and the KDoc's justification was inverted. Clearing the
latch on a bare false is not safe either - markGood answers several situations
with one false, and persist runs before apply, so meta.json is briefly ahead of
the live generation on every deploy. markGoodCanSucceed separates a failed write
from a store that moved on, and only the failed write clears.

Payload overtake. onPayload is oneway, so a slower older deploy can be overtaken
while it reads its payload and then publish itself over the newer one, leaving
disk a generation behind the running process until the next cold boot adopts it.
PayloadStore.apply already refuses a generation that is not strictly newer, so
this could never reach the screen - only disk. PayloadPersistence now keeps the
highest generation this process has published and refuses anything older,
throwing StalePayloadException so the deploy path can tell a lost race from a
broken store and stay silent about it. The bar rises only after the publishing
rename, so a persist that threw part-way does not block its own retry.

That guard also separates the two cases a generation number alone conflates. A
restarted host counter - the project's state dir wiped while the app stays
installed - always arrives in a process that has published nothing, so the mark
is zero and the low generation is adopted as before. Both counter-restart tests
now build a fresh store object over the same directory, which is the only shape
that case has on a device.

Payload memory. The resource apk and the assets zip were read whole into memory
and written straight back out to files that are reopened as files afterwards, so
a cold deploy held two payload-sized arrays live for no benefit on the devices
least able to spare them. persist now takes both as streams and copies them
through a 16 KB buffer into the same temp-then-fsync-then-rename write. Only the
dex stays a byte array, because InMemoryDexClassLoader needs one. The 64 MB cap
is unchanged and now guards the streaming path; the parameter types are what
keep it that way.

Also from Akash: the manifest-merger comment, the KDoc corrections, and the test
helper that divided length by width - it modelled a renderer that breaks
mid-word, so it read the banner's six real lines as five and could not have
caught the overflow it existed for. It wraps on words now, and was watched
failing at the old cap before the cap moved.

Both new gates were watched red first: the payload cap with its check stubbed
out, the overtake refusal with its condition forced false. Only the intended
test failed each time. 248 tests green.

Banner inset. Photographing the new banner at 2x font scale showed it drawing
over the status bar: getRootWindowInsets() comes back null on the first render
after a config-change recreate, and the overlay took that as a 0 inset. A null
read now means "not measured yet" - the margin is left alone and re-read after
the next layout, once, by a listener that removes itself. The decision is a pure
static so it can be unit-tested; the deferred re-read firing is checked on a
device (A56: banner flush below the 101 px bar at 1.0 and 2.0).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xsc7AMGBVyEMfrwpZX87iC

* ADFA-4128 (4/11): drop the unused IQuickBuildHost.disconnect AIDL method

The runtime only ever disconnected by process death, which
ProxyAppConnections.onDisconnected already handles. Asked for in review on #1718.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xsc7AMGBVyEMfrwpZX87iC

* ADFA-4128: qb-04 review fixes - overlay observer capture, BUILD_FAILED clamp, streamed apk write, resource-attach latch, log overload

Akash's 08-31 review of #1716, items 1/2/3/5/6 (4 and 7 deferred to
followup tickets per the triage):

- StatusOverlay: capture the ViewTreeObserver at add time; on GIVE_UP the
  banner is detached and a re-fetch returns a floating observer, making the
  removal a silent no-op. isAlive() guard with a decor-observer fallback.
- OverlayState/CrashSummary: BUILD_FAILED detail clamped to a one-line
  budget (BUILD_FAILED_DETAIL_CHARS) and the Build Output pointer appended,
  same shape as CRASHED; budget arithmetic documented from both ends.
- LegacyResourceSwap: stream the resource apk to disk instead of buffering
  it in heap (small-heap API 28/29 devices); partial file deleted on a
  failed copy so the throw-means-nothing-written contract holds.
- ResourceStore: attachedAppResources latched only when the attach
  succeeded, so a swallowed failure is retried on the next deploy.
- QuickBuildClient: two-arg RuntimeLog.w keeps the stack trace.

Tests: clamp test verified red before the fix; partial-file-delete test
added; three edge tests updated for the pointer line. 255 green.

Also: plain-language pass over the comments added by these fixes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STCsdMzx9daNBcqMN424Ci

* ADFA-4128: 0902 review round on quickbuild:runtime

Ordering and lifetime fixes from Akash's 2 September round, plus the nitpick
sweep.

- The failed reload's decision and its rollback now happen under one lock
  (PayloadStore.restoreIfCurrent), so a deploy that applies between the two is
  no longer rolled back by a decision taken before it existed.
  #1716 (comment)
- A generation whose resource swap failed is marked before the failure is
  dispatched off-thread, and the posted recreate skips it. Moving failReload
  off the looper had left the recreate free to render the generation the
  rollback was undoing, and the mark also covers the inline swap, which fails
  before the recreate is posted at all.
  #1716 (comment)
  #1716 (comment)
- Provider swaps carry their generation and drop an overtaken one instead of
  installing it, so a slower deploy's swap landing last cannot put the older
  table back under the newer generation's label.
  #1716 (comment)
- The connect() handshake - the one non-oneway host call - runs off the
  binding callback's thread, so a cold CoGo no longer blocks the proxy app's
  main thread.
  #1716 (comment)
- Asset extraction is capped cumulatively at MAX_PAYLOAD_BYTES, matching every
  other payload path.
  #1716 (comment)
- The five fallback catches in the component factory rethrow fatal errors, so
  an OOM is grouped as itself rather than under the payload's throwable.
  #1716 (comment)
- writeAtomic no longer short-circuits on the delete: a first write, where
  there is nothing to delete, skipped the retry and leaked the temp file.
  #1716 (comment)
- PayloadStore's last throwable-concatenation site takes the two-arg log.
  #1716 (comment)
- LegacyResourceSwap's KDoc names the test that exists.
  #1716 (comment)
- The fail-reload dispatch test's KDoc claims what the test pins - the helper's
  contract - and says what it does not.
  #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* style: spotless reformat of QuickBuildClient, no functional change

The Eclipse formatter's member sorting moves connectToHost below its caller.
Standalone so it does not read as a behavioural change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: hold a generation blamable until its first frame has drawn

Answers review thread 3926539435 (CRITICAL) on PR #1716.

onResume precedes the first traversal, so nothing that vouches for a payload
could legitimately happen there. The runtime acked the generation, cleared the
pending slot and wrote good.json from onActivityResumed, so a payload whose
activity resumed and then threw in measure, layout or draw reached the crash
guard with nothing pending and nothing unproven. generationToBlame returned -1,
quarantine then refused to name a generation already recorded good, and every
relaunch adopted it and died the same way with no in-app escape.

The ack, the pending-clear and the good-marking now move together into
onFirstFrameDrawn, released by a ViewTreeObserver.OnDrawListener whose
completion is posted rather than run inline: the listener fires at the start of
a draw pass, so only the posted message runs after the traversal that drew the
frame returns. An activity with no live view tree completes inline as before,
since waiting for a frame that will never arrive would strand the deploy
unacked.

The pending slot moves out of QuickBuildRuntime into FirstFrameGate, which is
plain Java and JVM-testable; FirstFrameGateTest pins that the generation stays
blamable across the undrawn window and is released only by a drawn frame.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: serialize and order the API 28/29 resource swap

Answers review threads 3926539450 and 3926539457 (both IMPORTANT) on PR #1716.

applyTableLegacy mutated the live AssetManager and flushed the Resources caches
straight off the binder thread the deploy arrived on, which is exactly what
swapProvidersOnMain's own KDoc says must not happen: addAssetPath re-tables the
AssetManager and flushCaches drops the drawable and typed-value caches, and
either can race an inflation already in progress. The path is live rather than
dead - CoGo's classifier routes resource edits with no SDK gate, so a res/ edit
on a 28/29 device lands here.

It also compared nothing before mounting, while both loader swaps drop an
overtaken generation against swappedGeneration. Two binder threads could
interleave so that the older table was the last one added, which wins the lookup:
the screen resolved gen N-1 while the store reported gen N, and legacyTableZip
then handed that apk to every activity created afterwards.

The write stays on the calling thread; only the mount is posted, under the
store's monitor, behind the same generation comparison its two siblings use. A
mount failure now travels back through the swap-failure listener instead of being
thrown synchronously, so the deploy still fails rather than acking a table that
never mounted.

Not JVM-tested: ResourceStore needs a Context, a Resources and a live main
Looper, and is one of the classes this module's coverage gate excludes as
device-only glue. Both siblings' identical guards are untested for the same
reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: a stale handshake failure must not tear down a live binding

Answers review thread 3926539468 (IMPORTANT) on PR #1716.

Moving the connect handshake onto its own thread left both failure branches
writing the shared host field unconditionally. The handshake can outlive its
binding: CoGo's service dies, onServiceDisconnected nulls the host, the framework
reconnects and a second handshake succeeds against a new proxy. The first
thread's connect() then fails and nulls that live host, unbinds a healthy channel
and schedules a rebind - and until the rebind lands, reportReloaded and
reportCrash only log "not connected", so every deploy in the window can end only
in the host's own timeout.

Both branches now go through abandonHandshake, which takes the monitor and
returns early unless the proxy that failed is still the live one. The test and
the teardown have to be one step because host is also written from the
framework's callback thread.

Not JVM-tested: QuickBuildClient is binder and ServiceConnection glue over a
Context, an IBinder and a main-thread Handler, and is one of the classes this
module's coverage gate excludes as device-only. The interleaving the reviewer
describes is a plausible reading of the code rather than one reproduced here;
what is verified is that the guard was absent and is now present.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: delete a temp file on every failure, not just a failed rename

Answers review thread 3926539487 (MINOR) on PR #1716.

AssetExtractor.writeFile deleted its .qb-tmp only from the rename fallback, so a
throw out of the copy - the payload cap, a write failure, a truncated entry -
left the partial file under current/assets/, which is the tree
DirectoryAssetsProvider resolves names against, and the app could open it by
name. Both write paths now delete the temp from one finally that covers the copy,
the close and the rename alike.

The reviewer cites two siblings as already correct. Only one is:
LegacyResourceSwap.writeResourceApk does delete its partial file when the copy
fails. PayloadPersistence.writeAtomic has the same gap being fixed here - its
finally only closed the stream, and its temp.delete() sat in the rename fallback
- so an oversize payload or a full disk left a .tmp in the store directory that
nothing sweeps. Both are fixed here.

Two tests, each verified to fail without the fix and for its own reason - a
leftover temp file, not an unexpected throw:
AssetExtractorFailurePathTest.aCopyThatFailsMidEntryLeavesNoTempFile drives a
truncated zip entry, and
PayloadPersistenceAtomicWriteTest.aStreamThatFailsMidCopyLeavesNoTempFile drives
a payload stream that dies mid-copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: drop the no-op test-heap override

Answers review thread 3926539494 (NITPICK) on PR #1716.

The root build already sets maxHeapSize = "1g" on every Test task in every
subproject (build.gradle.kts, the subprojects tasks.withType<Test> block), so
this module never saw Gradle's 512 MB default and the override changed nothing.
Its comment also compared against neither the real cap - Streams.MAX_PAYLOAD_BYTES
is 64 MB - nor the heap actually in force, so a reader trimming test memory later
would have trusted it twice over.

Verified against the root build before removing: the subprojects block does set
it, so dropping this leaves the same 1g in effect.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: style: spotless reformat, no functional change

The Eclipse Java formatter sorts members, so files this round touched came under
the ratchet and had their declarations reordered. Kept standalone so the
behavioural commits around it stay readable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: hold a backgrounded deploy's ack until its resource swaps commit

Answers review threads 3926539472 (IMPORTANT) and, in part, 3926539480
(IMPORTANT) on PR #1716.

The earlier swap-failure fix reached only the foreground branch. applyTable posts
its swap to the main looper and returns, and the backgrounded branch then acked on
the binder thread while that swap was still queued. When the swap failed,
onSwapFailed ran later, rolled the store back to gen N-1, quarantined N and
reported the crash - after CoGo had already been told N reloaded. DeployChannel
resolves a deploy on the first report naming the generation, so the ack won: the
build was recorded as a successful reload with a timing number, the Crashed branch
never ran, and the session manager's separate collector still raised
RELOAD_CRASHED, so one save produced both signals. The comment on that branch says
the backgrounded case is the normal edit loop, so it is the branch a failing
resource swap usually takes.

SwapFailure becomes SwapOutcome and gains the success counterpart the reviewer
points at. Its contract is that exactly one of the two fires per applyTable or
applyAssets call that returns normally - including the calls that queue nothing
because this SDK level has no swap to make, since a deploy waiting on one of those
would wait forever. A swap dropped as overtaken reports committed rather than
failed: it returned normally, and the generation that overtook it owns the screen
and its own ack.

SwapAckGate counts a deploy's posted swaps down to the one ack it owes, and a
failure cancels it for good so a second swap landing afterwards cannot turn a
rolled-back deploy back into a success. A dex-only deploy - the commonest one -
posts nothing and still acks immediately, through noSwapPosted rather than
through committed, so a deploy with one swap in flight cannot mistake that call
for its swap's own commit.

The resumed check moves above the applies, because the commit callback is free to
fire before handlePayload returns and has to know which branch it is completing.
Arming the first-frame gate moves with it, which also closes a smaller hole: an
apply that threw used to leave an older generation's value in the pending slot.

The abandoned-generation half of 3926539480 comes with it: an applyAssets failure
left the table swap applyTable had already queued live under the rolled-back dex,
and nothing marked the generation abandoned, since only onSwapFailed did that. The
recreate then rendered gen N's table over gen N-1's classes while the banner said
the app was on the last working version. handlePayload's catch now marks it.

DEFERRED, deliberately: the other half of 3926539480 - having failReload restore
the provider set alongside the payload. A resource rollback is a new capability,
not a guard: ResourceStore keeps no per-generation provider history, the API 28/29
path cannot unmount an added asset path at all, and the store's own KDoc already
documents "a swap that already took is not undone" as the contract. Adding one
belongs in its own change with its own device verification, not folded into a
review fix. Marking the generation abandoned already stops the recreate, which is
what makes the banner honest.

SwapAckGateTest pins the rule and was verified to fail without it: with the gate
mutated to ack regardless of queued swaps, and with failed() made a no-op, three
of its seven tests go red on exactly the assertions they are named for. The call
site itself - handlePayload counting its swaps - needs a binder thread, a main
looper and a Context, so it is checked on device; QuickBuildRuntime and
ResourceStore are both in this module's device-only coverage exclusion list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: name the unreported-relaunch-crash gap by its ticket, not by a planning-doc number

The backgrounded-deploy comment in QuickBuildRuntime deferred to "gap #91",
a number from quickbuild/docs/reliability-gaps.md that a reader of the
comment cannot follow. State the gap in words and cite ADFA-5466, filed for
it today; the gaps table carries the same key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: pin the first-frame call site, and refuse an abandoned generation's swap

Two review gaps on the runtime, both about a generation that is no longer
supposed to be believed.

The first-frame gate had a test for the gate class but nothing for its caller,
so reverting the routing - completing the reload at onResume again, which is
the pre-fix behaviour - left every test green. onActivityResumed needs an
Activity, a Window and a live ViewTreeObserver, so the routing moves into a
package-private seam, completeOnResume, the same shape as startFailReloadThread.
The new test drives that seam and asserts what the resume must NOT do: with a
frame still coming it completes nothing, so the generation stays pending in the
gate and BootProbation still names it.

The second is a swap the store used to commit after the deploy that queued it
had already been rolled back. A swap is posted to main and commits after
applyPayload returns, so a deploy that throws in a later step - applyTable posts
before applyAssets can throw - had its rollback run with its own table swap
still queued. The store already drops an OVERTAKEN swap in all three swap
bodies; this adds the sibling case, an ABANDONED one, through the same guard.
Undoing a committed swap is not available: the store keeps single provider slots
and closes the previous provider after each swap, and the API 28/29 path cannot
unmount an added asset path at all, so refusing the commit is the whole remedy.
The runtime calls abandon() from both places it already gives up on a
generation.

The three swap bodies run on the main looper, so their call to the guard is not
pinned by a JVM test; what is pinned is the decision they take.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: say why the banner copy is inline English

The runtime AAR is injected into the user's app and carries no res/ of
its own, so the banner cannot use a string resource; REVIEW.md asks for
that opt-out to be stated, not inferred. MAX_BANNER_LINES is derived
from the literals' character counts, so its KDoc now says the
arithmetic assumes the English copy.

Review thread: #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: report a mixed state when the failed swap had already committed

failReload rolls the dex back, but a resource swap that committed before
the failure stays live: the store keeps single provider slots and the
API 28/29 path cannot unmount an asset path. That is the common
ordering, since applyTable posts and returns while applyAssets merges
on the binder thread. The banner then said "App is on the last working
version" while the screen served the failed generation's table under
the previous generation's classes.

Now the failure path reads ResourceStore.swappedGeneration() after the
generation has been abandoned (so a still-queued swap is refused rather
than committing later) and, when it equals the failed generation, shows
OverlayState.mixed() - "Restart the app - it is running mixed versions"
- and prefixes the report to CoGo so Build Output carries the restart
instruction in full. The decision lives in Generations.leavesMixedState
so it is JVM-tested; the wiring in failReloadNow is device-only.

Review thread: #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: run the boot resource restore off the main thread and report its failure

applyPendingBootResources is dispatched from onActivityPreCreated, inside
the first activity's creation on the main thread, and ran the whole
restore inline: the asset merge (a recursive delete plus an unzip) and,
on API 28/29, the relinked apk copy, both bounded only by the 64 MB
payload cap. Every cold start that adopts a persisted generation with
resources - every save after a restart deploy, and every process death
- paid that as launch jank or an ANR on the low-end devices the legacy
path exists for.

The extraction now runs on a qb-boot-restore thread and only the swap
is posted; the first activity inflates against the baseline table and
is recreated once the last swap lands, counted by the same SwapAckGate
a backgrounded deploy uses. markLiveGenerationGood waits for the
restore, since a frame drawn against the baseline proves the code half
only.

The restore also had the one remaining null outcome listener, so a
corrupt store file or a full disk inside the merge left the process on
this generation's code over the installed resources with one log line
and nothing else. It now shows the mixed banner and reports to CoGo
with a boot-specific first line; the report is best-effort, since CoGo
may not have connected yet.

Review threads:
#1716 (comment)
#1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: remove the first-frame draw listener from the captured observer

The listener was added to the observer captured before the draw and
removed from a re-fetched decor.getViewTreeObserver(). Once the decor
is detached - the activity destroyed between the draw and the posted
completion - that accessor returns a fresh floating observer that never
held the listener, so the removal was a silent no-op. Remove from the
captured observer while it is alive, falling back to the decor's when
the framework has merged it away, as StatusOverlay.reapplyInsetAfterLayout
already does.

Review thread: #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: write the client's host proxy under the monitor everywhere

abandonHandshake tests host and tears the binding down under the
monitor, but the four framework callbacks wrote host as plain volatile
writes, so the exclusion held only against other synchronized callers.
A disconnect-then-reconnect on the main thread could still land between
the handshake thread's read and its write and unbind a healthy binding.
onServiceConnected now writes under the monitor and the three null
writes go through a synchronized dropHost(); the callbacks themselves
stay unsynchronized so the main thread does not wait on a handshake
thread's binder round trip.

Review thread: #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: document the recreate-into-stopped case FirstFrameGate does not release

A recreate that succeeds but relaunches into the stopped state, whose
task is then swiped away, never resumes: no draw callback is installed
and nothing else releases the slot, so the deploy ends in CoGo's
timeout and the generation stays blamable until the next save. The
KDoc listed two no-frame fallbacks and not this one. It is recorded
rather than wired: recreate() destroys the armed activity on every
normal reload, so an onActivityDestroyed release would also need to
know a relaunch is still pending, which nothing tracks yet.

Review thread: #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: refuse asset payloads below API 30 instead of acking a merge nothing reads

Nothing below API 30 reads the merged asset dir: DirectoryAssetsProvider
needs a ResourcesLoader and LegacyResourceSwap mounts the resource apk
only. The legacy arm still ran the merge and reported the swap
committed, which is what settles a backgrounded deploy's ack, so a
reload the app could not show was acked. It now reports failed with the
reason before touching the fd or the Context, and the comment names the
host gate (QuickBuildModule's assetsLiveReloadable, the classifier)
that keeps it unreachable today.

The applyAssets KDoc also said a partial merge stays live until the next
deploy overwrites it; extractCumulative leaves MERGE_PENDING_MARKER and
the next merge clears the whole dir. Reworded to say so.

Review threads:
#1716 (comment)
#1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* style: spotless reformat, no functional change

Eclipse member sorting over the members the review-fix commits added;
no line inside any member changed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: decide what a frame proves on its draw pass, not when its completion runs

markLiveGenerationGood read bootRestoreInFlight when the posted first-frame completion
ran. The draw listener fires inside the traversal, an async message ahead of the sync
barrier, and the completion is posted behind it, so a boot restore's swap message could
land in between: the frame drew the baseline table, the swap committed and cleared the
flag, and the completion then recorded good a generation whose table never rendered -
unblamable if that table fails on the next boot.

frameCompletion samples the flag on the draw pass and hands the fixed verdict to
onFirstFrameDrawn / markLiveGenerationGood, which no longer re-read it. The seam is
static and Android-free so QuickBuildRuntimeFrameCompletionTest can pin the ordering.

Adversarial review 2026-09-04, finding #3 on 540eb96dd.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: say what a failed boot restore actually leaves behind

The catch in restoreBootResources claimed a failed restore leaves the process
wholly on the baseline table. abandon() only refuses a swap still queued; a
table swap that committed before applyAssets threw stays live, the app runs
mixed, and onBootRestoreFailed already reports it as mixed. The comment now
says that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: serialize the cumulative asset merge against a second binder dispatch

Payloads arrive on a oneway binder callback, whose thread pool can dispatch two
at once - the interleaving PayloadPersistenceAtomicSetTest already pins for the
persist. extractCumulative merged into the one shared override dir with no lock,
so two merges could race entry-for-entry, and the pending marker cannot recover
that: the second merge clears it on the way out, leaving the dir holding two
generations with nothing left to notice.

The new test holds the extractor's monitor and asserts a concurrent merge cannot
finish, the same deterministic shape persistSerialisesOnTheStoreMonitor uses.
Without the synchronized keyword it fails with "a merge ran to completion while
the extractor monitor was held / expected to be false".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: stop two runtime tests passing for a reason they do not test

LegacyResourceSwapSweepTest.isBestEffortOverAnApkItCannotDelete assumed
setWritable(false) denies deletion. A root worker unlinks regardless, sweeps the
apk away, and fails the test for a reason it is not about. It now probes the
capability at stake - a sacrificial file in the same locked directory - and
skips when that deletes, rather than inferring privilege from a uid or from
user.name, which is not tied to the effective uid at all.

PayloadPersistenceAtomicSetTest.concurrentDeploysAlwaysLeaveOneWholeLoadable-
Generation read failure.get() straight after a timed-out join, so a worker still
inside persist could record its failure afterwards and the test would have
passed over it. Asserting the threads are not alive first closes that, and making
them daemons stops a hung persist outliving the Gradle worker.

Both proven by mutation: inverting the sweep test's guard reports it skipped with
"this worker deletes despite the directory mode", so the probe reads the real
filesystem capability; holding the store monitor across the joins leaves the
pre-fix test green with both workers still running, and red on "dex deploy thread
did not finish" with the assertions in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: refuse an abandoned generation's swaps by generation, not by watermark

A cold start restores persisted gen 10 on qb-boot-restore while CoGo's
catch-up gen 11 arrives and fails; abandon(11) as a high-water mark then
refused gen 10's queued swap, and a refused swap reports committed, so the
restore logged success and recreated the activity over the baseline table
with no banner. Abandonment is now a set of generations, pruned as swaps
commit since the overtaken rule already covers everything below the
committed one. The dropped-swap log lines name the actual reason.

Answers #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: sweep the API 28/29 apk cache from the first write, off the main thread

onActivityCreated runs on the main thread inside the first activity's
creation, and the sweep it called there is a readdir plus one unlink per
apk the previous process wrote - the launch-path disk IO the boot restore
was just moved off main to avoid. The sweep now lives in ResourceStore,
under the lock every legacy write takes, and runs once before this process
writes its first relinked apk: that is still ahead of the first mount, it
runs on whichever off-main thread the write arrives on, and no latch is
needed to keep a deploy's write from racing it.

Answers #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: stash a persisted generation for boot restore only when it carries resources

loadPersisted stashed every adopted generation as pending boot resources,
so a dex-only one - the usual case, and what a restart deploy persists -
started a restore thread that swapped nothing, reported itself landed and
recreated the first activity for it on every cold start. Only a Loaded
with an arsc or an assets file is pending now.

Answers #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: give the boot restore's failure listener the deploy path's two guards

A persisted generation carrying a table and assets lands in two swaps. When
the first failed, the listener reported the mixed state but never abandoned
the generation, so the second swap committed anyway; and when both failed,
onBootRestoreFailed ran twice - two banners, two crash reports for one boot.
The listener now abandons the generation and reports only the first failure,
which SwapAckGate.failed() reports by settling the gate exactly once. The
deploy path's listener takes the same first-failure guard.

Answers #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: report a payload that fails before acceptance instead of rolling back

handlePayload's catch passed the pre-apply snapshot to failReload, but
that snapshot is still null for a failure before the acceptance check -
a dex read that throws, a malformed metadata document. failReload then
restored null whenever the live generation equalled the failed one, so a
replayed generation whose read failed went inert and quarantined the
generation the app was running. A pre-acceptance failure now takes a
report-only path: banner and crash report, no restore, no quarantine.

Answers #1716 (comment)

No JVM test: handlePayload needs ParcelFileDescriptor and SystemClock.
Reasoned from the code, not run on a device.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: keep unbind and rebind outside the client monitor, and drop the host on a null proxy

abandonHandshake was synchronized as a whole, so unbindService - a
synchronous binder transaction - ran under the monitor the framework's
main-thread callbacks take, which is the stall dropHost's KDoc says the
design avoids. Only the host test and the null write need the monitor.

onServiceConnected's null-proxy branch left host set while it unbound
and scheduled a rebind, and the rebind runnable returns early while a
host is set; the other failure paths drop the host first, so this one
does too.

Answers #1716 (comment)
and #1716 (comment)

Not run on a device; both are reasoned from the code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: make the status banner a polite live region

The banner is the only in-app trace of a failed deploy and takes no
focus, so a screen reader never announced it.

Answers #1716 (comment)

Not yet checked with TalkBack on a device.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: clear a persisted payload the stamped baseline rejects

The store is keyed on the baseline dex alone, so a dex-identical
rebaseline left the superseded epoch's files on disk and the next
deploy's persist inherited that epoch's meta as its own history.
selectPersisted now clears the store when it rejects a payload.

Answers #1716 (comment)

PersistedSelectionTest.aRejectedPersistedPayloadIsClearedFromDisk fails
without the fix: "value of: load(...) expected: null but was:
PayloadPersistence$Loaded@226b143b".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: pass the stamp read failure to the logger

Answers #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: round 5 doc and comment fixes on quickbuild/runtime

- FirstFrameGate: cite ADFA-5524 for the unreleased-recreate case.
  #1716 (comment)
- PayloadStore.restore: documented as the test seam it is; production
  rollback goes through restoreIfCurrent.
  #1716 (comment)
- AssetExtractor.writeFile: temp-deletion note in the description,
  remaining and the return documented.
  #1716 (comment)
- ActivityTracker.onActivityCreated: the first activity attaches nothing;
  the boot restore's recreate delivers the restored table.
  #1716 (comment)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* style: spotless reformat, no functional change

Eclipse member ordering for the two Java files added in the round 5 fixes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: sample the drawn generation on the draw pass, not at completion

onFirstFrameDrawn read the store's live generation when the posted
completion ran. A deploy on a binder thread can persist, apply and arm a
newer generation between a frame's draw pass and that message, so the
completion acked the newer generation off a frame that drew the older one
and markLiveGenerationGood wrote good.json for a generation that had never
rendered - which quarantine() then refused to name.

frameCompletion now samples the generation on the draw pass alongside the
boot-restore flag, and drawn() and markLiveGenerationGood() take it instead
of re-reading the store. A newer generation armed in the gap stays pending
until a frame of its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: keep the boot restore's payload files out of the orphan sweep

collectOrphans protected only the just-published names and good.json's. On
a cold boot the adopted generation has never drawn, so good.json does not
name it, and the restore opens its files one at a time (on API 28/29 the
whole relinked apk is copied before the assets zip is opened). A catch-up
deploy persisting in that window swept the not-yet-opened file, openReadOnly
threw, and a deploy that succeeded raised the mixed banner plus a crash
report.

PayloadStore now retains the boot-pending names in the persistence store
when it stashes them, the sweep treats them as referenced, and the runtime
releases the hold once the restore has landed or failed. The set is a
volatile replaced whole so the main-thread release never contends with a
persist for the store's monitor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: guard the two apply catches on the swap gate too

onSwapFailed returns unless gate.failed() settled the gate, but the catch
blocks in handlePayload and restoreBootResources called failed(), discarded
the result and reported regardless. A persisted generation carrying both a
table and assets could then report twice for one boot: the table swap fails
on main and reports, the restore thread throws in applyAssets, and the catch
reports again - a second mixed banner and a second crash report.

ackGate moves out of handlePayload's try so its catch can ask it, and both
catches now report only when they are the first to settle the gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: put both apply catches through one tested report-once seam

The previous commit added the two guards but left them unreachable from a
JVM test: SwapAckGateTest pins SwapAckGate.failed()'s idempotence in
isolation, so deleting either `if` and reporting twice again kept the whole
suite green. The defect they fix is user-visible - a second mixed banner and
a duplicate crash report for one save - so it needs a test that goes red.

handlePayload's catch and restoreBootResources' catch now both call
reportApplyFailureOnce(gate, report), which owns the guard AND the report it
gates. That is what makes it testable: a bare boolean helper would still
leave the `if` at each call site, deletable with the suite green. The Runnable
seam follows completeOnResume, the same shape already in this file for the
same reason - the real call sites need a binder thread, a main looper and a
Context, so the branch is otherwise checkable only on a device.

Behaviour is unchanged. handlePayload's negative branch fell through to the
`finally` via `return`; it now falls through by not running the body, and the
boot restore's unconditional abandon stays ahead of the guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WMhaGYg4sSCcAzQEdNtLa

* ADFA-4128: say what PersistedSelection gates instead of naming a test scenario

Akash on #1716: "the S7 fix" is our shorthand for a scenario in a test-plan
doc, so a later reader of this class has nothing to look it up in. The
sentence after it already says what the gate does, so the shorthand was
carrying no information the KDoc did not otherwise have.

The test's class comment used the same two phrases; it now names the class
under test and the failure the mutation reproduces.

Comments only. No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XN91S41dghWHoXAoWhJAnm

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
fryanpan added a commit that referenced this pull request Oct 2, 2026
…the benchmark harness (#1723)

* ADFA-4128: style: spotless reformat of the build-status layout, no functional change

The Eclipse WTP formatter's own output on layout_editor_build_status.xml, landed
standalone as the repo's code-style section prescribes. It reindents the file to
tabs and rewraps the license header into one run, which is most of the diff and
none of the meaning.

Split out so the two-attribute change that follows is readable on its own.
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3926569470

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* style: spotless reformat of SaveFileAction.kt, no functional change

The Spotless ratchet reformats a touched file in full; this commit carries that
reformat alone so the three-line change that follows reads as three lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: qb 11/11 app + bench — wires Quick Build into the IDE and adds the debug-only benchmark harness

The app wiring and the bench surface land together because they are mutually
dependent: :app's ProjectHandlerActivity and QuickBuildModule call into
QuickBuildBenchHooks, and QuickBuildBenchHooks returns AutostartBuild and resolves
EnvironmentQuickBuildPaths. Neither ordering of a two-PR split compiles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W

* ADFA-4128: qb 11 review fixes — install rotation window, flag-off intent, test gaps

Important 1 (daemon idle-timeout/Metaspace tuner un-gated): kept un-gated by
design — the 384m Metaspace floor fixes real OOM-killed builds and the tiered
idle timeouts keep low-RAM devices from losing the IDE to lmkd; GradleBuildTuner
now states this in its KDoc. Ships flag-off; needs Bryan sign-off in the PR body.

Important 2 (generateSources narrowing un-gated): judged a genuine all-users
improvement, not QB-specific — the old code ran a Gradle generateSources after
EVERY save-all (and after any XML save in SaveFileAction), a per-save build tax;
flag-off the deferral degenerates to the same immediate call, so the narrowing
is the only behavior change. Known trade (manifest-only edits leave generated
Manifest/R intermediates stale until the next resource save or build) now
stated at both call sites. Ships flag-off; needs sign-off in the PR body.

Important 3 (install dropped on rotation, flag on): installApk's async path now
re-arms AwaitingInstall (BuildViewModel.reArmInstall, fires only from Idle)
from the coroutine's drop path, so a configuration change during the APK-manifest
parse makes the recreated activity's collector retry the install instead of
silently losing a successful build. Covered by BuildViewModelInstallReArmTest.

Test gap (zip-slip guard): extraction loop extracted to
QuickBuildArtifactStager.extractDaemonZip(InputStream, File); the guard is
watched going red by QuickBuildArtifactStagerTest (a ../ entry throws and
nothing lands outside the daemon dir).

Test gap (InstallationEventFlow mapping): InstallationEventFlowTest pins the
PackageInstaller status mapping, including the ABORTED-vs-FAILURE branch order
and the no-extras / no-status paths.

Test gap (service-side output capture): suppress/capture/drain routing
extracted from GradleBuildService.logOutput into InternalBuildOutputCapture;
bounded tail, drain-clears, throwing progress listener and editor-listener
routing pinned by InternalBuildOutputCaptureTest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W

* ADFA-4128: state the daemon-timeout rationale without a benchmark multiple

The Gradle daemon idle-timeout comment quoted a speedup multiple, which put a
benchmark figure into shipping production code. The reason the timeout is
generous is structural - a warm daemon skips the cold start, which dominates a
short rebuild - so the comment now says that instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W

* ADFA-4128 (11/11): address CodeRabbit review

- F1723-1 put QuickBuildPipelineTest into the suite that actually runs
- F1723-4 guard the deferred prebuild fire() against a throw

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7

* ADFA-4128: manual-QA script fixes from the 22-test device walk

The script has had one commit and predates several behaviour changes, so a
walker following it literally hits steps that cannot reach their stated end
state. None of these are product defects - the walk found no product failure.

- T7 criteria 4/5 described the pre-b6cddf035 world where rebaseline left the
  app un-relaunched and a tap was needed. Rebaseline relaunches now.
- T7b step 2 and T11 step 3 end at a modal OS install prompt that never times
  out; "do not tap anything" could not reach the end state.
- T1 gains a FAB baseline tap. Five later tests assert through the FAB, so a
  dead FAB failed them all with no way to tell when it broke.
- T1 gains a note that project creation already ran a setup build, so the first
  tap measures warm provisioning, not cold.
- T14's "no reinstall unless the bytes changed" inverted the design: the
  generation stamp lives in the APK, so a restart always mints new bytes.
- T20 names service-app; only 4 of 30 corpus apps declare a Service.
- T21 drops "wrap and push sora-editor-full first" - already wrapped, with all
  288 source files.

Adds a "Traps that make the product look broken" section for the four method
errors that produced wrong findings: tapping the geometric centre of a view
that extends under a system bar, Find-in-file being a regex search, relaunching
CoGo via monkey when it declares two LAUNCHER activities, and selecting a
wrapped corpus copy by mtime when the newest is pinned to AGP 9.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7

* ADFA-4128: doc corrections from the PR review sweep

Six places where a Quick Build doc contradicted the code it describes. Each was
re-verified against the source rather than taken from the review comment.

- debugging.md: the deploy round-trip row said the 15 s bound covers "one AIDL
  onPayload call". IQuickBuildTarget is a oneway interface, so that call returns
  immediately; DeployChannel wraps the call plus the wait for a
  generation-matched report, which is what its own KDoc already said.
- why-not-android-jar.md: listed native libs as hot-loadable. A .so under
  jniLibs forces a Gradle fallback (ChangeClassifier). Loadable at runtime and
  changeable via live reload are different properties.
- reliability-gaps.md: "five user-facing defects" against three fixed and four
  open. Seven were surfaced; the fixed three are relink-stuck, #88 and #90. Also
  states why Blocks v1? reads TBD - the decision at the top is a proposal, and
  the cells become "No" when it is confirmed.
- low-spec-devices.md: stated an inferred mechanism (SerialGC thrashing in a
  small heap) as the confirmed cause of the 1.9 GB failure. The outcome is
  measured; the mechanism is not, and the uncapped run that would confirm it is
  still unmeasured. Retitled to what was actually observed.
- concurrency.md: the tap-races-its-own-save section read as current behaviour.
  It describes the pre-2026-08-13 design that the redesign below it replaced.
- perf-roadmap.md: incomplete sentence.

Not applied: CodeRabbit's finding that manual-qa.md's screenrecord
--time-limit 1740 is invalid because AOSP caps at 180 s. False on our hardware -
recordings of 1774 s, 2432 s, 2592 s, 2842 s and 3534 s have all completed on
the A56, and the surrounding comment already documents the real 30-minute cap
that 1740 sits under. Applying it would break working recordings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7

* ADFA-4128: three small UX fixes found by the manual QA walk

None of these is a Quick Build failure - the walk passed every test. They are
places where the product is correct and unhelpful.

A2 - the bolt read identically to a screen reader in READY and ERROR. Every tone
has its own icon shape, so a sighted user can tell them apart; collapsing them
all to "Quick Build" hid that distinction from exactly the user who cannot see
the icon. ERROR, SLOW and RECONNECTING now announce their state. BUILDING and
the standard-build-blocked case already did.

A3 - after an undeliverable build the bar read "built, but could not be
delivered - see Build Output" on every poll, while the sentence naming the fix
("Your app is not running. Tap Quick Build to start it with your changes.") was
only in Build Output. The bar now names the tap when that is the whole problem.
Carried as a typed flag rather than matched on the message text, the same way
proxyAppNotConnected already is, and kept separate from it because they mean
opposite things: appNotRunning is "nobody opened it", proxyAppNotConnected is
"we launched it and it still did not arrive".

A4 - an orphaned proxy app reported CoGo's expected connect() rejection at W on
every attempt of the rebind backoff loop, 14 times in one restart window. The
behaviour is right (it continues standalone); repeating an expected rejection at
W buries the entries around it. Reported once per streak now, cleared by a
successful connect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7

* ADFA-4128: record the v1 blocking decision instead of proposing it

The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1,
then left the table's "Blocks v1?" column reading TBD on all four rows. A
proposal in a title and a TBD in a table say different things to a reader,
and CodeRabbit flagged the pair as an internal inconsistency.

Decision confirmed 2026-08-25: none of the four block v1. The four cells now
read "No - v1.1", the title states the answer rather than asking it, and the
prose no longer describes itself as awaiting confirmation.

No change to any gap's evidence, root cause, or fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7

* ADFA-4128: qb 12 review fixes - two doc corrections held on the trailing branch

F1713-3 (docs/concurrency.md): the thesis said every expensive thing runs
in another process while the table directly below it put the mtime poll
and the install call on Dispatchers.IO inside CoGo. Name the exception.

F1713-8 (docs/manual-qa.md): files are not killed, processes are - and a
teammate follows this runbook literally while holding a half-recorded QA
session. Say screenrecord.

Both patch text that exists only in the four trailing commits, so they
could not ship until those commits had a home.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xsc7AMGBVyEMfrwpZX87iC

* ADFA-4128: two review fixes - a repeated clobber prompt and a hard-coded Cancel

Both found by running the CodeRabbit CLI over this branch ourselves, since the
pull request's 119 files exceed the bot's 100-file cap.

- installApk consumed the tap-time clobber answer before entering the coroutine,
  so the answer left the ViewModel whether or not the install went on to
  dispatch. A rotation during the manifest parse cancels the coroutine, the
  finally block re-arms the install, and the retry then ran with no tap-time
  answer - asking the user to confirm the same overwrite a second time. The
  method's own KDoc says the re-check is silent unless the answer moved, which
  is precisely what this broke. The consume now sits inside the coroutine after
  the destroyed check, on the path that actually dispatches; there is no
  suspension point between it and the dispatch.

  Verified with a throwaway harness rather than a committed test: it drove the
  real BuildViewModel and installTimeClobberConfirmation through both orderings
  with a real cancellation, showed the old ordering re-asking and the new one
  silent, and was watched going red under a mutated expectation. It is not
  committed, because ProjectHandlerActivity is abstract and untested by any of
  the 64 JVM test files in app/src/test, so a committed version would model the
  ordering rather than read it and would stay green if the line moved back. The
  ordering is guarded by review only.

- QuickBuildScreen.declineClobberConfirm matched a hard-coded English "cancel"
  while its sibling acceptClobberConfirm resolved its label from resources, so
  the decline path alone broke on a non-English device. Both confirm paths reach
  one builder, which sets android.R.string.cancel, so the framework string is
  the right resource. Compiles; runtime behaviour on a non-English locale is
  unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xsc7AMGBVyEMfrwpZX87iC

* ADFA-4128: the CoGo reload-crashed notice carries the same text as the phone banner

Bryan pinned the wording on 2026-08-28 for the phone banner; the CoGo-side
notice for the same event still said "Your app crashed... Fix the crash and
save", which sends the user to fix code that was fine. The state is only ever
set from failReload, so the reload machinery failed, never their code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xsc7AMGBVyEMfrwpZX87iC

* ADFA-4128: resolve ProvisioningFailedUnexpectedly to a user-facing string

qb-08's review fix added QuickBuildMessage.ProvisioningFailedUnexpectedly,
the named case for a provisioning throw with no message of its own. The
exhaustive when in QuickBuildMessages.resolve had no arm for it, so the
restacked qb-11 would not compile.

Maps it to quick_build_provisioning_failed_unexpectedly, worded like the
neighbouring quick_build_setup_failed, and pins the mapping in
QuickBuildMessagesTest alongside the other valueless cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STCsdMzx9daNBcqMN424Ci

* ADFA-4128: reliability-gaps #89 is fixed in-stack, not open

Entry #89 (failed daemon respawn strands the session) was written against
the prototype and went stale: the missing QuickBuildTapped arm in
reduceDegraded landed with qb-08's review fixes, a failed respawn now
dispatches DaemonRestartFailed and surfaces a message, and qb-07's
trim-memory redesign no longer bumps the daemon epoch. Move #89 from the
open list to fixed-on-this-branch, update the counts and decision line,
and state what remains: no device repro on either side of the fix, so
the recovery arms are host-tested only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwVV7PzMYinSiq6FwC83Vw

* ADFA-4128: 0902 review round on the app wiring

Akash's 2 September round on the IDE-side wiring, plus the seven findings from
08-31 that were still open at head.

- The long-press dropdown's Quick Build row presents the same state as the button
  it hangs off: the toolbar's own prepare() already decided whether a build can
  start and what a tap does, and the row ignored both - so it offered "Quick
  Build" while the button was a stop button, and a tap there cancelled the build
  the user was waiting on.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916777814
- The plugin save path gates generateSources on resourceXmlSaved and routes it
  through GenerateSourcesDeferral, the same as the two UI save paths. It was the
  sibling the narrowing missed, so a plugin's save still ran a full
  generateSources on every xml and raced a live session's build.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916777825
- MODE_STANDARD says what it measures: the build only, because the install dialog
  an unattended run cannot answer is suppressed. Quick Build's arm measures build,
  deploy and reload, so the two are not like for like from in-app numbers alone.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916777832
- A landed build is announced once, not on every re-subscribe. The status is a
  StateFlow and the editor re-collects on every return to it, so the first
  emission was re-announcing "reloaded in 2.0s" over whatever the bar held,
  minutes after the build. Pinned by a test that fails without the guard.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916762978
- The status text takes three lines instead of one, and the app-not-running line
  is short enough to read: at 92 characters it was cut mid-sentence on a 360dp
  phone at the DEFAULT font scale, and the half that was cut was the remedy.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916763766
- The install's clobber dialog is awaited, so the coroutine is alive while it is
  up and an activity destroyed under it re-arms AwaitingInstall. dispatched moves
  after the decision. Moving it into the dialog callbacks as suggested does not
  work: they run long after the coroutine body returns, so the re-arm fires
  mid-dialog and loops - the re-armed AwaitingInstall shows a second dialog
  behind the first, and a decline shows a third.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916762606
- Only Needed tears the session down. NeededForUnknownAppId means the APK's own
  package did not parse, and a transient read cost the user their warm session.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916763243
- A Quick Build tap with no activity refuses rather than building: it needs one
  to flush the editor buffers and to ask about a clobber, and no caller reaches
  it today.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916763508
- Three unused imports, deleted by hand - ktlint's detection is substring-based,
  which is why a green spotlessCheck did not catch them.
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916763971
  https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3916764225
- manual-qa gains a font-scale block (T22): the 1.0/2.0 pass over the toolbar,
  the dropdown, every actionable status line and the clobber dialog. It is the
  check that would have caught the truncation above.

Two repairs the round did not ask for, found running the suite: :app's unit
tests did not compile at head (SessionRestartAndReprovisionRequested is a data
class and was referenced without its parentheses), and once they did,
FeatureFlagsTest failed five ways - it reflects on a `by lazy` property's field,
which is named downloadsDir$delegate and holds the Lazy. Both are one-liners and
:app:testV8DebugUnitTest is green.

The clobber gate follows the PR below it: QuickBuildClobberCheck's two reads
became suspend there, so quickBuildClobberConfirmation takes a suspend probe and
the two ensure*ClobberConfirmed gates own the coroutine rather than pushing it
onto their click-handler callers. And the unused-import sweep keeps
models.Position: stage's deep-link work landed a use for it while this branch was
out.

Not fixed here: requestDowngrade's unwired parameter, the bench re-open against a
stale project model, and the run-on GPL header - all deferred with reasons in
this round's replies. The PR description's own MODE_STANDARD_E2E claim and the
BenchQuickBuildMetricsSink field name are description work, drafted there too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: stop a re-subscribe re-stomping the bar with the start-failed line

Answers review threads 3926567255 and, for the ProjectHandlerActivity half,
3926567267.

3926567255 is confirmed. previousStatus lived inside the launch{} that
repeatOnLifecycle(STARTED) restarts, so it reset to null on every onStart
while status is a replaying StateFlow. Quick Build fails to start, the user
runs Standard Run successfully, backgrounds CoGo and returns - and the
replayed Hidden(lastStartFailed) writes "Quick Build: could not start" over
the standard build's own result line.

The suggested remedy - onlyIfOwned = true on this Show - is not taken,
because it regresses `a failed start still shows after an activity
recreation`. ownsQuickBuildStatus is an activity field, so after a genuine
recreation nothing owns the bar and a gated Show would be dropped; that test
pins the correct behaviour and stays as it is.

Fixed at the caller instead, which is where the defect actually is: the
previous status moves into QuickBuildStatusTracker, held by the activity, so
a re-subscription is no longer read as a first emission (previous == current
short-circuits to None) while an activity recreation still gets a fresh
tracker and writes the line. This closes the same class of stomp for every
first-emission-writing transition, not only StartFailed.

Not closed by this commit: the session manager is a Koin process singleton,
so if a NEW activity opens the next project while the sticky flag is still
set, that fresh tracker will show the line over "Project initialized". That
half needs the flag cleared at session scope and is not attempted here.

3926567267, doc half: quickBuildSessionManager()'s KDoc claimed resolving the
Koin singleton is cheap and that nothing spawns until the first quick build.
Both sentences were wrong. observeStates() resolves it from onCreate on the
main thread (as do onTrimMemory, onBuildServiceConnected and
QuickBuildAction.prepare()); the history store's constructor reads shared
preferences and the default scratch reads noBackupFilesDir, so it does
main-thread disk I/O; and the manager's init block installs the daemon death
listener plus FIVE coroutines, not the three the review counted. The KDoc now
says that, and names what genuinely still waits for the first tap: the daemon
process and the host service binding. The off-main warm-up the review asks
for is deferred, not done here.

Test: `a re-subscribe does not re-announce a failed start` in
QuickBuildStatusBarTest. Verified to fail without the fix by making
QuickBuildStatusTracker.record a no-op, which is exactly the pre-fix
collector-scoped behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: correct the Koin module's "nothing spawns" claim

Continues review thread 3926567267. The module's KDoc said everything is a
lazy singleton and nothing spawns until the first lightning-bolt tap resolves
the session manager. The lazy half is true of the daemon process and the host
service binding only: resolving the session manager itself reads shared
preferences and noBackupFilesDir and installs the daemon death listener plus
five coroutines on the session executor, and ProjectHandlerActivity resolves
it from onCreate on the main thread whenever experiments are on. Say that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: the status line cannot be read by swiping the sheet up

Answers review thread 3926567718, the half that is a false claim in our own
documentation. Both the layout comment and manual-qa.md T22 told the reader
that a status line too long for the collapsed header is read by swiping the
sheet up. It is not: EditorBottomSheet.onSlide sets the header container's
height to (collapsedHeight + padding) * (1 - sheetOffset), so expanding the
sheet drives the header to zero and the line disappears. No other surface
shows this text, so anything that overflows the fixed 100dp header is lost.

T22 now says to record an overflow as a failure rather than swiping to check,
and says plainly that whether the bar actually overflows at font scale 2.0 is
unmeasured - nobody has run that step on a device, so the arithmetic (three
lines of BodyMedium at 2.0 plus the 11sp hint, against 100dp) is an
expectation, not a result.

The layout remedy the review asks for - letting the header wrap its content
with 100dp as a minHeight - is not in this commit: it changes a shared
container that every build status shares, and it wants a device run to
confirm.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: style: spotless reformat of QuickBuildStatusBar, no functional change

spotlessApply's own output, landed standalone as the repo's code-style section
prescribes. ktlint converts quickBuildStatusBarUpdate's block body with a
single return to an expression body; nothing else changes.

Worth recording because it is not collateral from this round's work: the
violation is in a function these commits do not touch, and it is present at
8f79f47e, this PR's head before any of them. spotlessCheck was already red on
this branch - it is green on the gradle-plugin PR below it because
QuickBuildStatusBar.kt is added here, so the ratchet never saw it there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: delete the requestDowngrade parameter and its plumbing

Closes ADFA-5462. The parameter threaded a downgrade request from the
installer entry point down to a reflective @SystemApi call on
PackageInstaller.SessionParams, but no caller ever passed it: every call site
took the false default, so the reflection, the API-29 guard and the
intent-installer warning branch were unreachable.

It was added for a same-app-id Quick Build restore that would install the real
app over a higher pinned test versionCode. That restore path is not in the
stack, so the plumbing is speculative rather than dormant, and reflection into
a hidden API earns its keep only once something calls it.

Removed: the parameter on installApk, installUsingSession and
createSessionParams, the reflective setRequestDowngrade block, the isAtLeastQ
guard and its now-unused import, the intent-installer cannot-downgrade warning,
and the pass-through on ApkInstallationViewModel.installApk. No behaviour
changes for any existing caller, because none of them set it.

A repo-wide grep over Kotlin, Java, Markdown and XML finds no remaining
reference to requestDowngrade. :app:compileV8DebugKotlin and spotlessCheck are
both green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: give DaemonStartFailed the user-facing string it was missing

The core module gained a DaemonStartFailed message, but the app's resolver
never grew an arm for it, so the app module did not compile: "'when'
expression must be exhaustive. Add the 'is DaemonStartFailed' branch".

Adds the string and the arm, and pins the mapping in the resolver test so a
future arm pointed at the wrong resource fails rather than compiling.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: build the Quick Build graph off the main thread

The first resolve of the session manager constructs the whole Quick Build
graph: the history store reads shared preferences and the paths object reads
noBackupFilesDir, so it is disk I/O, and it ran on the main thread at project
open whenever experiments were on.

observeStates is already inside a coroutine, so the resolve moves to
Dispatchers.IO there. It stays inside repeatOnLifecycle, so a resolve that
failed once is still retried on the next return to the editor rather than
cached as null. Every other call site runs after it and finds a built
singleton, so those pay only a map lookup.

Not covered by a JVM test: the change is a dispatcher hop inside an Activity's
onCreate coroutine, and asserting it would amount to asserting withContext
itself. The check that means anything is on-device - open a project with
experiments on under StrictMode's main-thread disk-read detection and confirm
no violation is attributed to the Quick Build graph.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: pin the failed-start tone to the session's lifetime

The review thread asked for the failed-start bar line to be ownership-gated, so
a start failure in one project cannot stomp the next project's line. The gate is
the wrong lever - the ownership flag is an activity field, so gating would also
drop the line after an activity recreation, where it must come back.

What actually separates the two cases is the flag's lifetime, and the reducer
already ends the failed-start story on a teardown: from Idle in the
SessionRestartRequested arm, and from any other state in the top-level teardown
guard. Closing a project sends exactly that event. Nothing was missing in
production code, only the test that says so.

Adds two tests, one per arm. Both were checked against a mutant: removing the
Idle arm fails the first with "expected: Clear / but was: Show(...)", and having
the top-level guard keep its state instead of resting at Idle fails the second.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: size the collapsed sheet header to the status text, not to a fixed 100dp

At font scale 2.0 on the A56 the two longest status lines wrapped to two lines
and clipped at both the top edge of the collapsed sheet and the swipe hint below
them, and the hint truncated with no ellipsis in every state. Swiping up does not
help: onSlide scales the header to zero, so a line that does not fit is lost.

The collapsed height is now the larger of the dimen and what the status block
measures, so ordinary text keeps the familiar height and larger text gets the
room it needs. The block is measured against an unbounded height on its own,
because the header's height is set explicitly for the slide and so cannot wrap.
The re-measure runs after a status change and only applies while the sheet is
collapsed; mid-slide the height belongs to onSlide, which reads the same value.

The swipe hint gets ellipsize=end with maxLines=1. It is the one disposable
string here - it names a gesture rather than a remedy.

No JVM test is possible for this: it is view measurement, and the app module has
no Robolectric surface for the bottom sheet. The check that means anything is the
manual-QA font-scale block, at 1.0 and 2.0 on a device, which this pass did not
run. The doc is updated to say the fix itself is unverified there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: give the swipe hint room inside the collapsed sheet

Two defects hid the hint. 978ff9b21 measured the build-status block against an
UNSPECIFIED height spec, which ConstraintLayout does not support: it answered
146 px for a block whose own children reached 262 px, and that stale figure
became the block's laid-out height, clipping the hint away. Measure against a
bounded AT_MOST spec instead, and ask for a real layout pass afterwards, since
the manual measure ran outside one. Second, and older than that commit: the
sheet carries the status bar's height as top padding for its expanded state, so
a 100dp header did not fit in a 100dp peek and its bottom 108 px fell below the
window. The peek now covers that chrome as well as the header.

Measured on the A56 at font scale 1.0, 2.0 and 3.0 in the ready, live-reloaded
and BUILD FAILED states: the hint renders at 36, 77 and 115 px, and no content
bound passes y=2205, where the navigation bar starts. At 3.0 the status block
measures above the dimen floor, so the header and peek grow with it - the
growth path 978ff9b21 intended, which the bad measure had made unreachable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: keep the collapsed sheet at its earlier height

Drops the peek-height half of aae4bc519 by decision: the sheet's collapsed
peek is the header height alone again, so the sheet sits where it did
before the hint fix. The measure fix (AT_MOST instead of UNSPECIFIED) stays,
so the status block is laid out at its real height; the hint can still clip
where the sheet's status-bar padding pushes the header bottom below the
window. The QA doc says so and asks the tester to record hint visibility.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o3nPrBbGi2XYkMUGavG2A

* ADFA-4128: request a header layout only when the status height changed

refreshCollapsedHeight called header.requestLayout() before its unchanged-height
early-out, and setStatus posts it after every write - including every Gradle
progress line, for every user, with the Experiments flag off. That was an
off-pass measure plus a forced layout per task event during a standard build.
The request now sits below the early-out, where it runs only when the measured
height differs; the text change itself is already laid out by setText.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3934054531

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: a manifest save runs generateSources again

resourceXmlSaved was modified && isXml && isAndroidResource(), and
ProjectManagerImpl.isAndroidResource matches only a module's resource
directories. AndroidManifest.xml sits outside all of them, so a manifest-only
save no longer refreshed the generated Manifest class or the merged manifest -
for every user, flag off - where the old xmlSaved gate did. The flag fold now
sets it for AndroidManifest.xml by name, without paying for the resource lookup.
Both call-site comments and the SaveResult doc drop the "known trade" wording,
and manual-qa.md gets T23 for the manifest walk.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3934054548

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: spell the product "Code on the Go" in the unknown-app dialog

quick_build_switch_unknown_app_message said "Code On The Go" while
quick_build_reload_crashed four lines below, app_name and every other
user-facing string in this file say "Code on the Go" (44 occurrences; the
capitalised form survives only in three legacy strings). Both feed Crowdin, so
the mismatch would have shipped into every locale. The new string follows the
file, not the docs' spelling.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3934054610

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: hand status-line ownership to every writer, the debugger included

ownsQuickBuildStatus was cleared only in setStatus, but the debugger writes its
"Debugger starting" / "started" / "starting failed" lines through doSetStatus
directly (BaseEditorActivity). With Quick Build still owning the bar, a passive
refresh then overwrote the debugger's line and a session end blanked it. The
clear moves into a doSetStatus override, so ownership tracks whoever writes the
bar rather than whoever writes it through one overload. setStatus is now a
plain forward.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3936706719

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: arm the bench standard-build latch only once the build holds the slot

fireAutostartStandardBuild stamped standardBuildStarted and then called
runQuickBuild, which returns without publishing a state when a build is already
in progress. The stamp then stayed armed until the user's next build reached a
terminal state, and standardBuildEnded read that build as the autostarted one:
install suppressed, no message. The stamp now runs in runQuickBuild's
beforeBuild, which only executes after the slot was claimed. Debug source set
and bench flag only; no release path changes.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3936706890

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: never fire a bench autostart against another project's workspace

A bench open for project B while project A was initialized set
ProjectManagerImpl.projectPath = B and started the single-top editor; its
onNewIntent (run before EditorHandlerActivity's switch logic) claimed the latch
against the already-moved projectPath and tapped Quick Build with A's module
model still in the workspace. Two guards: the editor claims only when the
intent's project is the one its workspace was synced for, and the bench
trampoline refuses a different-project open while one is initialized - the
close-and-reopen path needs a dialog an unattended run cannot answer, and the
harness force-stops between projects anyway. Debug source set and bench flag
only.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3934056407

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: build the Quick Build graph once, off the main thread, and read it everywhere else

observeStates warmed the Koin graph on Dispatchers.IO, but the main-thread callers
(the toolbar action, the bench autostart, the stagger) resolved the same singleton
themselves through GlobalContext, so whichever ran first paid the graph build on the
main thread and the warm-up was ordering-dependent. QuickBuildGraphWarmUp owns the
one resolve: warmUp() builds off-main, sessionManagerOrNull is a plain read that
never resolves, and the two callers that must not miss the manager await() it.

Review thread: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3934056178

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: never wipe the daemon directory under a live compile daemon

Every provision, prebuild and rebaseline re-extracted the 62 MB daemon zip after
deleting daemon/. A rebaseline runs while the compile daemon is alive and loading
jars from that directory lazily, so the wipe could turn an unopened jar into a
NoClassDefFoundError inside the daemon. The extraction is now skipped when a stamp
keyed on the installed APK's versionCode and lastUpdateTime matches and the daemon
jar is present; the stamp is written last so a crash mid-extract re-stages. An APK
update force-stops the app and its children, so the one path that does wipe never
runs under a daemon.

Review thread: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3936707073

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: cover the analytics sink's fallbacks, route and outcome names

The sink's branches for a completion with no matching start, a success without a
start, the outcome and route metric names, an invalidation reason and a failed proxy
rebuild had no test. Each case now pins the emitted field values.

Review thread: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3934054575

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: adopt the suspend contracts and surface compile diagnostics in the app

GenerationTracker is now opened through its suspend GenerationTracker.open, so
the provisioner's baseline allocator becomes a suspend lambda; its one call
site already runs inside the suspend proxy app build.

A build that landed with warnings now lists them in the Build Output under the
"reloaded to generation" line, indented like a failed build's errors, from the
diagnostics the orchestration branch carries on QuickBuildStatus.UpToDate. A
warning the Gradle build would print no longer disappears because the quick
build succeeded.

Review threads:
https://github.com/appdevforall/CodeOnTheGo/pull/1719#discussion_r3934045941
https://github.com/appdevforall/CodeOnTheGo/pull/1719#discussion_r3934045924

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: lay the collapsed header out inside the window so the swipe hint survives 2x fonts

The sheet carries the status bar's height as top padding, but its collapsed peek is
the header height alone (2748c9ae3), so the bottom paddingTop + divider px of the
header (48 on the A06, 108 on the A56) hang below the window. The status block
filled the whole header and its spread chain put the hint last: at font scale 1.0
the chain's slack matched the hidden strip and the hint ended at the window edge;
at 2.0 the taller text shrank the gaps and the hint's lower 18 px went under the
navigation bar (device pass 0904, row C23).

The header now carries that strip as bottom padding, so the block is laid out in
the part that is on screen, and the collapsed height adds the strip back only when
the block measures more than the floor leaves visible. The block is included with
match_parent so it fills the header's content box, and its minHeight goes: the
floor lives in code. The padding is re-applied on the sheet's layout changes,
since the status-bar inset can land after setOffsetAnchor first padded the header
(and that call no longer accumulates paddingBottom on a second anchor).

Measured on the A06 (300dpi, font scale 1.0 and 2.0, idle and through a Gradle
sync): the header stays [1370,1510]; the hint renders in full at [1457,1482] and
[1448,1499], above the navigation bar at 1510. The sheet's on-screen height is
unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: tidy the stager KDoc and the no-op autostart branch

The QuickBuildArtifactStager class doc said a rebaseline provisions while the
compile daemon is running and that the stamp keeps that safe. The rebaseline
shuts the daemon down before its Gradle build, and no path stages under a live
daemon, so the doc now says what the stamp does: skip a 62 MB re-extraction
unless the APK changed, and keep the one wipe path behind an APK update that
force-stopped the app. The AutostartBuild.NONE arm loses its explicit Unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkGof8cLt23LkxZ8MKzin2

* ADFA-4128: keep the deferral's own generateSources build out of the hand-back

The hand-back marks a live session's baseline untrusted after every finished
Gradle build, and the resource-save generateSources build the deferral parks
is one of them - so the session paid a full recompile for a build that
regenerates nothing it compiles against (the daemon's classpath is the payload
jars the proxy app build diverted at provisioning, not the intermediates
R.jar). The deferral now remembers a dispatched build and the hand-back claims
that build's completion once; the next build to finish after a dispatch is the
deferral's, since the tooling server runs one build at a time and
generateSources refuses while another is in progress.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660753

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: size the header for the status block only while it is the child on show

The measured status height went onto header_container, a ViewFlipper, so a
three-line status line at 2x font scale inflated the symbol-input and
install-progress rows too. collapsedHeight now counts the status block only
while the flipper shows it, and showChild re-applies the height so the extra
rows leave with the status and come back with it. onSoftInputChanged routes
through showChild for the same reason.

Not device-verified: a JVM test covers the height rule; the keyboard-up and
install-progress flips still need a device at font scale 1.0 and 2.0.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660756

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: let an unknown-id consent at tap time cover the id the install resolves

NeededForUnknownAppId never equals Needed(id), so a Run whose applicationId
had not resolved at tap time asked at the tap and again at install once the
APK named the package. The tap-time answer already consented to replacing
whatever sits under this app's id; the install resolving that id is the same
answer with the name filled in, so it is no longer asked twice. The other
direction (a named tap, an unresolved install) still asks.

Review: https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660761

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: await the clobber confirmations on the tap path

Both tap-time clobber gates ran the callback-shaped dialog from inside a suspend function on lifecycleScope, so their cleanup ran before the user answered; they now await the dialog like the install path does, which also takes the dialog down when the activity goes. The callback form had no callers left and is gone. The accessor KDoc names onPause and onResume, where restartSession, the narrator reset and onHostForegrounded actually live.

Answers:
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660765
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660739

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: post one Quick Build save event per save operation

onFileSaved moved out of the per-editor write into the three save entry points, so a save-all posts one event instead of N identical ones. The dropdown KDoc says what the Help item does without a documentation row: it opens the no-tooltip fallback.

Answers:
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660746
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660738

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: abort a Run whose pre-build save found the editor gone

beforeBuild skipped the save-all for a destroyed activity and let the build go ahead on stale disk content; it now throws, which BuildViewModel's catch turns into an Error state.

Answers:
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660769

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: do not count a Quick Build stop tap as a feature use

The analytics call ran before the BUILDING cancel branch, so a stop was tracked as a use.

Answers:
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660733

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: ASCII ellipsis in the truncated Gradle cause

Three dots on a three-character budget, with the test asserting the same.

Answers:
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660740

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: pass the rejection to the repeated-connect debug log

RuntimeLog.d(String, Throwable) instead of string concatenation, matching the warn two lines below.

Answers:
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660749

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: carry the reinstalled app's uid on a rebuild success

The Gradle provisioner fills the proxyAppUid the rebuild outcome now carries, from the install verdict.

Answers:
https://github.com/appdevforall/CodeOnTheGo/pull/1720#discussion_r3951660396

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: manual QA case for Run during the eager proxy-app build

T24 in Block B covers the refusal window: the msg_build_slot_busy flash and no second build.

Answers:
https://github.com/appdevforall/CodeOnTheGo/pull/1723#discussion_r3951660729

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6rFuAJin4QFFwwzUTFZ1K

* ADFA-4128: Run Tasks must not inherit a Run tap's clobber answer

runTasks (stage #1803) claims the build slot but never resets clobberAnswerAtTap, so a Run whose clobber check was already answered, followed by a Run Tasks install of the same app, could skip the confirmation that install owes. Reset it when Run Tasks claims the slot; Run Tasks never asks, so the install must.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: carry a skipped relaunch as an absent relaunchOk through the app sinks

Follows the core change that made relaunchOk nullable: the analytics
metric omits relaunch_ok when no relaunch was attempted, and the bench
JSON line omits relaunchOk, so a skipped relaunch no longer reads as a
failed one.

Tests: AnalyticsQuickBuildMetricsSinkTest (expected: null / but was:
false) and BenchQuickBuildMetricsSinkTest (expected to be false) both
watched red with the null coalesced to false.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: route a created or deleted resource file through the generate-sources deferral

ProjectManagerImpl.onFileCreated and onFileDeleted called generateSources
directly, bypassing the deferral every save call site goes through. With
a live session the undeferred build landed mid-pipeline, nothing had
armed the deferral's claim, and the hand-back marked the baseline
untrusted, so the next save recompiled everything.

The project manager cannot see the deferral (it lives in the app), so it
gains a resourceChangeBuild seam that defaults to the direct call; the
Quick Build module points it at GenerateSourcesDeferral.notifyResourceSaved
when the session manager is created.

Test: ProjectManagerImplResourceChangeTest, watched red with the direct
call restored (expected: 1 / but was: 0).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: only grow the collapsed sheet while a build narrates

setStatus re-measures the collapsed header on every write, and a Gradle
build writes one task name per TaskStartEvent. At a 2x font scale those
alternate between one and three rows, so the peek height jumped on every
task for the whole build. While a user build is in progress the header
now keeps the tallest height it has needed, and re-fits the final status
line once the build ends.

Test: CollapsedHeaderHeightTest (statusHeightAfterMeasure), watched red
with the ratchet removed (expected:<222.0> but was:<108.0>). Not
device-verified.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: gate the Restart session row like the button

While a standard Gradle build holds the slot, a restart tore the warm
session down and the reprovision was then refused as slot-busy. The
dropdown row is disabled under the same QuickBuildAction gate that greys
the button. No JVM test: the popup is built inside the activity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: hand the outer internal build its progress listener back

withInternalBuild kept the listener in a single field and nulled it in
its finally, while the bracket around it counts depth. A nested internal
build therefore blinded the outer one for the rest of its run. The
listener now lives on the bracket, saved on entry and restored on exit.
Unreachable today (one caller, behind its own isBuildInProgress check).

Test: InternalBuildBracketTest "a nested hold hands the outer build its
progress listener back", watched red with the null restored (expected
specific instance: ...outer / but was: null).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: read the saved file, not the editor's, for the resource flag

The post-save resource check re-read frag.file off the main thread. That
goes through the view binding, which a tab closed after the write has
released, so the flag came back false and a layout save left the Java
LSP with stale R symbols. savedFile was captured on Main for exactly
this. No JVM test: activity code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: reset the narrator after the pane unbinds, and drop what follows

The reset ran in onPause beside restartSession(), before the teardown
narrated anything and while the pane was still bound, so it cleared an
empty queue; the lines that arrived after onDestroy unbound the pane
queued for the next project. The reset now runs from the same lifecycle
observer, after the unbind, when the project is closing - and until the
next pane binds the narrator drops lines instead of queueing them, since
the teardown narrates asynchronously after the reset.

Test: QuickBuildOutputNarratorTest "lines narrated after a reset with no
pane are dropped", watched red with the discard removed (expected to be
empty / but was: [Quick Build: :app:mergeV8DebugResources]).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: an empty slot at tap time is not widened by an unparseable APK

With no proxy app installed the tap settled NotNeeded. When the built
APK then failed to parse the install-time check came back
NeededForUnknownAppId and the unknown-occupant dialog was shown after a
successful build; a decline dropped the install with no message.
NotNeeded at the tap now covers an unknown id at install time.

Test: QuickBuildClobberConfirmationTest "an empty slot at tap time is not
widened by an APK that fails to parse", watched red with the branch
removed (expected: NotNeeded / but was: NeededForUnknownAppId).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: run the prebuild fire inside the stagger's own catch

The activity's fire body only launched a coroutine, so the try/catch the
stagger wrapped around fire() saw nothing and the await/onProjectSynced
work ran as a bare child of the editor scope - a plain Job with no
handler, where a throw cancels the scope and reaches the uncaught
handler. fire is now a suspend function the stagger launches on its
scope, on both arms, inside the catch.

Test: QuickBuildPrebuildStaggerTest "an immediate prebuild that throws
does not take the scope down with it", watched red with the guard
removed from the immediate arm (IllegalStateException: onProjectSynced
blew up).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: use the intent-extra constant in the bench activity

The one call site still writing the "PROJECT_PATH" literal is the one
the same-project autostart guard reads back through
EditorIntentExtras.EXTRA_PROJECT_PATH.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: count a Quick Build feature use only when a build starts

trackFeatureUsed sat above the save-all and the clobber confirmation, so
a failed save and a declined clobber both emitted a quick_build use with
no build behind them. It now runs next to onQuickBuildTapped. No JVM
test: the action needs an activity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: say in GradleBuildTuner's KDoc that it is Experiments-only

The class doc claimed the tuner applies to every Gradle build and was
deliberately not gated behind the Experiments flag. Its only caller,
GradleBuildService.prepareBuild, calls it inside
`if (FeatureFlags.isExperimentsEnabled)` and otherwise passes Gradle only
the IDE's extra arguments - so with the switch off Gradle runs on its own
defaults and neither the Metaspace floor nor the daemon idle timeouts
apply. Row 20 of the review round chose to fix the sentence rather than
the gate; this is that sentence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: gate the won't-stay-up dialog's Restart like the button

The dialog offers Restart as the one action that replaces a proxy app
which will not stay open, but during a standard Gradle build that restart
tears the warm session down and the reprovision behind it is refused as
slot-busy - the user loses the session and is told to wait. Same
QuickBuildAction.isBlockedByStandardBuild() gate the toolbar button and
the Restart session row (d28cf1e6c) already use; Dismiss stays live and
the notice is raised again if the streak continues.

No JVM test: the dialog is built inside the activity, as with d28cf1e6c.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: hold the narrator's discard until the old teardown is quiet

Closing a project cleared the narrator's queue and set discardUntilBound,
which bind() cleared. The next project's activity binds its Build Output
pane in onCreate, while the closing session's Gradle cancel is still
fire-and-forget (QuickBuildSessionManager.teardown ->
GradleQuickBuildProvisioner.cancelProxyAppBuild) and the build's progress
listener keeps firing - so late "Quick Build:" lines from the dying
project were written into the new project's pane, attributed to a build it
never ran.

reset() now takes an awaitable for the closing session's teardown, and
lines are dropped until that returns whether or not a pane has bound in
the meantime. QuickBuildSessionManager.awaitTeardown() exposes the
existing teardownWork join; it hops onto the session dispatcher, so called
after restartSession() it lands behind that teardown rather than reading
the previous one. The wait is capped at 30s so a teardown that never
reports quiet cannot silence the pane for the rest of the process, and the
call is off the UI thread on the narrator's own scope.

The new project's own narration is dropped inside that window too. The
window ends at the daemon shutdown plus the scratch-tree removal, and the
new project's eager prebuild is staggered well past it, so in practice
nothing of the new session lands in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: expect the tap's RecordAsk in the prebuild stagger's reducer tests

Restack fallout from the PendingAsk work on the core branch (0e8d15a78):
a user-initiated QuickBuildTapped now carries SessionEffect.RecordAsk
alongside whatever it starts. Both tests here assert a reducer transition
directly, so they saw the extra effect. Their subject is unchanged - one
pins that a tap in the stagger window provisions in the same transition
rather than queueing, the other that a tap mid-prebuild starts no build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153YfwDnqn7ktHcVgXSNNe8

* ADFA-4128: scope the narrator's teardown drop to the build that closed

The drop reset() arms was global: every line was dropped until the closing
project's teardown reported quiet, or for 30 s. But the next project's activity
binds its pane in onCreate, so a Quick Build tapped in the project the user just
opened narrated "running the initial full build", its Gradle task lines and its
timings into nothing - for as long as a daemon shutdown and a scratch-tree
removal take.

The leak that drop exists to stop is a specific one: the closing project's proxy
app build is cancelled fire-and-forget, so its Gradle progress listener keeps
firing - and its failure is quoted - after the new pane is bound. That is now
decided by naming the build rather than by timing it. A build takes a
ProxyAppBuildNarration before it starts; reset() retires every handle taken
before it, so the closed project's build is silenced for as long as it keeps
talking, while the build the project on screen started narrates throughout.

Status and timing lines carry no build of their own, so they keep the drop - but
it now also ends when a session announces its own full build. That stream is
ordered, so everything the closed session had left to say arrived before that
announcement, and the project on screen is never muted by it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WMhaGYg4sSCcAzQEdNtLa

* ADFA-4128: decide the won't-stay-up notice's Restart at the tap, not the show

The button's enabled state was sampled once, at show(). The dialog outlives that
reading, so the gate was wrong in both directions: raised during a standard
Gradle build it kept a dead Restart for the whole life of the dialog - the build
finishing never re-enabled it - and raised before one it kept a live Restart that
tore the warm session down into a reprovision the build then refused as
slot-busy. A device pass on 2026-09-10 hit the second case 5 times out of 5.

Worse, a disabled dialog button says nothing about why. The toolbar button at
least carries "Standard build in progress" as its content description; this had
no such channel, while the message body went on promising restart as the remedy.

So the gate is read on every tap. A tap that cannot act flashes the same
"Standard build in progress" string and leaves the notice up, so the user can
tap it again when the build ends - the only other route back to it is another
Quick Build failure streak. The rule lives in QuickBuildWontStayUpRestart, which
is JVM-testable; the activity keeps the wiring, and sets the listener on the
button rather than through setPositiveButton, since Material dismisses before
the builder's listener runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WMhaGYg4sSCcAzQEdNtLa

* ADFA-4128: cut the Material alert dialog's vertical insets to 24dp

Material's alertDialogStyle defaults backgroundInsetTop and
backgroundInsetBottom to 80dp each. On the A56 (1080x2340, density 450)
that is 450px of the 2340 spent on margin, leaving the panel 1654px for
content that needs about 1720px at 2x font scale -- so the message and
the "Not now" button are clipped.

24dp each returns 224px to the panel, which clears the shortfall with
room left. Verified in the built APK with aapt2: the style resolves and
alertDialogStyle points at it.

WIP: kept as its own commit so the stack can be restacked without
carrying an uncommitted worktree. Still open with Bryan: whether this
lands app-wide or stays contained to the dialogs that clip (39 of 57
alert dialogs inherit this theme).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZEZKHy8dkDGJM3nUut6hJ

* ADFA-4128: give :app a JaCoCo report task, pointed at the ASM-transformed classes

:app registered no JaCoCo report task, so every coverage number for this
module has come from a one-off invocation that left nothing behind to re-run.
The obvious way to write one is also wrong here, and wrong silently: :app runs
transformV8DebugClassesWithAsm, so a report pointed at tmp/kotlin-classes --
correct for the quickbuild modules, which run no transform -- makes 138 classes
report "Execution data ... do…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants