@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.
@@ -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 buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
116
- const cachedStatus = readObjectRecord(node?.cachedStatus);
117
- const cachedGit = readObjectRecord(cachedStatus.git);
118
- if (Object.keys(cachedGit).length) {
119
- const conflictFiles = Array.isArray(cachedGit.conflictFiles)
120
- ? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
121
- : [];
122
- const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
123
- const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
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
- workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
128
- repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
129
- isGitRepo,
130
- branch: readStringValue(cachedGit.branch) ?? null,
131
- headCommit: readStringValue(cachedGit.headCommit) ?? null,
132
- headMessage: readStringValue(cachedGit.headMessage) ?? null,
133
- upstream: readStringValue(cachedGit.upstream) ?? null,
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
- const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
150
- const gitResult = readObjectRecord(rawGit.result);
151
- const directStatus = readObjectRecord(rawGit.status);
152
- const nestedStatus = readObjectRecord(gitResult.status);
153
- const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
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 applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node: any): boolean {
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
- const git = buildCachedInlineMeshGitStatus(node);
199
- const error = readStringValue(cachedStatus.error, node?.error);
200
- const health = readStringValue(cachedStatus.health, node?.health);
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
- if (!git && !error && !health) return false;
203
- if (!machineStatus && !git && !error) return false;
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
- const dirty = Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
212
- status.health = git.isGitRepo === false ? 'degraded' : dirty ? 'dirty' : 'online';
530
+ status.health = deriveMeshNodeHealthFromGit(git);
213
531
  return true;
214
532
  }
215
- return false;
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.inlineMeshCache.set(meshId, inlineMesh as any);
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, inlineMesh);
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, inlineMesh);
781
- return cached ? { mesh: cached, inline: true } : null;
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 events = drainPendingMeshCoordinatorEvents();
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
- try {
1941
- const { getMesh } = await import('../config/mesh-config.js');
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 workspace = typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '';
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: node.id || node.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: node.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 (node.workspace && typeof node.workspace === 'string') {
2904
- if (!fs.existsSync(node.workspace as string) && applyCachedInlineMeshNodeStatus(status, node)) {
2905
- nodeStatuses.push(status);
2906
- continue;
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
- try {
2909
- const { execFile } = await import('node:child_process');
2910
- const { promisify } = await import('node:util');
2911
- const execFileAsync = promisify(execFile);
2912
-
2913
- const runGit = async (args: string[]): Promise<string> => {
2914
- const result = await execFileAsync('git', ['-C', node.workspace as string, ...args], {
2915
- encoding: 'utf8',
2916
- timeout: 10_000,
2917
- });
2918
- return result.stdout.trim();
2919
- };
2920
-
2921
- const branch = await runGit(['branch', '--show-current']).catch(() => '');
2922
- const porc = await runGit(['status', '--porcelain']).catch(() => '');
2923
- const headCommit = await runGit(['rev-parse', '--short', 'HEAD']).catch(() => null);
2924
- const headMessage = await runGit(['log', '-1', '--format=%s']).catch(() => null);
2925
- const upstream = await runGit(['rev-parse', '--abbrev-ref', '@{upstream}']).catch(() => null);
2926
- const aheadBehind = await runGit(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']).catch(() => '');
2927
- const stashCount = await runGit(['stash', 'list']).catch(() => '');
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
- const dirty = porc.length > 0;
2939
- const lines = porc ? porc.split('\n').filter(Boolean) : [];
2940
- let staged = 0, modified = 0, untracked = 0, deleted = 0, renamed = 0;
2941
- for (const line of lines) {
2942
- const xy = line.slice(0, 2);
2943
- if (xy[0] !== ' ' && xy[0] !== '?') staged++;
2944
- if (xy[1] === 'M') modified++;
2945
- if (xy[1] === 'D') deleted++;
2946
- if (xy[0] === 'R' || xy[1] === 'R') renamed++;
2947
- if (xy === '??') untracked++;
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
- status.git = {
2951
- workspace: node.workspace,
2952
- repoRoot: node.workspace,
2953
- isGitRepo: true,
2954
- branch: branch || null,
2955
- headCommit,
2956
- headMessage,
2957
- upstream,
2958
- ahead,
2959
- behind,
2960
- staged,
2961
- modified,
2962
- untracked,
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 },