@adhdev/daemon-core 0.9.82-rc.514 → 0.9.82-rc.515
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.
- package/dist/config/mesh-config.d.ts +6 -1
- package/dist/git/git-commands.d.ts +15 -1
- package/dist/index.js +96 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +96 -17
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-node-identity.d.ts +3 -0
- package/dist/mesh/mesh-node-slots.d.ts +29 -2
- package/dist/repo-mesh-types.d.ts +51 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +14 -4
- package/src/config/mesh-config.ts +12 -0
- package/src/git/git-commands.ts +36 -3
- package/src/mesh/mesh-node-identity.ts +79 -4
- package/src/mesh/mesh-node-slots.ts +56 -3
- package/src/repo-mesh-types.ts +52 -0
|
@@ -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,10 +13,37 @@ 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
|
|
17
|
-
*
|
|
16
|
+
* Resolve a node's capability slots with EXPLICIT self-vs-remote precedence
|
|
17
|
+
* (REMOTE-NODE-SLOT-CAP-CHIP fix, Direction B).
|
|
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.
|
|
30
|
+
*
|
|
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.
|
|
18
35
|
* (The former per-provider `providerRoles` cap has been removed; a persisted
|
|
19
36
|
* meshes.json is migrated to slots on load, so by the time a node reaches routing
|
|
20
37
|
* its cap already lives on `slots[].maxParallel`.)
|
|
21
38
|
*/
|
|
22
39
|
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[];
|
|
@@ -300,6 +300,39 @@ 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
|
+
* Direction-B unified mirrored member state (REMOTE-NODE-SLOT-CAP-CHIP fix).
|
|
305
|
+
*
|
|
306
|
+
* A single self-reported observability envelope stamped by the daemon that OWNS a
|
|
307
|
+
* 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
|
|
311
|
+
* (mesh-node-identity.recordInlineMeshDirectGitTruth).
|
|
312
|
+
*
|
|
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).
|
|
318
|
+
*/
|
|
319
|
+
export interface MeshReportedMemberState {
|
|
320
|
+
/** Reporter node's detected provider CLI/ACP versions (keyed by provider id). */
|
|
321
|
+
providerVersions?: Record<string, string>;
|
|
322
|
+
/** Reporter daemon's build version (getDaemonBuildInfo().version). */
|
|
323
|
+
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
|
+
/** Epoch ms the report was stamped; used for mirror staleness reasoning. */
|
|
334
|
+
lastReportedAt?: number;
|
|
335
|
+
}
|
|
303
336
|
export interface RepoMeshNodePolicy {
|
|
304
337
|
readOnly?: boolean;
|
|
305
338
|
canPush?: boolean;
|
|
@@ -634,6 +667,24 @@ export interface LocalMeshNodeEntry {
|
|
|
634
667
|
* owning daemon, carried on the git_status envelope (reporterDaemonBuildVersion)
|
|
635
668
|
* alongside the provider versions. Absent until first reported. */
|
|
636
669
|
reportedDaemonBuildVersion?: string;
|
|
670
|
+
/**
|
|
671
|
+
* Unified mirrored "member state" self-reported by the daemon that owns this
|
|
672
|
+
* 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, reportedDaemonBuildVersion — and 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.
|
|
686
|
+
*/
|
|
687
|
+
reportedMemberState?: MeshReportedMemberState;
|
|
637
688
|
/**
|
|
638
689
|
* The operator-set machine nickname (config.machineNickname) of the daemon
|
|
639
690
|
* 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.
|
|
3
|
+
"version": "0.9.82-rc.515",
|
|
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.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.515",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.515",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -37,6 +37,7 @@ 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';
|
|
40
41
|
import { setupMeshEventForwarding } from '../mesh/mesh-events.js';
|
|
41
42
|
import { setupMeshReconcileLoop } from '../mesh/mesh-reconcile-loop.js';
|
|
42
43
|
import { loadMeshCoordinatorRegistry } from '../mesh/coordinator-registry.js';
|
|
@@ -312,15 +313,24 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
312
313
|
instanceManager,
|
|
313
314
|
sessionRegistry,
|
|
314
315
|
gitCommandServices: createDefaultGitCommandServices({
|
|
315
|
-
// T7: fold this daemon's cached provider versions + build
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
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) => {
|
|
319
327
|
const providerVersions = getCachedProviderVersions(providerLoader);
|
|
320
328
|
const daemonBuildVersion = getDaemonBuildInfo().version;
|
|
329
|
+
const slots = resolveLocalNodeSlotsForWorkspace(workspace);
|
|
321
330
|
return {
|
|
322
331
|
...(Object.keys(providerVersions).length > 0 ? { providerVersions } : {}),
|
|
323
332
|
...(daemonBuildVersion && daemonBuildVersion !== 'unknown' ? { daemonBuildVersion } : {}),
|
|
333
|
+
...(slots.length > 0 ? { slots } : {}),
|
|
324
334
|
};
|
|
325
335
|
},
|
|
326
336
|
}),
|
|
@@ -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,11 @@ export function updateNode(
|
|
|
643
644
|
* is overwritten by the next report. */
|
|
644
645
|
reportedProviderVersions?: Record<string, string>;
|
|
645
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. */
|
|
651
|
+
reportedMemberState?: MeshReportedMemberState;
|
|
646
652
|
},
|
|
647
653
|
): LocalMeshNodeEntry | undefined {
|
|
648
654
|
const config = loadMeshConfig();
|
|
@@ -662,6 +668,12 @@ export function updateNode(
|
|
|
662
668
|
if (opts.reportedDaemonBuildVersion && opts.reportedDaemonBuildVersion.trim()) {
|
|
663
669
|
node.reportedDaemonBuildVersion = opts.reportedDaemonBuildVersion.trim();
|
|
664
670
|
}
|
|
671
|
+
if (opts.reportedMemberState) {
|
|
672
|
+
// Whole-object replace (auto-detected observability, overwritten by the next
|
|
673
|
+
// report so a stale mirror never sticks — same policy as the flat reported*
|
|
674
|
+
// fields). normalizeReportedMemberState upstream guarantees a clean shape.
|
|
675
|
+
node.reportedMemberState = opts.reportedMemberState;
|
|
676
|
+
}
|
|
665
677
|
if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
|
|
666
678
|
if (Object.prototype.hasOwnProperty.call(opts, 'capabilities')) {
|
|
667
679
|
// Explicit replace: normalize (trim/dedup/drop-empties); an empty result
|
package/src/git/git-commands.ts
CHANGED
|
@@ -109,7 +109,18 @@ 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?: () => {
|
|
112
|
+
getReporterProviderVersions?: (workspace?: string) => {
|
|
113
|
+
providerVersions?: Record<string, string>;
|
|
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
|
+
};
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
type GitCommandFailure = {
|
|
@@ -124,7 +135,11 @@ type GitCommandSuccess =
|
|
|
124
135
|
// node's userOverrides.platform/arch (the fields capability-tag routing reads).
|
|
125
136
|
// reporterMachineNickname carries the responding daemon's config.machineNickname
|
|
126
137
|
// so the coordinator can populate node.machineNickname → the friendly display label.
|
|
127
|
-
|
|
138
|
+
// reporterMemberState is the Direction-B unified envelope (versions + build + the
|
|
139
|
+
// reporter node's own resolved capability slots + lastReportedAt). The legacy flat
|
|
140
|
+
// reporterProviderVersions/reporterDaemonBuildVersion fields are ALSO emitted for
|
|
141
|
+
// 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 } }
|
|
128
143
|
| { success: true; diffSummary: GitDiffSummary }
|
|
129
144
|
| { success: true; diff: GitFileDiff }
|
|
130
145
|
| { success: true; snapshot: GitSnapshot }
|
|
@@ -346,7 +361,7 @@ export async function handleGitCommand(
|
|
|
346
361
|
// snapshot, so a cold cache simply omits the fields on this probe.
|
|
347
362
|
const reporterVersions = (() => {
|
|
348
363
|
try {
|
|
349
|
-
return services.getReporterProviderVersions?.() ?? {};
|
|
364
|
+
return services.getReporterProviderVersions?.(workspace) ?? {};
|
|
350
365
|
} catch {
|
|
351
366
|
return {};
|
|
352
367
|
}
|
|
@@ -359,6 +374,23 @@ export async function handleGitCommand(
|
|
|
359
374
|
typeof reporterVersions.daemonBuildVersion === 'string' && reporterVersions.daemonBuildVersion.trim()
|
|
360
375
|
? reporterVersions.daemonBuildVersion.trim()
|
|
361
376
|
: 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.
|
|
385
|
+
const reporterMemberState =
|
|
386
|
+
reporterProviderVersions || reporterDaemonBuildVersion || reporterSlots
|
|
387
|
+
? {
|
|
388
|
+
...(reporterProviderVersions ? { providerVersions: reporterProviderVersions } : {}),
|
|
389
|
+
...(reporterDaemonBuildVersion ? { daemonBuildVersion: reporterDaemonBuildVersion } : {}),
|
|
390
|
+
...(reporterSlots ? { slots: reporterSlots } : {}),
|
|
391
|
+
lastReportedAt: Date.now(),
|
|
392
|
+
}
|
|
393
|
+
: undefined;
|
|
362
394
|
return {
|
|
363
395
|
success: true,
|
|
364
396
|
status,
|
|
@@ -367,6 +399,7 @@ export async function handleGitCommand(
|
|
|
367
399
|
...(reporterMachineNickname ? { reporterMachineNickname } : {}),
|
|
368
400
|
...(reporterProviderVersions ? { reporterProviderVersions } : {}),
|
|
369
401
|
...(reporterDaemonBuildVersion ? { reporterDaemonBuildVersion } : {}),
|
|
402
|
+
...(reporterMemberState ? { reporterMemberState } : {}),
|
|
370
403
|
};
|
|
371
404
|
}
|
|
372
405
|
|
|
@@ -15,7 +15,8 @@ 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 } from '@adhdev/mesh-shared';
|
|
18
|
+
import { normalizeGitStatus as sharedNormalizeGitStatus, pickBestTransitGitStatus as sharedPickBestTransitGitStatus, summarizeGitShape as sharedSummarizeGitShape, normalizeMeshNodeId, daemonIdsEquivalent, meshWorkspacesEquivalent, sessionIdsEquivalent, withStatusProbeMarker, normalizeNodeCapabilitySlots, type NodeCapabilitySlot } 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,81 @@ 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(
|
|
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(
|
|
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 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).
|
|
418
|
+
if (!isLocalSource) {
|
|
419
|
+
const reportedSlots = normalizeNodeCapabilitySlots(unified?.slots);
|
|
420
|
+
const mirrored: {
|
|
421
|
+
providerVersions?: Record<string, string>;
|
|
422
|
+
daemonBuildVersion?: string;
|
|
423
|
+
slots?: NodeCapabilitySlot[];
|
|
424
|
+
lastReportedAt?: number;
|
|
425
|
+
} = {
|
|
426
|
+
...(reporterProviderVersions ? { providerVersions: reporterProviderVersions } : {}),
|
|
427
|
+
...(reporterDaemonBuildVersion ? { daemonBuildVersion: reporterDaemonBuildVersion } : {}),
|
|
428
|
+
...(reportedSlots.length ? { slots: reportedSlots } : {}),
|
|
429
|
+
lastReportedAt: readNumberValue(unified?.lastReportedAt) ?? checkedAt,
|
|
430
|
+
};
|
|
431
|
+
// Only stamp when there's actual reported content beyond the timestamp — a
|
|
432
|
+
// bare {lastReportedAt} carries no observability and would just churn the
|
|
433
|
+
// record on every probe.
|
|
434
|
+
if (mirrored.providerVersions || mirrored.daemonBuildVersion || mirrored.slots) {
|
|
435
|
+
node.reportedMemberState = mirrored;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
401
438
|
return {
|
|
402
439
|
reporterPlatform,
|
|
403
440
|
reporterArch,
|
|
404
441
|
reporterMachineNickname,
|
|
405
442
|
reporterProviderVersions,
|
|
406
443
|
reporterDaemonBuildVersion,
|
|
444
|
+
reportedMemberState: (!isLocalSource ? (node.reportedMemberState ?? null) : null) as MeshReportedMemberState | null,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Direction-B: coerce an unknown git-envelope `reporterMemberState` into a clean
|
|
450
|
+
* {@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.
|
|
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 slots = normalizeNodeCapabilitySlots(raw.slots);
|
|
461
|
+
const lastReportedAt = readNumberValue(raw.lastReportedAt);
|
|
462
|
+
if (!providerVersions && !daemonBuildVersion && !slots.length && lastReportedAt === undefined) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
...(providerVersions ? { providerVersions } : {}),
|
|
467
|
+
...(daemonBuildVersion ? { daemonBuildVersion } : {}),
|
|
468
|
+
...(slots.length ? { slots } : {}),
|
|
469
|
+
...(lastReportedAt !== undefined ? { lastReportedAt } : {}),
|
|
407
470
|
};
|
|
408
471
|
}
|
|
409
472
|
|
|
@@ -493,6 +556,7 @@ export function persistNodeReporterPlatform(
|
|
|
493
556
|
reporterMachineNickname?: string | null;
|
|
494
557
|
reporterProviderVersions?: Record<string, string> | null;
|
|
495
558
|
reporterDaemonBuildVersion?: string | null;
|
|
559
|
+
reportedMemberState?: MeshReportedMemberState | null;
|
|
496
560
|
},
|
|
497
561
|
): void {
|
|
498
562
|
if (meshSource !== 'local_config') return;
|
|
@@ -503,12 +567,16 @@ export function persistNodeReporterPlatform(
|
|
|
503
567
|
const reportedMachineNickname = reporter.reporterMachineNickname ?? undefined;
|
|
504
568
|
const reportedProviderVersions = reporter.reporterProviderVersions ?? undefined;
|
|
505
569
|
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).
|
|
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,12 @@ 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
|
+
// 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.
|
|
1625
|
+
const reporterMemberState = normalizeReportedMemberState(remoteResult?.reporterMemberState);
|
|
1626
|
+
if (reporterMemberState) git.reporterMemberState = reporterMemberState;
|
|
1552
1627
|
return git;
|
|
1553
1628
|
}
|
|
1554
1629
|
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
import {
|
|
13
13
|
deriveSlotsFromLegacy,
|
|
14
14
|
normalizeNodeCapabilitySlots,
|
|
15
|
+
meshWorkspacesEquivalent,
|
|
15
16
|
type NodeCapabilitySlot,
|
|
16
17
|
} from '@adhdev/mesh-shared';
|
|
17
|
-
import { getDifficultyBrains } from '../config/mesh-config.js';
|
|
18
|
+
import { getDifficultyBrains, listMeshes } from '../config/mesh-config.js';
|
|
18
19
|
|
|
19
20
|
/** Ordered, de-duplicated providerPriority from a node policy (defensive). */
|
|
20
21
|
export function normalizeProviderPriority(policy: unknown): string[] {
|
|
@@ -34,8 +35,25 @@ export function normalizeProviderPriority(policy: unknown): string[] {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
* Resolve a node's capability slots
|
|
38
|
-
*
|
|
38
|
+
* Resolve a node's capability slots with EXPLICIT self-vs-remote precedence
|
|
39
|
+
* (REMOTE-NODE-SLOT-CAP-CHIP fix, Direction B).
|
|
40
|
+
*
|
|
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.
|
|
52
|
+
*
|
|
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.
|
|
39
57
|
* (The former per-provider `providerRoles` cap has been removed; a persisted
|
|
40
58
|
* meshes.json is migrated to slots on load, so by the time a node reaches routing
|
|
41
59
|
* its cap already lives on `slots[].maxParallel`.)
|
|
@@ -43,6 +61,13 @@ export function normalizeProviderPriority(policy: unknown): string[] {
|
|
|
43
61
|
export function resolveNodeCapabilitySlots(node: any): NodeCapabilitySlot[] {
|
|
44
62
|
const explicit = normalizeNodeCapabilitySlots(node?.policy?.slots);
|
|
45
63
|
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;
|
|
46
71
|
let difficultyBrains: any;
|
|
47
72
|
try { difficultyBrains = getDifficultyBrains(); } catch { difficultyBrains = undefined; }
|
|
48
73
|
return deriveSlotsFromLegacy({
|
|
@@ -50,3 +75,31 @@ export function resolveNodeCapabilitySlots(node: any): NodeCapabilitySlot[] {
|
|
|
50
75
|
difficultyBrains,
|
|
51
76
|
});
|
|
52
77
|
}
|
|
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
|
+
}
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -398,6 +398,40 @@ export interface RepoMeshRelatedRepo {
|
|
|
398
398
|
* (see migrateLoadedMeshConfig).
|
|
399
399
|
*/
|
|
400
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Direction-B unified mirrored member state (REMOTE-NODE-SLOT-CAP-CHIP fix).
|
|
403
|
+
*
|
|
404
|
+
* A single self-reported observability envelope stamped by the daemon that OWNS a
|
|
405
|
+
* 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
|
|
409
|
+
* (mesh-node-identity.recordInlineMeshDirectGitTruth).
|
|
410
|
+
*
|
|
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).
|
|
416
|
+
*/
|
|
417
|
+
export interface MeshReportedMemberState {
|
|
418
|
+
/** Reporter node's detected provider CLI/ACP versions (keyed by provider id). */
|
|
419
|
+
providerVersions?: Record<string, string>;
|
|
420
|
+
/** Reporter daemon's build version (getDaemonBuildInfo().version). */
|
|
421
|
+
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
|
+
/** Epoch ms the report was stamped; used for mirror staleness reasoning. */
|
|
432
|
+
lastReportedAt?: number;
|
|
433
|
+
}
|
|
434
|
+
|
|
401
435
|
export interface RepoMeshNodePolicy {
|
|
402
436
|
readOnly?: boolean;
|
|
403
437
|
canPush?: boolean;
|
|
@@ -915,6 +949,24 @@ export interface LocalMeshNodeEntry {
|
|
|
915
949
|
* owning daemon, carried on the git_status envelope (reporterDaemonBuildVersion)
|
|
916
950
|
* alongside the provider versions. Absent until first reported. */
|
|
917
951
|
reportedDaemonBuildVersion?: string;
|
|
952
|
+
/**
|
|
953
|
+
* Unified mirrored "member state" self-reported by the daemon that owns this
|
|
954
|
+
* 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, reportedDaemonBuildVersion — and 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.
|
|
968
|
+
*/
|
|
969
|
+
reportedMemberState?: MeshReportedMemberState;
|
|
918
970
|
/**
|
|
919
971
|
* The operator-set machine nickname (config.machineNickname) of the daemon
|
|
920
972
|
* that owns this node's workspace. The local coordinator stamps its own
|