@adhdev/daemon-core 0.9.82-rc.3 → 0.9.82-rc.30
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 +5 -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 +863 -291
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +862 -291
- 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 +588 -147
- 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,34 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
112
114
|
return undefined;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
125
|
-
if (isGitRepo !== undefined) {
|
|
117
|
+
function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
118
|
+
if (!Array.isArray(value)) return undefined;
|
|
119
|
+
const submodules = value
|
|
120
|
+
.map(entry => {
|
|
121
|
+
const submodule = readObjectRecord(entry);
|
|
122
|
+
const path = readStringValue(submodule.path);
|
|
123
|
+
const commit = readStringValue(submodule.commit);
|
|
124
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root);
|
|
125
|
+
if (!path || !commit || !repoPath) return null;
|
|
126
126
|
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(),
|
|
127
|
+
path,
|
|
128
|
+
commit,
|
|
129
|
+
repoPath,
|
|
130
|
+
dirty: readBooleanValue(submodule.dirty) ?? false,
|
|
131
|
+
outOfSync: readBooleanValue(submodule.outOfSync, submodule.out_of_sync) ?? false,
|
|
132
|
+
lastCheckedAt: readNumberValue(submodule.lastCheckedAt, submodule.last_checked_at) ?? Date.now(),
|
|
133
|
+
...(readStringValue(submodule.error) ? { error: readStringValue(submodule.error) } : {}),
|
|
145
134
|
};
|
|
146
|
-
}
|
|
147
|
-
|
|
135
|
+
})
|
|
136
|
+
.filter((entry): entry is GitSubmoduleStatus => entry !== null);
|
|
137
|
+
return submodules.length > 0 ? submodules : undefined;
|
|
138
|
+
}
|
|
148
139
|
|
|
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
|
-
: {};
|
|
140
|
+
function normalizeInlineMeshGitStatus(
|
|
141
|
+
status: Record<string, unknown>,
|
|
142
|
+
node: any,
|
|
143
|
+
options?: { lastCheckedAt?: number },
|
|
144
|
+
): Record<string, unknown> | undefined {
|
|
167
145
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
168
146
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
169
147
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -171,6 +149,7 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
171
149
|
: [];
|
|
172
150
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
173
151
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
152
|
+
const submodules = readGitSubmodules(status.submodules);
|
|
174
153
|
return {
|
|
175
154
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
176
155
|
repoRoot: readStringValue(status.repoRoot, node?.repoRoot, node?.workspace) || null,
|
|
@@ -189,30 +168,369 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
189
168
|
hasConflicts,
|
|
190
169
|
conflictFiles,
|
|
191
170
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
192
|
-
lastCheckedAt: Date.now(),
|
|
171
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
172
|
+
...(submodules ? { submodules } : {}),
|
|
193
173
|
};
|
|
194
174
|
}
|
|
195
175
|
|
|
196
|
-
function
|
|
176
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
177
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
178
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
179
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
180
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
181
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
182
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
183
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
184
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
185
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
186
|
+
const status = Object.keys(directStatus).length
|
|
187
|
+
? directStatus
|
|
188
|
+
: Object.keys(nestedStatus).length
|
|
189
|
+
? nestedStatus
|
|
190
|
+
: Object.keys(probeDirectStatus).length
|
|
191
|
+
? probeDirectStatus
|
|
192
|
+
: Object.keys(probeNestedStatus).length
|
|
193
|
+
? probeNestedStatus
|
|
194
|
+
: {};
|
|
195
|
+
return normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
199
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
200
|
+
if (liveGit) return liveGit;
|
|
201
|
+
|
|
202
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
203
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
204
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
205
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
197
209
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const
|
|
210
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
211
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
212
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
213
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
214
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
215
|
+
const branch = readStringValue(cachedGit.branch);
|
|
216
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
217
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
221
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
222
|
+
const {
|
|
223
|
+
cachedStatus,
|
|
224
|
+
lastGit: _lastGit,
|
|
225
|
+
last_git: _lastGitLegacy,
|
|
226
|
+
lastProbe: _lastProbe,
|
|
227
|
+
last_probe: _lastProbeLegacy,
|
|
228
|
+
error: _error,
|
|
229
|
+
health: _health,
|
|
230
|
+
machineStatus: _machineStatus,
|
|
231
|
+
lastSeenAt: _lastSeenAt,
|
|
232
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
233
|
+
updatedAt: _updatedAt,
|
|
234
|
+
updated_at: _updatedAtLegacy,
|
|
235
|
+
activeSession: _activeSession,
|
|
236
|
+
active_session: _activeSessionLegacy,
|
|
237
|
+
activeSessionId: _activeSessionId,
|
|
238
|
+
active_session_id: _activeSessionIdLegacy,
|
|
239
|
+
sessionId: _sessionId,
|
|
240
|
+
session_id: _sessionIdLegacy,
|
|
241
|
+
providerType: _providerType,
|
|
242
|
+
provider_type: _providerTypeLegacy,
|
|
243
|
+
providers: _providers,
|
|
244
|
+
...rest
|
|
245
|
+
} = node as Record<string, unknown>;
|
|
246
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
247
|
+
return { ...rest, cachedStatus };
|
|
248
|
+
}
|
|
249
|
+
return rest;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
253
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
254
|
+
return 'cachedStatus' in node
|
|
255
|
+
|| 'lastGit' in node
|
|
256
|
+
|| 'last_git' in node
|
|
257
|
+
|| 'lastProbe' in node
|
|
258
|
+
|| 'last_probe' in node
|
|
259
|
+
|| 'error' in node
|
|
260
|
+
|| 'health' in node
|
|
261
|
+
|| 'machineStatus' in node
|
|
262
|
+
|| 'lastSeenAt' in node
|
|
263
|
+
|| 'last_seen_at' in node
|
|
264
|
+
|| 'updatedAt' in node
|
|
265
|
+
|| 'updated_at' in node
|
|
266
|
+
|| 'activeSession' in node
|
|
267
|
+
|| 'active_session' in node
|
|
268
|
+
|| 'activeSessionId' in node
|
|
269
|
+
|| 'active_session_id' in node
|
|
270
|
+
|| 'sessionId' in node
|
|
271
|
+
|| 'session_id' in node
|
|
272
|
+
|| 'providerType' in node
|
|
273
|
+
|| 'provider_type' in node
|
|
274
|
+
|| 'providers' in node;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readInlineMeshNodeId(node: any): string {
|
|
278
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
282
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
283
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
284
|
+
let changed = false;
|
|
285
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
286
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
287
|
+
changed = true;
|
|
288
|
+
return stripInlineMeshTransientNodeState(node);
|
|
289
|
+
});
|
|
290
|
+
if (!changed) return inlineMesh;
|
|
291
|
+
return {
|
|
292
|
+
...inlineMesh,
|
|
293
|
+
nodes,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
298
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
299
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
300
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
301
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
302
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
303
|
+
|
|
304
|
+
const incomingById = new Map<string, any>();
|
|
305
|
+
for (const node of incomingNodes) {
|
|
306
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
307
|
+
if (nodeId) incomingById.set(nodeId, node);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const nodes = cachedNodes.map((cachedNode: any) => {
|
|
311
|
+
const nodeId = readInlineMeshNodeId(cachedNode);
|
|
312
|
+
const incomingNode = nodeId ? incomingById.get(nodeId) : undefined;
|
|
313
|
+
if (!incomingNode) return cachedNode;
|
|
314
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
315
|
+
return { ...cachedNode, ...incomingNode };
|
|
316
|
+
}
|
|
317
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
...cached,
|
|
322
|
+
...incoming,
|
|
323
|
+
nodes,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
328
|
+
if (!git) return false;
|
|
329
|
+
return Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
333
|
+
const submodules = Array.isArray(git?.submodules) ? git.submodules : [];
|
|
334
|
+
let dirty = false;
|
|
335
|
+
let outOfSync = false;
|
|
336
|
+
for (const entry of submodules) {
|
|
337
|
+
const submodule = readObjectRecord(entry);
|
|
338
|
+
if (readBooleanValue(submodule.dirty) === true) dirty = true;
|
|
339
|
+
if (readBooleanValue(submodule.outOfSync) === true || !!readStringValue(submodule.error)) outOfSync = true;
|
|
340
|
+
}
|
|
341
|
+
return { dirty, outOfSync };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
345
|
+
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
346
|
+
const branch = readStringValue(git.branch);
|
|
347
|
+
if (!branch) return 'degraded';
|
|
348
|
+
const submoduleDrift = getGitSubmoduleDriftState(git);
|
|
349
|
+
if (submoduleDrift.outOfSync) return 'degraded';
|
|
350
|
+
if (submoduleDrift.dirty || hasGitWorktreeChanges(git)) return 'dirty';
|
|
351
|
+
return 'online';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
355
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
356
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
357
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
358
|
+
? activeSession
|
|
359
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
360
|
+
const sessionId = readStringValue(fallbackSession.id, fallbackSession.sessionId, fallbackSession.session_id, node?.activeSessionId, node?.active_session_id, node?.sessionId, node?.session_id);
|
|
361
|
+
return sessionId ? [sessionId] : [];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
365
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
366
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
367
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
368
|
+
? activeSession
|
|
369
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
370
|
+
const sessionId = readStringValue(
|
|
371
|
+
fallbackSession.id,
|
|
372
|
+
fallbackSession.sessionId,
|
|
373
|
+
fallbackSession.session_id,
|
|
374
|
+
node?.activeSessionId,
|
|
375
|
+
node?.active_session_id,
|
|
376
|
+
node?.sessionId,
|
|
377
|
+
node?.session_id,
|
|
378
|
+
);
|
|
379
|
+
if (!sessionId) return [];
|
|
380
|
+
return [{
|
|
381
|
+
sessionId,
|
|
382
|
+
providerType: readStringValue(
|
|
383
|
+
fallbackSession.providerType,
|
|
384
|
+
fallbackSession.provider_type,
|
|
385
|
+
fallbackSession.cliType,
|
|
386
|
+
fallbackSession.cli_type,
|
|
387
|
+
fallbackSession.provider,
|
|
388
|
+
node?.providerType,
|
|
389
|
+
node?.provider_type,
|
|
390
|
+
),
|
|
391
|
+
state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
|
|
392
|
+
lifecycle: readStringValue(fallbackSession.lifecycle),
|
|
393
|
+
title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
|
|
394
|
+
workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
|
|
395
|
+
lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
|
|
396
|
+
recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
|
|
397
|
+
isCached: true,
|
|
398
|
+
}];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function readLiveMeshSessionState(record: any): string | undefined {
|
|
402
|
+
return readStringValue(
|
|
403
|
+
record?.meta?.sessionStatus,
|
|
404
|
+
record?.meta?.status,
|
|
405
|
+
record?.meta?.providerStatus,
|
|
406
|
+
record?.status,
|
|
407
|
+
record?.state,
|
|
408
|
+
record?.lifecycle,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function toIsoTimestamp(value: unknown): string | null {
|
|
413
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
414
|
+
const stringValue = readStringValue(value);
|
|
415
|
+
return stringValue || null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
419
|
+
return {
|
|
420
|
+
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
421
|
+
providerType: readStringValue(record?.providerType),
|
|
422
|
+
state: readLiveMeshSessionState(record),
|
|
423
|
+
lifecycle: readStringValue(record?.lifecycle),
|
|
424
|
+
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
425
|
+
recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
|
|
426
|
+
workspace: readStringValue(record?.workspace) ?? null,
|
|
427
|
+
title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
|
|
428
|
+
lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
|
|
429
|
+
isCached: false,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
434
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
435
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
436
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
437
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
441
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
442
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
443
|
+
|
|
444
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
445
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
446
|
+
|
|
447
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
451
|
+
meshId: string;
|
|
452
|
+
nodeId: string;
|
|
453
|
+
liveSessionRecords: any[];
|
|
454
|
+
allowCoordinatorSession?: boolean;
|
|
455
|
+
}): string {
|
|
456
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
457
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
458
|
+
&& readStringValue(record?.workspace)
|
|
459
|
+
));
|
|
460
|
+
if (directNodeWorkspace) {
|
|
461
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (args.allowCoordinatorSession) {
|
|
465
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
466
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
467
|
+
&& readStringValue(record?.workspace)
|
|
468
|
+
));
|
|
469
|
+
if (coordinatorWorkspace) {
|
|
470
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return '';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function collectLiveMeshSessionRecords(args: {
|
|
478
|
+
meshId: string;
|
|
479
|
+
node: any;
|
|
480
|
+
nodeId: string;
|
|
481
|
+
liveSessionRecords: any[];
|
|
482
|
+
allowCoordinatorSession?: boolean;
|
|
483
|
+
}): any[] {
|
|
484
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
485
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
486
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
|
|
487
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (args.allowCoordinatorSession) {
|
|
491
|
+
for (const record of args.liveSessionRecords) {
|
|
492
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
493
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
494
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
495
|
+
matches.push(record);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return matches;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function applyCachedInlineMeshNodeStatus(
|
|
503
|
+
status: Record<string, unknown>,
|
|
504
|
+
node: any,
|
|
505
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
506
|
+
): boolean {
|
|
507
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
508
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
509
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
510
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
511
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
201
512
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
202
|
-
|
|
203
|
-
|
|
513
|
+
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
514
|
+
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
515
|
+
const activeSessions = readCachedInlineMeshActiveSessions(node);
|
|
516
|
+
const activeSessionDetails = readCachedInlineMeshActiveSessionDetails(node);
|
|
517
|
+
if (!git && !error && !health && !machineStatus && !lastSeenAt && !updatedAt && activeSessions.length === 0) return false;
|
|
204
518
|
if (git) status.git = git;
|
|
205
519
|
if (error) status.error = error;
|
|
520
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
521
|
+
if (lastSeenAt) status.lastSeenAt = lastSeenAt;
|
|
522
|
+
if (updatedAt) status.updatedAt = updatedAt;
|
|
523
|
+
if (activeSessions.length > 0) status.activeSessions = activeSessions;
|
|
524
|
+
if (activeSessionDetails.length > 0) status.activeSessionDetails = activeSessionDetails;
|
|
206
525
|
if (health) {
|
|
207
526
|
status.health = health;
|
|
208
527
|
return true;
|
|
209
528
|
}
|
|
210
529
|
if (git) {
|
|
211
|
-
|
|
212
|
-
status.health = git.isGitRepo === false ? 'degraded' : dirty ? 'dirty' : 'online';
|
|
530
|
+
status.health = deriveMeshNodeHealthFromGit(git);
|
|
213
531
|
return true;
|
|
214
532
|
}
|
|
215
|
-
return
|
|
533
|
+
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
216
534
|
}
|
|
217
535
|
|
|
218
536
|
async function resolveProviderTypeFromPriority(args: {
|
|
@@ -632,6 +950,10 @@ export interface CommandRouterDeps {
|
|
|
632
950
|
statusVersion?: string;
|
|
633
951
|
/** Session host control plane */
|
|
634
952
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
953
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
954
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
955
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
956
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
635
957
|
}
|
|
636
958
|
|
|
637
959
|
export interface CommandRouterResult {
|
|
@@ -756,29 +1078,45 @@ export class DaemonCommandRouter {
|
|
|
756
1078
|
|
|
757
1079
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
758
1080
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
759
|
-
this.
|
|
760
|
-
return inlineMesh as any;
|
|
1081
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
761
1082
|
}
|
|
762
1083
|
return this.inlineMeshCache.get(meshId);
|
|
763
1084
|
}
|
|
764
1085
|
|
|
1086
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
1087
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
1088
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
1089
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
1090
|
+
if (cached) {
|
|
1091
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
1092
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
1093
|
+
return merged;
|
|
1094
|
+
}
|
|
1095
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
1096
|
+
return sanitizedInlineMesh as any;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
765
1099
|
private async getMeshForCommand(
|
|
766
1100
|
meshId: string,
|
|
767
1101
|
inlineMesh?: unknown,
|
|
768
1102
|
options?: { preferInline?: boolean },
|
|
769
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
1103
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
770
1104
|
const preferInline = options?.preferInline === true;
|
|
771
1105
|
if (preferInline) {
|
|
772
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
773
|
-
if (cached) return { mesh: cached, inline: true };
|
|
1106
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1107
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1108
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1109
|
+
if (warmedInline) return { mesh: warmedInline, inline: true, source: 'inline_bootstrap' };
|
|
774
1110
|
}
|
|
775
1111
|
try {
|
|
776
1112
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
777
1113
|
const mesh = getMesh(meshId);
|
|
778
|
-
if (mesh) return { mesh, inline: false };
|
|
1114
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
779
1115
|
} catch { /* fall through to inline cache */ }
|
|
780
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
781
|
-
|
|
1116
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1117
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1118
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1119
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
782
1120
|
}
|
|
783
1121
|
|
|
784
1122
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -1075,6 +1413,7 @@ export class DaemonCommandRouter {
|
|
|
1075
1413
|
const deletedSessionIds: string[] = [];
|
|
1076
1414
|
const skippedSessionIds: string[] = [];
|
|
1077
1415
|
const skippedLiveSessionIds: string[] = [];
|
|
1416
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1078
1417
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1079
1418
|
const recordsRemainSessionIds: string[] = [];
|
|
1080
1419
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1109,6 +1448,12 @@ export class DaemonCommandRouter {
|
|
|
1109
1448
|
const completed = this.isCompletedHostedSession(record);
|
|
1110
1449
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1111
1450
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
1451
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
1452
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
1453
|
+
skippedSessionIds.push(sessionId);
|
|
1454
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1112
1457
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1113
1458
|
skippedSessionIds.push(sessionId);
|
|
1114
1459
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1178,6 +1523,7 @@ export class DaemonCommandRouter {
|
|
|
1178
1523
|
deletedSessionIds,
|
|
1179
1524
|
skippedSessionIds,
|
|
1180
1525
|
skippedLiveSessionIds,
|
|
1526
|
+
skippedCoordinatorSessionIds,
|
|
1181
1527
|
...(deleteUnsupported ? {
|
|
1182
1528
|
deleteUnsupported: true,
|
|
1183
1529
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1332,7 +1678,8 @@ export class DaemonCommandRouter {
|
|
|
1332
1678
|
}
|
|
1333
1679
|
|
|
1334
1680
|
case 'get_pending_mesh_events': {
|
|
1335
|
-
const
|
|
1681
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1682
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1336
1683
|
return { success: true, events };
|
|
1337
1684
|
}
|
|
1338
1685
|
|
|
@@ -1937,14 +2284,8 @@ export class DaemonCommandRouter {
|
|
|
1937
2284
|
case 'get_mesh': {
|
|
1938
2285
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1939
2286
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
const mesh = getMesh(meshId);
|
|
1943
|
-
if (mesh) return { success: true, mesh };
|
|
1944
|
-
} catch { /* fall through to inline cache */ }
|
|
1945
|
-
// Fallback: check in-memory cache for cloud-originating meshes
|
|
1946
|
-
const cached = this.inlineMeshCache.get(meshId);
|
|
1947
|
-
if (cached) return { success: true, mesh: cached };
|
|
2287
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2288
|
+
if (meshRecord?.mesh) return { success: true, mesh: meshRecord.mesh };
|
|
1948
2289
|
return { success: false, error: 'Mesh not found' };
|
|
1949
2290
|
}
|
|
1950
2291
|
|
|
@@ -2531,7 +2872,16 @@ export class DaemonCommandRouter {
|
|
|
2531
2872
|
cliType,
|
|
2532
2873
|
};
|
|
2533
2874
|
}
|
|
2534
|
-
const
|
|
2875
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
2876
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
2877
|
+
: [];
|
|
2878
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
2879
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
2880
|
+
meshId,
|
|
2881
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
2882
|
+
liveSessionRecords: liveMeshSessions,
|
|
2883
|
+
allowCoordinatorSession: true,
|
|
2884
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2535
2885
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2536
2886
|
if (!cliType) {
|
|
2537
2887
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -2884,98 +3234,179 @@ export class DaemonCommandRouter {
|
|
|
2884
3234
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2885
3235
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2886
3236
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
3237
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
3238
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
3239
|
+
: [];
|
|
3240
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
2887
3241
|
|
|
3242
|
+
const localMachineId = loadConfig().machineId || '';
|
|
3243
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
3244
|
+
mesh.coordinator?.preferredNodeId,
|
|
3245
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
3246
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
3247
|
+
);
|
|
3248
|
+
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
3249
|
+
? selectedCoordinatorNodeId
|
|
3250
|
+
: undefined;
|
|
3251
|
+
const refreshedAt = new Date().toISOString();
|
|
2888
3252
|
const nodeStatuses = [];
|
|
2889
|
-
for (const node of mesh.nodes || []) {
|
|
3253
|
+
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
3254
|
+
const nodeId = String(node.id || node.nodeId || '');
|
|
3255
|
+
const daemonId = readStringValue(node.daemonId);
|
|
3256
|
+
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
3257
|
+
const isSelfNode = Boolean(
|
|
3258
|
+
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
3259
|
+
) || Boolean(
|
|
3260
|
+
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
3261
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
2890
3262
|
const status: Record<string, unknown> = {
|
|
2891
|
-
nodeId
|
|
3263
|
+
nodeId,
|
|
2892
3264
|
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2893
3265
|
workspace: node.workspace,
|
|
2894
3266
|
repoRoot: node.repoRoot,
|
|
2895
3267
|
isLocalWorktree: node.isLocalWorktree,
|
|
2896
3268
|
worktreeBranch: node.worktreeBranch,
|
|
2897
|
-
daemonId
|
|
3269
|
+
daemonId,
|
|
2898
3270
|
machineId: node.machineId,
|
|
3271
|
+
machineStatus: node.machineStatus,
|
|
2899
3272
|
health: 'unknown',
|
|
2900
3273
|
providers: node.providers || [],
|
|
3274
|
+
providerPriority,
|
|
2901
3275
|
activeSessions: [],
|
|
3276
|
+
activeSessionDetails: [],
|
|
3277
|
+
launchReady: false,
|
|
2902
3278
|
};
|
|
2903
|
-
if (
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3279
|
+
if (isSelfNode) {
|
|
3280
|
+
status.connection = {
|
|
3281
|
+
perspective: 'selected_coordinator',
|
|
3282
|
+
source: 'mesh_peer_status',
|
|
3283
|
+
state: 'self',
|
|
3284
|
+
transport: 'local',
|
|
3285
|
+
reported: true,
|
|
3286
|
+
reason: 'Selected coordinator daemon',
|
|
3287
|
+
lastStateChangeAt: refreshedAt,
|
|
3288
|
+
};
|
|
3289
|
+
} else if (daemonId) {
|
|
3290
|
+
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
3291
|
+
status.connection = connection ?? {
|
|
3292
|
+
perspective: 'selected_coordinator',
|
|
3293
|
+
source: 'not_reported',
|
|
3294
|
+
state: 'unknown',
|
|
3295
|
+
transport: 'unknown',
|
|
3296
|
+
reported: false,
|
|
3297
|
+
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
3298
|
+
};
|
|
3299
|
+
} else {
|
|
3300
|
+
status.connection = {
|
|
3301
|
+
perspective: 'selected_coordinator',
|
|
3302
|
+
source: 'not_reported',
|
|
3303
|
+
state: 'unknown',
|
|
3304
|
+
transport: 'unknown',
|
|
3305
|
+
reported: false,
|
|
3306
|
+
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
3310
|
+
meshId,
|
|
3311
|
+
node,
|
|
3312
|
+
nodeId,
|
|
3313
|
+
liveSessionRecords: liveMeshSessions,
|
|
3314
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
3315
|
+
});
|
|
3316
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
3317
|
+
meshId,
|
|
3318
|
+
nodeId,
|
|
3319
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
3320
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
3321
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
3322
|
+
status.workspace = workspace || node.workspace;
|
|
3323
|
+
if (matchedLiveSessionRecords.length > 0) {
|
|
3324
|
+
const sessionIds = matchedLiveSessionRecords
|
|
3325
|
+
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
3326
|
+
.filter(Boolean);
|
|
3327
|
+
const providerTypes = matchedLiveSessionRecords
|
|
3328
|
+
.map((record: any) => readStringValue(record?.providerType))
|
|
3329
|
+
.filter(Boolean) as string[];
|
|
3330
|
+
status.activeSessions = sessionIds;
|
|
3331
|
+
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
3332
|
+
if (providerTypes.length > 0) {
|
|
3333
|
+
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
2907
3334
|
}
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
let ahead = 0, behind = 0;
|
|
2930
|
-
if (aheadBehind) {
|
|
2931
|
-
const parts = aheadBehind.split(/\s+/);
|
|
2932
|
-
if (parts.length >= 2) {
|
|
2933
|
-
behind = parseInt(parts[0], 10) || 0;
|
|
2934
|
-
ahead = parseInt(parts[1], 10) || 0;
|
|
3335
|
+
}
|
|
3336
|
+
if (workspace) {
|
|
3337
|
+
if (!fs.existsSync(workspace)) {
|
|
3338
|
+
// Workspace not local — attempt a P2P git probe for remote nodes.
|
|
3339
|
+
let remoteProbeApplied = false;
|
|
3340
|
+
if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand) {
|
|
3341
|
+
try {
|
|
3342
|
+
const remoteResult = await Promise.race([
|
|
3343
|
+
this.deps.dispatchMeshCommand(daemonId, 'git_status', { workspace }),
|
|
3344
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000)),
|
|
3345
|
+
]) as any;
|
|
3346
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
3347
|
+
if (remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean') {
|
|
3348
|
+
status.git = remoteGit;
|
|
3349
|
+
status.health = remoteGit.isGitRepo
|
|
3350
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
3351
|
+
: 'degraded';
|
|
3352
|
+
remoteProbeApplied = true;
|
|
3353
|
+
}
|
|
3354
|
+
} catch {
|
|
3355
|
+
// Probe timed out or P2P unavailable — fall back to cached status
|
|
2935
3356
|
}
|
|
2936
3357
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3358
|
+
if (!remoteProbeApplied) {
|
|
3359
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
3360
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
3361
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
3362
|
+
&& !isSelfNode
|
|
3363
|
+
&& !!daemonId
|
|
3364
|
+
&& (
|
|
3365
|
+
readStringValue(status.machineStatus) === 'online'
|
|
3366
|
+
|| readStringValue(status.health) === 'online'
|
|
3367
|
+
|| connectionState === 'connecting'
|
|
3368
|
+
|| connectionState === 'connected'
|
|
3369
|
+
|| connectionState === 'unknown'
|
|
3370
|
+
);
|
|
3371
|
+
if (pendingPeerGitProbe) {
|
|
3372
|
+
status.gitProbePending = true;
|
|
3373
|
+
status.health = 'unknown';
|
|
3374
|
+
}
|
|
3375
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
3376
|
+
status,
|
|
3377
|
+
node,
|
|
3378
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
3379
|
+
)) {
|
|
3380
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
3381
|
+
nodeStatuses.push(status);
|
|
3382
|
+
continue;
|
|
3383
|
+
}
|
|
3384
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
3385
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
3386
|
+
nodeStatuses.push(status);
|
|
3387
|
+
continue;
|
|
3388
|
+
}
|
|
2948
3389
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
workspace:
|
|
2952
|
-
|
|
2953
|
-
isGitRepo
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
deleted,
|
|
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';
|
|
3390
|
+
} else {
|
|
3391
|
+
try {
|
|
3392
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
3393
|
+
status.git = gitStatus;
|
|
3394
|
+
if (gitStatus.isGitRepo) {
|
|
3395
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
3396
|
+
} else {
|
|
3397
|
+
status.health = 'degraded';
|
|
3398
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
3399
|
+
}
|
|
3400
|
+
} catch {
|
|
3401
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
3402
|
+
status.health = 'degraded';
|
|
3403
|
+
}
|
|
2974
3404
|
}
|
|
2975
3405
|
}
|
|
2976
3406
|
} else {
|
|
2977
3407
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
2978
3408
|
}
|
|
3409
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
2979
3410
|
nodeStatuses.push(status);
|
|
2980
3411
|
}
|
|
2981
3412
|
|
|
@@ -2985,6 +3416,16 @@ export class DaemonCommandRouter {
|
|
|
2985
3416
|
meshName: mesh.name,
|
|
2986
3417
|
repoIdentity: mesh.repoIdentity,
|
|
2987
3418
|
defaultBranch: mesh.defaultBranch,
|
|
3419
|
+
refreshedAt: new Date().toISOString(),
|
|
3420
|
+
sourceOfTruth: {
|
|
3421
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
3422
|
+
? 'coordinator_inline_mesh_cache'
|
|
3423
|
+
: meshRecord?.source === 'local_config'
|
|
3424
|
+
? 'local_mesh_config'
|
|
3425
|
+
: 'inline_bootstrap_snapshot',
|
|
3426
|
+
coordinatorOwnsLiveTruth: meshRecord?.source !== 'inline_bootstrap',
|
|
3427
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
3428
|
+
},
|
|
2988
3429
|
nodes: nodeStatuses,
|
|
2989
3430
|
queue: { tasks: queue, summary: queueSummary },
|
|
2990
3431
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|