@adhdev/daemon-core 0.9.82-rc.514 → 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.
@@ -11,6 +11,7 @@
11
11
  * (e.g. `from '.../commands/router.js'`) keep working.
12
12
  */
13
13
  import type { ProviderLoader } from '../providers/provider-loader.js';
14
+ import type { MeshReportedMemberState } from '../repo-mesh-types.js';
14
15
  import { awaitWithWarmupDeadline } from '../mesh/mesh-warmup-deadline.js';
15
16
  export declare function readProviderPriorityFromPolicy(policy: unknown): string[];
16
17
  /**
@@ -51,6 +52,7 @@ export declare function recordInlineMeshDirectGitTruth(node: any, git: Record<st
51
52
  reporterMachineNickname: string | null;
52
53
  reporterProviderVersions: Record<string, string> | null;
53
54
  reporterDaemonBuildVersion: string | null;
55
+ reportedMemberState: MeshReportedMemberState | null;
54
56
  };
55
57
  /**
56
58
  * Persist the live self-reported platform/arch onto the local meshes.json node
@@ -69,6 +71,7 @@ export declare function persistNodeReporterPlatform(meshSource: 'inline_cache' |
69
71
  reporterMachineNickname?: string | null;
70
72
  reporterProviderVersions?: Record<string, string> | null;
71
73
  reporterDaemonBuildVersion?: string | null;
74
+ reportedMemberState?: MeshReportedMemberState | null;
72
75
  }): void;
73
76
  export declare function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean;
74
77
  export declare function readInlineMeshNodeId(node: any): string;
@@ -13,8 +13,19 @@ 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: explicit `policy.slots` when present, else
17
- * derived from the legacy `providerPriority` + machine-global difficultyBrains.
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
+ *
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.
28
+ *
18
29
  * (The former per-provider `providerRoles` cap has been removed; a persisted
19
30
  * meshes.json is migrated to slots on load, so by the time a node reaches routing
20
31
  * its cap already lives on `slots[].maxParallel`.)
@@ -300,6 +300,32 @@ export interface RepoMeshRelatedRepo {
300
300
  * persisted meshes.json that still carries it is migrated to `slots` on load
301
301
  * (see migrateLoadedMeshConfig).
302
302
  */
303
+ /**
304
+ * Unified mirrored member state — the per-machine RUNTIME facts a remote member
305
+ * self-reports (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix).
306
+ *
307
+ * A single self-reported observability envelope stamped by the daemon that OWNS a
308
+ * node's workspace and carried wholesale on the git_status envelope
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
312
+ * (mesh-node-identity.recordInlineMeshDirectGitTruth).
313
+ *
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.
320
+ */
321
+ export interface MeshReportedMemberState {
322
+ /** Reporter node's detected provider CLI/ACP versions (keyed by provider id). */
323
+ providerVersions?: Record<string, string>;
324
+ /** Reporter daemon's build version (getDaemonBuildInfo().version). */
325
+ daemonBuildVersion?: string;
326
+ /** Epoch ms the report was stamped; used for mirror staleness reasoning. */
327
+ lastReportedAt?: number;
328
+ }
303
329
  export interface RepoMeshNodePolicy {
304
330
  readOnly?: boolean;
305
331
  canPush?: boolean;
@@ -634,6 +660,20 @@ export interface LocalMeshNodeEntry {
634
660
  * owning daemon, carried on the git_status envelope (reporterDaemonBuildVersion)
635
661
  * alongside the provider versions. Absent until first reported. */
636
662
  reportedDaemonBuildVersion?: string;
663
+ /**
664
+ * Unified mirrored "member state" self-reported by the daemon that owns this
665
+ * node's workspace, carried wholesale on the git_status envelope
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.
675
+ */
676
+ reportedMemberState?: MeshReportedMemberState;
637
677
  /**
638
678
  * The operator-set machine nickname (config.machineNickname) of the daemon
639
679
  * that owns this node's workspace. The local coordinator stamps its own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.514",
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.514",
51
- "@adhdev/session-host-core": "0.9.82-rc.514",
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",
@@ -314,7 +314,18 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
314
314
  gitCommandServices: createDefaultGitCommandServices({
315
315
  // T7: fold this daemon's cached provider versions + build version onto the
316
316
  // git_status envelope so the mesh coordinator self-heals each node's
317
- // providerVersions. Non-blocking: reads a TTL cache, lazily refreshed.
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).
318
329
  getReporterProviderVersions: () => {
319
330
  const providerVersions = getCachedProviderVersions(providerLoader);
320
331
  const daemonBuildVersion = getDaemonBuildInfo().version;
@@ -21,6 +21,7 @@ import type {
21
21
  RepoMeshCoordinatorConfig,
22
22
  RepoMeshHostMetadata,
23
23
  RepoMeshDaemonRole,
24
+ MeshReportedMemberState,
24
25
  } from '../repo-mesh-types.js';
25
26
  import type { MagiKindPanelMap, MagiSlot, MagiTaskKind, DifficultyBrainMap, NodeCapabilitySlot } from '@adhdev/mesh-shared';
26
27
  import { normalizeDifficultyBrainMap, DEFAULT_DIFFICULTY_BRAINS, normalizeNodeCapabilitySlots, deriveSlotsFromLegacy } from '@adhdev/mesh-shared';
@@ -643,6 +644,13 @@ export function updateNode(
643
644
  * is overwritten by the next report. */
644
645
  reportedProviderVersions?: Record<string, string>;
645
646
  reportedDaemonBuildVersion?: string;
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). */
653
+ reportedMemberState?: MeshReportedMemberState;
646
654
  },
647
655
  ): LocalMeshNodeEntry | undefined {
648
656
  const config = loadMeshConfig();
@@ -662,6 +670,12 @@ export function updateNode(
662
670
  if (opts.reportedDaemonBuildVersion && opts.reportedDaemonBuildVersion.trim()) {
663
671
  node.reportedDaemonBuildVersion = opts.reportedDaemonBuildVersion.trim();
664
672
  }
673
+ if (opts.reportedMemberState) {
674
+ // Whole-object replace (auto-detected observability, overwritten by the next
675
+ // report so a stale mirror never sticks — same policy as the flat reported*
676
+ // fields). normalizeReportedMemberState upstream guarantees a clean shape.
677
+ node.reportedMemberState = opts.reportedMemberState;
678
+ }
665
679
  if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
666
680
  if (Object.prototype.hasOwnProperty.call(opts, 'capabilities')) {
667
681
  // Explicit replace: normalize (trim/dedup/drop-empties); an empty result
@@ -109,7 +109,10 @@ export interface GitCommandServices {
109
109
  * same way it does platform/arch. Wired at daemon boot from the cached CLI
110
110
  * detection snapshot; omitted (⇒ versions not reported) when unavailable.
111
111
  */
112
- getReporterProviderVersions?: () => { providerVersions?: Record<string, string>; daemonBuildVersion?: string };
112
+ getReporterProviderVersions?: (workspace?: string) => {
113
+ providerVersions?: Record<string, string>;
114
+ daemonBuildVersion?: string;
115
+ };
113
116
  }
114
117
 
115
118
  type GitCommandFailure = {
@@ -124,7 +127,13 @@ type GitCommandSuccess =
124
127
  // node's userOverrides.platform/arch (the fields capability-tag routing reads).
125
128
  // reporterMachineNickname carries the responding daemon's config.machineNickname
126
129
  // so the coordinator can populate node.machineNickname → the friendly display label.
127
- | { success: true; status: GitRepoStatus; reporterPlatform?: string; reporterArch?: string; reporterMachineNickname?: string; reporterProviderVersions?: Record<string, string>; reporterDaemonBuildVersion?: string }
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
134
+ // reporterProviderVersions/reporterDaemonBuildVersion fields are ALSO emitted for
135
+ // back-compat during a mixed-version-mesh rollout.
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 } }
128
137
  | { success: true; diffSummary: GitDiffSummary }
129
138
  | { success: true; diff: GitFileDiff }
130
139
  | { success: true; snapshot: GitSnapshot }
@@ -346,7 +355,7 @@ export async function handleGitCommand(
346
355
  // snapshot, so a cold cache simply omits the fields on this probe.
347
356
  const reporterVersions = (() => {
348
357
  try {
349
- return services.getReporterProviderVersions?.() ?? {};
358
+ return services.getReporterProviderVersions?.(workspace) ?? {};
350
359
  } catch {
351
360
  return {};
352
361
  }
@@ -359,6 +368,20 @@ export async function handleGitCommand(
359
368
  typeof reporterVersions.daemonBuildVersion === 'string' && reporterVersions.daemonBuildVersion.trim()
360
369
  ? reporterVersions.daemonBuildVersion.trim()
361
370
  : undefined;
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.
377
+ const reporterMemberState =
378
+ reporterProviderVersions || reporterDaemonBuildVersion
379
+ ? {
380
+ ...(reporterProviderVersions ? { providerVersions: reporterProviderVersions } : {}),
381
+ ...(reporterDaemonBuildVersion ? { daemonBuildVersion: reporterDaemonBuildVersion } : {}),
382
+ lastReportedAt: Date.now(),
383
+ }
384
+ : undefined;
362
385
  return {
363
386
  success: true,
364
387
  status,
@@ -367,6 +390,7 @@ export async function handleGitCommand(
367
390
  ...(reporterMachineNickname ? { reporterMachineNickname } : {}),
368
391
  ...(reporterProviderVersions ? { reporterProviderVersions } : {}),
369
392
  ...(reporterDaemonBuildVersion ? { reporterDaemonBuildVersion } : {}),
393
+ ...(reporterMemberState ? { reporterMemberState } : {}),
370
394
  };
371
395
  }
372
396
 
@@ -16,6 +16,7 @@ import { detectCLI, getCachedProviderVersions } from '../detection/cli-detector.
16
16
  import { getDaemonBuildInfo } from '../build-info.js';
17
17
  import { getGitRepoStatus } from '../git/git-status.js';
18
18
  import { normalizeGitStatus as sharedNormalizeGitStatus, pickBestTransitGitStatus as sharedPickBestTransitGitStatus, summarizeGitShape as sharedSummarizeGitShape, normalizeMeshNodeId, daemonIdsEquivalent, meshWorkspacesEquivalent, sessionIdsEquivalent, withStatusProbeMarker } from '@adhdev/mesh-shared';
19
+ import type { MeshReportedMemberState } from '../repo-mesh-types.js';
19
20
  import { LOG } from '../logging/logger.js';
20
21
  import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
21
22
  import { awaitWithWarmupDeadline, resolveWarmupDeadlineOpts } from '../mesh/mesh-warmup-deadline.js';
@@ -326,6 +327,7 @@ export function recordInlineMeshDirectGitTruth(
326
327
  reporterMachineNickname: string | null;
327
328
  reporterProviderVersions: Record<string, string> | null;
328
329
  reporterDaemonBuildVersion: string | null;
330
+ reportedMemberState: MeshReportedMemberState | null;
329
331
  } {
330
332
  if (!node || typeof node !== 'object' || Array.isArray(node)) {
331
333
  return {
@@ -334,6 +336,7 @@ export function recordInlineMeshDirectGitTruth(
334
336
  reporterMachineNickname: null,
335
337
  reporterProviderVersions: null,
336
338
  reporterDaemonBuildVersion: null,
339
+ reportedMemberState: null,
337
340
  };
338
341
  }
339
342
  const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
@@ -389,21 +392,79 @@ export function recordInlineMeshDirectGitTruth(
389
392
  // carries no reporter* versions — mirror the platform/arch self-heal above and read
390
393
  // this daemon's own warm version cache directly, so the coordinator's self node gets
391
394
  // the same provider/build chips a remote node does.
395
+ // Direction-B: parse the UNIFIED reportedMemberState envelope first (versions +
396
+ // build + the member's own resolved slots), falling back to the flat legacy
397
+ // fields so a mixed-version mesh during rollout still ingests. This is the single
398
+ // place the coordinator mirrors a remote member's reported state wholesale.
399
+ const unified = normalizeReportedMemberState(git.reporterMemberState);
392
400
  const reporterProviderVersions =
393
- readProviderVersionsRecord(git.reporterProviderVersions)
401
+ (unified?.providerVersions ? readProviderVersionsRecord(unified.providerVersions) : null)
402
+ ?? readProviderVersionsRecord(git.reporterProviderVersions)
394
403
  ?? (isLocalSource ? readLocalReporterProviderVersions() : null);
395
404
  if (reporterProviderVersions) node.reportedProviderVersions = reporterProviderVersions;
396
405
  const reporterDaemonBuildVersion =
397
- (readStringValue(git.reporterDaemonBuildVersion)
406
+ (readStringValue(unified?.daemonBuildVersion)
407
+ ?? readStringValue(git.reporterDaemonBuildVersion)
398
408
  ?? (isLocalSource ? readLocalReporterDaemonBuildVersion() : null))
399
409
  ?? null;
400
410
  if (reporterDaemonBuildVersion) node.reportedDaemonBuildVersion = reporterDaemonBuildVersion;
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).
419
+ if (!isLocalSource) {
420
+ const mirrored: {
421
+ providerVersions?: Record<string, string>;
422
+ daemonBuildVersion?: string;
423
+ lastReportedAt?: number;
424
+ } = {
425
+ ...(reporterProviderVersions ? { providerVersions: reporterProviderVersions } : {}),
426
+ ...(reporterDaemonBuildVersion ? { daemonBuildVersion: reporterDaemonBuildVersion } : {}),
427
+ lastReportedAt: readNumberValue(unified?.lastReportedAt) ?? checkedAt,
428
+ };
429
+ // Only stamp when there's actual reported content beyond the timestamp — a
430
+ // bare {lastReportedAt} carries no observability and would just churn the
431
+ // record on every probe.
432
+ if (mirrored.providerVersions || mirrored.daemonBuildVersion) {
433
+ node.reportedMemberState = mirrored;
434
+ }
435
+ }
401
436
  return {
402
437
  reporterPlatform,
403
438
  reporterArch,
404
439
  reporterMachineNickname,
405
440
  reporterProviderVersions,
406
441
  reporterDaemonBuildVersion,
442
+ reportedMemberState: (!isLocalSource ? (node.reportedMemberState ?? null) : null) as MeshReportedMemberState | null,
443
+ };
444
+ }
445
+
446
+ /**
447
+ * Coerce an unknown git-envelope `reporterMemberState` into a clean
448
+ * {@link MeshReportedMemberState}. Reuses readProviderVersionsRecord for the version
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
+ */
455
+ function normalizeReportedMemberState(value: unknown): MeshReportedMemberState | null {
456
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
457
+ const raw = value as Record<string, unknown>;
458
+ const providerVersions = readProviderVersionsRecord(raw.providerVersions);
459
+ const daemonBuildVersion = readStringValue(raw.daemonBuildVersion);
460
+ const lastReportedAt = readNumberValue(raw.lastReportedAt);
461
+ if (!providerVersions && !daemonBuildVersion && lastReportedAt === undefined) {
462
+ return null;
463
+ }
464
+ return {
465
+ ...(providerVersions ? { providerVersions } : {}),
466
+ ...(daemonBuildVersion ? { daemonBuildVersion } : {}),
467
+ ...(lastReportedAt !== undefined ? { lastReportedAt } : {}),
407
468
  };
408
469
  }
409
470
 
@@ -493,6 +554,7 @@ export function persistNodeReporterPlatform(
493
554
  reporterMachineNickname?: string | null;
494
555
  reporterProviderVersions?: Record<string, string> | null;
495
556
  reporterDaemonBuildVersion?: string | null;
557
+ reportedMemberState?: MeshReportedMemberState | null;
496
558
  },
497
559
  ): void {
498
560
  if (meshSource !== 'local_config') return;
@@ -503,12 +565,18 @@ export function persistNodeReporterPlatform(
503
565
  const reportedMachineNickname = reporter.reporterMachineNickname ?? undefined;
504
566
  const reportedProviderVersions = reporter.reporterProviderVersions ?? undefined;
505
567
  const reportedDaemonBuildVersion = reporter.reporterDaemonBuildVersion ?? undefined;
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
+ const reportedMemberState = reporter.reportedMemberState ?? undefined;
506
573
  if (
507
574
  !reportedPlatform &&
508
575
  !reportedArch &&
509
576
  !reportedMachineNickname &&
510
577
  !reportedProviderVersions &&
511
- !reportedDaemonBuildVersion
578
+ !reportedDaemonBuildVersion &&
579
+ !reportedMemberState
512
580
  ) {
513
581
  return;
514
582
  }
@@ -519,6 +587,7 @@ export function persistNodeReporterPlatform(
519
587
  reportedMachineNickname,
520
588
  reportedProviderVersions,
521
589
  reportedDaemonBuildVersion,
590
+ reportedMemberState,
522
591
  }))
523
592
  .catch(() => { /* best-effort self-heal; never block status assembly */ });
524
593
  }
@@ -1549,6 +1618,13 @@ async function probeRemoteMeshGitStatus(args: {
1549
1618
  if (reporterProviderVersions) git.reporterProviderVersions = reporterProviderVersions;
1550
1619
  const reporterDaemonBuildVersion = readStringValue(remoteResult?.reporterDaemonBuildVersion);
1551
1620
  if (reporterDaemonBuildVersion) git.reporterDaemonBuildVersion = reporterDaemonBuildVersion;
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.
1626
+ const reporterMemberState = normalizeReportedMemberState(remoteResult?.reporterMemberState);
1627
+ if (reporterMemberState) git.reporterMemberState = reporterMemberState;
1552
1628
  return git;
1553
1629
  }
1554
1630
 
@@ -34,8 +34,19 @@ export function normalizeProviderPriority(policy: unknown): string[] {
34
34
  }
35
35
 
36
36
  /**
37
- * Resolve a node's capability slots: explicit `policy.slots` when present, else
38
- * derived from the legacy `providerPriority` + machine-global difficultyBrains.
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).
39
+ *
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.
49
+ *
39
50
  * (The former per-provider `providerRoles` cap has been removed; a persisted
40
51
  * meshes.json is migrated to slots on load, so by the time a node reaches routing
41
52
  * its cap already lives on `slots[].maxParallel`.)
@@ -398,6 +398,33 @@ export interface RepoMeshRelatedRepo {
398
398
  * (see migrateLoadedMeshConfig).
399
399
  */
400
400
 
401
+ /**
402
+ * Unified mirrored member state — the per-machine RUNTIME facts a remote member
403
+ * self-reports (REMOTE-NODE-SLOTS-COORDINATOR-LOCAL fix).
404
+ *
405
+ * A single self-reported observability envelope stamped by the daemon that OWNS a
406
+ * node's workspace and carried wholesale on the git_status envelope
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
410
+ * (mesh-node-identity.recordInlineMeshDirectGitTruth).
411
+ *
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.
418
+ */
419
+ export interface MeshReportedMemberState {
420
+ /** Reporter node's detected provider CLI/ACP versions (keyed by provider id). */
421
+ providerVersions?: Record<string, string>;
422
+ /** Reporter daemon's build version (getDaemonBuildInfo().version). */
423
+ daemonBuildVersion?: string;
424
+ /** Epoch ms the report was stamped; used for mirror staleness reasoning. */
425
+ lastReportedAt?: number;
426
+ }
427
+
401
428
  export interface RepoMeshNodePolicy {
402
429
  readOnly?: boolean;
403
430
  canPush?: boolean;
@@ -915,6 +942,20 @@ export interface LocalMeshNodeEntry {
915
942
  * owning daemon, carried on the git_status envelope (reporterDaemonBuildVersion)
916
943
  * alongside the provider versions. Absent until first reported. */
917
944
  reportedDaemonBuildVersion?: string;
945
+ /**
946
+ * Unified mirrored "member state" self-reported by the daemon that owns this
947
+ * node's workspace, carried wholesale on the git_status envelope
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.
957
+ */
958
+ reportedMemberState?: MeshReportedMemberState;
918
959
  /**
919
960
  * The operator-set machine nickname (config.machineNickname) of the daemon
920
961
  * that owns this node's workspace. The local coordinator stamps its own