@adhdev/daemon-core 0.9.82-rc.304 → 0.9.82-rc.306
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/index.js +198 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +198 -170
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-missions.d.ts +9 -0
- package/dist/repo-mesh-types.d.ts +7 -2
- package/package.json +2 -2
- package/src/commands/router.ts +165 -68
- package/src/mesh/mesh-missions.ts +18 -2
- package/src/repo-mesh-types.ts +7 -2
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* never stored — it is derived from queue task statuses (mission_id) at
|
|
12
12
|
* query time.
|
|
13
13
|
*/
|
|
14
|
+
import { type MeshMissionStats } from './mesh-task-stats.js';
|
|
14
15
|
export type MeshMissionStatus = 'active' | 'paused' | 'completed' | 'abandoned';
|
|
15
16
|
export declare const MESH_MISSION_STATUSES: MeshMissionStatus[];
|
|
16
17
|
export interface MeshMissionRecord {
|
|
@@ -36,6 +37,13 @@ export interface MeshMissionTaskAggregate {
|
|
|
36
37
|
}
|
|
37
38
|
export interface MeshMissionSummary extends MeshMissionRecord {
|
|
38
39
|
tasks: MeshMissionTaskAggregate;
|
|
40
|
+
/**
|
|
41
|
+
* Operational rollup (durations / attempts) derived from the ledger via
|
|
42
|
+
* computeMeshMissionStats. Optional: only populated by surfaces that opt in
|
|
43
|
+
* (e.g. mesh_status), since the rollup scans a bounded ledger tail per
|
|
44
|
+
* mission. Absent on the lightweight task-aggregate-only summaries.
|
|
45
|
+
*/
|
|
46
|
+
stats?: MeshMissionStats;
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Slim mission summary for the mesh_status compact (default) surface. Drops the
|
|
@@ -83,6 +91,7 @@ export declare function getActiveMeshMissionSummaries(meshId: string): MeshMissi
|
|
|
83
91
|
export declare function getMeshStatusMissionSummaries(meshId: string, options?: {
|
|
84
92
|
historyLimit?: number;
|
|
85
93
|
verbose?: boolean;
|
|
94
|
+
withStats?: boolean;
|
|
86
95
|
}): MeshMissionSummary[] | MeshMissionSlimSummary[];
|
|
87
96
|
/**
|
|
88
97
|
* Read-only mission listing for the mesh_mission_list tool. Returns summaries
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* IMPORTANT: This file must remain runtime-free (types only).
|
|
12
12
|
*/
|
|
13
13
|
import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
|
|
14
|
-
import type { MeshMissionSummary } from './mesh/mesh-missions.js';
|
|
14
|
+
import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
|
|
15
15
|
export interface RepoMesh {
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
@@ -393,8 +393,13 @@ export interface RepoMeshStatus {
|
|
|
393
393
|
* capped, newest-first slice of completed/abandoned history. Omitted by older
|
|
394
394
|
* daemons — the dashboard must treat this as optional and render an empty
|
|
395
395
|
* state when absent. Split on each entry's `status` for live vs. history.
|
|
396
|
+
*
|
|
397
|
+
* Compact (the default) status calls send the slim shape — `goalPreview` +
|
|
398
|
+
* `goalTruncated` instead of the full `goal` — while verbose sends the full
|
|
399
|
+
* `goal`. Consumers must read `goal ?? goalPreview`. Each entry may also carry
|
|
400
|
+
* an optional `stats` operational rollup (durations / retries).
|
|
396
401
|
*/
|
|
397
|
-
missions?: MeshMissionSummary[];
|
|
402
|
+
missions?: (MeshMissionSummary | MeshMissionSlimSummary)[];
|
|
398
403
|
}
|
|
399
404
|
export type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
|
|
400
405
|
import type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
|
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.306",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.306",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/commands/router.ts
CHANGED
|
@@ -1003,17 +1003,83 @@ async function probeRemoteMeshGitStatus(args: {
|
|
|
1003
1003
|
: null;
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
|
+
/** Number of bounded retries after the initial direct-peer git probe attempt. */
|
|
1007
|
+
const MESH_DIRECT_PROBE_MAX_RETRIES = 2;
|
|
1008
|
+
|
|
1009
|
+
function readMeshConnectionState(connection: Record<string, unknown> | null | undefined): string | undefined {
|
|
1010
|
+
return readStringValue((connection as any)?.state);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Probe a remote peer's git_status with a bounded retry budget, but only while
|
|
1015
|
+
* the peer is reported `connected`. A single slow (often TURN-relayed) peer can
|
|
1016
|
+
* exceed one probe window; retrying — with the connection re-checked before each
|
|
1017
|
+
* attempt so we abandon a peer that dropped — recovers it without blocking the
|
|
1018
|
+
* mesh forever. Shared by the bootstrap hydrate path and the per-node render
|
|
1019
|
+
* path so both treat a connected-but-slow peer identically.
|
|
1020
|
+
*
|
|
1021
|
+
* Returns the git status on success, or null if every attempt failed/timed out
|
|
1022
|
+
* (caller decides how to classify). `getConnection` is consulted before each
|
|
1023
|
+
* attempt; a non-`connected` state short-circuits the retry loop (the very first
|
|
1024
|
+
* attempt always runs so a missing connection getter still gets one try).
|
|
1025
|
+
*/
|
|
1026
|
+
async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
1027
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
1028
|
+
daemonId: string;
|
|
1029
|
+
workspace: string;
|
|
1030
|
+
timeoutMs: number;
|
|
1031
|
+
/** Per-attempt timeout for retries (attempts > 0); defaults to timeoutMs. */
|
|
1032
|
+
retryTimeoutMs?: number;
|
|
1033
|
+
getConnection?: (daemonId: string) => Record<string, unknown> | null;
|
|
1034
|
+
onConnection?: (connection: Record<string, unknown>) => void;
|
|
1035
|
+
}): Promise<Record<string, unknown> | null> {
|
|
1036
|
+
for (let attempt = 0; attempt <= MESH_DIRECT_PROBE_MAX_RETRIES; attempt += 1) {
|
|
1037
|
+
if (attempt > 0) {
|
|
1038
|
+
// Re-check liveness before spending another probe window; a peer that
|
|
1039
|
+
// dropped between attempts is not worth retrying.
|
|
1040
|
+
const connection = args.getConnection?.(args.daemonId);
|
|
1041
|
+
if (args.getConnection && readMeshConnectionState(connection) !== 'connected') break;
|
|
1042
|
+
if (connection) args.onConnection?.(connection);
|
|
1043
|
+
// Exponential backoff: 250ms, 500ms before attempts 1 and 2.
|
|
1044
|
+
await new Promise(resolve => setTimeout(resolve, 250 * 2 ** (attempt - 1)));
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
1048
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1049
|
+
daemonId: args.daemonId,
|
|
1050
|
+
workspace: args.workspace,
|
|
1051
|
+
timeoutMs: attempt === 0 ? args.timeoutMs : (args.retryTimeoutMs ?? args.timeoutMs),
|
|
1052
|
+
});
|
|
1053
|
+
if (remoteGit) return remoteGit;
|
|
1054
|
+
} catch {
|
|
1055
|
+
// Timed out or P2P error — fall through to the next bounded attempt.
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1006
1061
|
async function hydrateInlineMeshDirectTruth(args: {
|
|
1007
1062
|
mesh: any;
|
|
1008
1063
|
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
1009
1064
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
1065
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1010
1066
|
statusInstanceId?: string;
|
|
1011
1067
|
localMachineId?: string;
|
|
1068
|
+
// Standing-state model: the default (non-refresh) bootstrap load must NOT
|
|
1069
|
+
// fan out a blocking git_status probe to every peer — a single slow
|
|
1070
|
+
// (TURN-relayed) peer would time out and mark the whole mesh unavailable,
|
|
1071
|
+
// blocking the graph. When false, a non-local node is satisfied from its
|
|
1072
|
+
// held standing git truth (lastGit / cachedStatus reflected via mesh
|
|
1073
|
+
// events) and is never pushed to unavailableNodeIds merely because no live
|
|
1074
|
+
// probe was attempted. Only an explicit refresh (probeRemotePeers=true)
|
|
1075
|
+
// performs the fan-out and classifies an unreachable peer as unavailable.
|
|
1076
|
+
probeRemotePeers: boolean;
|
|
1012
1077
|
}): Promise<{
|
|
1013
1078
|
directEvidenceCount: number;
|
|
1014
1079
|
localConfirmedCount: number;
|
|
1015
1080
|
peerAttemptedCount: number;
|
|
1016
1081
|
peerConfirmedCount: number;
|
|
1082
|
+
standingEvidenceCount: number;
|
|
1017
1083
|
unavailableNodeIds: string[];
|
|
1018
1084
|
}> {
|
|
1019
1085
|
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
@@ -1023,6 +1089,7 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1023
1089
|
localConfirmedCount: 0,
|
|
1024
1090
|
peerAttemptedCount: 0,
|
|
1025
1091
|
peerConfirmedCount: 0,
|
|
1092
|
+
standingEvidenceCount: 0,
|
|
1026
1093
|
unavailableNodeIds: [],
|
|
1027
1094
|
};
|
|
1028
1095
|
}
|
|
@@ -1036,6 +1103,7 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1036
1103
|
let localConfirmedCount = 0;
|
|
1037
1104
|
let peerAttemptedCount = 0;
|
|
1038
1105
|
let peerConfirmedCount = 0;
|
|
1106
|
+
let standingEvidenceCount = 0;
|
|
1039
1107
|
const unavailableNodeIds: string[] = [];
|
|
1040
1108
|
|
|
1041
1109
|
for (const [nodeIndex, node] of nodes.entries()) {
|
|
@@ -1066,36 +1134,64 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1066
1134
|
}
|
|
1067
1135
|
}
|
|
1068
1136
|
|
|
1137
|
+
// Standing-state first: a non-local peer's held git truth (reflected
|
|
1138
|
+
// from its self-emitted mesh events into node.lastGit / cachedStatus)
|
|
1139
|
+
// counts as direct evidence without any probe. On the default load this
|
|
1140
|
+
// is the ONLY thing we consult — no fan-out, so one slow peer can't
|
|
1141
|
+
// block the bootstrap.
|
|
1142
|
+
const standingGit = buildInlineMeshTransitGitStatus(node);
|
|
1143
|
+
if (standingGit) {
|
|
1144
|
+
standingEvidenceCount += 1;
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (!args.probeRemotePeers) {
|
|
1149
|
+
// Default (non-refresh) load: a peer with no held truth yet is left
|
|
1150
|
+
// pending (the per-node loop marks it gitProbePending and the graph
|
|
1151
|
+
// shows setup inventory for it). It is NOT unavailable — the graph
|
|
1152
|
+
// must still render. An explicit refresh will fan out and freshen it.
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1069
1156
|
if (!daemonId || !args.dispatchMeshCommand) {
|
|
1070
1157
|
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
1071
1158
|
continue;
|
|
1072
1159
|
}
|
|
1073
1160
|
|
|
1074
1161
|
peerAttemptedCount += 1;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1162
|
+
// Bounded retry, gated on the peer staying `connected`: a slow
|
|
1163
|
+
// (TURN-relayed) peer that just exceeds one probe window is recovered
|
|
1164
|
+
// instead of being hard-failed. The connection is re-checked before each
|
|
1165
|
+
// retry so a peer that actually dropped is abandoned promptly.
|
|
1166
|
+
const remoteGit = await probeRemoteMeshGitStatusWithRetry({
|
|
1167
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1168
|
+
daemonId,
|
|
1169
|
+
workspace,
|
|
1170
|
+
timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
|
|
1171
|
+
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
1172
|
+
getConnection: args.getMeshPeerConnectionStatus,
|
|
1173
|
+
});
|
|
1174
|
+
if (remoteGit) {
|
|
1175
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1176
|
+
peerConfirmedCount += 1;
|
|
1177
|
+
continue;
|
|
1089
1178
|
}
|
|
1090
1179
|
|
|
1180
|
+
// Invariant: a connected peer that still holds standing git truth is
|
|
1181
|
+
// never classified unavailable (standingGit short-circuited above, so by
|
|
1182
|
+
// here there is no held truth). Only push to unavailable when the peer is
|
|
1183
|
+
// not currently connected, or it is connected but every bounded probe
|
|
1184
|
+
// failed — that is the genuine "connected, no truth, retries exhausted"
|
|
1185
|
+
// case that drives the explicit-refresh hard-fail.
|
|
1091
1186
|
unavailableNodeIds.push(nodeId);
|
|
1092
1187
|
}
|
|
1093
1188
|
|
|
1094
1189
|
return {
|
|
1095
|
-
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
1190
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount + standingEvidenceCount,
|
|
1096
1191
|
localConfirmedCount,
|
|
1097
1192
|
peerAttemptedCount,
|
|
1098
1193
|
peerConfirmedCount,
|
|
1194
|
+
standingEvidenceCount,
|
|
1099
1195
|
unavailableNodeIds,
|
|
1100
1196
|
};
|
|
1101
1197
|
}
|
|
@@ -6504,12 +6600,17 @@ export class DaemonCommandRouter {
|
|
|
6504
6600
|
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
6505
6601
|
|
|
6506
6602
|
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
6603
|
+
// Only an explicit refresh fans out a blocking peer probe.
|
|
6604
|
+
// Default loads are satisfied from held standing-state git truth.
|
|
6605
|
+
const probeRemotePeers = args?.refresh === true || args?.forceRefresh === true;
|
|
6507
6606
|
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
6508
6607
|
mesh: meshRecord.mesh,
|
|
6509
6608
|
meshSource: meshRecord.source,
|
|
6510
6609
|
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6610
|
+
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
6511
6611
|
statusInstanceId: this.deps.statusInstanceId,
|
|
6512
6612
|
localMachineId: loadConfig().machineId || '',
|
|
6613
|
+
probeRemotePeers,
|
|
6513
6614
|
});
|
|
6514
6615
|
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
6515
6616
|
const sourceOfTruth = {
|
|
@@ -8396,14 +8497,20 @@ export class DaemonCommandRouter {
|
|
|
8396
8497
|
mesh,
|
|
8397
8498
|
meshSource: meshRecord.source,
|
|
8398
8499
|
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8500
|
+
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
8399
8501
|
statusInstanceId: this.deps.statusInstanceId,
|
|
8400
8502
|
localMachineId,
|
|
8503
|
+
// Standing-state model: only an explicit refresh fans
|
|
8504
|
+
// out a blocking peer git probe. Default loads return
|
|
8505
|
+
// held truth so one slow peer can't block the graph.
|
|
8506
|
+
probeRemotePeers: refreshRequested,
|
|
8401
8507
|
})
|
|
8402
8508
|
: {
|
|
8403
8509
|
directEvidenceCount: 0,
|
|
8404
8510
|
localConfirmedCount: 0,
|
|
8405
8511
|
peerAttemptedCount: 0,
|
|
8406
8512
|
peerConfirmedCount: 0,
|
|
8513
|
+
standingEvidenceCount: 0,
|
|
8407
8514
|
unavailableNodeIds: [] as string[],
|
|
8408
8515
|
};
|
|
8409
8516
|
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
@@ -8421,9 +8528,15 @@ export class DaemonCommandRouter {
|
|
|
8421
8528
|
&& mesh.nodes
|
|
8422
8529
|
.filter((node: any) => unavailableDirectTruthNodeIds.has(normalizeMeshNodeId(node) ?? ''))
|
|
8423
8530
|
.every((node: any) => node?.isLocalWorktree === true);
|
|
8531
|
+
// Default (non-refresh) loads never hard-fail: held
|
|
8532
|
+
// standing-state truth is returned and the graph renders
|
|
8533
|
+
// immediately. The hard mesh_direct_peer_truth_unavailable
|
|
8534
|
+
// failure is reserved for an explicit refresh that actually
|
|
8535
|
+
// attempted a peer probe and could not confirm any evidence.
|
|
8424
8536
|
const directTruthSatisfied = !requireDirectPeerTruth
|
|
8537
|
+
|| !refreshRequested
|
|
8425
8538
|
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
8426
|
-
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
8539
|
+
if (requireDirectPeerTruth && refreshRequested && !directTruthSatisfied) {
|
|
8427
8540
|
const failureResult = {
|
|
8428
8541
|
success: false,
|
|
8429
8542
|
code: 'mesh_direct_peer_truth_unavailable',
|
|
@@ -8588,58 +8701,37 @@ export class DaemonCommandRouter {
|
|
|
8588
8701
|
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
8589
8702
|
}
|
|
8590
8703
|
remoteProbeApplied = true;
|
|
8591
|
-
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
const
|
|
8615
|
-
const
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
const remoteGit = await probeRemoteMeshGitStatus({
|
|
8620
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8621
|
-
daemonId,
|
|
8622
|
-
workspace,
|
|
8623
|
-
timeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
8624
|
-
});
|
|
8625
|
-
if (remoteGit) {
|
|
8626
|
-
status.git = remoteGit;
|
|
8627
|
-
status.health = remoteGit.isGitRepo
|
|
8628
|
-
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
8629
|
-
: 'degraded';
|
|
8630
|
-
const connection = readObjectRecord(status.connection);
|
|
8631
|
-
const connectionState = readStringValue(connection.state);
|
|
8632
|
-
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
8633
|
-
if (!connectionReported || connectionState === 'unknown') {
|
|
8634
|
-
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
8635
|
-
}
|
|
8636
|
-
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
8637
|
-
remoteProbeApplied = true;
|
|
8638
|
-
}
|
|
8639
|
-
} catch {
|
|
8640
|
-
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
8641
|
-
}
|
|
8704
|
+
} else if (refreshRequested && !isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
8705
|
+
// Only an explicit refresh fans out a blocking
|
|
8706
|
+
// per-node git probe. On the default load a peer
|
|
8707
|
+
// with no held truth falls through to
|
|
8708
|
+
// gitProbePending below — the graph still renders.
|
|
8709
|
+
// Bounded retry (shared with the bootstrap hydrate
|
|
8710
|
+
// path), gated on the peer staying connected, so a
|
|
8711
|
+
// slow TURN-relayed peer is recovered rather than
|
|
8712
|
+
// dropped after a single timeout.
|
|
8713
|
+
const remoteGit = await probeRemoteMeshGitStatusWithRetry({
|
|
8714
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8715
|
+
daemonId,
|
|
8716
|
+
workspace,
|
|
8717
|
+
timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
|
|
8718
|
+
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
8719
|
+
getConnection: this.deps.getMeshPeerConnectionStatus,
|
|
8720
|
+
onConnection: connection => { status.connection = connection; },
|
|
8721
|
+
});
|
|
8722
|
+
if (remoteGit) {
|
|
8723
|
+
status.git = remoteGit;
|
|
8724
|
+
status.health = remoteGit.isGitRepo
|
|
8725
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
8726
|
+
: 'degraded';
|
|
8727
|
+
const connection = readObjectRecord(status.connection);
|
|
8728
|
+
const connectionState = readStringValue(connection.state);
|
|
8729
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
8730
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
8731
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
8642
8732
|
}
|
|
8733
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
8734
|
+
remoteProbeApplied = true;
|
|
8643
8735
|
}
|
|
8644
8736
|
}
|
|
8645
8737
|
if (!remoteProbeApplied) {
|
|
@@ -8730,7 +8822,12 @@ export class DaemonCommandRouter {
|
|
|
8730
8822
|
liveSessionRecords: liveMeshSessions,
|
|
8731
8823
|
});
|
|
8732
8824
|
const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
|
|
8733
|
-
|
|
8825
|
+
// withStats opts in to per-mission operational rollups (durations /
|
|
8826
|
+
// retries) for the dashboard mission detail. The rollup scans a
|
|
8827
|
+
// bounded ledger tail per mission, but only over the bounded set
|
|
8828
|
+
// returned here (live + capped history), so the cost stays linear
|
|
8829
|
+
// in visible missions rather than the whole mesh history.
|
|
8830
|
+
const missions = getMeshStatusMissionSummaries(meshId, { verbose: verboseMissions, withStats: true });
|
|
8734
8831
|
const statusResult = {
|
|
8735
8832
|
success: true,
|
|
8736
8833
|
meshId: mesh.id,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { randomUUID } from 'crypto';
|
|
16
16
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
17
17
|
import { getQueue } from './mesh-work-queue.js';
|
|
18
|
+
import { computeMeshMissionStats, type MeshMissionStats } from './mesh-task-stats.js';
|
|
18
19
|
|
|
19
20
|
export type MeshMissionStatus = 'active' | 'paused' | 'completed' | 'abandoned';
|
|
20
21
|
|
|
@@ -45,6 +46,13 @@ export interface MeshMissionTaskAggregate {
|
|
|
45
46
|
|
|
46
47
|
export interface MeshMissionSummary extends MeshMissionRecord {
|
|
47
48
|
tasks: MeshMissionTaskAggregate;
|
|
49
|
+
/**
|
|
50
|
+
* Operational rollup (durations / attempts) derived from the ledger via
|
|
51
|
+
* computeMeshMissionStats. Optional: only populated by surfaces that opt in
|
|
52
|
+
* (e.g. mesh_status), since the rollup scans a bounded ledger tail per
|
|
53
|
+
* mission. Absent on the lightweight task-aggregate-only summaries.
|
|
54
|
+
*/
|
|
55
|
+
stats?: MeshMissionStats;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
/**
|
|
@@ -171,7 +179,7 @@ function slimMissionSummary(summary: MeshMissionSummary): MeshMissionSlimSummary
|
|
|
171
179
|
*/
|
|
172
180
|
export function getMeshStatusMissionSummaries(
|
|
173
181
|
meshId: string,
|
|
174
|
-
options?: { historyLimit?: number; verbose?: boolean },
|
|
182
|
+
options?: { historyLimit?: number; verbose?: boolean; withStats?: boolean },
|
|
175
183
|
): MeshMissionSummary[] | MeshMissionSlimSummary[] {
|
|
176
184
|
const historyLimit = Math.max(0, options?.historyLimit ?? 10);
|
|
177
185
|
const all = getMeshMissions(meshId);
|
|
@@ -180,7 +188,15 @@ export function getMeshStatusMissionSummaries(
|
|
|
180
188
|
.filter(m => m.status === 'completed' || m.status === 'abandoned')
|
|
181
189
|
.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))
|
|
182
190
|
.slice(0, historyLimit);
|
|
183
|
-
|
|
191
|
+
let full = [...live, ...history].map(mission => summarizeMeshMission(meshId, mission));
|
|
192
|
+
// Operational stats (durations / attempts) are an opt-in projection: each
|
|
193
|
+
// mission's rollup scans a bounded ledger tail, so we only compute it for
|
|
194
|
+
// the bounded set we are about to return (live + capped history), not for
|
|
195
|
+
// every mission in the mesh. The dashboard graph opts in so mission detail
|
|
196
|
+
// can show wall-clock / retries without a second round trip.
|
|
197
|
+
if (options?.withStats) {
|
|
198
|
+
full = full.map(summary => ({ ...summary, stats: computeMeshMissionStats(meshId, summary.id) }));
|
|
199
|
+
}
|
|
184
200
|
return options?.verbose ? full : full.map(slimMissionSummary);
|
|
185
201
|
}
|
|
186
202
|
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
|
|
15
|
-
import type { MeshMissionSummary } from './mesh/mesh-missions.js';
|
|
15
|
+
import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
|
|
16
16
|
|
|
17
17
|
// ─── Core Mesh Types ────────────────────────────
|
|
18
18
|
|
|
@@ -486,8 +486,13 @@ export interface RepoMeshStatus {
|
|
|
486
486
|
* capped, newest-first slice of completed/abandoned history. Omitted by older
|
|
487
487
|
* daemons — the dashboard must treat this as optional and render an empty
|
|
488
488
|
* state when absent. Split on each entry's `status` for live vs. history.
|
|
489
|
+
*
|
|
490
|
+
* Compact (the default) status calls send the slim shape — `goalPreview` +
|
|
491
|
+
* `goalTruncated` instead of the full `goal` — while verbose sends the full
|
|
492
|
+
* `goal`. Consumers must read `goal ?? goalPreview`. Each entry may also carry
|
|
493
|
+
* an optional `stats` operational rollup (durations / retries).
|
|
489
494
|
*/
|
|
490
|
-
missions?: MeshMissionSummary[];
|
|
495
|
+
missions?: (MeshMissionSummary | MeshMissionSlimSummary)[];
|
|
491
496
|
}
|
|
492
497
|
|
|
493
498
|
// RepoMeshSessionStatus shape now lives in @adhdev/mesh-shared (shared with
|