@adhdev/daemon-core 0.9.82-rc.2 → 0.9.82-rc.21

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.
@@ -1,12 +1,25 @@
1
- import type { GitRepoStatus, GitSubmoduleStatus } from './git-types.js';
1
+ import type { GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
2
2
  import { GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
3
3
 
4
+ type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo: boolean };
5
+
4
6
  export interface GitStatusOptions {
5
7
  timeoutMs?: number;
6
8
  /** When true, include submodule status in the result. Defaults to true. */
7
9
  includeSubmodules?: boolean;
8
10
  /** Optional filter to exclude specific submodule paths from status */
9
11
  submoduleIgnorePaths?: string[];
12
+ /**
13
+ * When true, refresh the tracked remote before trusting ahead/behind.
14
+ * Callers should opt into this only for convergence-critical surfaces.
15
+ */
16
+ refreshUpstream?: boolean;
17
+ }
18
+
19
+ interface GitUpstreamProbe {
20
+ upstreamStatus: GitUpstreamFreshness;
21
+ upstreamFetchedAt?: number;
22
+ upstreamFetchError?: string;
10
23
  }
11
24
 
12
25
  export async function getGitRepoStatus(
@@ -18,8 +31,16 @@ export async function getGitRepoStatus(
18
31
 
19
32
  try {
20
33
  const repo = await resolveGitRepository(workspace, options);
21
- const statusOutput = await runGit(repo, ['status', '--porcelain=v2', '--branch'], options);
22
- const parsed = parsePorcelainV2Status(statusOutput.stdout);
34
+ let parsed = await readPorcelainStatus(repo, options);
35
+ let upstreamProbe: GitUpstreamProbe = getInitialUpstreamProbe(parsed);
36
+
37
+ if (options.refreshUpstream) {
38
+ upstreamProbe = await refreshTrackedUpstream(repo, parsed, options);
39
+ if (upstreamProbe.upstreamStatus === 'fresh') {
40
+ parsed = await readPorcelainStatus(repo, options);
41
+ }
42
+ }
43
+
23
44
  const head = await readHead(repo, options);
24
45
  const stashCount = await readStashCount(repo, options);
25
46
 
@@ -36,6 +57,9 @@ export async function getGitRepoStatus(
36
57
  headCommit: head.commit,
37
58
  headMessage: head.message,
38
59
  upstream: parsed.upstream,
60
+ upstreamStatus: parsed.upstream ? upstreamProbe.upstreamStatus : 'no_upstream',
61
+ upstreamFetchedAt: upstreamProbe.upstreamFetchedAt,
62
+ upstreamFetchError: upstreamProbe.upstreamFetchError,
39
63
  ahead: parsed.ahead,
40
64
  behind: parsed.behind,
41
65
  staged: parsed.staged,
@@ -74,6 +98,72 @@ interface ParsedPorcelainStatus {
74
98
  conflictFiles: string[];
75
99
  }
76
100
 
101
+ async function readPorcelainStatus(repo: ResolvedGitRepo, options: GitStatusOptions): Promise<ParsedPorcelainStatus> {
102
+ const statusOutput = await runGit(repo, ['status', '--porcelain=v2', '--branch'], options);
103
+ return parsePorcelainV2Status(statusOutput.stdout);
104
+ }
105
+
106
+ function getInitialUpstreamProbe(parsed: ParsedPorcelainStatus): GitUpstreamProbe {
107
+ return {
108
+ upstreamStatus: parsed.upstream ? 'unchecked' : 'no_upstream',
109
+ };
110
+ }
111
+
112
+ async function refreshTrackedUpstream(
113
+ repo: ResolvedGitRepo,
114
+ parsed: ParsedPorcelainStatus,
115
+ options: GitStatusOptions,
116
+ ): Promise<GitUpstreamProbe> {
117
+ if (!parsed.upstream || !parsed.branch) {
118
+ return { upstreamStatus: 'no_upstream' };
119
+ }
120
+
121
+ const remoteName = (await readBranchRemote(repo, parsed.branch, options)) ?? inferRemoteName(parsed.upstream);
122
+ if (!remoteName) {
123
+ return {
124
+ upstreamStatus: 'stale',
125
+ upstreamFetchError: `Unable to resolve remote for upstream '${parsed.upstream}'`,
126
+ };
127
+ }
128
+
129
+ try {
130
+ await runGit(repo, ['fetch', '--quiet', '--prune', '--no-tags', remoteName], options);
131
+ return {
132
+ upstreamStatus: 'fresh',
133
+ upstreamFetchedAt: Date.now(),
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ upstreamStatus: 'stale',
138
+ upstreamFetchError: formatGitError(error),
139
+ };
140
+ }
141
+ }
142
+
143
+ async function readBranchRemote(repo: ResolvedGitRepo, branch: string, options: GitStatusOptions): Promise<string | null> {
144
+ try {
145
+ const result = await runGit(repo, ['config', '--get', `branch.${branch}.remote`], options);
146
+ return result.stdout.trim() || null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ function inferRemoteName(upstream: string): string | null {
153
+ const [remoteName] = upstream.split('/');
154
+ return remoteName?.trim() || null;
155
+ }
156
+
157
+ function formatGitError(error: unknown): string {
158
+ if (error instanceof GitCommandError) {
159
+ return error.stderr || error.message;
160
+ }
161
+ if (error instanceof Error) {
162
+ return error.message;
163
+ }
164
+ return String(error);
165
+ }
166
+
77
167
  export function parsePorcelainV2Status(output: string): ParsedPorcelainStatus {
78
168
  const parsed: ParsedPorcelainStatus = {
79
169
  branch: null,
@@ -145,7 +235,7 @@ export function parsePorcelainV2Status(output: string): ParsedPorcelainStatus {
145
235
  }
146
236
 
147
237
  async function readHead(
148
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
238
+ repo: ResolvedGitRepo,
149
239
  options: GitStatusOptions,
150
240
  ): Promise<{ commit: string | null; message: string | null }> {
151
241
  try {
@@ -163,7 +253,7 @@ async function readHead(
163
253
  }
164
254
 
165
255
  async function readStashCount(
166
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
256
+ repo: ResolvedGitRepo,
167
257
  options: GitStatusOptions,
168
258
  ): Promise<number> {
169
259
  try {
@@ -187,6 +277,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
187
277
  headCommit: null,
188
278
  headMessage: null,
189
279
  upstream: null,
280
+ upstreamStatus: 'unavailable',
190
281
  ahead: 0,
191
282
  behind: 0,
192
283
  staged: 0,
@@ -206,7 +297,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
206
297
  // ─── Submodule Status ───────────────────────────
207
298
 
208
299
  async function getSubmoduleStatuses(
209
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
300
+ repo: ResolvedGitRepo,
210
301
  options: GitStatusOptions,
211
302
  ): Promise<GitSubmoduleStatus[]> {
212
303
  if (!repo.repoRoot) return [];
@@ -22,6 +22,9 @@ export function createGitCompactSummary(status: GitRepoStatus, diffSummary?: Git
22
22
  isGitRepo: status.isGitRepo,
23
23
  repoRoot: status.repoRoot,
24
24
  branch: status.branch,
25
+ upstreamStatus: status.upstreamStatus,
26
+ upstreamFetchedAt: status.upstreamFetchedAt,
27
+ upstreamFetchError: status.upstreamFetchError,
25
28
  dirty:
26
29
  status.staged > 0 ||
27
30
  status.modified > 0 ||
@@ -40,11 +40,19 @@ export interface GitSubmoduleStatus {
40
40
  error?: string;
41
41
  }
42
42
 
43
+ export type GitUpstreamFreshness = 'fresh' | 'unchecked' | 'stale' | 'no_upstream' | 'unavailable';
44
+
43
45
  export interface GitRepoStatus extends GitRepoIdentity {
44
46
  branch: string | null;
45
47
  headCommit: string | null;
46
48
  headMessage: string | null;
47
49
  upstream: string | null;
50
+ /** Whether ahead/behind was verified against a freshly fetched upstream ref. */
51
+ upstreamStatus: GitUpstreamFreshness;
52
+ /** Timestamp for the fetch that refreshed upstream refs when upstreamStatus === 'fresh'. */
53
+ upstreamFetchedAt?: number;
54
+ /** Error from the last refresh attempt when upstreamStatus === 'stale'. */
55
+ upstreamFetchError?: string;
48
56
  ahead: number;
49
57
  behind: number;
50
58
  staged: number;
@@ -134,6 +142,9 @@ export interface GitCompactSummary {
134
142
  isGitRepo: boolean;
135
143
  repoRoot: string | null;
136
144
  branch: string | null;
145
+ upstreamStatus: GitUpstreamFreshness;
146
+ upstreamFetchedAt?: number;
147
+ upstreamFetchError?: string;
137
148
  dirty: boolean;
138
149
  changedFiles: number;
139
150
  ahead: number;
package/src/index.ts CHANGED
@@ -103,6 +103,14 @@ export type {
103
103
  LocalMeshNodeEntry,
104
104
  RepoMeshStatus,
105
105
  RepoMeshNodeStatus,
106
+ RepoMeshSessionStatus,
107
+ RepoMeshQueueTask,
108
+ RepoMeshQueueTaskStatus,
109
+ RepoMeshQueueSummary,
110
+ RepoMeshQueueStatus,
111
+ RepoMeshLedgerEntryStatus,
112
+ RepoMeshLedgerSummaryStatus,
113
+ RepoMeshLedgerStatus,
106
114
  } from './repo-mesh-types.js';
107
115
  export { DEFAULT_MESH_POLICY } from './repo-mesh-types.js';
108
116
 
@@ -168,7 +176,7 @@ export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats } from './m
168
176
  // export type { MeshGraph, MeshGraphNode, MeshGraphEdge, MeshGraphNodeType, MeshGraphEdgeType } from './mesh/mesh-visualization.js';
169
177
 
170
178
  // ── Mesh Events ──
171
- export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents } from './mesh/mesh-events.js';
179
+ export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
172
180
  export type { PendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
173
181
 
174
182
  // ── Mesh P2P Relay Failure Classification ──
@@ -32,6 +32,8 @@ export interface PendingMeshCoordinatorEvent {
32
32
  event: string;
33
33
  meshId: string;
34
34
  nodeLabel: string;
35
+ nodeId?: string;
36
+ workspace?: string;
35
37
  metadataEvent: Record<string, unknown>;
36
38
  queuedAt: number;
37
39
  }
@@ -39,6 +41,14 @@ export interface PendingMeshCoordinatorEvent {
39
41
  const MAX_PENDING_EVENTS = 50;
40
42
  const pendingMeshCoordinatorEvents: PendingMeshCoordinatorEvent[] = [];
41
43
 
44
+ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
45
+ if (pendingMeshCoordinatorEvents.length >= MAX_PENDING_EVENTS) {
46
+ return false;
47
+ }
48
+ pendingMeshCoordinatorEvents.push(event);
49
+ return true;
50
+ }
51
+
42
52
  /** Drain and return all pending coordinator events, clearing the queue. */
43
53
  export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
44
54
  return pendingMeshCoordinatorEvents.splice(0);
@@ -781,17 +791,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
781
791
 
782
792
  if (coordinatorInstances.length === 0) {
783
793
  // No CLI coordinator session found — buffer for MCP-based coordinators.
784
- if (pendingMeshCoordinatorEvents.length < MAX_PENDING_EVENTS) {
785
- pendingMeshCoordinatorEvents.push({
794
+ if (queuePendingMeshCoordinatorEvent({
786
795
  event: args.event,
787
796
  meshId: args.meshId,
788
797
  nodeLabel: args.nodeLabel,
798
+ nodeId: args.nodeId || undefined,
799
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
789
800
  metadataEvent: {
790
801
  ...args.metadataEvent,
791
802
  ...(recoveryContext ? { recoveryContext } : {}),
792
803
  },
793
804
  queuedAt: Date.now(),
794
- });
805
+ })) {
795
806
  LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
796
807
  }
797
808
  return { success: true, forwarded: 0 };
@@ -1,9 +1,11 @@
1
1
  import type { ChatMessage } from '../types.js';
2
2
  import { flattenContent } from './contracts.js';
3
3
 
4
+ export const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 4_000;
5
+
4
6
  export function extractFinalSummaryFromMessages(
5
7
  messages: ChatMessage[] | null | undefined,
6
- maxChars: number = 500,
8
+ maxChars: number = DEFAULT_FINAL_SUMMARY_MAX_CHARS,
7
9
  ): string {
8
10
  if (!Array.isArray(messages) || messages.length === 0) return '';
9
11
 
@@ -259,17 +259,149 @@ export interface RepoMeshStatus {
259
259
  meshId: string;
260
260
  meshName: string;
261
261
  repoIdentity: string;
262
+ defaultBranch?: string;
262
263
  refreshedAt: string;
263
264
  nodes: RepoMeshNodeStatus[];
265
+ queue?: RepoMeshQueueStatus;
266
+ ledger?: RepoMeshLedgerStatus;
267
+ }
268
+
269
+ export interface RepoMeshSessionStatus {
270
+ sessionId: string;
271
+ providerType?: string;
272
+ state?: string;
273
+ lifecycle?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'interrupted';
274
+ surfaceKind?: 'live_runtime' | 'recovery_snapshot' | 'inactive_record';
275
+ recoveryState?: string | null;
276
+ workspace?: string | null;
277
+ title?: string | null;
278
+ lastActivityAt?: string | null;
279
+ isCached?: boolean;
280
+ }
281
+
282
+ export type RepoMeshPeerConnectionState = 'self' | 'connected' | 'connecting' | 'disconnected' | 'failed' | 'closed' | 'unknown';
283
+ export type RepoMeshPeerConnectionTransport = 'local' | 'direct' | 'relay' | 'unknown';
284
+
285
+ export interface RepoMeshPeerConnectionStatus {
286
+ perspective: 'selected_coordinator';
287
+ source: 'mesh_peer_status' | 'not_reported';
288
+ state: RepoMeshPeerConnectionState;
289
+ transport: RepoMeshPeerConnectionTransport;
290
+ reported: boolean;
291
+ reason?: string;
292
+ lastStateChangeAt?: string;
293
+ lastConnectedAt?: string;
294
+ lastCommandAt?: string;
264
295
  }
265
296
 
266
297
  export interface RepoMeshNodeStatus {
267
298
  nodeId: string;
268
299
  machineLabel: string;
269
300
  workspace: string;
301
+ repoRoot?: string;
302
+ daemonId?: string;
303
+ machineId?: string;
304
+ machineStatus?: string;
305
+ isLocalWorktree?: boolean;
306
+ worktreeBranch?: string;
270
307
  health: RepoMeshNodeHealth;
271
308
  git?: GitRepoStatus;
272
309
  providers: string[];
273
310
  activeSessions: string[];
311
+ activeSessionDetails?: RepoMeshSessionStatus[];
312
+ providerPriority?: string[];
313
+ launchReady?: boolean;
314
+ lastSeenAt?: string;
315
+ updatedAt?: string;
316
+ connection?: RepoMeshPeerConnectionStatus;
274
317
  error?: string;
275
318
  }
319
+
320
+ export type RepoMeshQueueTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
321
+
322
+ export interface RepoMeshQueueTask {
323
+ id: string;
324
+ meshId: string;
325
+ message: string;
326
+ status: RepoMeshQueueTaskStatus;
327
+ targetNodeId?: string;
328
+ targetSessionId?: string;
329
+ assignedNodeId?: string;
330
+ assignedSessionId?: string;
331
+ cancelReason?: string;
332
+ cancelledAt?: string;
333
+ requeueReason?: string;
334
+ requeuedAt?: string;
335
+ requeueCount?: number;
336
+ autoLaunch?: {
337
+ status: 'skipped' | 'started' | 'failed' | 'completed';
338
+ reason?: string;
339
+ nodeId?: string;
340
+ providerType?: string;
341
+ sessionId?: string;
342
+ updatedAt: string;
343
+ };
344
+ dispatchTimestamp?: string;
345
+ createdAt: string;
346
+ updatedAt: string;
347
+ }
348
+
349
+ export interface RepoMeshQueueSummary {
350
+ total: number;
351
+ active: number;
352
+ historical: number;
353
+ pending: number;
354
+ assigned: number;
355
+ completed: number;
356
+ failed: number;
357
+ cancelled: number;
358
+ activeCounts: {
359
+ pending: number;
360
+ assigned: number;
361
+ };
362
+ historicalCounts: {
363
+ completed: number;
364
+ failed: number;
365
+ cancelled: number;
366
+ };
367
+ activeAssignments: Array<{
368
+ id: string;
369
+ nodeId?: string;
370
+ sessionId?: string;
371
+ message: string;
372
+ }>;
373
+ }
374
+
375
+ export interface RepoMeshQueueStatus {
376
+ tasks: RepoMeshQueueTask[];
377
+ summary: RepoMeshQueueSummary;
378
+ }
379
+
380
+ export interface RepoMeshLedgerEntryStatus {
381
+ id: string;
382
+ meshId: string;
383
+ timestamp: string;
384
+ kind: string;
385
+ nodeId?: string;
386
+ sessionId?: string;
387
+ providerType?: string;
388
+ payload: Record<string, unknown>;
389
+ }
390
+
391
+ export interface RepoMeshLedgerSummaryStatus {
392
+ meshId: string;
393
+ totalEntries: number;
394
+ taskDispatched: number;
395
+ taskCompleted: number;
396
+ taskFailed: number;
397
+ taskStalled: number;
398
+ sessionLaunched: number;
399
+ checkpointCreated: number;
400
+ lastActivityAt: string | null;
401
+ recentFailures: number;
402
+ }
403
+
404
+ export interface RepoMeshLedgerStatus {
405
+ entries: RepoMeshLedgerEntryStatus[];
406
+ summary: RepoMeshLedgerSummaryStatus;
407
+ }