@adhdev/daemon-core 0.9.81 → 0.9.82-rc.10

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';
@@ -85,6 +87,276 @@ function readProviderPriorityFromPolicy(policy: unknown): string[] {
85
87
  });
86
88
  }
87
89
 
90
+ function readObjectRecord(value: unknown): Record<string, any> {
91
+ return value && typeof value === 'object' && !Array.isArray(value)
92
+ ? value as Record<string, any>
93
+ : {};
94
+ }
95
+
96
+ function readStringValue(...values: unknown[]): string | undefined {
97
+ for (const value of values) {
98
+ if (typeof value === 'string' && value.trim()) return value.trim();
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ function readNumberValue(...values: unknown[]): number | undefined {
104
+ for (const value of values) {
105
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
106
+ }
107
+ return undefined;
108
+ }
109
+
110
+ function readBooleanValue(...values: unknown[]): boolean | undefined {
111
+ for (const value of values) {
112
+ if (typeof value === 'boolean') return value;
113
+ }
114
+ return undefined;
115
+ }
116
+
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
+ return {
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) } : {}),
134
+ };
135
+ })
136
+ .filter((entry): entry is GitSubmoduleStatus => entry !== null);
137
+ return submodules.length > 0 ? submodules : undefined;
138
+ }
139
+
140
+ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
141
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
142
+ const cachedGit = readObjectRecord(cachedStatus.git);
143
+ if (Object.keys(cachedGit).length) {
144
+ const conflictFiles = Array.isArray(cachedGit.conflictFiles)
145
+ ? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
146
+ : [];
147
+ const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
148
+ const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
149
+ const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
150
+ if (isGitRepo !== undefined) {
151
+ const submodules = readGitSubmodules(cachedGit.submodules);
152
+ return {
153
+ workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
154
+ repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
155
+ isGitRepo,
156
+ branch: readStringValue(cachedGit.branch) ?? null,
157
+ headCommit: readStringValue(cachedGit.headCommit) ?? null,
158
+ headMessage: readStringValue(cachedGit.headMessage) ?? null,
159
+ upstream: readStringValue(cachedGit.upstream) ?? null,
160
+ ahead: readNumberValue(cachedGit.ahead) ?? 0,
161
+ behind: readNumberValue(cachedGit.behind) ?? 0,
162
+ staged: readNumberValue(cachedGit.staged) ?? 0,
163
+ modified: readNumberValue(cachedGit.modified) ?? 0,
164
+ untracked: readNumberValue(cachedGit.untracked) ?? 0,
165
+ deleted: readNumberValue(cachedGit.deleted) ?? 0,
166
+ renamed: readNumberValue(cachedGit.renamed) ?? 0,
167
+ hasConflicts,
168
+ conflictFiles,
169
+ stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
170
+ lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
171
+ ...(submodules ? { submodules } : {}),
172
+ };
173
+ }
174
+ }
175
+
176
+ const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
177
+ const gitResult = readObjectRecord(rawGit.result);
178
+ const directStatus = readObjectRecord(rawGit.status);
179
+ const nestedStatus = readObjectRecord(gitResult.status);
180
+ const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
181
+ const probeGit = readObjectRecord(rawProbe.git);
182
+ const probeGitResult = readObjectRecord(probeGit.result);
183
+ const probeDirectStatus = readObjectRecord(probeGit.status);
184
+ const probeNestedStatus = readObjectRecord(probeGitResult.status);
185
+ const status = Object.keys(directStatus).length
186
+ ? directStatus
187
+ : Object.keys(nestedStatus).length
188
+ ? nestedStatus
189
+ : Object.keys(probeDirectStatus).length
190
+ ? probeDirectStatus
191
+ : Object.keys(probeNestedStatus).length
192
+ ? probeNestedStatus
193
+ : {};
194
+ const isGitRepo = readBooleanValue(status.isGitRepo);
195
+ if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
196
+ const conflictFiles = Array.isArray(status.conflictFiles)
197
+ ? status.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
198
+ : [];
199
+ const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
200
+ const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
201
+ const submodules = readGitSubmodules(status.submodules);
202
+ return {
203
+ workspace: readStringValue(status.workspace, node?.workspace) || '',
204
+ repoRoot: readStringValue(status.repoRoot, node?.repoRoot, node?.workspace) || null,
205
+ isGitRepo,
206
+ branch: readStringValue(status.branch) ?? null,
207
+ headCommit: readStringValue(status.headCommit) ?? null,
208
+ headMessage: readStringValue(status.headMessage) ?? null,
209
+ upstream: readStringValue(status.upstream) ?? null,
210
+ ahead: readNumberValue(status.ahead) ?? 0,
211
+ behind: readNumberValue(status.behind) ?? 0,
212
+ staged: readNumberValue(status.staged) ?? 0,
213
+ modified: readNumberValue(status.modified) ?? 0,
214
+ untracked: readNumberValue(status.untracked) ?? 0,
215
+ deleted: readNumberValue(status.deleted) ?? 0,
216
+ renamed: readNumberValue(status.renamed) ?? 0,
217
+ hasConflicts,
218
+ conflictFiles,
219
+ stashCount: readNumberValue(status.stashCount) ?? 0,
220
+ lastCheckedAt: Date.now(),
221
+ ...(submodules ? { submodules } : {}),
222
+ };
223
+ }
224
+
225
+ function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
226
+ if (!git) return false;
227
+ return Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
228
+ }
229
+
230
+ function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
231
+ const submodules = Array.isArray(git?.submodules) ? git.submodules : [];
232
+ let dirty = false;
233
+ let outOfSync = false;
234
+ for (const entry of submodules) {
235
+ const submodule = readObjectRecord(entry);
236
+ if (readBooleanValue(submodule.dirty) === true) dirty = true;
237
+ if (readBooleanValue(submodule.outOfSync) === true || !!readStringValue(submodule.error)) outOfSync = true;
238
+ }
239
+ return { dirty, outOfSync };
240
+ }
241
+
242
+ function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
243
+ if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
244
+ const branch = readStringValue(git.branch);
245
+ if (!branch) return 'degraded';
246
+ const submoduleDrift = getGitSubmoduleDriftState(git);
247
+ if (submoduleDrift.outOfSync) return 'degraded';
248
+ if (submoduleDrift.dirty || hasGitWorktreeChanges(git)) return 'dirty';
249
+ return 'online';
250
+ }
251
+
252
+ function readCachedInlineMeshActiveSessions(node: any): string[] {
253
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
254
+ const activeSession = readObjectRecord(cachedStatus.activeSession);
255
+ const fallbackSession = Object.keys(activeSession).length
256
+ ? activeSession
257
+ : readObjectRecord(node?.activeSession ?? node?.active_session);
258
+ const sessionId = readStringValue(fallbackSession.id, fallbackSession.sessionId, fallbackSession.session_id, node?.activeSessionId, node?.active_session_id, node?.sessionId, node?.session_id);
259
+ return sessionId ? [sessionId] : [];
260
+ }
261
+
262
+ function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
263
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
264
+ const activeSession = readObjectRecord(cachedStatus.activeSession);
265
+ const fallbackSession = Object.keys(activeSession).length
266
+ ? activeSession
267
+ : readObjectRecord(node?.activeSession ?? node?.active_session);
268
+ const sessionId = readStringValue(
269
+ fallbackSession.id,
270
+ fallbackSession.sessionId,
271
+ fallbackSession.session_id,
272
+ node?.activeSessionId,
273
+ node?.active_session_id,
274
+ node?.sessionId,
275
+ node?.session_id,
276
+ );
277
+ if (!sessionId) return [];
278
+ return [{
279
+ sessionId,
280
+ providerType: readStringValue(
281
+ fallbackSession.providerType,
282
+ fallbackSession.provider_type,
283
+ fallbackSession.cliType,
284
+ fallbackSession.cli_type,
285
+ fallbackSession.provider,
286
+ node?.providerType,
287
+ node?.provider_type,
288
+ ),
289
+ state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
290
+ lifecycle: readStringValue(fallbackSession.lifecycle),
291
+ title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
292
+ workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
293
+ lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
294
+ recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
295
+ isCached: true,
296
+ }];
297
+ }
298
+
299
+ function readLiveMeshSessionState(record: any): string | undefined {
300
+ return readStringValue(
301
+ record?.meta?.sessionStatus,
302
+ record?.meta?.status,
303
+ record?.meta?.providerStatus,
304
+ record?.status,
305
+ record?.state,
306
+ record?.lifecycle,
307
+ );
308
+ }
309
+
310
+ function toIsoTimestamp(value: unknown): string | null {
311
+ if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
312
+ const stringValue = readStringValue(value);
313
+ return stringValue || null;
314
+ }
315
+
316
+ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
317
+ return {
318
+ sessionId: readStringValue(record?.sessionId) || 'unknown',
319
+ providerType: readStringValue(record?.providerType),
320
+ state: readLiveMeshSessionState(record),
321
+ lifecycle: readStringValue(record?.lifecycle),
322
+ surfaceKind: getSessionHostSurfaceKind(record as any),
323
+ recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
324
+ workspace: readStringValue(record?.workspace) ?? null,
325
+ title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
326
+ lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
327
+ isCached: false,
328
+ };
329
+ }
330
+
331
+ function applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node: any): boolean {
332
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
333
+ const git = buildCachedInlineMeshGitStatus(node);
334
+ const error = readStringValue(cachedStatus.error, node?.error);
335
+ const health = readStringValue(cachedStatus.health, node?.health);
336
+ const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
337
+ const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
338
+ const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
339
+ const activeSessions = readCachedInlineMeshActiveSessions(node);
340
+ const activeSessionDetails = readCachedInlineMeshActiveSessionDetails(node);
341
+ if (!git && !error && !health && !machineStatus && !lastSeenAt && !updatedAt && activeSessions.length === 0) return false;
342
+ if (git) status.git = git;
343
+ if (error) status.error = error;
344
+ if (machineStatus) status.machineStatus = machineStatus;
345
+ if (lastSeenAt) status.lastSeenAt = lastSeenAt;
346
+ if (updatedAt) status.updatedAt = updatedAt;
347
+ if (activeSessions.length > 0) status.activeSessions = activeSessions;
348
+ if (activeSessionDetails.length > 0) status.activeSessionDetails = activeSessionDetails;
349
+ if (health) {
350
+ status.health = health;
351
+ return true;
352
+ }
353
+ if (git) {
354
+ status.health = deriveMeshNodeHealthFromGit(git);
355
+ return true;
356
+ }
357
+ return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
358
+ }
359
+
88
360
  async function resolveProviderTypeFromPriority(args: {
89
361
  nodeId: string;
90
362
  providerPriority: string[];
@@ -502,6 +774,8 @@ export interface CommandRouterDeps {
502
774
  statusVersion?: string;
503
775
  /** Session host control plane */
504
776
  sessionHostControl?: SessionHostControlPlane | null;
777
+ /** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
778
+ getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
505
779
  }
506
780
 
507
781
  export interface CommandRouterResult {
@@ -632,7 +906,16 @@ export class DaemonCommandRouter {
632
906
  return this.inlineMeshCache.get(meshId);
633
907
  }
634
908
 
635
- private async getMeshForCommand(meshId: string, inlineMesh?: unknown): Promise<{ mesh: any; inline: boolean } | null> {
909
+ private async getMeshForCommand(
910
+ meshId: string,
911
+ inlineMesh?: unknown,
912
+ options?: { preferInline?: boolean },
913
+ ): Promise<{ mesh: any; inline: boolean } | null> {
914
+ const preferInline = options?.preferInline === true;
915
+ if (preferInline) {
916
+ const cached = this.getCachedInlineMesh(meshId, inlineMesh);
917
+ if (cached) return { mesh: cached, inline: true };
918
+ }
636
919
  try {
637
920
  const { getMesh } = await import('../config/mesh-config.js');
638
921
  const mesh = getMesh(meshId);
@@ -2734,7 +3017,7 @@ export class DaemonCommandRouter {
2734
3017
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2735
3018
  if (!meshId) return { success: false, error: 'meshId required' };
2736
3019
  try {
2737
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
3020
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
2738
3021
  const mesh = meshRecord?.mesh;
2739
3022
  if (!mesh) return { success: false, error: 'Mesh not found' };
2740
3023
 
@@ -2745,90 +3028,112 @@ export class DaemonCommandRouter {
2745
3028
  const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
2746
3029
  const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
2747
3030
  const ledgerSummary = getLedgerSummary(meshId);
3031
+ const sessionHostRecords = this.deps.sessionHostControl?.listSessions
3032
+ ? await this.deps.sessionHostControl.listSessions().catch(() => [])
3033
+ : [];
3034
+ const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
2748
3035
 
3036
+ const localMachineId = loadConfig().machineId || '';
3037
+ const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
3038
+ ? readStringValue((mesh.nodes[0] as any)?.id, (mesh.nodes[0] as any)?.nodeId)
3039
+ : undefined;
3040
+ const refreshedAt = new Date().toISOString();
2749
3041
  const nodeStatuses = [];
2750
- for (const node of mesh.nodes || []) {
3042
+ for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
3043
+ const nodeId = String(node.id || node.nodeId || '');
3044
+ const daemonId = readStringValue(node.daemonId);
3045
+ const providerPriority = readProviderPriorityFromPolicy(node.policy);
3046
+ const isSelfNode = Boolean(
3047
+ nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
3048
+ ) || Boolean(
3049
+ daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
3050
+ ) || Boolean(meshRecord?.inline && nodeIndex === 0);
2751
3051
  const status: Record<string, unknown> = {
2752
- nodeId: node.id || node.nodeId,
3052
+ nodeId,
2753
3053
  machineLabel: node.machineLabel || node.id || node.nodeId,
2754
3054
  workspace: node.workspace,
2755
3055
  repoRoot: node.repoRoot,
2756
3056
  isLocalWorktree: node.isLocalWorktree,
2757
3057
  worktreeBranch: node.worktreeBranch,
2758
- daemonId: node.daemonId,
3058
+ daemonId,
2759
3059
  machineId: node.machineId,
3060
+ machineStatus: node.machineStatus,
2760
3061
  health: 'unknown',
2761
3062
  providers: node.providers || [],
3063
+ providerPriority,
2762
3064
  activeSessions: [],
3065
+ activeSessionDetails: [],
3066
+ launchReady: false,
2763
3067
  };
3068
+ if (isSelfNode) {
3069
+ status.connection = {
3070
+ perspective: 'selected_coordinator',
3071
+ source: 'mesh_peer_status',
3072
+ state: 'self',
3073
+ transport: 'local',
3074
+ reported: true,
3075
+ reason: 'Selected coordinator daemon',
3076
+ lastStateChangeAt: refreshedAt,
3077
+ };
3078
+ } else if (daemonId) {
3079
+ const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
3080
+ status.connection = connection ?? {
3081
+ perspective: 'selected_coordinator',
3082
+ source: 'not_reported',
3083
+ state: 'unknown',
3084
+ transport: 'unknown',
3085
+ reported: false,
3086
+ reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
3087
+ };
3088
+ } else {
3089
+ status.connection = {
3090
+ perspective: 'selected_coordinator',
3091
+ source: 'not_reported',
3092
+ state: 'unknown',
3093
+ transport: 'unknown',
3094
+ reported: false,
3095
+ reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
3096
+ };
3097
+ }
3098
+ const matchedLiveSessionRecords = liveMeshSessions
3099
+ .filter((record) => this.sessionMatchesMeshNode(record, node, nodeId));
3100
+ if (matchedLiveSessionRecords.length > 0) {
3101
+ const sessionIds = matchedLiveSessionRecords
3102
+ .map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
3103
+ .filter(Boolean);
3104
+ const providerTypes = matchedLiveSessionRecords
3105
+ .map((record: any) => readStringValue(record?.providerType))
3106
+ .filter(Boolean) as string[];
3107
+ status.activeSessions = sessionIds;
3108
+ status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
3109
+ if (providerTypes.length > 0) {
3110
+ status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
3111
+ }
3112
+ }
2764
3113
  if (node.workspace && typeof node.workspace === 'string') {
3114
+ if (!fs.existsSync(node.workspace as string) && applyCachedInlineMeshNodeStatus(status, node)) {
3115
+ status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
3116
+ nodeStatuses.push(status);
3117
+ continue;
3118
+ }
2765
3119
  try {
2766
- const { execFile } = await import('node:child_process');
2767
- const { promisify } = await import('node:util');
2768
- const execFileAsync = promisify(execFile);
2769
-
2770
- const runGit = async (args: string[]): Promise<string> => {
2771
- const result = await execFileAsync('git', ['-C', node.workspace as string, ...args], {
2772
- encoding: 'utf8',
2773
- timeout: 10_000,
2774
- });
2775
- return result.stdout.trim();
2776
- };
2777
-
2778
- const branch = await runGit(['branch', '--show-current']).catch(() => '');
2779
- const porc = await runGit(['status', '--porcelain']).catch(() => '');
2780
- const headCommit = await runGit(['rev-parse', '--short', 'HEAD']).catch(() => null);
2781
- const headMessage = await runGit(['log', '-1', '--format=%s']).catch(() => null);
2782
- const upstream = await runGit(['rev-parse', '--abbrev-ref', '@{upstream}']).catch(() => null);
2783
- const aheadBehind = await runGit(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']).catch(() => '');
2784
- const stashCount = await runGit(['stash', 'list']).catch(() => '');
2785
-
2786
- let ahead = 0, behind = 0;
2787
- if (aheadBehind) {
2788
- const parts = aheadBehind.split(/\s+/);
2789
- if (parts.length >= 2) {
2790
- behind = parseInt(parts[0], 10) || 0;
2791
- ahead = parseInt(parts[1], 10) || 0;
2792
- }
3120
+ const gitStatus = await getGitRepoStatus(node.workspace as string, { timeoutMs: 10_000, refreshUpstream: true });
3121
+ status.git = gitStatus;
3122
+ if (gitStatus.isGitRepo) {
3123
+ status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
3124
+ } else {
3125
+ status.health = 'degraded';
3126
+ if (gitStatus.error && !status.error) status.error = gitStatus.error;
2793
3127
  }
2794
-
2795
- const dirty = porc.length > 0;
2796
- const lines = porc ? porc.split('\n').filter(Boolean) : [];
2797
- let staged = 0, modified = 0, untracked = 0, deleted = 0, renamed = 0;
2798
- for (const line of lines) {
2799
- const xy = line.slice(0, 2);
2800
- if (xy[0] !== ' ' && xy[0] !== '?') staged++;
2801
- if (xy[1] === 'M') modified++;
2802
- if (xy[1] === 'D') deleted++;
2803
- if (xy[0] === 'R' || xy[1] === 'R') renamed++;
2804
- if (xy === '??') untracked++;
2805
- }
2806
-
2807
- status.git = {
2808
- workspace: node.workspace,
2809
- repoRoot: node.workspace,
2810
- isGitRepo: true,
2811
- branch: branch || null,
2812
- headCommit,
2813
- headMessage,
2814
- upstream,
2815
- ahead,
2816
- behind,
2817
- staged,
2818
- modified,
2819
- untracked,
2820
- deleted,
2821
- renamed,
2822
- hasConflicts: false,
2823
- conflictFiles: [],
2824
- stashCount: stashCount ? stashCount.split('\n').filter(Boolean).length : 0,
2825
- lastCheckedAt: Date.now(),
2826
- };
2827
- status.health = branch ? (dirty ? 'dirty' : 'online') : 'degraded';
2828
3128
  } catch {
2829
- status.health = 'degraded';
3129
+ if (!applyCachedInlineMeshNodeStatus(status, node)) {
3130
+ status.health = 'degraded';
3131
+ }
2830
3132
  }
3133
+ } else {
3134
+ applyCachedInlineMeshNodeStatus(status, node);
2831
3135
  }
3136
+ status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
2832
3137
  nodeStatuses.push(status);
2833
3138
  }
2834
3139
 
@@ -2838,6 +3143,7 @@ export class DaemonCommandRouter {
2838
3143
  meshName: mesh.name,
2839
3144
  repoIdentity: mesh.repoIdentity,
2840
3145
  defaultBranch: mesh.defaultBranch,
3146
+ refreshedAt: new Date().toISOString(),
2841
3147
  nodes: nodeStatuses,
2842
3148
  queue: { tasks: queue, summary: queueSummary },
2843
3149
  ledger: { entries: ledgerEntries, summary: ledgerSummary },
@@ -62,7 +62,7 @@ export interface GitPushResult extends GitRepoIdentity {
62
62
  }
63
63
 
64
64
  export interface GitCommandServices {
65
- getStatus?: (params: { workspace: string }) => Promise<GitRepoStatus> | GitRepoStatus;
65
+ getStatus?: (params: { workspace: string; refreshUpstream?: boolean }) => Promise<GitRepoStatus> | GitRepoStatus;
66
66
  getDiffSummary?: (params: { workspace: string; staged?: boolean }) => Promise<GitDiffSummary> | GitDiffSummary;
67
67
  getDiffFile?: (params: { workspace: string; path: string; staged?: boolean }) => Promise<GitFileDiff> | GitFileDiff;
68
68
  createSnapshot?: (params: {
@@ -171,7 +171,7 @@ const defaultSnapshotStore = createGitSnapshotStore({
171
171
 
172
172
  export function createDefaultGitCommandServices(): GitCommandServices {
173
173
  return {
174
- getStatus: ({ workspace }) => getGitRepoStatus(workspace),
174
+ getStatus: ({ workspace, refreshUpstream }) => getGitRepoStatus(workspace, { refreshUpstream }),
175
175
  getDiffSummary: ({ workspace }) => getGitDiffSummary(workspace),
176
176
  getDiffFile: ({ workspace, path: filePath }) => getGitFileDiff(workspace, filePath),
177
177
  createSnapshot: ({ workspace, reason, sessionId, turnId }) => defaultSnapshotStore.create({
@@ -290,7 +290,7 @@ export async function handleGitCommand(
290
290
  switch (command) {
291
291
  case 'git_status': {
292
292
  if (!services.getStatus) return serviceNotImplemented(command);
293
- const status = await runService(() => services.getStatus!({ workspace }));
293
+ const status = await runService(() => services.getStatus!({ workspace, refreshUpstream: optionalBoolean(args?.refreshUpstream) }));
294
294
  return 'success' in status ? status : { success: true, status };
295
295
  }
296
296