Skip to content

Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair - #5487

Open
shai-almog wants to merge 262 commits into
masterfrom
watch-apps-product
Open

Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair#5487
shai-almog wants to merge 262 commits into
masterfrom
watch-apps-product

Conversation

@shai-almog

@shai-almog shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

isWatch() existed without a product on top of it. This turns the watch render
slice into a watch app: one setting builds it on both platforms, the two apps can
talk, and you can develop the pair on your desktop.

Bugs this fixes

Wearables are new and nothing depends on them, so these are fixed rather than
preserved:

  • A cloud build never produced a watch app. codename1.watchMain became a
    build argument only on the local path; the server lifts only codename1.arg.*
    keys out of the uploaded settings file, so the daemon asked for watchMain and
    got nothing.
  • The documented companion default never embedded the watch app.
    watchNative.embedCompanion defaulted to false, so the "Embed Watch Content"
    phase was actively removed even in companion mode.
  • watchMain reached only iOS. Wear OS was enabled by an unrelated
    android.wear hint, so a project had to declare the same intent twice.
  • The simulator had no watch form factor. JavaSEPort never overrode
    isWatch(), so the guide's advice to iterate on a watch layout locally was
    untrue.
  • A Wear app could not scroll. onGenericMotionEvent read only the mouse
    axes; rotary input arrives on SOURCE_ROTARY_ENCODER / AXIS_SCROLL.
  • A round Wear face clipped its own corners. No display cutout is reported,
    so the safe area came back zero.

What is new

One setting. codename1.watchMain is the entire opt-in on both platforms.
Nine build hints are deleted; bundle id, deployment target, team id and display
name are derived. codename1.watchStandalone is the only other setting — the one
thing not inferable from the project. Net new hints: zero.

com.codename1.wearable — the phone↔watch channel, same API on Apple Watch
and Wear OS, modelled on com.codename1.car (portable API, SPI bridge, inert
when there is nothing on the other end). It exposes the three transports the
platforms actually give us, because picking the wrong one is the usual reason a
watch app "never gets the update": sendMessage for a live answer, putData for
state that survives sleep and relaunch, transferFile for bulk. Callbacks arrive
on the EDT and are queued across a cold start — the platform starts an app purely
to hand it a payload. Backed by WCSession on Apple and the Wearable Data Layer
on Android, both gated by API scan so apps that never talk to a watch link
nothing.

A simulator that runs the pair. Four generated watch skins (Apple Watch 41/45,
Wear round, Wear square), isWatch() and the "watch" override layer, and a
Watch menu that launches watchMain in a second process wired to the first — so
sendMessage and putData genuinely round-trip on the desktop. Two processes,
not two windows: Display is a singleton and sharing it would hide the bugs that
only appear once the pair is real.

Complications as surfaces families. A complication is a WidgetKit widget in
an accessory family, so WATCH_CIRCULAR/RECTANGULAR/INLINE/CORNER join
WidgetSize rather than getting an API of their own.

The guide, rewritten around the two-app model, with the data-sharing decision
table as its centre.

Not yet done, and stated as such

  • The watchOS widget extension target and the Wear complication/tile services
    that render the watch families are not generated yet. The guide says so.
  • The watch target compiles its OWN ParparVM translation, rooted at
    codename1.watchMain, and boots a watch stub. Verified end to end: the watch
    target compiles, links and renders on the watchOS simulator (166 pass / 3 fail
    / 2 skip). Tree-shaking is real -- the phone tree carries 24 generated SVG
    classes, the watch tree none -- and that is also the one known regression:
    those classes are reached reflectively, so SVGStatic,
    SVGAnimatedScreenshotTest and LottieAnimatedScreenshotTest fall back to a
    placeholder render. The watch pass needs the same reflective roots the phone
    pass keeps. A project whose watch entry point IS the phone main keeps a single
    translation and is unaffected.
  • Companion mode on Android does not yet emit a second wear APK; standalone works.

Verification

CI is the gate. Everything below is what was additionally checked locally before
pushing, and the numbers move as the branch does.

  • codenameone-maven-plugin: 466 pass, 1 skipped
  • core-unittests: 4754 pass, with SpotBugs, PMD and Checkstyle at zero
  • core, JavaSE, iOS and Android ports build; the injected Android bridge sources
    type-check against a stub harness (nothing in CI compiles them) and the
    WatchConnectivity native passes clang -fsyntax-only for iphoneos/arm64
  • docs gates green: snippet validation (659 blocks), Asciidoctor
    --failure-level WARN, Vale, capitalization
  • build-ios-watch does run in CI -- it is a job in Test iOS UI build
    scripts
    , on macos-15 with DEVELOPER_DIR pointed at Xcode 26, and this PR
    touches paths that trigger it. It is the real gate for anything that alters
    the generated Xcode project. It is not run on the authoring machine, and
    during the rapid review cycle several of its runs were cancelled by
    supersession, so check it against the CURRENT head rather than an older run.

Server-side half: codenameone/BuildDaemon#watch-apps-product

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings July 29, 2026 11:21
chatgpt-codex-connector[bot]

This comment was marked as resolved.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 29, 2026 11:36
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 29, 2026 11:44
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@shai-almog

shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

Copilot AI review requested due to automatic review settings July 29, 2026 12:07
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@shai-almog

shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 8.09% (7868/97198 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.07% (41636/515701), branch 2.88% (1402/48723), complexity 3.18% (1663/52272), method 4.90% (1355/27642), class 9.97% (367/3680)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 8.09% (7868/97198 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.07% (41636/515701), branch 2.88% (1402/48723), complexity 3.18% (1663/52272), method 4.90% (1355/27642), class 9.97% (367/3680)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 310ms / native 120ms = 2.5x speedup
SIMD float-mul (64K x300) java 179ms / native 152ms = 1.1x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 96.000 ms
Base64 CN1 decode 85.000 ms
Base64 native encode 361.000 ms
Base64 encode ratio (CN1/native) 0.266x (73.4% faster)
Base64 native decode 270.000 ms
Base64 decode ratio (CN1/native) 0.315x (68.5% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

Copilot AI review requested due to automatic review settings July 29, 2026 12:47
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 29, 2026 12:56
chatgpt-codex-connector[bot]

This comment was marked as resolved.

isCompanionAppInstalled asked the node list, which answers "is a device
connected" rather than "is that device running this app" -- a bare watch reported
the companion as installed. The build now declares a cn1_wearable capability that
the peer half advertises, and the question is asked of that.

A build with codename1.watchMain but no codename1.watchStandalone now logs that
the companion Wear APK is not generated yet, so the gap is visible where a
developer is looking rather than only in the guide.

Mirrored to the BuildDaemon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sample declares a watchMain, so its app scheme carries an embedded watch app
with a ParparVM translation of its own and xcodebuild builds that before running a
single test. I raised this step's budget in scripts-ios-native.yml when it first
timed out and missed that ios-packaging.yml runs the very same script -- it timed
out there at 20 minutes for exactly the same reason.

Nothing else in that job was wrong: its iOS UI smoke step reports pass=48 fail=119
both here and on the previous commit, which passed, so those screenshot diffs are
pre-existing on this branch and non-gating rather than something this change
caused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9f1cb52ee

ℹ️ 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".

…nd tags are elements

mirrorSecondaryEntryPointsToBuildArgs runs AFTER overlayCommandLineBuildHints, so
a -Dcodename1.arg.watchMain= passed on the command line was already sitting in the
properties -- and the mirror overwrote it with the project file's
codename1.watchMain. The standard build-argument override silently did nothing on
a cloud build: the wrong lifecycle, or a companion where a standalone Wear build
was asked for. An existing value wins now; the mirror only carries a project-file
setting into the args channel when nothing has put it there already.

And the plist scanner matches ELEMENTS rather than literal tag text. `<key >` is
the same element as `<key>` to an XML parser -- which is what reads the phone's
plist -- so the phone accepted an override the watch never found, and the pair
shipped with different versions. Opening and closing tags are both patterns now,
and the scan advances past the content rather than by a fixed tag width, since
`</key >` is not the length of `</key>`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28745cb6c1

ℹ️ 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".

…() off the EDT

`#if !TARGET_OS_WATCH` around a phone-only @import is the standard Objective-C
spelling, and the conditional scanner only understood Swift's os() expressions --
so it read the guard as unevaluable, kept the import, and attached an iOS-only
package to the watch target over code the compiler excludes. TargetConditionals
are read now, with TARGET_OS_IPHONE deliberately absent from the excluding list:
it is 1 on watchOS, so a block guarded by it does compile there. `#ifdef
TARGET_OS_WATCH` is not a platform test either -- every one of those macros is
always defined, as 0 or 1 -- so it stays unevaluable.

And the durable transfer claim no longer writes from the EDT. The delivery
callback runs there once the listener has had the payload, and commit() is a
synchronous disk write, so a slow or contended store froze rendering and input for
the duration of every received transfer. It moves to the same single-threaded
timer the rest of the transfer bookkeeping uses -- claims still land in
confirmation order -- and the acknowledgement moves with it, staying behind the
write rather than reopening the window that ordering exists to close.

Nine conditional shapes checked, including the Swift ones from earlier rounds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9212138f1b

ℹ️ 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".

…and negation is read

A spooled record was claimed and charged an attempt even when no listener of its
kind existed. Replaying it then only parked the callback in memory, so the
confirmation never ran -- and a service process reclaimed over and over charged an
attempt per lifetime for a delivery that never reached application code, deleting
it on the budget alone after four of them. That is precisely the loss the budget
exists to bound. A record nobody can take is now left exactly as it was, for the
launch where someone can.

`#elif` is the C spelling of `#elseif`, and reading only the Swift form left a
suppressed first arm suppressed straight through the watch arm of an Objective-C
`#if TARGET_OS_IOS ... #elif TARGET_OS_WATCH ... #endif`.

And a NEGATED platform test is the opposite answer. `!TARGET_OS_IOS` is true on
watchOS, so that arm is the one the watch compiles -- excluding it merely because
the condition mentions TARGET_OS_IOS removed the active arm and dropped the
package imported there. The same rule was wrong on the Swift side for `!os(iOS)`
and is fixed with it. Ten shapes checked across both spellings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1433739fac

ℹ️ 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".

Comment thread CodenameOne/src/com/codename1/wearable/WearableConnection.java Outdated
shai-almog and others added 4 commits August 11, 2026 20:54
…are quoted

The clock seed was not enough on its own, as the review points out: twenty
requests and a restart ten milliseconds later reissues half of them, and a
delayed reply then completes the NEW process's handler with the old payload.
Tokens now come from a block reserved in Preferences, and the block after it is
recorded BEFORE any of it is handed out -- so a process that dies mid-run has
still moved the stored base past everything it could possibly have issued. Exact
rather than probabilistic, for one small write per process. With no storage at all
it falls back to the clock, which is a weaker discriminator than the counter and a
better one than starting from 1 again.

And the staged watch sources are emitted as quoted strings rather than a %w[] word
list. A translated native source is named after the class it came from, so a
project with a space in one -- `My Bridge.m` -- had that name split into two
words: the watch target referenced two files that do not exist and linked without
the symbols the real one carries. The excluded-sources list keeps %w[] and says
why -- it is a constant in this file that no project-supplied name reaches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping every .xcassets was too broad. A project that keeps its images in a
custom catalog had none of them on the watch, so a UIImage(named:) from
watch-reachable code returned nil at runtime with nothing in the build to explain
it. Only a catalog carrying an app icon or a launch image is iOS-specific -- those
are the sets with no watch-applicable content, and the reason the filter existed
-- while a catalog of ordinary image and colour sets compiles for watchOS like any
other. A catalog whose path cannot be read keeps the old conservative answer: a
missing asset is a runtime nil, but an uninspectable catalog might be the app's
own icon set, and that is a build error for everybody.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…module

The BuildDaemon build failed with "Could not find the selected project in the
reactor: cn1-hardening". Its CI builds the COMPANION CodenameOne branch when one
exists, so it was building this branch -- which was ten commits behind master and
did not yet have that module. Merging master brings it in; nothing here needed
changing for it.

putData and transferFile allocated their sequence on the calling thread, and
allocating one advances the durable clock floor with a synchronous commit(). Both
are called from application code that is usually the EDT, so an ordinary UI-driven
publish blocked rendering and input on storage I/O. The whole publication moves to
the transfer worker, which preserves the two things that matter: the commit still
completes before the item is published, and the worker is single-threaded, so two
publications keep the order their callers made them in -- which is exactly what
their sequence stamps are meant to record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e147610106

ℹ️ 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".

shai-almog and others added 3 commits August 11, 2026 22:05
…able

Deferring publication onto the worker made the caller's array a shared reference:
a caller that reuses or refills its buffer after putData or transferFile returns
had the worker publish whatever the array said by then rather than what was handed
over. Both clone before scheduling.

And a message or removal always goes through the spool now, even when a listener
is registered. Delivering directly looked like the fast path and left the same
hole the spool exists to close -- deliverMessage only QUEUES onto the EDT, so a
process killed between the queueing and the callback lost a one-shot the Data
Layer does not retain. One record, one ordering, and a release that happens when
the listener has actually had it. The cost is a small write per message on a Data
Layer worker, which is not a UI thread.

A reply-bearing request keeps its direct path, and deliberately: its answer cannot
be deferred, because the peer times out long before a later launch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deferring putData onto the worker left removeData issuing its delete straight from
the caller, so `putData(path, value); removeData(path);` could delete first and let
the put land behind it -- leaving the value published after the very call that
removed it. The delete goes to the same single-threaded worker now, so the Data
Layer sees these operations in the order the application asked for them.

That is every mutation of the data namespace on one queue: the publication, the
transfer, the transfer acknowledgement and now the removal. What stays off it is
sendMessage, which is a live delivery rather than a change to a published value
and has no ordering relationship with one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing for it

The acknowledgement is published under /cnxk and the listener service's manifest
filter routed only /cn1, so the sender was never woken when a receiver confirmed a
delivery. Nothing else arms the sender's sweep -- the bridge registers no
DataClient listener of its own -- so an acknowledged transfer stayed replicated on
both devices until the seven-day hard cap, unless the app happened to send another
file or restart. For a large file that is days of storage nobody is using.

The filter now carries both namespaces, and the service treats an acknowledgement
as what it is: bookkeeping between the two ports, never an app-visible callback,
on a path no application names. It arms the sweep through the same coalescing
entry point every other caller uses, and builds a bridge if the wake-up found none
-- an acknowledgement-only wake-up is exactly the cold case that needs one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e01ba6e96

ℹ️ 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".

shai-almog and others added 3 commits August 12, 2026 04:53
…ble, both watch slices required

getData collapsed a failed query into null when nothing was cached, which is the
public API's "nothing is published" -- so the first read in a cold process that
happened to time out told the caller an item was gone while it sat there, and a
caller acting on that discards valid state. A cold read is retried now, and the
retries are spent only when there is no snapshot to fall back on, since a cached
value settles the question on the first failure. When it still cannot be answered
the log says so, because the return value cannot.

A live request is written down before it is delivered. The live delivery only
queues onto the EDT, so a process killed in that window lost the request entirely
-- the sender times out either way, but the receiver never learned it had been
asked. The record survives and replays on the next launch as a plain message,
which is what the request has become by then; the delivery confirmation releases
it, so the normal case leaves nothing behind and nothing is delivered twice.

And a vendored bundle needs BOTH watch variants before it is linked. The target
builds for watchos and watchsimulator, so an .xcframework carrying only the device
library, or a .framework declaring only WatchOS, links on hardware and breaks
every simulator run -- the same asymmetry the system-framework check already
guards by requiring presence in both SDKs. Four bundle shapes checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sendMessage returns while the node query is still running when the cache is
stale -- the first send of a process, typically -- and hands the caller's array to
fanOut from the completion callback. A caller that reuses its buffer, which is the
natural thing to do with one, therefore had the peer receive whatever the array
said by then rather than the message it passed. The publication paths already copy
for this reason; this is the send that can outlive its call, and it was the one
still holding a reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
onPeerConnected arrives on an EXPORTED service with no binding permission, so any
installed app can invoke it with a node id of its choosing -- and the id went
straight into the provenance allowlist that isKnownNode consults first. The same
app could then forge a message or data callback under that id, launch this app and
reach its listeners, walking past the check that exists to stop exactly that.

Peer notifications are state, not identity. The callback no longer feeds the
allowlist: trust comes only from asking Play services -- connectedNodeIds,
capabilityNodeIds, getLocalNode -- which isKnownNode already does for itself on
the first event it cannot vouch for.

A pushed CONNECT no longer inserts its node into the reachable snapshot either,
since isPaired(), isReachable() and fanOut answer from that; it is treated as a
hint that something changed and answered with a real query. A DISCONNECT is still
applied as it arrives -- removing a node can only understate what is reachable,
and the next query corrects it.

Audited the other feeders while here: every remaining rememberAll() takes the
result of a getConnectedNodes() query, and capabilityChanged writes only the
bonded set, which no provenance decision reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20b966ea5d

ℹ️ 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".

Comment thread Ports/iOSPort/nativeSources/CN1WatchRuntime.m Outdated
shai-almog and others added 2 commits August 12, 2026 10:17
Clearing the phase queue before delivering is necessary -- a delivery runs
translated code and must not be replayed re-entrantly -- but on its own it left a
window. The bootstrap thread could copy a queued BACKGROUND phase and be
descheduled; the main thread's foreground transition then found an empty queue,
delivered itself immediately, and the older background arrived after it. The app
ends up stopped and minimized while visibly on screen.

An empty queue is therefore the wrong question. A new transition asks whether a
drain is in flight, and queues behind the batch if one is; the drain loops until
the queue is empty, so it delivers whatever arrived while it was working and
ownership is released in one place.

A static archive now needs a simulator-capable slice as well as a watch device
one: arm64_32 and armv7k exist nowhere but watchOS, but the target also builds for
watchsimulator, and a device-only archive fails every simulator run. The log names
the architectures found so a rejection is attributable.

And a DEFINEDNESS test is not a platform test. TargetConditionals defines every one
of its macros on every platform, as 0 or 1, so `#ifdef TARGET_OS_IOS` and
`#if defined(TARGET_OS_IOS)` are both true on watchOS and the branch compiles
there -- reading them as iOS-only dropped the package imported inside an arm the
watch does build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scanningClass contract now states what a null means and what a consumer owes
it: no attribution, and DISCARD whatever was being held. That is the client half
of a daemon-side gap -- its non-ASM fallback parser, used for every class compiled
to a version ASM cannot read, never named the class it was reading, so a
setWriteToStore(true) found there was credited to the last ASM-scanned class and
the per-root health walk entitled the wrong target or neither.

The client's Executor has no fallback path, so the fix itself lives in the
BuildDaemon copy; what belongs here is the contract both sides implement, and
IPhoneBuilder already ignores a null rather than keeping the previous class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df8757d74c

ℹ️ 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".

Comment thread Ports/iOSPort/nativeSources/CN1WatchRuntime.m Outdated
Comment thread tools/watch-skins/GenerateWatchSkins.java
shai-almog and others added 2 commits August 12, 2026 13:16
…rator

A java.util.Timer heap is keyed on execution time alone, so among the
schedule(task, 0) calls a burst of mutations makes it can pop tasks out of
submission order -- putData/removeData/putData could issue the last put before
the delete and leave the path absent, which is the opposite of the ordering two
comments in that file claimed. Same single worker thread, now a
ScheduledThreadPoolExecutor, which breaks the tie on a submission sequence.

The watch phase drain released ownership outside the critical section that saw
the queue empty. A transition arriving in that window was queued (a drain was in
flight) and then declined a drain thread (that drain's thread had not finished),
so it sat there unowned -- unrecoverable in the background, where no later paint
comes to notice. The queue test and the release are now one decision.

Three in the generated Xcode script, all behaviour a text assertion cannot see:
an arm reader that knew #if os(watchOS) but not the equally valid parenthesized
spelling, and so mirrored a phone-only import into the watch target; an archive
filter calling any arm64 slice watch-simulator-compatible when it is equally an
iPhone's, now reading the Mach-O platform; and a catalog filter that skipped a
whole mixed Assets.xcassets over its app icon, now staging a watch copy without
the incompatible sets. Both builders gain a test that runs the emitted ruby.

The watch skins described a landscape they do not ship artwork for. skin_l.png
is the portrait image because there is no landscape watch, but safeLandscape*
was the transpose -- so rotating measured layouts against a screen wider than
the one being drawn. The metadata now matches the image, and the simulator
disables rotation for a watch skin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replacing the Timer with a scheduled executor fixed the ordering but changed
what happens to an uncaught throwable. A Timer took its thread down with it --
catastrophic, but impossible to miss. schedule() instead reports the failure
into the Future it returns, and nothing here keeps those, so a bug in a mutation
or a retry would have vanished without a word.

afterExecute reads the completed Future and logs what it finds. A cancellation
is routine -- an armed sweep deadline that a nearer one replaced -- and is not
reported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e4aa700d0

ℹ️ 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".

Comment on lines +4311 to +4312
watchCmd.set(outIndex + 1,
WatchNativeBuilder.translationRoot(request.getMainClass()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve reflective asset roots in the watch translation

When watchMain differs from the phone entry point, this second translator pass roots the watch binary only at the generated watch stub, so classes reached exclusively through reflection are removed. The watch UI suite consequently renders placeholders in SVGStatic, SVGAnimatedScreenshotTest, and LottieAnimatedScreenshotTest; watch apps loading generated SVG or Lottie classes the same way will also ship without those assets. Pass the phone translation's reflective keep roots into this watch-specific translation.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e99b7e550c

ℹ️ 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".

@@ -489,14 +512,26 @@ private void renderPage() {

private void renderBasic() {
page.add(pageTitle("Basic", "Core application settings - title, version, package and icon."));
Container grid = new Container(new GridLayout(3, 2));
Container grid = new Container(new GridLayout(4, 2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allocate enough rows for all basic settings

The grid now contains nine components after adding the watch main, standalone switch, and TV main fields, but it declares only four rows by two columns. GridLayout uses the declared row count when calculating preferred height even when extra components force a fifth row during layout, so the Basic settings page underestimates its required height and compresses or clips the final row. Declare five rows (or use a column-driven layout) so all fields contribute to the preferred size.

Useful? React with 👍 / 👎.

The Cloudflare Pages step required both an immutable URL and a
"Deployment alias URL:" line in wrangler's output, and failed the job when
either was absent. wrangler does not print the alias line reliably -- one
deploy of this branch printed it and the next, to the same preview branch, did
not -- while the upload, the alias and the deployment had all succeeded. The
job went red over a line of console output.

The alias still existed: the branch URL this build could not scrape serves the
site. PREVIEW_BRANCH is built from a PR number, so it is always lowercase,
hyphenated and well inside the 28 characters Cloudflare truncates an alias to,
and the URL can simply be rebuilt from it. Only accepted if it responds;
otherwise the immutable deployment URL is used and a warning is emitted, since
that is a working preview link too.

A missing immutable URL is still fatal -- there is nothing to fall back to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd639b4b63

ℹ️ 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".

Comment on lines +1492 to +1494
if (dataListeners.isEmpty()) {
replayRequests.put(key == null ? replay.toString() : key, replay);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer replays while the data queue is draining

When a listener registers after more than MAX_PENDING tracked transfers accumulated, the drain runs a queued port replay while drainingData > 0, so every re-offered transfer is parked despite the listener being present. If that replay fills the queue, evicting a one-shot invokes requestReplayAfterDrain() again; this condition sees the registered listener and runs the replay immediately, recursively rescanning and evicting the same durable backlog until the process can overflow its stack or stall. Treat an active drain (and preferably a nonempty queue) like the no-listener case and retain the keyed replay for the next drain pass.

Useful? React with 👍 / 👎.

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