[WIP] Node Manager: desired-state, reconciliation & multi-step tasks - #3948
Draft
Apollon77 wants to merge 89 commits into
Draft
[WIP] Node Manager: desired-state, reconciliation & multi-step tasks#3948Apollon77 wants to merge 89 commits into
Apollon77 wants to merge 89 commits into
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>
…edStateBehavior Drop the second `, unknown` type param from both Observable declarations in DesiredStateBehavior.Events to match the codebase convention. Convert the two test handlers that return number (Array.push) to block bodies so they type-check under the stricter void return. Remove the inline WHAT comment above static schema. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add export * from "./desired-state/index.js" to system behavior barrel - Add DesiredStateBehavior to ClientNode.RootEndpoint.with(...) - New DesiredStatePersistenceTest: verifies registration on ClientNode and intent persistence across a node restart via shared Environment storage - Update PEER1_STATE in ClientNodeTest to include desiredState initial state Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add test coverage for GroupCapacityExceededError mapping and remove the unused unknownKindMapped fixture entry from the cache. 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>
Re-emit a peer's BasicInformation softwareVersion change as a node-level lifecycle signal, wired into Peers BasicInformation instrumentation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure status x mode branch-table function, internal to the package (consumed by the reconciler via #-import, not part of the public API). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opt-in ServerNode-root behavior driving reachable peers toward intended state: pure planActions decision + executeActions executor, six triggers (settle, sweep, peers add/del, subscription-active, intent-change, software-version-change), per-peer in-flight guard, capacity refresh, and asyncDispose cleanup. Reachability gated on an active sustained subscription. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion - Wire per-peer trigger handlers directly on the peer ObserverGroup instead of via this.callback, so they are torn down on peer removal (no reactor leak on peer churn). - #reachable mirrors NetworkClient.subscriptionActive: a sustained subscription counts as reachable only once active, not merely created. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconciler engine: @matter/node-manager package, ReconcilerBehavior (opt-in ServerNode-root, planActions decision + executeActions executor, six triggers, per-peer in-flight guard, capacity refresh, asyncDispose), plus @matter/node ephemeral capacity cache and softwareVersionChanged signal. 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>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rity bands Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng, dispose race Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ting Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ard test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tradeoff Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… unread Pre-flight admission stays meaningful instead of failing open; the device write remains the authoritative gate for over-capacity (RESOURCE_EXHAUSTED). 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>
…lFailed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… redundant comment Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g on stale state Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vert A slow forward apply() could write "committed" after a concurrent revert had flipped the intent to "deletePending", resurrecting the item the revert removed and silently undoing auto-rollback. Re-read live state after apply()/remove() and skip the status write when the intent transitioned, so a concurrent revert (or re-add) wins. Backstopped by the itemChanged follow-up reconcile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commissioned-peer scenarios: auto-rollback on hard failure, cancel revert handle, create-if-absent key-set provisioning, parked-revert resume across controller restart. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # packages/node/src/node/client/Peers.ts # packages/node/test/behaviors/group-key-management/GroupKeyManagementServerTest.ts # packages/node/test/node/ClientNodeTest.ts # packages/node/test/node/EventReadResponseTest.ts # packages/node/test/node/NodePhysicalPropertiesTest.ts
…roup on many endpoints
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o invariant Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prettier import ordering regressed when relative harness imports were repointed to @matter/node/testing during the main->node-manager merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he, not fallback Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ssion GroupMembershipItemKind.capacity counts distinct groups (groupTable length) against maxGroupsPerFabric, but admission counts per-endpoint membership keys (groupId:endpoint). Joining an already-mapped group on a new endpoint at the group-count limit would false-reject a valid operation. The group slot is already gated by GroupKeyMapItemKind; add ItemKind.excludeFromAdmission so a kind whose capacity counts a coarser resource opts out of admission while still serving the reconciler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # packages/node/test/node/ServerSubscriptionTest.ts
* feat(node-manager): groupKey write-if-set-differs (enables key rotation) * feat(node-manager): TaskContext.peersWithIntent for member derivation * feat(node-manager): RotateGroupKey task (gap-free 3-phase group-key rotation) * test(node-manager): 2-node group-key rotation harness (happy/park/restart/drift) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(node-manager): set revertOf at task creation, not post-run Thread revertOf into the persisted seed passed to TaskRegistry.create so the revert task's first persisted record already has it, instead of patching it onto the live task object after run() has already tracked/driven it. * test(node-manager): correct revertOf test comment to not overclaim The comment implied these assertions prove the pre-fix post-hoc assignment was racy; they don't discriminate old vs new code in the current call graph (revertOf is set synchronously before the revert's own first persist either way). Reword to describe the invariant actually being checked. * refactor(node-manager): narrow RunningTaskContext to ReconcilerSurface RunningTaskContext only ever calls itemKind() and reconcile() on the reconciler it's given, not the rest of ReconcilerBehavior's surface. Type the ctor param against a new 2-method ReconcilerSurface interface instead, so fakes in tests can implement it directly instead of casting through `as unknown as ReconcilerBehavior`. ReconcilerBehavior satisfies the new interface unchanged. * fix(node-manager): rotation is forward-only once cleanup begins (decline late cancel) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(node-manager): per-rotation id + revert only during distribute (PR review) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(node-manager): serialize rotations per key set (reject concurrent rotation) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(node-manager): rotation requires single-key steady state (validate, refuse loudly) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(node-manager): close cancel race at the distribute→activate boundary Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # packages/node/test/behavior/state/managed/values/StructManagerTest.ts # packages/node/test/behaviors/ota/OtaTest.ts # packages/node/test/node/ClientTuningTest.ts # packages/node/test/node/NodePhysicalPropertiesTest.ts # packages/node/test/node/ServerSubscriptionTest.ts
Contributor
There was a problem hiding this comment.
Pull request overview
Adds the WIP Node Manager foundation: persistent desired state, reconciliation, capacity handling, group orchestration, rollback, and shared node test utilities.
Changes:
- Adds desired-state behavior and lifecycle triggers to
@matter/node. - Introduces
@matter/node-managerwith reconcilers and persistent multi-step tasks. - Moves reusable node mocks into
@matter/node/testingand updates tests.
Verification: Not run during this review.
Reviewed changes
Copilot reviewed 173 out of 178 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
package.json |
Registers workspace package. |
package-lock.json |
Locks workspace package. |
tsconfig.json |
Adds project reference. |
packages/nodejs-shell/src/MatterNode.ts |
Reorders import. |
packages/node/package.json |
Exports testing utilities. |
packages/node/src/tsconfig.json |
Enables testing globals. |
packages/node/src/testing/index.ts |
Exports shared test helpers. |
packages/node/src/testing/mock-exchange.ts |
Adds exchange mock. |
packages/node/src/testing/mock-server-node.ts |
Adds server-node mock. |
packages/node/src/testing/mock-site.ts |
Adds network-site mock. |
packages/node/src/testing/node-helpers.ts |
Moves shared node helpers. |
packages/node/src/node/NodeLifecycle.ts |
Adds software-version event. |
packages/node/src/node/ClientNode.ts |
Mounts desired state. |
packages/node/src/node/client/Peers.ts |
Wires version changes. |
packages/node/src/behavior/system/index.ts |
Exports desired state. |
packages/node/src/behavior/system/desired-state/types.ts |
Defines desired-state records. |
packages/node/src/behavior/system/desired-state/errors.ts |
Defines typed errors. |
packages/node/src/behavior/system/desired-state/capacity.ts |
Adds capacity admission. |
packages/node/src/behavior/system/desired-state/ItemKind.ts |
Adds item-kind registry. |
packages/node/src/behavior/system/desired-state/DesiredStateBehavior.ts |
Implements intent storage. |
packages/node/src/behavior/system/desired-state/index.ts |
Exports desired-state API. |
packages/node/test/behavior/system/desired-state/TypesTest.ts |
Tests status/key helpers. |
packages/node/test/behavior/system/desired-state/ItemMapKeyTest.ts |
Tests key composition. |
packages/node/test/behavior/system/desired-state/ItemKindRegistryTest.ts |
Tests registry behavior. |
packages/node/test/behavior/system/desired-state/ErrorsTest.ts |
Tests error hierarchy. |
packages/node/test/behavior/system/desired-state/CapacityTest.ts |
Tests capacity checks. |
packages/node/test/behavior/system/desired-state/DesiredStateBehaviorTest.ts |
Tests intent operations. |
packages/node/test/behavior/system/desired-state/DesiredStatePersistenceTest.ts |
Tests persistence behavior. |
packages/node/test/node/ClientNodeTest.ts |
Updates expected desired state. |
packages/node/test/node/mock-exchange.ts |
Moves mock to source testing. |
packages/node/test/node/mock-server-node.ts |
Moves mock to source testing. |
packages/node/test/node/mock-site.ts |
Moves mock to source testing. |
packages/node/test/node/node-helpers.ts |
Moves helpers to source testing. |
packages/node/test/node/SplitCommissioningTest.ts |
Uses shared testing export. |
packages/node/test/node/ShutdownActivityTest.ts |
Uses shared testing export. |
packages/node/test/node/SetRegulatoryConfigSideEffectTest.ts |
Uses shared testing export. |
packages/node/test/node/ServerSubscriptionTest.ts |
Uses shared testing export. |
packages/node/test/node/ServerNodeTest.ts |
Uses shared testing export. |
packages/node/test/node/RegulatoryConfigCommissioningTest.ts |
Uses shared testing export. |
packages/node/test/node/read-helpers.ts |
Uses shared testing export. |
packages/node/test/node/PluginsTest.ts |
Uses shared testing export. |
packages/node/test/node/NodePhysicalPropertiesTest.ts |
Uses shared testing export. |
packages/node/test/node/NodeLifecycleSeededTest.ts |
Uses shared testing export. |
packages/node/test/node/FailsafeCommissioningTest.ts |
Uses shared testing export. |
packages/node/test/node/EventReadResponseTest.ts |
Uses shared testing export. |
packages/node/test/node/DecommissionTest.ts |
Uses shared testing export. |
packages/node/test/node/CommandInvokeResponseTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientTuningTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientSubscribeTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientStructureOffloadTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientStartUpTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientNodeTcpTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientNodePhysicalPropertiesTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientNodeConfigurationVersionTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientNestedStructStateTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientInvokeTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientEventNotificationTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientConnectivityTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientChunkedListTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientCacheBufferTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientAttestationTest.ts |
Uses shared testing export. |
packages/node/test/node/ClientAddressProbeTest.ts |
Uses shared testing export. |
packages/node/test/node/BitmapWriteValidationTest.ts |
Uses shared testing export. |
packages/node/test/node/AttributeWriteResponseTest.ts |
Uses shared testing export. |
packages/node/test/node/AttributeSubscriptionResponseTest.ts |
Uses shared testing export. |
packages/node/test/node/AttributeReadResponseTest.ts |
Uses shared testing export. |
packages/node/test/node/client/PeersInstrumentBridgedConfigurationVersionTest.ts |
Uses shared testing export. |
packages/node/test/node/client/PeersInstrumentBasicInformationTest.ts |
Uses shared testing export. |
packages/node/test/node/client/PeersCommissionedTest.ts |
Uses shared testing export. |
packages/node/test/node/client/PeersAutoStartTest.ts |
Uses shared testing export. |
packages/node/test/node/client/ClientNodePhysicalPropertiesIcdTest.ts |
Uses shared testing export. |
packages/node/test/node/client/ClientNodeInteractionIcdTest.ts |
Uses shared testing export. |
packages/node/test/endpoints/LargeBridgeTest.ts |
Uses shared testing export. |
packages/node/test/endpoints/ClusterClientIntegrationTest.ts |
Uses shared testing export. |
packages/node/test/endpoints/bridge-helpers.ts |
Uses shared testing export. |
packages/node/test/endpoint/server/ProtocolServiceTest.ts |
Uses shared testing export. |
packages/node/test/endpoint/server/InteractionTestUtils.ts |
Uses shared testing export. |
packages/node/test/endpoint/server/InteractionProtocolTest.ts |
Uses shared testing export. |
packages/node/test/endpoint/mock-endpoint.ts |
Uses shared testing export. |
packages/node/test/endpoint/EndpointVariableServiceTest.ts |
Uses shared testing export. |
packages/node/test/endpoint/EndpointTest.ts |
Uses shared testing export. |
packages/node/test/endpoint/EndpointGetServerTest.ts |
Uses shared testing export. |
packages/node/test/endpoint/EndpointGetClientTest.ts |
Uses shared testing export. |
packages/node/test/endpoint/EndpointClientFeaturesTest.ts |
Uses shared testing export. |
packages/node/test/behavior/ReactorTransactionDeadlockTest.ts |
Uses shared testing export. |
packages/node/test/behavior/state/managed/values/StructManagerTest.ts |
Uses shared testing export. |
packages/node/test/behavior/state/managed/values/ListManagerTest.ts |
Uses shared testing export. |
packages/node/test/behavior/system/sessions/SessionsBehaviorTest.ts |
Uses shared testing export. |
packages/node/test/behavior/system/network/ConnectionStateTest.ts |
Uses shared testing export. |
packages/node/test/behavior/system/icd/litSupportedTest.ts |
Uses shared testing export. |
packages/node/test/behavior/system/icd/IcdClientTest.ts |
Uses shared testing export. |
packages/node/test/behavior/system/commissioning/CommissioningWindowTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/access-control/AccessControlCustomExtensionTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/basic-information/CapabilityMinimaTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/basic-information/ConfigurationVersionTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/binding/BindingIntegrationTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/binding/BindingManagerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/binding/BindingServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/boolean-state/BooleanStateServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/color-control/ColorControlServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/concentration-measurement/ConcentrationMeasurementServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/descriptor/DescriptorServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/energy-evse-mode/EnergyEvsModeServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/fan-control/FanControlTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/general-diagnostics/GeneralDiagnosticsFeaturesTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/group-key-management/GroupKeyManagementServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/groupcast/GroupcastProvisionalTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/groups/GroupsServerSchemaTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/hepa-filter-monitoring/HepaFilterMonitoringServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/icd-management/IcdManagementServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/identify/IdentifyServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/level-control/LevelControlServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/occupancy-sensing/OccupancySensingServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/on-off/OnOffServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/ota/OtaTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/ota/ota-utils.ts |
Uses shared testing export. |
packages/node/test/behaviors/power-source/PowerSourceServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/rvc-operational-state/RvcOperationalStateServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/scenes-management/ScenesManagementServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/service-area/ServiceAreaServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/thermostat/AtomicWriteHandlerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/thermostat/ThermostatBehaviorTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/thermostat-user-interface-configuration/ThermostatUserInterfaceConfgurationServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/thread-network-diagnostics/ThreadNetworkDiagnosticsProvisionalTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/time-format-localization/TimeFormatLocalizationServerTest.ts |
Uses shared testing export. |
packages/node/test/behaviors/unit-localization/UnitLocalizationServerTest.ts |
Uses shared testing export. |
packages/node-manager/package.json |
Defines public package. |
packages/node-manager/tsconfig.json |
Defines composite project. |
packages/node-manager/src/tsconfig.json |
Configures library build. |
packages/node-manager/test/tsconfig.json |
Configures test build. |
packages/node-manager/.mocharc.cjs |
Configures test runner. |
packages/node-manager/src/index.ts |
Defines public exports. |
packages/node-manager/src/ReconcilerBehavior.ts |
Implements reconciliation triggers. |
packages/node-manager/src/reconcile/ReconcilerSurface.ts |
Defines task-facing interface. |
packages/node-manager/src/reconcile/priority.ts |
Defines action priorities. |
packages/node-manager/src/reconcile/planActions.ts |
Plans reconciliation actions. |
packages/node-manager/src/reconcile/executeActions.ts |
Executes reconciliation actions. |
packages/node-manager/src/reconcile/acl-coverage.ts |
Implements ACL coverage. |
packages/node-manager/src/reconcile/AclItemKind.ts |
Reconciles ACL entries. |
packages/node-manager/src/reconcile/BindingItemKind.ts |
Reconciles bindings. |
packages/node-manager/src/reconcile/GroupKeyItemKind.ts |
Reconciles group keys. |
packages/node-manager/src/reconcile/GroupKeyMapItemKind.ts |
Reconciles group mappings. |
packages/node-manager/src/reconcile/GroupMembershipItemKind.ts |
Reconciles memberships. |
packages/node-manager/src/task/types.ts |
Defines task contracts. |
packages/node-manager/src/task/errors.ts |
Defines task errors. |
packages/node-manager/src/task/Task.ts |
Defines task base class. |
packages/node-manager/src/task/TaskRegistry.ts |
Registers task types. |
packages/node-manager/src/task/RunningTaskContext.ts |
Implements task gates. |
packages/node-manager/src/task/TaskManagerBehavior.ts |
Drives persistent tasks. |
packages/node-manager/src/task/Revert.ts |
Implements rollback. |
packages/node-manager/src/task/groups/keys.ts |
Defines membership keys. |
packages/node-manager/src/task/groups/AddNodeToGroup.ts |
Implements group addition. |
packages/node-manager/src/task/groups/RemoveNodeFromGroup.ts |
Implements group removal. |
packages/node-manager/src/task/groups/RotateGroupKey.ts |
Implements key rotation. |
packages/node-manager/test/ReconcilerBehaviorTest.ts |
Tests reconciliation executor. |
packages/node-manager/test/ReconcilerIntegrationTest.ts |
Tests ACL reconciliation. |
packages/node-manager/test/BindingIntegrationTest.ts |
Tests binding reconciliation. |
packages/node-manager/test/GroupKeyIntegrationTest.ts |
Tests key reconciliation. |
packages/node-manager/test/GroupMembershipIntegrationTest.ts |
Tests membership reconciliation. |
packages/node-manager/test/reconcile/PlanActionsTest.ts |
Tests action planning. |
packages/node-manager/test/reconcile/AclCoverageTest.ts |
Tests ACL coverage. |
packages/node-manager/test/reconcile/AclItemKindTest.ts |
Tests ACL item kind. |
packages/node-manager/test/reconcile/BindingItemKindTest.ts |
Tests binding item kind. |
packages/node-manager/test/reconcile/GroupKeyItemKindTest.ts |
Tests key item kind. |
packages/node-manager/test/reconcile/GroupKeyMapItemKindTest.ts |
Tests mapping item kind. |
packages/node-manager/test/reconcile/GroupMembershipItemKindTest.ts |
Tests membership item kind. |
packages/node-manager/test/task/helpers.ts |
Adds task test doubles. |
packages/node-manager/test/task/AdmissionTest.ts |
Tests capacity admission. |
packages/node-manager/test/task/ChangesetTest.ts |
Tests prior-state capture. |
packages/node-manager/test/task/PeersWithIntentTest.ts |
Tests member discovery. |
packages/node-manager/test/task/RemoveIntentTest.ts |
Tests referenced removal. |
packages/node-manager/test/task/RevertTaskTest.ts |
Tests rollback behavior. |
packages/node-manager/test/task/RollbackIntegrationTest.ts |
Tests automatic rollback. |
packages/node-manager/test/task/TaskContextGateTest.ts |
Tests task gates. |
packages/node-manager/test/task/TaskLifecycleTest.ts |
Tests resume and cancellation. |
packages/node-manager/test/task/TaskManagerBehaviorTest.ts |
Tests task management. |
packages/node-manager/test/task/groups/AddNodeToGroupIntegrationTest.ts |
Tests group addition. |
packages/node-manager/test/task/groups/RemoveNodeFromGroupTest.ts |
Tests group removal logic. |
packages/node-manager/test/task/groups/RemoveNodeFromGroupIntegrationTest.ts |
Tests group removal. |
packages/node-manager/test/task/groups/RotateGroupKeyTest.ts |
Tests key rotation. |
packages/node-manager/test/task/groups/RotateGroupKeyIntegrationTest.ts |
Tests multi-node rotation. |
packages/node-manager/test/task/groups/RollbackIntegrationTest.ts |
Tests group rollback. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+36
to
+38
| export function itemMapKey(kind: string, key: string): string { | ||
| return `${kind}${ITEM_KEY_SEPARATOR}${key}`; | ||
| } |
| removes.sort((a, b) => priority(b.item, registry) - priority(a.item, registry)); | ||
|
|
||
| for (const { item, action } of [...others, ...removes]) { | ||
| const kind = registry.get(item.kind); |
Comment on lines
+86
to
+88
| async awaitCommitted(items: Array<{ peer: ClientNode; kind: string; key: string }>): Promise<void> { | ||
| const peers = [...new Set(items.map(i => i.peer))]; | ||
| await this.awaitGate(peers, () => items.every(i => this.#itemState(i.peer, i.kind, i.key) === "committed")); |
Comment on lines
+53
to
+57
| async setIntent(peer: ClientNode, kind: string, key: string, intent: unknown, mode: ItemMode = "converge") { | ||
| this.#record(peer, kind, key); | ||
| await peer.act(agent => { | ||
| agent.get(DesiredStateBehavior).setIntent(kind, key, intent, mode); | ||
| }); |
Comment on lines
+148
to
+151
| const rk = task.resourceKey(); | ||
| if (rk !== undefined) { | ||
| for (const t of this.internal.live.values()) { | ||
| if (t.id !== task.id && !TERMINAL_STATES.has(t.progress.state) && this.#occupies(t, rk)) { |
Comment on lines
+2
to
+4
| "name": "@matter/node-manager", | ||
| "version": "0.0.0-git", | ||
| "description": "Controller-side node management (desired-state reconciliation) for matter.js", |
Comment on lines
+9
to
+10
| export * from "./task/groups/AddNodeToGroup.js"; | ||
| export * from "./task/Task.js"; |
Comment on lines
+5
to
+17
| "references": [ | ||
| { | ||
| "path": "../../general/src" | ||
| }, | ||
| { | ||
| "path": "../../node/src" | ||
| }, | ||
| { | ||
| "path": "../../testing/src" | ||
| }, | ||
| { | ||
| "path": "../../types/src" | ||
| } |
Comment on lines
+28
to
+32
| "dependencies": { | ||
| "@matter/general": "*", | ||
| "@matter/node": "*", | ||
| "@matter/types": "*" | ||
| }, |
Comment on lines
+10
to
+24
| "references": [ | ||
| { | ||
| "path": "../../general/src" | ||
| }, | ||
| { | ||
| "path": "../../node/src" | ||
| }, | ||
| { | ||
| "path": "../../testing/src" | ||
| }, | ||
| { | ||
| "path": "../../types/src" | ||
| }, | ||
| { | ||
| "path": "../src" |
* fix(node-manager): task cancel liveness and lifecycle hardening The task layer's abort intent lived only on a per-task gate object that the driver created late, so a cancel arriving before the first phase built its gate was discarded: the task went on to write its intents to the peer after the cancel was accepted, parked on a peer that would never commit, and cancel() never settled. The gate is now created before driving starts, and the driver checks for a recorded abort after admission and again after the phase context is built -- a check at the loop top still let a cancel arriving during the act pass through to a device write. A cancel that overlapped shutdown was lost entirely: the drive unwound without persisting, and cancel()'s continuation then marked the task cancelled in memory and spawned a rollback the dispose drain had already passed, persisting into a mutex about to close. Storage kept the task non-terminal with no rollback, so the next start re-applied exactly what was cancelled. cancel() now refuses with TaskManagerClosingError once the endpoint leaves the active state, and writes the cancelled record and the rollback record in one transaction. A task that fails during teardown is left resumable rather than rolled back outside the drain, and a task type registered during teardown no longer resumes into it. internal.live had no terminal lifecycle, so re-running a task after a cancel returned the cancelled handle: no phase ran, nothing persisted, no error was raised, and the change was never applied until the process restarted. Terminal ids are now reusable, dedup applies only to a live non-terminal task with the same request, a differing request is rejected rather than silently ignored, and terminal tasks are retained as bounded observable history in both memory and storage. Parameter sameness compares storage-shaped values, because the persistence codec drops undefined-valued properties at every depth and leaves an undefined array element as a hole. A commit gate only ever observed success, so an intent the reconciler dropped after an unrecoverable device rejection parked its task forever and blocked the automatic rollback. Such a gate now fails with TaskFailedError. Underneath that, a dropped item announces itself on itemRemoved while the gate watched only itemChanged, so a parked gate had no wake source at all. A gate also evaluated its predicate once before registering the observers it parks on, so a change, removal or reachability flip arriving in between was announced before anything listened and the gate parked with the event it needed already gone. Registration now precedes the first evaluation, which reaches the same coalescing path as any later wakeup, so the window closes without adding a verify-reconcile to every park. A synchronous failure of that first evaluation closes the observers instead of leaking them, and an evaluation completing after the gate settled no longer starts a follow-up reconcile that would race the rollback a cancel spawns next. An externalId supplied by a caller that deduped onto an existing task was discarded, leaving that caller with an id resolving to nothing and a cancel indistinguishable from "nothing to revert". Tasks now carry the set of external ids that claim them, a conflicting claim is refused, and cancel() distinguishes an unknown id from a task with nothing to revert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(node-manager): refuse to activate a rotation member that joined after distribute The member set is re-derived per phase, which is what lets a peer whose intent was meanwhile removed drop out of the barrier. The cost is the opposite case: a peer that gains an intent for the key set between distribute and activate joins the barrier without ever having received the new key, so once members that already committed activate flip their transmit key it cannot decrypt their traffic -- precisely the gap the three-phase barrier exists to close. activate now refuses unless every current member already carries this rotation's new key in slot 1, which also covers the park/resume re-drive where a member has already committed activate. The distributed keys stay dormant, so a rotation with a fresh id recovers. A member joining at cleanup needs no guard: it converges onto the surviving key with no gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(node-manager): keep a task handle answering for its task TaskHandle.status was evaluated once when the handle was built, so a caller that held the handle and polled it observed the state frozen at creation time -- a task that had since completed or failed still read as running. The handle now reads through to the task it names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(node-manager): decide task admission by id, not by comparing parameters Deciding whether two requests are "the same request" by structurally comparing their parameters cannot be made correct. The comparator had to stay bit-exact with the storage codec, with a deep-equality helper that cannot compare a Map at all, and with a storage backend that keeps raw references in memory but JSON on disk -- three moving targets synchronized by hand. It produced a defect in four consecutive review rounds: any Map comparing equal to any other, arrays with undefined elements mismatching, and finally a Map comparing equal to an equivalent plain array so that two different requests deduplicated onto one task. Admission is now decided by the task id alone. Only one live task may hold an id: the caller that passes an externalId re-issues its own request idempotently, and any other request for a held id is refused with TaskConflictError rather than silently resolving onto work it did not ask for. This deliberately narrows the earlier promise that an anonymous identical re-issue is idempotent -- a refusal naming the live task is more honest than a guess about equivalence. Terminal-task retention goes with it. Retention bounded a history that the keying cannot keep: records are stored per task id and last-writer-wins, so re-running a task overwrites its finished record, and cancelling that re-run overwrites the previous rollback's record too. Bounding storage is worth doing once history has a representation that survives a repeat of the same work. Per-task sets of external ids go back to the single optional id they were: a second caller's id was accepted, resolved to the first caller's task, and left the second caller unable to observe or cancel what it asked for. What a terminal id no longer does is block a re-run -- that fix stays, because without it re-running after a cancel returned the cancelled handle and the requested change was never applied until a process restart. Identity, lookup and retention are being redesigned together around per-run ids, a slot key that carries exclusion, and history separate from the live table. This removes what that redesign replaces so the rest of the branch -- cancellation liveness, shutdown handling and gate wakeups -- lands on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(node-manager): record a rollback before it drives, and refuse a cancel the write cannot record A rollback began mutating peers before the write that names it was durable, and the lifecycle check that guards a cancel ran before its write was enqueued rather than when the write ran. Shutdown starting in that gap left the task cancelled in memory and linked to a rollback already driving, while storage still held the old non-terminal record and the caller saw a lifecycle error instead of TaskManagerClosingError. Creating a rollback is now separate from driving it. A cancel prepares the rollback, writes both records in one transaction, and only then starts it; a write that cannot land undoes the state change, forgets the prepared rollback and leaves nothing behind for the next start to resume. The refusal moved inside the serialized write, so a shutdown that begins while the write queues cannot slip past it. Auto-rollback in the driver follows the same order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(node-manager): refuse to complete a rotation a member joined during activate The activate phase checked its member set only on entry. Writing the intents and waiting on the barrier both yield, and provisioning a group takes no lock on its key set, so a peer could gain an intent afterwards: absent from the member set it never received the new key, and cleanup was the first phase to touch it. Between activate completing and that peer committing cleanup, the members that already flipped transmit with the new key and it cannot decrypt them. The condition is now re-established after the barrier, so the rotation fails instead of proceeding to cleanup and reporting success. It refuses rather than heals: the old key is still present on the members that flipped, and rotating again with the same new key covers every current member because distribute takes the whole set. Preventing the window would mean serializing group provisioning against a live rotation, which is too coarse -- legitimate concurrent joins to different groups sharing a key set would reject each other -- and belongs with the broader identity and exclusion redesign. Cleanup needs no equivalent guard: a peer joining there receives the sole surviving key back-dated to the original operational start, so it converges with no gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(node-manager): keep a task driveable when its cancel is refused Cancelling stops the driver and drops its gate before the rollback is prepared, so a rollback the manager refuses -- one whose id a live rollback already holds -- left the task reporting running or parked with nothing to advance it. It kept its state, as intended, but lost its driver, and only a restart brought it back. A declined cancel now re-drives the task before rethrowing, sharing the path the resume pass uses. The test that claimed the task was left untouched asserted only its state; it now also asserts the task still owns a driver, which is what makes "untouched" true. The rotationId documentation promised that re-issuing the same id is idempotent. Since admission became a matter of the task id alone that holds only for the caller whose externalId the live rotation carries; a terminal id may be run again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(node-manager): assert a task declined a cancel still converges The test asserted the task kept a driver but stopped there, so it did not show the driver still leads anywhere. It now lets the device have the item and waits for the task to complete, which is only reachable if declining the cancel restored both the driver and its gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Umbrella / integration branch for the Node Manager feature. Long-lived WIP PR — each phase merges into
node-managervia its own sub-PR; this PR is the CI backstop for the integrated branch againstmain. Do not merge until all phases land and it's de-WIP'd.Design doc:
docs/superpowers/specs/2026-06-14-node-manager-desired-state-design.mdWhat this builds
A controller-side layer holding the intended state of fabric nodes (certs/keys/IPK, group keys, bindings, ACLs, group membership), with offline-tolerant reconciliation and multi-step orchestration (e.g. group-key rotation). Modeled on the JointFabric Datastore cluster (0x0752) and generalized.
Phase checklist
@matter/node): ManagedItem/StatusEntry, ItemKind registry, capacity admission + typed errors, persistentDesiredStateBehavioronClientNode(Node Manager Phase 1: Tier-1 desired-state model #3946)@matter/node-managerpkg): triggers, settle delay, verify-barrier, priority ordering, capacity reads, concrete ItemKinds;RotateGroupKey/MoveNodeToGroup; changeset rollback; 2-node rotation harnessCarry-forwards tracked for Phase 2
limitonly, deriveusedfresh at admission.itemMapKeyseparator unescaped — escape/document before non-identifier keys.🤖 Generated with Claude Code