@adhdev/daemon-core 0.9.82-rc.6 → 0.9.82-rc.60
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 +3518 -593
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3492 -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 +2172 -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,64 @@ 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
|
+
eventDelivery: {
|
|
929
|
+
pendingEvents: true;
|
|
930
|
+
ledger: true;
|
|
931
|
+
};
|
|
932
|
+
evidence: {
|
|
933
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
934
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
935
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
936
|
+
};
|
|
295
937
|
};
|
|
296
938
|
|
|
939
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
940
|
+
|
|
297
941
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
298
942
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
299
943
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
300
944
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
301
945
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
946
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
302
947
|
|
|
303
948
|
function truncateValidationOutput(value: unknown): string {
|
|
304
949
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -306,171 +951,114 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
306
951
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
307
952
|
}
|
|
308
953
|
|
|
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);
|
|
954
|
+
function recordMeshRefineStage(
|
|
955
|
+
stages: Array<Record<string, unknown>>,
|
|
956
|
+
stage: string,
|
|
957
|
+
status: MeshRefineStageStatus,
|
|
958
|
+
startedAt: number,
|
|
959
|
+
details?: Record<string, unknown>,
|
|
960
|
+
): void {
|
|
961
|
+
stages.push({
|
|
962
|
+
stage,
|
|
963
|
+
status,
|
|
964
|
+
durationMs: Date.now() - startedAt,
|
|
965
|
+
...(details || {}),
|
|
407
966
|
});
|
|
408
967
|
}
|
|
409
968
|
|
|
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);
|
|
969
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
970
|
+
const { execFileSync } = await import('node:child_process');
|
|
971
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
972
|
+
cwd,
|
|
973
|
+
encoding: 'utf8',
|
|
974
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
975
|
+
});
|
|
976
|
+
if (!diff.trim()) return '';
|
|
977
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
978
|
+
cwd,
|
|
979
|
+
input: diff,
|
|
980
|
+
encoding: 'utf8',
|
|
981
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
982
|
+
}).trim();
|
|
983
|
+
return patchId.split(/\s+/)[0] || '';
|
|
426
984
|
}
|
|
427
985
|
|
|
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;
|
|
986
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
987
|
+
repoRoot: string,
|
|
988
|
+
baseHead: string,
|
|
989
|
+
branchHead: string,
|
|
990
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
991
|
+
const startedAt = Date.now();
|
|
992
|
+
try {
|
|
993
|
+
const { execFileSync } = await import('node:child_process');
|
|
994
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
995
|
+
cwd: repoRoot,
|
|
996
|
+
encoding: 'utf8',
|
|
997
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
998
|
+
});
|
|
999
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
1000
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
1001
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
1002
|
+
if (!mergeBase || !mergedTree) {
|
|
1003
|
+
return {
|
|
1004
|
+
status: 'failed',
|
|
1005
|
+
equivalent: false,
|
|
1006
|
+
baseHead,
|
|
1007
|
+
branchHead,
|
|
1008
|
+
mergeBase: mergeBase || undefined,
|
|
1009
|
+
mergedTree: mergedTree || undefined,
|
|
1010
|
+
durationMs: Date.now() - startedAt,
|
|
1011
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
1012
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
1013
|
+
};
|
|
461
1014
|
}
|
|
1015
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
1016
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
1017
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
1018
|
+
return {
|
|
1019
|
+
status: equivalent ? 'passed' : 'failed',
|
|
1020
|
+
equivalent,
|
|
1021
|
+
baseHead,
|
|
1022
|
+
branchHead,
|
|
1023
|
+
mergeBase,
|
|
1024
|
+
mergedTree,
|
|
1025
|
+
expectedPatchId,
|
|
1026
|
+
actualPatchId,
|
|
1027
|
+
durationMs: Date.now() - startedAt,
|
|
1028
|
+
};
|
|
1029
|
+
} catch (e: any) {
|
|
1030
|
+
return {
|
|
1031
|
+
status: 'failed',
|
|
1032
|
+
equivalent: false,
|
|
1033
|
+
baseHead,
|
|
1034
|
+
branchHead,
|
|
1035
|
+
durationMs: Date.now() - startedAt,
|
|
1036
|
+
error: e?.message || String(e),
|
|
1037
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1038
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1039
|
+
};
|
|
462
1040
|
}
|
|
1041
|
+
}
|
|
463
1042
|
|
|
1043
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1044
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
464
1045
|
return {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1046
|
+
source: plan.source,
|
|
1047
|
+
sourceType: plan.sourceType,
|
|
1048
|
+
commands: plan.commands.map(command => ({
|
|
1049
|
+
displayCommand: command.displayCommand,
|
|
1050
|
+
category: command.category,
|
|
1051
|
+
source: command.source,
|
|
1052
|
+
cwd: command.cwd,
|
|
1053
|
+
timeoutMs: command.timeoutMs,
|
|
1054
|
+
})),
|
|
1055
|
+
unavailableReason: plan.unavailableReason,
|
|
1056
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1057
|
+
suggestions: plan.suggestions,
|
|
1058
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1059
|
+
note: plan.sourceType === 'unavailable'
|
|
1060
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1061
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
474
1062
|
};
|
|
475
1063
|
}
|
|
476
1064
|
|
|
@@ -478,7 +1066,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
478
1066
|
const { execFile } = await import('node:child_process');
|
|
479
1067
|
const { promisify } = await import('node:util');
|
|
480
1068
|
const execFileAsync = promisify(execFile);
|
|
481
|
-
const selection =
|
|
1069
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
482
1070
|
const summary: MeshRefineValidationSummary = {
|
|
483
1071
|
status: 'skipped',
|
|
484
1072
|
required: true,
|
|
@@ -487,22 +1075,28 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
487
1075
|
skippedReason: undefined,
|
|
488
1076
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
489
1077
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1078
|
+
configSource: selection.source,
|
|
1079
|
+
configSourceType: selection.sourceType,
|
|
1080
|
+
suggestions: selection.suggestions,
|
|
1081
|
+
suggestedConfig: selection.suggestedConfig,
|
|
490
1082
|
};
|
|
491
1083
|
|
|
492
1084
|
if (!selection.commands.length) {
|
|
493
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1085
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
494
1086
|
return summary;
|
|
495
1087
|
}
|
|
496
1088
|
|
|
497
1089
|
for (const candidate of selection.commands) {
|
|
498
1090
|
const startedAt = Date.now();
|
|
1091
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1092
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
499
1093
|
try {
|
|
500
1094
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
501
|
-
cwd
|
|
1095
|
+
cwd,
|
|
502
1096
|
encoding: 'utf8',
|
|
503
|
-
timeout
|
|
1097
|
+
timeout,
|
|
504
1098
|
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
505
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
1099
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
506
1100
|
});
|
|
507
1101
|
summary.commandsRun.push({
|
|
508
1102
|
command: candidate.command,
|
|
@@ -510,6 +1104,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
510
1104
|
displayCommand: candidate.displayCommand,
|
|
511
1105
|
category: candidate.category,
|
|
512
1106
|
source: candidate.source,
|
|
1107
|
+
cwd,
|
|
513
1108
|
passed: true,
|
|
514
1109
|
exitCode: 0,
|
|
515
1110
|
durationMs: Date.now() - startedAt,
|
|
@@ -523,6 +1118,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
523
1118
|
displayCommand: candidate.displayCommand,
|
|
524
1119
|
category: candidate.category,
|
|
525
1120
|
source: candidate.source,
|
|
1121
|
+
cwd,
|
|
526
1122
|
passed: false,
|
|
527
1123
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
528
1124
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
@@ -661,6 +1257,10 @@ export interface CommandRouterDeps {
|
|
|
661
1257
|
statusVersion?: string;
|
|
662
1258
|
/** Session host control plane */
|
|
663
1259
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
1260
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
1261
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1262
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
1263
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
664
1264
|
}
|
|
665
1265
|
|
|
666
1266
|
export interface CommandRouterResult {
|
|
@@ -772,42 +1372,259 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
772
1372
|
};
|
|
773
1373
|
}
|
|
774
1374
|
|
|
1375
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
1376
|
+
const raw = hostAddress.trim();
|
|
1377
|
+
if (!raw) throw new Error('hostAddress required');
|
|
1378
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
1379
|
+
url.pathname = '/api/v1/command';
|
|
1380
|
+
url.search = '';
|
|
1381
|
+
url.hash = '';
|
|
1382
|
+
return url.toString();
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
1386
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
1387
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
1388
|
+
? args.memberNode as Record<string, any>
|
|
1389
|
+
: null;
|
|
1390
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
1391
|
+
? (requestedNodeId
|
|
1392
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
1393
|
+
: mesh.nodes[0])
|
|
1394
|
+
: null;
|
|
1395
|
+
const source = explicit || configured;
|
|
1396
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
1397
|
+
? source.workspace.trim()
|
|
1398
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
1399
|
+
? args.workspace.trim()
|
|
1400
|
+
: process.cwd();
|
|
1401
|
+
if (!workspace) return null;
|
|
1402
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
1403
|
+
? source.id.trim()
|
|
1404
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
1405
|
+
? source.nodeId.trim()
|
|
1406
|
+
: undefined;
|
|
1407
|
+
return {
|
|
1408
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
1409
|
+
workspace,
|
|
1410
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
1411
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
1412
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
1413
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
1414
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
1415
|
+
role: 'member',
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
775
1419
|
export class DaemonCommandRouter {
|
|
776
1420
|
private deps: CommandRouterDeps;
|
|
777
1421
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
778
1422
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
779
1423
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
780
1424
|
private inlineMeshCache = new Map<string, any>();
|
|
1425
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
1426
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any }>();
|
|
1427
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
1428
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
1429
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
1430
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
781
1431
|
|
|
782
1432
|
constructor(deps: CommandRouterDeps) {
|
|
783
1433
|
this.deps = deps;
|
|
784
1434
|
}
|
|
785
1435
|
|
|
1436
|
+
private cloneJsonValue<T>(value: T): T {
|
|
1437
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
1438
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
1442
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
1443
|
+
const inlineNodesById = new Map<string, any>();
|
|
1444
|
+
for (const node of mesh.nodes) {
|
|
1445
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
1446
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
1447
|
+
}
|
|
1448
|
+
if (!inlineNodesById.size) return snapshot;
|
|
1449
|
+
|
|
1450
|
+
let changed = false;
|
|
1451
|
+
const unavailableNodeIds = new Set<string>();
|
|
1452
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
1453
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
1454
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
1455
|
+
const nodeId = readStringValue(entry);
|
|
1456
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
1460
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
1461
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
1462
|
+
if (!inlineNode) return statusNode;
|
|
1463
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
1464
|
+
if (!liveGit) return statusNode;
|
|
1465
|
+
const nextStatus = { ...statusNode };
|
|
1466
|
+
nextStatus.git = liveGit;
|
|
1467
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
1468
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
1469
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
1470
|
+
const connectionState = readStringValue(connection.state);
|
|
1471
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
1472
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
1473
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
1474
|
+
}
|
|
1475
|
+
delete nextStatus.gitProbePending;
|
|
1476
|
+
const error = readStringValue(nextStatus.error);
|
|
1477
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
1478
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
1479
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
1480
|
+
changed = true;
|
|
1481
|
+
return nextStatus;
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0)) return snapshot;
|
|
1485
|
+
const nextSourceOfTruth = {
|
|
1486
|
+
...sourceOfTruth,
|
|
1487
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
1488
|
+
directPeerTruth: {
|
|
1489
|
+
...directPeerTruth,
|
|
1490
|
+
satisfied: options?.requireDirectPeerTruth === true ? unavailableNodeIds.size === 0 : directPeerTruth.satisfied,
|
|
1491
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
1492
|
+
},
|
|
1493
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
1494
|
+
coordinatorOwnsLiveTruth: unavailableNodeIds.size === 0,
|
|
1495
|
+
currentStatus: unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
1496
|
+
} : {}),
|
|
1497
|
+
} : {}),
|
|
1498
|
+
};
|
|
1499
|
+
return {
|
|
1500
|
+
...snapshot,
|
|
1501
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 ? {
|
|
1502
|
+
success: false,
|
|
1503
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
1504
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
1505
|
+
} : {}),
|
|
1506
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
1507
|
+
nodes,
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
1512
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
1513
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
1514
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
1515
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
1516
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
1517
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
1518
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
1519
|
+
? snapshot.sourceOfTruth
|
|
1520
|
+
: {};
|
|
1521
|
+
snapshot.sourceOfTruth = {
|
|
1522
|
+
...sourceOfTruth,
|
|
1523
|
+
aggregateSnapshot: {
|
|
1524
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
1525
|
+
? sourceOfTruth.aggregateSnapshot
|
|
1526
|
+
: {}),
|
|
1527
|
+
owner: 'coordinator_daemon_memory',
|
|
1528
|
+
cached: true,
|
|
1529
|
+
source: 'memory',
|
|
1530
|
+
refreshReason: 'memory_cache_hit',
|
|
1531
|
+
ageMs,
|
|
1532
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
1533
|
+
returnedAt: new Date().toISOString(),
|
|
1534
|
+
},
|
|
1535
|
+
};
|
|
1536
|
+
return snapshot;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
1540
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
1541
|
+
const builtAt = Date.now();
|
|
1542
|
+
const next = this.cloneJsonValue(snapshot);
|
|
1543
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
1544
|
+
? next.sourceOfTruth
|
|
1545
|
+
: {};
|
|
1546
|
+
next.sourceOfTruth = {
|
|
1547
|
+
...sourceOfTruth,
|
|
1548
|
+
aggregateSnapshot: {
|
|
1549
|
+
owner: 'coordinator_daemon_memory',
|
|
1550
|
+
cached: false,
|
|
1551
|
+
source: 'live_refresh',
|
|
1552
|
+
refreshReason,
|
|
1553
|
+
ageMs: 0,
|
|
1554
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
1555
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
1556
|
+
},
|
|
1557
|
+
};
|
|
1558
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next) });
|
|
1559
|
+
return next;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
786
1562
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
787
1563
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
788
|
-
this.
|
|
789
|
-
return inlineMesh as any;
|
|
1564
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
790
1565
|
}
|
|
791
1566
|
return this.inlineMeshCache.get(meshId);
|
|
792
1567
|
}
|
|
793
1568
|
|
|
1569
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
1570
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
1571
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
1572
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
1573
|
+
if (cached) {
|
|
1574
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
1575
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
1576
|
+
return merged;
|
|
1577
|
+
}
|
|
1578
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
1579
|
+
return sanitizedInlineMesh as any;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
794
1582
|
private async getMeshForCommand(
|
|
795
1583
|
meshId: string,
|
|
796
1584
|
inlineMesh?: unknown,
|
|
797
1585
|
options?: { preferInline?: boolean },
|
|
798
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
1586
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
799
1587
|
const preferInline = options?.preferInline === true;
|
|
800
1588
|
if (preferInline) {
|
|
801
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
802
|
-
if (cached)
|
|
1589
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1590
|
+
if (cached) {
|
|
1591
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1592
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
1593
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
1594
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
1595
|
+
}
|
|
1596
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1597
|
+
}
|
|
1598
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1599
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1600
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
1601
|
+
}
|
|
803
1602
|
}
|
|
804
1603
|
try {
|
|
805
1604
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
806
1605
|
const mesh = getMesh(meshId);
|
|
807
|
-
if (mesh) return { mesh, inline: false };
|
|
1606
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
808
1607
|
} catch { /* fall through to inline cache */ }
|
|
809
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
810
|
-
|
|
1608
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1609
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1610
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1611
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
1615
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
1620
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
1621
|
+
const mesh = meshRecord?.mesh;
|
|
1622
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1623
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
1624
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
1625
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
1626
|
+
}
|
|
1627
|
+
return null;
|
|
811
1628
|
}
|
|
812
1629
|
|
|
813
1630
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -817,6 +1634,7 @@ export class DaemonCommandRouter {
|
|
|
817
1634
|
else mesh.nodes.push(node);
|
|
818
1635
|
mesh.updatedAt = new Date().toISOString();
|
|
819
1636
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1637
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
820
1638
|
}
|
|
821
1639
|
|
|
822
1640
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -826,6 +1644,7 @@ export class DaemonCommandRouter {
|
|
|
826
1644
|
mesh.nodes.splice(idx, 1);
|
|
827
1645
|
mesh.updatedAt = new Date().toISOString();
|
|
828
1646
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1647
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
829
1648
|
return true;
|
|
830
1649
|
}
|
|
831
1650
|
|
|
@@ -1104,6 +1923,7 @@ export class DaemonCommandRouter {
|
|
|
1104
1923
|
const deletedSessionIds: string[] = [];
|
|
1105
1924
|
const skippedSessionIds: string[] = [];
|
|
1106
1925
|
const skippedLiveSessionIds: string[] = [];
|
|
1926
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1107
1927
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1108
1928
|
const recordsRemainSessionIds: string[] = [];
|
|
1109
1929
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1138,6 +1958,12 @@ export class DaemonCommandRouter {
|
|
|
1138
1958
|
const completed = this.isCompletedHostedSession(record);
|
|
1139
1959
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1140
1960
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
1961
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
1962
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
1963
|
+
skippedSessionIds.push(sessionId);
|
|
1964
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1141
1967
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1142
1968
|
skippedSessionIds.push(sessionId);
|
|
1143
1969
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1207,6 +2033,7 @@ export class DaemonCommandRouter {
|
|
|
1207
2033
|
deletedSessionIds,
|
|
1208
2034
|
skippedSessionIds,
|
|
1209
2035
|
skippedLiveSessionIds,
|
|
2036
|
+
skippedCoordinatorSessionIds,
|
|
1210
2037
|
...(deleteUnsupported ? {
|
|
1211
2038
|
deleteUnsupported: true,
|
|
1212
2039
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1317,34 +2144,404 @@ export class DaemonCommandRouter {
|
|
|
1317
2144
|
return daemonResult;
|
|
1318
2145
|
}
|
|
1319
2146
|
|
|
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
|
-
});
|
|
2147
|
+
// 2. Delegate to DaemonCommandHandler
|
|
2148
|
+
const handlerResult = await this.deps.commandHandler.handle(cmd, normalizedArgs);
|
|
2149
|
+
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: handlerResult.success, durationMs: Date.now() - cmdStart });
|
|
2150
|
+
recordDebugTrace({
|
|
2151
|
+
interactionId,
|
|
2152
|
+
category: 'command',
|
|
2153
|
+
stage: 'completed',
|
|
2154
|
+
level: handlerResult.success ? 'info' : 'warn',
|
|
2155
|
+
payload: { cmd, source: logSource, success: handlerResult.success, durationMs: Date.now() - cmdStart },
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
// 3. Post-chat command callback
|
|
2159
|
+
if (CHAT_COMMANDS.includes(cmd) && this.deps.onPostChatCommand) {
|
|
2160
|
+
this.deps.onPostChatCommand();
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
return handlerResult;
|
|
2164
|
+
} catch (e: any) {
|
|
2165
|
+
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: false, error: e.message, durationMs: Date.now() - cmdStart });
|
|
2166
|
+
recordDebugTrace({
|
|
2167
|
+
interactionId,
|
|
2168
|
+
category: 'command',
|
|
2169
|
+
stage: 'failed',
|
|
2170
|
+
level: 'error',
|
|
2171
|
+
payload: { cmd, source: logSource, error: e?.message || String(e), durationMs: Date.now() - cmdStart },
|
|
2172
|
+
});
|
|
2173
|
+
throw e;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
2179
|
+
return `${meshId}:${nodeId}`;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
private buildRefineJobHandle(args: {
|
|
2183
|
+
meshId: string;
|
|
2184
|
+
nodeId: string;
|
|
2185
|
+
node?: any;
|
|
2186
|
+
status?: MeshRefineAsyncJobStatus;
|
|
2187
|
+
startedAt?: string;
|
|
2188
|
+
completedAt?: string;
|
|
2189
|
+
jobId?: string;
|
|
2190
|
+
interactionId?: string;
|
|
2191
|
+
}): MeshRefineJobHandle {
|
|
2192
|
+
return {
|
|
2193
|
+
success: true,
|
|
2194
|
+
async: true,
|
|
2195
|
+
status: args.status || 'accepted',
|
|
2196
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
2197
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
2198
|
+
meshId: args.meshId,
|
|
2199
|
+
nodeId: args.nodeId,
|
|
2200
|
+
targetNodeId: args.nodeId,
|
|
2201
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
2202
|
+
workspace: readStringValue(args.node?.workspace),
|
|
2203
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
2204
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
2205
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
2206
|
+
evidence: {
|
|
2207
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
2208
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
2209
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
2210
|
+
},
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
2215
|
+
queuePendingMeshCoordinatorEvent({
|
|
2216
|
+
event,
|
|
2217
|
+
meshId: handle.meshId,
|
|
2218
|
+
nodeLabel: handle.targetNodeId,
|
|
2219
|
+
nodeId: handle.targetNodeId,
|
|
2220
|
+
workspace: handle.workspace,
|
|
2221
|
+
metadataEvent: {
|
|
2222
|
+
source: 'refine_mesh_node_async_job',
|
|
2223
|
+
jobId: handle.jobId,
|
|
2224
|
+
interactionId: handle.interactionId,
|
|
2225
|
+
meshId: handle.meshId,
|
|
2226
|
+
nodeId: handle.targetNodeId,
|
|
2227
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2228
|
+
workspace: handle.workspace,
|
|
2229
|
+
status: handle.status,
|
|
2230
|
+
startedAt: handle.startedAt,
|
|
2231
|
+
completedAt: handle.completedAt,
|
|
2232
|
+
...(result ? { result } : {}),
|
|
2233
|
+
},
|
|
2234
|
+
queuedAt: Date.now(),
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
2239
|
+
try {
|
|
2240
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2241
|
+
appendLedgerEntry(handle.meshId, {
|
|
2242
|
+
kind,
|
|
2243
|
+
nodeId: handle.targetNodeId,
|
|
2244
|
+
payload: {
|
|
2245
|
+
source: 'refine_mesh_node_async_job',
|
|
2246
|
+
refineJob: {
|
|
2247
|
+
jobId: handle.jobId,
|
|
2248
|
+
interactionId: handle.interactionId,
|
|
2249
|
+
status: handle.status,
|
|
2250
|
+
meshId: handle.meshId,
|
|
2251
|
+
nodeId: handle.targetNodeId,
|
|
2252
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2253
|
+
workspace: handle.workspace,
|
|
2254
|
+
startedAt: handle.startedAt,
|
|
2255
|
+
completedAt: handle.completedAt,
|
|
2256
|
+
},
|
|
2257
|
+
async: true,
|
|
2258
|
+
...(result ? {
|
|
2259
|
+
success: result.success === true,
|
|
2260
|
+
result,
|
|
2261
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
2262
|
+
} : {}),
|
|
2263
|
+
},
|
|
2264
|
+
});
|
|
2265
|
+
} catch (e: any) {
|
|
2266
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2271
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
2272
|
+
try {
|
|
2273
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2274
|
+
const mesh = meshRecord?.mesh;
|
|
2275
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2276
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
2277
|
+
|
|
2278
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
2279
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const sourceNode = node.clonedFromNodeId
|
|
2283
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2284
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2285
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2286
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
2287
|
+
|
|
2288
|
+
const { execFile } = await import('node:child_process');
|
|
2289
|
+
const { promisify } = await import('node:util');
|
|
2290
|
+
const execFileAsync = promisify(execFile);
|
|
2291
|
+
|
|
2292
|
+
const resolveStarted = Date.now();
|
|
2293
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2294
|
+
const branch = branchStdout.trim();
|
|
2295
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
2296
|
+
|
|
2297
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2298
|
+
const baseBranch = baseBranchStdout.trim();
|
|
2299
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2300
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
2301
|
+
const baseHead = baseHeadStdout.trim();
|
|
2302
|
+
const branchHead = branchHeadStdout.trim();
|
|
2303
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
2304
|
+
|
|
2305
|
+
const validationStarted = Date.now();
|
|
2306
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2307
|
+
recordMeshRefineStage(
|
|
2308
|
+
refineStages,
|
|
2309
|
+
'validation',
|
|
2310
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
2311
|
+
validationStarted,
|
|
2312
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
2313
|
+
);
|
|
2314
|
+
if (validationSummary.status === 'failed') {
|
|
2315
|
+
return {
|
|
2316
|
+
success: false,
|
|
2317
|
+
code: 'validation_failed',
|
|
2318
|
+
convergenceStatus: 'blocked_review',
|
|
2319
|
+
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2320
|
+
branch,
|
|
2321
|
+
into: baseBranch,
|
|
2322
|
+
validationSummary,
|
|
2323
|
+
refineStages,
|
|
2324
|
+
finalBranchConvergenceState: {
|
|
2325
|
+
branch,
|
|
2326
|
+
baseBranch,
|
|
2327
|
+
merged: false,
|
|
2328
|
+
removed: false,
|
|
2329
|
+
validation: 'failed',
|
|
2330
|
+
status: 'blocked_review',
|
|
2331
|
+
},
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
if (validationSummary.status === 'skipped') {
|
|
2335
|
+
return {
|
|
2336
|
+
success: false,
|
|
2337
|
+
code: 'validation_unavailable',
|
|
2338
|
+
convergenceStatus: 'blocked_review',
|
|
2339
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2340
|
+
branch,
|
|
2341
|
+
into: baseBranch,
|
|
2342
|
+
validationSummary,
|
|
2343
|
+
refineStages,
|
|
2344
|
+
finalBranchConvergenceState: {
|
|
2345
|
+
branch,
|
|
2346
|
+
baseBranch,
|
|
2347
|
+
merged: false,
|
|
2348
|
+
removed: false,
|
|
2349
|
+
validation: 'unavailable',
|
|
2350
|
+
status: 'blocked_review',
|
|
2351
|
+
},
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
const patchEquivalenceStarted = Date.now();
|
|
2356
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
2357
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
2358
|
+
equivalent: patchEquivalence.equivalent,
|
|
2359
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
2360
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
2361
|
+
error: patchEquivalence.error,
|
|
2362
|
+
});
|
|
2363
|
+
if (!patchEquivalence.equivalent) {
|
|
2364
|
+
return {
|
|
2365
|
+
success: false,
|
|
2366
|
+
code: 'patch_equivalence_failed',
|
|
2367
|
+
convergenceStatus: 'blocked_review',
|
|
2368
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
2369
|
+
branch,
|
|
2370
|
+
into: baseBranch,
|
|
2371
|
+
validationSummary,
|
|
2372
|
+
patchEquivalence,
|
|
2373
|
+
refineStages,
|
|
2374
|
+
finalBranchConvergenceState: {
|
|
2375
|
+
branch,
|
|
2376
|
+
baseBranch,
|
|
2377
|
+
merged: false,
|
|
2378
|
+
removed: false,
|
|
2379
|
+
validation: 'passed',
|
|
2380
|
+
patchEquivalence: 'failed',
|
|
2381
|
+
status: 'blocked_review',
|
|
2382
|
+
},
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
2387
|
+
const mergeStarted = Date.now();
|
|
2388
|
+
try {
|
|
2389
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2390
|
+
mergeResult = {
|
|
2391
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
2392
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
2393
|
+
durationMs: Date.now() - mergeStarted,
|
|
2394
|
+
};
|
|
2395
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
2396
|
+
} catch (e: any) {
|
|
2397
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
2398
|
+
error: e?.message || String(e),
|
|
2399
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
2400
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
2401
|
+
});
|
|
2402
|
+
return {
|
|
2403
|
+
success: false,
|
|
2404
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2405
|
+
validationSummary,
|
|
2406
|
+
patchEquivalence,
|
|
2407
|
+
refineStages,
|
|
2408
|
+
finalBranchConvergenceState: {
|
|
2409
|
+
branch,
|
|
2410
|
+
baseBranch,
|
|
2411
|
+
merged: false,
|
|
2412
|
+
removed: false,
|
|
2413
|
+
validation: 'passed',
|
|
2414
|
+
patchEquivalence: 'passed',
|
|
2415
|
+
status: 'not_mergeable',
|
|
2416
|
+
},
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
const cleanupStarted = Date.now();
|
|
2421
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
2422
|
+
meshId,
|
|
2423
|
+
nodeId,
|
|
2424
|
+
sessionCleanupMode: 'preserve',
|
|
2425
|
+
inlineMesh: args?.inlineMesh,
|
|
2426
|
+
});
|
|
2427
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
2428
|
+
removed: removeResult?.removed,
|
|
2429
|
+
code: removeResult?.code,
|
|
2430
|
+
error: removeResult?.error,
|
|
2431
|
+
});
|
|
2432
|
+
|
|
2433
|
+
let ledgerError: string | undefined;
|
|
2434
|
+
const ledgerStarted = Date.now();
|
|
2435
|
+
try {
|
|
2436
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2437
|
+
appendLedgerEntry(meshId, {
|
|
2438
|
+
kind: 'node_removed',
|
|
2439
|
+
nodeId,
|
|
2440
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
2441
|
+
});
|
|
2442
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
2443
|
+
} catch (e: any) {
|
|
2444
|
+
ledgerError = e?.message || String(e);
|
|
2445
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
const finalBranchConvergenceState = {
|
|
2449
|
+
branch: baseBranch,
|
|
2450
|
+
mergedBranch: branch,
|
|
2451
|
+
baseBranch,
|
|
2452
|
+
merged: true,
|
|
2453
|
+
removed: removeResult?.success !== false,
|
|
2454
|
+
validation: 'passed',
|
|
2455
|
+
patchEquivalence: 'passed',
|
|
2456
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2457
|
+
};
|
|
1330
2458
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2459
|
+
if (removeResult?.success === false) {
|
|
2460
|
+
return {
|
|
2461
|
+
success: false,
|
|
2462
|
+
code: 'cleanup_failed',
|
|
2463
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
2464
|
+
merged: true,
|
|
2465
|
+
branch,
|
|
2466
|
+
into: baseBranch,
|
|
2467
|
+
removeResult,
|
|
2468
|
+
validationSummary,
|
|
2469
|
+
patchEquivalence,
|
|
2470
|
+
mergeResult,
|
|
2471
|
+
refineStages,
|
|
2472
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2473
|
+
finalBranchConvergenceState,
|
|
2474
|
+
};
|
|
1334
2475
|
}
|
|
1335
2476
|
|
|
1336
|
-
return
|
|
2477
|
+
return {
|
|
2478
|
+
success: true,
|
|
2479
|
+
merged: true,
|
|
2480
|
+
branch,
|
|
2481
|
+
into: baseBranch,
|
|
2482
|
+
removeResult,
|
|
2483
|
+
validationSummary,
|
|
2484
|
+
patchEquivalence,
|
|
2485
|
+
mergeResult,
|
|
2486
|
+
refineStages,
|
|
2487
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2488
|
+
finalBranchConvergenceState,
|
|
2489
|
+
};
|
|
1337
2490
|
} catch (e: any) {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
2491
|
+
return { success: false, error: e.message, refineStages };
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
2496
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
2497
|
+
let result: Record<string, unknown>;
|
|
2498
|
+
try {
|
|
2499
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
2500
|
+
} catch (e: any) {
|
|
2501
|
+
result = { success: false, error: e?.message || String(e) };
|
|
1347
2502
|
}
|
|
2503
|
+
const completedAt = new Date().toISOString();
|
|
2504
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
2505
|
+
meshId: handle.meshId,
|
|
2506
|
+
nodeId: handle.targetNodeId,
|
|
2507
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
2508
|
+
startedAt: handle.startedAt,
|
|
2509
|
+
completedAt,
|
|
2510
|
+
jobId: handle.jobId,
|
|
2511
|
+
interactionId: handle.interactionId,
|
|
2512
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
2513
|
+
});
|
|
2514
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
2515
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
2516
|
+
this.runningRefineJobs.delete(key);
|
|
2517
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
2518
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
2519
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2523
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
2524
|
+
const running = this.runningRefineJobs.get(key);
|
|
2525
|
+
if (running) return { ...running, duplicate: true };
|
|
2526
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
2527
|
+
if (terminal) return { ...terminal, duplicate: true };
|
|
2528
|
+
|
|
2529
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2530
|
+
const mesh = meshRecord?.mesh;
|
|
2531
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2532
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
2533
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
2534
|
+
|
|
2535
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node });
|
|
2536
|
+
this.runningRefineJobs.set(key, handle);
|
|
2537
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
2538
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
2539
|
+
|
|
2540
|
+
setImmediate(() => {
|
|
2541
|
+
void this.finishMeshRefineJob(handle, args);
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
return handle;
|
|
1348
2545
|
}
|
|
1349
2546
|
|
|
1350
2547
|
// ─── Daemon-level command core ───────────────────
|
|
@@ -1361,7 +2558,8 @@ export class DaemonCommandRouter {
|
|
|
1361
2558
|
}
|
|
1362
2559
|
|
|
1363
2560
|
case 'get_pending_mesh_events': {
|
|
1364
|
-
const
|
|
2561
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2562
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1365
2563
|
return { success: true, events };
|
|
1366
2564
|
}
|
|
1367
2565
|
|
|
@@ -1966,15 +3164,44 @@ export class DaemonCommandRouter {
|
|
|
1966
3164
|
case 'get_mesh': {
|
|
1967
3165
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1968
3166
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
3167
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3168
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
3169
|
+
|
|
3170
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
3171
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
3172
|
+
mesh: meshRecord.mesh,
|
|
3173
|
+
meshSource: meshRecord.source,
|
|
3174
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
3175
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
3176
|
+
localMachineId: loadConfig().machineId || '',
|
|
3177
|
+
});
|
|
3178
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
3179
|
+
const sourceOfTruth = {
|
|
3180
|
+
membership: meshRecord.source === 'inline_cache'
|
|
3181
|
+
? 'coordinator_inline_mesh_cache'
|
|
3182
|
+
: meshRecord.source === 'local_config'
|
|
3183
|
+
? 'local_mesh_config'
|
|
3184
|
+
: 'inline_bootstrap_snapshot',
|
|
3185
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
3186
|
+
directPeerTruth: {
|
|
3187
|
+
required: requireDirectPeerTruth,
|
|
3188
|
+
satisfied: directTruthSatisfied,
|
|
3189
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
3190
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
3191
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
3192
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
3193
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
3194
|
+
},
|
|
3195
|
+
};
|
|
3196
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
3197
|
+
return {
|
|
3198
|
+
success: false,
|
|
3199
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
3200
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
3201
|
+
sourceOfTruth,
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
1978
3205
|
}
|
|
1979
3206
|
|
|
1980
3207
|
case 'create_mesh': {
|
|
@@ -1985,7 +3212,10 @@ export class DaemonCommandRouter {
|
|
|
1985
3212
|
if (!name) return { success: false, error: 'name required' };
|
|
1986
3213
|
try {
|
|
1987
3214
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
1988
|
-
const
|
|
3215
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
3216
|
+
? args.meshHost
|
|
3217
|
+
: undefined;
|
|
3218
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
1989
3219
|
return { success: true, mesh };
|
|
1990
3220
|
} catch (e: any) {
|
|
1991
3221
|
return { success: false, error: e.message };
|
|
@@ -2002,16 +3232,237 @@ export class DaemonCommandRouter {
|
|
|
2002
3232
|
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
2003
3233
|
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
2004
3234
|
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
3235
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
2005
3236
|
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
2006
3237
|
const mesh = updateMesh(meshId, patch as any);
|
|
2007
3238
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2008
3239
|
this.inlineMeshCache.set(meshId, mesh);
|
|
3240
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2009
3241
|
return { success: true, mesh };
|
|
2010
3242
|
} catch (e: any) {
|
|
2011
3243
|
return { success: false, error: e.message };
|
|
2012
3244
|
}
|
|
2013
3245
|
}
|
|
2014
3246
|
|
|
3247
|
+
case 'get_mesh_host_pairing': {
|
|
3248
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3249
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3250
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3251
|
+
const mesh = meshRecord?.mesh;
|
|
3252
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3253
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3254
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
3255
|
+
return {
|
|
3256
|
+
success: true,
|
|
3257
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
3258
|
+
meshId,
|
|
3259
|
+
hostAddress: meshHost.hostAddress,
|
|
3260
|
+
meshHost,
|
|
3261
|
+
manualPairing: {
|
|
3262
|
+
status: pairingStatus,
|
|
3263
|
+
joinImplemented: true,
|
|
3264
|
+
protocol: 'standalone_command_direct_v1',
|
|
3265
|
+
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.',
|
|
3266
|
+
},
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
case 'configure_mesh_host_pairing': {
|
|
3271
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3272
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
3273
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3274
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3275
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
3276
|
+
try {
|
|
3277
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
3278
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
3279
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
3280
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
3281
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
3282
|
+
return {
|
|
3283
|
+
success: true,
|
|
3284
|
+
code: 'mesh_host_pairing_pending',
|
|
3285
|
+
meshId,
|
|
3286
|
+
hostAddress: configured.hostAddress,
|
|
3287
|
+
meshHost,
|
|
3288
|
+
manualPairing: {
|
|
3289
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
3290
|
+
joinImplemented: true,
|
|
3291
|
+
protocol: 'standalone_command_direct_v1',
|
|
3292
|
+
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.',
|
|
3293
|
+
},
|
|
3294
|
+
};
|
|
3295
|
+
} catch (e: any) {
|
|
3296
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
case 'create_mesh_host_pairing_token': {
|
|
3301
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3302
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3303
|
+
try {
|
|
3304
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
3305
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
3306
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
3307
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
3308
|
+
});
|
|
3309
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
3310
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
3311
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3312
|
+
return {
|
|
3313
|
+
success: true,
|
|
3314
|
+
code: 'mesh_host_pairing_token_created',
|
|
3315
|
+
meshId,
|
|
3316
|
+
token: created.token,
|
|
3317
|
+
tokenId: created.tokenId,
|
|
3318
|
+
expiresAt: created.expiresAt,
|
|
3319
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
3320
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
3321
|
+
};
|
|
3322
|
+
} catch (e: any) {
|
|
3323
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
case 'apply_mesh_host_join': {
|
|
3328
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3329
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3330
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
3331
|
+
? args.memberNode
|
|
3332
|
+
: null;
|
|
3333
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3334
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
3335
|
+
try {
|
|
3336
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
3337
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
3338
|
+
token,
|
|
3339
|
+
memberNode: memberNode as any,
|
|
3340
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
3341
|
+
});
|
|
3342
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
3343
|
+
if (!applied.accepted) {
|
|
3344
|
+
return {
|
|
3345
|
+
success: false,
|
|
3346
|
+
code: 'mesh_host_join_rejected',
|
|
3347
|
+
meshId,
|
|
3348
|
+
tokenId: applied.tokenId,
|
|
3349
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
3350
|
+
error: applied.reason,
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
3354
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3355
|
+
try {
|
|
3356
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3357
|
+
appendLedgerEntry(meshId, {
|
|
3358
|
+
kind: 'node_joined',
|
|
3359
|
+
nodeId: applied.node.id,
|
|
3360
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
3361
|
+
});
|
|
3362
|
+
} catch { /* ledger append is best-effort */ }
|
|
3363
|
+
return {
|
|
3364
|
+
success: true,
|
|
3365
|
+
code: 'mesh_host_join_accepted',
|
|
3366
|
+
meshId,
|
|
3367
|
+
node: applied.node,
|
|
3368
|
+
tokenId: applied.tokenId,
|
|
3369
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
3370
|
+
};
|
|
3371
|
+
} catch (e: any) {
|
|
3372
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
case 'join_mesh_host_pairing': {
|
|
3377
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3378
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3379
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3380
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
3381
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3382
|
+
const mesh = meshRecord?.mesh;
|
|
3383
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3384
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3385
|
+
if (meshHost.role !== 'member') {
|
|
3386
|
+
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.' };
|
|
3387
|
+
}
|
|
3388
|
+
try {
|
|
3389
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
3390
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
3391
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
3392
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
3393
|
+
}
|
|
3394
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
3395
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
3396
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
3397
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
3398
|
+
? args.hostDaemonId.trim()
|
|
3399
|
+
: meshHost.hostDaemonId;
|
|
3400
|
+
let hostResult: any;
|
|
3401
|
+
let transport: string;
|
|
3402
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
3403
|
+
transport = 'mesh_command_dispatch';
|
|
3404
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
3405
|
+
meshId: hostMeshId,
|
|
3406
|
+
token,
|
|
3407
|
+
memberMeshId: meshId,
|
|
3408
|
+
memberNode,
|
|
3409
|
+
});
|
|
3410
|
+
} else if (meshHost.hostAddress) {
|
|
3411
|
+
transport = 'standalone_http_command';
|
|
3412
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
3413
|
+
const response = await fetch(commandUrl, {
|
|
3414
|
+
method: 'POST',
|
|
3415
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3416
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
3417
|
+
});
|
|
3418
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
3419
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
3420
|
+
} else {
|
|
3421
|
+
return {
|
|
3422
|
+
success: false,
|
|
3423
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
3424
|
+
meshId,
|
|
3425
|
+
meshHost,
|
|
3426
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
if (!hostResult?.success) {
|
|
3430
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
3431
|
+
}
|
|
3432
|
+
const joined = meshRecord.inline
|
|
3433
|
+
? null
|
|
3434
|
+
: markMeshHostPairingJoined(meshId, {
|
|
3435
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3436
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
3437
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
3438
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
3439
|
+
});
|
|
3440
|
+
if (joined) {
|
|
3441
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
3442
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3443
|
+
}
|
|
3444
|
+
return {
|
|
3445
|
+
success: true,
|
|
3446
|
+
code: 'mesh_host_join_applied',
|
|
3447
|
+
meshId,
|
|
3448
|
+
hostMeshId,
|
|
3449
|
+
transport,
|
|
3450
|
+
node: hostResult.node,
|
|
3451
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3452
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
3453
|
+
hostResult,
|
|
3454
|
+
manualPairing: {
|
|
3455
|
+
status: 'paired',
|
|
3456
|
+
joinImplemented: true,
|
|
3457
|
+
protocol: 'standalone_command_direct_v1',
|
|
3458
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
3459
|
+
},
|
|
3460
|
+
};
|
|
3461
|
+
} catch (e: any) {
|
|
3462
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
|
|
2015
3466
|
case 'delete_mesh': {
|
|
2016
3467
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2017
3468
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -2105,6 +3556,8 @@ export class DaemonCommandRouter {
|
|
|
2105
3556
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2106
3557
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2107
3558
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3559
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
3560
|
+
if (ownerFailure) return ownerFailure;
|
|
2108
3561
|
try {
|
|
2109
3562
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2110
3563
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -2120,6 +3573,8 @@ export class DaemonCommandRouter {
|
|
|
2120
3573
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2121
3574
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2122
3575
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3576
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
3577
|
+
if (ownerFailure) return ownerFailure;
|
|
2123
3578
|
try {
|
|
2124
3579
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2125
3580
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -2141,6 +3596,8 @@ export class DaemonCommandRouter {
|
|
|
2141
3596
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2142
3597
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2143
3598
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
3599
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
3600
|
+
if (ownerFailure) return ownerFailure;
|
|
2144
3601
|
try {
|
|
2145
3602
|
const { addNode } = await import('../config/mesh-config.js');
|
|
2146
3603
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -2151,7 +3608,8 @@ export class DaemonCommandRouter {
|
|
|
2151
3608
|
...(readOnly ? { readOnly: true } : {}),
|
|
2152
3609
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
2153
3610
|
};
|
|
2154
|
-
const
|
|
3611
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
3612
|
+
const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}), ...(role ? { role } : {}) });
|
|
2155
3613
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
2156
3614
|
return { success: true, node };
|
|
2157
3615
|
} catch (e: any) {
|
|
@@ -2163,6 +3621,8 @@ export class DaemonCommandRouter {
|
|
|
2163
3621
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2164
3622
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2165
3623
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3624
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
3625
|
+
if (ownerFailure) return ownerFailure;
|
|
2166
3626
|
try {
|
|
2167
3627
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2168
3628
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2191,6 +3651,8 @@ export class DaemonCommandRouter {
|
|
|
2191
3651
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2192
3652
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2193
3653
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3654
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
3655
|
+
if (ownerFailure) return ownerFailure;
|
|
2194
3656
|
try {
|
|
2195
3657
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2196
3658
|
const mesh = meshRecord?.mesh;
|
|
@@ -2216,131 +3678,91 @@ export class DaemonCommandRouter {
|
|
|
2216
3678
|
}
|
|
2217
3679
|
}
|
|
2218
3680
|
|
|
2219
|
-
case '
|
|
3681
|
+
case 'get_mesh_refine_config_schema': {
|
|
3682
|
+
return {
|
|
3683
|
+
success: true,
|
|
3684
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
3685
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
3686
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
3687
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
case 'validate_mesh_refine_config': {
|
|
3692
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
3693
|
+
const mesh = args?.inlineMesh || {};
|
|
3694
|
+
const loaded = args?.config !== undefined
|
|
3695
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
3696
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
3697
|
+
const validation = loaded.config
|
|
3698
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
3699
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
3700
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
case 'suggest_mesh_refine_config': {
|
|
3704
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
3705
|
+
const mesh = args?.inlineMesh || {};
|
|
3706
|
+
return {
|
|
3707
|
+
success: true,
|
|
3708
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
3709
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
case 'plan_mesh_refine_node': {
|
|
2220
3714
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2221
3715
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2222
3716
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2223
|
-
|
|
3717
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3718
|
+
const mesh = meshRecord?.mesh;
|
|
3719
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3720
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
3721
|
+
return {
|
|
3722
|
+
success: true,
|
|
3723
|
+
dryRun: true,
|
|
3724
|
+
nodeId,
|
|
3725
|
+
workspace: node.workspace,
|
|
3726
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
3727
|
+
mergeWillRun: false,
|
|
3728
|
+
cleanupWillRun: false,
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
case 'fast_forward_mesh_node': {
|
|
3733
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3734
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3735
|
+
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
3736
|
+
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
3737
|
+
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
3738
|
+
: undefined;
|
|
3739
|
+
if (!workspace && meshId && nodeId) {
|
|
2224
3740
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2225
3741
|
const mesh = meshRecord?.mesh;
|
|
2226
3742
|
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
|
-
};
|
|
3743
|
+
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
3744
|
+
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
3745
|
+
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
2306
3746
|
}
|
|
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
3747
|
}
|
|
3748
|
+
const result = await (fastForwardMeshNode({
|
|
3749
|
+
meshId: meshId || undefined,
|
|
3750
|
+
nodeId: nodeId || undefined,
|
|
3751
|
+
workspace,
|
|
3752
|
+
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
3753
|
+
execute: args?.execute === true,
|
|
3754
|
+
dryRun: args?.dryRun === true,
|
|
3755
|
+
updateSubmodules: args?.updateSubmodules === true,
|
|
3756
|
+
submoduleIgnorePaths,
|
|
3757
|
+
}) as Promise<unknown>);
|
|
3758
|
+
return result as CommandRouterResult;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
case 'refine_mesh_node': {
|
|
3762
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3763
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3764
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3765
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
2344
3766
|
}
|
|
2345
3767
|
|
|
2346
3768
|
case 'remove_mesh_node': {
|
|
@@ -2384,6 +3806,7 @@ export class DaemonCommandRouter {
|
|
|
2384
3806
|
} else {
|
|
2385
3807
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2386
3808
|
removed = removeNode(meshId, nodeId);
|
|
3809
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2387
3810
|
}
|
|
2388
3811
|
|
|
2389
3812
|
// Record in task ledger
|
|
@@ -2421,6 +3844,8 @@ export class DaemonCommandRouter {
|
|
|
2421
3844
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2422
3845
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2423
3846
|
if (!branch) return { success: false, error: 'branch required' };
|
|
3847
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
3848
|
+
if (ownerFailure) return ownerFailure;
|
|
2424
3849
|
|
|
2425
3850
|
try {
|
|
2426
3851
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2469,6 +3894,7 @@ export class DaemonCommandRouter {
|
|
|
2469
3894
|
policy: { ...(sourceNode.policy || {}) },
|
|
2470
3895
|
});
|
|
2471
3896
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
3897
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2472
3898
|
}
|
|
2473
3899
|
|
|
2474
3900
|
// Initialize submodules if policy allows (default: true)
|
|
@@ -2510,6 +3936,8 @@ export class DaemonCommandRouter {
|
|
|
2510
3936
|
case 'trigger_mesh_queue': {
|
|
2511
3937
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2512
3938
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3939
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
3940
|
+
if (ownerFailure) return ownerFailure;
|
|
2513
3941
|
try {
|
|
2514
3942
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2515
3943
|
if (meshId) {
|
|
@@ -2541,6 +3969,15 @@ export class DaemonCommandRouter {
|
|
|
2541
3969
|
mesh = getMesh(meshId);
|
|
2542
3970
|
}
|
|
2543
3971
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3972
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3973
|
+
if (!meshHost.canOwnCoordinator) {
|
|
3974
|
+
return {
|
|
3975
|
+
success: false,
|
|
3976
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
3977
|
+
meshId,
|
|
3978
|
+
cliType,
|
|
3979
|
+
};
|
|
3980
|
+
}
|
|
2544
3981
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2545
3982
|
|
|
2546
3983
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2560,7 +3997,16 @@ export class DaemonCommandRouter {
|
|
|
2560
3997
|
cliType,
|
|
2561
3998
|
};
|
|
2562
3999
|
}
|
|
2563
|
-
const
|
|
4000
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
4001
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
4002
|
+
: [];
|
|
4003
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
4004
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4005
|
+
meshId,
|
|
4006
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
4007
|
+
liveSessionRecords: liveMeshSessions,
|
|
4008
|
+
allowCoordinatorSession: true,
|
|
4009
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2564
4010
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2565
4011
|
if (!cliType) {
|
|
2566
4012
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -2905,6 +4351,27 @@ export class DaemonCommandRouter {
|
|
|
2905
4351
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2906
4352
|
const mesh = meshRecord?.mesh;
|
|
2907
4353
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4354
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4355
|
+
|
|
4356
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
4357
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
4358
|
+
if (!refreshRequested) {
|
|
4359
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
4360
|
+
if (cachedStatus) {
|
|
4361
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
4362
|
+
meshId,
|
|
4363
|
+
command: 'mesh_status',
|
|
4364
|
+
refreshRequested,
|
|
4365
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
4366
|
+
});
|
|
4367
|
+
return cachedStatus;
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
const refreshReason = refreshRequested
|
|
4371
|
+
? 'explicit_refresh'
|
|
4372
|
+
: hadAggregateCache
|
|
4373
|
+
? 'stale_pending_cache_refresh'
|
|
4374
|
+
: 'cold_cache_miss';
|
|
2908
4375
|
|
|
2909
4376
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
2910
4377
|
const queue = getQueue(meshId);
|
|
@@ -2913,58 +4380,344 @@ export class DaemonCommandRouter {
|
|
|
2913
4380
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2914
4381
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2915
4382
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
2916
|
-
|
|
4383
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
4384
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
4385
|
+
: [];
|
|
4386
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
4387
|
+
|
|
4388
|
+
const localMachineId = loadConfig().machineId || '';
|
|
4389
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
4390
|
+
const directTruth = requireDirectPeerTruth
|
|
4391
|
+
? await hydrateInlineMeshDirectTruth({
|
|
4392
|
+
mesh,
|
|
4393
|
+
meshSource: meshRecord.source,
|
|
4394
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4395
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
4396
|
+
localMachineId,
|
|
4397
|
+
})
|
|
4398
|
+
: {
|
|
4399
|
+
directEvidenceCount: 0,
|
|
4400
|
+
localConfirmedCount: 0,
|
|
4401
|
+
peerAttemptedCount: 0,
|
|
4402
|
+
peerConfirmedCount: 0,
|
|
4403
|
+
unavailableNodeIds: [] as string[],
|
|
4404
|
+
};
|
|
4405
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
4406
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
4407
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
4408
|
+
&& !refreshRequested
|
|
4409
|
+
&& directTruth.directEvidenceCount > 0
|
|
4410
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
4411
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
4412
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
4413
|
+
: directTruth;
|
|
4414
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
4415
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && effectiveDirectTruth.unavailableNodeIds.length === 0);
|
|
4416
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4417
|
+
const failureResult = {
|
|
4418
|
+
success: false,
|
|
4419
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
4420
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
4421
|
+
sourceOfTruth: {
|
|
4422
|
+
membership: meshRecord.source === 'inline_cache'
|
|
4423
|
+
? 'coordinator_inline_mesh_cache'
|
|
4424
|
+
: meshRecord.source === 'local_config'
|
|
4425
|
+
? 'local_mesh_config'
|
|
4426
|
+
: 'inline_bootstrap_snapshot',
|
|
4427
|
+
coordinatorOwnsLiveTruth: false,
|
|
4428
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
4429
|
+
directPeerTruth: {
|
|
4430
|
+
required: true,
|
|
4431
|
+
satisfied: false,
|
|
4432
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4433
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4434
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4435
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4436
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4437
|
+
},
|
|
4438
|
+
},
|
|
4439
|
+
};
|
|
4440
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
4441
|
+
meshId,
|
|
4442
|
+
command: 'mesh_status',
|
|
4443
|
+
refreshRequested,
|
|
4444
|
+
meshSource: meshRecord.source,
|
|
4445
|
+
directTruth,
|
|
4446
|
+
});
|
|
4447
|
+
return failureResult;
|
|
4448
|
+
}
|
|
4449
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
4450
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
4451
|
+
mesh.coordinator?.preferredNodeId,
|
|
4452
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
4453
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
4454
|
+
);
|
|
4455
|
+
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
4456
|
+
? selectedCoordinatorNodeId
|
|
4457
|
+
: undefined;
|
|
4458
|
+
const refreshedAt = new Date().toISOString();
|
|
2917
4459
|
const nodeStatuses = [];
|
|
2918
|
-
for (const node of mesh.nodes || []) {
|
|
4460
|
+
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
4461
|
+
const nodeId = String(node.id || node.nodeId || '');
|
|
4462
|
+
const daemonId = readStringValue(node.daemonId);
|
|
4463
|
+
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
4464
|
+
const isSelfNode = Boolean(
|
|
4465
|
+
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
4466
|
+
) || Boolean(
|
|
4467
|
+
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
4468
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
2919
4469
|
const status: Record<string, unknown> = {
|
|
2920
|
-
nodeId
|
|
4470
|
+
nodeId,
|
|
2921
4471
|
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2922
4472
|
workspace: node.workspace,
|
|
2923
4473
|
repoRoot: node.repoRoot,
|
|
2924
4474
|
isLocalWorktree: node.isLocalWorktree,
|
|
2925
4475
|
worktreeBranch: node.worktreeBranch,
|
|
2926
|
-
|
|
4476
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
4477
|
+
daemonId,
|
|
2927
4478
|
machineId: node.machineId,
|
|
4479
|
+
machineStatus: node.machineStatus,
|
|
2928
4480
|
health: 'unknown',
|
|
2929
4481
|
providers: node.providers || [],
|
|
4482
|
+
providerPriority,
|
|
2930
4483
|
activeSessions: [],
|
|
4484
|
+
activeSessionDetails: [],
|
|
4485
|
+
launchReady: false,
|
|
2931
4486
|
};
|
|
2932
|
-
if (
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
4487
|
+
if (isSelfNode) {
|
|
4488
|
+
status.connection = {
|
|
4489
|
+
perspective: 'selected_coordinator',
|
|
4490
|
+
source: 'mesh_peer_status',
|
|
4491
|
+
state: 'self',
|
|
4492
|
+
transport: 'local',
|
|
4493
|
+
reported: true,
|
|
4494
|
+
reason: 'Selected coordinator daemon',
|
|
4495
|
+
lastStateChangeAt: refreshedAt,
|
|
4496
|
+
};
|
|
4497
|
+
} else if (daemonId) {
|
|
4498
|
+
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
4499
|
+
status.connection = connection ?? {
|
|
4500
|
+
perspective: 'selected_coordinator',
|
|
4501
|
+
source: 'not_reported',
|
|
4502
|
+
state: 'unknown',
|
|
4503
|
+
transport: 'unknown',
|
|
4504
|
+
reported: false,
|
|
4505
|
+
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
4506
|
+
};
|
|
4507
|
+
} else {
|
|
4508
|
+
status.connection = {
|
|
4509
|
+
perspective: 'selected_coordinator',
|
|
4510
|
+
source: 'not_reported',
|
|
4511
|
+
state: 'unknown',
|
|
4512
|
+
transport: 'unknown',
|
|
4513
|
+
reported: false,
|
|
4514
|
+
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
4515
|
+
};
|
|
4516
|
+
}
|
|
4517
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
4518
|
+
meshId,
|
|
4519
|
+
node,
|
|
4520
|
+
nodeId,
|
|
4521
|
+
liveSessionRecords: liveMeshSessions,
|
|
4522
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4523
|
+
});
|
|
4524
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4525
|
+
meshId,
|
|
4526
|
+
nodeId,
|
|
4527
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
4528
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4529
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
4530
|
+
status.workspace = workspace || node.workspace;
|
|
4531
|
+
if (matchedLiveSessionRecords.length > 0) {
|
|
4532
|
+
const sessionIds = matchedLiveSessionRecords
|
|
4533
|
+
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
4534
|
+
.filter(Boolean);
|
|
4535
|
+
const providerTypes = matchedLiveSessionRecords
|
|
4536
|
+
.map((record: any) => readStringValue(record?.providerType))
|
|
4537
|
+
.filter(Boolean) as string[];
|
|
4538
|
+
status.activeSessions = sessionIds;
|
|
4539
|
+
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
4540
|
+
if (providerTypes.length > 0) {
|
|
4541
|
+
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
2936
4542
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
status.
|
|
2945
|
-
|
|
4543
|
+
}
|
|
4544
|
+
if (workspace) {
|
|
4545
|
+
if (!fs.existsSync(workspace)) {
|
|
4546
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
4547
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
4548
|
+
let remoteProbeApplied = false;
|
|
4549
|
+
if (inlineTransitGit) {
|
|
4550
|
+
status.git = inlineTransitGit;
|
|
4551
|
+
status.health = inlineTransitGit.isGitRepo
|
|
4552
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
4553
|
+
: 'degraded';
|
|
4554
|
+
const connection = readObjectRecord(status.connection);
|
|
4555
|
+
const connectionState = readStringValue(connection.state);
|
|
4556
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4557
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4558
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4559
|
+
}
|
|
4560
|
+
remoteProbeApplied = true;
|
|
4561
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
4562
|
+
try {
|
|
4563
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
4564
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4565
|
+
daemonId,
|
|
4566
|
+
workspace,
|
|
4567
|
+
timeoutMs: 8000,
|
|
4568
|
+
});
|
|
4569
|
+
if (remoteGit) {
|
|
4570
|
+
status.git = remoteGit;
|
|
4571
|
+
status.health = remoteGit.isGitRepo
|
|
4572
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
4573
|
+
: 'degraded';
|
|
4574
|
+
const connection = readObjectRecord(status.connection);
|
|
4575
|
+
const connectionState = readStringValue(connection.state);
|
|
4576
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4577
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4578
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4579
|
+
}
|
|
4580
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
4581
|
+
remoteProbeApplied = true;
|
|
4582
|
+
}
|
|
4583
|
+
} catch {
|
|
4584
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
4585
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
4586
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
4587
|
+
status.connection = refreshedConnection;
|
|
4588
|
+
try {
|
|
4589
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
4590
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4591
|
+
daemonId,
|
|
4592
|
+
workspace,
|
|
4593
|
+
timeoutMs: 12000,
|
|
4594
|
+
});
|
|
4595
|
+
if (remoteGit) {
|
|
4596
|
+
status.git = remoteGit;
|
|
4597
|
+
status.health = remoteGit.isGitRepo
|
|
4598
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
4599
|
+
: 'degraded';
|
|
4600
|
+
const connection = readObjectRecord(status.connection);
|
|
4601
|
+
const connectionState = readStringValue(connection.state);
|
|
4602
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4603
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4604
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4605
|
+
}
|
|
4606
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
4607
|
+
remoteProbeApplied = true;
|
|
4608
|
+
}
|
|
4609
|
+
} catch {
|
|
4610
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
2946
4614
|
}
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
4615
|
+
if (!remoteProbeApplied) {
|
|
4616
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
4617
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
4618
|
+
&& !isSelfNode
|
|
4619
|
+
&& !!daemonId
|
|
4620
|
+
&& (
|
|
4621
|
+
readStringValue(status.machineStatus) === 'online'
|
|
4622
|
+
|| readStringValue(status.health) === 'online'
|
|
4623
|
+
|| connectionState === 'connecting'
|
|
4624
|
+
|| connectionState === 'connected'
|
|
4625
|
+
|| connectionState === 'unknown'
|
|
4626
|
+
);
|
|
4627
|
+
if (pendingPeerGitProbe) {
|
|
4628
|
+
status.gitProbePending = true;
|
|
4629
|
+
status.health = 'unknown';
|
|
4630
|
+
}
|
|
4631
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
4632
|
+
status,
|
|
4633
|
+
node,
|
|
4634
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
4635
|
+
)) {
|
|
4636
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
4637
|
+
nodeStatuses.push(status);
|
|
4638
|
+
continue;
|
|
4639
|
+
}
|
|
4640
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
4641
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
4642
|
+
nodeStatuses.push(status);
|
|
4643
|
+
continue;
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
} else {
|
|
4647
|
+
try {
|
|
4648
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
4649
|
+
status.git = gitStatus;
|
|
4650
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
4651
|
+
if (gitStatus.isGitRepo) {
|
|
4652
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
4653
|
+
} else {
|
|
4654
|
+
status.health = 'degraded';
|
|
4655
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
4656
|
+
}
|
|
4657
|
+
} catch {
|
|
4658
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
4659
|
+
status.health = 'degraded';
|
|
4660
|
+
}
|
|
2950
4661
|
}
|
|
2951
4662
|
}
|
|
2952
4663
|
} else {
|
|
2953
4664
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
2954
4665
|
}
|
|
4666
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
2955
4667
|
nodeStatuses.push(status);
|
|
2956
4668
|
}
|
|
2957
4669
|
|
|
2958
|
-
|
|
4670
|
+
const statusResult = {
|
|
2959
4671
|
success: true,
|
|
2960
4672
|
meshId: mesh.id,
|
|
2961
4673
|
meshName: mesh.name,
|
|
2962
4674
|
repoIdentity: mesh.repoIdentity,
|
|
2963
4675
|
defaultBranch: mesh.defaultBranch,
|
|
4676
|
+
refreshedAt,
|
|
4677
|
+
meshHost,
|
|
4678
|
+
sourceOfTruth: {
|
|
4679
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
4680
|
+
? 'coordinator_inline_mesh_cache'
|
|
4681
|
+
: meshRecord?.source === 'local_config'
|
|
4682
|
+
? 'local_mesh_config'
|
|
4683
|
+
: 'inline_bootstrap_snapshot',
|
|
4684
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
4685
|
+
meshHost: {
|
|
4686
|
+
owner: 'mesh_host_daemon',
|
|
4687
|
+
localRole: meshHost.role,
|
|
4688
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
4689
|
+
hostNodeId: meshHost.hostNodeId,
|
|
4690
|
+
hostAddress: meshHost.hostAddress,
|
|
4691
|
+
},
|
|
4692
|
+
...(requireDirectPeerTruth ? {
|
|
4693
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
4694
|
+
directPeerTruth: {
|
|
4695
|
+
required: true,
|
|
4696
|
+
satisfied: directTruthSatisfied,
|
|
4697
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
4698
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
4699
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
4700
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
4701
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
4702
|
+
},
|
|
4703
|
+
} : {}),
|
|
4704
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
4705
|
+
},
|
|
2964
4706
|
nodes: nodeStatuses,
|
|
2965
4707
|
queue: { tasks: queue, summary: queueSummary },
|
|
2966
4708
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
2967
4709
|
};
|
|
4710
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, statusResult, refreshReason);
|
|
4711
|
+
logRepoMeshStatusDebug('return_live', {
|
|
4712
|
+
meshId,
|
|
4713
|
+
command: 'mesh_status',
|
|
4714
|
+
refreshRequested,
|
|
4715
|
+
refreshReason,
|
|
4716
|
+
meshSource: meshRecord.source,
|
|
4717
|
+
directTruth,
|
|
4718
|
+
summary: summarizeRepoMeshStatusDebug(rememberedStatus),
|
|
4719
|
+
});
|
|
4720
|
+
return rememberedStatus;
|
|
2968
4721
|
} catch (e: any) {
|
|
2969
4722
|
return { success: false, error: e.message };
|
|
2970
4723
|
}
|