@adhdev/daemon-core 0.9.82-rc.6 → 0.9.82-rc.61
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 +24 -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 +13 -6
- package/dist/index.js +3522 -593
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3496 -587
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +48 -0
- package/dist/mesh/mesh-events.d.ts +17 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +23 -5
- package/dist/mesh/refine-config.d.ts +119 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.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/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/commands/router.ts +2178 -419
- 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 +39 -5
- package/src/mesh/coordinator-prompt.ts +4 -2
- package/src/mesh/mesh-active-work.ts +205 -0
- package/src/mesh/mesh-events.ts +210 -38
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +137 -0
- package/src/mesh/mesh-work-queue.ts +202 -122
- package/src/mesh/refine-config.ts +306 -0
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/providers/cli-provider-instance.ts +66 -1
- package/src/repo-mesh-types.ts +174 -0
package/src/commands/router.ts
CHANGED
|
@@ -38,7 +38,18 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
|
|
|
38
38
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
|
-
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
|
|
41
|
+
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
42
|
+
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
43
|
+
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
44
|
+
import {
|
|
45
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
46
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
47
|
+
loadMeshRefineConfig,
|
|
48
|
+
resolveMeshRefineValidationPlan,
|
|
49
|
+
suggestMeshRefineConfig,
|
|
50
|
+
validateMeshRefineConfig,
|
|
51
|
+
type MeshRefineValidationCommandPlan,
|
|
52
|
+
} from '../mesh/refine-config.js';
|
|
42
53
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
43
54
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
44
55
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
@@ -114,14 +125,93 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
114
125
|
return undefined;
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
function
|
|
128
|
+
function summarizeRepoMeshDebugGit(git: unknown): Record<string, unknown> | null {
|
|
129
|
+
const record = readObjectRecord(git);
|
|
130
|
+
if (!Object.keys(record).length) return null;
|
|
131
|
+
const submodules = Array.isArray(record.submodules)
|
|
132
|
+
? record.submodules.map((entry: any) => ({
|
|
133
|
+
path: readStringValue(entry?.path) ?? null,
|
|
134
|
+
commit: readStringValue(entry?.commit)?.slice(0, 12) ?? null,
|
|
135
|
+
dirty: readBooleanValue(entry?.dirty) ?? false,
|
|
136
|
+
outOfSync: readBooleanValue(entry?.outOfSync, entry?.out_of_sync) ?? false,
|
|
137
|
+
}))
|
|
138
|
+
: [];
|
|
139
|
+
return {
|
|
140
|
+
isGitRepo: readBooleanValue(record.isGitRepo),
|
|
141
|
+
workspace: readStringValue(record.workspace) ?? null,
|
|
142
|
+
repoRoot: readStringValue(record.repoRoot, record.repo_root) ?? null,
|
|
143
|
+
branch: readStringValue(record.branch) ?? null,
|
|
144
|
+
upstream: readStringValue(record.upstream) ?? null,
|
|
145
|
+
upstreamStatus: readStringValue(record.upstreamStatus, record.upstream_status) ?? null,
|
|
146
|
+
headCommit: readStringValue(record.headCommit, record.head_commit)?.slice(0, 12) ?? null,
|
|
147
|
+
ahead: readNumberValue(record.ahead) ?? null,
|
|
148
|
+
behind: readNumberValue(record.behind) ?? null,
|
|
149
|
+
dirtyCounts: {
|
|
150
|
+
staged: readNumberValue(record.staged) ?? 0,
|
|
151
|
+
modified: readNumberValue(record.modified) ?? 0,
|
|
152
|
+
untracked: readNumberValue(record.untracked) ?? 0,
|
|
153
|
+
deleted: readNumberValue(record.deleted) ?? 0,
|
|
154
|
+
renamed: readNumberValue(record.renamed) ?? 0,
|
|
155
|
+
},
|
|
156
|
+
lastCheckedAt: readNumberValue(record.lastCheckedAt, record.last_checked_at) ?? null,
|
|
157
|
+
submoduleCount: submodules.length,
|
|
158
|
+
submodules,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
163
|
+
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
164
|
+
return {
|
|
165
|
+
success: status?.success,
|
|
166
|
+
meshId: readStringValue(status?.meshId, status?.mesh_id) ?? null,
|
|
167
|
+
refreshedAt: readStringValue(status?.refreshedAt, status?.refreshed_at) ?? null,
|
|
168
|
+
sourceOfTruth: status?.sourceOfTruth ?? null,
|
|
169
|
+
nodeCount: nodes.length,
|
|
170
|
+
nodes: nodes.map((node: any) => ({
|
|
171
|
+
nodeId: readStringValue(node?.nodeId, node?.id) ?? null,
|
|
172
|
+
daemonId: readStringValue(node?.daemonId, node?.daemon_id) ?? null,
|
|
173
|
+
workspace: readStringValue(node?.workspace, node?.git?.workspace) ?? null,
|
|
174
|
+
health: readStringValue(node?.health) ?? null,
|
|
175
|
+
machineStatus: readStringValue(node?.machineStatus, node?.machine_status) ?? null,
|
|
176
|
+
connection: node?.connection && typeof node.connection === 'object' ? {
|
|
177
|
+
state: readStringValue(node.connection.state) ?? null,
|
|
178
|
+
transport: readStringValue(node.connection.transport) ?? null,
|
|
179
|
+
source: readStringValue(node.connection.source) ?? null,
|
|
180
|
+
reported: readBooleanValue(node.connection.reported) ?? null,
|
|
181
|
+
} : null,
|
|
182
|
+
gitProbePending: node?.gitProbePending === true,
|
|
183
|
+
launchReady: node?.launchReady === true,
|
|
184
|
+
git: summarizeRepoMeshDebugGit(node?.git),
|
|
185
|
+
})),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
190
|
+
try {
|
|
191
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
192
|
+
} catch {
|
|
193
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${event}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
|
|
198
|
+
const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : '';
|
|
199
|
+
const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : '';
|
|
200
|
+
if (!normalizedPath) return undefined;
|
|
201
|
+
if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath;
|
|
202
|
+
if (!normalizedRoot) return undefined;
|
|
203
|
+
return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
|
|
118
207
|
if (!Array.isArray(value)) return undefined;
|
|
119
208
|
const submodules = value
|
|
120
209
|
.map(entry => {
|
|
121
210
|
const submodule = readObjectRecord(entry);
|
|
122
211
|
const path = readStringValue(submodule.path);
|
|
123
212
|
const commit = readStringValue(submodule.commit);
|
|
124
|
-
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
213
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
214
|
+
?? joinRepoPath(parentRepoRoot, path);
|
|
125
215
|
if (!path || !commit || !repoPath) return null;
|
|
126
216
|
return {
|
|
127
217
|
path,
|
|
@@ -137,60 +227,11 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
|
137
227
|
return submodules.length > 0 ? submodules : undefined;
|
|
138
228
|
}
|
|
139
229
|
|
|
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
|
-
: {};
|
|
230
|
+
function normalizeInlineMeshGitStatus(
|
|
231
|
+
status: Record<string, unknown>,
|
|
232
|
+
node: any,
|
|
233
|
+
options?: { lastCheckedAt?: number },
|
|
234
|
+
): Record<string, unknown> | undefined {
|
|
194
235
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
236
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
237
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -198,10 +239,11 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
198
239
|
: [];
|
|
199
240
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
241
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
-
const
|
|
242
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
243
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
202
244
|
return {
|
|
203
245
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
-
repoRoot:
|
|
246
|
+
repoRoot: repoRoot ?? null,
|
|
205
247
|
isGitRepo,
|
|
206
248
|
branch: readStringValue(status.branch) ?? null,
|
|
207
249
|
headCommit: readStringValue(status.headCommit) ?? null,
|
|
@@ -217,31 +259,589 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
217
259
|
hasConflicts,
|
|
218
260
|
conflictFiles,
|
|
219
261
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
-
lastCheckedAt: Date.now(),
|
|
262
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
221
263
|
...(submodules ? { submodules } : {}),
|
|
222
264
|
};
|
|
223
265
|
}
|
|
224
266
|
|
|
225
|
-
function
|
|
267
|
+
function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
|
|
268
|
+
if (!git) return Number.NEGATIVE_INFINITY;
|
|
269
|
+
let score = 0;
|
|
270
|
+
if (readBooleanValue(git.isGitRepo) === true) score += 50;
|
|
271
|
+
if (readBooleanValue(git.isGitRepo) === false) score -= 10;
|
|
272
|
+
if (readStringValue(git.branch)) score += 20;
|
|
273
|
+
if (readStringValue(git.headCommit)) score += 20;
|
|
274
|
+
if (readStringValue(git.upstream)) score += 10;
|
|
275
|
+
if (readStringValue(git.upstreamStatus)) score += 5;
|
|
276
|
+
if (readNumberValue(git.ahead) !== undefined) score += 2;
|
|
277
|
+
if (readNumberValue(git.behind) !== undefined) score += 2;
|
|
278
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
|
|
279
|
+
if (readStringValue(git.error)) score -= 20;
|
|
280
|
+
return score;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
284
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
285
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
286
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
287
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
288
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
289
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
290
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
291
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
292
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
293
|
+
const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
|
|
294
|
+
let best: { git: Record<string, unknown>; score: number } | null = null;
|
|
295
|
+
for (const status of candidates) {
|
|
296
|
+
const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
297
|
+
if (!normalized) continue;
|
|
298
|
+
const score = scoreInlineMeshGitStatus(normalized);
|
|
299
|
+
if (!best || score > best.score) best = { git: normalized, score };
|
|
300
|
+
}
|
|
301
|
+
return best?.git;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
|
|
305
|
+
if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
|
|
306
|
+
return snapshot.nodes.some((node: any) => {
|
|
307
|
+
if (node?.gitProbePending !== true) return false;
|
|
308
|
+
const git = readObjectRecord(node?.git);
|
|
309
|
+
return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
314
|
+
const source = readStringValue(connection.source);
|
|
315
|
+
const transport = readStringValue(connection.transport);
|
|
316
|
+
return {
|
|
317
|
+
...connection,
|
|
318
|
+
perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
|
|
319
|
+
source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
|
|
320
|
+
state: 'connected',
|
|
321
|
+
transport: transport && transport !== 'unknown' ? transport : 'direct',
|
|
322
|
+
reported: true,
|
|
323
|
+
reason: 'Live peer git snapshot reported by the selected coordinator.',
|
|
324
|
+
lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function recordInlineMeshDirectGitTruth(
|
|
329
|
+
node: any,
|
|
330
|
+
git: Record<string, unknown>,
|
|
331
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
332
|
+
): void {
|
|
333
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
334
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
335
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
336
|
+
const nextGit: Record<string, unknown> = {
|
|
337
|
+
...git,
|
|
338
|
+
lastCheckedAt: checkedAt,
|
|
339
|
+
};
|
|
340
|
+
node.lastGit = {
|
|
341
|
+
source,
|
|
342
|
+
checkedAt,
|
|
343
|
+
status: nextGit,
|
|
344
|
+
};
|
|
345
|
+
node.last_git = node.lastGit;
|
|
346
|
+
node.machineStatus = 'online';
|
|
347
|
+
node.updatedAt = updatedAt;
|
|
348
|
+
node.lastSeenAt = updatedAt;
|
|
349
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
350
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
354
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
355
|
+
if (liveGit) return liveGit;
|
|
356
|
+
|
|
357
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
358
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
359
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
360
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
364
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
365
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
366
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
367
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
368
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
369
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
370
|
+
const branch = readStringValue(cachedGit.branch);
|
|
371
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
372
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
376
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
377
|
+
const {
|
|
378
|
+
cachedStatus,
|
|
379
|
+
lastGit: _lastGit,
|
|
380
|
+
last_git: _lastGitLegacy,
|
|
381
|
+
lastProbe: _lastProbe,
|
|
382
|
+
last_probe: _lastProbeLegacy,
|
|
383
|
+
error: _error,
|
|
384
|
+
health: _health,
|
|
385
|
+
machineStatus: _machineStatus,
|
|
386
|
+
lastSeenAt: _lastSeenAt,
|
|
387
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
388
|
+
updatedAt: _updatedAt,
|
|
389
|
+
updated_at: _updatedAtLegacy,
|
|
390
|
+
activeSession: _activeSession,
|
|
391
|
+
active_session: _activeSessionLegacy,
|
|
392
|
+
activeSessionId: _activeSessionId,
|
|
393
|
+
active_session_id: _activeSessionIdLegacy,
|
|
394
|
+
sessionId: _sessionId,
|
|
395
|
+
session_id: _sessionIdLegacy,
|
|
396
|
+
providerType: _providerType,
|
|
397
|
+
provider_type: _providerTypeLegacy,
|
|
398
|
+
...rest
|
|
399
|
+
} = node as Record<string, unknown>;
|
|
400
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
401
|
+
return { ...rest, cachedStatus };
|
|
402
|
+
}
|
|
403
|
+
return rest;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
407
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
408
|
+
return 'cachedStatus' in node
|
|
409
|
+
|| 'lastGit' in node
|
|
410
|
+
|| 'last_git' in node
|
|
411
|
+
|| 'lastProbe' in node
|
|
412
|
+
|| 'last_probe' in node
|
|
413
|
+
|| 'error' in node
|
|
414
|
+
|| 'health' in node
|
|
415
|
+
|| 'machineStatus' in node
|
|
416
|
+
|| 'lastSeenAt' in node
|
|
417
|
+
|| 'last_seen_at' in node
|
|
418
|
+
|| 'updatedAt' in node
|
|
419
|
+
|| 'updated_at' in node
|
|
420
|
+
|| 'activeSession' in node
|
|
421
|
+
|| 'active_session' in node
|
|
422
|
+
|| 'activeSessionId' in node
|
|
423
|
+
|| 'active_session_id' in node
|
|
424
|
+
|| 'sessionId' in node
|
|
425
|
+
|| 'session_id' in node
|
|
426
|
+
|| 'providerType' in node
|
|
427
|
+
|| 'provider_type' in node;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
431
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
432
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
433
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function readInlineMeshNodeId(node: any): string {
|
|
437
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
441
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
442
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
443
|
+
let changed = false;
|
|
444
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
445
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
446
|
+
changed = true;
|
|
447
|
+
return stripInlineMeshTransientNodeState(node);
|
|
448
|
+
});
|
|
449
|
+
if (!changed) return inlineMesh;
|
|
450
|
+
return {
|
|
451
|
+
...inlineMesh,
|
|
452
|
+
nodes,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
457
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
458
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
459
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
460
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
461
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
462
|
+
|
|
463
|
+
const cachedUpdatedAt = Date.parse(readStringValue(cached.updatedAt, cached.updated_at) || '');
|
|
464
|
+
const incomingUpdatedAt = Date.parse(readStringValue(incoming.updatedAt, incoming.updated_at) || '');
|
|
465
|
+
const preserveCachedMembership = Number.isFinite(cachedUpdatedAt)
|
|
466
|
+
&& (!Number.isFinite(incomingUpdatedAt) || cachedUpdatedAt > incomingUpdatedAt);
|
|
467
|
+
|
|
468
|
+
const cachedById = new Map<string, any>();
|
|
469
|
+
for (const node of cachedNodes) {
|
|
470
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
471
|
+
if (nodeId) cachedById.set(nodeId, node);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const nodes = incomingNodes.map((incomingNode: any) => {
|
|
475
|
+
const nodeId = readInlineMeshNodeId(incomingNode);
|
|
476
|
+
const cachedNode = nodeId ? cachedById.get(nodeId) : undefined;
|
|
477
|
+
if (!cachedNode && preserveCachedMembership) return null;
|
|
478
|
+
if (!cachedNode) return incomingNode;
|
|
479
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
480
|
+
return { ...cachedNode, ...incomingNode };
|
|
481
|
+
}
|
|
482
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
483
|
+
}).filter(Boolean);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
...cached,
|
|
487
|
+
...incoming,
|
|
488
|
+
nodes,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
493
|
+
if (!git) return false;
|
|
494
|
+
return Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
498
|
+
const submodules = Array.isArray(git?.submodules) ? git.submodules : [];
|
|
499
|
+
let dirty = false;
|
|
500
|
+
let outOfSync = false;
|
|
501
|
+
for (const entry of submodules) {
|
|
502
|
+
const submodule = readObjectRecord(entry);
|
|
503
|
+
if (readBooleanValue(submodule.dirty) === true) dirty = true;
|
|
504
|
+
if (readBooleanValue(submodule.outOfSync) === true || !!readStringValue(submodule.error)) outOfSync = true;
|
|
505
|
+
}
|
|
506
|
+
return { dirty, outOfSync };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
510
|
+
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
511
|
+
const branch = readStringValue(git.branch);
|
|
512
|
+
if (!branch) return 'degraded';
|
|
513
|
+
const submoduleDrift = getGitSubmoduleDriftState(git);
|
|
514
|
+
if (submoduleDrift.outOfSync) return 'degraded';
|
|
515
|
+
if (submoduleDrift.dirty || hasGitWorktreeChanges(git)) return 'dirty';
|
|
516
|
+
return 'online';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
520
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
521
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
522
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
523
|
+
? activeSession
|
|
524
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
525
|
+
const sessionId = readStringValue(fallbackSession.id, fallbackSession.sessionId, fallbackSession.session_id, node?.activeSessionId, node?.active_session_id, node?.sessionId, node?.session_id);
|
|
526
|
+
return sessionId ? [sessionId] : [];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
226
530
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
531
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
532
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
533
|
+
? activeSession
|
|
534
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
535
|
+
const sessionId = readStringValue(
|
|
536
|
+
fallbackSession.id,
|
|
537
|
+
fallbackSession.sessionId,
|
|
538
|
+
fallbackSession.session_id,
|
|
539
|
+
node?.activeSessionId,
|
|
540
|
+
node?.active_session_id,
|
|
541
|
+
node?.sessionId,
|
|
542
|
+
node?.session_id,
|
|
543
|
+
);
|
|
544
|
+
if (!sessionId) return [];
|
|
545
|
+
return [{
|
|
546
|
+
sessionId,
|
|
547
|
+
providerType: readStringValue(
|
|
548
|
+
fallbackSession.providerType,
|
|
549
|
+
fallbackSession.provider_type,
|
|
550
|
+
fallbackSession.cliType,
|
|
551
|
+
fallbackSession.cli_type,
|
|
552
|
+
fallbackSession.provider,
|
|
553
|
+
node?.providerType,
|
|
554
|
+
node?.provider_type,
|
|
555
|
+
),
|
|
556
|
+
state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
|
|
557
|
+
lifecycle: readStringValue(fallbackSession.lifecycle),
|
|
558
|
+
title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
|
|
559
|
+
workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
|
|
560
|
+
lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
|
|
561
|
+
recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
|
|
562
|
+
isCached: true,
|
|
563
|
+
}];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function readLiveMeshSessionState(record: any): string | undefined {
|
|
567
|
+
return readStringValue(
|
|
568
|
+
record?.meta?.sessionStatus,
|
|
569
|
+
record?.meta?.status,
|
|
570
|
+
record?.meta?.providerStatus,
|
|
571
|
+
record?.status,
|
|
572
|
+
record?.state,
|
|
573
|
+
record?.lifecycle,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function toIsoTimestamp(value: unknown): string | null {
|
|
578
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
579
|
+
const stringValue = readStringValue(value);
|
|
580
|
+
return stringValue || null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
584
|
+
const connection = readObjectRecord(status.connection);
|
|
585
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
586
|
+
const git = readObjectRecord(status.git);
|
|
587
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
588
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
589
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
590
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function finalizeMeshNodeStatus(args: {
|
|
595
|
+
status: Record<string, unknown>;
|
|
596
|
+
node: any;
|
|
597
|
+
daemonId?: string;
|
|
598
|
+
isSelfNode: boolean;
|
|
599
|
+
}): void {
|
|
600
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
601
|
+
if (!readStringValue(status.machineStatus)) {
|
|
602
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
603
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
604
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
605
|
+
}
|
|
606
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
607
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
608
|
+
status.launchReady = !!daemonId && (
|
|
609
|
+
readStringValue(status.machineStatus) === 'online'
|
|
610
|
+
|| connectionState === 'connected'
|
|
611
|
+
|| isSelfNode
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
616
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
617
|
+
daemonId: string;
|
|
618
|
+
workspace: string;
|
|
619
|
+
timeoutMs: number;
|
|
620
|
+
}): Promise<Record<string, unknown> | null> {
|
|
621
|
+
if (!args.dispatchMeshCommand) return null;
|
|
622
|
+
const remoteResult = await Promise.race([
|
|
623
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace }),
|
|
624
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
625
|
+
]) as any;
|
|
626
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
627
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
628
|
+
? remoteGit as Record<string, unknown>
|
|
629
|
+
: null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
633
|
+
mesh: any;
|
|
634
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
635
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
636
|
+
statusInstanceId?: string;
|
|
637
|
+
localMachineId?: string;
|
|
638
|
+
}): Promise<{
|
|
639
|
+
directEvidenceCount: number;
|
|
640
|
+
localConfirmedCount: number;
|
|
641
|
+
peerAttemptedCount: number;
|
|
642
|
+
peerConfirmedCount: number;
|
|
643
|
+
unavailableNodeIds: string[];
|
|
644
|
+
}> {
|
|
645
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
646
|
+
if (!nodes.length) {
|
|
647
|
+
return {
|
|
648
|
+
directEvidenceCount: 0,
|
|
649
|
+
localConfirmedCount: 0,
|
|
650
|
+
peerAttemptedCount: 0,
|
|
651
|
+
peerConfirmedCount: 0,
|
|
652
|
+
unavailableNodeIds: [],
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
657
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
658
|
+
nodes[0]?.id,
|
|
659
|
+
nodes[0]?.nodeId,
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
let localConfirmedCount = 0;
|
|
663
|
+
let peerAttemptedCount = 0;
|
|
664
|
+
let peerConfirmedCount = 0;
|
|
665
|
+
const unavailableNodeIds: string[] = [];
|
|
666
|
+
|
|
667
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
668
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
669
|
+
const workspace = readStringValue(node?.workspace);
|
|
670
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
671
|
+
const isSelfNode = Boolean(
|
|
672
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
673
|
+
) || Boolean(
|
|
674
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
675
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
676
|
+
|
|
677
|
+
if (!workspace) {
|
|
678
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (fs.existsSync(workspace)) {
|
|
683
|
+
try {
|
|
684
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
685
|
+
if (localGit?.isGitRepo) {
|
|
686
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
687
|
+
localConfirmedCount += 1;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
// Fall through to remote classification.
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
696
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
peerAttemptedCount += 1;
|
|
701
|
+
try {
|
|
702
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
703
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
704
|
+
daemonId,
|
|
705
|
+
workspace,
|
|
706
|
+
timeoutMs: 8_000,
|
|
707
|
+
});
|
|
708
|
+
if (remoteGit) {
|
|
709
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
710
|
+
peerConfirmedCount += 1;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
} catch {
|
|
714
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
unavailableNodeIds.push(nodeId);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
722
|
+
localConfirmedCount,
|
|
723
|
+
peerAttemptedCount,
|
|
724
|
+
peerConfirmedCount,
|
|
725
|
+
unavailableNodeIds,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
730
|
+
return {
|
|
731
|
+
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
732
|
+
providerType: readStringValue(record?.providerType),
|
|
733
|
+
state: readLiveMeshSessionState(record),
|
|
734
|
+
lifecycle: readStringValue(record?.lifecycle),
|
|
735
|
+
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
736
|
+
recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
|
|
737
|
+
workspace: readStringValue(record?.workspace) ?? null,
|
|
738
|
+
title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
|
|
739
|
+
lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
|
|
740
|
+
isCached: false,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
745
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
746
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
747
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
748
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
752
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
753
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
754
|
+
|
|
755
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
756
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
757
|
+
|
|
758
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
762
|
+
meshId: string;
|
|
763
|
+
nodeId: string;
|
|
764
|
+
liveSessionRecords: any[];
|
|
765
|
+
allowCoordinatorSession?: boolean;
|
|
766
|
+
}): string {
|
|
767
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
768
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
769
|
+
&& readStringValue(record?.workspace)
|
|
770
|
+
));
|
|
771
|
+
if (directNodeWorkspace) {
|
|
772
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (args.allowCoordinatorSession) {
|
|
776
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
777
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
778
|
+
&& readStringValue(record?.workspace)
|
|
779
|
+
));
|
|
780
|
+
if (coordinatorWorkspace) {
|
|
781
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return '';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function collectLiveMeshSessionRecords(args: {
|
|
789
|
+
meshId: string;
|
|
790
|
+
node: any;
|
|
791
|
+
nodeId: string;
|
|
792
|
+
liveSessionRecords: any[];
|
|
793
|
+
allowCoordinatorSession?: boolean;
|
|
794
|
+
}): any[] {
|
|
795
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
796
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
797
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
|
|
798
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
if (args.allowCoordinatorSession) {
|
|
802
|
+
for (const record of args.liveSessionRecords) {
|
|
803
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
804
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
805
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
806
|
+
matches.push(record);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return matches;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function applyCachedInlineMeshNodeStatus(
|
|
814
|
+
status: Record<string, unknown>,
|
|
815
|
+
node: any,
|
|
816
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
817
|
+
): boolean {
|
|
818
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
819
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
820
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
821
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
822
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
230
823
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
231
|
-
|
|
232
|
-
|
|
824
|
+
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
825
|
+
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
826
|
+
const activeSessions = readCachedInlineMeshActiveSessions(node);
|
|
827
|
+
const activeSessionDetails = readCachedInlineMeshActiveSessionDetails(node);
|
|
828
|
+
if (!git && !error && !health && !machineStatus && !lastSeenAt && !updatedAt && activeSessions.length === 0) return false;
|
|
233
829
|
if (git) status.git = git;
|
|
234
830
|
if (error) status.error = error;
|
|
831
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
832
|
+
if (lastSeenAt) status.lastSeenAt = lastSeenAt;
|
|
833
|
+
if (updatedAt) status.updatedAt = updatedAt;
|
|
834
|
+
if (activeSessions.length > 0) status.activeSessions = activeSessions;
|
|
835
|
+
if (activeSessionDetails.length > 0) status.activeSessionDetails = activeSessionDetails;
|
|
235
836
|
if (health) {
|
|
236
837
|
status.health = health;
|
|
237
838
|
return true;
|
|
238
839
|
}
|
|
239
840
|
if (git) {
|
|
240
|
-
|
|
241
|
-
status.health = git.isGitRepo === false ? 'degraded' : dirty ? 'dirty' : 'online';
|
|
841
|
+
status.health = deriveMeshNodeHealthFromGit(git);
|
|
242
842
|
return true;
|
|
243
843
|
}
|
|
244
|
-
return
|
|
844
|
+
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
245
845
|
}
|
|
246
846
|
|
|
247
847
|
async function resolveProviderTypeFromPriority(args: {
|
|
@@ -276,13 +876,7 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
276
876
|
}
|
|
277
877
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
278
878
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
279
|
-
type MeshRefineValidationCommand =
|
|
280
|
-
command: string;
|
|
281
|
-
args: string[];
|
|
282
|
-
displayCommand: string;
|
|
283
|
-
category: string;
|
|
284
|
-
source: string;
|
|
285
|
-
};
|
|
879
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
286
880
|
|
|
287
881
|
type MeshRefineValidationSummary = {
|
|
288
882
|
status: MeshRefineValidationStatus;
|
|
@@ -292,13 +886,65 @@ type MeshRefineValidationSummary = {
|
|
|
292
886
|
skippedReason?: string;
|
|
293
887
|
timeoutMs: number;
|
|
294
888
|
outputLimitBytes: number;
|
|
889
|
+
configSource?: string;
|
|
890
|
+
configSourceType?: string;
|
|
891
|
+
suggestions?: unknown[];
|
|
892
|
+
suggestedConfig?: unknown;
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
896
|
+
|
|
897
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
898
|
+
status: MeshRefineStageStatus;
|
|
899
|
+
equivalent: boolean;
|
|
900
|
+
baseHead: string;
|
|
901
|
+
branchHead: string;
|
|
902
|
+
mergeBase?: string;
|
|
903
|
+
mergedTree?: string;
|
|
904
|
+
expectedPatchId?: string;
|
|
905
|
+
actualPatchId?: string;
|
|
906
|
+
durationMs: number;
|
|
907
|
+
error?: string;
|
|
908
|
+
stdout?: string;
|
|
909
|
+
stderr?: string;
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
913
|
+
|
|
914
|
+
type MeshRefineJobHandle = {
|
|
915
|
+
success: true;
|
|
916
|
+
async: true;
|
|
917
|
+
status: MeshRefineAsyncJobStatus;
|
|
918
|
+
jobId: string;
|
|
919
|
+
interactionId: string;
|
|
920
|
+
meshId: string;
|
|
921
|
+
nodeId: string;
|
|
922
|
+
targetNodeId: string;
|
|
923
|
+
targetDaemonId?: string;
|
|
924
|
+
workspace?: string;
|
|
925
|
+
startedAt: string;
|
|
926
|
+
completedAt?: string;
|
|
927
|
+
duplicate?: boolean;
|
|
928
|
+
retryOfJobId?: string;
|
|
929
|
+
eventDelivery: {
|
|
930
|
+
pendingEvents: true;
|
|
931
|
+
ledger: true;
|
|
932
|
+
};
|
|
933
|
+
evidence: {
|
|
934
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
935
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
936
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
937
|
+
};
|
|
295
938
|
};
|
|
296
939
|
|
|
940
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
941
|
+
|
|
297
942
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
298
943
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
299
944
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
300
945
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
301
946
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
947
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
302
948
|
|
|
303
949
|
function truncateValidationOutput(value: unknown): string {
|
|
304
950
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -306,171 +952,114 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
306
952
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
307
953
|
}
|
|
308
954
|
|
|
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);
|
|
955
|
+
function recordMeshRefineStage(
|
|
956
|
+
stages: Array<Record<string, unknown>>,
|
|
957
|
+
stage: string,
|
|
958
|
+
status: MeshRefineStageStatus,
|
|
959
|
+
startedAt: number,
|
|
960
|
+
details?: Record<string, unknown>,
|
|
961
|
+
): void {
|
|
962
|
+
stages.push({
|
|
963
|
+
stage,
|
|
964
|
+
status,
|
|
965
|
+
durationMs: Date.now() - startedAt,
|
|
966
|
+
...(details || {}),
|
|
407
967
|
});
|
|
408
968
|
}
|
|
409
969
|
|
|
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);
|
|
970
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
971
|
+
const { execFileSync } = await import('node:child_process');
|
|
972
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
973
|
+
cwd,
|
|
974
|
+
encoding: 'utf8',
|
|
975
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
976
|
+
});
|
|
977
|
+
if (!diff.trim()) return '';
|
|
978
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
979
|
+
cwd,
|
|
980
|
+
input: diff,
|
|
981
|
+
encoding: 'utf8',
|
|
982
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
983
|
+
}).trim();
|
|
984
|
+
return patchId.split(/\s+/)[0] || '';
|
|
426
985
|
}
|
|
427
986
|
|
|
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;
|
|
987
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
988
|
+
repoRoot: string,
|
|
989
|
+
baseHead: string,
|
|
990
|
+
branchHead: string,
|
|
991
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
992
|
+
const startedAt = Date.now();
|
|
993
|
+
try {
|
|
994
|
+
const { execFileSync } = await import('node:child_process');
|
|
995
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
996
|
+
cwd: repoRoot,
|
|
997
|
+
encoding: 'utf8',
|
|
998
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
999
|
+
});
|
|
1000
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
1001
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
1002
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
1003
|
+
if (!mergeBase || !mergedTree) {
|
|
1004
|
+
return {
|
|
1005
|
+
status: 'failed',
|
|
1006
|
+
equivalent: false,
|
|
1007
|
+
baseHead,
|
|
1008
|
+
branchHead,
|
|
1009
|
+
mergeBase: mergeBase || undefined,
|
|
1010
|
+
mergedTree: mergedTree || undefined,
|
|
1011
|
+
durationMs: Date.now() - startedAt,
|
|
1012
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
1013
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
1014
|
+
};
|
|
461
1015
|
}
|
|
1016
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
1017
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
1018
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
1019
|
+
return {
|
|
1020
|
+
status: equivalent ? 'passed' : 'failed',
|
|
1021
|
+
equivalent,
|
|
1022
|
+
baseHead,
|
|
1023
|
+
branchHead,
|
|
1024
|
+
mergeBase,
|
|
1025
|
+
mergedTree,
|
|
1026
|
+
expectedPatchId,
|
|
1027
|
+
actualPatchId,
|
|
1028
|
+
durationMs: Date.now() - startedAt,
|
|
1029
|
+
};
|
|
1030
|
+
} catch (e: any) {
|
|
1031
|
+
return {
|
|
1032
|
+
status: 'failed',
|
|
1033
|
+
equivalent: false,
|
|
1034
|
+
baseHead,
|
|
1035
|
+
branchHead,
|
|
1036
|
+
durationMs: Date.now() - startedAt,
|
|
1037
|
+
error: e?.message || String(e),
|
|
1038
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1039
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1040
|
+
};
|
|
462
1041
|
}
|
|
1042
|
+
}
|
|
463
1043
|
|
|
1044
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1045
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
464
1046
|
return {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1047
|
+
source: plan.source,
|
|
1048
|
+
sourceType: plan.sourceType,
|
|
1049
|
+
commands: plan.commands.map(command => ({
|
|
1050
|
+
displayCommand: command.displayCommand,
|
|
1051
|
+
category: command.category,
|
|
1052
|
+
source: command.source,
|
|
1053
|
+
cwd: command.cwd,
|
|
1054
|
+
timeoutMs: command.timeoutMs,
|
|
1055
|
+
})),
|
|
1056
|
+
unavailableReason: plan.unavailableReason,
|
|
1057
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1058
|
+
suggestions: plan.suggestions,
|
|
1059
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1060
|
+
note: plan.sourceType === 'unavailable'
|
|
1061
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1062
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
474
1063
|
};
|
|
475
1064
|
}
|
|
476
1065
|
|
|
@@ -478,7 +1067,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
478
1067
|
const { execFile } = await import('node:child_process');
|
|
479
1068
|
const { promisify } = await import('node:util');
|
|
480
1069
|
const execFileAsync = promisify(execFile);
|
|
481
|
-
const selection =
|
|
1070
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
482
1071
|
const summary: MeshRefineValidationSummary = {
|
|
483
1072
|
status: 'skipped',
|
|
484
1073
|
required: true,
|
|
@@ -487,22 +1076,28 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
487
1076
|
skippedReason: undefined,
|
|
488
1077
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
489
1078
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1079
|
+
configSource: selection.source,
|
|
1080
|
+
configSourceType: selection.sourceType,
|
|
1081
|
+
suggestions: selection.suggestions,
|
|
1082
|
+
suggestedConfig: selection.suggestedConfig,
|
|
490
1083
|
};
|
|
491
1084
|
|
|
492
1085
|
if (!selection.commands.length) {
|
|
493
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1086
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
494
1087
|
return summary;
|
|
495
1088
|
}
|
|
496
1089
|
|
|
497
1090
|
for (const candidate of selection.commands) {
|
|
498
1091
|
const startedAt = Date.now();
|
|
1092
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1093
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
499
1094
|
try {
|
|
500
1095
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
501
|
-
cwd
|
|
1096
|
+
cwd,
|
|
502
1097
|
encoding: 'utf8',
|
|
503
|
-
timeout
|
|
1098
|
+
timeout,
|
|
504
1099
|
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
505
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
1100
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
506
1101
|
});
|
|
507
1102
|
summary.commandsRun.push({
|
|
508
1103
|
command: candidate.command,
|
|
@@ -510,6 +1105,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
510
1105
|
displayCommand: candidate.displayCommand,
|
|
511
1106
|
category: candidate.category,
|
|
512
1107
|
source: candidate.source,
|
|
1108
|
+
cwd,
|
|
513
1109
|
passed: true,
|
|
514
1110
|
exitCode: 0,
|
|
515
1111
|
durationMs: Date.now() - startedAt,
|
|
@@ -523,6 +1119,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
523
1119
|
displayCommand: candidate.displayCommand,
|
|
524
1120
|
category: candidate.category,
|
|
525
1121
|
source: candidate.source,
|
|
1122
|
+
cwd,
|
|
526
1123
|
passed: false,
|
|
527
1124
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
528
1125
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
@@ -661,6 +1258,10 @@ export interface CommandRouterDeps {
|
|
|
661
1258
|
statusVersion?: string;
|
|
662
1259
|
/** Session host control plane */
|
|
663
1260
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
1261
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
1262
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1263
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
1264
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
664
1265
|
}
|
|
665
1266
|
|
|
666
1267
|
export interface CommandRouterResult {
|
|
@@ -772,42 +1373,259 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
772
1373
|
};
|
|
773
1374
|
}
|
|
774
1375
|
|
|
1376
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
1377
|
+
const raw = hostAddress.trim();
|
|
1378
|
+
if (!raw) throw new Error('hostAddress required');
|
|
1379
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
1380
|
+
url.pathname = '/api/v1/command';
|
|
1381
|
+
url.search = '';
|
|
1382
|
+
url.hash = '';
|
|
1383
|
+
return url.toString();
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
1387
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
1388
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
1389
|
+
? args.memberNode as Record<string, any>
|
|
1390
|
+
: null;
|
|
1391
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
1392
|
+
? (requestedNodeId
|
|
1393
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
1394
|
+
: mesh.nodes[0])
|
|
1395
|
+
: null;
|
|
1396
|
+
const source = explicit || configured;
|
|
1397
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
1398
|
+
? source.workspace.trim()
|
|
1399
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
1400
|
+
? args.workspace.trim()
|
|
1401
|
+
: process.cwd();
|
|
1402
|
+
if (!workspace) return null;
|
|
1403
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
1404
|
+
? source.id.trim()
|
|
1405
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
1406
|
+
? source.nodeId.trim()
|
|
1407
|
+
: undefined;
|
|
1408
|
+
return {
|
|
1409
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
1410
|
+
workspace,
|
|
1411
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
1412
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
1413
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
1414
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
1415
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
1416
|
+
role: 'member',
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
775
1420
|
export class DaemonCommandRouter {
|
|
776
1421
|
private deps: CommandRouterDeps;
|
|
777
1422
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
778
1423
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
779
1424
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
780
1425
|
private inlineMeshCache = new Map<string, any>();
|
|
1426
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
1427
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any }>();
|
|
1428
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
1429
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
1430
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
1431
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
781
1432
|
|
|
782
1433
|
constructor(deps: CommandRouterDeps) {
|
|
783
1434
|
this.deps = deps;
|
|
784
1435
|
}
|
|
785
1436
|
|
|
1437
|
+
private cloneJsonValue<T>(value: T): T {
|
|
1438
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
1439
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
1443
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
1444
|
+
const inlineNodesById = new Map<string, any>();
|
|
1445
|
+
for (const node of mesh.nodes) {
|
|
1446
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
1447
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
1448
|
+
}
|
|
1449
|
+
if (!inlineNodesById.size) return snapshot;
|
|
1450
|
+
|
|
1451
|
+
let changed = false;
|
|
1452
|
+
const unavailableNodeIds = new Set<string>();
|
|
1453
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
1454
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
1455
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
1456
|
+
const nodeId = readStringValue(entry);
|
|
1457
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
1461
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
1462
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
1463
|
+
if (!inlineNode) return statusNode;
|
|
1464
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
1465
|
+
if (!liveGit) return statusNode;
|
|
1466
|
+
const nextStatus = { ...statusNode };
|
|
1467
|
+
nextStatus.git = liveGit;
|
|
1468
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
1469
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
1470
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
1471
|
+
const connectionState = readStringValue(connection.state);
|
|
1472
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
1473
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
1474
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
1475
|
+
}
|
|
1476
|
+
delete nextStatus.gitProbePending;
|
|
1477
|
+
const error = readStringValue(nextStatus.error);
|
|
1478
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
1479
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
1480
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
1481
|
+
changed = true;
|
|
1482
|
+
return nextStatus;
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0)) return snapshot;
|
|
1486
|
+
const nextSourceOfTruth = {
|
|
1487
|
+
...sourceOfTruth,
|
|
1488
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
1489
|
+
directPeerTruth: {
|
|
1490
|
+
...directPeerTruth,
|
|
1491
|
+
satisfied: options?.requireDirectPeerTruth === true ? unavailableNodeIds.size === 0 : directPeerTruth.satisfied,
|
|
1492
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
1493
|
+
},
|
|
1494
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
1495
|
+
coordinatorOwnsLiveTruth: unavailableNodeIds.size === 0,
|
|
1496
|
+
currentStatus: unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
1497
|
+
} : {}),
|
|
1498
|
+
} : {}),
|
|
1499
|
+
};
|
|
1500
|
+
return {
|
|
1501
|
+
...snapshot,
|
|
1502
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 ? {
|
|
1503
|
+
success: false,
|
|
1504
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
1505
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
1506
|
+
} : {}),
|
|
1507
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
1508
|
+
nodes,
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
1513
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
1514
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
1515
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
1516
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
1517
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
1518
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
1519
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
1520
|
+
? snapshot.sourceOfTruth
|
|
1521
|
+
: {};
|
|
1522
|
+
snapshot.sourceOfTruth = {
|
|
1523
|
+
...sourceOfTruth,
|
|
1524
|
+
aggregateSnapshot: {
|
|
1525
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
1526
|
+
? sourceOfTruth.aggregateSnapshot
|
|
1527
|
+
: {}),
|
|
1528
|
+
owner: 'coordinator_daemon_memory',
|
|
1529
|
+
cached: true,
|
|
1530
|
+
source: 'memory',
|
|
1531
|
+
refreshReason: 'memory_cache_hit',
|
|
1532
|
+
ageMs,
|
|
1533
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
1534
|
+
returnedAt: new Date().toISOString(),
|
|
1535
|
+
},
|
|
1536
|
+
};
|
|
1537
|
+
return snapshot;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
1541
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
1542
|
+
const builtAt = Date.now();
|
|
1543
|
+
const next = this.cloneJsonValue(snapshot);
|
|
1544
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
1545
|
+
? next.sourceOfTruth
|
|
1546
|
+
: {};
|
|
1547
|
+
next.sourceOfTruth = {
|
|
1548
|
+
...sourceOfTruth,
|
|
1549
|
+
aggregateSnapshot: {
|
|
1550
|
+
owner: 'coordinator_daemon_memory',
|
|
1551
|
+
cached: false,
|
|
1552
|
+
source: 'live_refresh',
|
|
1553
|
+
refreshReason,
|
|
1554
|
+
ageMs: 0,
|
|
1555
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
1556
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
1557
|
+
},
|
|
1558
|
+
};
|
|
1559
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next) });
|
|
1560
|
+
return next;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
786
1563
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
787
1564
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
788
|
-
this.
|
|
789
|
-
return inlineMesh as any;
|
|
1565
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
790
1566
|
}
|
|
791
1567
|
return this.inlineMeshCache.get(meshId);
|
|
792
1568
|
}
|
|
793
1569
|
|
|
1570
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
1571
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
1572
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
1573
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
1574
|
+
if (cached) {
|
|
1575
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
1576
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
1577
|
+
return merged;
|
|
1578
|
+
}
|
|
1579
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
1580
|
+
return sanitizedInlineMesh as any;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
794
1583
|
private async getMeshForCommand(
|
|
795
1584
|
meshId: string,
|
|
796
1585
|
inlineMesh?: unknown,
|
|
797
1586
|
options?: { preferInline?: boolean },
|
|
798
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
1587
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
799
1588
|
const preferInline = options?.preferInline === true;
|
|
800
1589
|
if (preferInline) {
|
|
801
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
802
|
-
if (cached)
|
|
1590
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1591
|
+
if (cached) {
|
|
1592
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1593
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
1594
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
1595
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
1596
|
+
}
|
|
1597
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1598
|
+
}
|
|
1599
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1600
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1601
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
1602
|
+
}
|
|
803
1603
|
}
|
|
804
1604
|
try {
|
|
805
1605
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
806
1606
|
const mesh = getMesh(meshId);
|
|
807
|
-
if (mesh) return { mesh, inline: false };
|
|
1607
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
808
1608
|
} catch { /* fall through to inline cache */ }
|
|
809
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
810
|
-
|
|
1609
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1610
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1611
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1612
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
1616
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
1621
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
1622
|
+
const mesh = meshRecord?.mesh;
|
|
1623
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1624
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
1625
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
1626
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
1627
|
+
}
|
|
1628
|
+
return null;
|
|
811
1629
|
}
|
|
812
1630
|
|
|
813
1631
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -817,6 +1635,7 @@ export class DaemonCommandRouter {
|
|
|
817
1635
|
else mesh.nodes.push(node);
|
|
818
1636
|
mesh.updatedAt = new Date().toISOString();
|
|
819
1637
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1638
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
820
1639
|
}
|
|
821
1640
|
|
|
822
1641
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -826,6 +1645,7 @@ export class DaemonCommandRouter {
|
|
|
826
1645
|
mesh.nodes.splice(idx, 1);
|
|
827
1646
|
mesh.updatedAt = new Date().toISOString();
|
|
828
1647
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1648
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
829
1649
|
return true;
|
|
830
1650
|
}
|
|
831
1651
|
|
|
@@ -1104,6 +1924,7 @@ export class DaemonCommandRouter {
|
|
|
1104
1924
|
const deletedSessionIds: string[] = [];
|
|
1105
1925
|
const skippedSessionIds: string[] = [];
|
|
1106
1926
|
const skippedLiveSessionIds: string[] = [];
|
|
1927
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1107
1928
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1108
1929
|
const recordsRemainSessionIds: string[] = [];
|
|
1109
1930
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1138,6 +1959,12 @@ export class DaemonCommandRouter {
|
|
|
1138
1959
|
const completed = this.isCompletedHostedSession(record);
|
|
1139
1960
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1140
1961
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
1962
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
1963
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
1964
|
+
skippedSessionIds.push(sessionId);
|
|
1965
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1141
1968
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1142
1969
|
skippedSessionIds.push(sessionId);
|
|
1143
1970
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1207,6 +2034,7 @@ export class DaemonCommandRouter {
|
|
|
1207
2034
|
deletedSessionIds,
|
|
1208
2035
|
skippedSessionIds,
|
|
1209
2036
|
skippedLiveSessionIds,
|
|
2037
|
+
skippedCoordinatorSessionIds,
|
|
1210
2038
|
...(deleteUnsupported ? {
|
|
1211
2039
|
deleteUnsupported: true,
|
|
1212
2040
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1317,34 +2145,409 @@ export class DaemonCommandRouter {
|
|
|
1317
2145
|
return daemonResult;
|
|
1318
2146
|
}
|
|
1319
2147
|
|
|
1320
|
-
// 2. Delegate to DaemonCommandHandler
|
|
1321
|
-
const handlerResult = await this.deps.commandHandler.handle(cmd, normalizedArgs);
|
|
1322
|
-
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: handlerResult.success, durationMs: Date.now() - cmdStart });
|
|
1323
|
-
recordDebugTrace({
|
|
1324
|
-
interactionId,
|
|
1325
|
-
category: 'command',
|
|
1326
|
-
stage: 'completed',
|
|
1327
|
-
level: handlerResult.success ? 'info' : 'warn',
|
|
1328
|
-
payload: { cmd, source: logSource, success: handlerResult.success, durationMs: Date.now() - cmdStart },
|
|
1329
|
-
});
|
|
2148
|
+
// 2. Delegate to DaemonCommandHandler
|
|
2149
|
+
const handlerResult = await this.deps.commandHandler.handle(cmd, normalizedArgs);
|
|
2150
|
+
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: handlerResult.success, durationMs: Date.now() - cmdStart });
|
|
2151
|
+
recordDebugTrace({
|
|
2152
|
+
interactionId,
|
|
2153
|
+
category: 'command',
|
|
2154
|
+
stage: 'completed',
|
|
2155
|
+
level: handlerResult.success ? 'info' : 'warn',
|
|
2156
|
+
payload: { cmd, source: logSource, success: handlerResult.success, durationMs: Date.now() - cmdStart },
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
// 3. Post-chat command callback
|
|
2160
|
+
if (CHAT_COMMANDS.includes(cmd) && this.deps.onPostChatCommand) {
|
|
2161
|
+
this.deps.onPostChatCommand();
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
return handlerResult;
|
|
2165
|
+
} catch (e: any) {
|
|
2166
|
+
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: false, error: e.message, durationMs: Date.now() - cmdStart });
|
|
2167
|
+
recordDebugTrace({
|
|
2168
|
+
interactionId,
|
|
2169
|
+
category: 'command',
|
|
2170
|
+
stage: 'failed',
|
|
2171
|
+
level: 'error',
|
|
2172
|
+
payload: { cmd, source: logSource, error: e?.message || String(e), durationMs: Date.now() - cmdStart },
|
|
2173
|
+
});
|
|
2174
|
+
throw e;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
|
|
2179
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
2180
|
+
return `${meshId}:${nodeId}`;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
private buildRefineJobHandle(args: {
|
|
2184
|
+
meshId: string;
|
|
2185
|
+
nodeId: string;
|
|
2186
|
+
node?: any;
|
|
2187
|
+
status?: MeshRefineAsyncJobStatus;
|
|
2188
|
+
startedAt?: string;
|
|
2189
|
+
completedAt?: string;
|
|
2190
|
+
jobId?: string;
|
|
2191
|
+
interactionId?: string;
|
|
2192
|
+
retryOfJobId?: string;
|
|
2193
|
+
}): MeshRefineJobHandle {
|
|
2194
|
+
return {
|
|
2195
|
+
success: true,
|
|
2196
|
+
async: true,
|
|
2197
|
+
status: args.status || 'accepted',
|
|
2198
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
2199
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
2200
|
+
meshId: args.meshId,
|
|
2201
|
+
nodeId: args.nodeId,
|
|
2202
|
+
targetNodeId: args.nodeId,
|
|
2203
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
2204
|
+
workspace: readStringValue(args.node?.workspace),
|
|
2205
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
2206
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
2207
|
+
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
2208
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
2209
|
+
evidence: {
|
|
2210
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
2211
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
2212
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
2213
|
+
},
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
2218
|
+
queuePendingMeshCoordinatorEvent({
|
|
2219
|
+
event,
|
|
2220
|
+
meshId: handle.meshId,
|
|
2221
|
+
nodeLabel: handle.targetNodeId,
|
|
2222
|
+
nodeId: handle.targetNodeId,
|
|
2223
|
+
workspace: handle.workspace,
|
|
2224
|
+
metadataEvent: {
|
|
2225
|
+
source: 'refine_mesh_node_async_job',
|
|
2226
|
+
jobId: handle.jobId,
|
|
2227
|
+
interactionId: handle.interactionId,
|
|
2228
|
+
meshId: handle.meshId,
|
|
2229
|
+
nodeId: handle.targetNodeId,
|
|
2230
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2231
|
+
workspace: handle.workspace,
|
|
2232
|
+
status: handle.status,
|
|
2233
|
+
startedAt: handle.startedAt,
|
|
2234
|
+
completedAt: handle.completedAt,
|
|
2235
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2236
|
+
...(result ? { result } : {}),
|
|
2237
|
+
},
|
|
2238
|
+
queuedAt: Date.now(),
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
2243
|
+
try {
|
|
2244
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2245
|
+
appendLedgerEntry(handle.meshId, {
|
|
2246
|
+
kind,
|
|
2247
|
+
nodeId: handle.targetNodeId,
|
|
2248
|
+
payload: {
|
|
2249
|
+
source: 'refine_mesh_node_async_job',
|
|
2250
|
+
refineJob: {
|
|
2251
|
+
jobId: handle.jobId,
|
|
2252
|
+
interactionId: handle.interactionId,
|
|
2253
|
+
status: handle.status,
|
|
2254
|
+
meshId: handle.meshId,
|
|
2255
|
+
nodeId: handle.targetNodeId,
|
|
2256
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2257
|
+
workspace: handle.workspace,
|
|
2258
|
+
startedAt: handle.startedAt,
|
|
2259
|
+
completedAt: handle.completedAt,
|
|
2260
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2261
|
+
},
|
|
2262
|
+
async: true,
|
|
2263
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2264
|
+
...(result ? {
|
|
2265
|
+
success: result.success === true,
|
|
2266
|
+
result,
|
|
2267
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
2268
|
+
} : {}),
|
|
2269
|
+
},
|
|
2270
|
+
});
|
|
2271
|
+
} catch (e: any) {
|
|
2272
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2277
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
2278
|
+
try {
|
|
2279
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2280
|
+
const mesh = meshRecord?.mesh;
|
|
2281
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2282
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
2283
|
+
|
|
2284
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
2285
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
const sourceNode = node.clonedFromNodeId
|
|
2289
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2290
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2291
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2292
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
2293
|
+
|
|
2294
|
+
const { execFile } = await import('node:child_process');
|
|
2295
|
+
const { promisify } = await import('node:util');
|
|
2296
|
+
const execFileAsync = promisify(execFile);
|
|
2297
|
+
|
|
2298
|
+
const resolveStarted = Date.now();
|
|
2299
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2300
|
+
const branch = branchStdout.trim();
|
|
2301
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
2302
|
+
|
|
2303
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2304
|
+
const baseBranch = baseBranchStdout.trim();
|
|
2305
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2306
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
2307
|
+
const baseHead = baseHeadStdout.trim();
|
|
2308
|
+
const branchHead = branchHeadStdout.trim();
|
|
2309
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
2310
|
+
|
|
2311
|
+
const validationStarted = Date.now();
|
|
2312
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2313
|
+
recordMeshRefineStage(
|
|
2314
|
+
refineStages,
|
|
2315
|
+
'validation',
|
|
2316
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
2317
|
+
validationStarted,
|
|
2318
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
2319
|
+
);
|
|
2320
|
+
if (validationSummary.status === 'failed') {
|
|
2321
|
+
return {
|
|
2322
|
+
success: false,
|
|
2323
|
+
code: 'validation_failed',
|
|
2324
|
+
convergenceStatus: 'blocked_review',
|
|
2325
|
+
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2326
|
+
branch,
|
|
2327
|
+
into: baseBranch,
|
|
2328
|
+
validationSummary,
|
|
2329
|
+
refineStages,
|
|
2330
|
+
finalBranchConvergenceState: {
|
|
2331
|
+
branch,
|
|
2332
|
+
baseBranch,
|
|
2333
|
+
merged: false,
|
|
2334
|
+
removed: false,
|
|
2335
|
+
validation: 'failed',
|
|
2336
|
+
status: 'blocked_review',
|
|
2337
|
+
},
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
if (validationSummary.status === 'skipped') {
|
|
2341
|
+
return {
|
|
2342
|
+
success: false,
|
|
2343
|
+
code: 'validation_unavailable',
|
|
2344
|
+
convergenceStatus: 'blocked_review',
|
|
2345
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2346
|
+
branch,
|
|
2347
|
+
into: baseBranch,
|
|
2348
|
+
validationSummary,
|
|
2349
|
+
refineStages,
|
|
2350
|
+
finalBranchConvergenceState: {
|
|
2351
|
+
branch,
|
|
2352
|
+
baseBranch,
|
|
2353
|
+
merged: false,
|
|
2354
|
+
removed: false,
|
|
2355
|
+
validation: 'unavailable',
|
|
2356
|
+
status: 'blocked_review',
|
|
2357
|
+
},
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
const patchEquivalenceStarted = Date.now();
|
|
2362
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
2363
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
2364
|
+
equivalent: patchEquivalence.equivalent,
|
|
2365
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
2366
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
2367
|
+
error: patchEquivalence.error,
|
|
2368
|
+
});
|
|
2369
|
+
if (!patchEquivalence.equivalent) {
|
|
2370
|
+
return {
|
|
2371
|
+
success: false,
|
|
2372
|
+
code: 'patch_equivalence_failed',
|
|
2373
|
+
convergenceStatus: 'blocked_review',
|
|
2374
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
2375
|
+
branch,
|
|
2376
|
+
into: baseBranch,
|
|
2377
|
+
validationSummary,
|
|
2378
|
+
patchEquivalence,
|
|
2379
|
+
refineStages,
|
|
2380
|
+
finalBranchConvergenceState: {
|
|
2381
|
+
branch,
|
|
2382
|
+
baseBranch,
|
|
2383
|
+
merged: false,
|
|
2384
|
+
removed: false,
|
|
2385
|
+
validation: 'passed',
|
|
2386
|
+
patchEquivalence: 'failed',
|
|
2387
|
+
status: 'blocked_review',
|
|
2388
|
+
},
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
2393
|
+
const mergeStarted = Date.now();
|
|
2394
|
+
try {
|
|
2395
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2396
|
+
mergeResult = {
|
|
2397
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
2398
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
2399
|
+
durationMs: Date.now() - mergeStarted,
|
|
2400
|
+
};
|
|
2401
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
2402
|
+
} catch (e: any) {
|
|
2403
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
2404
|
+
error: e?.message || String(e),
|
|
2405
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
2406
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
2407
|
+
});
|
|
2408
|
+
return {
|
|
2409
|
+
success: false,
|
|
2410
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2411
|
+
validationSummary,
|
|
2412
|
+
patchEquivalence,
|
|
2413
|
+
refineStages,
|
|
2414
|
+
finalBranchConvergenceState: {
|
|
2415
|
+
branch,
|
|
2416
|
+
baseBranch,
|
|
2417
|
+
merged: false,
|
|
2418
|
+
removed: false,
|
|
2419
|
+
validation: 'passed',
|
|
2420
|
+
patchEquivalence: 'passed',
|
|
2421
|
+
status: 'not_mergeable',
|
|
2422
|
+
},
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
const cleanupStarted = Date.now();
|
|
2427
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
2428
|
+
meshId,
|
|
2429
|
+
nodeId,
|
|
2430
|
+
sessionCleanupMode: 'preserve',
|
|
2431
|
+
inlineMesh: args?.inlineMesh,
|
|
2432
|
+
});
|
|
2433
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
2434
|
+
removed: removeResult?.removed,
|
|
2435
|
+
code: removeResult?.code,
|
|
2436
|
+
error: removeResult?.error,
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
let ledgerError: string | undefined;
|
|
2440
|
+
const ledgerStarted = Date.now();
|
|
2441
|
+
try {
|
|
2442
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2443
|
+
appendLedgerEntry(meshId, {
|
|
2444
|
+
kind: 'node_removed',
|
|
2445
|
+
nodeId,
|
|
2446
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
2447
|
+
});
|
|
2448
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
2449
|
+
} catch (e: any) {
|
|
2450
|
+
ledgerError = e?.message || String(e);
|
|
2451
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
const finalBranchConvergenceState = {
|
|
2455
|
+
branch: baseBranch,
|
|
2456
|
+
mergedBranch: branch,
|
|
2457
|
+
baseBranch,
|
|
2458
|
+
merged: true,
|
|
2459
|
+
removed: removeResult?.success !== false,
|
|
2460
|
+
validation: 'passed',
|
|
2461
|
+
patchEquivalence: 'passed',
|
|
2462
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2463
|
+
};
|
|
1330
2464
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2465
|
+
if (removeResult?.success === false) {
|
|
2466
|
+
return {
|
|
2467
|
+
success: false,
|
|
2468
|
+
code: 'cleanup_failed',
|
|
2469
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
2470
|
+
merged: true,
|
|
2471
|
+
branch,
|
|
2472
|
+
into: baseBranch,
|
|
2473
|
+
removeResult,
|
|
2474
|
+
validationSummary,
|
|
2475
|
+
patchEquivalence,
|
|
2476
|
+
mergeResult,
|
|
2477
|
+
refineStages,
|
|
2478
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2479
|
+
finalBranchConvergenceState,
|
|
2480
|
+
};
|
|
1334
2481
|
}
|
|
1335
2482
|
|
|
1336
|
-
return
|
|
2483
|
+
return {
|
|
2484
|
+
success: true,
|
|
2485
|
+
merged: true,
|
|
2486
|
+
branch,
|
|
2487
|
+
into: baseBranch,
|
|
2488
|
+
removeResult,
|
|
2489
|
+
validationSummary,
|
|
2490
|
+
patchEquivalence,
|
|
2491
|
+
mergeResult,
|
|
2492
|
+
refineStages,
|
|
2493
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2494
|
+
finalBranchConvergenceState,
|
|
2495
|
+
};
|
|
1337
2496
|
} catch (e: any) {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
2497
|
+
return { success: false, error: e.message, refineStages };
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
2502
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
2503
|
+
let result: Record<string, unknown>;
|
|
2504
|
+
try {
|
|
2505
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
2506
|
+
} catch (e: any) {
|
|
2507
|
+
result = { success: false, error: e?.message || String(e) };
|
|
1347
2508
|
}
|
|
2509
|
+
const completedAt = new Date().toISOString();
|
|
2510
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
2511
|
+
meshId: handle.meshId,
|
|
2512
|
+
nodeId: handle.targetNodeId,
|
|
2513
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
2514
|
+
startedAt: handle.startedAt,
|
|
2515
|
+
completedAt,
|
|
2516
|
+
jobId: handle.jobId,
|
|
2517
|
+
interactionId: handle.interactionId,
|
|
2518
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2519
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
2520
|
+
});
|
|
2521
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
2522
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
2523
|
+
this.runningRefineJobs.delete(key);
|
|
2524
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
2525
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
2526
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2530
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
2531
|
+
const running = this.runningRefineJobs.get(key);
|
|
2532
|
+
if (running) return { ...running, duplicate: true };
|
|
2533
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
2534
|
+
|
|
2535
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2536
|
+
const mesh = meshRecord?.mesh;
|
|
2537
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2538
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
2539
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
2540
|
+
|
|
2541
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
|
|
2542
|
+
this.runningRefineJobs.set(key, handle);
|
|
2543
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
2544
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
2545
|
+
|
|
2546
|
+
setImmediate(() => {
|
|
2547
|
+
void this.finishMeshRefineJob(handle, args);
|
|
2548
|
+
});
|
|
2549
|
+
|
|
2550
|
+
return handle;
|
|
1348
2551
|
}
|
|
1349
2552
|
|
|
1350
2553
|
// ─── Daemon-level command core ───────────────────
|
|
@@ -1361,7 +2564,8 @@ export class DaemonCommandRouter {
|
|
|
1361
2564
|
}
|
|
1362
2565
|
|
|
1363
2566
|
case 'get_pending_mesh_events': {
|
|
1364
|
-
const
|
|
2567
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2568
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1365
2569
|
return { success: true, events };
|
|
1366
2570
|
}
|
|
1367
2571
|
|
|
@@ -1966,15 +3170,44 @@ export class DaemonCommandRouter {
|
|
|
1966
3170
|
case 'get_mesh': {
|
|
1967
3171
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1968
3172
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
3173
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3174
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
3175
|
+
|
|
3176
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
3177
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
3178
|
+
mesh: meshRecord.mesh,
|
|
3179
|
+
meshSource: meshRecord.source,
|
|
3180
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
3181
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
3182
|
+
localMachineId: loadConfig().machineId || '',
|
|
3183
|
+
});
|
|
3184
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
3185
|
+
const sourceOfTruth = {
|
|
3186
|
+
membership: meshRecord.source === 'inline_cache'
|
|
3187
|
+
? 'coordinator_inline_mesh_cache'
|
|
3188
|
+
: meshRecord.source === 'local_config'
|
|
3189
|
+
? 'local_mesh_config'
|
|
3190
|
+
: 'inline_bootstrap_snapshot',
|
|
3191
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
3192
|
+
directPeerTruth: {
|
|
3193
|
+
required: requireDirectPeerTruth,
|
|
3194
|
+
satisfied: directTruthSatisfied,
|
|
3195
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
3196
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
3197
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
3198
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
3199
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
3200
|
+
},
|
|
3201
|
+
};
|
|
3202
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
3203
|
+
return {
|
|
3204
|
+
success: false,
|
|
3205
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
3206
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
3207
|
+
sourceOfTruth,
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
1978
3211
|
}
|
|
1979
3212
|
|
|
1980
3213
|
case 'create_mesh': {
|
|
@@ -1985,7 +3218,10 @@ export class DaemonCommandRouter {
|
|
|
1985
3218
|
if (!name) return { success: false, error: 'name required' };
|
|
1986
3219
|
try {
|
|
1987
3220
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
1988
|
-
const
|
|
3221
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
3222
|
+
? args.meshHost
|
|
3223
|
+
: undefined;
|
|
3224
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
1989
3225
|
return { success: true, mesh };
|
|
1990
3226
|
} catch (e: any) {
|
|
1991
3227
|
return { success: false, error: e.message };
|
|
@@ -2002,16 +3238,237 @@ export class DaemonCommandRouter {
|
|
|
2002
3238
|
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
2003
3239
|
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
2004
3240
|
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
3241
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
2005
3242
|
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
2006
3243
|
const mesh = updateMesh(meshId, patch as any);
|
|
2007
3244
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2008
3245
|
this.inlineMeshCache.set(meshId, mesh);
|
|
3246
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2009
3247
|
return { success: true, mesh };
|
|
2010
3248
|
} catch (e: any) {
|
|
2011
3249
|
return { success: false, error: e.message };
|
|
2012
3250
|
}
|
|
2013
3251
|
}
|
|
2014
3252
|
|
|
3253
|
+
case 'get_mesh_host_pairing': {
|
|
3254
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3255
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3256
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3257
|
+
const mesh = meshRecord?.mesh;
|
|
3258
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3259
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3260
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
3261
|
+
return {
|
|
3262
|
+
success: true,
|
|
3263
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
3264
|
+
meshId,
|
|
3265
|
+
hostAddress: meshHost.hostAddress,
|
|
3266
|
+
meshHost,
|
|
3267
|
+
manualPairing: {
|
|
3268
|
+
status: pairingStatus,
|
|
3269
|
+
joinImplemented: true,
|
|
3270
|
+
protocol: 'standalone_command_direct_v1',
|
|
3271
|
+
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.',
|
|
3272
|
+
},
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
case 'configure_mesh_host_pairing': {
|
|
3277
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3278
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
3279
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3280
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3281
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
3282
|
+
try {
|
|
3283
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
3284
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
3285
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
3286
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
3287
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
3288
|
+
return {
|
|
3289
|
+
success: true,
|
|
3290
|
+
code: 'mesh_host_pairing_pending',
|
|
3291
|
+
meshId,
|
|
3292
|
+
hostAddress: configured.hostAddress,
|
|
3293
|
+
meshHost,
|
|
3294
|
+
manualPairing: {
|
|
3295
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
3296
|
+
joinImplemented: true,
|
|
3297
|
+
protocol: 'standalone_command_direct_v1',
|
|
3298
|
+
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.',
|
|
3299
|
+
},
|
|
3300
|
+
};
|
|
3301
|
+
} catch (e: any) {
|
|
3302
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
case 'create_mesh_host_pairing_token': {
|
|
3307
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3308
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3309
|
+
try {
|
|
3310
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
3311
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
3312
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
3313
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
3314
|
+
});
|
|
3315
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
3316
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
3317
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3318
|
+
return {
|
|
3319
|
+
success: true,
|
|
3320
|
+
code: 'mesh_host_pairing_token_created',
|
|
3321
|
+
meshId,
|
|
3322
|
+
token: created.token,
|
|
3323
|
+
tokenId: created.tokenId,
|
|
3324
|
+
expiresAt: created.expiresAt,
|
|
3325
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
3326
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
3327
|
+
};
|
|
3328
|
+
} catch (e: any) {
|
|
3329
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
case 'apply_mesh_host_join': {
|
|
3334
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3335
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3336
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
3337
|
+
? args.memberNode
|
|
3338
|
+
: null;
|
|
3339
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3340
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
3341
|
+
try {
|
|
3342
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
3343
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
3344
|
+
token,
|
|
3345
|
+
memberNode: memberNode as any,
|
|
3346
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
3347
|
+
});
|
|
3348
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
3349
|
+
if (!applied.accepted) {
|
|
3350
|
+
return {
|
|
3351
|
+
success: false,
|
|
3352
|
+
code: 'mesh_host_join_rejected',
|
|
3353
|
+
meshId,
|
|
3354
|
+
tokenId: applied.tokenId,
|
|
3355
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
3356
|
+
error: applied.reason,
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
3360
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3361
|
+
try {
|
|
3362
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3363
|
+
appendLedgerEntry(meshId, {
|
|
3364
|
+
kind: 'node_joined',
|
|
3365
|
+
nodeId: applied.node.id,
|
|
3366
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
3367
|
+
});
|
|
3368
|
+
} catch { /* ledger append is best-effort */ }
|
|
3369
|
+
return {
|
|
3370
|
+
success: true,
|
|
3371
|
+
code: 'mesh_host_join_accepted',
|
|
3372
|
+
meshId,
|
|
3373
|
+
node: applied.node,
|
|
3374
|
+
tokenId: applied.tokenId,
|
|
3375
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
3376
|
+
};
|
|
3377
|
+
} catch (e: any) {
|
|
3378
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
case 'join_mesh_host_pairing': {
|
|
3383
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3384
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3385
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3386
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
3387
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3388
|
+
const mesh = meshRecord?.mesh;
|
|
3389
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3390
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3391
|
+
if (meshHost.role !== 'member') {
|
|
3392
|
+
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.' };
|
|
3393
|
+
}
|
|
3394
|
+
try {
|
|
3395
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
3396
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
3397
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
3398
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
3399
|
+
}
|
|
3400
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
3401
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
3402
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
3403
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
3404
|
+
? args.hostDaemonId.trim()
|
|
3405
|
+
: meshHost.hostDaemonId;
|
|
3406
|
+
let hostResult: any;
|
|
3407
|
+
let transport: string;
|
|
3408
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
3409
|
+
transport = 'mesh_command_dispatch';
|
|
3410
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
3411
|
+
meshId: hostMeshId,
|
|
3412
|
+
token,
|
|
3413
|
+
memberMeshId: meshId,
|
|
3414
|
+
memberNode,
|
|
3415
|
+
});
|
|
3416
|
+
} else if (meshHost.hostAddress) {
|
|
3417
|
+
transport = 'standalone_http_command';
|
|
3418
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
3419
|
+
const response = await fetch(commandUrl, {
|
|
3420
|
+
method: 'POST',
|
|
3421
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3422
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
3423
|
+
});
|
|
3424
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
3425
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
3426
|
+
} else {
|
|
3427
|
+
return {
|
|
3428
|
+
success: false,
|
|
3429
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
3430
|
+
meshId,
|
|
3431
|
+
meshHost,
|
|
3432
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
if (!hostResult?.success) {
|
|
3436
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
3437
|
+
}
|
|
3438
|
+
const joined = meshRecord.inline
|
|
3439
|
+
? null
|
|
3440
|
+
: markMeshHostPairingJoined(meshId, {
|
|
3441
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3442
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
3443
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
3444
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
3445
|
+
});
|
|
3446
|
+
if (joined) {
|
|
3447
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
3448
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3449
|
+
}
|
|
3450
|
+
return {
|
|
3451
|
+
success: true,
|
|
3452
|
+
code: 'mesh_host_join_applied',
|
|
3453
|
+
meshId,
|
|
3454
|
+
hostMeshId,
|
|
3455
|
+
transport,
|
|
3456
|
+
node: hostResult.node,
|
|
3457
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3458
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
3459
|
+
hostResult,
|
|
3460
|
+
manualPairing: {
|
|
3461
|
+
status: 'paired',
|
|
3462
|
+
joinImplemented: true,
|
|
3463
|
+
protocol: 'standalone_command_direct_v1',
|
|
3464
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
3465
|
+
},
|
|
3466
|
+
};
|
|
3467
|
+
} catch (e: any) {
|
|
3468
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
2015
3472
|
case 'delete_mesh': {
|
|
2016
3473
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2017
3474
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -2105,6 +3562,8 @@ export class DaemonCommandRouter {
|
|
|
2105
3562
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2106
3563
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2107
3564
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3565
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
3566
|
+
if (ownerFailure) return ownerFailure;
|
|
2108
3567
|
try {
|
|
2109
3568
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2110
3569
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -2120,6 +3579,8 @@ export class DaemonCommandRouter {
|
|
|
2120
3579
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2121
3580
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2122
3581
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3582
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
3583
|
+
if (ownerFailure) return ownerFailure;
|
|
2123
3584
|
try {
|
|
2124
3585
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2125
3586
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -2141,6 +3602,8 @@ export class DaemonCommandRouter {
|
|
|
2141
3602
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2142
3603
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2143
3604
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
3605
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
3606
|
+
if (ownerFailure) return ownerFailure;
|
|
2144
3607
|
try {
|
|
2145
3608
|
const { addNode } = await import('../config/mesh-config.js');
|
|
2146
3609
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -2151,7 +3614,8 @@ export class DaemonCommandRouter {
|
|
|
2151
3614
|
...(readOnly ? { readOnly: true } : {}),
|
|
2152
3615
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
2153
3616
|
};
|
|
2154
|
-
const
|
|
3617
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
3618
|
+
const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}), ...(role ? { role } : {}) });
|
|
2155
3619
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
2156
3620
|
return { success: true, node };
|
|
2157
3621
|
} catch (e: any) {
|
|
@@ -2163,6 +3627,8 @@ export class DaemonCommandRouter {
|
|
|
2163
3627
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2164
3628
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2165
3629
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3630
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
3631
|
+
if (ownerFailure) return ownerFailure;
|
|
2166
3632
|
try {
|
|
2167
3633
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2168
3634
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2191,6 +3657,8 @@ export class DaemonCommandRouter {
|
|
|
2191
3657
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2192
3658
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2193
3659
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3660
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
3661
|
+
if (ownerFailure) return ownerFailure;
|
|
2194
3662
|
try {
|
|
2195
3663
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2196
3664
|
const mesh = meshRecord?.mesh;
|
|
@@ -2216,131 +3684,91 @@ export class DaemonCommandRouter {
|
|
|
2216
3684
|
}
|
|
2217
3685
|
}
|
|
2218
3686
|
|
|
2219
|
-
case '
|
|
3687
|
+
case 'get_mesh_refine_config_schema': {
|
|
3688
|
+
return {
|
|
3689
|
+
success: true,
|
|
3690
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
3691
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
3692
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
3693
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
case 'validate_mesh_refine_config': {
|
|
3698
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
3699
|
+
const mesh = args?.inlineMesh || {};
|
|
3700
|
+
const loaded = args?.config !== undefined
|
|
3701
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
3702
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
3703
|
+
const validation = loaded.config
|
|
3704
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
3705
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
3706
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
case 'suggest_mesh_refine_config': {
|
|
3710
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
3711
|
+
const mesh = args?.inlineMesh || {};
|
|
3712
|
+
return {
|
|
3713
|
+
success: true,
|
|
3714
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
3715
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
case 'plan_mesh_refine_node': {
|
|
2220
3720
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2221
3721
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2222
3722
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2223
|
-
|
|
3723
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3724
|
+
const mesh = meshRecord?.mesh;
|
|
3725
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3726
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
3727
|
+
return {
|
|
3728
|
+
success: true,
|
|
3729
|
+
dryRun: true,
|
|
3730
|
+
nodeId,
|
|
3731
|
+
workspace: node.workspace,
|
|
3732
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
3733
|
+
mergeWillRun: false,
|
|
3734
|
+
cleanupWillRun: false,
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
case 'fast_forward_mesh_node': {
|
|
3739
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3740
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3741
|
+
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
3742
|
+
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
3743
|
+
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
3744
|
+
: undefined;
|
|
3745
|
+
if (!workspace && meshId && nodeId) {
|
|
2224
3746
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2225
3747
|
const mesh = meshRecord?.mesh;
|
|
2226
3748
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
return { success: false, error: `Refinery requires a local worktree node` };
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
const sourceNode = node.clonedFromNodeId
|
|
2234
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2235
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2236
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2237
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2238
|
-
|
|
2239
|
-
const { execFile } = await import('node:child_process');
|
|
2240
|
-
const { promisify } = await import('node:util');
|
|
2241
|
-
const execFileAsync = promisify(execFile);
|
|
2242
|
-
|
|
2243
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2244
|
-
const branch = branchStdout.trim();
|
|
2245
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2246
|
-
|
|
2247
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2248
|
-
const baseBranch = baseBranchStdout.trim();
|
|
2249
|
-
|
|
2250
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2251
|
-
if (validationSummary.status === 'failed') {
|
|
2252
|
-
return {
|
|
2253
|
-
success: false,
|
|
2254
|
-
code: 'validation_failed',
|
|
2255
|
-
convergenceStatus: 'blocked_review',
|
|
2256
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2257
|
-
branch,
|
|
2258
|
-
into: baseBranch,
|
|
2259
|
-
validationSummary,
|
|
2260
|
-
finalBranchConvergenceState: {
|
|
2261
|
-
branch,
|
|
2262
|
-
baseBranch,
|
|
2263
|
-
merged: false,
|
|
2264
|
-
removed: false,
|
|
2265
|
-
validation: 'failed',
|
|
2266
|
-
status: 'blocked_review',
|
|
2267
|
-
},
|
|
2268
|
-
};
|
|
2269
|
-
}
|
|
2270
|
-
if (validationSummary.status === 'skipped') {
|
|
2271
|
-
return {
|
|
2272
|
-
success: false,
|
|
2273
|
-
code: 'validation_unavailable',
|
|
2274
|
-
convergenceStatus: 'blocked_review',
|
|
2275
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2276
|
-
branch,
|
|
2277
|
-
into: baseBranch,
|
|
2278
|
-
validationSummary,
|
|
2279
|
-
finalBranchConvergenceState: {
|
|
2280
|
-
branch,
|
|
2281
|
-
baseBranch,
|
|
2282
|
-
merged: false,
|
|
2283
|
-
removed: false,
|
|
2284
|
-
validation: 'unavailable',
|
|
2285
|
-
status: 'blocked_review',
|
|
2286
|
-
},
|
|
2287
|
-
};
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
try {
|
|
2291
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2292
|
-
} catch (e: any) {
|
|
2293
|
-
return {
|
|
2294
|
-
success: false,
|
|
2295
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2296
|
-
validationSummary,
|
|
2297
|
-
finalBranchConvergenceState: {
|
|
2298
|
-
branch,
|
|
2299
|
-
baseBranch,
|
|
2300
|
-
merged: false,
|
|
2301
|
-
removed: false,
|
|
2302
|
-
validation: 'passed',
|
|
2303
|
-
status: 'not_mergeable',
|
|
2304
|
-
},
|
|
2305
|
-
};
|
|
3749
|
+
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
3750
|
+
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
3751
|
+
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
2306
3752
|
}
|
|
2307
|
-
|
|
2308
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
2309
|
-
meshId,
|
|
2310
|
-
nodeId,
|
|
2311
|
-
sessionCleanupMode: 'kill',
|
|
2312
|
-
inlineMesh: args?.inlineMesh,
|
|
2313
|
-
});
|
|
2314
|
-
|
|
2315
|
-
try {
|
|
2316
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2317
|
-
appendLedgerEntry(meshId, {
|
|
2318
|
-
kind: 'node_removed',
|
|
2319
|
-
nodeId,
|
|
2320
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
2321
|
-
});
|
|
2322
|
-
} catch {}
|
|
2323
|
-
|
|
2324
|
-
return {
|
|
2325
|
-
success: true,
|
|
2326
|
-
merged: true,
|
|
2327
|
-
branch,
|
|
2328
|
-
into: baseBranch,
|
|
2329
|
-
removeResult,
|
|
2330
|
-
validationSummary,
|
|
2331
|
-
finalBranchConvergenceState: {
|
|
2332
|
-
branch: baseBranch,
|
|
2333
|
-
mergedBranch: branch,
|
|
2334
|
-
baseBranch,
|
|
2335
|
-
merged: true,
|
|
2336
|
-
removed: removeResult?.success !== false,
|
|
2337
|
-
validation: 'passed',
|
|
2338
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2339
|
-
},
|
|
2340
|
-
};
|
|
2341
|
-
} catch (e: any) {
|
|
2342
|
-
return { success: false, error: e.message };
|
|
2343
3753
|
}
|
|
3754
|
+
const result = await (fastForwardMeshNode({
|
|
3755
|
+
meshId: meshId || undefined,
|
|
3756
|
+
nodeId: nodeId || undefined,
|
|
3757
|
+
workspace,
|
|
3758
|
+
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
3759
|
+
execute: args?.execute === true,
|
|
3760
|
+
dryRun: args?.dryRun === true,
|
|
3761
|
+
updateSubmodules: args?.updateSubmodules === true,
|
|
3762
|
+
submoduleIgnorePaths,
|
|
3763
|
+
}) as Promise<unknown>);
|
|
3764
|
+
return result as CommandRouterResult;
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
case 'refine_mesh_node': {
|
|
3768
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3769
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3770
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3771
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
2344
3772
|
}
|
|
2345
3773
|
|
|
2346
3774
|
case 'remove_mesh_node': {
|
|
@@ -2384,6 +3812,7 @@ export class DaemonCommandRouter {
|
|
|
2384
3812
|
} else {
|
|
2385
3813
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2386
3814
|
removed = removeNode(meshId, nodeId);
|
|
3815
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2387
3816
|
}
|
|
2388
3817
|
|
|
2389
3818
|
// Record in task ledger
|
|
@@ -2421,6 +3850,8 @@ export class DaemonCommandRouter {
|
|
|
2421
3850
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2422
3851
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2423
3852
|
if (!branch) return { success: false, error: 'branch required' };
|
|
3853
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
3854
|
+
if (ownerFailure) return ownerFailure;
|
|
2424
3855
|
|
|
2425
3856
|
try {
|
|
2426
3857
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2469,6 +3900,7 @@ export class DaemonCommandRouter {
|
|
|
2469
3900
|
policy: { ...(sourceNode.policy || {}) },
|
|
2470
3901
|
});
|
|
2471
3902
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
3903
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2472
3904
|
}
|
|
2473
3905
|
|
|
2474
3906
|
// Initialize submodules if policy allows (default: true)
|
|
@@ -2510,6 +3942,8 @@ export class DaemonCommandRouter {
|
|
|
2510
3942
|
case 'trigger_mesh_queue': {
|
|
2511
3943
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2512
3944
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3945
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
3946
|
+
if (ownerFailure) return ownerFailure;
|
|
2513
3947
|
try {
|
|
2514
3948
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2515
3949
|
if (meshId) {
|
|
@@ -2541,6 +3975,15 @@ export class DaemonCommandRouter {
|
|
|
2541
3975
|
mesh = getMesh(meshId);
|
|
2542
3976
|
}
|
|
2543
3977
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3978
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3979
|
+
if (!meshHost.canOwnCoordinator) {
|
|
3980
|
+
return {
|
|
3981
|
+
success: false,
|
|
3982
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
3983
|
+
meshId,
|
|
3984
|
+
cliType,
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
2544
3987
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2545
3988
|
|
|
2546
3989
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2560,7 +4003,16 @@ export class DaemonCommandRouter {
|
|
|
2560
4003
|
cliType,
|
|
2561
4004
|
};
|
|
2562
4005
|
}
|
|
2563
|
-
const
|
|
4006
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
4007
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
4008
|
+
: [];
|
|
4009
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
4010
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4011
|
+
meshId,
|
|
4012
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
4013
|
+
liveSessionRecords: liveMeshSessions,
|
|
4014
|
+
allowCoordinatorSession: true,
|
|
4015
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2564
4016
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2565
4017
|
if (!cliType) {
|
|
2566
4018
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -2905,6 +4357,27 @@ export class DaemonCommandRouter {
|
|
|
2905
4357
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2906
4358
|
const mesh = meshRecord?.mesh;
|
|
2907
4359
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4360
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4361
|
+
|
|
4362
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
4363
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
4364
|
+
if (!refreshRequested) {
|
|
4365
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
4366
|
+
if (cachedStatus) {
|
|
4367
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
4368
|
+
meshId,
|
|
4369
|
+
command: 'mesh_status',
|
|
4370
|
+
refreshRequested,
|
|
4371
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
4372
|
+
});
|
|
4373
|
+
return cachedStatus;
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
const refreshReason = refreshRequested
|
|
4377
|
+
? 'explicit_refresh'
|
|
4378
|
+
: hadAggregateCache
|
|
4379
|
+
? 'stale_pending_cache_refresh'
|
|
4380
|
+
: 'cold_cache_miss';
|
|
2908
4381
|
|
|
2909
4382
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
2910
4383
|
const queue = getQueue(meshId);
|
|
@@ -2913,58 +4386,344 @@ export class DaemonCommandRouter {
|
|
|
2913
4386
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2914
4387
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2915
4388
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
2916
|
-
|
|
4389
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
4390
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
4391
|
+
: [];
|
|
4392
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
4393
|
+
|
|
4394
|
+
const localMachineId = loadConfig().machineId || '';
|
|
4395
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
4396
|
+
const directTruth = requireDirectPeerTruth
|
|
4397
|
+
? await hydrateInlineMeshDirectTruth({
|
|
4398
|
+
mesh,
|
|
4399
|
+
meshSource: meshRecord.source,
|
|
4400
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4401
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
4402
|
+
localMachineId,
|
|
4403
|
+
})
|
|
4404
|
+
: {
|
|
4405
|
+
directEvidenceCount: 0,
|
|
4406
|
+
localConfirmedCount: 0,
|
|
4407
|
+
peerAttemptedCount: 0,
|
|
4408
|
+
peerConfirmedCount: 0,
|
|
4409
|
+
unavailableNodeIds: [] as string[],
|
|
4410
|
+
};
|
|
4411
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
4412
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
4413
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
4414
|
+
&& !refreshRequested
|
|
4415
|
+
&& directTruth.directEvidenceCount > 0
|
|
4416
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
4417
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
4418
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
4419
|
+
: directTruth;
|
|
4420
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
4421
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && effectiveDirectTruth.unavailableNodeIds.length === 0);
|
|
4422
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4423
|
+
const failureResult = {
|
|
4424
|
+
success: false,
|
|
4425
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
4426
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
4427
|
+
sourceOfTruth: {
|
|
4428
|
+
membership: meshRecord.source === 'inline_cache'
|
|
4429
|
+
? 'coordinator_inline_mesh_cache'
|
|
4430
|
+
: meshRecord.source === 'local_config'
|
|
4431
|
+
? 'local_mesh_config'
|
|
4432
|
+
: 'inline_bootstrap_snapshot',
|
|
4433
|
+
coordinatorOwnsLiveTruth: false,
|
|
4434
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
4435
|
+
directPeerTruth: {
|
|
4436
|
+
required: true,
|
|
4437
|
+
satisfied: false,
|
|
4438
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4439
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4440
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4441
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4442
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4443
|
+
},
|
|
4444
|
+
},
|
|
4445
|
+
};
|
|
4446
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
4447
|
+
meshId,
|
|
4448
|
+
command: 'mesh_status',
|
|
4449
|
+
refreshRequested,
|
|
4450
|
+
meshSource: meshRecord.source,
|
|
4451
|
+
directTruth,
|
|
4452
|
+
});
|
|
4453
|
+
return failureResult;
|
|
4454
|
+
}
|
|
4455
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
4456
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
4457
|
+
mesh.coordinator?.preferredNodeId,
|
|
4458
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
4459
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
4460
|
+
);
|
|
4461
|
+
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
4462
|
+
? selectedCoordinatorNodeId
|
|
4463
|
+
: undefined;
|
|
4464
|
+
const refreshedAt = new Date().toISOString();
|
|
2917
4465
|
const nodeStatuses = [];
|
|
2918
|
-
for (const node of mesh.nodes || []) {
|
|
4466
|
+
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
4467
|
+
const nodeId = String(node.id || node.nodeId || '');
|
|
4468
|
+
const daemonId = readStringValue(node.daemonId);
|
|
4469
|
+
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
4470
|
+
const isSelfNode = Boolean(
|
|
4471
|
+
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
4472
|
+
) || Boolean(
|
|
4473
|
+
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
4474
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
2919
4475
|
const status: Record<string, unknown> = {
|
|
2920
|
-
nodeId
|
|
4476
|
+
nodeId,
|
|
2921
4477
|
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2922
4478
|
workspace: node.workspace,
|
|
2923
4479
|
repoRoot: node.repoRoot,
|
|
2924
4480
|
isLocalWorktree: node.isLocalWorktree,
|
|
2925
4481
|
worktreeBranch: node.worktreeBranch,
|
|
2926
|
-
|
|
4482
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
4483
|
+
daemonId,
|
|
2927
4484
|
machineId: node.machineId,
|
|
4485
|
+
machineStatus: node.machineStatus,
|
|
2928
4486
|
health: 'unknown',
|
|
2929
4487
|
providers: node.providers || [],
|
|
4488
|
+
providerPriority,
|
|
2930
4489
|
activeSessions: [],
|
|
4490
|
+
activeSessionDetails: [],
|
|
4491
|
+
launchReady: false,
|
|
2931
4492
|
};
|
|
2932
|
-
if (
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
4493
|
+
if (isSelfNode) {
|
|
4494
|
+
status.connection = {
|
|
4495
|
+
perspective: 'selected_coordinator',
|
|
4496
|
+
source: 'mesh_peer_status',
|
|
4497
|
+
state: 'self',
|
|
4498
|
+
transport: 'local',
|
|
4499
|
+
reported: true,
|
|
4500
|
+
reason: 'Selected coordinator daemon',
|
|
4501
|
+
lastStateChangeAt: refreshedAt,
|
|
4502
|
+
};
|
|
4503
|
+
} else if (daemonId) {
|
|
4504
|
+
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
4505
|
+
status.connection = connection ?? {
|
|
4506
|
+
perspective: 'selected_coordinator',
|
|
4507
|
+
source: 'not_reported',
|
|
4508
|
+
state: 'unknown',
|
|
4509
|
+
transport: 'unknown',
|
|
4510
|
+
reported: false,
|
|
4511
|
+
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
4512
|
+
};
|
|
4513
|
+
} else {
|
|
4514
|
+
status.connection = {
|
|
4515
|
+
perspective: 'selected_coordinator',
|
|
4516
|
+
source: 'not_reported',
|
|
4517
|
+
state: 'unknown',
|
|
4518
|
+
transport: 'unknown',
|
|
4519
|
+
reported: false,
|
|
4520
|
+
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
4521
|
+
};
|
|
4522
|
+
}
|
|
4523
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
4524
|
+
meshId,
|
|
4525
|
+
node,
|
|
4526
|
+
nodeId,
|
|
4527
|
+
liveSessionRecords: liveMeshSessions,
|
|
4528
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4529
|
+
});
|
|
4530
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4531
|
+
meshId,
|
|
4532
|
+
nodeId,
|
|
4533
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
4534
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4535
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
4536
|
+
status.workspace = workspace || node.workspace;
|
|
4537
|
+
if (matchedLiveSessionRecords.length > 0) {
|
|
4538
|
+
const sessionIds = matchedLiveSessionRecords
|
|
4539
|
+
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
4540
|
+
.filter(Boolean);
|
|
4541
|
+
const providerTypes = matchedLiveSessionRecords
|
|
4542
|
+
.map((record: any) => readStringValue(record?.providerType))
|
|
4543
|
+
.filter(Boolean) as string[];
|
|
4544
|
+
status.activeSessions = sessionIds;
|
|
4545
|
+
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
4546
|
+
if (providerTypes.length > 0) {
|
|
4547
|
+
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
2936
4548
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
status.
|
|
2945
|
-
|
|
4549
|
+
}
|
|
4550
|
+
if (workspace) {
|
|
4551
|
+
if (!fs.existsSync(workspace)) {
|
|
4552
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
4553
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
4554
|
+
let remoteProbeApplied = false;
|
|
4555
|
+
if (inlineTransitGit) {
|
|
4556
|
+
status.git = inlineTransitGit;
|
|
4557
|
+
status.health = inlineTransitGit.isGitRepo
|
|
4558
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
4559
|
+
: 'degraded';
|
|
4560
|
+
const connection = readObjectRecord(status.connection);
|
|
4561
|
+
const connectionState = readStringValue(connection.state);
|
|
4562
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4563
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4564
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4565
|
+
}
|
|
4566
|
+
remoteProbeApplied = true;
|
|
4567
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
4568
|
+
try {
|
|
4569
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
4570
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4571
|
+
daemonId,
|
|
4572
|
+
workspace,
|
|
4573
|
+
timeoutMs: 8000,
|
|
4574
|
+
});
|
|
4575
|
+
if (remoteGit) {
|
|
4576
|
+
status.git = remoteGit;
|
|
4577
|
+
status.health = remoteGit.isGitRepo
|
|
4578
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
4579
|
+
: 'degraded';
|
|
4580
|
+
const connection = readObjectRecord(status.connection);
|
|
4581
|
+
const connectionState = readStringValue(connection.state);
|
|
4582
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4583
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4584
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4585
|
+
}
|
|
4586
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
4587
|
+
remoteProbeApplied = true;
|
|
4588
|
+
}
|
|
4589
|
+
} catch {
|
|
4590
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
4591
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
4592
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
4593
|
+
status.connection = refreshedConnection;
|
|
4594
|
+
try {
|
|
4595
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
4596
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4597
|
+
daemonId,
|
|
4598
|
+
workspace,
|
|
4599
|
+
timeoutMs: 12000,
|
|
4600
|
+
});
|
|
4601
|
+
if (remoteGit) {
|
|
4602
|
+
status.git = remoteGit;
|
|
4603
|
+
status.health = remoteGit.isGitRepo
|
|
4604
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
4605
|
+
: 'degraded';
|
|
4606
|
+
const connection = readObjectRecord(status.connection);
|
|
4607
|
+
const connectionState = readStringValue(connection.state);
|
|
4608
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4609
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4610
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4611
|
+
}
|
|
4612
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
4613
|
+
remoteProbeApplied = true;
|
|
4614
|
+
}
|
|
4615
|
+
} catch {
|
|
4616
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
2946
4620
|
}
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
4621
|
+
if (!remoteProbeApplied) {
|
|
4622
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
4623
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
4624
|
+
&& !isSelfNode
|
|
4625
|
+
&& !!daemonId
|
|
4626
|
+
&& (
|
|
4627
|
+
readStringValue(status.machineStatus) === 'online'
|
|
4628
|
+
|| readStringValue(status.health) === 'online'
|
|
4629
|
+
|| connectionState === 'connecting'
|
|
4630
|
+
|| connectionState === 'connected'
|
|
4631
|
+
|| connectionState === 'unknown'
|
|
4632
|
+
);
|
|
4633
|
+
if (pendingPeerGitProbe) {
|
|
4634
|
+
status.gitProbePending = true;
|
|
4635
|
+
status.health = 'unknown';
|
|
4636
|
+
}
|
|
4637
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
4638
|
+
status,
|
|
4639
|
+
node,
|
|
4640
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
4641
|
+
)) {
|
|
4642
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
4643
|
+
nodeStatuses.push(status);
|
|
4644
|
+
continue;
|
|
4645
|
+
}
|
|
4646
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
4647
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
4648
|
+
nodeStatuses.push(status);
|
|
4649
|
+
continue;
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
} else {
|
|
4653
|
+
try {
|
|
4654
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
4655
|
+
status.git = gitStatus;
|
|
4656
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
4657
|
+
if (gitStatus.isGitRepo) {
|
|
4658
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
4659
|
+
} else {
|
|
4660
|
+
status.health = 'degraded';
|
|
4661
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
4662
|
+
}
|
|
4663
|
+
} catch {
|
|
4664
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
4665
|
+
status.health = 'degraded';
|
|
4666
|
+
}
|
|
2950
4667
|
}
|
|
2951
4668
|
}
|
|
2952
4669
|
} else {
|
|
2953
4670
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
2954
4671
|
}
|
|
4672
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
2955
4673
|
nodeStatuses.push(status);
|
|
2956
4674
|
}
|
|
2957
4675
|
|
|
2958
|
-
|
|
4676
|
+
const statusResult = {
|
|
2959
4677
|
success: true,
|
|
2960
4678
|
meshId: mesh.id,
|
|
2961
4679
|
meshName: mesh.name,
|
|
2962
4680
|
repoIdentity: mesh.repoIdentity,
|
|
2963
4681
|
defaultBranch: mesh.defaultBranch,
|
|
4682
|
+
refreshedAt,
|
|
4683
|
+
meshHost,
|
|
4684
|
+
sourceOfTruth: {
|
|
4685
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
4686
|
+
? 'coordinator_inline_mesh_cache'
|
|
4687
|
+
: meshRecord?.source === 'local_config'
|
|
4688
|
+
? 'local_mesh_config'
|
|
4689
|
+
: 'inline_bootstrap_snapshot',
|
|
4690
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
4691
|
+
meshHost: {
|
|
4692
|
+
owner: 'mesh_host_daemon',
|
|
4693
|
+
localRole: meshHost.role,
|
|
4694
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
4695
|
+
hostNodeId: meshHost.hostNodeId,
|
|
4696
|
+
hostAddress: meshHost.hostAddress,
|
|
4697
|
+
},
|
|
4698
|
+
...(requireDirectPeerTruth ? {
|
|
4699
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
4700
|
+
directPeerTruth: {
|
|
4701
|
+
required: true,
|
|
4702
|
+
satisfied: directTruthSatisfied,
|
|
4703
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
4704
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
4705
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
4706
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
4707
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
4708
|
+
},
|
|
4709
|
+
} : {}),
|
|
4710
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
4711
|
+
},
|
|
2964
4712
|
nodes: nodeStatuses,
|
|
2965
4713
|
queue: { tasks: queue, summary: queueSummary },
|
|
2966
4714
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
2967
4715
|
};
|
|
4716
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, statusResult, refreshReason);
|
|
4717
|
+
logRepoMeshStatusDebug('return_live', {
|
|
4718
|
+
meshId,
|
|
4719
|
+
command: 'mesh_status',
|
|
4720
|
+
refreshRequested,
|
|
4721
|
+
refreshReason,
|
|
4722
|
+
meshSource: meshRecord.source,
|
|
4723
|
+
directTruth,
|
|
4724
|
+
summary: summarizeRepoMeshStatusDebug(rememberedStatus),
|
|
4725
|
+
});
|
|
4726
|
+
return rememberedStatus;
|
|
2968
4727
|
} catch (e: any) {
|
|
2969
4728
|
return { success: false, error: e.message };
|
|
2970
4729
|
}
|