Skip to content

feat: ClientNode controller API additions + deprecate legacy controller API - #4091

Merged
Apollon77 merged 93 commits into
mainfrom
feat/clientnode-controller-api
Aug 7, 2026
Merged

feat: ClientNode controller API additions + deprecate legacy controller API#4091
Apollon77 merged 93 commits into
mainfrom
feat/clientnode-controller-api

Conversation

@Apollon77

Copy link
Copy Markdown
Collaborator

Summary

Adds the ClientNode-based controller convenience API that closes the gaps blocking deprecation of the legacy PairedNode / CommissioningController, and deprecates that legacy API for 0.19 removal. Everything is a thin, additive surface over existing machinery; legacy PairedNode/CommissioningController stay fully functional through the deprecation period.

Implemented from the plan docs/superpowers/plans/2026-07-14-clientnode-controller-api-additions.md.

What's added

  • Behaviors.forCluster(clusterId) + a model-scoped cluster name↔id resolver (relocated to @matter/types ClusterHelper, now with an optional MatterModel param defaulting to global Matter).
  • Peers.commissioned — commissioned operational peers (excludes commissionable discoveries and group nodes).
  • NodeLifecycle.isSeeded / seeded — node structure read at least once (BasicInformation present and beyond the root endpoint); seeded emits once. Also fixes Endpoints.size double-counting the root endpoint.
  • Commissioning-window helpers on CommissioningClient + ClientNode: openBasicCommissioningWindow / openEnhancedCommissioningWindow (returns { manualPairingCode, qrPairingCode }).
  • Peer.establishmentUnresponsive (@matter/protocol) — fires once when a CASE (re)establishment attempt retransmits past the MRP budget; observation-only, retransmission behavior unchanged.
  • Connection-state engine on NetworkClient exposed via NodeLifecycle: NodeConnectionState (enum values pinned to legacy NodeStates: Connected=0, Disconnected=1, Reconnecting=2, WaitingForDeviceDiscovery=3), connectionState, connectionStateChanged, isConnected. Disconnected = stopped/not-started/disabled; lifecycle on start is Disconnected → Reconnecting → Connected.

Deprecation

The legacy @project-chip/matter.js controller/device API (CommissioningController, PairedNode, Device/Endpoint/Aggregator/ComposedDevice, the cluster clients, and their public members — ~330 @deprecated tags) is marked deprecated for removal in 0.19, pointing at docs/MIGRATION_CONTROLLER_018.md (WIP guide included here).

Testing

Full gate green at HEAD: build --clean, format-verify, lint, and package suites — @matter/types 374/374, @matter/protocol 1450/1450, @matter/node 1471/1471, @project-chip/matter.js 5/5 (ESM/CJS/Web). New test files cover each addition with real-behavior assertions (idempotency, recovery, emit-once, transition ordering).

Reviews

Per-task spec+quality reviews (all clean) + a final independent adversarial whole-branch review (Ready to merge, 0 Critical/Important) + a "greg" architectural review (Approve-with-fixes — ClusterNaming relocation applied) + a second independent adversarial pass (Ready to merge). The two highest-risk paths — a synchronous signal emitted inside the MRP retransmission path, and reactor re-registration across stop/start — were each traced to correct handling (offline-transaction fault isolation; Reactors.add dedup).

Follow-ups (not in this PR)

  • Hard-send-failure (ENETUNREACH) as a third likelyOffline trigger — needs a new Peer-level observable; connection state currently degrades to Reconnecting in that window (mDNS still recovers).
  • Peers.#instrumentBasicInformation registers non-self-disposing .on handlers (pre-existing) that could accumulate if clusterInstalled re-emits.
  • Fill remaining MIGRATION_CONTROLLER_018.md sections (events bus, subscription examples, FAQ) alongside the shell + server migrations.

🤖 Generated with Claude Code

Apollon77 and others added 11 commits July 14, 2026 17:01
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lientNode

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…et on CASE reconnect

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Disconnected now means the network client is stopped, not started or
disabled, driven by lifecycle.shouldBeOffline instead of !isCommissioned.
A decommissioned node is destroyed and no longer drives connection state.
Reset the likelyOffline latch on the Disconnected branch so a re-enabled
or restarted node does not resume in WaitingForDeviceDiscovery, and set
targetState offline in ClientNode.disable() so disabling settles straight
to Disconnected without a spurious Reconnecting emission.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ith optional model

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 14, 2026 21:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a set of controller-facing convenience APIs centered on ClientNode (controller peers) to enable deprecation of the legacy CommissioningController / PairedNode controller surface, while keeping the legacy API functional through the deprecation window.

Changes:

  • Added new ClientNode controller conveniences (commissioning-window helpers, commissioned-peer enumeration, seeded/connection-state lifecycle signals).
  • Added model-scoped cluster name/id resolution helpers and Behaviors.forCluster(clusterId) for cluster→behavior lookup.
  • Marked the legacy controller/device APIs in @project-chip/matter.js as deprecated for removal in 0.19 and added an initial migration guide + changelog entries.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/types/test/cluster/ClusterNamingTest.ts Adds tests for new cluster name↔id helper resolution (including custom MatterModel).
packages/types/src/cluster/ClusterHelper.ts Extends cluster/endpoint name resolution to accept a MatterModel and adds attribute/command/event id↔name helpers.
packages/protocol/test/peer/PeerEstablishmentProgressTest.ts Adds tests for establishment-unresponsive detection logic and MRP-driven behavior.
packages/protocol/src/peer/PeerConnection.ts Adds retransmission observer hook + establishmentUnresponsiveDetector and wires it into CASE establishment.
packages/protocol/src/peer/Peer.ts Exposes peer.establishmentUnresponsive observable for higher layers to react to likely-offline detection.
packages/node/test/node/NodeLifecycleSeededTest.ts Adds tests for NodeLifecycle.isSeeded/seeded latch behavior and listener disposal.
packages/node/test/node/client/PeersCommissionedTest.ts Adds tests for Peers.commissioned filtering behavior.
packages/node/test/endpoint/properties/BehaviorsForClusterTest.ts Adds tests for Behaviors.forCluster(clusterId).
packages/node/test/behavior/system/network/ConnectionStateTest.ts Adds tests for NodeLifecycle connection-state transitions and idempotency.
packages/node/test/behavior/system/commissioning/CommissioningWindowTest.ts Adds tests for basic/enhanced commissioning-window helpers and revoke tolerance.
packages/node/src/node/NodeLifecycle.ts Adds seeded tracking + connection-state enum/observables and convenience accessors.
packages/node/src/node/ClientNode.ts Adds commissioning-window helpers and adjusts disable flow to avoid transient connection-state artifacts.
packages/node/src/node/client/Peers.ts Adds peers.commissioned and seeded evaluation/latching logic for peers.
packages/node/src/endpoint/properties/Endpoints.ts Fixes endpoints.size to avoid double-counting the root endpoint.
packages/node/src/endpoint/properties/Behaviors.ts Adds Behaviors.forCluster(clusterId) lookup.
packages/node/src/behavior/system/network/NetworkClient.ts Adds connection-state recomputation engine and likely-offline latch wiring.
packages/node/src/behavior/system/commissioning/CommissioningClient.ts Adds openBasicCommissioningWindow / openEnhancedCommissioningWindow helpers + revoke tolerance.
packages/matter.js/src/PaseCommissioner.ts Marks legacy commissioner wrapper API deprecated (0.19 removal).
packages/matter.js/src/device/PairedNode.ts Marks legacy PairedNode controller API surface deprecated (0.19 removal).
packages/matter.js/src/device/Endpoint.ts Marks legacy endpoint wrapper API surface deprecated (0.19 removal).
packages/matter.js/src/device/DeviceTypes.ts Marks legacy device types helpers deprecated (0.19 removal).
packages/matter.js/src/device/DeviceInformation.ts Marks legacy device-info wrapper types deprecated (0.19 removal).
packages/matter.js/src/device/Device.ts Marks legacy device wrapper classes deprecated (0.19 removal).
packages/matter.js/src/device/ComposedDevice.ts Marks legacy composed-device wrapper deprecated (0.19 removal).
packages/matter.js/src/device/Aggregator.ts Marks legacy aggregator wrapper deprecated (0.19 removal).
packages/matter.js/src/ControllerStore.ts Marks legacy controller store surface deprecated (0.19 removal).
packages/matter.js/src/CommissioningController.ts Marks legacy controller/discovery APIs deprecated (0.19 removal).
packages/matter.js/src/cluster/client/InteractionClient.ts Marks legacy interaction client and related types deprecated (0.19 removal).
packages/matter.js/src/cluster/client/EventClient.ts Marks legacy event client deprecated (0.19 removal).
packages/matter.js/src/cluster/client/DecodedDataReport.ts Marks legacy decoded-report types/helpers deprecated (0.19 removal).
packages/matter.js/src/cluster/client/ClusterClientTypes.ts Marks legacy cluster client types deprecated (0.19 removal).
packages/matter.js/src/cluster/client/ClusterClient.ts Marks legacy cluster client factory APIs deprecated (0.19 removal).
packages/matter.js/src/cluster/client/AttributeClient.ts Marks legacy attribute client deprecated (0.19 removal).
docs/MIGRATION_CONTROLLER_018.md Adds initial (WIP) migration guide from legacy controller API to ClientNode.
CHANGELOG.md Documents new controller conveniences and legacy API deprecation in WIP changelog.

Comment thread packages/matter.js/src/CommissioningController.ts Outdated
Apollon77 and others added 4 commits July 15, 2026 08:06
…plicate side effects

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@mergify

mergify Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

Apollon77 and others added 6 commits July 15, 2026 11:34
…migration guide

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Apollon77
Apollon77 marked this pull request as draft July 17, 2026 11:00
Apollon77 and others added 4 commits July 18, 2026 16:23
…Node

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e; explicit return type

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

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 62 out of 62 changed files in this pull request and generated no new comments.

Suppressed comments (5)

packages/nodejs-shell/src/util/ClusterEndpoint.ts:60

  • An empty accepted-command list is a valid, fully initialized result for clusters that support none of their optional commands. Using elements.size > 0 as the readiness check therefore treats every modeled command as supported and sends an invocation that the device explicitly omitted. Use the populated attribute list as the readiness sentinel instead; global attributes make it non-empty after the lists have been read.
    packages/nodejs-shell/src/shell/cmd_identify.ts:43
  • The timeout result is ignored, so after reporting that an offline peer did not seed, the command still iterates cached endpoints and attempts Identify. This defeats awaitSeeded's bounded-abort contract; exclude nodes that remain unseeded before invoking commands.
    packages/node/src/behavior/system/network/NetworkClient.ts:353
  • This documentation lists hard send failures as a source of likelyOffline, but this change only sets the latch from establishment-unresponsive and missed-ICD-check-in signals; hard send failure handling is explicitly not implemented. Remove that trigger from the comment so consumers do not expect WaitingForDeviceDiscovery after ENETUNREACH-style failures.
        /**
         * Latch indicating the peer is likely offline (unresponsive establishment, hard send failure or a missed ICD
         * check-in).  Set by liveness signals, cleared on a fresh session, an active subscription or when the node is
         * disabled/stopped.  Drives {@link NodeConnectionState.WaitingForDeviceDiscovery}.

packages/node/test/node/SplitCommissioningTest.ts:264

  • The repository requires typed MatterError subclasses rather than throwing a plain Error, including in test doubles. Model this simulated persistence failure with StorageError (and import it from @matter/general) so the test follows the same error contract as production storage code.
    packages/nodejs-shell/src/shell/cmd_cluster-attributes.ts:219
  • The local branch ignores fabricFiltered. In particular, a fabric-scoped attribute was always remotely read by the legacy AttributeClient, but this migration now returns cached state even for --no-fabric-filtered, so the command cannot retrieve the requested unfiltered value unless users also discover they must pass --remote. Force a remote read for fabric-scoped attributes (for example via attribute.effectiveAccess.fabricScoped) as well as when requestRemote is set.

Apollon77 and others added 16 commits August 7, 2026 13:42
The ServerNode-based controller storage migration landed in 0.16.0, not
0.13. Correct the FAQ version boundary and describe the two migration
steps (fabric/CA relayout, then per-node attribute data).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shell migrated to the @matter/node controller API in this branch and
no longer imports the legacy package. Remove the unused dependency and relock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the trailing restatement of what the guard does; keep only the
cross-file coupling to migrateLegacyCommissionedNodes's retry-skip
behavior.
Removes a vague cross-reference and updates the eager-creation invariant
comment now that two branches (factory reset, legacy migration) rely on it.
… on success

Call the self-guarding migration helpers unconditionally on startup so a
migration interrupted after only some peers were migrated resumes on the
next boot instead of being skipped by the outer needed-gate. Skip cleanup
when any peer failed to migrate this run, warn instead of the reset flag
being silently dropped, and skip (not abort startup) on an unexpected
fabric count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the removal of the unused @project-chip/matter.js dependency by
dropping its stale TypeScript project reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on-guide guard

Factory reset cleared only the current-format fabric, leaving the legacy
credentials/commissionedNodes/node-* source in place. The next boot then
re-migrated it, resurrecting the fabric identity and key material the reset
destroyed. The reset path now also wipes the legacy source (eraseLegacyStorage,
unconditional — the reset is discarding everything regardless).

Also correct the migration guide: call the migration helpers unconditionally
(they self-guard) rather than gating on legacyMigrationNeeded, which reports
false once any peer is migrated and would strand the rest on resume.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 66 out of 67 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

packages/protocol/src/peer/PeerConnection.ts:829

  • This resets the latch for every new message, not every CASE attempt: MessageExchange.send() resets its retransmission counter to zero, while one CASE exchange sends multiple handshake messages (for example Sigma1 and Sigma3). If two messages in one attempt each exceed the budget, establishmentUnresponsive fires twice, contrary to the once-per-attempt contract. The detector is already created per establishment exchange, so keep it latched for its lifetime.
    packages/node/src/behavior/system/network/NetworkClient.ts:49
  • Stopping a peer with autoSubscribe: false can leave connectionState stuck at Reconnecting. Node.stop() only changes lifecycle.targetState; with no active subscription, teardown emits no subscriptionStatusChanged, and none of these reactors observes the offline transition. The documented stopped → Disconnected behavior therefore only works when a subscription happens to drop. Recompute (and clear likelyOffline) when the lifecycle goes offline, and cover the non-subscribed stop path.
            this.reactTo(this.events.subscriptionStatusChanged, this.#recomputeConnectionState);
            this.reactTo(this.events.isDisabled$Changed, this.#recomputeConnectionState);
            this.reactTo(this.#node.lifecycle.decommissioned, this.#recomputeConnectionState);

Comment thread packages/nodejs-shell/src/util/legacyStorageMigration.ts
Apollon77 and others added 3 commits August 7, 2026 19:37
… fabric-count anomaly

When migrateLegacyCommissionedNodes skips because the migrated fabric count is
not exactly one, it now reports the skipped peers as failed rather than zero, so
the shell's failed===0 cleanup gate does not proceed to delete the un-migrated
legacy peers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shell already auto-migrated pre-0.16 storage before (via the legacy
controller); that behavior is only preserved here. The new capability is the
--cleanup-legacy-storage removal of the leftover data.

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

# Conflicts:
#	CHANGELOG.md
#	packages/matter.js/src/CommissioningController.ts
#	packages/matter.js/src/ControllerStore.ts
#	packages/matter.js/src/PaseCommissioner.ts
#	packages/matter.js/src/cluster/client/AttributeClient.ts
#	packages/matter.js/src/cluster/client/ClusterClient.ts
#	packages/matter.js/src/cluster/client/ClusterClientTypes.ts
#	packages/matter.js/src/cluster/client/EventClient.ts
#	packages/matter.js/src/cluster/client/InteractionClient.ts
#	packages/matter.js/src/device/Aggregator.ts
#	packages/matter.js/src/device/ComposedDevice.ts
#	packages/matter.js/src/device/Device.ts
#	packages/matter.js/src/device/DeviceInformation.ts
#	packages/matter.js/src/device/DeviceTypes.ts
#	packages/matter.js/src/device/Endpoint.ts
#	packages/matter.js/src/device/PairedNode.ts
@Apollon77
Apollon77 requested a balanced review from Copilot August 7, 2026 18:08
@Apollon77 Apollon77 added automerge Set this label if the PR is ready to automatically merged after approval and removed on hold labels Aug 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 51 out of 52 changed files in this pull request and generated no new comments.

Suppressed comments (3)

packages/node/src/behavior/system/network/NetworkClient.ts:49

  • When autoSubscribe is false there is no active subscription to emit subscriptionStatusChanged(false) during ClientNode.stop(). The node therefore remains in Reconnecting after it has gone offline, even though Disconnected is documented to cover stopped nodes. React to lifecycle.goingOffline as well so stopping an unsubscribed peer recomputes the final state.
            this.reactTo(this.events.subscriptionStatusChanged, this.#recomputeConnectionState);
            this.reactTo(this.events.isDisabled$Changed, this.#recomputeConnectionState);
            this.reactTo(this.#node.lifecycle.decommissioned, this.#recomputeConnectionState);

packages/nodejs-shell/src/util/legacyStorageMigration.ts:211

  • A failure from peerNode.delete() escapes this catch block, masks the original per-peer migration error, and aborts migration of every remaining peer. This contradicts the function's failure-isolation contract. Cleanup needs its own error handling (while still attempting clearAll) so the loop can record the failed peer and continue.
    packages/nodejs-shell/src/shell/cmd_nodes.ts:885
  • The actual peer address is validated immediately above, but the update is still queued against hard-coded fabric index 1. If the controller's fabric has another local index, SoftwareUpdateManager targets a different/nonexistent peer. Pass peerAddress directly.

@Apollon77
Apollon77 merged commit 335c329 into main Aug 7, 2026
45 checks passed
@Apollon77
Apollon77 deleted the feat/clientnode-controller-api branch August 7, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge Set this label if the PR is ready to automatically merged after approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants