@adhdev/daemon-core 0.9.82-rc.4 → 0.9.82-rc.40
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 +11 -0
- 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 +2 -2
- package/dist/index.js +1363 -311
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1362 -311
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +17 -5
- package/dist/mesh/mesh-work-queue.d.ts +3 -1
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/repo-mesh-types.d.ts +128 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +3 -0
- package/src/commands/router.ts +1200 -169
- 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 +9 -1
- package/src/mesh/mesh-events.ts +168 -30
- package/src/mesh/mesh-work-queue.ts +135 -119
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/repo-mesh-types.ts +138 -0
package/src/commands/router.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { getSavedProviderSessions } from '../config/saved-sessions.js';
|
|
|
26
26
|
import { listProviderHistorySessions } from '../config/chat-history.js';
|
|
27
27
|
import { detectIDEs } from '../detection/ide-detector.js';
|
|
28
28
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
29
|
+
import { getGitRepoStatus } from '../git/git-status.js';
|
|
30
|
+
import type { GitSubmoduleStatus } from '../git/git-types.js';
|
|
29
31
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
30
32
|
import { LOG } from '../logging/logger.js';
|
|
31
33
|
import { logCommand } from '../logging/command-log.js';
|
|
@@ -112,58 +114,44 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
112
114
|
return undefined;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
function
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
|
|
118
|
+
const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : '';
|
|
119
|
+
const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : '';
|
|
120
|
+
if (!normalizedPath) return undefined;
|
|
121
|
+
if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath;
|
|
122
|
+
if (!normalizedRoot) return undefined;
|
|
123
|
+
return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
|
|
127
|
+
if (!Array.isArray(value)) return undefined;
|
|
128
|
+
const submodules = value
|
|
129
|
+
.map(entry => {
|
|
130
|
+
const submodule = readObjectRecord(entry);
|
|
131
|
+
const path = readStringValue(submodule.path);
|
|
132
|
+
const commit = readStringValue(submodule.commit);
|
|
133
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
134
|
+
?? joinRepoPath(parentRepoRoot, path);
|
|
135
|
+
if (!path || !commit || !repoPath) return null;
|
|
126
136
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
ahead: readNumberValue(cachedGit.ahead) ?? 0,
|
|
135
|
-
behind: readNumberValue(cachedGit.behind) ?? 0,
|
|
136
|
-
staged: readNumberValue(cachedGit.staged) ?? 0,
|
|
137
|
-
modified: readNumberValue(cachedGit.modified) ?? 0,
|
|
138
|
-
untracked: readNumberValue(cachedGit.untracked) ?? 0,
|
|
139
|
-
deleted: readNumberValue(cachedGit.deleted) ?? 0,
|
|
140
|
-
renamed: readNumberValue(cachedGit.renamed) ?? 0,
|
|
141
|
-
hasConflicts,
|
|
142
|
-
conflictFiles,
|
|
143
|
-
stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
|
|
144
|
-
lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
|
|
137
|
+
path,
|
|
138
|
+
commit,
|
|
139
|
+
repoPath,
|
|
140
|
+
dirty: readBooleanValue(submodule.dirty) ?? false,
|
|
141
|
+
outOfSync: readBooleanValue(submodule.outOfSync, submodule.out_of_sync) ?? false,
|
|
142
|
+
lastCheckedAt: readNumberValue(submodule.lastCheckedAt, submodule.last_checked_at) ?? Date.now(),
|
|
143
|
+
...(readStringValue(submodule.error) ? { error: readStringValue(submodule.error) } : {}),
|
|
145
144
|
};
|
|
146
|
-
}
|
|
147
|
-
|
|
145
|
+
})
|
|
146
|
+
.filter((entry): entry is GitSubmoduleStatus => entry !== null);
|
|
147
|
+
return submodules.length > 0 ? submodules : undefined;
|
|
148
|
+
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const probeGit = readObjectRecord(rawProbe.git);
|
|
155
|
-
const probeGitResult = readObjectRecord(probeGit.result);
|
|
156
|
-
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
157
|
-
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
158
|
-
const status = Object.keys(directStatus).length
|
|
159
|
-
? directStatus
|
|
160
|
-
: Object.keys(nestedStatus).length
|
|
161
|
-
? nestedStatus
|
|
162
|
-
: Object.keys(probeDirectStatus).length
|
|
163
|
-
? probeDirectStatus
|
|
164
|
-
: Object.keys(probeNestedStatus).length
|
|
165
|
-
? probeNestedStatus
|
|
166
|
-
: {};
|
|
150
|
+
function normalizeInlineMeshGitStatus(
|
|
151
|
+
status: Record<string, unknown>,
|
|
152
|
+
node: any,
|
|
153
|
+
options?: { lastCheckedAt?: number },
|
|
154
|
+
): Record<string, unknown> | undefined {
|
|
167
155
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
168
156
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
169
157
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -171,9 +159,11 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
171
159
|
: [];
|
|
172
160
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
173
161
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
162
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
163
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
174
164
|
return {
|
|
175
165
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
176
|
-
repoRoot:
|
|
166
|
+
repoRoot: repoRoot ?? null,
|
|
177
167
|
isGitRepo,
|
|
178
168
|
branch: readStringValue(status.branch) ?? null,
|
|
179
169
|
headCommit: readStringValue(status.headCommit) ?? null,
|
|
@@ -189,30 +179,544 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
189
179
|
hasConflicts,
|
|
190
180
|
conflictFiles,
|
|
191
181
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
192
|
-
lastCheckedAt: Date.now(),
|
|
182
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
183
|
+
...(submodules ? { submodules } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
188
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
189
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
190
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
191
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
192
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
193
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
194
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
195
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
196
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
197
|
+
const status = Object.keys(directStatus).length
|
|
198
|
+
? directStatus
|
|
199
|
+
: Object.keys(nestedStatus).length
|
|
200
|
+
? nestedStatus
|
|
201
|
+
: Object.keys(probeDirectStatus).length
|
|
202
|
+
? probeDirectStatus
|
|
203
|
+
: Object.keys(probeNestedStatus).length
|
|
204
|
+
? probeNestedStatus
|
|
205
|
+
: {};
|
|
206
|
+
return normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function recordInlineMeshDirectGitTruth(
|
|
210
|
+
node: any,
|
|
211
|
+
git: Record<string, unknown>,
|
|
212
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
213
|
+
): void {
|
|
214
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
215
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
216
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
217
|
+
const nextGit: Record<string, unknown> = {
|
|
218
|
+
...git,
|
|
219
|
+
lastCheckedAt: checkedAt,
|
|
193
220
|
};
|
|
221
|
+
node.lastGit = {
|
|
222
|
+
source,
|
|
223
|
+
checkedAt,
|
|
224
|
+
status: nextGit,
|
|
225
|
+
};
|
|
226
|
+
node.last_git = node.lastGit;
|
|
227
|
+
node.machineStatus = 'online';
|
|
228
|
+
node.updatedAt = updatedAt;
|
|
229
|
+
node.lastSeenAt = updatedAt;
|
|
230
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
231
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
235
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
236
|
+
if (liveGit) return liveGit;
|
|
237
|
+
|
|
238
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
239
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
240
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
241
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
245
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
246
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
247
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
248
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
249
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
250
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
251
|
+
const branch = readStringValue(cachedGit.branch);
|
|
252
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
253
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
257
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
258
|
+
const {
|
|
259
|
+
cachedStatus,
|
|
260
|
+
lastGit: _lastGit,
|
|
261
|
+
last_git: _lastGitLegacy,
|
|
262
|
+
lastProbe: _lastProbe,
|
|
263
|
+
last_probe: _lastProbeLegacy,
|
|
264
|
+
error: _error,
|
|
265
|
+
health: _health,
|
|
266
|
+
machineStatus: _machineStatus,
|
|
267
|
+
lastSeenAt: _lastSeenAt,
|
|
268
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
269
|
+
updatedAt: _updatedAt,
|
|
270
|
+
updated_at: _updatedAtLegacy,
|
|
271
|
+
activeSession: _activeSession,
|
|
272
|
+
active_session: _activeSessionLegacy,
|
|
273
|
+
activeSessionId: _activeSessionId,
|
|
274
|
+
active_session_id: _activeSessionIdLegacy,
|
|
275
|
+
sessionId: _sessionId,
|
|
276
|
+
session_id: _sessionIdLegacy,
|
|
277
|
+
providerType: _providerType,
|
|
278
|
+
provider_type: _providerTypeLegacy,
|
|
279
|
+
...rest
|
|
280
|
+
} = node as Record<string, unknown>;
|
|
281
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
282
|
+
return { ...rest, cachedStatus };
|
|
283
|
+
}
|
|
284
|
+
return rest;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
288
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
289
|
+
return 'cachedStatus' in node
|
|
290
|
+
|| 'lastGit' in node
|
|
291
|
+
|| 'last_git' in node
|
|
292
|
+
|| 'lastProbe' in node
|
|
293
|
+
|| 'last_probe' in node
|
|
294
|
+
|| 'error' in node
|
|
295
|
+
|| 'health' in node
|
|
296
|
+
|| 'machineStatus' in node
|
|
297
|
+
|| 'lastSeenAt' in node
|
|
298
|
+
|| 'last_seen_at' in node
|
|
299
|
+
|| 'updatedAt' in node
|
|
300
|
+
|| 'updated_at' in node
|
|
301
|
+
|| 'activeSession' in node
|
|
302
|
+
|| 'active_session' in node
|
|
303
|
+
|| 'activeSessionId' in node
|
|
304
|
+
|| 'active_session_id' in node
|
|
305
|
+
|| 'sessionId' in node
|
|
306
|
+
|| 'session_id' in node
|
|
307
|
+
|| 'providerType' in node
|
|
308
|
+
|| 'provider_type' in node;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
312
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
313
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
314
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readInlineMeshNodeId(node: any): string {
|
|
318
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
322
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
323
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
324
|
+
let changed = false;
|
|
325
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
326
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
327
|
+
changed = true;
|
|
328
|
+
return stripInlineMeshTransientNodeState(node);
|
|
329
|
+
});
|
|
330
|
+
if (!changed) return inlineMesh;
|
|
331
|
+
return {
|
|
332
|
+
...inlineMesh,
|
|
333
|
+
nodes,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
338
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
339
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
340
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
341
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
342
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
343
|
+
|
|
344
|
+
const incomingById = new Map<string, any>();
|
|
345
|
+
for (const node of incomingNodes) {
|
|
346
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
347
|
+
if (nodeId) incomingById.set(nodeId, node);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const nodes = cachedNodes.map((cachedNode: any) => {
|
|
351
|
+
const nodeId = readInlineMeshNodeId(cachedNode);
|
|
352
|
+
const incomingNode = nodeId ? incomingById.get(nodeId) : undefined;
|
|
353
|
+
if (!incomingNode) return cachedNode;
|
|
354
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
355
|
+
return { ...cachedNode, ...incomingNode };
|
|
356
|
+
}
|
|
357
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
...cached,
|
|
362
|
+
...incoming,
|
|
363
|
+
nodes,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
368
|
+
if (!git) return false;
|
|
369
|
+
return Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
373
|
+
const submodules = Array.isArray(git?.submodules) ? git.submodules : [];
|
|
374
|
+
let dirty = false;
|
|
375
|
+
let outOfSync = false;
|
|
376
|
+
for (const entry of submodules) {
|
|
377
|
+
const submodule = readObjectRecord(entry);
|
|
378
|
+
if (readBooleanValue(submodule.dirty) === true) dirty = true;
|
|
379
|
+
if (readBooleanValue(submodule.outOfSync) === true || !!readStringValue(submodule.error)) outOfSync = true;
|
|
380
|
+
}
|
|
381
|
+
return { dirty, outOfSync };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
385
|
+
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
386
|
+
const branch = readStringValue(git.branch);
|
|
387
|
+
if (!branch) return 'degraded';
|
|
388
|
+
const submoduleDrift = getGitSubmoduleDriftState(git);
|
|
389
|
+
if (submoduleDrift.outOfSync) return 'degraded';
|
|
390
|
+
if (submoduleDrift.dirty || hasGitWorktreeChanges(git)) return 'dirty';
|
|
391
|
+
return 'online';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
395
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
396
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
397
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
398
|
+
? activeSession
|
|
399
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
400
|
+
const sessionId = readStringValue(fallbackSession.id, fallbackSession.sessionId, fallbackSession.session_id, node?.activeSessionId, node?.active_session_id, node?.sessionId, node?.session_id);
|
|
401
|
+
return sessionId ? [sessionId] : [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
405
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
406
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
407
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
408
|
+
? activeSession
|
|
409
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
410
|
+
const sessionId = readStringValue(
|
|
411
|
+
fallbackSession.id,
|
|
412
|
+
fallbackSession.sessionId,
|
|
413
|
+
fallbackSession.session_id,
|
|
414
|
+
node?.activeSessionId,
|
|
415
|
+
node?.active_session_id,
|
|
416
|
+
node?.sessionId,
|
|
417
|
+
node?.session_id,
|
|
418
|
+
);
|
|
419
|
+
if (!sessionId) return [];
|
|
420
|
+
return [{
|
|
421
|
+
sessionId,
|
|
422
|
+
providerType: readStringValue(
|
|
423
|
+
fallbackSession.providerType,
|
|
424
|
+
fallbackSession.provider_type,
|
|
425
|
+
fallbackSession.cliType,
|
|
426
|
+
fallbackSession.cli_type,
|
|
427
|
+
fallbackSession.provider,
|
|
428
|
+
node?.providerType,
|
|
429
|
+
node?.provider_type,
|
|
430
|
+
),
|
|
431
|
+
state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
|
|
432
|
+
lifecycle: readStringValue(fallbackSession.lifecycle),
|
|
433
|
+
title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
|
|
434
|
+
workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
|
|
435
|
+
lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
|
|
436
|
+
recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
|
|
437
|
+
isCached: true,
|
|
438
|
+
}];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function readLiveMeshSessionState(record: any): string | undefined {
|
|
442
|
+
return readStringValue(
|
|
443
|
+
record?.meta?.sessionStatus,
|
|
444
|
+
record?.meta?.status,
|
|
445
|
+
record?.meta?.providerStatus,
|
|
446
|
+
record?.status,
|
|
447
|
+
record?.state,
|
|
448
|
+
record?.lifecycle,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function toIsoTimestamp(value: unknown): string | null {
|
|
453
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
454
|
+
const stringValue = readStringValue(value);
|
|
455
|
+
return stringValue || null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
459
|
+
const connection = readObjectRecord(status.connection);
|
|
460
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
461
|
+
const git = readObjectRecord(status.git);
|
|
462
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
463
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
464
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
465
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function finalizeMeshNodeStatus(args: {
|
|
470
|
+
status: Record<string, unknown>;
|
|
471
|
+
node: any;
|
|
472
|
+
daemonId?: string;
|
|
473
|
+
isSelfNode: boolean;
|
|
474
|
+
}): void {
|
|
475
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
476
|
+
if (!readStringValue(status.machineStatus)) {
|
|
477
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
478
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
479
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
480
|
+
}
|
|
481
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
482
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
483
|
+
status.launchReady = !!daemonId && (
|
|
484
|
+
readStringValue(status.machineStatus) === 'online'
|
|
485
|
+
|| connectionState === 'connected'
|
|
486
|
+
|| isSelfNode
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
491
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
492
|
+
daemonId: string;
|
|
493
|
+
workspace: string;
|
|
494
|
+
timeoutMs: number;
|
|
495
|
+
}): Promise<Record<string, unknown> | null> {
|
|
496
|
+
if (!args.dispatchMeshCommand) return null;
|
|
497
|
+
const remoteResult = await Promise.race([
|
|
498
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace }),
|
|
499
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
500
|
+
]) as any;
|
|
501
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
502
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
503
|
+
? remoteGit as Record<string, unknown>
|
|
504
|
+
: null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
508
|
+
mesh: any;
|
|
509
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
510
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
511
|
+
statusInstanceId?: string;
|
|
512
|
+
localMachineId?: string;
|
|
513
|
+
}): Promise<{
|
|
514
|
+
directEvidenceCount: number;
|
|
515
|
+
localConfirmedCount: number;
|
|
516
|
+
peerAttemptedCount: number;
|
|
517
|
+
peerConfirmedCount: number;
|
|
518
|
+
unavailableNodeIds: string[];
|
|
519
|
+
}> {
|
|
520
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
521
|
+
if (!nodes.length) {
|
|
522
|
+
return {
|
|
523
|
+
directEvidenceCount: 0,
|
|
524
|
+
localConfirmedCount: 0,
|
|
525
|
+
peerAttemptedCount: 0,
|
|
526
|
+
peerConfirmedCount: 0,
|
|
527
|
+
unavailableNodeIds: [],
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
532
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
533
|
+
nodes[0]?.id,
|
|
534
|
+
nodes[0]?.nodeId,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
let localConfirmedCount = 0;
|
|
538
|
+
let peerAttemptedCount = 0;
|
|
539
|
+
let peerConfirmedCount = 0;
|
|
540
|
+
const unavailableNodeIds: string[] = [];
|
|
541
|
+
|
|
542
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
543
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
544
|
+
const workspace = readStringValue(node?.workspace);
|
|
545
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
546
|
+
const isSelfNode = Boolean(
|
|
547
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
548
|
+
) || Boolean(
|
|
549
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
550
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
551
|
+
|
|
552
|
+
if (!workspace) {
|
|
553
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (isSelfNode && fs.existsSync(workspace)) {
|
|
558
|
+
try {
|
|
559
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
560
|
+
if (localGit?.isGitRepo) {
|
|
561
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
562
|
+
localConfirmedCount += 1;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
// Fall through to remote classification.
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
571
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
peerAttemptedCount += 1;
|
|
576
|
+
try {
|
|
577
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
578
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
579
|
+
daemonId,
|
|
580
|
+
workspace,
|
|
581
|
+
timeoutMs: 8_000,
|
|
582
|
+
});
|
|
583
|
+
if (remoteGit) {
|
|
584
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
585
|
+
peerConfirmedCount += 1;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
unavailableNodeIds.push(nodeId);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
597
|
+
localConfirmedCount,
|
|
598
|
+
peerAttemptedCount,
|
|
599
|
+
peerConfirmedCount,
|
|
600
|
+
unavailableNodeIds,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
605
|
+
return {
|
|
606
|
+
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
607
|
+
providerType: readStringValue(record?.providerType),
|
|
608
|
+
state: readLiveMeshSessionState(record),
|
|
609
|
+
lifecycle: readStringValue(record?.lifecycle),
|
|
610
|
+
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
611
|
+
recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
|
|
612
|
+
workspace: readStringValue(record?.workspace) ?? null,
|
|
613
|
+
title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
|
|
614
|
+
lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
|
|
615
|
+
isCached: false,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
620
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
621
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
622
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
623
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
194
624
|
}
|
|
195
625
|
|
|
196
|
-
function
|
|
626
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
627
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
628
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
629
|
+
|
|
630
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
631
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
632
|
+
|
|
633
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
637
|
+
meshId: string;
|
|
638
|
+
nodeId: string;
|
|
639
|
+
liveSessionRecords: any[];
|
|
640
|
+
allowCoordinatorSession?: boolean;
|
|
641
|
+
}): string {
|
|
642
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
643
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
644
|
+
&& readStringValue(record?.workspace)
|
|
645
|
+
));
|
|
646
|
+
if (directNodeWorkspace) {
|
|
647
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (args.allowCoordinatorSession) {
|
|
651
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
652
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
653
|
+
&& readStringValue(record?.workspace)
|
|
654
|
+
));
|
|
655
|
+
if (coordinatorWorkspace) {
|
|
656
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return '';
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function collectLiveMeshSessionRecords(args: {
|
|
664
|
+
meshId: string;
|
|
665
|
+
node: any;
|
|
666
|
+
nodeId: string;
|
|
667
|
+
liveSessionRecords: any[];
|
|
668
|
+
allowCoordinatorSession?: boolean;
|
|
669
|
+
}): any[] {
|
|
670
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
671
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
672
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
|
|
673
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
if (args.allowCoordinatorSession) {
|
|
677
|
+
for (const record of args.liveSessionRecords) {
|
|
678
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
679
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
680
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
681
|
+
matches.push(record);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return matches;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function applyCachedInlineMeshNodeStatus(
|
|
689
|
+
status: Record<string, unknown>,
|
|
690
|
+
node: any,
|
|
691
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
692
|
+
): boolean {
|
|
197
693
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
694
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
695
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
696
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
697
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
201
698
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
202
|
-
|
|
203
|
-
|
|
699
|
+
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
700
|
+
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
701
|
+
const activeSessions = readCachedInlineMeshActiveSessions(node);
|
|
702
|
+
const activeSessionDetails = readCachedInlineMeshActiveSessionDetails(node);
|
|
703
|
+
if (!git && !error && !health && !machineStatus && !lastSeenAt && !updatedAt && activeSessions.length === 0) return false;
|
|
204
704
|
if (git) status.git = git;
|
|
205
705
|
if (error) status.error = error;
|
|
706
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
707
|
+
if (lastSeenAt) status.lastSeenAt = lastSeenAt;
|
|
708
|
+
if (updatedAt) status.updatedAt = updatedAt;
|
|
709
|
+
if (activeSessions.length > 0) status.activeSessions = activeSessions;
|
|
710
|
+
if (activeSessionDetails.length > 0) status.activeSessionDetails = activeSessionDetails;
|
|
206
711
|
if (health) {
|
|
207
712
|
status.health = health;
|
|
208
713
|
return true;
|
|
209
714
|
}
|
|
210
715
|
if (git) {
|
|
211
|
-
|
|
212
|
-
status.health = git.isGitRepo === false ? 'degraded' : dirty ? 'dirty' : 'online';
|
|
716
|
+
status.health = deriveMeshNodeHealthFromGit(git);
|
|
213
717
|
return true;
|
|
214
718
|
}
|
|
215
|
-
return
|
|
719
|
+
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
216
720
|
}
|
|
217
721
|
|
|
218
722
|
async function resolveProviderTypeFromPriority(args: {
|
|
@@ -265,11 +769,29 @@ type MeshRefineValidationSummary = {
|
|
|
265
769
|
outputLimitBytes: number;
|
|
266
770
|
};
|
|
267
771
|
|
|
772
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
773
|
+
|
|
774
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
775
|
+
status: MeshRefineStageStatus;
|
|
776
|
+
equivalent: boolean;
|
|
777
|
+
baseHead: string;
|
|
778
|
+
branchHead: string;
|
|
779
|
+
mergeBase?: string;
|
|
780
|
+
mergedTree?: string;
|
|
781
|
+
expectedPatchId?: string;
|
|
782
|
+
actualPatchId?: string;
|
|
783
|
+
durationMs: number;
|
|
784
|
+
error?: string;
|
|
785
|
+
stdout?: string;
|
|
786
|
+
stderr?: string;
|
|
787
|
+
};
|
|
788
|
+
|
|
268
789
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
269
790
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
270
791
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
271
792
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
272
793
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
794
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
273
795
|
|
|
274
796
|
function truncateValidationOutput(value: unknown): string {
|
|
275
797
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -277,6 +799,95 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
277
799
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
278
800
|
}
|
|
279
801
|
|
|
802
|
+
function recordMeshRefineStage(
|
|
803
|
+
stages: Array<Record<string, unknown>>,
|
|
804
|
+
stage: string,
|
|
805
|
+
status: MeshRefineStageStatus,
|
|
806
|
+
startedAt: number,
|
|
807
|
+
details?: Record<string, unknown>,
|
|
808
|
+
): void {
|
|
809
|
+
stages.push({
|
|
810
|
+
stage,
|
|
811
|
+
status,
|
|
812
|
+
durationMs: Date.now() - startedAt,
|
|
813
|
+
...(details || {}),
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
818
|
+
const { execFileSync } = await import('node:child_process');
|
|
819
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
820
|
+
cwd,
|
|
821
|
+
encoding: 'utf8',
|
|
822
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
823
|
+
});
|
|
824
|
+
if (!diff.trim()) return '';
|
|
825
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
826
|
+
cwd,
|
|
827
|
+
input: diff,
|
|
828
|
+
encoding: 'utf8',
|
|
829
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
830
|
+
}).trim();
|
|
831
|
+
return patchId.split(/\s+/)[0] || '';
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
835
|
+
repoRoot: string,
|
|
836
|
+
baseHead: string,
|
|
837
|
+
branchHead: string,
|
|
838
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
839
|
+
const startedAt = Date.now();
|
|
840
|
+
try {
|
|
841
|
+
const { execFileSync } = await import('node:child_process');
|
|
842
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
843
|
+
cwd: repoRoot,
|
|
844
|
+
encoding: 'utf8',
|
|
845
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
846
|
+
});
|
|
847
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
848
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
849
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
850
|
+
if (!mergeBase || !mergedTree) {
|
|
851
|
+
return {
|
|
852
|
+
status: 'failed',
|
|
853
|
+
equivalent: false,
|
|
854
|
+
baseHead,
|
|
855
|
+
branchHead,
|
|
856
|
+
mergeBase: mergeBase || undefined,
|
|
857
|
+
mergedTree: mergedTree || undefined,
|
|
858
|
+
durationMs: Date.now() - startedAt,
|
|
859
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
860
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
864
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
865
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
866
|
+
return {
|
|
867
|
+
status: equivalent ? 'passed' : 'failed',
|
|
868
|
+
equivalent,
|
|
869
|
+
baseHead,
|
|
870
|
+
branchHead,
|
|
871
|
+
mergeBase,
|
|
872
|
+
mergedTree,
|
|
873
|
+
expectedPatchId,
|
|
874
|
+
actualPatchId,
|
|
875
|
+
durationMs: Date.now() - startedAt,
|
|
876
|
+
};
|
|
877
|
+
} catch (e: any) {
|
|
878
|
+
return {
|
|
879
|
+
status: 'failed',
|
|
880
|
+
equivalent: false,
|
|
881
|
+
baseHead,
|
|
882
|
+
branchHead,
|
|
883
|
+
durationMs: Date.now() - startedAt,
|
|
884
|
+
error: e?.message || String(e),
|
|
885
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
886
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
280
891
|
function readPackageScripts(workspace: string): Record<string, string> {
|
|
281
892
|
try {
|
|
282
893
|
const packageJsonPath = pathJoin(workspace, 'package.json');
|
|
@@ -632,6 +1243,10 @@ export interface CommandRouterDeps {
|
|
|
632
1243
|
statusVersion?: string;
|
|
633
1244
|
/** Session host control plane */
|
|
634
1245
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
1246
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
1247
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1248
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
1249
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
635
1250
|
}
|
|
636
1251
|
|
|
637
1252
|
export interface CommandRouterResult {
|
|
@@ -749,36 +1364,114 @@ export class DaemonCommandRouter {
|
|
|
749
1364
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
750
1365
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
751
1366
|
private inlineMeshCache = new Map<string, any>();
|
|
1367
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
1368
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any }>();
|
|
752
1369
|
|
|
753
1370
|
constructor(deps: CommandRouterDeps) {
|
|
754
1371
|
this.deps = deps;
|
|
755
1372
|
}
|
|
756
1373
|
|
|
1374
|
+
private cloneJsonValue<T>(value: T): T {
|
|
1375
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
1376
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
private getCachedAggregateMeshStatus(meshId: string): any | null {
|
|
1380
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
1381
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
1382
|
+
const snapshot = this.cloneJsonValue(cached.snapshot);
|
|
1383
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
1384
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
1385
|
+
? snapshot.sourceOfTruth
|
|
1386
|
+
: {};
|
|
1387
|
+
snapshot.sourceOfTruth = {
|
|
1388
|
+
...sourceOfTruth,
|
|
1389
|
+
aggregateSnapshot: {
|
|
1390
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
1391
|
+
? sourceOfTruth.aggregateSnapshot
|
|
1392
|
+
: {}),
|
|
1393
|
+
owner: 'coordinator_daemon_memory',
|
|
1394
|
+
cached: true,
|
|
1395
|
+
source: 'memory',
|
|
1396
|
+
refreshReason: 'memory_cache_hit',
|
|
1397
|
+
ageMs,
|
|
1398
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
1399
|
+
returnedAt: new Date().toISOString(),
|
|
1400
|
+
},
|
|
1401
|
+
};
|
|
1402
|
+
return snapshot;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
1406
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
1407
|
+
const builtAt = Date.now();
|
|
1408
|
+
const next = this.cloneJsonValue(snapshot);
|
|
1409
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
1410
|
+
? next.sourceOfTruth
|
|
1411
|
+
: {};
|
|
1412
|
+
next.sourceOfTruth = {
|
|
1413
|
+
...sourceOfTruth,
|
|
1414
|
+
aggregateSnapshot: {
|
|
1415
|
+
owner: 'coordinator_daemon_memory',
|
|
1416
|
+
cached: false,
|
|
1417
|
+
source: 'live_refresh',
|
|
1418
|
+
refreshReason,
|
|
1419
|
+
ageMs: 0,
|
|
1420
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
1421
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
1422
|
+
},
|
|
1423
|
+
};
|
|
1424
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next) });
|
|
1425
|
+
return next;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
757
1428
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
758
1429
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
759
|
-
this.
|
|
760
|
-
return inlineMesh as any;
|
|
1430
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
761
1431
|
}
|
|
762
1432
|
return this.inlineMeshCache.get(meshId);
|
|
763
1433
|
}
|
|
764
1434
|
|
|
1435
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
1436
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
1437
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
1438
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
1439
|
+
if (cached) {
|
|
1440
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
1441
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
1442
|
+
return merged;
|
|
1443
|
+
}
|
|
1444
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
1445
|
+
return sanitizedInlineMesh as any;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
765
1448
|
private async getMeshForCommand(
|
|
766
1449
|
meshId: string,
|
|
767
1450
|
inlineMesh?: unknown,
|
|
768
1451
|
options?: { preferInline?: boolean },
|
|
769
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
1452
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
770
1453
|
const preferInline = options?.preferInline === true;
|
|
771
1454
|
if (preferInline) {
|
|
772
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
773
|
-
if (cached) return { mesh: cached, inline: true };
|
|
1455
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1456
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1457
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1458
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1459
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
1460
|
+
}
|
|
774
1461
|
}
|
|
775
1462
|
try {
|
|
776
1463
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
777
1464
|
const mesh = getMesh(meshId);
|
|
778
|
-
if (mesh) return { mesh, inline: false };
|
|
1465
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
779
1466
|
} catch { /* fall through to inline cache */ }
|
|
780
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
781
|
-
|
|
1467
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1468
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1469
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1470
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
1474
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
782
1475
|
}
|
|
783
1476
|
|
|
784
1477
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -788,6 +1481,7 @@ export class DaemonCommandRouter {
|
|
|
788
1481
|
else mesh.nodes.push(node);
|
|
789
1482
|
mesh.updatedAt = new Date().toISOString();
|
|
790
1483
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1484
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
791
1485
|
}
|
|
792
1486
|
|
|
793
1487
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -797,6 +1491,7 @@ export class DaemonCommandRouter {
|
|
|
797
1491
|
mesh.nodes.splice(idx, 1);
|
|
798
1492
|
mesh.updatedAt = new Date().toISOString();
|
|
799
1493
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1494
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
800
1495
|
return true;
|
|
801
1496
|
}
|
|
802
1497
|
|
|
@@ -1075,6 +1770,7 @@ export class DaemonCommandRouter {
|
|
|
1075
1770
|
const deletedSessionIds: string[] = [];
|
|
1076
1771
|
const skippedSessionIds: string[] = [];
|
|
1077
1772
|
const skippedLiveSessionIds: string[] = [];
|
|
1773
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1078
1774
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1079
1775
|
const recordsRemainSessionIds: string[] = [];
|
|
1080
1776
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1109,6 +1805,12 @@ export class DaemonCommandRouter {
|
|
|
1109
1805
|
const completed = this.isCompletedHostedSession(record);
|
|
1110
1806
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1111
1807
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
1808
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
1809
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
1810
|
+
skippedSessionIds.push(sessionId);
|
|
1811
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1112
1814
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1113
1815
|
skippedSessionIds.push(sessionId);
|
|
1114
1816
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1178,6 +1880,7 @@ export class DaemonCommandRouter {
|
|
|
1178
1880
|
deletedSessionIds,
|
|
1179
1881
|
skippedSessionIds,
|
|
1180
1882
|
skippedLiveSessionIds,
|
|
1883
|
+
skippedCoordinatorSessionIds,
|
|
1181
1884
|
...(deleteUnsupported ? {
|
|
1182
1885
|
deleteUnsupported: true,
|
|
1183
1886
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1332,7 +2035,8 @@ export class DaemonCommandRouter {
|
|
|
1332
2035
|
}
|
|
1333
2036
|
|
|
1334
2037
|
case 'get_pending_mesh_events': {
|
|
1335
|
-
const
|
|
2038
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2039
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1336
2040
|
return { success: true, events };
|
|
1337
2041
|
}
|
|
1338
2042
|
|
|
@@ -1937,15 +2641,44 @@ export class DaemonCommandRouter {
|
|
|
1937
2641
|
case 'get_mesh': {
|
|
1938
2642
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1939
2643
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2644
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2645
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
2646
|
+
|
|
2647
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
2648
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
2649
|
+
mesh: meshRecord.mesh,
|
|
2650
|
+
meshSource: meshRecord.source,
|
|
2651
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
2652
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
2653
|
+
localMachineId: loadConfig().machineId || '',
|
|
2654
|
+
});
|
|
2655
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
2656
|
+
const sourceOfTruth = {
|
|
2657
|
+
membership: meshRecord.source === 'inline_cache'
|
|
2658
|
+
? 'coordinator_inline_mesh_cache'
|
|
2659
|
+
: meshRecord.source === 'local_config'
|
|
2660
|
+
? 'local_mesh_config'
|
|
2661
|
+
: 'inline_bootstrap_snapshot',
|
|
2662
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
2663
|
+
directPeerTruth: {
|
|
2664
|
+
required: requireDirectPeerTruth,
|
|
2665
|
+
satisfied: directTruthSatisfied,
|
|
2666
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
2667
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
2668
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
2669
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
2670
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
2671
|
+
},
|
|
2672
|
+
};
|
|
2673
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
2674
|
+
return {
|
|
2675
|
+
success: false,
|
|
2676
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
2677
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
2678
|
+
sourceOfTruth,
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
1949
2682
|
}
|
|
1950
2683
|
|
|
1951
2684
|
case 'create_mesh': {
|
|
@@ -1977,6 +2710,7 @@ export class DaemonCommandRouter {
|
|
|
1977
2710
|
const mesh = updateMesh(meshId, patch as any);
|
|
1978
2711
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1979
2712
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2713
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
1980
2714
|
return { success: true, mesh };
|
|
1981
2715
|
} catch (e: any) {
|
|
1982
2716
|
return { success: false, error: e.message };
|
|
@@ -2191,34 +2925,49 @@ export class DaemonCommandRouter {
|
|
|
2191
2925
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2192
2926
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2193
2927
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2928
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
2194
2929
|
try {
|
|
2195
2930
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2196
2931
|
const mesh = meshRecord?.mesh;
|
|
2197
2932
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2198
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh
|
|
2933
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
2199
2934
|
|
|
2200
2935
|
if (!node.isLocalWorktree || !node.workspace) {
|
|
2201
|
-
return { success: false, error: `Refinery requires a local worktree node
|
|
2936
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
2202
2937
|
}
|
|
2203
2938
|
|
|
2204
2939
|
const sourceNode = node.clonedFromNodeId
|
|
2205
2940
|
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2206
2941
|
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2207
2942
|
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2208
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2943
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
2209
2944
|
|
|
2210
2945
|
const { execFile } = await import('node:child_process');
|
|
2211
2946
|
const { promisify } = await import('node:util');
|
|
2212
2947
|
const execFileAsync = promisify(execFile);
|
|
2213
2948
|
|
|
2949
|
+
const resolveStarted = Date.now();
|
|
2214
2950
|
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2215
2951
|
const branch = branchStdout.trim();
|
|
2216
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2952
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
2217
2953
|
|
|
2218
2954
|
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2219
2955
|
const baseBranch = baseBranchStdout.trim();
|
|
2956
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2957
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
2958
|
+
const baseHead = baseHeadStdout.trim();
|
|
2959
|
+
const branchHead = branchHeadStdout.trim();
|
|
2960
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
2220
2961
|
|
|
2962
|
+
const validationStarted = Date.now();
|
|
2221
2963
|
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2964
|
+
recordMeshRefineStage(
|
|
2965
|
+
refineStages,
|
|
2966
|
+
'validation',
|
|
2967
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
2968
|
+
validationStarted,
|
|
2969
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
2970
|
+
);
|
|
2222
2971
|
if (validationSummary.status === 'failed') {
|
|
2223
2972
|
return {
|
|
2224
2973
|
success: false,
|
|
@@ -2228,6 +2977,7 @@ export class DaemonCommandRouter {
|
|
|
2228
2977
|
branch,
|
|
2229
2978
|
into: baseBranch,
|
|
2230
2979
|
validationSummary,
|
|
2980
|
+
refineStages,
|
|
2231
2981
|
finalBranchConvergenceState: {
|
|
2232
2982
|
branch,
|
|
2233
2983
|
baseBranch,
|
|
@@ -2247,6 +2997,7 @@ export class DaemonCommandRouter {
|
|
|
2247
2997
|
branch,
|
|
2248
2998
|
into: baseBranch,
|
|
2249
2999
|
validationSummary,
|
|
3000
|
+
refineStages,
|
|
2250
3001
|
finalBranchConvergenceState: {
|
|
2251
3002
|
branch,
|
|
2252
3003
|
baseBranch,
|
|
@@ -2258,39 +3009,127 @@ export class DaemonCommandRouter {
|
|
|
2258
3009
|
};
|
|
2259
3010
|
}
|
|
2260
3011
|
|
|
3012
|
+
const patchEquivalenceStarted = Date.now();
|
|
3013
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3014
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
3015
|
+
equivalent: patchEquivalence.equivalent,
|
|
3016
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
3017
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
3018
|
+
error: patchEquivalence.error,
|
|
3019
|
+
});
|
|
3020
|
+
if (!patchEquivalence.equivalent) {
|
|
3021
|
+
return {
|
|
3022
|
+
success: false,
|
|
3023
|
+
code: 'patch_equivalence_failed',
|
|
3024
|
+
convergenceStatus: 'blocked_review',
|
|
3025
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
3026
|
+
branch,
|
|
3027
|
+
into: baseBranch,
|
|
3028
|
+
validationSummary,
|
|
3029
|
+
patchEquivalence,
|
|
3030
|
+
refineStages,
|
|
3031
|
+
finalBranchConvergenceState: {
|
|
3032
|
+
branch,
|
|
3033
|
+
baseBranch,
|
|
3034
|
+
merged: false,
|
|
3035
|
+
removed: false,
|
|
3036
|
+
validation: 'passed',
|
|
3037
|
+
patchEquivalence: 'failed',
|
|
3038
|
+
status: 'blocked_review',
|
|
3039
|
+
},
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
3044
|
+
const mergeStarted = Date.now();
|
|
2261
3045
|
try {
|
|
2262
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3046
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3047
|
+
mergeResult = {
|
|
3048
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
3049
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
3050
|
+
durationMs: Date.now() - mergeStarted,
|
|
3051
|
+
};
|
|
3052
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
2263
3053
|
} catch (e: any) {
|
|
3054
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
3055
|
+
error: e?.message || String(e),
|
|
3056
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
3057
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
3058
|
+
});
|
|
2264
3059
|
return {
|
|
2265
3060
|
success: false,
|
|
2266
3061
|
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2267
3062
|
validationSummary,
|
|
3063
|
+
patchEquivalence,
|
|
3064
|
+
refineStages,
|
|
2268
3065
|
finalBranchConvergenceState: {
|
|
2269
3066
|
branch,
|
|
2270
3067
|
baseBranch,
|
|
2271
3068
|
merged: false,
|
|
2272
3069
|
removed: false,
|
|
2273
3070
|
validation: 'passed',
|
|
3071
|
+
patchEquivalence: 'passed',
|
|
2274
3072
|
status: 'not_mergeable',
|
|
2275
3073
|
},
|
|
2276
3074
|
};
|
|
2277
3075
|
}
|
|
2278
3076
|
|
|
3077
|
+
const cleanupStarted = Date.now();
|
|
2279
3078
|
const removeResult = await this.execute('remove_mesh_node', {
|
|
2280
3079
|
meshId,
|
|
2281
3080
|
nodeId,
|
|
2282
|
-
sessionCleanupMode: '
|
|
3081
|
+
sessionCleanupMode: 'preserve',
|
|
2283
3082
|
inlineMesh: args?.inlineMesh,
|
|
2284
3083
|
});
|
|
3084
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
3085
|
+
removed: removeResult?.removed,
|
|
3086
|
+
code: removeResult?.code,
|
|
3087
|
+
error: removeResult?.error,
|
|
3088
|
+
});
|
|
2285
3089
|
|
|
3090
|
+
let ledgerError: string | undefined;
|
|
3091
|
+
const ledgerStarted = Date.now();
|
|
2286
3092
|
try {
|
|
2287
3093
|
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2288
3094
|
appendLedgerEntry(meshId, {
|
|
2289
3095
|
kind: 'node_removed',
|
|
2290
3096
|
nodeId,
|
|
2291
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
3097
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
2292
3098
|
});
|
|
2293
|
-
|
|
3099
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
3100
|
+
} catch (e: any) {
|
|
3101
|
+
ledgerError = e?.message || String(e);
|
|
3102
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
const finalBranchConvergenceState = {
|
|
3106
|
+
branch: baseBranch,
|
|
3107
|
+
mergedBranch: branch,
|
|
3108
|
+
baseBranch,
|
|
3109
|
+
merged: true,
|
|
3110
|
+
removed: removeResult?.success !== false,
|
|
3111
|
+
validation: 'passed',
|
|
3112
|
+
patchEquivalence: 'passed',
|
|
3113
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
3114
|
+
};
|
|
3115
|
+
|
|
3116
|
+
if (removeResult?.success === false) {
|
|
3117
|
+
return {
|
|
3118
|
+
success: false,
|
|
3119
|
+
code: 'cleanup_failed',
|
|
3120
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
3121
|
+
merged: true,
|
|
3122
|
+
branch,
|
|
3123
|
+
into: baseBranch,
|
|
3124
|
+
removeResult,
|
|
3125
|
+
validationSummary,
|
|
3126
|
+
patchEquivalence,
|
|
3127
|
+
mergeResult,
|
|
3128
|
+
refineStages,
|
|
3129
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3130
|
+
finalBranchConvergenceState,
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
2294
3133
|
|
|
2295
3134
|
return {
|
|
2296
3135
|
success: true,
|
|
@@ -2299,18 +3138,14 @@ export class DaemonCommandRouter {
|
|
|
2299
3138
|
into: baseBranch,
|
|
2300
3139
|
removeResult,
|
|
2301
3140
|
validationSummary,
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
removed: removeResult?.success !== false,
|
|
2308
|
-
validation: 'passed',
|
|
2309
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2310
|
-
},
|
|
3141
|
+
patchEquivalence,
|
|
3142
|
+
mergeResult,
|
|
3143
|
+
refineStages,
|
|
3144
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3145
|
+
finalBranchConvergenceState,
|
|
2311
3146
|
};
|
|
2312
3147
|
} catch (e: any) {
|
|
2313
|
-
return { success: false, error: e.message };
|
|
3148
|
+
return { success: false, error: e.message, refineStages };
|
|
2314
3149
|
}
|
|
2315
3150
|
}
|
|
2316
3151
|
|
|
@@ -2355,6 +3190,7 @@ export class DaemonCommandRouter {
|
|
|
2355
3190
|
} else {
|
|
2356
3191
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2357
3192
|
removed = removeNode(meshId, nodeId);
|
|
3193
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2358
3194
|
}
|
|
2359
3195
|
|
|
2360
3196
|
// Record in task ledger
|
|
@@ -2440,6 +3276,7 @@ export class DaemonCommandRouter {
|
|
|
2440
3276
|
policy: { ...(sourceNode.policy || {}) },
|
|
2441
3277
|
});
|
|
2442
3278
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
3279
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2443
3280
|
}
|
|
2444
3281
|
|
|
2445
3282
|
// Initialize submodules if policy allows (default: true)
|
|
@@ -2531,7 +3368,16 @@ export class DaemonCommandRouter {
|
|
|
2531
3368
|
cliType,
|
|
2532
3369
|
};
|
|
2533
3370
|
}
|
|
2534
|
-
const
|
|
3371
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
3372
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
3373
|
+
: [];
|
|
3374
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
3375
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
3376
|
+
meshId,
|
|
3377
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
3378
|
+
liveSessionRecords: liveMeshSessions,
|
|
3379
|
+
allowCoordinatorSession: true,
|
|
3380
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2535
3381
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2536
3382
|
if (!cliType) {
|
|
2537
3383
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -2877,6 +3723,13 @@ export class DaemonCommandRouter {
|
|
|
2877
3723
|
const mesh = meshRecord?.mesh;
|
|
2878
3724
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2879
3725
|
|
|
3726
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
3727
|
+
if (!refreshRequested) {
|
|
3728
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId);
|
|
3729
|
+
if (cachedStatus) return cachedStatus;
|
|
3730
|
+
}
|
|
3731
|
+
const refreshReason = refreshRequested ? 'explicit_refresh' : 'cold_cache_miss';
|
|
3732
|
+
|
|
2880
3733
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
2881
3734
|
const queue = getQueue(meshId);
|
|
2882
3735
|
const queueSummary = getMeshQueueStats(meshId);
|
|
@@ -2884,111 +3737,289 @@ export class DaemonCommandRouter {
|
|
|
2884
3737
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2885
3738
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2886
3739
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
2887
|
-
|
|
3740
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
3741
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
3742
|
+
: [];
|
|
3743
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
3744
|
+
|
|
3745
|
+
const localMachineId = loadConfig().machineId || '';
|
|
3746
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
3747
|
+
const directTruth = requireDirectPeerTruth
|
|
3748
|
+
? await hydrateInlineMeshDirectTruth({
|
|
3749
|
+
mesh,
|
|
3750
|
+
meshSource: meshRecord.source,
|
|
3751
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
3752
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
3753
|
+
localMachineId,
|
|
3754
|
+
})
|
|
3755
|
+
: {
|
|
3756
|
+
directEvidenceCount: 0,
|
|
3757
|
+
localConfirmedCount: 0,
|
|
3758
|
+
peerAttemptedCount: 0,
|
|
3759
|
+
peerConfirmedCount: 0,
|
|
3760
|
+
unavailableNodeIds: [] as string[],
|
|
3761
|
+
};
|
|
3762
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
3763
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
3764
|
+
return {
|
|
3765
|
+
success: false,
|
|
3766
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
3767
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
3768
|
+
sourceOfTruth: {
|
|
3769
|
+
membership: meshRecord.source === 'inline_cache'
|
|
3770
|
+
? 'coordinator_inline_mesh_cache'
|
|
3771
|
+
: meshRecord.source === 'local_config'
|
|
3772
|
+
? 'local_mesh_config'
|
|
3773
|
+
: 'inline_bootstrap_snapshot',
|
|
3774
|
+
coordinatorOwnsLiveTruth: false,
|
|
3775
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
3776
|
+
directPeerTruth: {
|
|
3777
|
+
required: true,
|
|
3778
|
+
satisfied: false,
|
|
3779
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
3780
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
3781
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
3782
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
3783
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
3784
|
+
},
|
|
3785
|
+
},
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
const directTruthUnavailableNodeIds = new Set(directTruth.unavailableNodeIds);
|
|
3789
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
3790
|
+
mesh.coordinator?.preferredNodeId,
|
|
3791
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
3792
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
3793
|
+
);
|
|
3794
|
+
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
3795
|
+
? selectedCoordinatorNodeId
|
|
3796
|
+
: undefined;
|
|
3797
|
+
const refreshedAt = new Date().toISOString();
|
|
2888
3798
|
const nodeStatuses = [];
|
|
2889
|
-
for (const node of mesh.nodes || []) {
|
|
3799
|
+
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
3800
|
+
const nodeId = String(node.id || node.nodeId || '');
|
|
3801
|
+
const daemonId = readStringValue(node.daemonId);
|
|
3802
|
+
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
3803
|
+
const isSelfNode = Boolean(
|
|
3804
|
+
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
3805
|
+
) || Boolean(
|
|
3806
|
+
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
3807
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
2890
3808
|
const status: Record<string, unknown> = {
|
|
2891
|
-
nodeId
|
|
3809
|
+
nodeId,
|
|
2892
3810
|
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2893
3811
|
workspace: node.workspace,
|
|
2894
3812
|
repoRoot: node.repoRoot,
|
|
2895
3813
|
isLocalWorktree: node.isLocalWorktree,
|
|
2896
3814
|
worktreeBranch: node.worktreeBranch,
|
|
2897
|
-
daemonId
|
|
3815
|
+
daemonId,
|
|
2898
3816
|
machineId: node.machineId,
|
|
3817
|
+
machineStatus: node.machineStatus,
|
|
2899
3818
|
health: 'unknown',
|
|
2900
3819
|
providers: node.providers || [],
|
|
3820
|
+
providerPriority,
|
|
2901
3821
|
activeSessions: [],
|
|
3822
|
+
activeSessionDetails: [],
|
|
3823
|
+
launchReady: false,
|
|
2902
3824
|
};
|
|
2903
|
-
if (
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3825
|
+
if (isSelfNode) {
|
|
3826
|
+
status.connection = {
|
|
3827
|
+
perspective: 'selected_coordinator',
|
|
3828
|
+
source: 'mesh_peer_status',
|
|
3829
|
+
state: 'self',
|
|
3830
|
+
transport: 'local',
|
|
3831
|
+
reported: true,
|
|
3832
|
+
reason: 'Selected coordinator daemon',
|
|
3833
|
+
lastStateChangeAt: refreshedAt,
|
|
3834
|
+
};
|
|
3835
|
+
} else if (daemonId) {
|
|
3836
|
+
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
3837
|
+
status.connection = connection ?? {
|
|
3838
|
+
perspective: 'selected_coordinator',
|
|
3839
|
+
source: 'not_reported',
|
|
3840
|
+
state: 'unknown',
|
|
3841
|
+
transport: 'unknown',
|
|
3842
|
+
reported: false,
|
|
3843
|
+
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
3844
|
+
};
|
|
3845
|
+
} else {
|
|
3846
|
+
status.connection = {
|
|
3847
|
+
perspective: 'selected_coordinator',
|
|
3848
|
+
source: 'not_reported',
|
|
3849
|
+
state: 'unknown',
|
|
3850
|
+
transport: 'unknown',
|
|
3851
|
+
reported: false,
|
|
3852
|
+
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
3856
|
+
meshId,
|
|
3857
|
+
node,
|
|
3858
|
+
nodeId,
|
|
3859
|
+
liveSessionRecords: liveMeshSessions,
|
|
3860
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
3861
|
+
});
|
|
3862
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
3863
|
+
meshId,
|
|
3864
|
+
nodeId,
|
|
3865
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
3866
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
3867
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
3868
|
+
status.workspace = workspace || node.workspace;
|
|
3869
|
+
if (matchedLiveSessionRecords.length > 0) {
|
|
3870
|
+
const sessionIds = matchedLiveSessionRecords
|
|
3871
|
+
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
3872
|
+
.filter(Boolean);
|
|
3873
|
+
const providerTypes = matchedLiveSessionRecords
|
|
3874
|
+
.map((record: any) => readStringValue(record?.providerType))
|
|
3875
|
+
.filter(Boolean) as string[];
|
|
3876
|
+
status.activeSessions = sessionIds;
|
|
3877
|
+
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
3878
|
+
if (providerTypes.length > 0) {
|
|
3879
|
+
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
2907
3880
|
}
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3881
|
+
}
|
|
3882
|
+
if (workspace) {
|
|
3883
|
+
if (!fs.existsSync(workspace)) {
|
|
3884
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
3885
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
3886
|
+
let remoteProbeApplied = false;
|
|
3887
|
+
if (inlineTransitGit) {
|
|
3888
|
+
status.git = inlineTransitGit;
|
|
3889
|
+
status.health = inlineTransitGit.isGitRepo
|
|
3890
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
3891
|
+
: 'degraded';
|
|
3892
|
+
remoteProbeApplied = true;
|
|
3893
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
3894
|
+
try {
|
|
3895
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
3896
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
3897
|
+
daemonId,
|
|
3898
|
+
workspace,
|
|
3899
|
+
timeoutMs: 8000,
|
|
3900
|
+
});
|
|
3901
|
+
if (remoteGit) {
|
|
3902
|
+
status.git = remoteGit;
|
|
3903
|
+
status.health = remoteGit.isGitRepo
|
|
3904
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
3905
|
+
: 'degraded';
|
|
3906
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
3907
|
+
remoteProbeApplied = true;
|
|
3908
|
+
}
|
|
3909
|
+
} catch {
|
|
3910
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
3911
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
3912
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
3913
|
+
status.connection = refreshedConnection;
|
|
3914
|
+
try {
|
|
3915
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
3916
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
3917
|
+
daemonId,
|
|
3918
|
+
workspace,
|
|
3919
|
+
timeoutMs: 12000,
|
|
3920
|
+
});
|
|
3921
|
+
if (remoteGit) {
|
|
3922
|
+
status.git = remoteGit;
|
|
3923
|
+
status.health = remoteGit.isGitRepo
|
|
3924
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
3925
|
+
: 'degraded';
|
|
3926
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
3927
|
+
remoteProbeApplied = true;
|
|
3928
|
+
}
|
|
3929
|
+
} catch {
|
|
3930
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
2935
3933
|
}
|
|
2936
3934
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3935
|
+
if (!remoteProbeApplied) {
|
|
3936
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
3937
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
3938
|
+
&& !isSelfNode
|
|
3939
|
+
&& !!daemonId
|
|
3940
|
+
&& (
|
|
3941
|
+
readStringValue(status.machineStatus) === 'online'
|
|
3942
|
+
|| readStringValue(status.health) === 'online'
|
|
3943
|
+
|| connectionState === 'connecting'
|
|
3944
|
+
|| connectionState === 'connected'
|
|
3945
|
+
|| connectionState === 'unknown'
|
|
3946
|
+
);
|
|
3947
|
+
if (pendingPeerGitProbe) {
|
|
3948
|
+
status.gitProbePending = true;
|
|
3949
|
+
status.health = 'unknown';
|
|
3950
|
+
}
|
|
3951
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
3952
|
+
status,
|
|
3953
|
+
node,
|
|
3954
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
3955
|
+
)) {
|
|
3956
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
3957
|
+
nodeStatuses.push(status);
|
|
3958
|
+
continue;
|
|
3959
|
+
}
|
|
3960
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
3961
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
3962
|
+
nodeStatuses.push(status);
|
|
3963
|
+
continue;
|
|
3964
|
+
}
|
|
2948
3965
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
workspace:
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
renamed,
|
|
2965
|
-
hasConflicts: false,
|
|
2966
|
-
conflictFiles: [],
|
|
2967
|
-
stashCount: stashCount ? stashCount.split('\n').filter(Boolean).length : 0,
|
|
2968
|
-
lastCheckedAt: Date.now(),
|
|
2969
|
-
};
|
|
2970
|
-
status.health = branch ? (dirty ? 'dirty' : 'online') : 'degraded';
|
|
2971
|
-
} catch {
|
|
2972
|
-
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
2973
|
-
status.health = 'degraded';
|
|
3966
|
+
} else {
|
|
3967
|
+
try {
|
|
3968
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
3969
|
+
status.git = gitStatus;
|
|
3970
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
3971
|
+
if (gitStatus.isGitRepo) {
|
|
3972
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
3973
|
+
} else {
|
|
3974
|
+
status.health = 'degraded';
|
|
3975
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
3976
|
+
}
|
|
3977
|
+
} catch {
|
|
3978
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
3979
|
+
status.health = 'degraded';
|
|
3980
|
+
}
|
|
2974
3981
|
}
|
|
2975
3982
|
}
|
|
2976
3983
|
} else {
|
|
2977
3984
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
2978
3985
|
}
|
|
3986
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
2979
3987
|
nodeStatuses.push(status);
|
|
2980
3988
|
}
|
|
2981
3989
|
|
|
2982
|
-
|
|
3990
|
+
const statusResult = {
|
|
2983
3991
|
success: true,
|
|
2984
3992
|
meshId: mesh.id,
|
|
2985
3993
|
meshName: mesh.name,
|
|
2986
3994
|
repoIdentity: mesh.repoIdentity,
|
|
2987
3995
|
defaultBranch: mesh.defaultBranch,
|
|
3996
|
+
refreshedAt,
|
|
3997
|
+
sourceOfTruth: {
|
|
3998
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
3999
|
+
? 'coordinator_inline_mesh_cache'
|
|
4000
|
+
: meshRecord?.source === 'local_config'
|
|
4001
|
+
? 'local_mesh_config'
|
|
4002
|
+
: 'inline_bootstrap_snapshot',
|
|
4003
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
4004
|
+
...(requireDirectPeerTruth ? {
|
|
4005
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
4006
|
+
directPeerTruth: {
|
|
4007
|
+
required: true,
|
|
4008
|
+
satisfied: directTruthSatisfied,
|
|
4009
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4010
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4011
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4012
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4013
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4014
|
+
},
|
|
4015
|
+
} : {}),
|
|
4016
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
4017
|
+
},
|
|
2988
4018
|
nodes: nodeStatuses,
|
|
2989
4019
|
queue: { tasks: queue, summary: queueSummary },
|
|
2990
4020
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
2991
4021
|
};
|
|
4022
|
+
return this.rememberAggregateMeshStatus(meshId, statusResult, refreshReason);
|
|
2992
4023
|
} catch (e: any) {
|
|
2993
4024
|
return { success: false, error: e.message };
|
|
2994
4025
|
}
|