@adhdev/daemon-core 0.9.82-rc.515 → 0.9.82-rc.516

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,37 +13,21 @@ import { type NodeCapabilitySlot } from '@adhdev/mesh-shared';
13
13
  /** Ordered, de-duplicated providerPriority from a node policy (defensive). */
14
14
  export declare function normalizeProviderPriority(policy: unknown): string[];
15
15
  /**
16
- * Resolve a node's capability slots with EXPLICIT self-vs-remote precedence
17
- * (REMOTE-NODE-SLOT-CAP-CHIP fix, Direction B).
16
+ * Resolve a node's capability slots coordinator-owned config is the single source
17
+ * of truth for ALL nodes (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix).
18
18
  *
19
- * Precedence, avoiding the precedence-inversion a blind `policy.slots || mirror`
20
- * would introduce:
21
- * 1. Local `policy.slots` authoritative for the SELF/locally-owned node (the
22
- * node owns its own config locally). This ALSO wins for a remote node in the
23
- * unusual case an operator configured slots on the coordinator side.
24
- * 2. `reportedMemberState.slots` — the remote member's OWN resolved slots,
25
- * mirrored wholesale over P2P. Authoritative for a REMOTE mirrored node whose
26
- * coordinator-side policy copy is the stale join-time empty `{}` (which
27
- * normalizes to []). This is the field the coordinator's join-time copy lacks
28
- * — the reason remote slot-cap chips were missing.
29
- * 3. Legacy-derived from `providerPriority` + machine-global difficultyBrains.
19
+ * The coordinator's local meshes.json owns `policy.slots` for every node in the mesh
20
+ * (self AND remote members alike — a remote member's mesh config lives on the
21
+ * coordinator, its own on-disk meshes.json is empty). So slots resolve directly from
22
+ * the coordinator's locally-owned `node.policy.slots`, with no remote reporter
23
+ * round-trip: there is no per-node "reported slots" mirror to consult.
24
+ *
25
+ * Precedence:
26
+ * 1. `policy.slots` coordinator-owned, authoritative for self and remote alike.
27
+ * 2. Legacy-derived from `providerPriority` + machine-global difficultyBrains.
30
28
  *
31
- * The mirror only ever fills the gap left by an empty local policy — it can never
32
- * shadow a populated local policy — so there is no inversion. And a self node never
33
- * carries a reportedMemberState (the ingest path stamps it ONLY for remote
34
- * members), so a self node's local policy is always used directly.
35
29
  * (The former per-provider `providerRoles` cap has been removed; a persisted
36
30
  * meshes.json is migrated to slots on load, so by the time a node reaches routing
37
31
  * its cap already lives on `slots[].maxParallel`.)
38
32
  */
39
33
  export declare function resolveNodeCapabilitySlots(node: any): NodeCapabilitySlot[];
40
- /**
41
- * Reporter-side helper (Direction B): resolve THIS daemon's own capability slots for
42
- * a given workspace by finding the local meshes.json node whose workspace matches
43
- * and running the same {@link resolveNodeCapabilitySlots} over its real local policy.
44
- * Used at the git_status reporter wiring so a member node self-reports its own
45
- * resolved slots inside the unified reporterMemberState envelope. Returns [] when no
46
- * local node matches the workspace or slots can't be resolved (best-effort, never
47
- * throws) so the reporter simply omits the field.
48
- */
49
- export declare function resolveLocalNodeSlotsForWorkspace(workspace: string | undefined): NodeCapabilitySlot[];
@@ -301,35 +301,28 @@ export interface RepoMeshRelatedRepo {
301
301
  * (see migrateLoadedMeshConfig).
302
302
  */
303
303
  /**
304
- * Direction-B unified mirrored member state (REMOTE-NODE-SLOT-CAP-CHIP fix).
304
+ * Unified mirrored member state — the per-machine RUNTIME facts a remote member
305
+ * self-reports (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix).
305
306
  *
306
307
  * A single self-reported observability envelope stamped by the daemon that OWNS a
307
308
  * node's workspace and carried wholesale on the git_status envelope
308
- * (reporterMemberState), replacing the fragmented per-field self-heal that mirrored
309
- * providerVersions and daemonBuildVersion ad-hoc but silently dropped `slots`. The
310
- * coordinator ingests this whole object onto the mirrored remote node in one place
309
+ * (reporterMemberState), consolidating the previously per-field self-heal of
310
+ * providerVersions and daemonBuildVersion. The coordinator ingests this whole object
311
+ * onto the mirrored remote node in one place
311
312
  * (mesh-node-identity.recordInlineMeshDirectGitTruth).
312
313
  *
313
- * Source-of-truth model: EACH NODE owns its own slots/versions locally; the
314
- * coordinator is a pure mirror of a remote member's reported state. Its own
315
- * join-time `policy` copy for a remote member is a stale empty `{}` and must never
316
- * shadow this mirror (that would be a precedence inversion — see
317
- * resolveNodeCapabilitySlots).
314
+ * Source-of-truth model: provider VERSIONS + daemon BUILD are per-machine runtime
315
+ * facts (e.g. a member has claude-cli@2.1.168 while the coordinator has @2.1.170), so
316
+ * they are reported here. SLOTS are NOT reported they are coordinator-owned config
317
+ * (node.policy.slots for every node, self and remote alike; see
318
+ * resolveNodeCapabilitySlots), a single source of truth on the coordinator with no
319
+ * reporter round-trip.
318
320
  */
319
321
  export interface MeshReportedMemberState {
320
322
  /** Reporter node's detected provider CLI/ACP versions (keyed by provider id). */
321
323
  providerVersions?: Record<string, string>;
322
324
  /** Reporter daemon's build version (getDaemonBuildInfo().version). */
323
325
  daemonBuildVersion?: string;
324
- /**
325
- * Reporter node's OWN resolved capability slots (resolveNodeCapabilitySlots over
326
- * its LOCAL real policy). This is the field the coordinator's join-time policy
327
- * copy lacks for a remote member, and the reason remote slot-cap chips were
328
- * missing. For a remote mirrored node this is authoritative over the (empty)
329
- * local policy.slots; for the coordinator's own node the local policy is used
330
- * directly (never mirrored here).
331
- */
332
- slots?: NodeCapabilitySlot[];
333
326
  /** Epoch ms the report was stamped; used for mirror staleness reasoning. */
334
327
  lastReportedAt?: number;
335
328
  }
@@ -670,19 +663,15 @@ export interface LocalMeshNodeEntry {
670
663
  /**
671
664
  * Unified mirrored "member state" self-reported by the daemon that owns this
672
665
  * node's workspace, carried wholesale on the git_status envelope
673
- * (reporterMemberState) and ingested by the coordinator in one place. This is
674
- * the Direction-B consolidation of the previously per-field self-heal
675
- * (reportedProviderVersions, reportedDaemonBuildVersionand now the node's
676
- * OWN resolved capability `slots`, which the coordinator's join-time policy copy
677
- * for a remote member does NOT carry). Each node owns its own slots/versions
678
- * locally; the coordinator is a pure mirror of what a remote member reports over
679
- * P2P it must never treat its stale join-time policy copy as authoritative for
680
- * a remote node's slots. The legacy flat fields above stay populated in parallel
681
- * during rollout for back-compat (see mesh-node-identity ingest). `slots` is the
682
- * reporter node's own resolved capability slots (resolveNodeCapabilitySlots over
683
- * its LOCAL real policy). `lastReportedAt` is the epoch ms the report was
684
- * stamped, so a partitioned node's mirror does not linger as fresh forever.
685
- * Absent until the first envelope carrying it is ingested.
666
+ * (reporterMemberState) and ingested by the coordinator in one place. It carries
667
+ * the per-machine RUNTIME facts (reportedProviderVersions, reportedDaemonBuildVersion)
668
+ * consolidated into one envelope NOT slots, which are coordinator-owned config
669
+ * resolved from node.policy.slots for every node, not reported
670
+ * (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix). The legacy flat fields above stay
671
+ * populated in parallel during rollout for back-compat (see mesh-node-identity
672
+ * ingest). `lastReportedAt` is the epoch ms the report was stamped, so a
673
+ * partitioned node's mirror does not linger as fresh forever. Absent until the
674
+ * first envelope carrying it is ingested.
686
675
  */
687
676
  reportedMemberState?: MeshReportedMemberState;
688
677
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.515",
3
+ "version": "0.9.82-rc.516",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "0.9.82-rc.515",
51
- "@adhdev/session-host-core": "0.9.82-rc.515",
50
+ "@adhdev/mesh-shared": "0.9.82-rc.516",
51
+ "@adhdev/session-host-core": "0.9.82-rc.516",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -37,7 +37,6 @@ import { loadConfig } from '../config/config.js';
37
37
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
38
38
  import type { IdeProviderInstance } from '../providers/ide-provider-instance.js';
39
39
  import { createDefaultGitCommandServices } from '../git/git-commands.js';
40
- import { resolveLocalNodeSlotsForWorkspace } from '../mesh/mesh-node-slots.js';
41
40
  import { setupMeshEventForwarding } from '../mesh/mesh-events.js';
42
41
  import { setupMeshReconcileLoop } from '../mesh/mesh-reconcile-loop.js';
43
42
  import { loadMeshCoordinatorRegistry } from '../mesh/coordinator-registry.js';
@@ -313,24 +312,26 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
313
312
  instanceManager,
314
313
  sessionRegistry,
315
314
  gitCommandServices: createDefaultGitCommandServices({
316
- // T7 + Direction-B: fold this daemon's cached provider versions + build
317
- // version AND its OWN resolved capability slots for the reported workspace
318
- // onto the git_status envelope so the mesh coordinator self-heals each
319
- // node's providerVersions AND its slot-cap chips (REMOTE-NODE-SLOT-CAP-CHIP
320
- // fix). Non-blocking: reads a TTL cache + local meshes.json, lazily
321
- // refreshed. The slots the reporter resolves are its LOCAL real policy's
322
- // slots the coordinator mirrors them wholesale for a remote member whose
323
- // join-time policy copy is empty. (setDefaultProviderLoader is registered
324
- // above so getCachedProviderVersions/slot resolution is not empty — see
325
- // the 380556d3 version-chip lesson.)
326
- getReporterProviderVersions: (workspace?: string) => {
315
+ // T7: fold this daemon's cached provider versions + build version onto the
316
+ // git_status envelope so the mesh coordinator self-heals each node's
317
+ // providerVersions/build (the blue version chips) these are per-machine
318
+ // RUNTIME facts (e.g. this node has claude-cli@2.1.168 while the
319
+ // coordinator has @2.1.170), so they stay reported. Non-blocking: reads a
320
+ // TTL cache, lazily refreshed. (setDefaultProviderLoader is registered
321
+ // above so getCachedProviderVersions is not empty see the 380556d3
322
+ // version-chip lesson.)
323
+ //
324
+ // Slots are NOT reported: they are coordinator-owned config
325
+ // (node.policy.slots for every node, resolved via
326
+ // resolveNodeCapabilitySlots), not a per-machine runtime fact — the
327
+ // reporter round-trip for slots was removed (REMOTE-NODE-SLOTS-
328
+ // COORDINATOR-LOCAL fix).
329
+ getReporterProviderVersions: () => {
327
330
  const providerVersions = getCachedProviderVersions(providerLoader);
328
331
  const daemonBuildVersion = getDaemonBuildInfo().version;
329
- const slots = resolveLocalNodeSlotsForWorkspace(workspace);
330
332
  return {
331
333
  ...(Object.keys(providerVersions).length > 0 ? { providerVersions } : {}),
332
334
  ...(daemonBuildVersion && daemonBuildVersion !== 'unknown' ? { daemonBuildVersion } : {}),
333
- ...(slots.length > 0 ? { slots } : {}),
334
335
  };
335
336
  },
336
337
  }),
@@ -644,10 +644,12 @@ export function updateNode(
644
644
  * is overwritten by the next report. */
645
645
  reportedProviderVersions?: Record<string, string>;
646
646
  reportedDaemonBuildVersion?: string;
647
- /** Direction-B unified mirrored member state (versions + build + own resolved
648
- * slots + lastReportedAt) self-reported by the owning daemon on the git_status
649
- * envelope. Persisted wholesale so a remote node's slot-cap chips survive a
650
- * coordinator restart, mirroring the per-field reported* self-heal. */
647
+ /** Unified mirrored member state (per-machine runtime facts: provider versions
648
+ * + daemon build + lastReportedAt) self-reported by the owning daemon on the
649
+ * git_status envelope. Persisted wholesale so a remote node's version chips
650
+ * survive a coordinator restart, mirroring the per-field reported* self-heal.
651
+ * Slots are NOT carried — they are coordinator-owned config
652
+ * (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix). */
651
653
  reportedMemberState?: MeshReportedMemberState;
652
654
  },
653
655
  ): LocalMeshNodeEntry | undefined {
@@ -112,14 +112,6 @@ export interface GitCommandServices {
112
112
  getReporterProviderVersions?: (workspace?: string) => {
113
113
  providerVersions?: Record<string, string>;
114
114
  daemonBuildVersion?: string;
115
- /**
116
- * Direction-B: the reporter node's OWN resolved capability slots
117
- * (resolveNodeCapabilitySlots over its LOCAL policy for this workspace). Carried
118
- * inside the unified reporterMemberState so a remote node's slot-cap chips
119
- * self-heal on the coordinator the same way versions do. Best-effort; omitted
120
- * when the reporter cannot resolve slots for the workspace.
121
- */
122
- slots?: unknown[];
123
115
  };
124
116
  }
125
117
 
@@ -135,11 +127,13 @@ type GitCommandSuccess =
135
127
  // node's userOverrides.platform/arch (the fields capability-tag routing reads).
136
128
  // reporterMachineNickname carries the responding daemon's config.machineNickname
137
129
  // so the coordinator can populate node.machineNickname → the friendly display label.
138
- // reporterMemberState is the Direction-B unified envelope (versions + build + the
139
- // reporter node's own resolved capability slots + lastReportedAt). The legacy flat
130
+ // reporterMemberState is the unified envelope carrying the reporter node's
131
+ // per-machine RUNTIME facts (provider versions + daemon build + lastReportedAt).
132
+ // Slots are NOT carried here — they are coordinator-owned config, not reported
133
+ // (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix). The legacy flat
140
134
  // reporterProviderVersions/reporterDaemonBuildVersion fields are ALSO emitted for
141
135
  // back-compat during a mixed-version-mesh rollout.
142
- | { success: true; status: GitRepoStatus; reporterPlatform?: string; reporterArch?: string; reporterMachineNickname?: string; reporterProviderVersions?: Record<string, string>; reporterDaemonBuildVersion?: string; reporterMemberState?: { providerVersions?: Record<string, string>; daemonBuildVersion?: string; slots?: unknown[]; lastReportedAt?: number } }
136
+ | { success: true; status: GitRepoStatus; reporterPlatform?: string; reporterArch?: string; reporterMachineNickname?: string; reporterProviderVersions?: Record<string, string>; reporterDaemonBuildVersion?: string; reporterMemberState?: { providerVersions?: Record<string, string>; daemonBuildVersion?: string; lastReportedAt?: number } }
143
137
  | { success: true; diffSummary: GitDiffSummary }
144
138
  | { success: true; diff: GitFileDiff }
145
139
  | { success: true; snapshot: GitSnapshot }
@@ -374,20 +368,17 @@ export async function handleGitCommand(
374
368
  typeof reporterVersions.daemonBuildVersion === 'string' && reporterVersions.daemonBuildVersion.trim()
375
369
  ? reporterVersions.daemonBuildVersion.trim()
376
370
  : undefined;
377
- const reporterSlots =
378
- Array.isArray(reporterVersions.slots) && reporterVersions.slots.length > 0
379
- ? reporterVersions.slots
380
- : undefined;
381
- // Direction-B unified envelope: fold versions/build/slots into ONE object the
382
- // coordinator ingests wholesale. Emitted only when at least one field is
383
- // present so a fully-cold reporter doesn't ship an empty stub. The legacy flat
384
- // fields above ride alongside for back-compat during rollout.
371
+ // Unified envelope: fold the reporter's per-machine RUNTIME facts
372
+ // (versions/build) into ONE object the coordinator ingests wholesale. Slots are
373
+ // NOT carried — they are coordinator-owned config, not reported
374
+ // (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix). Emitted only when at least one
375
+ // field is present so a fully-cold reporter doesn't ship an empty stub. The
376
+ // legacy flat fields above ride alongside for back-compat during rollout.
385
377
  const reporterMemberState =
386
- reporterProviderVersions || reporterDaemonBuildVersion || reporterSlots
378
+ reporterProviderVersions || reporterDaemonBuildVersion
387
379
  ? {
388
380
  ...(reporterProviderVersions ? { providerVersions: reporterProviderVersions } : {}),
389
381
  ...(reporterDaemonBuildVersion ? { daemonBuildVersion: reporterDaemonBuildVersion } : {}),
390
- ...(reporterSlots ? { slots: reporterSlots } : {}),
391
382
  lastReportedAt: Date.now(),
392
383
  }
393
384
  : undefined;
@@ -15,7 +15,7 @@ import type { ProviderLoader } from '../providers/provider-loader.js';
15
15
  import { detectCLI, getCachedProviderVersions } from '../detection/cli-detector.js';
16
16
  import { getDaemonBuildInfo } from '../build-info.js';
17
17
  import { getGitRepoStatus } from '../git/git-status.js';
18
- import { normalizeGitStatus as sharedNormalizeGitStatus, pickBestTransitGitStatus as sharedPickBestTransitGitStatus, summarizeGitShape as sharedSummarizeGitShape, normalizeMeshNodeId, daemonIdsEquivalent, meshWorkspacesEquivalent, sessionIdsEquivalent, withStatusProbeMarker, normalizeNodeCapabilitySlots, type NodeCapabilitySlot } from '@adhdev/mesh-shared';
18
+ import { normalizeGitStatus as sharedNormalizeGitStatus, pickBestTransitGitStatus as sharedPickBestTransitGitStatus, summarizeGitShape as sharedSummarizeGitShape, normalizeMeshNodeId, daemonIdsEquivalent, meshWorkspacesEquivalent, sessionIdsEquivalent, withStatusProbeMarker } from '@adhdev/mesh-shared';
19
19
  import type { MeshReportedMemberState } from '../repo-mesh-types.js';
20
20
  import { LOG } from '../logging/logger.js';
21
21
  import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
@@ -408,30 +408,28 @@ export function recordInlineMeshDirectGitTruth(
408
408
  ?? (isLocalSource ? readLocalReporterDaemonBuildVersion() : null))
409
409
  ?? null;
410
410
  if (reporterDaemonBuildVersion) node.reportedDaemonBuildVersion = reporterDaemonBuildVersion;
411
- // Mirror the unified state onto the node record so resolveNodeCapabilitySlots can
412
- // read the remote member's OWN resolved slots. Only stamped for a REMOTE member
413
- // (!isLocalSource): the coordinator's own / worktree nodes own their slots via
414
- // local policy.slots, so a self node deliberately carries NO reportedMemberState
415
- // this is what keeps the resolver's precedence inversion-free (a self node's
416
- // local policy is never shadowed by a mirror it doesn't have). Synthesize from
417
- // the legacy flat fields when a node reports only those (no unified envelope yet).
411
+ // Mirror the unified state onto the node record for observability. Only stamped
412
+ // for a REMOTE member (!isLocalSource): the coordinator's own / worktree nodes
413
+ // read their runtime facts from the local warm cache above, so a self node
414
+ // deliberately carries NO reportedMemberState. Carries the per-machine RUNTIME
415
+ // facts (provider versions + daemon build) NOT slots, which are coordinator-
416
+ // owned config resolved from node.policy.slots, not reported
417
+ // (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix). Synthesizes from the legacy flat
418
+ // fields when a node reports only those (no unified envelope yet).
418
419
  if (!isLocalSource) {
419
- const reportedSlots = normalizeNodeCapabilitySlots(unified?.slots);
420
420
  const mirrored: {
421
421
  providerVersions?: Record<string, string>;
422
422
  daemonBuildVersion?: string;
423
- slots?: NodeCapabilitySlot[];
424
423
  lastReportedAt?: number;
425
424
  } = {
426
425
  ...(reporterProviderVersions ? { providerVersions: reporterProviderVersions } : {}),
427
426
  ...(reporterDaemonBuildVersion ? { daemonBuildVersion: reporterDaemonBuildVersion } : {}),
428
- ...(reportedSlots.length ? { slots: reportedSlots } : {}),
429
427
  lastReportedAt: readNumberValue(unified?.lastReportedAt) ?? checkedAt,
430
428
  };
431
429
  // Only stamp when there's actual reported content beyond the timestamp — a
432
430
  // bare {lastReportedAt} carries no observability and would just churn the
433
431
  // record on every probe.
434
- if (mirrored.providerVersions || mirrored.daemonBuildVersion || mirrored.slots) {
432
+ if (mirrored.providerVersions || mirrored.daemonBuildVersion) {
435
433
  node.reportedMemberState = mirrored;
436
434
  }
437
435
  }
@@ -446,26 +444,26 @@ export function recordInlineMeshDirectGitTruth(
446
444
  }
447
445
 
448
446
  /**
449
- * Direction-B: coerce an unknown git-envelope `reporterMemberState` into a clean
447
+ * Coerce an unknown git-envelope `reporterMemberState` into a clean
450
448
  * {@link MeshReportedMemberState}. Reuses readProviderVersionsRecord for the version
451
- * map and normalizeNodeCapabilitySlots for slots (dropping provider-less entries).
452
- * Returns null when nothing usable is present (bare/absent envelope) so callers fall
453
- * back to the legacy flat fields. Best-effort, never throws.
449
+ * map. Carries the per-machine RUNTIME facts (provider versions + daemon build) only
450
+ * slots are coordinator-owned config, not reported (REMOTE-NODE-SLOTS-COORDINATOR-
451
+ * LOCAL fix), and any stray `slots` on a legacy envelope is ignored. Returns null
452
+ * when nothing usable is present (bare/absent envelope) so callers fall back to the
453
+ * legacy flat fields. Best-effort, never throws.
454
454
  */
455
455
  function normalizeReportedMemberState(value: unknown): MeshReportedMemberState | null {
456
456
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
457
457
  const raw = value as Record<string, unknown>;
458
458
  const providerVersions = readProviderVersionsRecord(raw.providerVersions);
459
459
  const daemonBuildVersion = readStringValue(raw.daemonBuildVersion);
460
- const slots = normalizeNodeCapabilitySlots(raw.slots);
461
460
  const lastReportedAt = readNumberValue(raw.lastReportedAt);
462
- if (!providerVersions && !daemonBuildVersion && !slots.length && lastReportedAt === undefined) {
461
+ if (!providerVersions && !daemonBuildVersion && lastReportedAt === undefined) {
463
462
  return null;
464
463
  }
465
464
  return {
466
465
  ...(providerVersions ? { providerVersions } : {}),
467
466
  ...(daemonBuildVersion ? { daemonBuildVersion } : {}),
468
- ...(slots.length ? { slots } : {}),
469
467
  ...(lastReportedAt !== undefined ? { lastReportedAt } : {}),
470
468
  };
471
469
  }
@@ -567,8 +565,10 @@ export function persistNodeReporterPlatform(
567
565
  const reportedMachineNickname = reporter.reporterMachineNickname ?? undefined;
568
566
  const reportedProviderVersions = reporter.reporterProviderVersions ?? undefined;
569
567
  const reportedDaemonBuildVersion = reporter.reporterDaemonBuildVersion ?? undefined;
570
- // Direction-B: persist the unified mirrored member state so a remote node's
571
- // slot-cap chips survive a coordinator restart (mirrors the per-field self-heal).
568
+ // Persist the unified mirrored member state so a remote node's version chips
569
+ // survive a coordinator restart (mirrors the per-field self-heal). Carries
570
+ // per-machine runtime facts only (versions + build) — slots are coordinator-owned
571
+ // config, not reported (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix).
572
572
  const reportedMemberState = reporter.reportedMemberState ?? undefined;
573
573
  if (
574
574
  !reportedPlatform &&
@@ -1618,10 +1618,11 @@ async function probeRemoteMeshGitStatus(args: {
1618
1618
  if (reporterProviderVersions) git.reporterProviderVersions = reporterProviderVersions;
1619
1619
  const reporterDaemonBuildVersion = readStringValue(remoteResult?.reporterDaemonBuildVersion);
1620
1620
  if (reporterDaemonBuildVersion) git.reporterDaemonBuildVersion = reporterDaemonBuildVersion;
1621
- // Direction-B: propagate the member's UNIFIED reportedMemberState (versions +
1622
- // build + its own resolved slots + lastReportedAt) on the same channel so the
1623
- // coordinator can ingest it wholesale. Carried alongside the legacy flat fields
1624
- // above for a mixed-version-mesh rollout.
1621
+ // Propagate the member's UNIFIED reportedMemberState (per-machine runtime facts:
1622
+ // versions + build + lastReportedAt) on the same channel so the coordinator can
1623
+ // ingest it wholesale. Slots are NOT carried coordinator-owned config
1624
+ // (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix). Carried alongside the legacy flat
1625
+ // fields above for a mixed-version-mesh rollout.
1625
1626
  const reporterMemberState = normalizeReportedMemberState(remoteResult?.reporterMemberState);
1626
1627
  if (reporterMemberState) git.reporterMemberState = reporterMemberState;
1627
1628
  return git;
@@ -12,10 +12,9 @@
12
12
  import {
13
13
  deriveSlotsFromLegacy,
14
14
  normalizeNodeCapabilitySlots,
15
- meshWorkspacesEquivalent,
16
15
  type NodeCapabilitySlot,
17
16
  } from '@adhdev/mesh-shared';
18
- import { getDifficultyBrains, listMeshes } from '../config/mesh-config.js';
17
+ import { getDifficultyBrains } from '../config/mesh-config.js';
19
18
 
20
19
  /** Ordered, de-duplicated providerPriority from a node policy (defensive). */
21
20
  export function normalizeProviderPriority(policy: unknown): string[] {
@@ -35,25 +34,19 @@ export function normalizeProviderPriority(policy: unknown): string[] {
35
34
  }
36
35
 
37
36
  /**
38
- * Resolve a node's capability slots with EXPLICIT self-vs-remote precedence
39
- * (REMOTE-NODE-SLOT-CAP-CHIP fix, Direction B).
37
+ * Resolve a node's capability slots coordinator-owned config is the single source
38
+ * of truth for ALL nodes (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix).
40
39
  *
41
- * Precedence, avoiding the precedence-inversion a blind `policy.slots || mirror`
42
- * would introduce:
43
- * 1. Local `policy.slots` authoritative for the SELF/locally-owned node (the
44
- * node owns its own config locally). This ALSO wins for a remote node in the
45
- * unusual case an operator configured slots on the coordinator side.
46
- * 2. `reportedMemberState.slots` — the remote member's OWN resolved slots,
47
- * mirrored wholesale over P2P. Authoritative for a REMOTE mirrored node whose
48
- * coordinator-side policy copy is the stale join-time empty `{}` (which
49
- * normalizes to []). This is the field the coordinator's join-time copy lacks
50
- * — the reason remote slot-cap chips were missing.
51
- * 3. Legacy-derived from `providerPriority` + machine-global difficultyBrains.
40
+ * The coordinator's local meshes.json owns `policy.slots` for every node in the mesh
41
+ * (self AND remote members alike — a remote member's mesh config lives on the
42
+ * coordinator, its own on-disk meshes.json is empty). So slots resolve directly from
43
+ * the coordinator's locally-owned `node.policy.slots`, with no remote reporter
44
+ * round-trip: there is no per-node "reported slots" mirror to consult.
45
+ *
46
+ * Precedence:
47
+ * 1. `policy.slots` coordinator-owned, authoritative for self and remote alike.
48
+ * 2. Legacy-derived from `providerPriority` + machine-global difficultyBrains.
52
49
  *
53
- * The mirror only ever fills the gap left by an empty local policy — it can never
54
- * shadow a populated local policy — so there is no inversion. And a self node never
55
- * carries a reportedMemberState (the ingest path stamps it ONLY for remote
56
- * members), so a self node's local policy is always used directly.
57
50
  * (The former per-provider `providerRoles` cap has been removed; a persisted
58
51
  * meshes.json is migrated to slots on load, so by the time a node reaches routing
59
52
  * its cap already lives on `slots[].maxParallel`.)
@@ -61,13 +54,6 @@ export function normalizeProviderPriority(policy: unknown): string[] {
61
54
  export function resolveNodeCapabilitySlots(node: any): NodeCapabilitySlot[] {
62
55
  const explicit = normalizeNodeCapabilitySlots(node?.policy?.slots);
63
56
  if (explicit.length) return explicit;
64
- // Remote mirror: the member's own resolved slots, carried on the git_status
65
- // envelope and ingested onto the node record. Preferred over the legacy derive
66
- // (it is the member's real resolved profile), but only when the local policy is
67
- // empty — so it fills the coordinator's join-time gap without overriding an
68
- // explicit local override.
69
- const mirrored = normalizeNodeCapabilitySlots(node?.reportedMemberState?.slots);
70
- if (mirrored.length) return mirrored;
71
57
  let difficultyBrains: any;
72
58
  try { difficultyBrains = getDifficultyBrains(); } catch { difficultyBrains = undefined; }
73
59
  return deriveSlotsFromLegacy({
@@ -75,31 +61,3 @@ export function resolveNodeCapabilitySlots(node: any): NodeCapabilitySlot[] {
75
61
  difficultyBrains,
76
62
  });
77
63
  }
78
-
79
- /**
80
- * Reporter-side helper (Direction B): resolve THIS daemon's own capability slots for
81
- * a given workspace by finding the local meshes.json node whose workspace matches
82
- * and running the same {@link resolveNodeCapabilitySlots} over its real local policy.
83
- * Used at the git_status reporter wiring so a member node self-reports its own
84
- * resolved slots inside the unified reporterMemberState envelope. Returns [] when no
85
- * local node matches the workspace or slots can't be resolved (best-effort, never
86
- * throws) so the reporter simply omits the field.
87
- */
88
- export function resolveLocalNodeSlotsForWorkspace(workspace: string | undefined): NodeCapabilitySlot[] {
89
- const ws = typeof workspace === 'string' ? workspace.trim() : '';
90
- if (!ws) return [];
91
- let meshes: any[];
92
- try { meshes = listMeshes(); } catch { return []; }
93
- for (const mesh of Array.isArray(meshes) ? meshes : []) {
94
- for (const node of Array.isArray(mesh?.nodes) ? mesh.nodes : []) {
95
- const nodeWs = typeof node?.workspace === 'string' ? node.workspace : '';
96
- if (nodeWs && meshWorkspacesEquivalent(nodeWs, ws)) {
97
- // Resolve from the LOCAL node's real policy only — never re-read its
98
- // own reportedMemberState (a member does not mirror itself).
99
- const slots = resolveNodeCapabilitySlots({ policy: node?.policy });
100
- if (slots.length) return slots;
101
- }
102
- }
103
- }
104
- return [];
105
- }
@@ -399,35 +399,28 @@ export interface RepoMeshRelatedRepo {
399
399
  */
400
400
 
401
401
  /**
402
- * Direction-B unified mirrored member state (REMOTE-NODE-SLOT-CAP-CHIP fix).
402
+ * Unified mirrored member state — the per-machine RUNTIME facts a remote member
403
+ * self-reports (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix).
403
404
  *
404
405
  * A single self-reported observability envelope stamped by the daemon that OWNS a
405
406
  * node's workspace and carried wholesale on the git_status envelope
406
- * (reporterMemberState), replacing the fragmented per-field self-heal that mirrored
407
- * providerVersions and daemonBuildVersion ad-hoc but silently dropped `slots`. The
408
- * coordinator ingests this whole object onto the mirrored remote node in one place
407
+ * (reporterMemberState), consolidating the previously per-field self-heal of
408
+ * providerVersions and daemonBuildVersion. The coordinator ingests this whole object
409
+ * onto the mirrored remote node in one place
409
410
  * (mesh-node-identity.recordInlineMeshDirectGitTruth).
410
411
  *
411
- * Source-of-truth model: EACH NODE owns its own slots/versions locally; the
412
- * coordinator is a pure mirror of a remote member's reported state. Its own
413
- * join-time `policy` copy for a remote member is a stale empty `{}` and must never
414
- * shadow this mirror (that would be a precedence inversion — see
415
- * resolveNodeCapabilitySlots).
412
+ * Source-of-truth model: provider VERSIONS + daemon BUILD are per-machine runtime
413
+ * facts (e.g. a member has claude-cli@2.1.168 while the coordinator has @2.1.170), so
414
+ * they are reported here. SLOTS are NOT reported they are coordinator-owned config
415
+ * (node.policy.slots for every node, self and remote alike; see
416
+ * resolveNodeCapabilitySlots), a single source of truth on the coordinator with no
417
+ * reporter round-trip.
416
418
  */
417
419
  export interface MeshReportedMemberState {
418
420
  /** Reporter node's detected provider CLI/ACP versions (keyed by provider id). */
419
421
  providerVersions?: Record<string, string>;
420
422
  /** Reporter daemon's build version (getDaemonBuildInfo().version). */
421
423
  daemonBuildVersion?: string;
422
- /**
423
- * Reporter node's OWN resolved capability slots (resolveNodeCapabilitySlots over
424
- * its LOCAL real policy). This is the field the coordinator's join-time policy
425
- * copy lacks for a remote member, and the reason remote slot-cap chips were
426
- * missing. For a remote mirrored node this is authoritative over the (empty)
427
- * local policy.slots; for the coordinator's own node the local policy is used
428
- * directly (never mirrored here).
429
- */
430
- slots?: NodeCapabilitySlot[];
431
424
  /** Epoch ms the report was stamped; used for mirror staleness reasoning. */
432
425
  lastReportedAt?: number;
433
426
  }
@@ -952,19 +945,15 @@ export interface LocalMeshNodeEntry {
952
945
  /**
953
946
  * Unified mirrored "member state" self-reported by the daemon that owns this
954
947
  * node's workspace, carried wholesale on the git_status envelope
955
- * (reporterMemberState) and ingested by the coordinator in one place. This is
956
- * the Direction-B consolidation of the previously per-field self-heal
957
- * (reportedProviderVersions, reportedDaemonBuildVersionand now the node's
958
- * OWN resolved capability `slots`, which the coordinator's join-time policy copy
959
- * for a remote member does NOT carry). Each node owns its own slots/versions
960
- * locally; the coordinator is a pure mirror of what a remote member reports over
961
- * P2P it must never treat its stale join-time policy copy as authoritative for
962
- * a remote node's slots. The legacy flat fields above stay populated in parallel
963
- * during rollout for back-compat (see mesh-node-identity ingest). `slots` is the
964
- * reporter node's own resolved capability slots (resolveNodeCapabilitySlots over
965
- * its LOCAL real policy). `lastReportedAt` is the epoch ms the report was
966
- * stamped, so a partitioned node's mirror does not linger as fresh forever.
967
- * Absent until the first envelope carrying it is ingested.
948
+ * (reporterMemberState) and ingested by the coordinator in one place. It carries
949
+ * the per-machine RUNTIME facts (reportedProviderVersions, reportedDaemonBuildVersion)
950
+ * consolidated into one envelope NOT slots, which are coordinator-owned config
951
+ * resolved from node.policy.slots for every node, not reported
952
+ * (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix). The legacy flat fields above stay
953
+ * populated in parallel during rollout for back-compat (see mesh-node-identity
954
+ * ingest). `lastReportedAt` is the epoch ms the report was stamped, so a
955
+ * partitioned node's mirror does not linger as fresh forever. Absent until the
956
+ * first envelope carrying it is ingested.
968
957
  */
969
958
  reportedMemberState?: MeshReportedMemberState;
970
959
  /**