feat: ClientNode controller API additions + deprecate legacy controller API - #4091
Conversation
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>
There was a problem hiding this comment.
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
ClientNodecontroller 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.jsas 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. |
…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>
|
Tick the box to add this pull request to the merge queue (same as
|
…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>
…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>
There was a problem hiding this comment.
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 > 0as 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 expectWaitingForDeviceDiscoveryafter 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
MatterErrorsubclasses rather than throwing a plainError, including in test doubles. Model this simulated persistence failure withStorageError(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 legacyAttributeClient, 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 viaattribute.effectiveAccess.fabricScoped) as well as whenrequestRemoteis set.
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>
There was a problem hiding this comment.
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,establishmentUnresponsivefires 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: falsecan leaveconnectionStatestuck atReconnecting.Node.stop()only changeslifecycle.targetState; with no active subscription, teardown emits nosubscriptionStatusChanged, and none of these reactors observes the offline transition. The documented stopped →Disconnectedbehavior therefore only works when a subscription happens to drop. Recompute (and clearlikelyOffline) 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);
… 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
There was a problem hiding this comment.
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
autoSubscribeis false there is no active subscription to emitsubscriptionStatusChanged(false)duringClientNode.stop(). The node therefore remains inReconnectingafter it has gone offline, even thoughDisconnectedis documented to cover stopped nodes. React tolifecycle.goingOfflineas 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 attemptingclearAll) 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,
SoftwareUpdateManagertargets a different/nonexistent peer. PasspeerAddressdirectly.
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; legacyPairedNode/CommissioningControllerstay 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/typesClusterHelper, now with an optionalMatterModelparam defaulting to globalMatter).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);seededemits once. Also fixesEndpoints.sizedouble-counting the root endpoint.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.NetworkClientexposed viaNodeLifecycle:NodeConnectionState(enum values pinned to legacyNodeStates:Connected=0, Disconnected=1, Reconnecting=2, WaitingForDeviceDiscovery=3),connectionState,connectionStateChanged,isConnected.Disconnected= stopped/not-started/disabled; lifecycle on start isDisconnected → Reconnecting → Connected.Deprecation
The legacy
@project-chip/matter.jscontroller/device API (CommissioningController,PairedNode,Device/Endpoint/Aggregator/ComposedDevice, the cluster clients, and their public members — ~330@deprecatedtags) is marked deprecated for removal in 0.19, pointing atdocs/MIGRATION_CONTROLLER_018.md(WIP guide included here).Testing
Full gate green at HEAD:
build --clean,format-verify,lint, and package suites —@matter/types374/374,@matter/protocol1450/1450,@matter/node1471/1471,@project-chip/matter.js5/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 —
ClusterNamingrelocation 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.adddedup).Follow-ups (not in this PR)
likelyOfflinetrigger — needs a newPeer-level observable; connection state currently degrades toReconnectingin that window (mDNS still recovers).Peers.#instrumentBasicInformationregisters non-self-disposing.onhandlers (pre-existing) that could accumulate ifclusterInstalledre-emits.MIGRATION_CONTROLLER_018.mdsections (events bus, subscription examples, FAQ) alongside the shell + server migrations.🤖 Generated with Claude Code