State restoration and continuity across devices - #5663
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: def44c429d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Compared 12 screenshots: 12 matched. |
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9431117ba
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Cloudflare Preview
|
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 166 screenshots: 166 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 166 screenshots: 166 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 166 screenshots: 166 matched. |
|
Compared 166 screenshots: 166 matched. |
|
Compared 166 screenshots: 166 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 181 screenshots: 181 matched. |
|
Compared 160 screenshots: 160 matched. Benchmark Results
Detailed Performance Metrics
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 85e4b48ba4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2c5a39af91
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ecf68fc364
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 646d895af1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f2b72c7e5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e121fdcef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1185b5b3f2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5eaf1db562
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 34598c5aa2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Compared 143 screenshots: 143 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
|
Compared 148 screenshots: 148 matched. Benchmark Results
Detailed Performance Metrics
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f730882180
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 33e4b0d530
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dca8d2d773
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Compared 149 screenshots: 149 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
…able origin ContinuityCallback lets a port call from any thread, and the decision reads `enabled` and the application's choice, which the event thread owns. Reading them from elsewhere was argued safe in one direction -- a decline is recoverable, because the port retains the activity and offers it again -- and that argument died the moment the "off" answer became a CLAIM, two rounds ago and by my hand: a claim drops the activity, so a stale read there loses an arrival outright rather than delaying it. Marshalled rather than published. This framework is single threaded on the event thread; the answer to reading its state from elsewhere is to stop, not to put memory barriers around fields that have one owner. Claimed on the way out, because that is then the truth: the framework has taken the activity and will deal with it, and nothing else answers to this application's own activity type, which the check above has already established. Every port shipped here already marshals -- the iOS one hands over through callSerially, the simulator's hooks are dispatched on the event thread, Android has no continuation callback at all -- so this is the guarantee for a bridge written elsewhere rather than a change to how ours behave. I checked all three before changing anything. The test that comes with it is a REGRESSION GUARD and says so: a memory-visibility fix has nothing a test can expose, and this one passes against the code without it. What it pins is what the marshalling must not break -- a port calling from its own thread is told the activity was taken, and the arrival actually arrives. Separately, an inbound device id longer than a checkpoint can hold is refused, where an oversized title is dropped. The two are not alike: a title is a label a receiving device may show, while the origin id is the key every mark and every dedup decision is made against, and admit() refuses a state without one anyway. Carrying it is what does damage -- commit() writes it through Util.writeUTF, which throws every time, so the arrival is parked, re-applied on every retry and holds every relay publication behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
💡 Codex ReviewCodenameOne/CodenameOne/src/com/codename1/router/Navigation.java Lines 363 to 364 in c09cf58 When the restored form's show callback calls CodenameOne/CodenameOne/src/com/codename1/continuity/Continuity.java Lines 3124 to 3132 in c09cf58 For a conforming custom bridge that invokes this callback from a background thread after ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
ContinuityStateTest stops the tvOS run dead. The console's last line is the support-probe INFO and then nothing -- no stage=ran, no suite finished, and SUITE:FINISHED never appears -- so every test after it alphabetically is reported as "missing actual" and the job fails on DesktopMode and Media360Panorama, which are fine and never got to run. Established before changing anything: - It is this branch. The same tvOS job passed on location-button-android17 with all 144 tests matching. - It is tvOS. The same test runs to completion on the iOS GL job in this very run, where the suite finishes normally. - It is not today's commits. The tvOS comment on this PR reported the same two missing at 13:46 yesterday, so it has been there since the test was added. - It is not a crash the console records, and it is not any of the framework's blocking calls: all three callSeriallyAndWait sites are on worker threads that this test never starts. The one difference the log does show is that isContinuationSupported() answers false on tvOS and true on iOS. That is as far as evidence goes without running tvOS, so this adds phase markers rather than guessing at a fix. The next run says which statement it stops on, and the markers keep earning their place afterwards: this is a device conformance test whose whole job is to report what a platform does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 97f3cb3f3f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Found it. The phase markers pushed one commit ago say the tvOS suite stops in
setStateProvider(), which calls enable(), which mints the device id -- and it
did that through Util.getUUID(). Instantiating Util.UUID runs a static
initialiser that seeds itself from getUniqueDeviceID(), which reaches
CN.getProperty("User-Agent"), and the Apple ports answer that by standing up a
web view.
tvOS has no WebKit. The call never returns, enable() hangs on the event thread,
and the device suite stops dead on the first test that installs a state
provider -- taking every test after it alphabetically with it, which is why the
job failed on DesktopMode and Media360Panorama, two tests that are perfectly
fine and never ran.
The two consoles from the same CI run settle it: iOS logs 1056 com.apple.WebKit
lines, the first burst starting at the exact instant of that test's support-probe
line, and tvOS logs zero.
An id that identifies one installation to the devices it syncs with does not
need a browser's user agent to seed it, and must not need a working web view to
exist. Time plus two draws from a Random seeded independently of it is ample:
this runs once per install and the result is persisted. Formatted through
Integer.toHexString on each half, because Long.toHexString is in neither
vm/JavaAPI nor Ports/CLDC11 -- the same trap that made this branch red two
commits ago.
The test asserts the SHAPE of the id, which is what a test can reach here: the
JavaSE port answers getProperty("User-Agent") out of a table and never builds a
browser, so the hang itself does not reproduce in the harness. Reverting to
Util.getUUID() produces a dashed UUID and fails that assertion, which is what
makes it worth writing.
Note the wider hazard this leaves behind: Util.getUUID() hangs on tvOS for ANY
caller, not just this one. That is not mine to fix in this PR, but it is worth
knowing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b742d14aa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… a decline Three findings I had not seen, two of them from a PR issue comment rather than a review thread -- my monitor only watched threads, which is the gap this exposed. All three are consequences of my own recent changes. restoreStack() rolls its screen back when show() throws, and it did that whenever the display had changed -- which is true of two different things. show() installs the form and only THEN runs its listeners, so a listener that finds the session expired, calls clear(), opens a login form and then throws has already replaced the screen with its own choice. Re-showing the pre-restore form put the signed-out account's screen back in front of the user, which is the one thing that callback ran to prevent. Asking whether `top` is still current separates the two. Continuity.restore() cannot correct this from outside: by the time it runs, the rollback has happened -- which is why the guard I added there last round was not enough on its own. clear() cleared this class's parked slot and left the PORT holding its own. A Handoff that cold-launches a logged-out app reaches IOSContinuityCallbacks before anything has installed a callback and is held there, so a clear() before the first enable() cleared nothing that existed, and the enable() that came with the later login drained the port into the next account. It drains now, through a one-shot confined to that call rather than by recording a choice: clear() is a logout, not "I do not want continuity", and an arrival that comes AFTER it is not from before it and must still be held for the enable() that is coming. The test pins both halves. And the marshalling from last round answered "claimed" unconditionally while the queued decision could decline -- because the application has not said whether it wants continuity. A bridge acts on the synchronous answer: it lets go of an activity the framework never kept, so the enable() following a sync-only listener has nothing to deliver. That is exactly the loss the retention contract exists to prevent, reintroduced by the fix for the visibility problem. The background path now declines when the application has not chosen, and only claims when it has. applicationHasChosen is the one flag that can be read from that thread: it goes false to true once and never back, so a stale read answers false -- a decline, which the port recovers from by offering again. `enabled` has no such property, which is why the decision itself stays marshalled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac35d9a9de
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two of my own fixes were in direct conflict and this is the seam between them. Installing a callback is ALSO how a port is asked to re-offer a continuation it declined earlier and is holding -- our iOS port does exactly that, and it is what recovers a Handoff that cold-launched the app before anything was listening. Making installation strictly once removed that event: a sync-only application installs the seam through SyncedStore.addChangeListener, the arrival is declined and held, and the enable() that came with the login skipped the install and never asked for it again. The cold-launch continuation was lost. The distinction that matters is not how many times setCallback is called but WHY. "Make sure a seam exists" is what every store listener wants and must not grow with their number -- that was the reported harm, and it is unbounded in application code. "And hand me anything you kept" is worth a re-install and happens at three bounded moments where the framework's answer to a held arrival changes: enable(), disable() and clear(). refreshBridge() is the first kind, not the second: it exists for a bridge the port has SWAPPED, and the instance guard is exactly that test. The SPI says so now, because a port cannot honour a contract it is not told about: setCallback replaces the seam, may be called more than once, and a port registering a native observer must register it once and replace the reference. The test that asserted "exactly one callback" is rewritten. That was the wrong invariant and only the retention path showed it: what must hold is that the count does not grow with the number of LISTENERS, which it now checks with three of them, and that enabling does ask for what the port held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 315ccc936b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The same defect I fixed in StateCodec.fromJson, in a second parser I had not
enumerated. An NSUserActivity from an untagged or hand-written sender crosses
the native boundary as JSON and is parsed back to a Map here, and this parser
used the static defaults -- so a raw JSON true reached the listeners and the
provider as the String "true", passed validation because a String is a
representable type, and was acknowledged.
includeNulls goes with it, and that half is not in the report. fromMap() refuses
a null nested in a list -- a property list cannot carry one, and the iOS
sanitiser drops it and shifts every index after it -- but only if it can see it.
With the parser dropping nulls, the list simply arrives one element shorter and
the check I added for exactly this corruption never fires. The two settings are
one configuration and both doors need it.
Enumerated rather than patched where it was reported: there are three JSON
parses in this feature. StateCodec is the reference, this one now matches it,
and the third -- the synced store's {"keys":[...]} in IOSContinuityBridge -- is
correct with the defaults, because our own native side writes it, its elements
are strings and the loop discards anything that is not one. That one is
annotated rather than changed, so the next reader does not have to work it out
again or "fix" it.
Verified by inspection and by compiling the port. There is no unit-test harness
for the iOS port's Java -- maven/ios has no test source root and core-unittests
does not depend on it -- and parse() is private static, so nothing here can
exercise it. Building that harness is worth doing and is not this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a94ff072f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…here Two findings that turn out to be one knot, and both undo something I got wrong. The generation was read too late. Every hop between the activity and the decision is a queue, and a logout already sitting on the event queue runs first, so a generation read after those hops is the one AFTER the logout: every later check passes and the previous account's state is restored and persisted by a session that promised nothing from before it survives -- clear() deliberately leaves continuity enabled, so nothing else refuses it. I bound deliver() to the arrival two rounds ago and missed that the iOS port added a hop of its own in front of it. The port hands over directly now and the framework marshals, which is the right split since the framework started marshalling: the generation is read when the activity actually arrives, and the delegate gets the framework's real answer instead of an unconditional true. That left the synchronous answer. It could not be state-dependent -- `enabled` is not monotonic, and `applicationHasChosen` only LOOKED safe: I argued a stale false meant declining and that a decline is recoverable because the port re-offers, and that is untrue once enable() has installed the seam, because no later install comes. A decline then strands the arrival with a port that was already told to let go, or loses it with one that does not retain. So the framework claims it and keeps it. An arrival that reaches the callback before the application has chosen is parked HERE, and enable() drains that slot -- the retention that used to be borrowed from the port now lives where the state does, and no argument about cross-thread visibility is load-bearing any more. Two tests were rewritten and I checked in each case that the harm they were written for is still prevented, rather than that they now pass. One asserted a decline that the new design deliberately does not make; it asserts instead that the arrival is not lost, which is what it was always about. The other had to move onto a background thread: called from the event thread the decision runs inline, before the queued logout, and deliver()'s older guard covers it -- the first version passed with the new check removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 06f9ba62a1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A port may retain the same continuation its pre-enable callback declined -- ContinuityBridge says so -- and since last commit this class parks a copy too. So both can be holding one, and enable() re-offers the port's while draining ours. Ours went straight to dispatch, past the (origin, sequence) check that admission exists for, so the listeners and the provider ran twice on one arrival: the user is asked to continue the same work, or has it restored, twice. The comment I wrote to justify parking said the two copies dedup at admission. They only do if they both go through it, and the code I wrote next to that comment did not. It calls admit() now. The test drives both holders at once -- a bridge that keeps a declined arrival and a framework that parks the same one -- and counts restoreState calls, which is the observable the user actually feels. It fails with two when the drain bypasses admission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e35d2440c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // A one-shot rather than applicationHasChosen: clear() is a logout, not "I do not want | ||
| // continuity", and it deliberately leaves an enabled framework enabled. Recording a | ||
| // choice here would make every arrival AFTER the clear be dropped instead of held for | ||
| // the enable() that is about to come -- and an arrival after the clear is not from |
There was a problem hiding this comment.
Disable continuity across the logout gap
When an app follows the documented logout flow and calls only Continuity.clear(), this block deliberately leaves enabled true. Unlike the previously reported queued or held arrivals, a continuation that genuinely arrives after clear() returns therefore passes Callback.decide() and admit(), allowing the signed-out account's payload and routes to be restored and persisted over the login screen; a later enable() is also a no-op because the framework never became disabled. Either make the logout operation suspend arrivals or require the documented logout flow to pair clear() with disable().
Useful? React with 👍 / 👎.
| // Read from whatever thread the port called on. An int read from another thread | ||
| // yields a value the event thread wrote at some point and never a future one, so it | ||
| // can be stale-old but never stale-new: the worst it does is refuse an arrival that | ||
| // raced the logout exactly, which is the answer that side of the race wants. | ||
| final int arrivedIn = lifecycle; |
There was a problem hiding this comment.
Safely publish lifecycle generations to callback threads
When a conforming bridge invokes the callback from a long-lived background thread after a clear()/disable() and subsequent enable, this unsynchronized read may continue observing the old lifecycle value because the EDT write has no happens-before relationship with it. The queued decide() then compares that stale generation with the current one and silently discards a valid arrival. Fresh evidence after the earlier visibility finding is that the replacement still reads the same plain field off-EDT and explicitly permits a stale value; make the generation safely published or capture it through synchronized EDT coordination.
Useful? React with 👍 / 👎.
An origin is the key every mark and every dedup decision is made against, so a state without one cannot take part at all -- admit() logs it and drops it. Counting the read as SUCCESSFUL anyway is what does the damage: it clears the unread flag and releases a checkpoint queued during the GET, and that publish overwrites the relay's only copy of remote work nothing here could read. Fixed on the relay's own path rather than in the codec. Requiring an origin in requireKnownTypes was my first attempt and it was wrong: the codec also reads states this device built, so a round trip through toMap started failing for any AppState whose deviceId was never set -- eight wire tests said so. AppState does not require an origin; the relay contract does. Checked in pollFinished, it also covers every StateRelay rather than only the one shipped here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3836707e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A relay answering with a valid object that carries no fields this build knows --
{"error":"temporarily unavailable"} returned with a 2xx is the shape -- came back
as null, and null means "the relay holds nothing" to the code that reads a fetch.
The publisher is released and a local checkpoint overwrites a document this
device never managed to read.
NON-EMPTY is the whole condition, and the distinction is the point. A bare {} is
a plausible way for an endpoint to say it holds nothing, alongside the 404 and
the empty body this class documents, and refusing that would leave such an
endpoint unable to publish anything ever. An object carrying fields none of which
are ours is a different thing: something is there and this build cannot read it.
Thrown from fromJson rather than fromMap, so the platform continuation path is
untouched -- an unrecognised NSUserActivity still comes back as null and is
declined, which is what stops the callback claiming it and prompting the user
over nothing.
An existing test asserted null for that document. Its stated harm is about
FABRICATING a state, and that is preserved and now asserted on the path it
applies to -- fromMap still answers null -- with the relay's reading asserted
beside it.
Also: AppStateWireTest now extends UITestBase, as the other three test classes in
this package already did. Core code logs, and Log.print() reaches
Display.getInstance() the first time it runs, so in a class with no Display the
first test that makes the framework log dies in Util.cleanup(). Which test that
is depends on the order they run in, so the class passed until an edit moved a
different one to the front. It fails in isolation without this change and without
any of mine, which is how I know it was already there rather than something I
introduced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 217be7b114
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ync opt-in routeStackChanged() returns early while a restore is being applied, and it has to: without that the rebuild checkpoints and republishes the state it is applying, and the two devices bounce it back and forth. But the restored form's show callback is application code and may navigate -- a screen that redirects to a newer one, an expired detail page sending the user to a list. Both notifications for that navigation land inside the window and are dropped, so the checkpoint recorded the routes that ARRIVED instead of the ones the user is on, and a process death before the next one restored the screen the application had redirected away from. The reconciliation goes AFTER commit(), and the ordering is the whole of it. commit() clears the pending flag as part of settling the arrival, so asking before it set a flag commit then wiped and the scheduled flush found nothing owed -- the fix looked right and did nothing, which the test caught. It is also after the lifecycle branch, because a callback that ends the session leaves the stack different from what was restored too, and checkpointing there writes for a session that has just ended -- an existing test caught that one. Separately, ios.continuity.sync=true is a DECLARATION and the build ignored it. The hint documents itself as "set true to say so explicitly", and the signing preflight already reads it that way -- it is how a project says it wants the store without that check having to read bytecode. The builder used it only as a veto, so a project that says so and whose usage the scan cannot see got neither the entitlement nor the define, while the preflight warned about a profile for a capability the build was never going to ask for. Both flags now, for the reason the scan's own comment gives: an entitlement without the define is a SyncedStore that reports itself unsupported on the device. Only an explicit true does it. Unset still means "the bytecode decides", which is what keeps an app that merely hands work to a nearby device from being given an iCloud entitlement its App ID may not carry. That half has no unit-test seam: the flag resolution lives inside build(), and the plist tests beside it drive static helpers rather than the build flow. I verified the placement instead -- both consumers, the entitlement block and injectToPlist(), run later in the same method -- and the plugin's 1938 tests still pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d667acb232
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…y enable() The door my own parking change opened, and the symmetry I missed when I closed the same one for clear(). Callback.decide() parks an arrival that reaches the seam before the application has chosen -- a synced-store listener installs that seam without enabling continuity -- so by the time a logged-out app says "off" there can be a copy here as well as at the port. disable()'s early return drained only the port's, and enable() drains this slot on purpose, so the login restored a payload and routes that arrived before the application said it wanted none. disable() documents the opposite. The full path below already clears it as part of ending the session; the early return leaves before reaching that, which is the whole of the difference between the two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f1aaf2ca3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The previous commit put `parked = null` inside `if (!enabled)`, which is check-a-static-then-assign-a-static and trips the forbidden PMD rule NonThreadSafeSingleton. Both paths want it -- the full one clears the slot a few lines further down anyway -- so it is hoisted above the split rather than suppressed: one statement, said once, and the shape the rule objects to is gone. Recording the process failure rather than only the fix: I ran the quality gate and the commit in one chained command, so the push went out while the gate was reporting a violation. The gate's exit code has to be read before the commit, not beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b44659b47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// Hands an arrival to the framework, or holds it. Called on whatever thread the activity | ||
| /// arrived on; the framework marshals what it needs to. | ||
| private static boolean deliverToFramework(String activityType, String userInfoJson) { | ||
| ContinuityCallback c = callback; |
There was a problem hiding this comment.
Safely publish the installed iOS callback
When continuity is already enabled and iOS invokes nativeContinuation() on its platform thread, this unsynchronized read has no happens-before relationship with the EDT write in setCallback(), so it may still observe null. The arrival is then reported unclaimed and placed in pendingType/pendingJson, but no later callback installation is guaranteed to re-offer it, leaving a valid Handoff ignored indefinitely; publish the callback across threads or synchronously obtain it through EDT coordination.
Useful? React with 👍 / 👎.
ContinuityCallback lets a port call from any thread and says nothing about the map outliving the call, so a port that recycles one buffer per arrival -- or clears it once the call returns -- is within its rights. The off-EDT branch queued only the reference and had ALREADY told that port the activity was claimed, so the decision read whatever the bridge had put there since: a different state, or none at all. Read into an AppState before returning instead. fromMap() detaches it completely, which is why no copy helper is needed here -- setPayload deep-copies, the routes are rebuilt into a new list, and everything else a payload may hold is immutable -- so what is queued is the framework's own object. The claim gets honest with it, which is the part worth having: a document that yields no state is now declined rather than claimed, because nothing was taken. The test drives a bridge that empties and refills its map the instant the call returns, and checks the payload that reaches the provider is the one that was handed over. Without the fix nothing is delivered at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 285561b401
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
I gated the display half of this rollback last round and left the stack half unconditional, which made the two disagree. A show callback that navigates somewhere of its own and then throws -- or navigates and has a later listener throw -- has already changed both, so erasing the stack while leaving that screen up describes a place the user is not: back() then works on a history that does not include what is in front of them. Same rule as the ordinary navigations already use, and for the same reason: whatever ran later and changed the stack meant to, and it wins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a3878e3654
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| } | ||
| List<String> routes = usableRoutes(state.getRoutes()); | ||
| if (routes.isEmpty()) { |
There was a problem hiding this comment.
Apply filtered routes before the payload-only commit
When an inbound state contains a valid payload but every route exceeds the local writeUTF limit, usableRoutes() returns an empty list and this branch commits before state.setRoutesUnchecked(routes) runs. The provider has already applied the payload, but persist(state) repeatedly fails on the original oversized routes, leaving the arrival parked, reapplying its payload on retries, and blocking relay publications. Update the state with the filtered routes before taking this early return.
Useful? React with 👍 / 👎.
What this is
A Codename One app that the operating system kills comes back to its first screen.
Lifecycle.stop()kept the currentFormin a plain field, so a suspend and resume looked right and a reclaimed process lost everything -- which on Android is the ordinary outcome of a few minutes in another app.com.codename1.continuitysaves what the user was doing and brings it back, and on Apple platforms offers that same work to the other devices the person is signed in to.The substrate was already here and unused:
com.codename1.routerkeeps a stack of deep-link paths, which is exactly a serializable, portable "where the user is".Navigation.restoreStackrebuilds it without animating through every screen on the way.Two packages, because they cost different things
@Routescreen stackStateRelayStateRelayStateRelayStateRelaycom.codename1.continuitybuys a native define and oneNSUserActivityTypesentry, and no entitlement.com.codename1.continuity.syncbuys the iCloud key-value store, whose entitlement has to be granted on the App ID -- handing that to an app that only wanted to pass work to the tablet in the user's other hand would fail its codesigning for a capability it never asked for. Same split, same reason, asusesSmartHome/usesHomeAccessoryData.Three decisions worth recording
stop()returns, so an app that saved there would pay for it on every suspend.start()is unchanged for every existing app, andrestore()is never called for anyone -- where restoration belongs in a launch is a decision only the app can make.StateRelayagainst the app's own endpoint, because deciding which saved states belong to the same person is the app's account system's question.The load-bearing detail
The iOS delegate matches continuity before intents. The intents block ends in a general branch that hands any remaining activity to Java and returns Java's answer, and
Intents.dispatchUserActivitycorrectly declines a type it never declared -- so an app using both would have had its own continuation asked about by the wrong framework, told no, and dropped.NSUserActivityTypesstays a single key for the same reason a second one is worse than none: iOS reads a duplicated key unpredictably. The two contributors meet inuserActivityTypesKey.Android needs nothing injected -- no permission, no manifest entry, no dependency. The bridge exists there for one job: flushing the checkpoint from
onSaveInstanceState, the last callback guaranteed before a background process is reclaimed. Both cross-device capabilities report themselves unsupported rather than being emulated, because an app told "yes" by a bridge that then dropped the state is worse off than one told "no", which can fall back to a relay and reach an iPhone as easily as another Android.Verification
The load-bearing one: a real Xcode build of the generated project produced one
NSUserActivityTypesarray carrying both the sample's three App Intent ids and...hellocodenameone.continuity.BUILD SUCCEEDEDfor iOS and watchOS, and a deliberate probe inside the#ifdef CN1_USE_CONTINUITYblock failed the build, so that check is not vacuous. Dead-code elimination left both native callbacks with real bodies.Also green locally: 46 core unit tests, 10 plist-merge tests, 7 preflight tests, 5 shipped-hooks tests; SpotBugs 0 findings across
core-unittests,android,ios,codenameone-maven-pluginandbuild-hint-catalog; and the cast-semantics, control-character, copyright, build-hint, snippet and prose gates.Paired change
The builder half is mirrored in BuildDaemon; that PR has to land with this one.
🤖 Generated with Claude Code