@adhdev/daemon-core 0.9.82-rc.460 → 0.9.82-rc.461
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 +7 -0
- package/dist/detection/cli-detector.d.ts +17 -0
- package/dist/git/git-commands.d.ts +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +461 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +459 -21
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-pending.d.ts +43 -1
- package/dist/mesh/mesh-events.d.ts +1 -1
- package/dist/mesh/mesh-node-identity.d.ts +4 -0
- package/dist/mesh/mesh-runtime-store.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +80 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +15 -2
- package/src/commands/high-family/mesh-status.ts +10 -0
- package/src/config/mesh-config.ts +13 -0
- package/src/detection/cli-detector.ts +66 -0
- package/src/git/git-commands.ts +35 -2
- package/src/index.ts +1 -1
- package/src/mesh/coordinator-prompt.ts +19 -1
- package/src/mesh/mesh-event-forwarding.ts +19 -1
- package/src/mesh/mesh-events-pending.ts +371 -5
- package/src/mesh/mesh-events.ts +2 -0
- package/src/mesh/mesh-node-identity.ts +77 -6
- package/src/mesh/mesh-reconcile-loop.ts +8 -1
- package/src/mesh/mesh-runtime-store.ts +30 -0
- package/src/repo-mesh-types.ts +79 -0
|
@@ -52,6 +52,31 @@ export interface PendingEventEmitHint {
|
|
|
52
52
|
/** coordinatorRunId to fold into the derived identity when the event lacks one. */
|
|
53
53
|
coordinatorRunId?: string;
|
|
54
54
|
}
|
|
55
|
+
/** Observability counters for the accept-and-warn rollout. Read by tests and (later,
|
|
56
|
+
* B4) surfaced in mesh_status. Process-lifetime totals — never reset in production. */
|
|
57
|
+
declare const meshV2DrainCounters: {
|
|
58
|
+
/** v2 events that passed validation and unicast/broadcast routing → delivered. */
|
|
59
|
+
v2Delivered: number;
|
|
60
|
+
/** v2 unicast events skipped because intendedFor addressed another coordinator. */
|
|
61
|
+
v2RoutedAway: number;
|
|
62
|
+
/** v2 events skipped because their eventId was already drained (idempotency). */
|
|
63
|
+
v2DedupSkipped: number;
|
|
64
|
+
/** v2 events that failed assertPendingMeshCoordinatorEventV2 but were PASSED
|
|
65
|
+
* THROUGH (accept mode). Non-zero here is the rollout signal that a producer
|
|
66
|
+
* emits a malformed envelope. */
|
|
67
|
+
v2ValidationFailedAccepted: number;
|
|
68
|
+
/** unicast events re-attributed to the drainer via daemon-core match (a
|
|
69
|
+
* coordinatorRunId change orphaned them). */
|
|
70
|
+
v2ReattributedToDrainer: number;
|
|
71
|
+
/** v1 (unversioned) events passed through as broadcast (rollout baseline). */
|
|
72
|
+
v1BroadcastAccepted: number;
|
|
73
|
+
};
|
|
74
|
+
/** Test/observability accessor for the v2 drain counters (snapshot copy). */
|
|
75
|
+
export declare function getMeshV2DrainCounters(): Readonly<typeof meshV2DrainCounters>;
|
|
76
|
+
/** Test helper: zero the v2 drain counters so a test starts from a clean slate. */
|
|
77
|
+
export declare function __resetMeshV2DrainCountersForTests(): void;
|
|
78
|
+
/** Test helper: clear the one-shot WARN dedup set. */
|
|
79
|
+
export declare function __resetMeshV2WarnDedupForTests(): void;
|
|
55
80
|
export declare function readRefineJobId(event: {
|
|
56
81
|
metadataEvent?: Record<string, unknown>;
|
|
57
82
|
} | Record<string, unknown>): string;
|
|
@@ -73,6 +98,19 @@ export declare function hasPendingCoordinatorEventDuplicate(event: PendingMeshCo
|
|
|
73
98
|
* key is stable across re-delivery. An already-stamped event is returned as-is.
|
|
74
99
|
*/
|
|
75
100
|
export declare function stampPendingEventV2(event: PendingMeshCoordinatorEvent, hint?: PendingEventEmitHint): PendingMeshCoordinatorEvent;
|
|
101
|
+
/**
|
|
102
|
+
* Copy the v2 envelope fields that are present on `event` onto a flat wire
|
|
103
|
+
* payload. Only sets a field when it is present, so a v1 event contributes
|
|
104
|
+
* nothing (the payload stays v1-shaped and version-skew safe).
|
|
105
|
+
*/
|
|
106
|
+
export declare function serializeV2EnvelopeToWire(event: PendingMeshCoordinatorEvent): Record<string, unknown>;
|
|
107
|
+
/**
|
|
108
|
+
* Restore the v2 envelope fields from a flat wire payload for a re-queue. Only
|
|
109
|
+
* returns fields that survive validation; a payload missing/malforming a field
|
|
110
|
+
* yields a partial (or empty) object so the re-queue path stays v1-safe. The
|
|
111
|
+
* eventId is returned verbatim — its preservation is the idempotency guarantee.
|
|
112
|
+
*/
|
|
113
|
+
export declare function readV2EnvelopeFromWire(payload: Record<string, unknown>): Partial<Pick<PendingMeshCoordinatorEvent, 'protocolVersion' | 'eventId' | 'scope' | 'dispatchedBy' | 'intendedFor'>>;
|
|
76
114
|
export declare function queuePendingMeshCoordinatorEvent(rawEvent: PendingMeshCoordinatorEvent, hint?: PendingEventEmitHint): boolean;
|
|
77
115
|
/**
|
|
78
116
|
* Drain and return pending coordinator events for meshId, removing the drained
|
|
@@ -87,6 +125,7 @@ export declare function queuePendingMeshCoordinatorEvent(rawEvent: PendingMeshCo
|
|
|
87
125
|
*/
|
|
88
126
|
export declare function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>, opts?: {
|
|
89
127
|
onlyEvents?: ReadonlySet<string>;
|
|
128
|
+
drainerIdentity?: CoordinatorIdentity;
|
|
90
129
|
}): PendingMeshCoordinatorEvent[];
|
|
91
130
|
/**
|
|
92
131
|
* FALSE-BLOCKER-CLONE-QUEUE: retract any still-UNDELIVERED `mesh:dispatch_blocked`
|
|
@@ -103,7 +142,9 @@ export declare function drainPendingMeshCoordinatorEvents(meshId?: string, coord
|
|
|
103
142
|
*/
|
|
104
143
|
export declare function retractPendingDispatchBlockedEvent(meshId: string | undefined, taskId: string | undefined, coordinatorDaemonId?: string): number;
|
|
105
144
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
106
|
-
export declare function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string
|
|
145
|
+
export declare function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string | ReadonlyArray<string>, opts?: {
|
|
146
|
+
drainerIdentity?: CoordinatorIdentity;
|
|
147
|
+
}): readonly PendingMeshCoordinatorEvent[];
|
|
107
148
|
/**
|
|
108
149
|
* Test helper: purge all pending-event state for a mesh — SQLite rows
|
|
109
150
|
* (including drained fingerprint history) and JSONL files.
|
|
@@ -111,3 +152,4 @@ export declare function getPendingMeshCoordinatorEvents(meshId?: string, coordin
|
|
|
111
152
|
export declare function __clearMeshPendingEventsForTests(meshId: string): void;
|
|
112
153
|
/** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
|
|
113
154
|
export declare function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void;
|
|
155
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
2
|
-
export { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, } from './mesh-events-pending.js';
|
|
2
|
+
export { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, serializeV2EnvelopeToWire, readV2EnvelopeFromWire, } from './mesh-events-pending.js';
|
|
3
3
|
export { reconcileDirectDispatchCompletionFromTranscript, } from './mesh-events-stale.js';
|
|
4
4
|
export { setupMeshReconcileLoop, runMeshReconcileTick, resolveCoordinatorDrainDeliverability, shouldHoldPendingDrainForBusyLocalCoordinator, } from './mesh-reconcile-loop.js';
|
|
5
5
|
export type { MeshQueueTriggerResult } from './mesh-events-coordinator.js';
|
|
@@ -49,6 +49,8 @@ export declare function recordInlineMeshDirectGitTruth(node: any, git: Record<st
|
|
|
49
49
|
reporterPlatform: string | null;
|
|
50
50
|
reporterArch: string | null;
|
|
51
51
|
reporterMachineNickname: string | null;
|
|
52
|
+
reporterProviderVersions: Record<string, string> | null;
|
|
53
|
+
reporterDaemonBuildVersion: string | null;
|
|
52
54
|
};
|
|
53
55
|
/**
|
|
54
56
|
* Persist the live self-reported platform/arch onto the local meshes.json node
|
|
@@ -65,6 +67,8 @@ export declare function persistNodeReporterPlatform(meshSource: 'inline_cache' |
|
|
|
65
67
|
reporterPlatform: string | null;
|
|
66
68
|
reporterArch: string | null;
|
|
67
69
|
reporterMachineNickname?: string | null;
|
|
70
|
+
reporterProviderVersions?: Record<string, string> | null;
|
|
71
|
+
reporterDaemonBuildVersion?: string | null;
|
|
68
72
|
}): void;
|
|
69
73
|
export declare function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean;
|
|
70
74
|
export declare function readInlineMeshNodeId(node: any): string;
|
|
@@ -425,6 +425,23 @@ export declare class MeshRuntimeStore {
|
|
|
425
425
|
payload: unknown;
|
|
426
426
|
}>;
|
|
427
427
|
hasPendingEventFingerprint(meshId: string, fingerprint: string): boolean;
|
|
428
|
+
/**
|
|
429
|
+
* B3a — v2 eventId idempotency. Returns true when a row with this event_id has
|
|
430
|
+
* ALREADY been drained (drained = 1) for the mesh. Drained rows are retained
|
|
431
|
+
* (soft-marked, not deleted until mesh deletion), so this is a durable, restart-
|
|
432
|
+
* surviving dedup: a v2 event whose eventId was already consumed is skipped on
|
|
433
|
+
* re-delivery even when its content fingerprint differs. Scoped by mesh_id +
|
|
434
|
+
* the partial event_id index (idx_mesh_pending_events_event_id).
|
|
435
|
+
*/
|
|
436
|
+
hasDrainedEventId(meshId: string, eventId: string): boolean;
|
|
437
|
+
/**
|
|
438
|
+
* B3a — snapshot of the v2 event_ids ALREADY drained (drained = 1) for the mesh.
|
|
439
|
+
* Taken BEFORE a drain call marks the current batch drained=1, so the resulting
|
|
440
|
+
* set names only PRIOR drains — the re-delivery dedup baseline. (Reading it after
|
|
441
|
+
* the drain would self-match the batch's own freshly-drained rows.) Non-v2 rows
|
|
442
|
+
* have a NULL event_id and are excluded by the index/WHERE.
|
|
443
|
+
*/
|
|
444
|
+
drainedEventIdsForMesh(meshId: string): Set<string>;
|
|
428
445
|
upsertMission(mission: {
|
|
429
446
|
id: string;
|
|
430
447
|
meshId: string;
|
|
@@ -447,6 +447,23 @@ export interface RepoMeshNodeCapabilities {
|
|
|
447
447
|
canPush?: boolean;
|
|
448
448
|
readOnly?: boolean;
|
|
449
449
|
userLabels?: string[];
|
|
450
|
+
/**
|
|
451
|
+
* Detected provider CLI/ACP versions on this node, keyed by provider id
|
|
452
|
+
* (e.g. `{ 'claude-cli': '1.2.3', 'codex-cli': '0.9.0' }`). Populated from the
|
|
453
|
+
* same CLI detection pass that feeds providerPriority (see buildProviderVersions
|
|
454
|
+
* over detectCLIs' CLIInfo[]). Absent/undefined when detection has not run or a
|
|
455
|
+
* daemon predates the exposure — never a hard signal, purely observability so a
|
|
456
|
+
* coordinator can spot a provider-version skew across nodes before dispatch.
|
|
457
|
+
* Additive: existing status consumers ignore it.
|
|
458
|
+
*/
|
|
459
|
+
providerVersions?: Record<string, string>;
|
|
460
|
+
/**
|
|
461
|
+
* The daemon build version (package.json version baked into the running bundle,
|
|
462
|
+
* see getDaemonBuildInfo().version) that detected the above providerVersions.
|
|
463
|
+
* Complements the commit-level daemonBuild stamp with a human-readable version
|
|
464
|
+
* for node-card rendering. Absent when the build define was not injected.
|
|
465
|
+
*/
|
|
466
|
+
daemonBuildVersion?: string;
|
|
450
467
|
}
|
|
451
468
|
export interface DetectedCommand {
|
|
452
469
|
command: string;
|
|
@@ -580,6 +597,19 @@ export interface LocalMeshNodeEntry {
|
|
|
580
597
|
*/
|
|
581
598
|
reportedPlatform?: string;
|
|
582
599
|
reportedArch?: string;
|
|
600
|
+
/**
|
|
601
|
+
* Live, self-healed provider CLI/ACP versions reported by the daemon that owns
|
|
602
|
+
* this node's workspace, carried on the git_status envelope (reporterProviderVersions)
|
|
603
|
+
* and persisted by the coordinator on each direct git probe — mirrors the
|
|
604
|
+
* reportedPlatform/reportedArch self-heal pattern. Auto-detected truth, overwritten
|
|
605
|
+
* by the next report so a stale value never sticks. Absent until the first probe
|
|
606
|
+
* carrying versions succeeds. Surfaced as RepoMeshNodeStatus.providerVersions.
|
|
607
|
+
*/
|
|
608
|
+
reportedProviderVersions?: Record<string, string>;
|
|
609
|
+
/** Live, self-healed daemon build version (getDaemonBuildInfo().version) of the
|
|
610
|
+
* owning daemon, carried on the git_status envelope (reporterDaemonBuildVersion)
|
|
611
|
+
* alongside the provider versions. Absent until first reported. */
|
|
612
|
+
reportedDaemonBuildVersion?: string;
|
|
583
613
|
/**
|
|
584
614
|
* The operator-set machine nickname (config.machineNickname) of the daemon
|
|
585
615
|
* that owns this node's workspace. The local coordinator stamps its own
|
|
@@ -697,6 +727,45 @@ export interface RepoMeshStatus {
|
|
|
697
727
|
* optional. Mirrors the MCP `mesh_status` tool's `magiActivity` field.
|
|
698
728
|
*/
|
|
699
729
|
magiActivity?: MeshMagiActivitySummary[];
|
|
730
|
+
/**
|
|
731
|
+
* T7 (visibility 7-2b): provider CLI/ACP version skew across nodes. Each entry
|
|
732
|
+
* names a provider running ≥2 distinct versions across the nodes that reported
|
|
733
|
+
* it, with the node ids per version. Observational only — never a dispatch
|
|
734
|
+
* blocker. Omitted when every reported provider is uniform (or none reported).
|
|
735
|
+
* Mirrors the MCP `mesh_status` tool's `providerVersionSkew` field.
|
|
736
|
+
*/
|
|
737
|
+
providerVersionSkew?: MeshProviderVersionSkew[];
|
|
738
|
+
/** Human-readable companion warning to providerVersionSkew. Omitted when no skew. */
|
|
739
|
+
providerVersionSkewWarning?: string;
|
|
740
|
+
/**
|
|
741
|
+
* T7 (B4): mesh-protocol-v2 adoption metrics for the batch of pending events
|
|
742
|
+
* surfaced in the drain backing this status. Snapshot, not a durable counter.
|
|
743
|
+
* Omitted when nothing was drained. Mirrors the MCP tool's meshProtocolMetrics.
|
|
744
|
+
*/
|
|
745
|
+
meshProtocolMetrics?: MeshProtocolMetrics;
|
|
746
|
+
}
|
|
747
|
+
/** One provider's version skew across mesh nodes (see RepoMeshStatus.providerVersionSkew). */
|
|
748
|
+
export interface MeshProviderVersionSkew {
|
|
749
|
+
/** Provider id (e.g. 'claude-cli'). */
|
|
750
|
+
provider: string;
|
|
751
|
+
/** Each distinct detected version and the node ids running it. */
|
|
752
|
+
versions: Array<{
|
|
753
|
+
version: string;
|
|
754
|
+
nodeIds: string[];
|
|
755
|
+
}>;
|
|
756
|
+
}
|
|
757
|
+
/** Mesh-protocol-v2 adoption snapshot over one drain (see RepoMeshStatus.meshProtocolMetrics). */
|
|
758
|
+
export interface MeshProtocolMetrics {
|
|
759
|
+
/** Total pending events surfaced in the drain. */
|
|
760
|
+
total: number;
|
|
761
|
+
/** Count carrying a v2 envelope (protocolVersion '2.0'). */
|
|
762
|
+
v2: number;
|
|
763
|
+
/** Count still on v1 (unstamped). */
|
|
764
|
+
v1: number;
|
|
765
|
+
/** v2/total, rounded to 2 decimals (0 when total is 0). */
|
|
766
|
+
v2Ratio: number;
|
|
767
|
+
/** Scope breakdown of the v2 events (unicast/broadcast/system/unspecified → count). */
|
|
768
|
+
scopes: Record<string, number>;
|
|
700
769
|
}
|
|
701
770
|
export type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
|
|
702
771
|
import type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
|
|
@@ -741,6 +810,17 @@ export interface RepoMeshNodeStatus {
|
|
|
741
810
|
*/
|
|
742
811
|
gitProbePending?: boolean;
|
|
743
812
|
providers: string[];
|
|
813
|
+
/**
|
|
814
|
+
* Detected provider CLI/ACP versions on this node, keyed by provider id. Mirrors
|
|
815
|
+
* RepoMeshNodeCapabilities.providerVersions onto the status snapshot so the mesh
|
|
816
|
+
* UI / coordinator prompt can render per-provider versions and flag a version
|
|
817
|
+
* skew across nodes. Optional — omitted by daemons predating the exposure or when
|
|
818
|
+
* detection has not run. Additive; existing consumers ignore it. */
|
|
819
|
+
providerVersions?: Record<string, string>;
|
|
820
|
+
/** Human-readable daemon build version (getDaemonBuildInfo().version) of the
|
|
821
|
+
* daemon that owns this node. Complements the per-daemon commit stamp
|
|
822
|
+
* (daemonBuilds) for node-card display. Omitted when unknown. */
|
|
823
|
+
daemonBuildVersion?: string;
|
|
744
824
|
activeSessions: string[];
|
|
745
825
|
activeSessionDetails?: RepoMeshSessionStatus[];
|
|
746
826
|
providerPriority?: string[];
|
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.461",
|
|
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.461",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.461",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -25,7 +25,8 @@ import { VersionArchive, detectAllVersions } from '../providers/version-archive.
|
|
|
25
25
|
import { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
26
26
|
import { DevServer } from '../daemon/dev-server.js';
|
|
27
27
|
import { detectIDEs, type IDEInfo } from '../detection/ide-detector.js';
|
|
28
|
-
import { detectCLI, detectCLIs } from '../detection/cli-detector.js';
|
|
28
|
+
import { detectCLI, detectCLIs, getCachedProviderVersions } from '../detection/cli-detector.js';
|
|
29
|
+
import { getDaemonBuildInfo } from '../build-info.js';
|
|
29
30
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
30
31
|
import { LOG, installGlobalInterceptor } from '../logging/logger.js';
|
|
31
32
|
import {
|
|
@@ -304,7 +305,19 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
304
305
|
providerLoader,
|
|
305
306
|
instanceManager,
|
|
306
307
|
sessionRegistry,
|
|
307
|
-
gitCommandServices: createDefaultGitCommandServices(
|
|
308
|
+
gitCommandServices: createDefaultGitCommandServices({
|
|
309
|
+
// T7: fold this daemon's cached provider versions + build version onto the
|
|
310
|
+
// git_status envelope so the mesh coordinator self-heals each node's
|
|
311
|
+
// providerVersions. Non-blocking: reads a TTL cache, lazily refreshed.
|
|
312
|
+
getReporterProviderVersions: () => {
|
|
313
|
+
const providerVersions = getCachedProviderVersions(providerLoader);
|
|
314
|
+
const daemonBuildVersion = getDaemonBuildInfo().version;
|
|
315
|
+
return {
|
|
316
|
+
...(Object.keys(providerVersions).length > 0 ? { providerVersions } : {}),
|
|
317
|
+
...(daemonBuildVersion && daemonBuildVersion !== 'unknown' ? { daemonBuildVersion } : {}),
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
}),
|
|
308
321
|
onProviderSettingChanged: async (providerType) => {
|
|
309
322
|
await refreshProviderAvailability(providerType);
|
|
310
323
|
config.onStatusChange?.();
|
|
@@ -340,6 +340,16 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
|
|
|
340
340
|
machineStatus: node.machineStatus,
|
|
341
341
|
health: 'unknown',
|
|
342
342
|
providers: node.providers || [],
|
|
343
|
+
// T7: surface self-healed provider versions + build version (from the
|
|
344
|
+
// git_status envelope, persisted on the node) so the coordinator/UI can
|
|
345
|
+
// spot a provider-version skew across nodes. Additive; omitted when a
|
|
346
|
+
// node has never reported them.
|
|
347
|
+
...(node.reportedProviderVersions && typeof node.reportedProviderVersions === 'object'
|
|
348
|
+
? { providerVersions: node.reportedProviderVersions }
|
|
349
|
+
: {}),
|
|
350
|
+
...(typeof node.reportedDaemonBuildVersion === 'string' && node.reportedDaemonBuildVersion
|
|
351
|
+
? { daemonBuildVersion: node.reportedDaemonBuildVersion }
|
|
352
|
+
: {}),
|
|
343
353
|
providerPriority,
|
|
344
354
|
activeSessions: [],
|
|
345
355
|
activeSessionDetails: [],
|
|
@@ -583,6 +583,13 @@ export function updateNode(
|
|
|
583
583
|
* git_status envelope. Persisted so the friendly label survives across
|
|
584
584
|
* coordinator restarts (mirrors reportedPlatform/reportedArch). */
|
|
585
585
|
reportedMachineNickname?: string;
|
|
586
|
+
/** Owning daemon's self-reported provider CLI/ACP versions + build version,
|
|
587
|
+
* carried on the git_status envelope. Persisted distinctly from userOverrides
|
|
588
|
+
* (auto-detected observability, not operator intent), mirroring the
|
|
589
|
+
* reportedPlatform/reportedArch self-heal so the value survives restarts and
|
|
590
|
+
* is overwritten by the next report. */
|
|
591
|
+
reportedProviderVersions?: Record<string, string>;
|
|
592
|
+
reportedDaemonBuildVersion?: string;
|
|
586
593
|
},
|
|
587
594
|
): LocalMeshNodeEntry | undefined {
|
|
588
595
|
const config = loadMeshConfig();
|
|
@@ -596,6 +603,12 @@ export function updateNode(
|
|
|
596
603
|
if (opts.reportedPlatform && opts.reportedPlatform.trim()) node.reportedPlatform = opts.reportedPlatform.trim();
|
|
597
604
|
if (opts.reportedArch && opts.reportedArch.trim()) node.reportedArch = opts.reportedArch.trim();
|
|
598
605
|
if (opts.reportedMachineNickname && opts.reportedMachineNickname.trim()) node.machineNickname = opts.reportedMachineNickname.trim();
|
|
606
|
+
if (opts.reportedProviderVersions && Object.keys(opts.reportedProviderVersions).length > 0) {
|
|
607
|
+
node.reportedProviderVersions = { ...opts.reportedProviderVersions };
|
|
608
|
+
}
|
|
609
|
+
if (opts.reportedDaemonBuildVersion && opts.reportedDaemonBuildVersion.trim()) {
|
|
610
|
+
node.reportedDaemonBuildVersion = opts.reportedDaemonBuildVersion.trim();
|
|
611
|
+
}
|
|
599
612
|
if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
|
|
600
613
|
if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
|
|
601
614
|
if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
|
|
@@ -200,3 +200,69 @@ export async function detectCLI(
|
|
|
200
200
|
const all = await detectCLIs(providerLoader, options);
|
|
201
201
|
return all.find((c) => c.id === resolvedId && c.installed) || null;
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Fold a detected CLIInfo[] into the `{ providerId: version }` map surfaced as a
|
|
206
|
+
* node's providerVersions (see RepoMeshNodeCapabilities.providerVersions). Only
|
|
207
|
+
* installed providers that produced a parseable version string are included, so a
|
|
208
|
+
* missing entry means "not installed or version unknown" — never a fabricated value.
|
|
209
|
+
* Pure/deterministic: the same detection input always yields the same map, which is
|
|
210
|
+
* what the git_status envelope carries and the mesh_status skew check compares.
|
|
211
|
+
*/
|
|
212
|
+
export function buildProviderVersions(detected: CLIInfo[]): Record<string, string> {
|
|
213
|
+
const out: Record<string, string> = {};
|
|
214
|
+
for (const cli of detected) {
|
|
215
|
+
if (!cli.installed) continue;
|
|
216
|
+
const version = typeof cli.version === 'string' ? cli.version.trim() : '';
|
|
217
|
+
if (!version) continue;
|
|
218
|
+
out[cli.id] = version;
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Cached provider-versions snapshot (mesh visibility, T7) ────────────────
|
|
224
|
+
//
|
|
225
|
+
// A node's providerVersions rides the git_status envelope, which a coordinator
|
|
226
|
+
// probes on every graph refresh. Running the full `--version` exec fan-out on
|
|
227
|
+
// each probe would be far too costly (one spawn per provider per probe), so the
|
|
228
|
+
// snapshot is memoized with a TTL and refreshed lazily in the background: a probe
|
|
229
|
+
// reads the current cache immediately (empty until the first refresh completes)
|
|
230
|
+
// and kicks off a refresh only when the cache is stale, never blocking the git
|
|
231
|
+
// response on version detection. Best-effort observability — a cold/empty map
|
|
232
|
+
// simply omits providerVersions from that probe, and the next probe carries it.
|
|
233
|
+
const PROVIDER_VERSIONS_TTL_MS = 5 * 60 * 1000; // 5 min — provider installs change rarely
|
|
234
|
+
let cachedProviderVersions: Record<string, string> = {};
|
|
235
|
+
let cachedProviderVersionsAt = 0;
|
|
236
|
+
let providerVersionsRefreshInFlight: Promise<void> | null = null;
|
|
237
|
+
|
|
238
|
+
function refreshProviderVersionsSnapshot(providerLoader?: ProviderLoader): Promise<void> {
|
|
239
|
+
if (providerVersionsRefreshInFlight) return providerVersionsRefreshInFlight;
|
|
240
|
+
providerVersionsRefreshInFlight = (async () => {
|
|
241
|
+
try {
|
|
242
|
+
const detected = await detectCLIs(providerLoader, { includeVersion: true });
|
|
243
|
+
cachedProviderVersions = buildProviderVersions(detected);
|
|
244
|
+
cachedProviderVersionsAt = Date.now();
|
|
245
|
+
} catch {
|
|
246
|
+
// Best-effort: keep the last good snapshot on failure.
|
|
247
|
+
} finally {
|
|
248
|
+
providerVersionsRefreshInFlight = null;
|
|
249
|
+
}
|
|
250
|
+
})();
|
|
251
|
+
return providerVersionsRefreshInFlight;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Return the current cached provider-versions map for this daemon, triggering a
|
|
256
|
+
* lazy background refresh when the snapshot is stale/cold. Never blocks: returns
|
|
257
|
+
* whatever is currently cached (an empty object before the first refresh lands).
|
|
258
|
+
* This is the source the git_status envelope reads to self-report a node's
|
|
259
|
+
* provider versions to the mesh coordinator.
|
|
260
|
+
*/
|
|
261
|
+
export function getCachedProviderVersions(providerLoader?: ProviderLoader): Record<string, string> {
|
|
262
|
+
const stale = Date.now() - cachedProviderVersionsAt > PROVIDER_VERSIONS_TTL_MS;
|
|
263
|
+
if (stale) {
|
|
264
|
+
// Fire-and-forget; the current (possibly empty) snapshot is returned now.
|
|
265
|
+
void refreshProviderVersionsSnapshot(providerLoader);
|
|
266
|
+
}
|
|
267
|
+
return { ...cachedProviderVersions };
|
|
268
|
+
}
|
package/src/git/git-commands.ts
CHANGED
|
@@ -102,6 +102,14 @@ export interface GitCommandServices {
|
|
|
102
102
|
checkoutFiles?: (params: { workspace: string; paths: string[] }) => Promise<{ checkedOut: string[] }>;
|
|
103
103
|
getRemoteUrl?: (params: { workspace: string; remote?: string }) => Promise<{ remoteUrl: string; remote: string }>;
|
|
104
104
|
push?: (params: { workspace: string; remote?: string; branch?: string; setUpstream?: boolean }) => Promise<GitPushResult>;
|
|
105
|
+
/**
|
|
106
|
+
* Best-effort, non-blocking source of this daemon's detected provider CLI/ACP
|
|
107
|
+
* versions (keyed by provider id) + its build version, folded onto the git_status
|
|
108
|
+
* envelope so a mesh coordinator can self-heal each node's providerVersions the
|
|
109
|
+
* same way it does platform/arch. Wired at daemon boot from the cached CLI
|
|
110
|
+
* detection snapshot; omitted (⇒ versions not reported) when unavailable.
|
|
111
|
+
*/
|
|
112
|
+
getReporterProviderVersions?: () => { providerVersions?: Record<string, string>; daemonBuildVersion?: string };
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
type GitCommandFailure = {
|
|
@@ -116,7 +124,7 @@ type GitCommandSuccess =
|
|
|
116
124
|
// node's userOverrides.platform/arch (the fields capability-tag routing reads).
|
|
117
125
|
// reporterMachineNickname carries the responding daemon's config.machineNickname
|
|
118
126
|
// so the coordinator can populate node.machineNickname → the friendly display label.
|
|
119
|
-
| { success: true; status: GitRepoStatus; reporterPlatform?: string; reporterArch?: string; reporterMachineNickname?: string }
|
|
127
|
+
| { success: true; status: GitRepoStatus; reporterPlatform?: string; reporterArch?: string; reporterMachineNickname?: string; reporterProviderVersions?: Record<string, string>; reporterDaemonBuildVersion?: string }
|
|
120
128
|
| { success: true; diffSummary: GitDiffSummary }
|
|
121
129
|
| { success: true; diff: GitFileDiff }
|
|
122
130
|
| { success: true; snapshot: GitSnapshot }
|
|
@@ -179,7 +187,9 @@ const defaultSnapshotStore = createGitSnapshotStore({
|
|
|
179
187
|
getDiffSummary: (workspace) => getGitDiffSummary(workspace),
|
|
180
188
|
});
|
|
181
189
|
|
|
182
|
-
export function createDefaultGitCommandServices(
|
|
190
|
+
export function createDefaultGitCommandServices(
|
|
191
|
+
overrides?: Pick<GitCommandServices, 'getReporterProviderVersions'>,
|
|
192
|
+
): GitCommandServices {
|
|
183
193
|
return {
|
|
184
194
|
getStatus: ({ workspace, refreshUpstream }) => getGitRepoStatus(workspace, { refreshUpstream }),
|
|
185
195
|
getDiffSummary: ({ workspace, base }) => getGitDiffSummary(workspace, base ? { baseRef: base } : {}),
|
|
@@ -199,6 +209,9 @@ export function createDefaultGitCommandServices(): GitCommandServices {
|
|
|
199
209
|
getRemoteUrl: async ({ workspace, remote = 'origin' }) => gitGetRemoteUrl(workspace, remote),
|
|
200
210
|
push: async ({ workspace, remote = 'origin', branch, setUpstream = false }) =>
|
|
201
211
|
gitPush(workspace, remote, branch, setUpstream),
|
|
212
|
+
...(overrides?.getReporterProviderVersions
|
|
213
|
+
? { getReporterProviderVersions: overrides.getReporterProviderVersions }
|
|
214
|
+
: {}),
|
|
202
215
|
};
|
|
203
216
|
}
|
|
204
217
|
|
|
@@ -328,12 +341,32 @@ export async function handleGitCommand(
|
|
|
328
341
|
return undefined;
|
|
329
342
|
}
|
|
330
343
|
})();
|
|
344
|
+
// Provider versions + build version ride the same self-report channel (T7
|
|
345
|
+
// visibility). Best-effort and non-blocking: the service reads a cached
|
|
346
|
+
// snapshot, so a cold cache simply omits the fields on this probe.
|
|
347
|
+
const reporterVersions = (() => {
|
|
348
|
+
try {
|
|
349
|
+
return services.getReporterProviderVersions?.() ?? {};
|
|
350
|
+
} catch {
|
|
351
|
+
return {};
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
354
|
+
const reporterProviderVersions =
|
|
355
|
+
reporterVersions.providerVersions && Object.keys(reporterVersions.providerVersions).length > 0
|
|
356
|
+
? reporterVersions.providerVersions
|
|
357
|
+
: undefined;
|
|
358
|
+
const reporterDaemonBuildVersion =
|
|
359
|
+
typeof reporterVersions.daemonBuildVersion === 'string' && reporterVersions.daemonBuildVersion.trim()
|
|
360
|
+
? reporterVersions.daemonBuildVersion.trim()
|
|
361
|
+
: undefined;
|
|
331
362
|
return {
|
|
332
363
|
success: true,
|
|
333
364
|
status,
|
|
334
365
|
reporterPlatform: process.platform,
|
|
335
366
|
reporterArch: process.arch,
|
|
336
367
|
...(reporterMachineNickname ? { reporterMachineNickname } : {}),
|
|
368
|
+
...(reporterProviderVersions ? { reporterProviderVersions } : {}),
|
|
369
|
+
...(reporterDaemonBuildVersion ? { reporterDaemonBuildVersion } : {}),
|
|
337
370
|
};
|
|
338
371
|
}
|
|
339
372
|
|
package/src/index.ts
CHANGED
|
@@ -295,7 +295,7 @@ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHost
|
|
|
295
295
|
// export type { MeshGraph, MeshGraphNode, MeshGraphEdge, MeshGraphNodeType, MeshGraphEdgeType } from './mesh/mesh-visualization.js';
|
|
296
296
|
|
|
297
297
|
// ── Mesh Events ──
|
|
298
|
-
export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, reconcileDirectDispatchCompletionFromTranscript } from './mesh/mesh-events.js';
|
|
298
|
+
export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, serializeV2EnvelopeToWire, readV2EnvelopeFromWire, reconcileDirectDispatchCompletionFromTranscript } from './mesh/mesh-events.js';
|
|
299
299
|
export type { PendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
|
|
300
300
|
// The coordinator-side preview surfaced from a worker's completion/status event
|
|
301
301
|
// (finalSummary / workerResult.summary / lastMessagePreview). Same data the mobile
|
|
@@ -392,9 +392,27 @@ function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
|
|
|
392
392
|
? `sessions: ${n.activeSessions.join(', ')}`
|
|
393
393
|
: 'no active sessions';
|
|
394
394
|
const branch = n.git?.branch ? `branch: \`${n.git.branch}\`` : '';
|
|
395
|
+
// Render each provider with its detected version when known (T7 visibility)
|
|
396
|
+
// so the coordinator can eyeball a per-node provider-version skew inline —
|
|
397
|
+
// e.g. `claude-cli@1.2.3`. Providers without a reported version render bare.
|
|
398
|
+
const providerVersions = n.providerVersions && typeof n.providerVersions === 'object'
|
|
399
|
+
? n.providerVersions
|
|
400
|
+
: undefined;
|
|
401
|
+
const providersRendered = n.providers?.length
|
|
402
|
+
? n.providers
|
|
403
|
+
.map((p) => {
|
|
404
|
+
const version = providerVersions?.[p];
|
|
405
|
+
return version ? `${p}@${version}` : p;
|
|
406
|
+
})
|
|
407
|
+
.join(', ')
|
|
408
|
+
: '';
|
|
409
|
+
const buildVersion = typeof n.daemonBuildVersion === 'string' && n.daemonBuildVersion
|
|
410
|
+
? `build: ${n.daemonBuildVersion}`
|
|
411
|
+
: '';
|
|
395
412
|
const context = [
|
|
396
413
|
n.daemonId ? `daemon: \`${n.daemonId}\`` : '',
|
|
397
|
-
|
|
414
|
+
providersRendered ? `providers: ${providersRendered}` : '',
|
|
415
|
+
buildVersion,
|
|
398
416
|
].filter(Boolean).join(' | ');
|
|
399
417
|
lines.push(`- ${healthIcon} **${n.machineLabel}** (nodeId: \`${n.nodeId}\`)`);
|
|
400
418
|
lines.push(` workspace: \`${n.workspace}\`${context ? ` | ${context}` : ''} | ${branch} | ${sessions}`);
|
|
@@ -7,7 +7,7 @@ import type { SessionRecoveryContext } from './mesh-ledger.js';
|
|
|
7
7
|
import { updateSessionTaskStatus, enqueueTask, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches, hasPendingDependents, getQueue } from './mesh-work-queue.js';
|
|
8
8
|
import { markSessionDeliveriesTerminal, updateSessionDeliveryStatus, recordCompletionConflict } from './mesh-delivery-policy.js';
|
|
9
9
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
10
|
-
import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, type PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
10
|
+
import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, readV2EnvelopeFromWire, type PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
11
11
|
import type { ProviderInstance } from '../providers/provider-instance.js';
|
|
12
12
|
import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent, isUnroutableDelegateRejection } from './mesh-routing.js';
|
|
13
13
|
import { resolveMeshHostStatus } from './mesh-host-ownership.js';
|
|
@@ -681,6 +681,13 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
681
681
|
nodeLabel: string;
|
|
682
682
|
event: string;
|
|
683
683
|
metadataEvent: Record<string, unknown>;
|
|
684
|
+
// T4 (B3b): v2 envelope restored from a remote (P2P) relay's flat payload. Only the
|
|
685
|
+
// relay path (handleMeshForwardEvent) sets it; the in-process forward path leaves it
|
|
686
|
+
// undefined and the local emit stamp applies as usual. When present with a preserved
|
|
687
|
+
// eventId, it is spread onto the re-queued pending event so stampPendingEventV2's
|
|
688
|
+
// already-stamped short-circuit keeps the ORIGINAL eventId (cross-machine idempotency).
|
|
689
|
+
v2Envelope?: Partial<Pick<PendingMeshCoordinatorEvent,
|
|
690
|
+
'protocolVersion' | 'eventId' | 'scope' | 'dispatchedBy' | 'intendedFor'>>;
|
|
684
691
|
}) {
|
|
685
692
|
const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
686
693
|
const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
@@ -1363,6 +1370,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1363
1370
|
// Top-level session anchor for the local PHASE 2 strict-match on the coordinator
|
|
1364
1371
|
// daemon. Absent → daemon-level broadcast (legacy / single-coordinator path).
|
|
1365
1372
|
...(workerCoordinatorSessionId ? { targetCoordinatorSessionId: workerCoordinatorSessionId } : {}),
|
|
1373
|
+
// T4 (B3b): restore the v2 envelope from the remote relay so the re-queue keeps the
|
|
1374
|
+
// ORIGINAL eventId. Spread LAST so its authoritative eventId/scope/identity win over
|
|
1375
|
+
// any default. queuePendingMeshCoordinatorEvent → stampPendingEventV2 then no-ops
|
|
1376
|
+
// (already-stamped short-circuit) instead of minting a fresh eventId. Empty object
|
|
1377
|
+
// for a v1 relay → unchanged v1 emit-stamp path (version-skew safe).
|
|
1378
|
+
...(args.v2Envelope ?? {}),
|
|
1366
1379
|
};
|
|
1367
1380
|
if (queuePendingMeshCoordinatorEvent(pendingEvent)) {
|
|
1368
1381
|
LOG.info('MeshEvents', `Queued ${args.event} for coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''}${workerCoordinatorSessionId ? `, coordinator session ${workerCoordinatorSessionId}` : ''})`);
|
|
@@ -1518,6 +1531,11 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
1518
1531
|
nodeLabel,
|
|
1519
1532
|
event: eventName,
|
|
1520
1533
|
metadataEvent: buildRelayMetadataEvent(payload),
|
|
1534
|
+
// T4 (B3b): restore the v2 envelope carried at the top level of the relayed flat
|
|
1535
|
+
// payload (buildForwardPayloadFromPending → serializeV2EnvelopeToWire) so the
|
|
1536
|
+
// re-queue preserves the original eventId (idempotency) and unicast routing rather
|
|
1537
|
+
// than re-stamping a fresh v1/broadcast event. Empty for a v1 relay (version-skew safe).
|
|
1538
|
+
v2Envelope: readV2EnvelopeFromWire(payload),
|
|
1521
1539
|
});
|
|
1522
1540
|
}
|
|
1523
1541
|
|