@adhdev/daemon-core 0.9.82-rc.5 → 0.9.82-rc.51
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/boot/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/router.d.ts +13 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +10 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +2483 -434
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2463 -427
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +17 -5
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-work-queue.d.ts +11 -5
- package/dist/mesh/refine-config.d.ts +119 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/repo-mesh-types.d.ts +160 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +4 -0
- package/src/commands/router.ts +1831 -296
- package/src/config/mesh-config.ts +244 -1
- package/src/git/git-commands.ts +3 -3
- package/src/git/git-status.ts +97 -6
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +11 -0
- package/src/index.ts +32 -2
- package/src/mesh/mesh-events.ts +168 -30
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +1 -0
- package/src/mesh/mesh-work-queue.ts +149 -122
- package/src/mesh/refine-config.ts +306 -0
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/repo-mesh-types.ts +174 -0
package/src/commands/router.ts
CHANGED
|
@@ -39,6 +39,16 @@ import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../sessi
|
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
41
|
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
|
|
42
|
+
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
43
|
+
import {
|
|
44
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
45
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
46
|
+
loadMeshRefineConfig,
|
|
47
|
+
resolveMeshRefineValidationPlan,
|
|
48
|
+
suggestMeshRefineConfig,
|
|
49
|
+
validateMeshRefineConfig,
|
|
50
|
+
type MeshRefineValidationCommandPlan,
|
|
51
|
+
} from '../mesh/refine-config.js';
|
|
42
52
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
43
53
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
44
54
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
@@ -114,14 +124,93 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
114
124
|
return undefined;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
function
|
|
127
|
+
function summarizeRepoMeshDebugGit(git: unknown): Record<string, unknown> | null {
|
|
128
|
+
const record = readObjectRecord(git);
|
|
129
|
+
if (!Object.keys(record).length) return null;
|
|
130
|
+
const submodules = Array.isArray(record.submodules)
|
|
131
|
+
? record.submodules.map((entry: any) => ({
|
|
132
|
+
path: readStringValue(entry?.path) ?? null,
|
|
133
|
+
commit: readStringValue(entry?.commit)?.slice(0, 12) ?? null,
|
|
134
|
+
dirty: readBooleanValue(entry?.dirty) ?? false,
|
|
135
|
+
outOfSync: readBooleanValue(entry?.outOfSync, entry?.out_of_sync) ?? false,
|
|
136
|
+
}))
|
|
137
|
+
: [];
|
|
138
|
+
return {
|
|
139
|
+
isGitRepo: readBooleanValue(record.isGitRepo),
|
|
140
|
+
workspace: readStringValue(record.workspace) ?? null,
|
|
141
|
+
repoRoot: readStringValue(record.repoRoot, record.repo_root) ?? null,
|
|
142
|
+
branch: readStringValue(record.branch) ?? null,
|
|
143
|
+
upstream: readStringValue(record.upstream) ?? null,
|
|
144
|
+
upstreamStatus: readStringValue(record.upstreamStatus, record.upstream_status) ?? null,
|
|
145
|
+
headCommit: readStringValue(record.headCommit, record.head_commit)?.slice(0, 12) ?? null,
|
|
146
|
+
ahead: readNumberValue(record.ahead) ?? null,
|
|
147
|
+
behind: readNumberValue(record.behind) ?? null,
|
|
148
|
+
dirtyCounts: {
|
|
149
|
+
staged: readNumberValue(record.staged) ?? 0,
|
|
150
|
+
modified: readNumberValue(record.modified) ?? 0,
|
|
151
|
+
untracked: readNumberValue(record.untracked) ?? 0,
|
|
152
|
+
deleted: readNumberValue(record.deleted) ?? 0,
|
|
153
|
+
renamed: readNumberValue(record.renamed) ?? 0,
|
|
154
|
+
},
|
|
155
|
+
lastCheckedAt: readNumberValue(record.lastCheckedAt, record.last_checked_at) ?? null,
|
|
156
|
+
submoduleCount: submodules.length,
|
|
157
|
+
submodules,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
162
|
+
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
163
|
+
return {
|
|
164
|
+
success: status?.success,
|
|
165
|
+
meshId: readStringValue(status?.meshId, status?.mesh_id) ?? null,
|
|
166
|
+
refreshedAt: readStringValue(status?.refreshedAt, status?.refreshed_at) ?? null,
|
|
167
|
+
sourceOfTruth: status?.sourceOfTruth ?? null,
|
|
168
|
+
nodeCount: nodes.length,
|
|
169
|
+
nodes: nodes.map((node: any) => ({
|
|
170
|
+
nodeId: readStringValue(node?.nodeId, node?.id) ?? null,
|
|
171
|
+
daemonId: readStringValue(node?.daemonId, node?.daemon_id) ?? null,
|
|
172
|
+
workspace: readStringValue(node?.workspace, node?.git?.workspace) ?? null,
|
|
173
|
+
health: readStringValue(node?.health) ?? null,
|
|
174
|
+
machineStatus: readStringValue(node?.machineStatus, node?.machine_status) ?? null,
|
|
175
|
+
connection: node?.connection && typeof node.connection === 'object' ? {
|
|
176
|
+
state: readStringValue(node.connection.state) ?? null,
|
|
177
|
+
transport: readStringValue(node.connection.transport) ?? null,
|
|
178
|
+
source: readStringValue(node.connection.source) ?? null,
|
|
179
|
+
reported: readBooleanValue(node.connection.reported) ?? null,
|
|
180
|
+
} : null,
|
|
181
|
+
gitProbePending: node?.gitProbePending === true,
|
|
182
|
+
launchReady: node?.launchReady === true,
|
|
183
|
+
git: summarizeRepoMeshDebugGit(node?.git),
|
|
184
|
+
})),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
189
|
+
try {
|
|
190
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
191
|
+
} catch {
|
|
192
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${event}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
|
|
197
|
+
const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : '';
|
|
198
|
+
const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : '';
|
|
199
|
+
if (!normalizedPath) return undefined;
|
|
200
|
+
if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath;
|
|
201
|
+
if (!normalizedRoot) return undefined;
|
|
202
|
+
return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
|
|
118
206
|
if (!Array.isArray(value)) return undefined;
|
|
119
207
|
const submodules = value
|
|
120
208
|
.map(entry => {
|
|
121
209
|
const submodule = readObjectRecord(entry);
|
|
122
210
|
const path = readStringValue(submodule.path);
|
|
123
211
|
const commit = readStringValue(submodule.commit);
|
|
124
|
-
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
212
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
213
|
+
?? joinRepoPath(parentRepoRoot, path);
|
|
125
214
|
if (!path || !commit || !repoPath) return null;
|
|
126
215
|
return {
|
|
127
216
|
path,
|
|
@@ -137,60 +226,11 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
|
137
226
|
return submodules.length > 0 ? submodules : undefined;
|
|
138
227
|
}
|
|
139
228
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
|
|
146
|
-
: [];
|
|
147
|
-
const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
|
|
148
|
-
const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
|
|
149
|
-
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
150
|
-
if (isGitRepo !== undefined) {
|
|
151
|
-
const submodules = readGitSubmodules(cachedGit.submodules);
|
|
152
|
-
return {
|
|
153
|
-
workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
|
|
154
|
-
repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
|
|
155
|
-
isGitRepo,
|
|
156
|
-
branch: readStringValue(cachedGit.branch) ?? null,
|
|
157
|
-
headCommit: readStringValue(cachedGit.headCommit) ?? null,
|
|
158
|
-
headMessage: readStringValue(cachedGit.headMessage) ?? null,
|
|
159
|
-
upstream: readStringValue(cachedGit.upstream) ?? null,
|
|
160
|
-
ahead: readNumberValue(cachedGit.ahead) ?? 0,
|
|
161
|
-
behind: readNumberValue(cachedGit.behind) ?? 0,
|
|
162
|
-
staged: readNumberValue(cachedGit.staged) ?? 0,
|
|
163
|
-
modified: readNumberValue(cachedGit.modified) ?? 0,
|
|
164
|
-
untracked: readNumberValue(cachedGit.untracked) ?? 0,
|
|
165
|
-
deleted: readNumberValue(cachedGit.deleted) ?? 0,
|
|
166
|
-
renamed: readNumberValue(cachedGit.renamed) ?? 0,
|
|
167
|
-
hasConflicts,
|
|
168
|
-
conflictFiles,
|
|
169
|
-
stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
|
|
170
|
-
lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
|
|
171
|
-
...(submodules ? { submodules } : {}),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
177
|
-
const gitResult = readObjectRecord(rawGit.result);
|
|
178
|
-
const directStatus = readObjectRecord(rawGit.status);
|
|
179
|
-
const nestedStatus = readObjectRecord(gitResult.status);
|
|
180
|
-
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
181
|
-
const probeGit = readObjectRecord(rawProbe.git);
|
|
182
|
-
const probeGitResult = readObjectRecord(probeGit.result);
|
|
183
|
-
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
184
|
-
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
185
|
-
const status = Object.keys(directStatus).length
|
|
186
|
-
? directStatus
|
|
187
|
-
: Object.keys(nestedStatus).length
|
|
188
|
-
? nestedStatus
|
|
189
|
-
: Object.keys(probeDirectStatus).length
|
|
190
|
-
? probeDirectStatus
|
|
191
|
-
: Object.keys(probeNestedStatus).length
|
|
192
|
-
? probeNestedStatus
|
|
193
|
-
: {};
|
|
229
|
+
function normalizeInlineMeshGitStatus(
|
|
230
|
+
status: Record<string, unknown>,
|
|
231
|
+
node: any,
|
|
232
|
+
options?: { lastCheckedAt?: number },
|
|
233
|
+
): Record<string, unknown> | undefined {
|
|
194
234
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
235
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
236
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -198,10 +238,11 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
198
238
|
: [];
|
|
199
239
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
240
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
-
const
|
|
241
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
242
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
202
243
|
return {
|
|
203
244
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
-
repoRoot:
|
|
245
|
+
repoRoot: repoRoot ?? null,
|
|
205
246
|
isGitRepo,
|
|
206
247
|
branch: readStringValue(status.branch) ?? null,
|
|
207
248
|
headCommit: readStringValue(status.headCommit) ?? null,
|
|
@@ -217,31 +258,583 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
217
258
|
hasConflicts,
|
|
218
259
|
conflictFiles,
|
|
219
260
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
-
lastCheckedAt: Date.now(),
|
|
261
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
221
262
|
...(submodules ? { submodules } : {}),
|
|
222
263
|
};
|
|
223
264
|
}
|
|
224
265
|
|
|
225
|
-
function
|
|
266
|
+
function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
|
|
267
|
+
if (!git) return Number.NEGATIVE_INFINITY;
|
|
268
|
+
let score = 0;
|
|
269
|
+
if (readBooleanValue(git.isGitRepo) === true) score += 50;
|
|
270
|
+
if (readBooleanValue(git.isGitRepo) === false) score -= 10;
|
|
271
|
+
if (readStringValue(git.branch)) score += 20;
|
|
272
|
+
if (readStringValue(git.headCommit)) score += 20;
|
|
273
|
+
if (readStringValue(git.upstream)) score += 10;
|
|
274
|
+
if (readStringValue(git.upstreamStatus)) score += 5;
|
|
275
|
+
if (readNumberValue(git.ahead) !== undefined) score += 2;
|
|
276
|
+
if (readNumberValue(git.behind) !== undefined) score += 2;
|
|
277
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
|
|
278
|
+
if (readStringValue(git.error)) score -= 20;
|
|
279
|
+
return score;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
283
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
284
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
285
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
286
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
287
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
288
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
289
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
290
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
291
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
292
|
+
const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
|
|
293
|
+
let best: { git: Record<string, unknown>; score: number } | null = null;
|
|
294
|
+
for (const status of candidates) {
|
|
295
|
+
const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
296
|
+
if (!normalized) continue;
|
|
297
|
+
const score = scoreInlineMeshGitStatus(normalized);
|
|
298
|
+
if (!best || score > best.score) best = { git: normalized, score };
|
|
299
|
+
}
|
|
300
|
+
return best?.git;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
|
|
304
|
+
if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
|
|
305
|
+
return snapshot.nodes.some((node: any) => {
|
|
306
|
+
if (node?.gitProbePending !== true) return false;
|
|
307
|
+
const git = readObjectRecord(node?.git);
|
|
308
|
+
return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
313
|
+
const source = readStringValue(connection.source);
|
|
314
|
+
const transport = readStringValue(connection.transport);
|
|
315
|
+
return {
|
|
316
|
+
...connection,
|
|
317
|
+
perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
|
|
318
|
+
source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
|
|
319
|
+
state: 'connected',
|
|
320
|
+
transport: transport && transport !== 'unknown' ? transport : 'direct',
|
|
321
|
+
reported: true,
|
|
322
|
+
reason: 'Live peer git snapshot reported by the selected coordinator.',
|
|
323
|
+
lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function recordInlineMeshDirectGitTruth(
|
|
328
|
+
node: any,
|
|
329
|
+
git: Record<string, unknown>,
|
|
330
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
331
|
+
): void {
|
|
332
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
333
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
334
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
335
|
+
const nextGit: Record<string, unknown> = {
|
|
336
|
+
...git,
|
|
337
|
+
lastCheckedAt: checkedAt,
|
|
338
|
+
};
|
|
339
|
+
node.lastGit = {
|
|
340
|
+
source,
|
|
341
|
+
checkedAt,
|
|
342
|
+
status: nextGit,
|
|
343
|
+
};
|
|
344
|
+
node.last_git = node.lastGit;
|
|
345
|
+
node.machineStatus = 'online';
|
|
346
|
+
node.updatedAt = updatedAt;
|
|
347
|
+
node.lastSeenAt = updatedAt;
|
|
348
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
349
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
353
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
354
|
+
if (liveGit) return liveGit;
|
|
355
|
+
|
|
356
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
357
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
358
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
359
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
363
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
364
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
365
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
366
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
367
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
368
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
369
|
+
const branch = readStringValue(cachedGit.branch);
|
|
370
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
371
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
375
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
376
|
+
const {
|
|
377
|
+
cachedStatus,
|
|
378
|
+
lastGit: _lastGit,
|
|
379
|
+
last_git: _lastGitLegacy,
|
|
380
|
+
lastProbe: _lastProbe,
|
|
381
|
+
last_probe: _lastProbeLegacy,
|
|
382
|
+
error: _error,
|
|
383
|
+
health: _health,
|
|
384
|
+
machineStatus: _machineStatus,
|
|
385
|
+
lastSeenAt: _lastSeenAt,
|
|
386
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
387
|
+
updatedAt: _updatedAt,
|
|
388
|
+
updated_at: _updatedAtLegacy,
|
|
389
|
+
activeSession: _activeSession,
|
|
390
|
+
active_session: _activeSessionLegacy,
|
|
391
|
+
activeSessionId: _activeSessionId,
|
|
392
|
+
active_session_id: _activeSessionIdLegacy,
|
|
393
|
+
sessionId: _sessionId,
|
|
394
|
+
session_id: _sessionIdLegacy,
|
|
395
|
+
providerType: _providerType,
|
|
396
|
+
provider_type: _providerTypeLegacy,
|
|
397
|
+
...rest
|
|
398
|
+
} = node as Record<string, unknown>;
|
|
399
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
400
|
+
return { ...rest, cachedStatus };
|
|
401
|
+
}
|
|
402
|
+
return rest;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
406
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
407
|
+
return 'cachedStatus' in node
|
|
408
|
+
|| 'lastGit' in node
|
|
409
|
+
|| 'last_git' in node
|
|
410
|
+
|| 'lastProbe' in node
|
|
411
|
+
|| 'last_probe' in node
|
|
412
|
+
|| 'error' in node
|
|
413
|
+
|| 'health' in node
|
|
414
|
+
|| 'machineStatus' in node
|
|
415
|
+
|| 'lastSeenAt' in node
|
|
416
|
+
|| 'last_seen_at' in node
|
|
417
|
+
|| 'updatedAt' in node
|
|
418
|
+
|| 'updated_at' in node
|
|
419
|
+
|| 'activeSession' in node
|
|
420
|
+
|| 'active_session' in node
|
|
421
|
+
|| 'activeSessionId' in node
|
|
422
|
+
|| 'active_session_id' in node
|
|
423
|
+
|| 'sessionId' in node
|
|
424
|
+
|| 'session_id' in node
|
|
425
|
+
|| 'providerType' in node
|
|
426
|
+
|| 'provider_type' in node;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
430
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
431
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
432
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function readInlineMeshNodeId(node: any): string {
|
|
436
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
440
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
441
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
442
|
+
let changed = false;
|
|
443
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
444
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
445
|
+
changed = true;
|
|
446
|
+
return stripInlineMeshTransientNodeState(node);
|
|
447
|
+
});
|
|
448
|
+
if (!changed) return inlineMesh;
|
|
449
|
+
return {
|
|
450
|
+
...inlineMesh,
|
|
451
|
+
nodes,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
456
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
457
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
458
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
459
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
460
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
461
|
+
|
|
462
|
+
const incomingById = new Map<string, any>();
|
|
463
|
+
for (const node of incomingNodes) {
|
|
464
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
465
|
+
if (nodeId) incomingById.set(nodeId, node);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const nodes = cachedNodes.map((cachedNode: any) => {
|
|
469
|
+
const nodeId = readInlineMeshNodeId(cachedNode);
|
|
470
|
+
const incomingNode = nodeId ? incomingById.get(nodeId) : undefined;
|
|
471
|
+
if (!incomingNode) return cachedNode;
|
|
472
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
473
|
+
return { ...cachedNode, ...incomingNode };
|
|
474
|
+
}
|
|
475
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
...cached,
|
|
480
|
+
...incoming,
|
|
481
|
+
nodes,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
486
|
+
if (!git) return false;
|
|
487
|
+
return Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
491
|
+
const submodules = Array.isArray(git?.submodules) ? git.submodules : [];
|
|
492
|
+
let dirty = false;
|
|
493
|
+
let outOfSync = false;
|
|
494
|
+
for (const entry of submodules) {
|
|
495
|
+
const submodule = readObjectRecord(entry);
|
|
496
|
+
if (readBooleanValue(submodule.dirty) === true) dirty = true;
|
|
497
|
+
if (readBooleanValue(submodule.outOfSync) === true || !!readStringValue(submodule.error)) outOfSync = true;
|
|
498
|
+
}
|
|
499
|
+
return { dirty, outOfSync };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
503
|
+
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
504
|
+
const branch = readStringValue(git.branch);
|
|
505
|
+
if (!branch) return 'degraded';
|
|
506
|
+
const submoduleDrift = getGitSubmoduleDriftState(git);
|
|
507
|
+
if (submoduleDrift.outOfSync) return 'degraded';
|
|
508
|
+
if (submoduleDrift.dirty || hasGitWorktreeChanges(git)) return 'dirty';
|
|
509
|
+
return 'online';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
513
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
514
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
515
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
516
|
+
? activeSession
|
|
517
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
518
|
+
const sessionId = readStringValue(fallbackSession.id, fallbackSession.sessionId, fallbackSession.session_id, node?.activeSessionId, node?.active_session_id, node?.sessionId, node?.session_id);
|
|
519
|
+
return sessionId ? [sessionId] : [];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
523
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
524
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
525
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
526
|
+
? activeSession
|
|
527
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
528
|
+
const sessionId = readStringValue(
|
|
529
|
+
fallbackSession.id,
|
|
530
|
+
fallbackSession.sessionId,
|
|
531
|
+
fallbackSession.session_id,
|
|
532
|
+
node?.activeSessionId,
|
|
533
|
+
node?.active_session_id,
|
|
534
|
+
node?.sessionId,
|
|
535
|
+
node?.session_id,
|
|
536
|
+
);
|
|
537
|
+
if (!sessionId) return [];
|
|
538
|
+
return [{
|
|
539
|
+
sessionId,
|
|
540
|
+
providerType: readStringValue(
|
|
541
|
+
fallbackSession.providerType,
|
|
542
|
+
fallbackSession.provider_type,
|
|
543
|
+
fallbackSession.cliType,
|
|
544
|
+
fallbackSession.cli_type,
|
|
545
|
+
fallbackSession.provider,
|
|
546
|
+
node?.providerType,
|
|
547
|
+
node?.provider_type,
|
|
548
|
+
),
|
|
549
|
+
state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
|
|
550
|
+
lifecycle: readStringValue(fallbackSession.lifecycle),
|
|
551
|
+
title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
|
|
552
|
+
workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
|
|
553
|
+
lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
|
|
554
|
+
recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
|
|
555
|
+
isCached: true,
|
|
556
|
+
}];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function readLiveMeshSessionState(record: any): string | undefined {
|
|
560
|
+
return readStringValue(
|
|
561
|
+
record?.meta?.sessionStatus,
|
|
562
|
+
record?.meta?.status,
|
|
563
|
+
record?.meta?.providerStatus,
|
|
564
|
+
record?.status,
|
|
565
|
+
record?.state,
|
|
566
|
+
record?.lifecycle,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function toIsoTimestamp(value: unknown): string | null {
|
|
571
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
572
|
+
const stringValue = readStringValue(value);
|
|
573
|
+
return stringValue || null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
577
|
+
const connection = readObjectRecord(status.connection);
|
|
578
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
579
|
+
const git = readObjectRecord(status.git);
|
|
580
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
581
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
582
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
583
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function finalizeMeshNodeStatus(args: {
|
|
588
|
+
status: Record<string, unknown>;
|
|
589
|
+
node: any;
|
|
590
|
+
daemonId?: string;
|
|
591
|
+
isSelfNode: boolean;
|
|
592
|
+
}): void {
|
|
593
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
594
|
+
if (!readStringValue(status.machineStatus)) {
|
|
595
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
596
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
597
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
598
|
+
}
|
|
599
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
600
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
601
|
+
status.launchReady = !!daemonId && (
|
|
602
|
+
readStringValue(status.machineStatus) === 'online'
|
|
603
|
+
|| connectionState === 'connected'
|
|
604
|
+
|| isSelfNode
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
609
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
610
|
+
daemonId: string;
|
|
611
|
+
workspace: string;
|
|
612
|
+
timeoutMs: number;
|
|
613
|
+
}): Promise<Record<string, unknown> | null> {
|
|
614
|
+
if (!args.dispatchMeshCommand) return null;
|
|
615
|
+
const remoteResult = await Promise.race([
|
|
616
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace }),
|
|
617
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
618
|
+
]) as any;
|
|
619
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
620
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
621
|
+
? remoteGit as Record<string, unknown>
|
|
622
|
+
: null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
626
|
+
mesh: any;
|
|
627
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
628
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
629
|
+
statusInstanceId?: string;
|
|
630
|
+
localMachineId?: string;
|
|
631
|
+
}): Promise<{
|
|
632
|
+
directEvidenceCount: number;
|
|
633
|
+
localConfirmedCount: number;
|
|
634
|
+
peerAttemptedCount: number;
|
|
635
|
+
peerConfirmedCount: number;
|
|
636
|
+
unavailableNodeIds: string[];
|
|
637
|
+
}> {
|
|
638
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
639
|
+
if (!nodes.length) {
|
|
640
|
+
return {
|
|
641
|
+
directEvidenceCount: 0,
|
|
642
|
+
localConfirmedCount: 0,
|
|
643
|
+
peerAttemptedCount: 0,
|
|
644
|
+
peerConfirmedCount: 0,
|
|
645
|
+
unavailableNodeIds: [],
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
650
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
651
|
+
nodes[0]?.id,
|
|
652
|
+
nodes[0]?.nodeId,
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
let localConfirmedCount = 0;
|
|
656
|
+
let peerAttemptedCount = 0;
|
|
657
|
+
let peerConfirmedCount = 0;
|
|
658
|
+
const unavailableNodeIds: string[] = [];
|
|
659
|
+
|
|
660
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
661
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
662
|
+
const workspace = readStringValue(node?.workspace);
|
|
663
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
664
|
+
const isSelfNode = Boolean(
|
|
665
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
666
|
+
) || Boolean(
|
|
667
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
668
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
669
|
+
|
|
670
|
+
if (!workspace) {
|
|
671
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (isSelfNode && fs.existsSync(workspace)) {
|
|
676
|
+
try {
|
|
677
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
678
|
+
if (localGit?.isGitRepo) {
|
|
679
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
680
|
+
localConfirmedCount += 1;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
// Fall through to remote classification.
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
689
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
peerAttemptedCount += 1;
|
|
694
|
+
try {
|
|
695
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
696
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
697
|
+
daemonId,
|
|
698
|
+
workspace,
|
|
699
|
+
timeoutMs: 8_000,
|
|
700
|
+
});
|
|
701
|
+
if (remoteGit) {
|
|
702
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
703
|
+
peerConfirmedCount += 1;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
unavailableNodeIds.push(nodeId);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
715
|
+
localConfirmedCount,
|
|
716
|
+
peerAttemptedCount,
|
|
717
|
+
peerConfirmedCount,
|
|
718
|
+
unavailableNodeIds,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
723
|
+
return {
|
|
724
|
+
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
725
|
+
providerType: readStringValue(record?.providerType),
|
|
726
|
+
state: readLiveMeshSessionState(record),
|
|
727
|
+
lifecycle: readStringValue(record?.lifecycle),
|
|
728
|
+
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
729
|
+
recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
|
|
730
|
+
workspace: readStringValue(record?.workspace) ?? null,
|
|
731
|
+
title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
|
|
732
|
+
lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
|
|
733
|
+
isCached: false,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
738
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
739
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
740
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
741
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
745
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
746
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
747
|
+
|
|
748
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
749
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
750
|
+
|
|
751
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
755
|
+
meshId: string;
|
|
756
|
+
nodeId: string;
|
|
757
|
+
liveSessionRecords: any[];
|
|
758
|
+
allowCoordinatorSession?: boolean;
|
|
759
|
+
}): string {
|
|
760
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
761
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
762
|
+
&& readStringValue(record?.workspace)
|
|
763
|
+
));
|
|
764
|
+
if (directNodeWorkspace) {
|
|
765
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (args.allowCoordinatorSession) {
|
|
769
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
770
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
771
|
+
&& readStringValue(record?.workspace)
|
|
772
|
+
));
|
|
773
|
+
if (coordinatorWorkspace) {
|
|
774
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return '';
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function collectLiveMeshSessionRecords(args: {
|
|
782
|
+
meshId: string;
|
|
783
|
+
node: any;
|
|
784
|
+
nodeId: string;
|
|
785
|
+
liveSessionRecords: any[];
|
|
786
|
+
allowCoordinatorSession?: boolean;
|
|
787
|
+
}): any[] {
|
|
788
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
789
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
790
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
|
|
791
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
if (args.allowCoordinatorSession) {
|
|
795
|
+
for (const record of args.liveSessionRecords) {
|
|
796
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
797
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
798
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
799
|
+
matches.push(record);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return matches;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function applyCachedInlineMeshNodeStatus(
|
|
807
|
+
status: Record<string, unknown>,
|
|
808
|
+
node: any,
|
|
809
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
810
|
+
): boolean {
|
|
226
811
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
const
|
|
812
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
813
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
814
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
815
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
230
816
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
231
|
-
|
|
232
|
-
|
|
817
|
+
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
818
|
+
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
819
|
+
const activeSessions = readCachedInlineMeshActiveSessions(node);
|
|
820
|
+
const activeSessionDetails = readCachedInlineMeshActiveSessionDetails(node);
|
|
821
|
+
if (!git && !error && !health && !machineStatus && !lastSeenAt && !updatedAt && activeSessions.length === 0) return false;
|
|
233
822
|
if (git) status.git = git;
|
|
234
823
|
if (error) status.error = error;
|
|
824
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
825
|
+
if (lastSeenAt) status.lastSeenAt = lastSeenAt;
|
|
826
|
+
if (updatedAt) status.updatedAt = updatedAt;
|
|
827
|
+
if (activeSessions.length > 0) status.activeSessions = activeSessions;
|
|
828
|
+
if (activeSessionDetails.length > 0) status.activeSessionDetails = activeSessionDetails;
|
|
235
829
|
if (health) {
|
|
236
830
|
status.health = health;
|
|
237
831
|
return true;
|
|
238
832
|
}
|
|
239
833
|
if (git) {
|
|
240
|
-
|
|
241
|
-
status.health = git.isGitRepo === false ? 'degraded' : dirty ? 'dirty' : 'online';
|
|
834
|
+
status.health = deriveMeshNodeHealthFromGit(git);
|
|
242
835
|
return true;
|
|
243
836
|
}
|
|
244
|
-
return
|
|
837
|
+
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
245
838
|
}
|
|
246
839
|
|
|
247
840
|
async function resolveProviderTypeFromPriority(args: {
|
|
@@ -276,13 +869,7 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
276
869
|
}
|
|
277
870
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
278
871
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
279
|
-
type MeshRefineValidationCommand =
|
|
280
|
-
command: string;
|
|
281
|
-
args: string[];
|
|
282
|
-
displayCommand: string;
|
|
283
|
-
category: string;
|
|
284
|
-
source: string;
|
|
285
|
-
};
|
|
872
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
286
873
|
|
|
287
874
|
type MeshRefineValidationSummary = {
|
|
288
875
|
status: MeshRefineValidationStatus;
|
|
@@ -292,6 +879,27 @@ type MeshRefineValidationSummary = {
|
|
|
292
879
|
skippedReason?: string;
|
|
293
880
|
timeoutMs: number;
|
|
294
881
|
outputLimitBytes: number;
|
|
882
|
+
configSource?: string;
|
|
883
|
+
configSourceType?: string;
|
|
884
|
+
suggestions?: unknown[];
|
|
885
|
+
suggestedConfig?: unknown;
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
889
|
+
|
|
890
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
891
|
+
status: MeshRefineStageStatus;
|
|
892
|
+
equivalent: boolean;
|
|
893
|
+
baseHead: string;
|
|
894
|
+
branchHead: string;
|
|
895
|
+
mergeBase?: string;
|
|
896
|
+
mergedTree?: string;
|
|
897
|
+
expectedPatchId?: string;
|
|
898
|
+
actualPatchId?: string;
|
|
899
|
+
durationMs: number;
|
|
900
|
+
error?: string;
|
|
901
|
+
stdout?: string;
|
|
902
|
+
stderr?: string;
|
|
295
903
|
};
|
|
296
904
|
|
|
297
905
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
@@ -299,6 +907,7 @@ const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
|
299
907
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
300
908
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
301
909
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
910
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
302
911
|
|
|
303
912
|
function truncateValidationOutput(value: unknown): string {
|
|
304
913
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -306,171 +915,114 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
306
915
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
307
916
|
}
|
|
308
917
|
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
function tokenizeValidationCommand(command: string): string[] | null {
|
|
322
|
-
const trimmed = command.trim();
|
|
323
|
-
if (!trimmed) return null;
|
|
324
|
-
// Fail closed: the gate never hands shell syntax to a shell. Package-manager
|
|
325
|
-
// scripts are invoked via execFile(binary, args), and metacharacters/quotes are
|
|
326
|
-
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
327
|
-
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
328
|
-
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
329
|
-
if (!tokens.length) return null;
|
|
330
|
-
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
331
|
-
return tokens;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
|
|
335
|
-
return scriptName === category || scriptName.startsWith(`${category}:`);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function parsePackageManagerValidationCommand(
|
|
339
|
-
rawCommand: string,
|
|
340
|
-
category: string,
|
|
341
|
-
scripts: Record<string, string>,
|
|
342
|
-
source: string,
|
|
343
|
-
): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
|
|
344
|
-
const tokens = tokenizeValidationCommand(rawCommand);
|
|
345
|
-
if (!tokens) {
|
|
346
|
-
return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const [binary, second, third, ...rest] = tokens;
|
|
350
|
-
let scriptName = '';
|
|
351
|
-
let command = binary;
|
|
352
|
-
let args: string[] = [];
|
|
353
|
-
|
|
354
|
-
if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
|
|
355
|
-
scriptName = third;
|
|
356
|
-
args = ['run', scriptName, ...rest];
|
|
357
|
-
} else if (binary === 'npm' && second === 'test' && !third) {
|
|
358
|
-
scriptName = 'test';
|
|
359
|
-
args = ['test'];
|
|
360
|
-
} else if (binary === 'yarn' && second === 'run' && third) {
|
|
361
|
-
scriptName = third;
|
|
362
|
-
args = ['run', scriptName, ...rest];
|
|
363
|
-
} else if (binary === 'yarn' && second && !third) {
|
|
364
|
-
scriptName = second;
|
|
365
|
-
args = [scriptName];
|
|
366
|
-
} else {
|
|
367
|
-
return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
371
|
-
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
|
|
372
|
-
}
|
|
373
|
-
if (!scriptMatchesValidationCategory(scriptName, category)) {
|
|
374
|
-
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return {
|
|
378
|
-
command: {
|
|
379
|
-
command,
|
|
380
|
-
args,
|
|
381
|
-
displayCommand: [command, ...args].join(' '),
|
|
382
|
-
category,
|
|
383
|
-
source,
|
|
384
|
-
},
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function collectProjectContextValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string; confidence?: string }> {
|
|
389
|
-
const commands = mesh?.projectContext?.commands;
|
|
390
|
-
if (!commands || typeof commands !== 'object' || Array.isArray(commands)) return [];
|
|
391
|
-
const candidates: Array<{ command: string; category: string; source: string; confidence?: string }> = [];
|
|
392
|
-
for (const category of REFINE_VALIDATION_CATEGORIES) {
|
|
393
|
-
const entries = Array.isArray(commands[category]) ? commands[category] : [];
|
|
394
|
-
for (const entry of entries) {
|
|
395
|
-
if (typeof entry?.command !== 'string') continue;
|
|
396
|
-
candidates.push({
|
|
397
|
-
command: entry.command,
|
|
398
|
-
category,
|
|
399
|
-
source: typeof entry.sourcePath === 'string' ? entry.sourcePath : 'projectContext.commands',
|
|
400
|
-
confidence: typeof entry.confidence === 'string' ? entry.confidence : undefined,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return candidates.sort((a, b) => {
|
|
405
|
-
const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
|
|
406
|
-
return rank(a.confidence) - rank(b.confidence);
|
|
918
|
+
function recordMeshRefineStage(
|
|
919
|
+
stages: Array<Record<string, unknown>>,
|
|
920
|
+
stage: string,
|
|
921
|
+
status: MeshRefineStageStatus,
|
|
922
|
+
startedAt: number,
|
|
923
|
+
details?: Record<string, unknown>,
|
|
924
|
+
): void {
|
|
925
|
+
stages.push({
|
|
926
|
+
stage,
|
|
927
|
+
status,
|
|
928
|
+
durationMs: Date.now() - startedAt,
|
|
929
|
+
...(details || {}),
|
|
407
930
|
});
|
|
408
931
|
}
|
|
409
932
|
|
|
410
|
-
function
|
|
411
|
-
const
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
.filter((entry: any) => !!entry.category);
|
|
933
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
934
|
+
const { execFileSync } = await import('node:child_process');
|
|
935
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
936
|
+
cwd,
|
|
937
|
+
encoding: 'utf8',
|
|
938
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
939
|
+
});
|
|
940
|
+
if (!diff.trim()) return '';
|
|
941
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
942
|
+
cwd,
|
|
943
|
+
input: diff,
|
|
944
|
+
encoding: 'utf8',
|
|
945
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
946
|
+
}).trim();
|
|
947
|
+
return patchId.split(/\s+/)[0] || '';
|
|
426
948
|
}
|
|
427
949
|
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (!
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
seen.add(fallback.command.displayCommand);
|
|
457
|
-
} else if (fallback.rejected) {
|
|
458
|
-
rejectedCommands.push(fallback.rejected);
|
|
459
|
-
}
|
|
460
|
-
if (selected.length >= 2) break;
|
|
950
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
951
|
+
repoRoot: string,
|
|
952
|
+
baseHead: string,
|
|
953
|
+
branchHead: string,
|
|
954
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
955
|
+
const startedAt = Date.now();
|
|
956
|
+
try {
|
|
957
|
+
const { execFileSync } = await import('node:child_process');
|
|
958
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
959
|
+
cwd: repoRoot,
|
|
960
|
+
encoding: 'utf8',
|
|
961
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
962
|
+
});
|
|
963
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
964
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
965
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
966
|
+
if (!mergeBase || !mergedTree) {
|
|
967
|
+
return {
|
|
968
|
+
status: 'failed',
|
|
969
|
+
equivalent: false,
|
|
970
|
+
baseHead,
|
|
971
|
+
branchHead,
|
|
972
|
+
mergeBase: mergeBase || undefined,
|
|
973
|
+
mergedTree: mergedTree || undefined,
|
|
974
|
+
durationMs: Date.now() - startedAt,
|
|
975
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
976
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
977
|
+
};
|
|
461
978
|
}
|
|
979
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
980
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
981
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
982
|
+
return {
|
|
983
|
+
status: equivalent ? 'passed' : 'failed',
|
|
984
|
+
equivalent,
|
|
985
|
+
baseHead,
|
|
986
|
+
branchHead,
|
|
987
|
+
mergeBase,
|
|
988
|
+
mergedTree,
|
|
989
|
+
expectedPatchId,
|
|
990
|
+
actualPatchId,
|
|
991
|
+
durationMs: Date.now() - startedAt,
|
|
992
|
+
};
|
|
993
|
+
} catch (e: any) {
|
|
994
|
+
return {
|
|
995
|
+
status: 'failed',
|
|
996
|
+
equivalent: false,
|
|
997
|
+
baseHead,
|
|
998
|
+
branchHead,
|
|
999
|
+
durationMs: Date.now() - startedAt,
|
|
1000
|
+
error: e?.message || String(e),
|
|
1001
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1002
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1003
|
+
};
|
|
462
1004
|
}
|
|
1005
|
+
}
|
|
463
1006
|
|
|
1007
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1008
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
464
1009
|
return {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1010
|
+
source: plan.source,
|
|
1011
|
+
sourceType: plan.sourceType,
|
|
1012
|
+
commands: plan.commands.map(command => ({
|
|
1013
|
+
displayCommand: command.displayCommand,
|
|
1014
|
+
category: command.category,
|
|
1015
|
+
source: command.source,
|
|
1016
|
+
cwd: command.cwd,
|
|
1017
|
+
timeoutMs: command.timeoutMs,
|
|
1018
|
+
})),
|
|
1019
|
+
unavailableReason: plan.unavailableReason,
|
|
1020
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1021
|
+
suggestions: plan.suggestions,
|
|
1022
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1023
|
+
note: plan.sourceType === 'unavailable'
|
|
1024
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1025
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
474
1026
|
};
|
|
475
1027
|
}
|
|
476
1028
|
|
|
@@ -478,7 +1030,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
478
1030
|
const { execFile } = await import('node:child_process');
|
|
479
1031
|
const { promisify } = await import('node:util');
|
|
480
1032
|
const execFileAsync = promisify(execFile);
|
|
481
|
-
const selection =
|
|
1033
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
482
1034
|
const summary: MeshRefineValidationSummary = {
|
|
483
1035
|
status: 'skipped',
|
|
484
1036
|
required: true,
|
|
@@ -487,22 +1039,28 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
487
1039
|
skippedReason: undefined,
|
|
488
1040
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
489
1041
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1042
|
+
configSource: selection.source,
|
|
1043
|
+
configSourceType: selection.sourceType,
|
|
1044
|
+
suggestions: selection.suggestions,
|
|
1045
|
+
suggestedConfig: selection.suggestedConfig,
|
|
490
1046
|
};
|
|
491
1047
|
|
|
492
1048
|
if (!selection.commands.length) {
|
|
493
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1049
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
494
1050
|
return summary;
|
|
495
1051
|
}
|
|
496
1052
|
|
|
497
1053
|
for (const candidate of selection.commands) {
|
|
498
1054
|
const startedAt = Date.now();
|
|
1055
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1056
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
499
1057
|
try {
|
|
500
1058
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
501
|
-
cwd
|
|
1059
|
+
cwd,
|
|
502
1060
|
encoding: 'utf8',
|
|
503
|
-
timeout
|
|
1061
|
+
timeout,
|
|
504
1062
|
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
505
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
1063
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
506
1064
|
});
|
|
507
1065
|
summary.commandsRun.push({
|
|
508
1066
|
command: candidate.command,
|
|
@@ -510,6 +1068,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
510
1068
|
displayCommand: candidate.displayCommand,
|
|
511
1069
|
category: candidate.category,
|
|
512
1070
|
source: candidate.source,
|
|
1071
|
+
cwd,
|
|
513
1072
|
passed: true,
|
|
514
1073
|
exitCode: 0,
|
|
515
1074
|
durationMs: Date.now() - startedAt,
|
|
@@ -523,6 +1082,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
523
1082
|
displayCommand: candidate.displayCommand,
|
|
524
1083
|
category: candidate.category,
|
|
525
1084
|
source: candidate.source,
|
|
1085
|
+
cwd,
|
|
526
1086
|
passed: false,
|
|
527
1087
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
528
1088
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
@@ -661,6 +1221,10 @@ export interface CommandRouterDeps {
|
|
|
661
1221
|
statusVersion?: string;
|
|
662
1222
|
/** Session host control plane */
|
|
663
1223
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
1224
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
1225
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1226
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
1227
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
664
1228
|
}
|
|
665
1229
|
|
|
666
1230
|
export interface CommandRouterResult {
|
|
@@ -772,42 +1336,255 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
772
1336
|
};
|
|
773
1337
|
}
|
|
774
1338
|
|
|
1339
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
1340
|
+
const raw = hostAddress.trim();
|
|
1341
|
+
if (!raw) throw new Error('hostAddress required');
|
|
1342
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
1343
|
+
url.pathname = '/api/v1/command';
|
|
1344
|
+
url.search = '';
|
|
1345
|
+
url.hash = '';
|
|
1346
|
+
return url.toString();
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
1350
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
1351
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
1352
|
+
? args.memberNode as Record<string, any>
|
|
1353
|
+
: null;
|
|
1354
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
1355
|
+
? (requestedNodeId
|
|
1356
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
1357
|
+
: mesh.nodes[0])
|
|
1358
|
+
: null;
|
|
1359
|
+
const source = explicit || configured;
|
|
1360
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
1361
|
+
? source.workspace.trim()
|
|
1362
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
1363
|
+
? args.workspace.trim()
|
|
1364
|
+
: process.cwd();
|
|
1365
|
+
if (!workspace) return null;
|
|
1366
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
1367
|
+
? source.id.trim()
|
|
1368
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
1369
|
+
? source.nodeId.trim()
|
|
1370
|
+
: undefined;
|
|
1371
|
+
return {
|
|
1372
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
1373
|
+
workspace,
|
|
1374
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
1375
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
1376
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
1377
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
1378
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
1379
|
+
role: 'member',
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
775
1383
|
export class DaemonCommandRouter {
|
|
776
1384
|
private deps: CommandRouterDeps;
|
|
777
1385
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
778
1386
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
779
1387
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
780
1388
|
private inlineMeshCache = new Map<string, any>();
|
|
1389
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
1390
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any }>();
|
|
781
1391
|
|
|
782
1392
|
constructor(deps: CommandRouterDeps) {
|
|
783
1393
|
this.deps = deps;
|
|
784
1394
|
}
|
|
785
1395
|
|
|
1396
|
+
private cloneJsonValue<T>(value: T): T {
|
|
1397
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
1398
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
1402
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
1403
|
+
const inlineNodesById = new Map<string, any>();
|
|
1404
|
+
for (const node of mesh.nodes) {
|
|
1405
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
1406
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
1407
|
+
}
|
|
1408
|
+
if (!inlineNodesById.size) return snapshot;
|
|
1409
|
+
|
|
1410
|
+
let changed = false;
|
|
1411
|
+
const unavailableNodeIds = new Set<string>();
|
|
1412
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
1413
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
1414
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
1415
|
+
const nodeId = readStringValue(entry);
|
|
1416
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
1420
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
1421
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
1422
|
+
if (!inlineNode) return statusNode;
|
|
1423
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
1424
|
+
if (!liveGit) return statusNode;
|
|
1425
|
+
const nextStatus = { ...statusNode };
|
|
1426
|
+
nextStatus.git = liveGit;
|
|
1427
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
1428
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
1429
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
1430
|
+
const connectionState = readStringValue(connection.state);
|
|
1431
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
1432
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
1433
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
1434
|
+
}
|
|
1435
|
+
delete nextStatus.gitProbePending;
|
|
1436
|
+
const error = readStringValue(nextStatus.error);
|
|
1437
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
1438
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
1439
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
1440
|
+
changed = true;
|
|
1441
|
+
return nextStatus;
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0)) return snapshot;
|
|
1445
|
+
const nextSourceOfTruth = {
|
|
1446
|
+
...sourceOfTruth,
|
|
1447
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
1448
|
+
directPeerTruth: {
|
|
1449
|
+
...directPeerTruth,
|
|
1450
|
+
satisfied: options?.requireDirectPeerTruth === true ? unavailableNodeIds.size === 0 : directPeerTruth.satisfied,
|
|
1451
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
1452
|
+
},
|
|
1453
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
1454
|
+
coordinatorOwnsLiveTruth: unavailableNodeIds.size === 0,
|
|
1455
|
+
currentStatus: unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
1456
|
+
} : {}),
|
|
1457
|
+
} : {}),
|
|
1458
|
+
};
|
|
1459
|
+
return {
|
|
1460
|
+
...snapshot,
|
|
1461
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 ? {
|
|
1462
|
+
success: false,
|
|
1463
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
1464
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
1465
|
+
} : {}),
|
|
1466
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
1467
|
+
nodes,
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
1472
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
1473
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
1474
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
1475
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
1476
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
1477
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
1478
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
1479
|
+
? snapshot.sourceOfTruth
|
|
1480
|
+
: {};
|
|
1481
|
+
snapshot.sourceOfTruth = {
|
|
1482
|
+
...sourceOfTruth,
|
|
1483
|
+
aggregateSnapshot: {
|
|
1484
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
1485
|
+
? sourceOfTruth.aggregateSnapshot
|
|
1486
|
+
: {}),
|
|
1487
|
+
owner: 'coordinator_daemon_memory',
|
|
1488
|
+
cached: true,
|
|
1489
|
+
source: 'memory',
|
|
1490
|
+
refreshReason: 'memory_cache_hit',
|
|
1491
|
+
ageMs,
|
|
1492
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
1493
|
+
returnedAt: new Date().toISOString(),
|
|
1494
|
+
},
|
|
1495
|
+
};
|
|
1496
|
+
return snapshot;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
1500
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
1501
|
+
const builtAt = Date.now();
|
|
1502
|
+
const next = this.cloneJsonValue(snapshot);
|
|
1503
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
1504
|
+
? next.sourceOfTruth
|
|
1505
|
+
: {};
|
|
1506
|
+
next.sourceOfTruth = {
|
|
1507
|
+
...sourceOfTruth,
|
|
1508
|
+
aggregateSnapshot: {
|
|
1509
|
+
owner: 'coordinator_daemon_memory',
|
|
1510
|
+
cached: false,
|
|
1511
|
+
source: 'live_refresh',
|
|
1512
|
+
refreshReason,
|
|
1513
|
+
ageMs: 0,
|
|
1514
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
1515
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
1516
|
+
},
|
|
1517
|
+
};
|
|
1518
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next) });
|
|
1519
|
+
return next;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
786
1522
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
787
1523
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
788
|
-
this.
|
|
789
|
-
return inlineMesh as any;
|
|
1524
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
790
1525
|
}
|
|
791
1526
|
return this.inlineMeshCache.get(meshId);
|
|
792
1527
|
}
|
|
793
1528
|
|
|
1529
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
1530
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
1531
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
1532
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
1533
|
+
if (cached) {
|
|
1534
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
1535
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
1536
|
+
return merged;
|
|
1537
|
+
}
|
|
1538
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
1539
|
+
return sanitizedInlineMesh as any;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
794
1542
|
private async getMeshForCommand(
|
|
795
1543
|
meshId: string,
|
|
796
1544
|
inlineMesh?: unknown,
|
|
797
1545
|
options?: { preferInline?: boolean },
|
|
798
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
1546
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
799
1547
|
const preferInline = options?.preferInline === true;
|
|
800
1548
|
if (preferInline) {
|
|
801
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
802
|
-
if (cached)
|
|
1549
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1550
|
+
if (cached) {
|
|
1551
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1552
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
1553
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
1554
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
1555
|
+
}
|
|
1556
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1557
|
+
}
|
|
1558
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1559
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1560
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
1561
|
+
}
|
|
803
1562
|
}
|
|
804
1563
|
try {
|
|
805
1564
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
806
1565
|
const mesh = getMesh(meshId);
|
|
807
|
-
if (mesh) return { mesh, inline: false };
|
|
1566
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
808
1567
|
} catch { /* fall through to inline cache */ }
|
|
809
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
810
|
-
|
|
1568
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1569
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1570
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1571
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
1575
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
1580
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
1581
|
+
const mesh = meshRecord?.mesh;
|
|
1582
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1583
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
1584
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
1585
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
1586
|
+
}
|
|
1587
|
+
return null;
|
|
811
1588
|
}
|
|
812
1589
|
|
|
813
1590
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -817,6 +1594,7 @@ export class DaemonCommandRouter {
|
|
|
817
1594
|
else mesh.nodes.push(node);
|
|
818
1595
|
mesh.updatedAt = new Date().toISOString();
|
|
819
1596
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1597
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
820
1598
|
}
|
|
821
1599
|
|
|
822
1600
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -826,6 +1604,7 @@ export class DaemonCommandRouter {
|
|
|
826
1604
|
mesh.nodes.splice(idx, 1);
|
|
827
1605
|
mesh.updatedAt = new Date().toISOString();
|
|
828
1606
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1607
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
829
1608
|
return true;
|
|
830
1609
|
}
|
|
831
1610
|
|
|
@@ -1104,6 +1883,7 @@ export class DaemonCommandRouter {
|
|
|
1104
1883
|
const deletedSessionIds: string[] = [];
|
|
1105
1884
|
const skippedSessionIds: string[] = [];
|
|
1106
1885
|
const skippedLiveSessionIds: string[] = [];
|
|
1886
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1107
1887
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1108
1888
|
const recordsRemainSessionIds: string[] = [];
|
|
1109
1889
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1138,6 +1918,12 @@ export class DaemonCommandRouter {
|
|
|
1138
1918
|
const completed = this.isCompletedHostedSession(record);
|
|
1139
1919
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1140
1920
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
1921
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
1922
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
1923
|
+
skippedSessionIds.push(sessionId);
|
|
1924
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1141
1927
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1142
1928
|
skippedSessionIds.push(sessionId);
|
|
1143
1929
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1207,6 +1993,7 @@ export class DaemonCommandRouter {
|
|
|
1207
1993
|
deletedSessionIds,
|
|
1208
1994
|
skippedSessionIds,
|
|
1209
1995
|
skippedLiveSessionIds,
|
|
1996
|
+
skippedCoordinatorSessionIds,
|
|
1210
1997
|
...(deleteUnsupported ? {
|
|
1211
1998
|
deleteUnsupported: true,
|
|
1212
1999
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1361,7 +2148,8 @@ export class DaemonCommandRouter {
|
|
|
1361
2148
|
}
|
|
1362
2149
|
|
|
1363
2150
|
case 'get_pending_mesh_events': {
|
|
1364
|
-
const
|
|
2151
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2152
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1365
2153
|
return { success: true, events };
|
|
1366
2154
|
}
|
|
1367
2155
|
|
|
@@ -1966,15 +2754,44 @@ export class DaemonCommandRouter {
|
|
|
1966
2754
|
case 'get_mesh': {
|
|
1967
2755
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1968
2756
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2757
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2758
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
2759
|
+
|
|
2760
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
2761
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
2762
|
+
mesh: meshRecord.mesh,
|
|
2763
|
+
meshSource: meshRecord.source,
|
|
2764
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
2765
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
2766
|
+
localMachineId: loadConfig().machineId || '',
|
|
2767
|
+
});
|
|
2768
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
2769
|
+
const sourceOfTruth = {
|
|
2770
|
+
membership: meshRecord.source === 'inline_cache'
|
|
2771
|
+
? 'coordinator_inline_mesh_cache'
|
|
2772
|
+
: meshRecord.source === 'local_config'
|
|
2773
|
+
? 'local_mesh_config'
|
|
2774
|
+
: 'inline_bootstrap_snapshot',
|
|
2775
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
2776
|
+
directPeerTruth: {
|
|
2777
|
+
required: requireDirectPeerTruth,
|
|
2778
|
+
satisfied: directTruthSatisfied,
|
|
2779
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
2780
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
2781
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
2782
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
2783
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
2784
|
+
},
|
|
2785
|
+
};
|
|
2786
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
2787
|
+
return {
|
|
2788
|
+
success: false,
|
|
2789
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
2790
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
2791
|
+
sourceOfTruth,
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
1978
2795
|
}
|
|
1979
2796
|
|
|
1980
2797
|
case 'create_mesh': {
|
|
@@ -1985,7 +2802,10 @@ export class DaemonCommandRouter {
|
|
|
1985
2802
|
if (!name) return { success: false, error: 'name required' };
|
|
1986
2803
|
try {
|
|
1987
2804
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
1988
|
-
const
|
|
2805
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
2806
|
+
? args.meshHost
|
|
2807
|
+
: undefined;
|
|
2808
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
1989
2809
|
return { success: true, mesh };
|
|
1990
2810
|
} catch (e: any) {
|
|
1991
2811
|
return { success: false, error: e.message };
|
|
@@ -2002,16 +2822,237 @@ export class DaemonCommandRouter {
|
|
|
2002
2822
|
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
2003
2823
|
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
2004
2824
|
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
2825
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
2005
2826
|
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
2006
2827
|
const mesh = updateMesh(meshId, patch as any);
|
|
2007
2828
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2008
2829
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2830
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2009
2831
|
return { success: true, mesh };
|
|
2010
2832
|
} catch (e: any) {
|
|
2011
2833
|
return { success: false, error: e.message };
|
|
2012
2834
|
}
|
|
2013
2835
|
}
|
|
2014
2836
|
|
|
2837
|
+
case 'get_mesh_host_pairing': {
|
|
2838
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2839
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2840
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2841
|
+
const mesh = meshRecord?.mesh;
|
|
2842
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2843
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
2844
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
2845
|
+
return {
|
|
2846
|
+
success: true,
|
|
2847
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
2848
|
+
meshId,
|
|
2849
|
+
hostAddress: meshHost.hostAddress,
|
|
2850
|
+
meshHost,
|
|
2851
|
+
manualPairing: {
|
|
2852
|
+
status: pairingStatus,
|
|
2853
|
+
joinImplemented: true,
|
|
2854
|
+
protocol: 'standalone_command_direct_v1',
|
|
2855
|
+
description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
|
|
2856
|
+
},
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
case 'configure_mesh_host_pairing': {
|
|
2861
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2862
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
2863
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
2864
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2865
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
2866
|
+
try {
|
|
2867
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
2868
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
2869
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
2870
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
2871
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
2872
|
+
return {
|
|
2873
|
+
success: true,
|
|
2874
|
+
code: 'mesh_host_pairing_pending',
|
|
2875
|
+
meshId,
|
|
2876
|
+
hostAddress: configured.hostAddress,
|
|
2877
|
+
meshHost,
|
|
2878
|
+
manualPairing: {
|
|
2879
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
2880
|
+
joinImplemented: true,
|
|
2881
|
+
protocol: 'standalone_command_direct_v1',
|
|
2882
|
+
description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
|
|
2883
|
+
},
|
|
2884
|
+
};
|
|
2885
|
+
} catch (e: any) {
|
|
2886
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
case 'create_mesh_host_pairing_token': {
|
|
2891
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2892
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2893
|
+
try {
|
|
2894
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
2895
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
2896
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
2897
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
2898
|
+
});
|
|
2899
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
2900
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
2901
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2902
|
+
return {
|
|
2903
|
+
success: true,
|
|
2904
|
+
code: 'mesh_host_pairing_token_created',
|
|
2905
|
+
meshId,
|
|
2906
|
+
token: created.token,
|
|
2907
|
+
tokenId: created.tokenId,
|
|
2908
|
+
expiresAt: created.expiresAt,
|
|
2909
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
2910
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
2911
|
+
};
|
|
2912
|
+
} catch (e: any) {
|
|
2913
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
case 'apply_mesh_host_join': {
|
|
2918
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2919
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
2920
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
2921
|
+
? args.memberNode
|
|
2922
|
+
: null;
|
|
2923
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2924
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
2925
|
+
try {
|
|
2926
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
2927
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
2928
|
+
token,
|
|
2929
|
+
memberNode: memberNode as any,
|
|
2930
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
2931
|
+
});
|
|
2932
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
2933
|
+
if (!applied.accepted) {
|
|
2934
|
+
return {
|
|
2935
|
+
success: false,
|
|
2936
|
+
code: 'mesh_host_join_rejected',
|
|
2937
|
+
meshId,
|
|
2938
|
+
tokenId: applied.tokenId,
|
|
2939
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
2940
|
+
error: applied.reason,
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
2944
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2945
|
+
try {
|
|
2946
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2947
|
+
appendLedgerEntry(meshId, {
|
|
2948
|
+
kind: 'node_joined',
|
|
2949
|
+
nodeId: applied.node.id,
|
|
2950
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
2951
|
+
});
|
|
2952
|
+
} catch { /* ledger append is best-effort */ }
|
|
2953
|
+
return {
|
|
2954
|
+
success: true,
|
|
2955
|
+
code: 'mesh_host_join_accepted',
|
|
2956
|
+
meshId,
|
|
2957
|
+
node: applied.node,
|
|
2958
|
+
tokenId: applied.tokenId,
|
|
2959
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
2960
|
+
};
|
|
2961
|
+
} catch (e: any) {
|
|
2962
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
case 'join_mesh_host_pairing': {
|
|
2967
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2968
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
2969
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2970
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
2971
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2972
|
+
const mesh = meshRecord?.mesh;
|
|
2973
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2974
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
2975
|
+
if (meshHost.role !== 'member') {
|
|
2976
|
+
return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
|
|
2977
|
+
}
|
|
2978
|
+
try {
|
|
2979
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
2980
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
2981
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
2982
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
2983
|
+
}
|
|
2984
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
2985
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
2986
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
2987
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
2988
|
+
? args.hostDaemonId.trim()
|
|
2989
|
+
: meshHost.hostDaemonId;
|
|
2990
|
+
let hostResult: any;
|
|
2991
|
+
let transport: string;
|
|
2992
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
2993
|
+
transport = 'mesh_command_dispatch';
|
|
2994
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
2995
|
+
meshId: hostMeshId,
|
|
2996
|
+
token,
|
|
2997
|
+
memberMeshId: meshId,
|
|
2998
|
+
memberNode,
|
|
2999
|
+
});
|
|
3000
|
+
} else if (meshHost.hostAddress) {
|
|
3001
|
+
transport = 'standalone_http_command';
|
|
3002
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
3003
|
+
const response = await fetch(commandUrl, {
|
|
3004
|
+
method: 'POST',
|
|
3005
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3006
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
3007
|
+
});
|
|
3008
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
3009
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
3010
|
+
} else {
|
|
3011
|
+
return {
|
|
3012
|
+
success: false,
|
|
3013
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
3014
|
+
meshId,
|
|
3015
|
+
meshHost,
|
|
3016
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
if (!hostResult?.success) {
|
|
3020
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
3021
|
+
}
|
|
3022
|
+
const joined = meshRecord.inline
|
|
3023
|
+
? null
|
|
3024
|
+
: markMeshHostPairingJoined(meshId, {
|
|
3025
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3026
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
3027
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
3028
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
3029
|
+
});
|
|
3030
|
+
if (joined) {
|
|
3031
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
3032
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3033
|
+
}
|
|
3034
|
+
return {
|
|
3035
|
+
success: true,
|
|
3036
|
+
code: 'mesh_host_join_applied',
|
|
3037
|
+
meshId,
|
|
3038
|
+
hostMeshId,
|
|
3039
|
+
transport,
|
|
3040
|
+
node: hostResult.node,
|
|
3041
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3042
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
3043
|
+
hostResult,
|
|
3044
|
+
manualPairing: {
|
|
3045
|
+
status: 'paired',
|
|
3046
|
+
joinImplemented: true,
|
|
3047
|
+
protocol: 'standalone_command_direct_v1',
|
|
3048
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
3049
|
+
},
|
|
3050
|
+
};
|
|
3051
|
+
} catch (e: any) {
|
|
3052
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
|
|
2015
3056
|
case 'delete_mesh': {
|
|
2016
3057
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2017
3058
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -2105,6 +3146,8 @@ export class DaemonCommandRouter {
|
|
|
2105
3146
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2106
3147
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2107
3148
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3149
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
3150
|
+
if (ownerFailure) return ownerFailure;
|
|
2108
3151
|
try {
|
|
2109
3152
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2110
3153
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -2120,6 +3163,8 @@ export class DaemonCommandRouter {
|
|
|
2120
3163
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2121
3164
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2122
3165
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3166
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
3167
|
+
if (ownerFailure) return ownerFailure;
|
|
2123
3168
|
try {
|
|
2124
3169
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2125
3170
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -2141,6 +3186,8 @@ export class DaemonCommandRouter {
|
|
|
2141
3186
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2142
3187
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2143
3188
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
3189
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
3190
|
+
if (ownerFailure) return ownerFailure;
|
|
2144
3191
|
try {
|
|
2145
3192
|
const { addNode } = await import('../config/mesh-config.js');
|
|
2146
3193
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -2151,7 +3198,8 @@ export class DaemonCommandRouter {
|
|
|
2151
3198
|
...(readOnly ? { readOnly: true } : {}),
|
|
2152
3199
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
2153
3200
|
};
|
|
2154
|
-
const
|
|
3201
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
3202
|
+
const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}), ...(role ? { role } : {}) });
|
|
2155
3203
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
2156
3204
|
return { success: true, node };
|
|
2157
3205
|
} catch (e: any) {
|
|
@@ -2163,6 +3211,8 @@ export class DaemonCommandRouter {
|
|
|
2163
3211
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2164
3212
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2165
3213
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3214
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
3215
|
+
if (ownerFailure) return ownerFailure;
|
|
2166
3216
|
try {
|
|
2167
3217
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2168
3218
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2191,6 +3241,8 @@ export class DaemonCommandRouter {
|
|
|
2191
3241
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2192
3242
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2193
3243
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3244
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
3245
|
+
if (ownerFailure) return ownerFailure;
|
|
2194
3246
|
try {
|
|
2195
3247
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2196
3248
|
const mesh = meshRecord?.mesh;
|
|
@@ -2216,38 +3268,104 @@ export class DaemonCommandRouter {
|
|
|
2216
3268
|
}
|
|
2217
3269
|
}
|
|
2218
3270
|
|
|
3271
|
+
case 'get_mesh_refine_config_schema': {
|
|
3272
|
+
return {
|
|
3273
|
+
success: true,
|
|
3274
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
3275
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
3276
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
3277
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
case 'validate_mesh_refine_config': {
|
|
3282
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
3283
|
+
const mesh = args?.inlineMesh || {};
|
|
3284
|
+
const loaded = args?.config !== undefined
|
|
3285
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
3286
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
3287
|
+
const validation = loaded.config
|
|
3288
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
3289
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
3290
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
case 'suggest_mesh_refine_config': {
|
|
3294
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
3295
|
+
const mesh = args?.inlineMesh || {};
|
|
3296
|
+
return {
|
|
3297
|
+
success: true,
|
|
3298
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
3299
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
case 'plan_mesh_refine_node': {
|
|
3304
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3305
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3306
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3307
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3308
|
+
const mesh = meshRecord?.mesh;
|
|
3309
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3310
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
3311
|
+
return {
|
|
3312
|
+
success: true,
|
|
3313
|
+
dryRun: true,
|
|
3314
|
+
nodeId,
|
|
3315
|
+
workspace: node.workspace,
|
|
3316
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
3317
|
+
mergeWillRun: false,
|
|
3318
|
+
cleanupWillRun: false,
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
|
|
2219
3322
|
case 'refine_mesh_node': {
|
|
2220
3323
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2221
3324
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2222
3325
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3326
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
2223
3327
|
try {
|
|
2224
3328
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2225
3329
|
const mesh = meshRecord?.mesh;
|
|
2226
3330
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2227
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh
|
|
3331
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
2228
3332
|
|
|
2229
3333
|
if (!node.isLocalWorktree || !node.workspace) {
|
|
2230
|
-
return { success: false, error: `Refinery requires a local worktree node
|
|
3334
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
2231
3335
|
}
|
|
2232
3336
|
|
|
2233
3337
|
const sourceNode = node.clonedFromNodeId
|
|
2234
3338
|
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2235
3339
|
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2236
3340
|
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2237
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
3341
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
2238
3342
|
|
|
2239
3343
|
const { execFile } = await import('node:child_process');
|
|
2240
3344
|
const { promisify } = await import('node:util');
|
|
2241
3345
|
const execFileAsync = promisify(execFile);
|
|
2242
3346
|
|
|
3347
|
+
const resolveStarted = Date.now();
|
|
2243
3348
|
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2244
3349
|
const branch = branchStdout.trim();
|
|
2245
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
3350
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
2246
3351
|
|
|
2247
3352
|
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2248
3353
|
const baseBranch = baseBranchStdout.trim();
|
|
3354
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3355
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
3356
|
+
const baseHead = baseHeadStdout.trim();
|
|
3357
|
+
const branchHead = branchHeadStdout.trim();
|
|
3358
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
2249
3359
|
|
|
3360
|
+
const validationStarted = Date.now();
|
|
2250
3361
|
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
3362
|
+
recordMeshRefineStage(
|
|
3363
|
+
refineStages,
|
|
3364
|
+
'validation',
|
|
3365
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
3366
|
+
validationStarted,
|
|
3367
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
3368
|
+
);
|
|
2251
3369
|
if (validationSummary.status === 'failed') {
|
|
2252
3370
|
return {
|
|
2253
3371
|
success: false,
|
|
@@ -2257,6 +3375,7 @@ export class DaemonCommandRouter {
|
|
|
2257
3375
|
branch,
|
|
2258
3376
|
into: baseBranch,
|
|
2259
3377
|
validationSummary,
|
|
3378
|
+
refineStages,
|
|
2260
3379
|
finalBranchConvergenceState: {
|
|
2261
3380
|
branch,
|
|
2262
3381
|
baseBranch,
|
|
@@ -2276,6 +3395,7 @@ export class DaemonCommandRouter {
|
|
|
2276
3395
|
branch,
|
|
2277
3396
|
into: baseBranch,
|
|
2278
3397
|
validationSummary,
|
|
3398
|
+
refineStages,
|
|
2279
3399
|
finalBranchConvergenceState: {
|
|
2280
3400
|
branch,
|
|
2281
3401
|
baseBranch,
|
|
@@ -2287,39 +3407,127 @@ export class DaemonCommandRouter {
|
|
|
2287
3407
|
};
|
|
2288
3408
|
}
|
|
2289
3409
|
|
|
3410
|
+
const patchEquivalenceStarted = Date.now();
|
|
3411
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3412
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
3413
|
+
equivalent: patchEquivalence.equivalent,
|
|
3414
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
3415
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
3416
|
+
error: patchEquivalence.error,
|
|
3417
|
+
});
|
|
3418
|
+
if (!patchEquivalence.equivalent) {
|
|
3419
|
+
return {
|
|
3420
|
+
success: false,
|
|
3421
|
+
code: 'patch_equivalence_failed',
|
|
3422
|
+
convergenceStatus: 'blocked_review',
|
|
3423
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
3424
|
+
branch,
|
|
3425
|
+
into: baseBranch,
|
|
3426
|
+
validationSummary,
|
|
3427
|
+
patchEquivalence,
|
|
3428
|
+
refineStages,
|
|
3429
|
+
finalBranchConvergenceState: {
|
|
3430
|
+
branch,
|
|
3431
|
+
baseBranch,
|
|
3432
|
+
merged: false,
|
|
3433
|
+
removed: false,
|
|
3434
|
+
validation: 'passed',
|
|
3435
|
+
patchEquivalence: 'failed',
|
|
3436
|
+
status: 'blocked_review',
|
|
3437
|
+
},
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
3442
|
+
const mergeStarted = Date.now();
|
|
2290
3443
|
try {
|
|
2291
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3444
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3445
|
+
mergeResult = {
|
|
3446
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
3447
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
3448
|
+
durationMs: Date.now() - mergeStarted,
|
|
3449
|
+
};
|
|
3450
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
2292
3451
|
} catch (e: any) {
|
|
3452
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
3453
|
+
error: e?.message || String(e),
|
|
3454
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
3455
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
3456
|
+
});
|
|
2293
3457
|
return {
|
|
2294
3458
|
success: false,
|
|
2295
3459
|
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2296
3460
|
validationSummary,
|
|
3461
|
+
patchEquivalence,
|
|
3462
|
+
refineStages,
|
|
2297
3463
|
finalBranchConvergenceState: {
|
|
2298
3464
|
branch,
|
|
2299
3465
|
baseBranch,
|
|
2300
3466
|
merged: false,
|
|
2301
3467
|
removed: false,
|
|
2302
3468
|
validation: 'passed',
|
|
3469
|
+
patchEquivalence: 'passed',
|
|
2303
3470
|
status: 'not_mergeable',
|
|
2304
3471
|
},
|
|
2305
3472
|
};
|
|
2306
3473
|
}
|
|
2307
3474
|
|
|
3475
|
+
const cleanupStarted = Date.now();
|
|
2308
3476
|
const removeResult = await this.execute('remove_mesh_node', {
|
|
2309
3477
|
meshId,
|
|
2310
3478
|
nodeId,
|
|
2311
|
-
sessionCleanupMode: '
|
|
3479
|
+
sessionCleanupMode: 'preserve',
|
|
2312
3480
|
inlineMesh: args?.inlineMesh,
|
|
2313
3481
|
});
|
|
3482
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
3483
|
+
removed: removeResult?.removed,
|
|
3484
|
+
code: removeResult?.code,
|
|
3485
|
+
error: removeResult?.error,
|
|
3486
|
+
});
|
|
2314
3487
|
|
|
3488
|
+
let ledgerError: string | undefined;
|
|
3489
|
+
const ledgerStarted = Date.now();
|
|
2315
3490
|
try {
|
|
2316
3491
|
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2317
3492
|
appendLedgerEntry(meshId, {
|
|
2318
3493
|
kind: 'node_removed',
|
|
2319
3494
|
nodeId,
|
|
2320
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
3495
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
2321
3496
|
});
|
|
2322
|
-
|
|
3497
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
3498
|
+
} catch (e: any) {
|
|
3499
|
+
ledgerError = e?.message || String(e);
|
|
3500
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
const finalBranchConvergenceState = {
|
|
3504
|
+
branch: baseBranch,
|
|
3505
|
+
mergedBranch: branch,
|
|
3506
|
+
baseBranch,
|
|
3507
|
+
merged: true,
|
|
3508
|
+
removed: removeResult?.success !== false,
|
|
3509
|
+
validation: 'passed',
|
|
3510
|
+
patchEquivalence: 'passed',
|
|
3511
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
3512
|
+
};
|
|
3513
|
+
|
|
3514
|
+
if (removeResult?.success === false) {
|
|
3515
|
+
return {
|
|
3516
|
+
success: false,
|
|
3517
|
+
code: 'cleanup_failed',
|
|
3518
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
3519
|
+
merged: true,
|
|
3520
|
+
branch,
|
|
3521
|
+
into: baseBranch,
|
|
3522
|
+
removeResult,
|
|
3523
|
+
validationSummary,
|
|
3524
|
+
patchEquivalence,
|
|
3525
|
+
mergeResult,
|
|
3526
|
+
refineStages,
|
|
3527
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3528
|
+
finalBranchConvergenceState,
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
2323
3531
|
|
|
2324
3532
|
return {
|
|
2325
3533
|
success: true,
|
|
@@ -2328,18 +3536,14 @@ export class DaemonCommandRouter {
|
|
|
2328
3536
|
into: baseBranch,
|
|
2329
3537
|
removeResult,
|
|
2330
3538
|
validationSummary,
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
removed: removeResult?.success !== false,
|
|
2337
|
-
validation: 'passed',
|
|
2338
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2339
|
-
},
|
|
3539
|
+
patchEquivalence,
|
|
3540
|
+
mergeResult,
|
|
3541
|
+
refineStages,
|
|
3542
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3543
|
+
finalBranchConvergenceState,
|
|
2340
3544
|
};
|
|
2341
3545
|
} catch (e: any) {
|
|
2342
|
-
return { success: false, error: e.message };
|
|
3546
|
+
return { success: false, error: e.message, refineStages };
|
|
2343
3547
|
}
|
|
2344
3548
|
}
|
|
2345
3549
|
|
|
@@ -2384,6 +3588,7 @@ export class DaemonCommandRouter {
|
|
|
2384
3588
|
} else {
|
|
2385
3589
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2386
3590
|
removed = removeNode(meshId, nodeId);
|
|
3591
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2387
3592
|
}
|
|
2388
3593
|
|
|
2389
3594
|
// Record in task ledger
|
|
@@ -2421,6 +3626,8 @@ export class DaemonCommandRouter {
|
|
|
2421
3626
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2422
3627
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2423
3628
|
if (!branch) return { success: false, error: 'branch required' };
|
|
3629
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
3630
|
+
if (ownerFailure) return ownerFailure;
|
|
2424
3631
|
|
|
2425
3632
|
try {
|
|
2426
3633
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2469,6 +3676,7 @@ export class DaemonCommandRouter {
|
|
|
2469
3676
|
policy: { ...(sourceNode.policy || {}) },
|
|
2470
3677
|
});
|
|
2471
3678
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
3679
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2472
3680
|
}
|
|
2473
3681
|
|
|
2474
3682
|
// Initialize submodules if policy allows (default: true)
|
|
@@ -2510,6 +3718,8 @@ export class DaemonCommandRouter {
|
|
|
2510
3718
|
case 'trigger_mesh_queue': {
|
|
2511
3719
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2512
3720
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3721
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
3722
|
+
if (ownerFailure) return ownerFailure;
|
|
2513
3723
|
try {
|
|
2514
3724
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2515
3725
|
if (meshId) {
|
|
@@ -2541,6 +3751,15 @@ export class DaemonCommandRouter {
|
|
|
2541
3751
|
mesh = getMesh(meshId);
|
|
2542
3752
|
}
|
|
2543
3753
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3754
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3755
|
+
if (!meshHost.canOwnCoordinator) {
|
|
3756
|
+
return {
|
|
3757
|
+
success: false,
|
|
3758
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
3759
|
+
meshId,
|
|
3760
|
+
cliType,
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
2544
3763
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2545
3764
|
|
|
2546
3765
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2560,7 +3779,16 @@ export class DaemonCommandRouter {
|
|
|
2560
3779
|
cliType,
|
|
2561
3780
|
};
|
|
2562
3781
|
}
|
|
2563
|
-
const
|
|
3782
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
3783
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
3784
|
+
: [];
|
|
3785
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
3786
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
3787
|
+
meshId,
|
|
3788
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
3789
|
+
liveSessionRecords: liveMeshSessions,
|
|
3790
|
+
allowCoordinatorSession: true,
|
|
3791
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2564
3792
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2565
3793
|
if (!cliType) {
|
|
2566
3794
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -2905,6 +4133,27 @@ export class DaemonCommandRouter {
|
|
|
2905
4133
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2906
4134
|
const mesh = meshRecord?.mesh;
|
|
2907
4135
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4136
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4137
|
+
|
|
4138
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
4139
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
4140
|
+
if (!refreshRequested) {
|
|
4141
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
4142
|
+
if (cachedStatus) {
|
|
4143
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
4144
|
+
meshId,
|
|
4145
|
+
command: 'mesh_status',
|
|
4146
|
+
refreshRequested,
|
|
4147
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
4148
|
+
});
|
|
4149
|
+
return cachedStatus;
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
const refreshReason = refreshRequested
|
|
4153
|
+
? 'explicit_refresh'
|
|
4154
|
+
: hadAggregateCache
|
|
4155
|
+
? 'stale_pending_cache_refresh'
|
|
4156
|
+
: 'cold_cache_miss';
|
|
2908
4157
|
|
|
2909
4158
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
2910
4159
|
const queue = getQueue(meshId);
|
|
@@ -2913,58 +4162,344 @@ export class DaemonCommandRouter {
|
|
|
2913
4162
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2914
4163
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2915
4164
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
2916
|
-
|
|
4165
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
4166
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
4167
|
+
: [];
|
|
4168
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
4169
|
+
|
|
4170
|
+
const localMachineId = loadConfig().machineId || '';
|
|
4171
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
4172
|
+
const directTruth = requireDirectPeerTruth
|
|
4173
|
+
? await hydrateInlineMeshDirectTruth({
|
|
4174
|
+
mesh,
|
|
4175
|
+
meshSource: meshRecord.source,
|
|
4176
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4177
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
4178
|
+
localMachineId,
|
|
4179
|
+
})
|
|
4180
|
+
: {
|
|
4181
|
+
directEvidenceCount: 0,
|
|
4182
|
+
localConfirmedCount: 0,
|
|
4183
|
+
peerAttemptedCount: 0,
|
|
4184
|
+
peerConfirmedCount: 0,
|
|
4185
|
+
unavailableNodeIds: [] as string[],
|
|
4186
|
+
};
|
|
4187
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
4188
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
4189
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
4190
|
+
&& !refreshRequested
|
|
4191
|
+
&& directTruth.directEvidenceCount > 0
|
|
4192
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
4193
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
4194
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
4195
|
+
: directTruth;
|
|
4196
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
4197
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && effectiveDirectTruth.unavailableNodeIds.length === 0);
|
|
4198
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4199
|
+
const failureResult = {
|
|
4200
|
+
success: false,
|
|
4201
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
4202
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
4203
|
+
sourceOfTruth: {
|
|
4204
|
+
membership: meshRecord.source === 'inline_cache'
|
|
4205
|
+
? 'coordinator_inline_mesh_cache'
|
|
4206
|
+
: meshRecord.source === 'local_config'
|
|
4207
|
+
? 'local_mesh_config'
|
|
4208
|
+
: 'inline_bootstrap_snapshot',
|
|
4209
|
+
coordinatorOwnsLiveTruth: false,
|
|
4210
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
4211
|
+
directPeerTruth: {
|
|
4212
|
+
required: true,
|
|
4213
|
+
satisfied: false,
|
|
4214
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4215
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4216
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4217
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4218
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4219
|
+
},
|
|
4220
|
+
},
|
|
4221
|
+
};
|
|
4222
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
4223
|
+
meshId,
|
|
4224
|
+
command: 'mesh_status',
|
|
4225
|
+
refreshRequested,
|
|
4226
|
+
meshSource: meshRecord.source,
|
|
4227
|
+
directTruth,
|
|
4228
|
+
});
|
|
4229
|
+
return failureResult;
|
|
4230
|
+
}
|
|
4231
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
4232
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
4233
|
+
mesh.coordinator?.preferredNodeId,
|
|
4234
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
4235
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
4236
|
+
);
|
|
4237
|
+
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
4238
|
+
? selectedCoordinatorNodeId
|
|
4239
|
+
: undefined;
|
|
4240
|
+
const refreshedAt = new Date().toISOString();
|
|
2917
4241
|
const nodeStatuses = [];
|
|
2918
|
-
for (const node of mesh.nodes || []) {
|
|
4242
|
+
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
4243
|
+
const nodeId = String(node.id || node.nodeId || '');
|
|
4244
|
+
const daemonId = readStringValue(node.daemonId);
|
|
4245
|
+
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
4246
|
+
const isSelfNode = Boolean(
|
|
4247
|
+
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
4248
|
+
) || Boolean(
|
|
4249
|
+
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
4250
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
2919
4251
|
const status: Record<string, unknown> = {
|
|
2920
|
-
nodeId
|
|
4252
|
+
nodeId,
|
|
2921
4253
|
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2922
4254
|
workspace: node.workspace,
|
|
2923
4255
|
repoRoot: node.repoRoot,
|
|
2924
4256
|
isLocalWorktree: node.isLocalWorktree,
|
|
2925
4257
|
worktreeBranch: node.worktreeBranch,
|
|
2926
|
-
|
|
4258
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
4259
|
+
daemonId,
|
|
2927
4260
|
machineId: node.machineId,
|
|
4261
|
+
machineStatus: node.machineStatus,
|
|
2928
4262
|
health: 'unknown',
|
|
2929
4263
|
providers: node.providers || [],
|
|
4264
|
+
providerPriority,
|
|
2930
4265
|
activeSessions: [],
|
|
4266
|
+
activeSessionDetails: [],
|
|
4267
|
+
launchReady: false,
|
|
2931
4268
|
};
|
|
2932
|
-
if (
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
4269
|
+
if (isSelfNode) {
|
|
4270
|
+
status.connection = {
|
|
4271
|
+
perspective: 'selected_coordinator',
|
|
4272
|
+
source: 'mesh_peer_status',
|
|
4273
|
+
state: 'self',
|
|
4274
|
+
transport: 'local',
|
|
4275
|
+
reported: true,
|
|
4276
|
+
reason: 'Selected coordinator daemon',
|
|
4277
|
+
lastStateChangeAt: refreshedAt,
|
|
4278
|
+
};
|
|
4279
|
+
} else if (daemonId) {
|
|
4280
|
+
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
4281
|
+
status.connection = connection ?? {
|
|
4282
|
+
perspective: 'selected_coordinator',
|
|
4283
|
+
source: 'not_reported',
|
|
4284
|
+
state: 'unknown',
|
|
4285
|
+
transport: 'unknown',
|
|
4286
|
+
reported: false,
|
|
4287
|
+
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
4288
|
+
};
|
|
4289
|
+
} else {
|
|
4290
|
+
status.connection = {
|
|
4291
|
+
perspective: 'selected_coordinator',
|
|
4292
|
+
source: 'not_reported',
|
|
4293
|
+
state: 'unknown',
|
|
4294
|
+
transport: 'unknown',
|
|
4295
|
+
reported: false,
|
|
4296
|
+
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
4300
|
+
meshId,
|
|
4301
|
+
node,
|
|
4302
|
+
nodeId,
|
|
4303
|
+
liveSessionRecords: liveMeshSessions,
|
|
4304
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4305
|
+
});
|
|
4306
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4307
|
+
meshId,
|
|
4308
|
+
nodeId,
|
|
4309
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
4310
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4311
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
4312
|
+
status.workspace = workspace || node.workspace;
|
|
4313
|
+
if (matchedLiveSessionRecords.length > 0) {
|
|
4314
|
+
const sessionIds = matchedLiveSessionRecords
|
|
4315
|
+
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
4316
|
+
.filter(Boolean);
|
|
4317
|
+
const providerTypes = matchedLiveSessionRecords
|
|
4318
|
+
.map((record: any) => readStringValue(record?.providerType))
|
|
4319
|
+
.filter(Boolean) as string[];
|
|
4320
|
+
status.activeSessions = sessionIds;
|
|
4321
|
+
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
4322
|
+
if (providerTypes.length > 0) {
|
|
4323
|
+
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
2936
4324
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
status.
|
|
2945
|
-
|
|
4325
|
+
}
|
|
4326
|
+
if (workspace) {
|
|
4327
|
+
if (!fs.existsSync(workspace)) {
|
|
4328
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
4329
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
4330
|
+
let remoteProbeApplied = false;
|
|
4331
|
+
if (inlineTransitGit) {
|
|
4332
|
+
status.git = inlineTransitGit;
|
|
4333
|
+
status.health = inlineTransitGit.isGitRepo
|
|
4334
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
4335
|
+
: 'degraded';
|
|
4336
|
+
const connection = readObjectRecord(status.connection);
|
|
4337
|
+
const connectionState = readStringValue(connection.state);
|
|
4338
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4339
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4340
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4341
|
+
}
|
|
4342
|
+
remoteProbeApplied = true;
|
|
4343
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
4344
|
+
try {
|
|
4345
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
4346
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4347
|
+
daemonId,
|
|
4348
|
+
workspace,
|
|
4349
|
+
timeoutMs: 8000,
|
|
4350
|
+
});
|
|
4351
|
+
if (remoteGit) {
|
|
4352
|
+
status.git = remoteGit;
|
|
4353
|
+
status.health = remoteGit.isGitRepo
|
|
4354
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
4355
|
+
: 'degraded';
|
|
4356
|
+
const connection = readObjectRecord(status.connection);
|
|
4357
|
+
const connectionState = readStringValue(connection.state);
|
|
4358
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4359
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4360
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4361
|
+
}
|
|
4362
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
4363
|
+
remoteProbeApplied = true;
|
|
4364
|
+
}
|
|
4365
|
+
} catch {
|
|
4366
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
4367
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
4368
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
4369
|
+
status.connection = refreshedConnection;
|
|
4370
|
+
try {
|
|
4371
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
4372
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4373
|
+
daemonId,
|
|
4374
|
+
workspace,
|
|
4375
|
+
timeoutMs: 12000,
|
|
4376
|
+
});
|
|
4377
|
+
if (remoteGit) {
|
|
4378
|
+
status.git = remoteGit;
|
|
4379
|
+
status.health = remoteGit.isGitRepo
|
|
4380
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
4381
|
+
: 'degraded';
|
|
4382
|
+
const connection = readObjectRecord(status.connection);
|
|
4383
|
+
const connectionState = readStringValue(connection.state);
|
|
4384
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4385
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4386
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4387
|
+
}
|
|
4388
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
4389
|
+
remoteProbeApplied = true;
|
|
4390
|
+
}
|
|
4391
|
+
} catch {
|
|
4392
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
2946
4396
|
}
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
4397
|
+
if (!remoteProbeApplied) {
|
|
4398
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
4399
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
4400
|
+
&& !isSelfNode
|
|
4401
|
+
&& !!daemonId
|
|
4402
|
+
&& (
|
|
4403
|
+
readStringValue(status.machineStatus) === 'online'
|
|
4404
|
+
|| readStringValue(status.health) === 'online'
|
|
4405
|
+
|| connectionState === 'connecting'
|
|
4406
|
+
|| connectionState === 'connected'
|
|
4407
|
+
|| connectionState === 'unknown'
|
|
4408
|
+
);
|
|
4409
|
+
if (pendingPeerGitProbe) {
|
|
4410
|
+
status.gitProbePending = true;
|
|
4411
|
+
status.health = 'unknown';
|
|
4412
|
+
}
|
|
4413
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
4414
|
+
status,
|
|
4415
|
+
node,
|
|
4416
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
4417
|
+
)) {
|
|
4418
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
4419
|
+
nodeStatuses.push(status);
|
|
4420
|
+
continue;
|
|
4421
|
+
}
|
|
4422
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
4423
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
4424
|
+
nodeStatuses.push(status);
|
|
4425
|
+
continue;
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
} else {
|
|
4429
|
+
try {
|
|
4430
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
4431
|
+
status.git = gitStatus;
|
|
4432
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
4433
|
+
if (gitStatus.isGitRepo) {
|
|
4434
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
4435
|
+
} else {
|
|
4436
|
+
status.health = 'degraded';
|
|
4437
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
4438
|
+
}
|
|
4439
|
+
} catch {
|
|
4440
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
4441
|
+
status.health = 'degraded';
|
|
4442
|
+
}
|
|
2950
4443
|
}
|
|
2951
4444
|
}
|
|
2952
4445
|
} else {
|
|
2953
4446
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
2954
4447
|
}
|
|
4448
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
2955
4449
|
nodeStatuses.push(status);
|
|
2956
4450
|
}
|
|
2957
4451
|
|
|
2958
|
-
|
|
4452
|
+
const statusResult = {
|
|
2959
4453
|
success: true,
|
|
2960
4454
|
meshId: mesh.id,
|
|
2961
4455
|
meshName: mesh.name,
|
|
2962
4456
|
repoIdentity: mesh.repoIdentity,
|
|
2963
4457
|
defaultBranch: mesh.defaultBranch,
|
|
4458
|
+
refreshedAt,
|
|
4459
|
+
meshHost,
|
|
4460
|
+
sourceOfTruth: {
|
|
4461
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
4462
|
+
? 'coordinator_inline_mesh_cache'
|
|
4463
|
+
: meshRecord?.source === 'local_config'
|
|
4464
|
+
? 'local_mesh_config'
|
|
4465
|
+
: 'inline_bootstrap_snapshot',
|
|
4466
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
4467
|
+
meshHost: {
|
|
4468
|
+
owner: 'mesh_host_daemon',
|
|
4469
|
+
localRole: meshHost.role,
|
|
4470
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
4471
|
+
hostNodeId: meshHost.hostNodeId,
|
|
4472
|
+
hostAddress: meshHost.hostAddress,
|
|
4473
|
+
},
|
|
4474
|
+
...(requireDirectPeerTruth ? {
|
|
4475
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
4476
|
+
directPeerTruth: {
|
|
4477
|
+
required: true,
|
|
4478
|
+
satisfied: directTruthSatisfied,
|
|
4479
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
4480
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
4481
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
4482
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
4483
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
4484
|
+
},
|
|
4485
|
+
} : {}),
|
|
4486
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
4487
|
+
},
|
|
2964
4488
|
nodes: nodeStatuses,
|
|
2965
4489
|
queue: { tasks: queue, summary: queueSummary },
|
|
2966
4490
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
2967
4491
|
};
|
|
4492
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, statusResult, refreshReason);
|
|
4493
|
+
logRepoMeshStatusDebug('return_live', {
|
|
4494
|
+
meshId,
|
|
4495
|
+
command: 'mesh_status',
|
|
4496
|
+
refreshRequested,
|
|
4497
|
+
refreshReason,
|
|
4498
|
+
meshSource: meshRecord.source,
|
|
4499
|
+
directTruth,
|
|
4500
|
+
summary: summarizeRepoMeshStatusDebug(rememberedStatus),
|
|
4501
|
+
});
|
|
4502
|
+
return rememberedStatus;
|
|
2968
4503
|
} catch (e: any) {
|
|
2969
4504
|
return { success: false, error: e.message };
|
|
2970
4505
|
}
|