@adhdev/daemon-core 0.9.82-rc.365 → 0.9.82-rc.367
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/commands/chat-commands.d.ts +27 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +60 -0
- package/dist/commands/router.d.ts +242 -0
- package/dist/index.js +2100 -1769
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2085 -1754
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +8 -0
- package/package.json +2 -2
- package/src/commands/chat-commands.ts +96 -2
- package/src/commands/cli-manager.ts +28 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +592 -0
- package/src/commands/high-family/mesh-events.ts +47 -0
- package/src/commands/high-family/mesh-status.ts +639 -0
- package/src/commands/high-family/types.ts +76 -0
- package/src/commands/router.ts +407 -1254
- package/src/mesh/mesh-events-coordinator.ts +78 -13
- package/src/mesh/mesh-reconcile-loop.ts +38 -0
package/src/commands/router.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { lowFamilyRegistry } from './low-family/index.js';
|
|
|
16
16
|
import { medFamilyRegistry } from './med-family/index.js';
|
|
17
17
|
import { launchIde } from './med-family/ide.js';
|
|
18
18
|
import type { MedFamilyContext } from './med-family/index.js';
|
|
19
|
+
import { highFamilyRegistry } from './high-family/index.js';
|
|
20
|
+
import type { HighFamilyContext } from './high-family/index.js';
|
|
19
21
|
import { DaemonCliManager } from './cli-manager.js';
|
|
20
22
|
import { supportsExplicitSessionResume } from './cli-manager.js';
|
|
21
23
|
import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
normalizeMeshNodeId,
|
|
46
48
|
meshNodeIdMatches,
|
|
47
49
|
daemonIdsEquivalent,
|
|
50
|
+
meshWorkspacesEquivalent,
|
|
48
51
|
} from '@adhdev/mesh-shared';
|
|
49
52
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
50
53
|
import { LOG } from '../logging/logger.js';
|
|
@@ -52,17 +55,12 @@ import { logCommand } from '../logging/command-log.js';
|
|
|
52
55
|
import type { CommandLogEntry } from '../logging/command-log.js';
|
|
53
56
|
import * as yaml from 'js-yaml';
|
|
54
57
|
import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
|
|
55
|
-
import { getSessionHostSurfaceKind
|
|
56
|
-
import {
|
|
57
|
-
import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
|
|
58
|
-
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
59
|
-
import { getRecentUnroutableDeliveries } from '../mesh/mesh-routing.js';
|
|
58
|
+
import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
|
|
59
|
+
import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
60
60
|
import { buildMeshWorkerRelayStamp } from '../mesh/mesh-events-utils.js';
|
|
61
|
-
import { buildMeshHostRequiredFailure,
|
|
61
|
+
import { buildMeshHostRequiredFailure, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
62
62
|
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
63
63
|
import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
|
|
64
|
-
import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
|
|
65
|
-
import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
|
|
66
64
|
import {
|
|
67
65
|
MESH_REFINE_CONFIG_LOCATIONS,
|
|
68
66
|
MESH_REFINE_CONFIG_SCHEMA,
|
|
@@ -88,11 +86,10 @@ import { homedir, hostname as osHostname } from 'os';
|
|
|
88
86
|
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
89
87
|
import * as fs from 'fs';
|
|
90
88
|
import { execFileSync } from 'node:child_process';
|
|
91
|
-
import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
|
|
92
89
|
import { workingDirBasename } from '../providers/working-dir.js';
|
|
93
90
|
import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
|
|
94
91
|
|
|
95
|
-
function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
92
|
+
export function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
96
93
|
const record = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
97
94
|
? policy as Record<string, unknown>
|
|
98
95
|
: {};
|
|
@@ -153,7 +150,7 @@ function readNumberValue(...values: unknown[]): number | undefined {
|
|
|
153
150
|
return undefined;
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
153
|
+
export function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
157
154
|
for (const value of values) {
|
|
158
155
|
if (typeof value === 'boolean') return value;
|
|
159
156
|
}
|
|
@@ -163,7 +160,7 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
163
160
|
// summarizeRepoMeshDebugGit was a hand-synced copy of the cloud git-shape
|
|
164
161
|
// summarizer; both now call shared summarizeGitShape (@adhdev/mesh-shared).
|
|
165
162
|
|
|
166
|
-
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
163
|
+
export function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
167
164
|
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
168
165
|
return {
|
|
169
166
|
success: status?.success,
|
|
@@ -195,7 +192,7 @@ function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
|
195
192
|
};
|
|
196
193
|
}
|
|
197
194
|
|
|
198
|
-
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
195
|
+
export function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
199
196
|
try {
|
|
200
197
|
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
201
198
|
} catch {
|
|
@@ -257,7 +254,7 @@ function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined
|
|
|
257
254
|
);
|
|
258
255
|
}
|
|
259
256
|
|
|
260
|
-
function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
257
|
+
export function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
261
258
|
return readStringValue(
|
|
262
259
|
node.hostname,
|
|
263
260
|
node.host,
|
|
@@ -297,7 +294,7 @@ function compactMeshIdentityEvidence(value: string | undefined): string | undefi
|
|
|
297
294
|
return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
|
|
298
295
|
}
|
|
299
296
|
|
|
300
|
-
function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
297
|
+
export function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
301
298
|
localMachineId?: string;
|
|
302
299
|
localDaemonId?: string;
|
|
303
300
|
coordinatorHostname?: string;
|
|
@@ -357,7 +354,7 @@ function normalizeInlineMeshGitStatus(
|
|
|
357
354
|
return sharedNormalizeGitStatus(status, readObjectRecord(node), options) as Record<string, unknown> | undefined;
|
|
358
355
|
}
|
|
359
356
|
|
|
360
|
-
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
357
|
+
export function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
361
358
|
return sharedPickBestTransitGitStatus(readObjectRecord(node), { lastCheckedAt: Date.now() }) as Record<string, unknown> | undefined;
|
|
362
359
|
}
|
|
363
360
|
|
|
@@ -370,7 +367,7 @@ function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDi
|
|
|
370
367
|
});
|
|
371
368
|
}
|
|
372
369
|
|
|
373
|
-
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
370
|
+
export function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
374
371
|
const source = readStringValue(connection.source);
|
|
375
372
|
const transport = readStringValue(connection.transport);
|
|
376
373
|
return {
|
|
@@ -385,7 +382,7 @@ function buildLivePeerGitConnection(connection: Record<string, unknown>, timesta
|
|
|
385
382
|
};
|
|
386
383
|
}
|
|
387
384
|
|
|
388
|
-
function recordInlineMeshDirectGitTruth(
|
|
385
|
+
export function recordInlineMeshDirectGitTruth(
|
|
389
386
|
node: any,
|
|
390
387
|
git: Record<string, unknown>,
|
|
391
388
|
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
@@ -462,7 +459,7 @@ function stampNodeReporterPlatform(node: any, platform: string | null, arch: str
|
|
|
462
459
|
* Fire-and-forget (same pattern as the worktreeBootstrap writer) — a persistence
|
|
463
460
|
* failure must never block the status response.
|
|
464
461
|
*/
|
|
465
|
-
function persistNodeReporterPlatform(
|
|
462
|
+
export function persistNodeReporterPlatform(
|
|
466
463
|
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config',
|
|
467
464
|
mesh: any,
|
|
468
465
|
nodeId: string | undefined,
|
|
@@ -736,7 +733,7 @@ function isInlineMeshAutoFastForwardEligible(git: Record<string, unknown> | null
|
|
|
736
733
|
return dirty !== true && countGitWorktreeChanges(git) === 0;
|
|
737
734
|
}
|
|
738
735
|
|
|
739
|
-
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
736
|
+
export function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
740
737
|
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
741
738
|
const branch = readStringValue(git.branch);
|
|
742
739
|
if (!branch) return 'degraded';
|
|
@@ -875,7 +872,7 @@ function buildInlineMeshBranchConvergence(args: {
|
|
|
875
872
|
};
|
|
876
873
|
}
|
|
877
874
|
|
|
878
|
-
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
875
|
+
export function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
879
876
|
const git = readObjectRecord(status.git);
|
|
880
877
|
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
881
878
|
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
@@ -890,7 +887,7 @@ function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<s
|
|
|
890
887
|
}
|
|
891
888
|
}
|
|
892
889
|
|
|
893
|
-
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
890
|
+
export function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
894
891
|
const followUps = nodes
|
|
895
892
|
.filter(node => {
|
|
896
893
|
if (readObjectRecord(node.branchConvergence).needsConvergence !== true) return false;
|
|
@@ -1076,7 +1073,7 @@ function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknow
|
|
|
1076
1073
|
}
|
|
1077
1074
|
}
|
|
1078
1075
|
|
|
1079
|
-
function finalizeMeshNodeStatus(args: {
|
|
1076
|
+
export function finalizeMeshNodeStatus(args: {
|
|
1080
1077
|
status: Record<string, unknown>;
|
|
1081
1078
|
node: any;
|
|
1082
1079
|
daemonId?: string;
|
|
@@ -1132,8 +1129,23 @@ function readMeshTimeoutEnvMs(name: string, defaultMs: number): number {
|
|
|
1132
1129
|
// round-trip to slow (often TURN-relayed) peers, so such a node was permanently
|
|
1133
1130
|
// marked unavailable and blocked the whole mesh graph. Default raised to 25s
|
|
1134
1131
|
// (still under the P2P REQUEST_TIMEOUT of 30s) and made env-overridable.
|
|
1135
|
-
const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
|
|
1136
|
-
const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
|
|
1132
|
+
export const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
|
|
1133
|
+
export const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
|
|
1134
|
+
// Cold-open warmup budget for the FIRST direct-peer probe to a peer whose mesh
|
|
1135
|
+
// DataChannel is not open yet. A fresh cross-machine, TURN-relayed handshake
|
|
1136
|
+
// (ICE gather + TURN allocation + DTLS across two residential networks) routinely
|
|
1137
|
+
// needs many seconds. Charging that warmup against the response deadline
|
|
1138
|
+
// (MESH_DIRECT_PROBE_TIMEOUT_MS) made the very first git_status to a cold peer
|
|
1139
|
+
// false-timeout, after which the warm retry — reusing the now-open channel —
|
|
1140
|
+
// succeeded: the classic cold-open signature. This budget bounds ONLY the
|
|
1141
|
+
// "channel not open yet" phase; once the channel opens the response deadline
|
|
1142
|
+
// governs the round trip. A genuine connect failure still rejects immediately —
|
|
1143
|
+
// the mesh manager fails the peer the instant its PeerConnection state goes
|
|
1144
|
+
// terminal, and isMeshConnectionDefinitivelyDown pre-gates an already-dead peer —
|
|
1145
|
+
// so this never masks a real failure for the whole window; it only grants a
|
|
1146
|
+
// still-handshaking peer the time it legitimately needs. Matches the daemon-cloud
|
|
1147
|
+
// DaemonMeshManager CONNECT_TIMEOUT_MS (45s). Env-overridable for very slow links.
|
|
1148
|
+
export const MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS', 45_000);
|
|
1137
1149
|
// How long a successful per-peer git_status probe stays fresh enough to be
|
|
1138
1150
|
// reused instead of issuing another blocking `refreshUpstream:true` fan-out.
|
|
1139
1151
|
// A slow (TURN-relayed) peer's probe can take 9-23s, and the dashboard's
|
|
@@ -1202,17 +1214,120 @@ export class MeshGitProbeCache {
|
|
|
1202
1214
|
}
|
|
1203
1215
|
}
|
|
1204
1216
|
|
|
1217
|
+
/**
|
|
1218
|
+
* Await `work` under a warmup-aware deadline so a cold-open DataChannel handshake
|
|
1219
|
+
* is NOT charged against the command response budget — the root cause of the
|
|
1220
|
+
* "first mesh probe to a cold peer false-times-out, the warm retry succeeds"
|
|
1221
|
+
* signature. Two budgets, switched by the live peer connection state:
|
|
1222
|
+
*
|
|
1223
|
+
* - While `isConnected()` returns false the peer's channel is still opening; the
|
|
1224
|
+
* cold-open `connectTimeoutMs` budget applies. This phase is deliberately
|
|
1225
|
+
* generous because a TURN-relayed cross-machine handshake legitimately needs
|
|
1226
|
+
* many seconds — but a genuine connect *failure* is surfaced by `work`
|
|
1227
|
+
* rejecting on its own (the mesh manager fails the peer the instant its
|
|
1228
|
+
* PeerConnection state goes terminal), so a real failure is never masked for
|
|
1229
|
+
* the whole window.
|
|
1230
|
+
* - The first time `isConnected()` returns true the channel is warm; from that
|
|
1231
|
+
* instant the tight `responseTimeoutMs` governs how long the handler may take.
|
|
1232
|
+
* Warm-channel callers therefore see behavior identical to the old single
|
|
1233
|
+
* `Promise.race(work, responseTimeoutMs)`.
|
|
1234
|
+
*
|
|
1235
|
+
* Rejects with `Error('timeout')` when either budget is exhausted, mirroring the
|
|
1236
|
+
* previous single-race contract. Pure except for timers + the injected
|
|
1237
|
+
* `isConnected` probe, so it is unit-testable under fake timers without any real
|
|
1238
|
+
* WebRTC. When no connection getter is wired `isConnected` should be `() => true`
|
|
1239
|
+
* (the caller's choice) so the response deadline governs from t0 — the legacy
|
|
1240
|
+
* single-budget behavior, never a combined connect+response window.
|
|
1241
|
+
*/
|
|
1242
|
+
export function awaitWithWarmupDeadline<T>(
|
|
1243
|
+
work: Promise<T>,
|
|
1244
|
+
opts: {
|
|
1245
|
+
isConnected: () => boolean;
|
|
1246
|
+
connectTimeoutMs: number;
|
|
1247
|
+
responseTimeoutMs: number;
|
|
1248
|
+
pollIntervalMs?: number;
|
|
1249
|
+
},
|
|
1250
|
+
): Promise<T> {
|
|
1251
|
+
const pollMs = Math.max(1, Math.min(opts.pollIntervalMs ?? 200, opts.connectTimeoutMs));
|
|
1252
|
+
return new Promise<T>((resolve, reject) => {
|
|
1253
|
+
let done = false;
|
|
1254
|
+
let poll: ReturnType<typeof setInterval> | undefined;
|
|
1255
|
+
let responseTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1256
|
+
const startedAt = Date.now();
|
|
1257
|
+
const cleanup = () => {
|
|
1258
|
+
if (poll) { clearInterval(poll); poll = undefined; }
|
|
1259
|
+
if (responseTimer) { clearTimeout(responseTimer); responseTimer = undefined; }
|
|
1260
|
+
};
|
|
1261
|
+
const settle = (fn: () => void) => {
|
|
1262
|
+
if (done) return;
|
|
1263
|
+
done = true;
|
|
1264
|
+
cleanup();
|
|
1265
|
+
fn();
|
|
1266
|
+
};
|
|
1267
|
+
// Arm the response deadline exactly once, the moment the channel is warm.
|
|
1268
|
+
const armResponse = () => {
|
|
1269
|
+
if (responseTimer || done) return;
|
|
1270
|
+
responseTimer = setTimeout(
|
|
1271
|
+
() => settle(() => reject(new Error('timeout'))),
|
|
1272
|
+
opts.responseTimeoutMs,
|
|
1273
|
+
);
|
|
1274
|
+
if (typeof responseTimer.unref === 'function') responseTimer.unref();
|
|
1275
|
+
};
|
|
1276
|
+
const onPoll = () => {
|
|
1277
|
+
if (done) return;
|
|
1278
|
+
if (opts.isConnected()) {
|
|
1279
|
+
if (poll) { clearInterval(poll); poll = undefined; }
|
|
1280
|
+
armResponse();
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (Date.now() - startedAt >= opts.connectTimeoutMs) {
|
|
1284
|
+
settle(() => reject(new Error('timeout')));
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
if (opts.isConnected()) {
|
|
1288
|
+
// Already warm (e.g. a retry over an open channel) — skip the warmup
|
|
1289
|
+
// phase entirely and let the response deadline govern from t0.
|
|
1290
|
+
armResponse();
|
|
1291
|
+
} else {
|
|
1292
|
+
poll = setInterval(onPoll, pollMs);
|
|
1293
|
+
if (typeof poll.unref === 'function') poll.unref();
|
|
1294
|
+
}
|
|
1295
|
+
work.then(
|
|
1296
|
+
(val) => settle(() => resolve(val)),
|
|
1297
|
+
(err) => settle(() => reject(err)),
|
|
1298
|
+
);
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1205
1302
|
async function probeRemoteMeshGitStatus(args: {
|
|
1206
1303
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
1207
1304
|
daemonId: string;
|
|
1208
1305
|
workspace: string;
|
|
1209
|
-
|
|
1306
|
+
// Response deadline — applies only once the peer's DataChannel is open (warm).
|
|
1307
|
+
responseTimeoutMs: number;
|
|
1308
|
+
// Cold-open warmup budget — applies only while the channel is still opening.
|
|
1309
|
+
connectTimeoutMs: number;
|
|
1310
|
+
// Live peer connection snapshot getter; lets the deadline tell "still warming
|
|
1311
|
+
// up" apart from "warm but slow". Absent → behave as if always warm (the
|
|
1312
|
+
// response deadline governs from t0, i.e. the legacy single-budget behavior).
|
|
1313
|
+
getConnection?: (daemonId: string) => Record<string, unknown> | null;
|
|
1210
1314
|
}): Promise<Record<string, unknown> | null> {
|
|
1211
1315
|
if (!args.dispatchMeshCommand) return null;
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1316
|
+
// Fire the dispatch first — this is what drives the mesh manager to ensure /
|
|
1317
|
+
// open the peer connection. The warmup-aware deadline then charges the
|
|
1318
|
+
// cold-open handshake to the connect budget and only the warm round trip to
|
|
1319
|
+
// the response budget, so the first probe to a cold peer is no longer
|
|
1320
|
+
// false-timed-out before its channel has even opened.
|
|
1321
|
+
const dispatch = args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true });
|
|
1322
|
+
const getConnection = args.getConnection;
|
|
1323
|
+
const isConnected = getConnection
|
|
1324
|
+
? () => readMeshConnectionState(getConnection(args.daemonId)) === 'connected'
|
|
1325
|
+
: () => true;
|
|
1326
|
+
const remoteResult = await awaitWithWarmupDeadline(dispatch, {
|
|
1327
|
+
isConnected,
|
|
1328
|
+
connectTimeoutMs: args.connectTimeoutMs,
|
|
1329
|
+
responseTimeoutMs: args.responseTimeoutMs,
|
|
1330
|
+
}) as any;
|
|
1216
1331
|
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
1217
1332
|
if (!remoteGit || typeof remoteGit !== 'object' || typeof remoteGit.isGitRepo !== 'boolean') return null;
|
|
1218
1333
|
// The member daemon stamps its own platform/arch onto the git_status result
|
|
@@ -1266,13 +1381,15 @@ function isMeshConnectionDefinitivelyDown(
|
|
|
1266
1381
|
* attempt; a non-`connected` state short-circuits the retry loop (the very first
|
|
1267
1382
|
* attempt always runs so a missing connection getter still gets one try).
|
|
1268
1383
|
*/
|
|
1269
|
-
async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
1384
|
+
export async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
1270
1385
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
1271
1386
|
daemonId: string;
|
|
1272
1387
|
workspace: string;
|
|
1273
1388
|
timeoutMs: number;
|
|
1274
1389
|
/** Per-attempt timeout for retries (attempts > 0); defaults to timeoutMs. */
|
|
1275
1390
|
retryTimeoutMs?: number;
|
|
1391
|
+
/** Cold-open warmup budget per attempt; defaults to MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS. */
|
|
1392
|
+
connectTimeoutMs?: number;
|
|
1276
1393
|
getConnection?: (daemonId: string) => Record<string, unknown> | null;
|
|
1277
1394
|
onConnection?: (connection: Record<string, unknown>) => void;
|
|
1278
1395
|
}): Promise<Record<string, unknown> | null> {
|
|
@@ -1304,7 +1421,9 @@ async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
|
1304
1421
|
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1305
1422
|
daemonId: args.daemonId,
|
|
1306
1423
|
workspace: args.workspace,
|
|
1307
|
-
|
|
1424
|
+
responseTimeoutMs: attempt === 0 ? args.timeoutMs : (args.retryTimeoutMs ?? args.timeoutMs),
|
|
1425
|
+
connectTimeoutMs: args.connectTimeoutMs ?? MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS,
|
|
1426
|
+
getConnection: args.getConnection,
|
|
1308
1427
|
});
|
|
1309
1428
|
if (remoteGit) return remoteGit;
|
|
1310
1429
|
} catch {
|
|
@@ -1451,6 +1570,7 @@ export async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1451
1570
|
workspace,
|
|
1452
1571
|
timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
|
|
1453
1572
|
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
1573
|
+
connectTimeoutMs: MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS,
|
|
1454
1574
|
getConnection: args.getMeshPeerConnectionStatus,
|
|
1455
1575
|
});
|
|
1456
1576
|
const remoteGit = args.probeCache
|
|
@@ -1483,7 +1603,7 @@ export async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1483
1603
|
};
|
|
1484
1604
|
}
|
|
1485
1605
|
|
|
1486
|
-
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
1606
|
+
export function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
1487
1607
|
const meta = readObjectRecord(record?.meta);
|
|
1488
1608
|
const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
|
|
1489
1609
|
const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
|
|
@@ -1516,14 +1636,17 @@ function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: s
|
|
|
1516
1636
|
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
1517
1637
|
if (nodeIsMissingLocalWorktree) return false;
|
|
1518
1638
|
const recordWorkspace = readStringValue(record?.workspace);
|
|
1519
|
-
|
|
1639
|
+
// Normalized compare (shared WTCLAIM rule): a base node and a co-located worktree
|
|
1640
|
+
// clone differ ONLY by workspace root, so a separator/case-skewed exact compare
|
|
1641
|
+
// could wrongly keep a sibling worktree's session attached to this node.
|
|
1642
|
+
if (nodeWorkspace && recordWorkspace && !meshWorkspacesEquivalent(recordWorkspace, nodeWorkspace)) return false;
|
|
1520
1643
|
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1521
1644
|
return !recordMeshId || recordMeshId === meshId;
|
|
1522
1645
|
}
|
|
1523
1646
|
|
|
1524
1647
|
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
1525
1648
|
const recordWorkspace = readStringValue(record?.workspace);
|
|
1526
|
-
if (!recordWorkspace || !workspace || recordWorkspace
|
|
1649
|
+
if (!recordWorkspace || !workspace || !meshWorkspacesEquivalent(recordWorkspace, workspace)) return false;
|
|
1527
1650
|
|
|
1528
1651
|
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1529
1652
|
if (recordMeshId) return recordMeshId === meshId;
|
|
@@ -1531,7 +1654,7 @@ function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, work
|
|
|
1531
1654
|
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
1532
1655
|
}
|
|
1533
1656
|
|
|
1534
|
-
function readLiveMeshNodeWorkspace(args: {
|
|
1657
|
+
export function readLiveMeshNodeWorkspace(args: {
|
|
1535
1658
|
meshId: string;
|
|
1536
1659
|
nodeId: string;
|
|
1537
1660
|
liveSessionRecords: any[];
|
|
@@ -1558,7 +1681,7 @@ function readLiveMeshNodeWorkspace(args: {
|
|
|
1558
1681
|
return '';
|
|
1559
1682
|
}
|
|
1560
1683
|
|
|
1561
|
-
function collectLiveMeshSessionRecords(args: {
|
|
1684
|
+
export function collectLiveMeshSessionRecords(args: {
|
|
1562
1685
|
meshId: string;
|
|
1563
1686
|
node: any;
|
|
1564
1687
|
nodeId: string;
|
|
@@ -1589,7 +1712,7 @@ function collectLiveMeshSessionRecords(args: {
|
|
|
1589
1712
|
return matches;
|
|
1590
1713
|
}
|
|
1591
1714
|
|
|
1592
|
-
function buildHistoricalMeshSessions(args: {
|
|
1715
|
+
export function buildHistoricalMeshSessions(args: {
|
|
1593
1716
|
meshId: string;
|
|
1594
1717
|
nodes: any[];
|
|
1595
1718
|
liveSessionRecords: any[];
|
|
@@ -1635,7 +1758,7 @@ function buildHistoricalMeshSessions(args: {
|
|
|
1635
1758
|
};
|
|
1636
1759
|
}
|
|
1637
1760
|
|
|
1638
|
-
function applyCachedInlineMeshNodeStatus(
|
|
1761
|
+
export function applyCachedInlineMeshNodeStatus(
|
|
1639
1762
|
status: Record<string, unknown>,
|
|
1640
1763
|
node: any,
|
|
1641
1764
|
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
@@ -1669,7 +1792,7 @@ function applyCachedInlineMeshNodeStatus(
|
|
|
1669
1792
|
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
1670
1793
|
}
|
|
1671
1794
|
|
|
1672
|
-
async function resolveProviderTypeFromPriority(args: {
|
|
1795
|
+
export async function resolveProviderTypeFromPriority(args: {
|
|
1673
1796
|
nodeId: string;
|
|
1674
1797
|
providerPriority: string[];
|
|
1675
1798
|
providerLoader: ProviderLoader;
|
|
@@ -1699,7 +1822,7 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
1699
1822
|
|
|
1700
1823
|
return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
|
|
1701
1824
|
}
|
|
1702
|
-
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
1825
|
+
export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
1703
1826
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
1704
1827
|
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
1705
1828
|
|
|
@@ -1849,7 +1972,7 @@ type MeshRefineSubmoduleReachabilitySummary = {
|
|
|
1849
1972
|
|
|
1850
1973
|
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1851
1974
|
|
|
1852
|
-
type MeshRefineJobHandle = {
|
|
1975
|
+
export type MeshRefineJobHandle = {
|
|
1853
1976
|
success: true;
|
|
1854
1977
|
async: true;
|
|
1855
1978
|
status: MeshRefineAsyncJobStatus;
|
|
@@ -1982,6 +2105,51 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
|
|
|
1982
2105
|
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
|
|
1983
2106
|
}
|
|
1984
2107
|
|
|
2108
|
+
/**
|
|
2109
|
+
* Async git exec helper used across the synchronous-refine stage pipeline. Bound
|
|
2110
|
+
* once in the orchestrator and threaded through RefineContext so every stage runs
|
|
2111
|
+
* git the same way (execFile + promisify, utf8). Returns the child's stdout/stderr.
|
|
2112
|
+
*/
|
|
2113
|
+
type RefineExecFileAsync = (file: string, args: string[], options: { cwd: string; encoding: 'utf8' }) => Promise<{ stdout: string; stderr: string }>;
|
|
2114
|
+
|
|
2115
|
+
/**
|
|
2116
|
+
* Accumulated state shared by the synchronous-refine stages. The orchestrator
|
|
2117
|
+
* (executeMeshRefineNodeSynchronously) seeds this in the resolve_refs stage and
|
|
2118
|
+
* each later stage reads / extends it. `branchHead` and `patchEquivalence` are the
|
|
2119
|
+
* only fields a stage mutates after creation (auto-rebase updates both), so they
|
|
2120
|
+
* are carried on the mutable context rather than re-threaded through return types.
|
|
2121
|
+
*/
|
|
2122
|
+
interface RefineContext {
|
|
2123
|
+
meshId: string;
|
|
2124
|
+
nodeId: string;
|
|
2125
|
+
args: any;
|
|
2126
|
+
refineStages: Array<Record<string, unknown>>;
|
|
2127
|
+
execFileAsync: RefineExecFileAsync;
|
|
2128
|
+
mesh: any;
|
|
2129
|
+
node: any;
|
|
2130
|
+
sourceNode: any;
|
|
2131
|
+
repoRoot: string;
|
|
2132
|
+
branch: string;
|
|
2133
|
+
baseBranch: string;
|
|
2134
|
+
baseHead: string;
|
|
2135
|
+
branchHead: string;
|
|
2136
|
+
validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
|
|
2137
|
+
patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
|
|
2138
|
+
submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
/**
|
|
2142
|
+
* Stage outcome for the synchronous-refine pipeline. A stage either produces a
|
|
2143
|
+
* terminal CommandRouterResult (an early-exit gate failure, or a successful
|
|
2144
|
+
* already-merged short-circuit), in which case the orchestrator returns it
|
|
2145
|
+
* immediately, or it returns `continue` with the (possibly extended) context for
|
|
2146
|
+
* the next stage. This makes the orchestrator a flat sequence of stage calls
|
|
2147
|
+
* while preserving the original body's exact early-return control flow.
|
|
2148
|
+
*/
|
|
2149
|
+
type RefineStageOutcome =
|
|
2150
|
+
| { kind: 'terminal'; result: CommandRouterResult }
|
|
2151
|
+
| { kind: 'continue'; ctx: RefineContext };
|
|
2152
|
+
|
|
1985
2153
|
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1986
2154
|
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1987
2155
|
process.stderr.write(
|
|
@@ -3285,18 +3453,18 @@ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, op
|
|
|
3285
3453
|
return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
|
|
3286
3454
|
}
|
|
3287
3455
|
|
|
3288
|
-
function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
3456
|
+
export function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
3289
3457
|
return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
|
|
3290
3458
|
}
|
|
3291
3459
|
|
|
3292
|
-
function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
3460
|
+
export function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
3293
3461
|
if (!text.trim()) return {};
|
|
3294
3462
|
if (format === 'claude_mcp_json') return JSON.parse(text);
|
|
3295
3463
|
const parsed = loadYamlModule().load(text);
|
|
3296
3464
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
3297
3465
|
}
|
|
3298
3466
|
|
|
3299
|
-
function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
3467
|
+
export function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
3300
3468
|
if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
|
|
3301
3469
|
return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
|
|
3302
3470
|
}
|
|
@@ -3306,7 +3474,7 @@ function resolveHermesUserHome(): string {
|
|
|
3306
3474
|
return explicitHome || pathJoin(homedir(), '.hermes');
|
|
3307
3475
|
}
|
|
3308
3476
|
|
|
3309
|
-
function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
|
|
3477
|
+
export function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
|
|
3310
3478
|
const sourceHome = resolveHermesUserHome();
|
|
3311
3479
|
const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
|
|
3312
3480
|
if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
|
|
@@ -3317,7 +3485,7 @@ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Re
|
|
|
3317
3485
|
return { config: baseConfig, sourceHome, sourceConfigPath };
|
|
3318
3486
|
}
|
|
3319
3487
|
|
|
3320
|
-
function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
|
|
3488
|
+
export function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
|
|
3321
3489
|
const {
|
|
3322
3490
|
model: _model,
|
|
3323
3491
|
provider: _provider,
|
|
@@ -3346,7 +3514,7 @@ function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string,
|
|
|
3346
3514
|
return sanitized;
|
|
3347
3515
|
}
|
|
3348
3516
|
|
|
3349
|
-
function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
|
|
3517
|
+
export function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
|
|
3350
3518
|
if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
|
|
3351
3519
|
for (const fileName of ['.env', 'auth.json']) {
|
|
3352
3520
|
const sourcePath = pathJoin(sourceHome, fileName);
|
|
@@ -3897,6 +4065,30 @@ export class DaemonCommandRouter {
|
|
|
3897
4065
|
return ctx;
|
|
3898
4066
|
}
|
|
3899
4067
|
|
|
4068
|
+
/**
|
|
4069
|
+
* Build the HighFamilyContext handed to RF-ROUTER HIGH family handlers. Binds
|
|
4070
|
+
* the router-private collaborators those handlers need (mesh resolution, the
|
|
4071
|
+
* aggregate-status memory cache + its bound read/write helpers, the
|
|
4072
|
+
* running-refine-job table, inline-mesh + git-probe caches, and the router's
|
|
4073
|
+
* own `execute` for the get_mesh_review_inbox mesh_status re-entry). HIGH
|
|
4074
|
+
* handlers reach more router-owned state than MED, but the binding shape is
|
|
4075
|
+
* the same: bound methods + direct field references, none reachable from
|
|
4076
|
+
* `deps`.
|
|
4077
|
+
*/
|
|
4078
|
+
private buildHighFamilyContext(): HighFamilyContext {
|
|
4079
|
+
return {
|
|
4080
|
+
deps: this.deps,
|
|
4081
|
+
getMeshForCommand: this.getMeshForCommand.bind(this),
|
|
4082
|
+
getCachedAggregateMeshStatus: this.getCachedAggregateMeshStatus.bind(this),
|
|
4083
|
+
rememberAggregateMeshStatus: this.rememberAggregateMeshStatus.bind(this),
|
|
4084
|
+
execute: this.execute.bind(this),
|
|
4085
|
+
aggregateMeshStatusCache: this.aggregateMeshStatusCache,
|
|
4086
|
+
runningRefineJobs: this.runningRefineJobs,
|
|
4087
|
+
inlineMeshCache: this.inlineMeshCache,
|
|
4088
|
+
meshGitProbeCache: this.meshGitProbeCache,
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
|
|
3900
4092
|
|
|
3901
4093
|
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
3902
4094
|
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
@@ -4801,33 +4993,77 @@ export class DaemonCommandRouter {
|
|
|
4801
4993
|
}
|
|
4802
4994
|
}
|
|
4803
4995
|
|
|
4996
|
+
/**
|
|
4997
|
+
* Synchronous refinery for a single worktree node — the gate pipeline that
|
|
4998
|
+
* validates, preflights (patch-equivalence / submodule-reachability /
|
|
4999
|
+
* no-op), merges, aligns submodules, cleans up the worktree node and
|
|
5000
|
+
* (optionally) pushes. The body is a flat sequence of stage methods; each
|
|
5001
|
+
* stage either returns a terminal CommandRouterResult (gate failure or a
|
|
5002
|
+
* successful already-merged short-circuit) or `continue` with the extended
|
|
5003
|
+
* context. Behavior — stage order, every early-exit, and every result shape —
|
|
5004
|
+
* is identical to the previous single inlined body.
|
|
5005
|
+
*/
|
|
4804
5006
|
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
4805
5007
|
const refineStages: Array<Record<string, unknown>> = [];
|
|
4806
5008
|
try {
|
|
5009
|
+
const resolved = await this.refineResolveRefsStage(meshId, nodeId, args, refineStages);
|
|
5010
|
+
if (resolved.kind === 'terminal') return resolved.result;
|
|
5011
|
+
const ctx = resolved.ctx;
|
|
5012
|
+
|
|
5013
|
+
const validation = await this.refineValidationStage(ctx);
|
|
5014
|
+
if (validation.kind === 'terminal') return validation.result;
|
|
5015
|
+
|
|
5016
|
+
const patchEquivalence = await this.refinePatchEquivalenceStage(ctx);
|
|
5017
|
+
if (patchEquivalence.kind === 'terminal') return patchEquivalence.result;
|
|
5018
|
+
|
|
5019
|
+
const submoduleReachability = await this.refineSubmoduleReachabilityStage(ctx);
|
|
5020
|
+
if (submoduleReachability.kind === 'terminal') return submoduleReachability.result;
|
|
5021
|
+
|
|
5022
|
+
const effectiveDiff = await this.refineEffectiveDiffStage(ctx);
|
|
5023
|
+
if (effectiveDiff.kind === 'terminal') return effectiveDiff.result;
|
|
5024
|
+
|
|
5025
|
+
const merge = await this.refineMergeAndFinalizeStage(ctx);
|
|
5026
|
+
return (merge as { kind: 'terminal'; result: CommandRouterResult }).result;
|
|
5027
|
+
} catch (e: any) {
|
|
5028
|
+
return { success: false, error: e.message, refineStages };
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
/**
|
|
5033
|
+
* resolve_refs stage: resolve the mesh / worktree node / source node /
|
|
5034
|
+
* repoRoot, then the worktree branch, base branch, fetched base head and
|
|
5035
|
+
* branch head. Seeds the RefineContext consumed by every later stage.
|
|
5036
|
+
*/
|
|
5037
|
+
private async refineResolveRefsStage(
|
|
5038
|
+
meshId: string,
|
|
5039
|
+
nodeId: string,
|
|
5040
|
+
args: any,
|
|
5041
|
+
refineStages: Array<Record<string, unknown>>,
|
|
5042
|
+
): Promise<RefineStageOutcome> {
|
|
4807
5043
|
// preferInline: same as startMeshRefineJob — inline-cache-only clone nodes must resolve.
|
|
4808
5044
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4809
5045
|
const mesh = meshRecord?.mesh;
|
|
4810
5046
|
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
4811
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
5047
|
+
if (!node) return { kind: 'terminal', result: { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages } };
|
|
4812
5048
|
|
|
4813
5049
|
if (!node.isLocalWorktree || !node.workspace) {
|
|
4814
|
-
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
5050
|
+
return { kind: 'terminal', result: { success: false, error: `Refinery requires a local worktree node`, refineStages } };
|
|
4815
5051
|
}
|
|
4816
5052
|
|
|
4817
5053
|
const sourceNode = node.clonedFromNodeId
|
|
4818
5054
|
? mesh?.nodes.find((n: any) => meshNodeIdMatches(n, node.clonedFromNodeId))
|
|
4819
5055
|
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
4820
5056
|
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
4821
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
5057
|
+
if (!repoRoot) return { kind: 'terminal', result: { success: false, error: 'Source node repoRoot not found', refineStages } };
|
|
4822
5058
|
|
|
4823
5059
|
const { execFile } = await import('node:child_process');
|
|
4824
5060
|
const { promisify } = await import('node:util');
|
|
4825
|
-
const execFileAsync = promisify(execFile);
|
|
5061
|
+
const execFileAsync = promisify(execFile) as unknown as RefineExecFileAsync;
|
|
4826
5062
|
|
|
4827
5063
|
const resolveStarted = Date.now();
|
|
4828
5064
|
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
4829
5065
|
const branch = branchStdout.trim();
|
|
4830
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
5066
|
+
if (!branch) return { kind: 'terminal', result: { success: false, error: 'Could not determine branch of the worktree node', refineStages } };
|
|
4831
5067
|
|
|
4832
5068
|
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
4833
5069
|
const baseBranch = baseBranchStdout.trim();
|
|
@@ -4854,9 +5090,39 @@ export class DaemonCommandRouter {
|
|
|
4854
5090
|
|
|
4855
5091
|
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
4856
5092
|
const baseHead = baseHeadRaw;
|
|
4857
|
-
|
|
5093
|
+
const branchHead = branchHeadStdout.trim();
|
|
4858
5094
|
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead, ...(fetchWarning ? { fetchWarning } : {}) });
|
|
4859
5095
|
|
|
5096
|
+
return {
|
|
5097
|
+
kind: 'continue',
|
|
5098
|
+
ctx: {
|
|
5099
|
+
meshId,
|
|
5100
|
+
nodeId,
|
|
5101
|
+
args,
|
|
5102
|
+
refineStages,
|
|
5103
|
+
execFileAsync,
|
|
5104
|
+
mesh,
|
|
5105
|
+
node,
|
|
5106
|
+
sourceNode,
|
|
5107
|
+
repoRoot,
|
|
5108
|
+
branch,
|
|
5109
|
+
baseBranch,
|
|
5110
|
+
baseHead,
|
|
5111
|
+
branchHead,
|
|
5112
|
+
validationSummary: undefined as any,
|
|
5113
|
+
patchEquivalence: undefined as any,
|
|
5114
|
+
submoduleReachability: undefined as any,
|
|
5115
|
+
},
|
|
5116
|
+
};
|
|
5117
|
+
}
|
|
5118
|
+
|
|
5119
|
+
/**
|
|
5120
|
+
* validation stage: run the refinery validation gate (typecheck / test /
|
|
5121
|
+
* lint / build per node config) and block on failure or when no allowlisted
|
|
5122
|
+
* command was available. On pass, stores the summary on the context.
|
|
5123
|
+
*/
|
|
5124
|
+
private async refineValidationStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5125
|
+
const { mesh, node, branch, baseBranch, refineStages } = ctx;
|
|
4860
5126
|
const validationStarted = Date.now();
|
|
4861
5127
|
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
|
|
4862
5128
|
// M2-2: consume the node's persisted bootstrap state; persist re-runs.
|
|
@@ -4868,6 +5134,7 @@ export class DaemonCommandRouter {
|
|
|
4868
5134
|
.catch(() => { /* persistence is best-effort */ });
|
|
4869
5135
|
},
|
|
4870
5136
|
});
|
|
5137
|
+
ctx.validationSummary = validationSummary;
|
|
4871
5138
|
recordMeshRefineStage(
|
|
4872
5139
|
refineStages,
|
|
4873
5140
|
'validation',
|
|
@@ -4903,7 +5170,7 @@ export class DaemonCommandRouter {
|
|
|
4903
5170
|
tail ? `Output (tail):\n${tail}` : '',
|
|
4904
5171
|
].filter(Boolean).join('\n');
|
|
4905
5172
|
};
|
|
4906
|
-
return {
|
|
5173
|
+
return { kind: 'terminal', result: {
|
|
4907
5174
|
success: false,
|
|
4908
5175
|
code: validationSummary.failureCode || 'validation_failed',
|
|
4909
5176
|
convergenceStatus: 'blocked_review',
|
|
@@ -4920,10 +5187,10 @@ export class DaemonCommandRouter {
|
|
|
4920
5187
|
validation: 'failed',
|
|
4921
5188
|
status: 'blocked_review',
|
|
4922
5189
|
},
|
|
4923
|
-
};
|
|
5190
|
+
} };
|
|
4924
5191
|
}
|
|
4925
5192
|
if (validationSummary.status === 'skipped') {
|
|
4926
|
-
return {
|
|
5193
|
+
return { kind: 'terminal', result: {
|
|
4927
5194
|
success: false,
|
|
4928
5195
|
code: 'validation_unavailable',
|
|
4929
5196
|
convergenceStatus: 'blocked_review',
|
|
@@ -4940,9 +5207,22 @@ export class DaemonCommandRouter {
|
|
|
4940
5207
|
validation: 'unavailable',
|
|
4941
5208
|
status: 'blocked_review',
|
|
4942
5209
|
},
|
|
4943
|
-
};
|
|
5210
|
+
} };
|
|
4944
5211
|
}
|
|
4945
5212
|
|
|
5213
|
+
return { kind: 'continue', ctx };
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
/**
|
|
5217
|
+
* patch_equivalence stage: preflight that the worktree branch's cumulative
|
|
5218
|
+
* patch is equivalent to base+branch. On a "behind base" branch, auto-rebase
|
|
5219
|
+
* once and re-check; on an empty merge-tree with real branch changes, treat as
|
|
5220
|
+
* already-merged-via-another-path and short-circuit to cleanup. Mutates the
|
|
5221
|
+
* context's branchHead (after rebase) and patchEquivalence (rebased gate).
|
|
5222
|
+
*/
|
|
5223
|
+
private async refinePatchEquivalenceStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5224
|
+
const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, validationSummary, refineStages, execFileAsync } = ctx;
|
|
5225
|
+
let branchHead = ctx.branchHead;
|
|
4946
5226
|
const patchEquivalenceStarted = Date.now();
|
|
4947
5227
|
let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
4948
5228
|
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
@@ -4985,7 +5265,7 @@ export class DaemonCommandRouter {
|
|
|
4985
5265
|
patchEquivalence = rebasedPatchEquivalence;
|
|
4986
5266
|
didAutoRebase = true;
|
|
4987
5267
|
} else {
|
|
4988
|
-
return {
|
|
5268
|
+
return { kind: 'terminal', result: {
|
|
4989
5269
|
success: false,
|
|
4990
5270
|
code: 'needs_rebase',
|
|
4991
5271
|
convergenceStatus: 'blocked_review',
|
|
@@ -5004,14 +5284,14 @@ export class DaemonCommandRouter {
|
|
|
5004
5284
|
patchEquivalence: 'failed',
|
|
5005
5285
|
status: 'blocked_review',
|
|
5006
5286
|
},
|
|
5007
|
-
};
|
|
5287
|
+
} };
|
|
5008
5288
|
}
|
|
5009
5289
|
} catch (rebaseErr: any) {
|
|
5010
5290
|
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
5011
5291
|
recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
|
|
5012
5292
|
error: rebaseErr?.message || String(rebaseErr),
|
|
5013
5293
|
});
|
|
5014
|
-
return {
|
|
5294
|
+
return { kind: 'terminal', result: {
|
|
5015
5295
|
success: false,
|
|
5016
5296
|
code: 'needs_rebase_with_conflicts',
|
|
5017
5297
|
convergenceStatus: 'blocked_review',
|
|
@@ -5030,7 +5310,7 @@ export class DaemonCommandRouter {
|
|
|
5030
5310
|
patchEquivalence: 'failed',
|
|
5031
5311
|
status: 'blocked_review',
|
|
5032
5312
|
},
|
|
5033
|
-
};
|
|
5313
|
+
} };
|
|
5034
5314
|
}
|
|
5035
5315
|
}
|
|
5036
5316
|
|
|
@@ -5046,7 +5326,7 @@ export class DaemonCommandRouter {
|
|
|
5046
5326
|
// is a degenerate worktree case, not an "already merged" scenario.
|
|
5047
5327
|
const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
|
|
5048
5328
|
if (!didAutoRebase && !alreadyMergedViaOtherPath) {
|
|
5049
|
-
return {
|
|
5329
|
+
return { kind: 'terminal', result: {
|
|
5050
5330
|
success: false,
|
|
5051
5331
|
code: 'patch_equivalence_failed',
|
|
5052
5332
|
convergenceStatus: 'blocked_review',
|
|
@@ -5065,7 +5345,7 @@ export class DaemonCommandRouter {
|
|
|
5065
5345
|
patchEquivalence: 'failed',
|
|
5066
5346
|
status: 'blocked_review',
|
|
5067
5347
|
},
|
|
5068
|
-
};
|
|
5348
|
+
} };
|
|
5069
5349
|
}
|
|
5070
5350
|
|
|
5071
5351
|
if (!didAutoRebase && alreadyMergedViaOtherPath) {
|
|
@@ -5094,7 +5374,7 @@ export class DaemonCommandRouter {
|
|
|
5094
5374
|
payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
5095
5375
|
});
|
|
5096
5376
|
} catch { /* ledger append is best-effort */ }
|
|
5097
|
-
return {
|
|
5377
|
+
return { kind: 'terminal', result: {
|
|
5098
5378
|
success: removeResult?.success !== false,
|
|
5099
5379
|
code: 'already_merged',
|
|
5100
5380
|
merged: false,
|
|
@@ -5116,10 +5396,23 @@ export class DaemonCommandRouter {
|
|
|
5116
5396
|
patchEquivalence: 'already_merged',
|
|
5117
5397
|
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
|
|
5118
5398
|
},
|
|
5119
|
-
};
|
|
5399
|
+
} };
|
|
5120
5400
|
}
|
|
5121
5401
|
}
|
|
5122
5402
|
|
|
5403
|
+
ctx.branchHead = branchHead;
|
|
5404
|
+
ctx.patchEquivalence = patchEquivalence;
|
|
5405
|
+
return { kind: 'continue', ctx };
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5408
|
+
/**
|
|
5409
|
+
* submodule_reachability stage: verify every submodule gitlink commit that
|
|
5410
|
+
* would land via the merge is reachable from its configured remote main
|
|
5411
|
+
* branch (optionally auto-publishing when policy allows). Blocks the merge
|
|
5412
|
+
* when any commit is unreachable. Stores the result on the context.
|
|
5413
|
+
*/
|
|
5414
|
+
private async refineSubmoduleReachabilityStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5415
|
+
const { mesh, node, repoRoot, branch, baseBranch, branchHead, validationSummary, patchEquivalence, refineStages } = ctx;
|
|
5123
5416
|
const submoduleReachabilityStarted = Date.now();
|
|
5124
5417
|
const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
|
|
5125
5418
|
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
@@ -5176,7 +5469,7 @@ export class DaemonCommandRouter {
|
|
|
5176
5469
|
});
|
|
5177
5470
|
if (submoduleReachability.status === 'failed') {
|
|
5178
5471
|
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
5179
|
-
return {
|
|
5472
|
+
return { kind: 'terminal', result: {
|
|
5180
5473
|
success: false,
|
|
5181
5474
|
code: 'submodule_reachability_failed',
|
|
5182
5475
|
convergenceStatus: 'blocked_review',
|
|
@@ -5224,9 +5517,21 @@ export class DaemonCommandRouter {
|
|
|
5224
5517
|
reason: 'submodule_publish_required',
|
|
5225
5518
|
nextStep,
|
|
5226
5519
|
},
|
|
5227
|
-
};
|
|
5520
|
+
} };
|
|
5228
5521
|
}
|
|
5229
5522
|
|
|
5523
|
+
ctx.submoduleReachability = submoduleReachability;
|
|
5524
|
+
return { kind: 'continue', ctx };
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
/**
|
|
5528
|
+
* effective_diff stage (no-op guard): block a silent no-op merge where the
|
|
5529
|
+
* branch produces no effective root-tree diff against base — typically a
|
|
5530
|
+
* submodule that has commits but whose root-level gitlink (pointer) bump was
|
|
5531
|
+
* never committed, so the merge would land nothing real on main.
|
|
5532
|
+
*/
|
|
5533
|
+
private async refineEffectiveDiffStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5534
|
+
const { repoRoot, baseHead, branchHead, branch, baseBranch, validationSummary, patchEquivalence, refineStages } = ctx;
|
|
5230
5535
|
// No-op guard: block a silent no-op merge where the root tree is identical to base.
|
|
5231
5536
|
// This catches the trap where a submodule has commits but the root branch never
|
|
5232
5537
|
// committed the gitlink (oss-pointer) bump — merging would report success while the
|
|
@@ -5248,7 +5553,7 @@ export class DaemonCommandRouter {
|
|
|
5248
5553
|
hintLines.length ? `Submodules with uncommitted pointer bumps:\n${hintLines.join('\n')}` : '',
|
|
5249
5554
|
`Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`,
|
|
5250
5555
|
].filter(Boolean).join('\n');
|
|
5251
|
-
return {
|
|
5556
|
+
return { kind: 'terminal', result: {
|
|
5252
5557
|
success: false,
|
|
5253
5558
|
code: 'no_effective_diff',
|
|
5254
5559
|
convergenceStatus: 'blocked_review',
|
|
@@ -5271,9 +5576,20 @@ export class DaemonCommandRouter {
|
|
|
5271
5576
|
reason: 'no_effective_diff',
|
|
5272
5577
|
...(effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}),
|
|
5273
5578
|
},
|
|
5274
|
-
};
|
|
5579
|
+
} };
|
|
5275
5580
|
}
|
|
5276
5581
|
|
|
5582
|
+
return { kind: 'continue', ctx };
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5585
|
+
/**
|
|
5586
|
+
* merge + finalize stage: perform the --no-ff merge, align submodule
|
|
5587
|
+
* checkouts after merge, clean up (remove) the worktree node per policy,
|
|
5588
|
+
* append the refinery ledger entry, and (unless approval is required) push the
|
|
5589
|
+
* base branch. Always terminal — produces the final CommandRouterResult.
|
|
5590
|
+
*/
|
|
5591
|
+
private async refineMergeAndFinalizeStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5592
|
+
const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync } = ctx;
|
|
5277
5593
|
let mergeResult: Record<string, unknown> | undefined;
|
|
5278
5594
|
const mergeStarted = Date.now();
|
|
5279
5595
|
try {
|
|
@@ -5290,7 +5606,7 @@ export class DaemonCommandRouter {
|
|
|
5290
5606
|
stdout: truncateValidationOutput(e?.stdout),
|
|
5291
5607
|
stderr: truncateValidationOutput(e?.stderr),
|
|
5292
5608
|
});
|
|
5293
|
-
return {
|
|
5609
|
+
return { kind: 'terminal', result: {
|
|
5294
5610
|
success: false,
|
|
5295
5611
|
error: `Merge failed (conflicts?): ${e.message}`,
|
|
5296
5612
|
validationSummary,
|
|
@@ -5305,7 +5621,7 @@ export class DaemonCommandRouter {
|
|
|
5305
5621
|
patchEquivalence: 'passed',
|
|
5306
5622
|
status: 'not_mergeable',
|
|
5307
5623
|
},
|
|
5308
|
-
};
|
|
5624
|
+
} };
|
|
5309
5625
|
}
|
|
5310
5626
|
|
|
5311
5627
|
const submoduleAlignmentStarted = Date.now();
|
|
@@ -5325,7 +5641,7 @@ export class DaemonCommandRouter {
|
|
|
5325
5641
|
});
|
|
5326
5642
|
}
|
|
5327
5643
|
if (submoduleAlignment.status === 'failed') {
|
|
5328
|
-
return {
|
|
5644
|
+
return { kind: 'terminal', result: {
|
|
5329
5645
|
success: false,
|
|
5330
5646
|
code: 'post_merge_submodule_alignment_failed',
|
|
5331
5647
|
error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
|
|
@@ -5351,7 +5667,7 @@ export class DaemonCommandRouter {
|
|
|
5351
5667
|
status: 'post_merge_alignment_failed',
|
|
5352
5668
|
nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
|
|
5353
5669
|
},
|
|
5354
|
-
};
|
|
5670
|
+
} };
|
|
5355
5671
|
}
|
|
5356
5672
|
|
|
5357
5673
|
const cleanupStarted = Date.now();
|
|
@@ -5431,7 +5747,7 @@ export class DaemonCommandRouter {
|
|
|
5431
5747
|
};
|
|
5432
5748
|
|
|
5433
5749
|
if (removeResult?.success === false) {
|
|
5434
|
-
return {
|
|
5750
|
+
return { kind: 'terminal', result: {
|
|
5435
5751
|
success: false,
|
|
5436
5752
|
code: 'cleanup_failed',
|
|
5437
5753
|
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
@@ -5447,7 +5763,7 @@ export class DaemonCommandRouter {
|
|
|
5447
5763
|
refineStages,
|
|
5448
5764
|
...(ledgerError ? { ledgerError } : {}),
|
|
5449
5765
|
finalBranchConvergenceState,
|
|
5450
|
-
};
|
|
5766
|
+
} };
|
|
5451
5767
|
}
|
|
5452
5768
|
|
|
5453
5769
|
// Push logic: after a successful merge, either auto-push or surface push info
|
|
@@ -5474,7 +5790,7 @@ export class DaemonCommandRouter {
|
|
|
5474
5790
|
}
|
|
5475
5791
|
}
|
|
5476
5792
|
|
|
5477
|
-
return {
|
|
5793
|
+
return { kind: 'terminal', result: {
|
|
5478
5794
|
success: true,
|
|
5479
5795
|
merged: true,
|
|
5480
5796
|
branch,
|
|
@@ -5496,10 +5812,7 @@ export class DaemonCommandRouter {
|
|
|
5496
5812
|
pushCommand: `git push origin ${baseBranch}`,
|
|
5497
5813
|
pushNote: 'requireApprovalForPush is enabled — run the push command or obtain user approval before pushing.',
|
|
5498
5814
|
}),
|
|
5499
|
-
};
|
|
5500
|
-
} catch (e: any) {
|
|
5501
|
-
return { success: false, error: e.message, refineStages };
|
|
5502
|
-
}
|
|
5815
|
+
} };
|
|
5503
5816
|
}
|
|
5504
5817
|
|
|
5505
5818
|
/**
|
|
@@ -6231,1179 +6544,19 @@ export class DaemonCommandRouter {
|
|
|
6231
6544
|
return await medFamilyHandler(this.buildMedFamilyContext(), args);
|
|
6232
6545
|
}
|
|
6233
6546
|
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
: undefined;
|
|
6248
|
-
const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
|
|
6249
|
-
return { success: true, events };
|
|
6250
|
-
}
|
|
6251
|
-
|
|
6252
|
-
case 'interactive_prompt_response': {
|
|
6253
|
-
const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
|
|
6254
|
-
? args.targetSessionId.trim()
|
|
6255
|
-
: typeof args?.sessionId === 'string' && args.sessionId.trim()
|
|
6256
|
-
? args.sessionId.trim()
|
|
6257
|
-
: '';
|
|
6258
|
-
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
6259
|
-
const response = normalizeInteractivePromptResponse(args?.response ?? args);
|
|
6260
|
-
const instance = this.deps.instanceManager.getInstance(sessionId);
|
|
6261
|
-
if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
|
|
6262
|
-
this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
|
|
6263
|
-
return { success: true };
|
|
6264
|
-
}
|
|
6265
|
-
|
|
6266
|
-
// ─── Mesh Coordinator Launch ───
|
|
6267
|
-
case 'launch_mesh_coordinator': {
|
|
6268
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6269
|
-
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
6270
|
-
// Optional per-launch system-prompt addition. Dashboard or API
|
|
6271
|
-
// callers (e.g. when spawning a mesh-node-specific coordinator)
|
|
6272
|
-
// can pass extra context that gets appended to the rendered
|
|
6273
|
-
// default prompt under the "## Additional Context" section.
|
|
6274
|
-
// Going through buildCoordinatorSystemPrompt's userInstruction
|
|
6275
|
-
// means user-level override files (~/.adhdev/coordinator-prompts)
|
|
6276
|
-
// and this per-launch addition compose cleanly: an override
|
|
6277
|
-
// wins outright, but if there's no override, the default
|
|
6278
|
-
// prompt + the optional append.md file + this extra context
|
|
6279
|
-
// all stack in declared order.
|
|
6280
|
-
const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
|
|
6281
|
-
? args.extraSystemPrompt.trim()
|
|
6282
|
-
: '';
|
|
6283
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6284
|
-
|
|
6285
|
-
try {
|
|
6286
|
-
const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
|
|
6287
|
-
const { buildMissionPromptSection } = await import('../mesh/mesh-missions.js');
|
|
6288
|
-
// M3-3: inject the active mission summary into the coordinator prompt.
|
|
6289
|
-
// Best-effort — a store failure must not block coordinator launch.
|
|
6290
|
-
const buildMissionSectionBestEffort = (id: string): string => {
|
|
6291
|
-
try { return buildMissionPromptSection(id); } catch { return ''; }
|
|
6292
|
-
};
|
|
6293
|
-
|
|
6294
|
-
// Support inline mesh data from cloud (bypasses local meshes.json lookup)
|
|
6295
|
-
let mesh: any;
|
|
6296
|
-
if (args?.inlineMesh && typeof args.inlineMesh === 'object') {
|
|
6297
|
-
mesh = args.inlineMesh;
|
|
6298
|
-
// Cache cloud mesh so the MCP server can retrieve it via get_mesh
|
|
6299
|
-
this.inlineMeshCache.set(meshId, mesh);
|
|
6300
|
-
} else {
|
|
6301
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
6302
|
-
mesh = getMesh(meshId);
|
|
6303
|
-
}
|
|
6304
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6305
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
6306
|
-
if (!meshHost.canOwnCoordinator) {
|
|
6307
|
-
return {
|
|
6308
|
-
success: false,
|
|
6309
|
-
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
6310
|
-
meshId,
|
|
6311
|
-
cliType,
|
|
6312
|
-
};
|
|
6313
|
-
}
|
|
6314
|
-
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
6315
|
-
|
|
6316
|
-
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
6317
|
-
? args.coordinatorNodeId.trim()
|
|
6318
|
-
: '';
|
|
6319
|
-
const preferredCoordinatorNodeId = requestedCoordinatorNodeId
|
|
6320
|
-
|| (typeof mesh.coordinator?.preferredNodeId === 'string' ? mesh.coordinator.preferredNodeId.trim() : '');
|
|
6321
|
-
const coordinatorNode = preferredCoordinatorNodeId
|
|
6322
|
-
? mesh.nodes.find((node: any) => node?.id === preferredCoordinatorNodeId || node?.nodeId === preferredCoordinatorNodeId)
|
|
6323
|
-
: mesh.nodes[0];
|
|
6324
|
-
if (!coordinatorNode) {
|
|
6325
|
-
return {
|
|
6326
|
-
success: false,
|
|
6327
|
-
code: 'mesh_coordinator_node_not_found',
|
|
6328
|
-
error: `Coordinator node ${preferredCoordinatorNodeId} was not found in mesh`,
|
|
6329
|
-
meshId,
|
|
6330
|
-
cliType,
|
|
6331
|
-
};
|
|
6332
|
-
}
|
|
6333
|
-
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
6334
|
-
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
6335
|
-
: [];
|
|
6336
|
-
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
6337
|
-
const workspace = readLiveMeshNodeWorkspace({
|
|
6338
|
-
meshId,
|
|
6339
|
-
nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || ''),
|
|
6340
|
-
liveSessionRecords: liveMeshSessions,
|
|
6341
|
-
allowCoordinatorSession: true,
|
|
6342
|
-
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
6343
|
-
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
6344
|
-
if (!cliType) {
|
|
6345
|
-
const resolved = await resolveProviderTypeFromPriority({
|
|
6346
|
-
nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || 'coordinator'),
|
|
6347
|
-
providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
|
|
6348
|
-
providerLoader: this.deps.providerLoader,
|
|
6349
|
-
onStatusChange: this.deps.onStatusChange,
|
|
6350
|
-
});
|
|
6351
|
-
if (!resolved.providerType) {
|
|
6352
|
-
return {
|
|
6353
|
-
success: false,
|
|
6354
|
-
code: 'mesh_coordinator_provider_priority_unusable',
|
|
6355
|
-
error: resolved.error || 'No usable provider found from node providerPriority',
|
|
6356
|
-
meshId,
|
|
6357
|
-
cliType,
|
|
6358
|
-
workspace,
|
|
6359
|
-
};
|
|
6360
|
-
}
|
|
6361
|
-
cliType = resolved.providerType;
|
|
6362
|
-
}
|
|
6363
|
-
const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
|
|
6364
|
-
const coordinatorSetup = resolveMeshCoordinatorSetup({
|
|
6365
|
-
provider: providerMeta,
|
|
6366
|
-
cliType,
|
|
6367
|
-
meshId,
|
|
6368
|
-
workspace,
|
|
6369
|
-
});
|
|
6370
|
-
|
|
6371
|
-
if (coordinatorSetup.kind === 'unsupported') {
|
|
6372
|
-
return {
|
|
6373
|
-
success: false,
|
|
6374
|
-
code: 'mesh_coordinator_unsupported',
|
|
6375
|
-
error: coordinatorSetup.reason,
|
|
6376
|
-
meshId,
|
|
6377
|
-
cliType,
|
|
6378
|
-
workspace,
|
|
6379
|
-
};
|
|
6380
|
-
}
|
|
6381
|
-
|
|
6382
|
-
if (coordinatorSetup.kind === 'manual') {
|
|
6383
|
-
return {
|
|
6384
|
-
success: false,
|
|
6385
|
-
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
6386
|
-
error: coordinatorSetup.instructions,
|
|
6387
|
-
meshId,
|
|
6388
|
-
cliType,
|
|
6389
|
-
workspace,
|
|
6390
|
-
meshCoordinatorSetup: coordinatorSetup,
|
|
6391
|
-
};
|
|
6392
|
-
}
|
|
6393
|
-
|
|
6394
|
-
// ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
|
|
6395
|
-
if (coordinatorSetup.kind === 'cli_command') {
|
|
6396
|
-
// Build coordinator prompt first — fail closed on errors.
|
|
6397
|
-
let cliCmdSystemPrompt = '';
|
|
6398
|
-
try {
|
|
6399
|
-
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
|
|
6400
|
-
} catch (error: any) {
|
|
6401
|
-
const message = error?.message || String(error);
|
|
6402
|
-
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
6403
|
-
return {
|
|
6404
|
-
success: false,
|
|
6405
|
-
code: 'mesh_coordinator_prompt_failed',
|
|
6406
|
-
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
6407
|
-
meshId, cliType, workspace,
|
|
6408
|
-
};
|
|
6409
|
-
}
|
|
6410
|
-
|
|
6411
|
-
// Run the provider's MCP registration command under a
|
|
6412
|
-
// PTY. Some providers (agy, future bubbletea CLIs)
|
|
6413
|
-
// refuse to run without /dev/tty, so pipe-only
|
|
6414
|
-
// execFileSync silently no-ops the registration and
|
|
6415
|
-
// the coordinator ends up without any mcp tools. With
|
|
6416
|
-
// a real PTY the registration goes through and the
|
|
6417
|
-
// exit code tells us whether it actually persisted.
|
|
6418
|
-
let mcpRegistrationOk = false;
|
|
6419
|
-
let mcpRegistrationFailure: {
|
|
6420
|
-
command: string;
|
|
6421
|
-
output: string;
|
|
6422
|
-
exitCode: number | null;
|
|
6423
|
-
signal: number | null;
|
|
6424
|
-
timedOut: boolean;
|
|
6425
|
-
} | null = null;
|
|
6426
|
-
try {
|
|
6427
|
-
const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
|
|
6428
|
-
const registrationPlan = buildMeshCoordinatorRegistrationPlan(
|
|
6429
|
-
cliType,
|
|
6430
|
-
coordinatorSetup.serverName,
|
|
6431
|
-
coordinatorSetup.command,
|
|
6432
|
-
);
|
|
6433
|
-
for (const step of registrationPlan) {
|
|
6434
|
-
const renderedCommand = [step.command, ...step.args].join(' ');
|
|
6435
|
-
LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
|
|
6436
|
-
const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
|
|
6437
|
-
if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
|
|
6438
|
-
if (step.required) mcpRegistrationOk = true;
|
|
6439
|
-
continue;
|
|
6440
|
-
}
|
|
6441
|
-
LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
|
|
6442
|
-
if (step.required) {
|
|
6443
|
-
mcpRegistrationFailure = {
|
|
6444
|
-
command: renderedCommand,
|
|
6445
|
-
output: ptyResult.output.slice(-2000),
|
|
6446
|
-
exitCode: ptyResult.exitCode,
|
|
6447
|
-
signal: ptyResult.signal,
|
|
6448
|
-
timedOut: ptyResult.timedOut,
|
|
6449
|
-
};
|
|
6450
|
-
break;
|
|
6451
|
-
}
|
|
6452
|
-
}
|
|
6453
|
-
} catch (error: any) {
|
|
6454
|
-
LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
|
|
6455
|
-
mcpRegistrationFailure = {
|
|
6456
|
-
command: coordinatorSetup.command,
|
|
6457
|
-
output: error?.message || String(error),
|
|
6458
|
-
exitCode: null,
|
|
6459
|
-
signal: null,
|
|
6460
|
-
timedOut: false,
|
|
6461
|
-
};
|
|
6462
|
-
}
|
|
6463
|
-
|
|
6464
|
-
if (!mcpRegistrationOk) {
|
|
6465
|
-
return {
|
|
6466
|
-
success: false,
|
|
6467
|
-
code: 'mesh_coordinator_mcp_registration_failed',
|
|
6468
|
-
error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
|
|
6469
|
-
meshId,
|
|
6470
|
-
cliType,
|
|
6471
|
-
workspace,
|
|
6472
|
-
registration: mcpRegistrationFailure,
|
|
6473
|
-
};
|
|
6474
|
-
}
|
|
6475
|
-
|
|
6476
|
-
// Codex gives repo-local .mcp.json precedence over its
|
|
6477
|
-
// global `codex mcp add` registration. Refresh an
|
|
6478
|
-
// existing ADHDev entry so a stale workspace command
|
|
6479
|
-
// cannot shadow the registration we just verified.
|
|
6480
|
-
if (cliType === 'codex-cli') {
|
|
6481
|
-
const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
|
|
6482
|
-
if (fs.existsSync(repoMcpConfigPath)) {
|
|
6483
|
-
try {
|
|
6484
|
-
const repoMcpConfig = parseMeshCoordinatorMcpConfig(
|
|
6485
|
-
fs.readFileSync(repoMcpConfigPath, 'utf-8'),
|
|
6486
|
-
'claude_mcp_json',
|
|
6487
|
-
);
|
|
6488
|
-
const existingServers = repoMcpConfig.mcpServers;
|
|
6489
|
-
if (
|
|
6490
|
-
existingServers
|
|
6491
|
-
&& typeof existingServers === 'object'
|
|
6492
|
-
&& !Array.isArray(existingServers)
|
|
6493
|
-
&& existingServers[coordinatorSetup.serverName]
|
|
6494
|
-
) {
|
|
6495
|
-
fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
|
|
6496
|
-
...repoMcpConfig,
|
|
6497
|
-
mcpServers: {
|
|
6498
|
-
...existingServers,
|
|
6499
|
-
[coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
|
|
6500
|
-
},
|
|
6501
|
-
}, 'claude_mcp_json'), 'utf-8');
|
|
6502
|
-
LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
|
|
6503
|
-
}
|
|
6504
|
-
} catch (error: any) {
|
|
6505
|
-
return {
|
|
6506
|
-
success: false,
|
|
6507
|
-
code: 'mesh_coordinator_config_write_failed',
|
|
6508
|
-
error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
|
|
6509
|
-
meshId,
|
|
6510
|
-
cliType,
|
|
6511
|
-
workspace,
|
|
6512
|
-
};
|
|
6513
|
-
}
|
|
6514
|
-
}
|
|
6515
|
-
}
|
|
6516
|
-
|
|
6517
|
-
// Inject system prompt declaratively from provider.v1.json.
|
|
6518
|
-
const cliCmdArgs: string[] = [];
|
|
6519
|
-
const cliCmdEnv: Record<string, string> = {};
|
|
6520
|
-
let cliCmdContextFilePath: string | undefined;
|
|
6521
|
-
if (cliCmdSystemPrompt) {
|
|
6522
|
-
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
6523
|
-
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
6524
|
-
cliCmdSystemPrompt,
|
|
6525
|
-
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
6526
|
-
{ cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
|
|
6527
|
-
);
|
|
6528
|
-
cliCmdContextFilePath = effect.contextFilePath;
|
|
6529
|
-
}
|
|
6530
|
-
|
|
6531
|
-
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
6532
|
-
cliType,
|
|
6533
|
-
dir: workspace,
|
|
6534
|
-
cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
|
|
6535
|
-
env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
|
|
6536
|
-
settings: { meshCoordinatorFor: meshId },
|
|
6537
|
-
});
|
|
6538
|
-
|
|
6539
|
-
// R48 inject-then-remove. Spawn was just kicked off above; agy and
|
|
6540
|
-
// gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
|
|
6541
|
-
// cache it for the rest of the session, so we can safely strip
|
|
6542
|
-
// the wrapper from disk shortly after launch. That keeps any
|
|
6543
|
-
// worker session launched into the same workspace later from
|
|
6544
|
-
// picking up our wrapper block.
|
|
6545
|
-
if (cliCmdLaunch?.success && cliCmdContextFilePath) {
|
|
6546
|
-
const stripPath = cliCmdContextFilePath;
|
|
6547
|
-
setTimeout(() => {
|
|
6548
|
-
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
6549
|
-
stripCoordinatorWrapperFile(stripPath);
|
|
6550
|
-
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
|
|
6551
|
-
}).catch(() => { /* best-effort */ });
|
|
6552
|
-
}, 5000);
|
|
6553
|
-
}
|
|
6554
|
-
|
|
6555
|
-
if (!cliCmdLaunch?.success) {
|
|
6556
|
-
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
6557
|
-
}
|
|
6558
|
-
|
|
6559
|
-
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
6560
|
-
const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
|
|
6561
|
-
if (cliCmdSessionId) {
|
|
6562
|
-
const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
6563
|
-
registerMeshCoordinator({
|
|
6564
|
-
meshId,
|
|
6565
|
-
sessionId: cliCmdSessionId,
|
|
6566
|
-
workspace,
|
|
6567
|
-
startedAt: Date.now(),
|
|
6568
|
-
cliType,
|
|
6569
|
-
systemPrompt: cliCmdSystemPrompt || undefined,
|
|
6570
|
-
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
6571
|
-
injection: cliCmdInjectionDecl ? {
|
|
6572
|
-
mode: cliCmdInjectionDecl.mode,
|
|
6573
|
-
target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
|
|
6574
|
-
: 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
|
|
6575
|
-
: 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
|
|
6576
|
-
: undefined,
|
|
6577
|
-
} : undefined,
|
|
6578
|
-
});
|
|
6579
|
-
}
|
|
6580
|
-
try {
|
|
6581
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
6582
|
-
appendLedgerEntry(meshId, {
|
|
6583
|
-
kind: 'coordinator_started',
|
|
6584
|
-
sessionId: cliCmdSessionId,
|
|
6585
|
-
providerType: cliType,
|
|
6586
|
-
payload: { workspace },
|
|
6587
|
-
});
|
|
6588
|
-
} catch { /* best-effort */ }
|
|
6589
|
-
|
|
6590
|
-
return {
|
|
6591
|
-
success: true,
|
|
6592
|
-
meshId,
|
|
6593
|
-
cliType,
|
|
6594
|
-
workspace,
|
|
6595
|
-
sessionId: cliCmdSessionId,
|
|
6596
|
-
mcpRegistered: mcpRegistrationOk,
|
|
6597
|
-
};
|
|
6598
|
-
}
|
|
6599
|
-
|
|
6600
|
-
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
6601
|
-
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
6602
|
-
return {
|
|
6603
|
-
success: false,
|
|
6604
|
-
code: 'mesh_coordinator_unsupported',
|
|
6605
|
-
error: `Unsupported auto-import MCP config format: ${String(coordinatorSetup.configFormat)}`,
|
|
6606
|
-
meshId,
|
|
6607
|
-
cliType,
|
|
6608
|
-
workspace,
|
|
6609
|
-
};
|
|
6610
|
-
}
|
|
6611
|
-
|
|
6612
|
-
// Build the coordinator prompt before mutating workspace config or launching.
|
|
6613
|
-
// Prompt generation failures are configuration/data-shape errors; fail closed so
|
|
6614
|
-
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
6615
|
-
let systemPrompt = '';
|
|
6616
|
-
try {
|
|
6617
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
|
|
6618
|
-
} catch (error: any) {
|
|
6619
|
-
const message = error?.message || String(error);
|
|
6620
|
-
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
6621
|
-
return {
|
|
6622
|
-
success: false,
|
|
6623
|
-
code: 'mesh_coordinator_prompt_failed',
|
|
6624
|
-
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
6625
|
-
meshId,
|
|
6626
|
-
cliType,
|
|
6627
|
-
workspace,
|
|
6628
|
-
};
|
|
6629
|
-
}
|
|
6630
|
-
|
|
6631
|
-
// 1. Write provider-declared MCP config for CLIs that auto-import it.
|
|
6632
|
-
const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
|
|
6633
|
-
const { dirname } = await import('path');
|
|
6634
|
-
const mcpConfigPath = coordinatorSetup.configPath;
|
|
6635
|
-
const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
|
|
6636
|
-
? createHermesManualMeshCoordinatorSetup(meshId, workspace)
|
|
6637
|
-
: null;
|
|
6638
|
-
let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
|
|
6639
|
-
if (hermesManualFallback) {
|
|
6640
|
-
try {
|
|
6641
|
-
hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
|
|
6642
|
-
} catch (error: any) {
|
|
6643
|
-
const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
|
|
6644
|
-
LOG.error('MeshCoordinator', message);
|
|
6645
|
-
return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
|
|
6646
|
-
}
|
|
6647
|
-
}
|
|
6648
|
-
const returnManualFallback = (message: string) => ({
|
|
6649
|
-
success: false,
|
|
6650
|
-
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
6651
|
-
error: message,
|
|
6652
|
-
meshId,
|
|
6653
|
-
cliType,
|
|
6654
|
-
workspace,
|
|
6655
|
-
meshCoordinatorSetup: hermesManualFallback,
|
|
6656
|
-
});
|
|
6657
|
-
|
|
6658
|
-
// Merge ADHDev mesh server into existing config.
|
|
6659
|
-
// Pass full mesh data as env var so the MCP server can bootstrap
|
|
6660
|
-
// without depending on meshes.json or a running daemon.
|
|
6661
|
-
const mcpServerEntry: Record<string, any> = {
|
|
6662
|
-
command: coordinatorSetup.mcpServer.command,
|
|
6663
|
-
args: coordinatorSetup.mcpServer.args,
|
|
6664
|
-
};
|
|
6665
|
-
if (args?.inlineMesh) {
|
|
6666
|
-
const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
|
|
6667
|
-
const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
|
|
6668
|
-
mcpServerEntry.env = {
|
|
6669
|
-
ADHDEV_INLINE_MESH: JSON.stringify(mesh),
|
|
6670
|
-
ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
|
|
6671
|
-
};
|
|
6672
|
-
}
|
|
6673
|
-
|
|
6674
|
-
try {
|
|
6675
|
-
mkdirSync(dirname(mcpConfigPath), { recursive: true });
|
|
6676
|
-
} catch (error: any) {
|
|
6677
|
-
const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
|
|
6678
|
-
LOG.error('MeshCoordinator', message);
|
|
6679
|
-
if (hermesManualFallback) return returnManualFallback(message);
|
|
6680
|
-
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
6681
|
-
}
|
|
6682
|
-
|
|
6683
|
-
// Backup existing MCP config if present.
|
|
6684
|
-
const hadExistingMcpConfig = existsSync(mcpConfigPath);
|
|
6685
|
-
let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
|
|
6686
|
-
if (hermesBaseConfig) {
|
|
6687
|
-
copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
|
|
6688
|
-
}
|
|
6689
|
-
if (hadExistingMcpConfig) {
|
|
6690
|
-
try {
|
|
6691
|
-
const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
6692
|
-
const existingCoordinatorConfig = hermesManualFallback
|
|
6693
|
-
? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
|
|
6694
|
-
: parsedExistingMcpConfig;
|
|
6695
|
-
existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
|
|
6696
|
-
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
6697
|
-
} catch (error: any) {
|
|
6698
|
-
LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
|
|
6699
|
-
return {
|
|
6700
|
-
success: false,
|
|
6701
|
-
code: 'mesh_coordinator_config_parse_failed',
|
|
6702
|
-
error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
|
|
6703
|
-
};
|
|
6704
|
-
}
|
|
6705
|
-
}
|
|
6706
|
-
|
|
6707
|
-
const mcpServersKey = getMcpServersKey(configFormat);
|
|
6708
|
-
const existingServers = existingMcpConfig[mcpServersKey];
|
|
6709
|
-
const mcpConfig = {
|
|
6710
|
-
...existingMcpConfig,
|
|
6711
|
-
[mcpServersKey]: {
|
|
6712
|
-
...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
|
|
6713
|
-
[coordinatorSetup.serverName]: mcpServerEntry,
|
|
6714
|
-
},
|
|
6715
|
-
};
|
|
6716
|
-
try {
|
|
6717
|
-
writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
|
|
6718
|
-
} catch (error: any) {
|
|
6719
|
-
const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
|
|
6720
|
-
LOG.error('MeshCoordinator', message);
|
|
6721
|
-
if (hermesManualFallback) return returnManualFallback(message);
|
|
6722
|
-
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
6723
|
-
}
|
|
6724
|
-
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
6725
|
-
|
|
6726
|
-
const cliArgs: string[] = [];
|
|
6727
|
-
const launchEnv: Record<string, string> = {};
|
|
6728
|
-
if (configFormat === 'hermes_config_yaml') {
|
|
6729
|
-
launchEnv.HERMES_HOME = dirname(mcpConfigPath);
|
|
6730
|
-
launchEnv.HERMES_IGNORE_USER_CONFIG = '';
|
|
6731
|
-
}
|
|
6732
|
-
let autoImportContextFilePath: string | undefined;
|
|
6733
|
-
if (systemPrompt) {
|
|
6734
|
-
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
6735
|
-
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
6736
|
-
systemPrompt,
|
|
6737
|
-
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
6738
|
-
{ cliArgs, launchEnv, workspace, cliType },
|
|
6739
|
-
);
|
|
6740
|
-
autoImportContextFilePath = effect.contextFilePath;
|
|
6741
|
-
}
|
|
6742
|
-
if (cliType === 'claude-cli') {
|
|
6743
|
-
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
6744
|
-
}
|
|
6745
|
-
|
|
6746
|
-
// 3. Launch CLI session via existing cliManager.
|
|
6747
|
-
// Provider-specific prompt injection remains fail-closed: Claude gets
|
|
6748
|
-
// explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
|
|
6749
|
-
const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
6750
|
-
cliType,
|
|
6751
|
-
dir: workspace,
|
|
6752
|
-
cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
|
|
6753
|
-
env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
|
|
6754
|
-
settings: {
|
|
6755
|
-
meshCoordinatorFor: meshId
|
|
6756
|
-
}
|
|
6757
|
-
});
|
|
6758
|
-
|
|
6759
|
-
// R48 inject-then-remove. See the cli_command branch for context;
|
|
6760
|
-
// same idea: strip the wrapper from disk ~5s after launch so the
|
|
6761
|
-
// user's AGENTS.md / GEMINI.md is untouched the moment any
|
|
6762
|
-
// worker session opens up in the same workspace.
|
|
6763
|
-
if (launchResult?.success && autoImportContextFilePath) {
|
|
6764
|
-
const stripPath = autoImportContextFilePath;
|
|
6765
|
-
setTimeout(() => {
|
|
6766
|
-
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
6767
|
-
stripCoordinatorWrapperFile(stripPath);
|
|
6768
|
-
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
|
|
6769
|
-
}).catch(() => { /* best-effort */ });
|
|
6770
|
-
}, 5000);
|
|
6771
|
-
}
|
|
6772
|
-
|
|
6773
|
-
if (!launchResult?.success) {
|
|
6774
|
-
return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
|
|
6775
|
-
}
|
|
6776
|
-
|
|
6777
|
-
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
|
|
6778
|
-
const launchSessionId = launchResult.sessionId || launchResult.id;
|
|
6779
|
-
if (launchSessionId) {
|
|
6780
|
-
const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
6781
|
-
registerMeshCoordinator({
|
|
6782
|
-
meshId,
|
|
6783
|
-
sessionId: launchSessionId,
|
|
6784
|
-
workspace,
|
|
6785
|
-
startedAt: Date.now(),
|
|
6786
|
-
cliType,
|
|
6787
|
-
systemPrompt: systemPrompt || undefined,
|
|
6788
|
-
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
6789
|
-
mcpConfigPath,
|
|
6790
|
-
injection: autoImportInjectionDecl ? {
|
|
6791
|
-
mode: autoImportInjectionDecl.mode,
|
|
6792
|
-
target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
|
|
6793
|
-
: 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
|
|
6794
|
-
: 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
|
|
6795
|
-
: undefined,
|
|
6796
|
-
} : undefined,
|
|
6797
|
-
});
|
|
6798
|
-
}
|
|
6799
|
-
|
|
6800
|
-
// Record coordinator launch in task ledger
|
|
6801
|
-
try {
|
|
6802
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
6803
|
-
appendLedgerEntry(meshId, {
|
|
6804
|
-
kind: 'coordinator_started',
|
|
6805
|
-
sessionId: launchSessionId,
|
|
6806
|
-
providerType: cliType,
|
|
6807
|
-
payload: { workspace },
|
|
6808
|
-
});
|
|
6809
|
-
} catch { /* ledger append is best-effort */ }
|
|
6810
|
-
|
|
6811
|
-
return {
|
|
6812
|
-
success: true,
|
|
6813
|
-
meshId,
|
|
6814
|
-
cliType,
|
|
6815
|
-
workspace,
|
|
6816
|
-
sessionId: launchSessionId,
|
|
6817
|
-
mcpConfigWritten: true,
|
|
6818
|
-
};
|
|
6819
|
-
} catch (e: any) {
|
|
6820
|
-
LOG.error('MeshCoordinator', `Failed: ${e.message}`);
|
|
6821
|
-
return { success: false, error: e.message };
|
|
6822
|
-
}
|
|
6823
|
-
}
|
|
6824
|
-
|
|
6825
|
-
case 'mesh_status': {
|
|
6826
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6827
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6828
|
-
try {
|
|
6829
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6830
|
-
const mesh = meshRecord?.mesh;
|
|
6831
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6832
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
6833
|
-
|
|
6834
|
-
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
6835
|
-
// Compact (default) elides each mission's full goal text from the
|
|
6836
|
-
// payload — coordinators polling node health don't need every
|
|
6837
|
-
// mission's multi-hundred-char goal repeated. verbose=true (or the
|
|
6838
|
-
// explicit compact=false) restores full goals. Verbose bypasses the
|
|
6839
|
-
// shared (compact) aggregate cache so a verbose call never poisons
|
|
6840
|
-
// the compact cache and vice versa.
|
|
6841
|
-
const verboseMissions = args?.verbose === true || args?.compact === false;
|
|
6842
|
-
// See (B3) below: scope the peek to this daemon when the
|
|
6843
|
-
// caller doesn't tell us, otherwise scoped events look
|
|
6844
|
-
// missing and we falsely return a stale cache.
|
|
6845
|
-
const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
6846
|
-
? args.coordinatorDaemonId.trim()
|
|
6847
|
-
: (this.deps.statusInstanceId || undefined);
|
|
6848
|
-
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
|
|
6849
|
-
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
6850
|
-
if (!refreshRequested && !verboseMissions && pendingCoordinatorEventCount === 0) {
|
|
6851
|
-
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
6852
|
-
if (cachedStatus) {
|
|
6853
|
-
logRepoMeshStatusDebug('return_cached', {
|
|
6854
|
-
meshId,
|
|
6855
|
-
command: 'mesh_status',
|
|
6856
|
-
refreshRequested,
|
|
6857
|
-
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
6858
|
-
});
|
|
6859
|
-
return cachedStatus;
|
|
6860
|
-
}
|
|
6861
|
-
}
|
|
6862
|
-
const refreshReason = refreshRequested
|
|
6863
|
-
? 'explicit_refresh'
|
|
6864
|
-
: pendingCoordinatorEventCount > 0
|
|
6865
|
-
? 'pending_coordinator_events'
|
|
6866
|
-
: hadAggregateCache
|
|
6867
|
-
? 'stale_pending_cache_refresh'
|
|
6868
|
-
: 'cold_cache_miss';
|
|
6869
|
-
|
|
6870
|
-
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
6871
|
-
const queue = getQueue(meshId);
|
|
6872
|
-
const queueSummary = getMeshQueueStats(meshId);
|
|
6873
|
-
|
|
6874
|
-
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
6875
|
-
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
6876
|
-
const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
|
|
6877
|
-
const ledgerSummary = getLedgerSummary(meshId);
|
|
6878
|
-
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
6879
|
-
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
6880
|
-
: [];
|
|
6881
|
-
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
6882
|
-
|
|
6883
|
-
const localMachineId = loadConfig().machineId || '';
|
|
6884
|
-
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
6885
|
-
// Shared probe gate for this mesh_status call: the bootstrap
|
|
6886
|
-
// hydrate below and the per-node render loop further down both
|
|
6887
|
-
// probe the same peers — route both through this cache so they
|
|
6888
|
-
// dedup within the call and reuse recent results across calls.
|
|
6889
|
-
const meshGitProbeCache = this.meshGitProbeCache;
|
|
6890
|
-
const directTruth = requireDirectPeerTruth
|
|
6891
|
-
? await hydrateInlineMeshDirectTruth({
|
|
6892
|
-
mesh,
|
|
6893
|
-
meshSource: meshRecord.source,
|
|
6894
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6895
|
-
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
6896
|
-
statusInstanceId: this.deps.statusInstanceId,
|
|
6897
|
-
localMachineId,
|
|
6898
|
-
// Standing-state model: only an explicit refresh fans
|
|
6899
|
-
// out a blocking peer git probe. Default loads return
|
|
6900
|
-
// held truth so one slow peer can't block the graph.
|
|
6901
|
-
probeRemotePeers: refreshRequested,
|
|
6902
|
-
probeCache: meshGitProbeCache,
|
|
6903
|
-
})
|
|
6904
|
-
: {
|
|
6905
|
-
directEvidenceCount: 0,
|
|
6906
|
-
localConfirmedCount: 0,
|
|
6907
|
-
peerAttemptedCount: 0,
|
|
6908
|
-
peerConfirmedCount: 0,
|
|
6909
|
-
standingEvidenceCount: 0,
|
|
6910
|
-
unavailableNodeIds: [] as string[],
|
|
6911
|
-
deadNodeIds: [] as string[],
|
|
6912
|
-
};
|
|
6913
|
-
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
6914
|
-
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
6915
|
-
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
6916
|
-
&& !refreshRequested
|
|
6917
|
-
&& directTruth.directEvidenceCount > 0
|
|
6918
|
-
&& directTruth.peerAttemptedCount === 0;
|
|
6919
|
-
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
6920
|
-
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
6921
|
-
: directTruth;
|
|
6922
|
-
const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
6923
|
-
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
6924
|
-
&& Array.isArray(mesh.nodes)
|
|
6925
|
-
&& mesh.nodes
|
|
6926
|
-
.filter((node: any) => unavailableDirectTruthNodeIds.has(normalizeMeshNodeId(node) ?? ''))
|
|
6927
|
-
.every((node: any) => node?.isLocalWorktree === true);
|
|
6928
|
-
// Default (non-refresh) loads never hard-fail: held
|
|
6929
|
-
// standing-state truth is returned and the graph renders
|
|
6930
|
-
// immediately. The hard mesh_direct_peer_truth_unavailable
|
|
6931
|
-
// failure is reserved for an explicit refresh that actually
|
|
6932
|
-
// attempted a peer probe and could not confirm any evidence.
|
|
6933
|
-
const directTruthSatisfied = !requireDirectPeerTruth
|
|
6934
|
-
|| !refreshRequested
|
|
6935
|
-
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
6936
|
-
if (requireDirectPeerTruth && refreshRequested && !directTruthSatisfied) {
|
|
6937
|
-
const failureResult = {
|
|
6938
|
-
success: false,
|
|
6939
|
-
code: 'mesh_direct_peer_truth_unavailable',
|
|
6940
|
-
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
6941
|
-
sourceOfTruth: {
|
|
6942
|
-
membership: meshRecord.source === 'inline_cache'
|
|
6943
|
-
? 'coordinator_inline_mesh_cache'
|
|
6944
|
-
: meshRecord.source === 'local_config'
|
|
6945
|
-
? 'local_mesh_config'
|
|
6946
|
-
: 'inline_bootstrap_snapshot',
|
|
6947
|
-
coordinatorOwnsLiveTruth: false,
|
|
6948
|
-
currentStatus: 'direct_peer_truth_unavailable',
|
|
6949
|
-
directPeerTruth: {
|
|
6950
|
-
required: true,
|
|
6951
|
-
satisfied: false,
|
|
6952
|
-
directEvidenceCount: directTruth.directEvidenceCount,
|
|
6953
|
-
localConfirmedCount: directTruth.localConfirmedCount,
|
|
6954
|
-
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
6955
|
-
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
6956
|
-
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
6957
|
-
},
|
|
6958
|
-
},
|
|
6959
|
-
};
|
|
6960
|
-
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
6961
|
-
meshId,
|
|
6962
|
-
command: 'mesh_status',
|
|
6963
|
-
refreshRequested,
|
|
6964
|
-
meshSource: meshRecord.source,
|
|
6965
|
-
directTruth,
|
|
6966
|
-
});
|
|
6967
|
-
return failureResult;
|
|
6968
|
-
}
|
|
6969
|
-
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
6970
|
-
const coordinatorHostname = osHostname();
|
|
6971
|
-
const selectedCoordinatorNodeId = readStringValue(
|
|
6972
|
-
mesh.coordinator?.preferredNodeId,
|
|
6973
|
-
normalizeMeshNodeId(mesh.nodes?.[0] as any),
|
|
6974
|
-
);
|
|
6975
|
-
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
6976
|
-
? selectedCoordinatorNodeId
|
|
6977
|
-
: undefined;
|
|
6978
|
-
const refreshedAt = new Date().toISOString();
|
|
6979
|
-
const nodeStatuses = [];
|
|
6980
|
-
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
6981
|
-
const nodeId = normalizeMeshNodeId(node) ?? '';
|
|
6982
|
-
const daemonId = readStringValue(node.daemonId);
|
|
6983
|
-
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
|
|
6984
|
-
const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
|
|
6985
|
-
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
6986
|
-
const configuredCoordinatorNode = Boolean(
|
|
6987
|
-
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
6988
|
-
);
|
|
6989
|
-
const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
|
|
6990
|
-
&& !daemonId
|
|
6991
|
-
&& !nodeMachineId
|
|
6992
|
-
&& !nodeHostname;
|
|
6993
|
-
const isSelfNode = Boolean(
|
|
6994
|
-
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
6995
|
-
) || Boolean(
|
|
6996
|
-
daemonId && (daemonIdsEquivalent(daemonId, localMachineId) || daemonIdsEquivalent(daemonId, this.deps.statusInstanceId)),
|
|
6997
|
-
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
6998
|
-
|| sparseConfiguredCoordinatorNode;
|
|
6999
|
-
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
7000
|
-
localMachineId,
|
|
7001
|
-
localDaemonId: this.deps.statusInstanceId,
|
|
7002
|
-
coordinatorHostname,
|
|
7003
|
-
isSelfNode,
|
|
7004
|
-
});
|
|
7005
|
-
const status: Record<string, unknown> = {
|
|
7006
|
-
nodeId,
|
|
7007
|
-
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
7008
|
-
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
7009
|
-
? 'explicit_metadata'
|
|
7010
|
-
: 'workspace_host_provider_context',
|
|
7011
|
-
workspace: node.workspace,
|
|
7012
|
-
repoRoot: node.repoRoot,
|
|
7013
|
-
isLocalWorktree: node.isLocalWorktree,
|
|
7014
|
-
worktreeBranch: node.worktreeBranch,
|
|
7015
|
-
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
7016
|
-
daemonId,
|
|
7017
|
-
machineId: nodeMachineId || node.machineId,
|
|
7018
|
-
machine: machineIdentity,
|
|
7019
|
-
machineStatus: node.machineStatus,
|
|
7020
|
-
health: 'unknown',
|
|
7021
|
-
providers: node.providers || [],
|
|
7022
|
-
providerPriority,
|
|
7023
|
-
activeSessions: [],
|
|
7024
|
-
activeSessionDetails: [],
|
|
7025
|
-
launchReady: false,
|
|
7026
|
-
};
|
|
7027
|
-
if (isSelfNode) {
|
|
7028
|
-
status.connection = {
|
|
7029
|
-
perspective: 'selected_coordinator',
|
|
7030
|
-
source: 'mesh_peer_status',
|
|
7031
|
-
state: 'self',
|
|
7032
|
-
transport: 'local',
|
|
7033
|
-
reported: true,
|
|
7034
|
-
reason: 'Selected coordinator daemon',
|
|
7035
|
-
lastStateChangeAt: refreshedAt,
|
|
7036
|
-
};
|
|
7037
|
-
} else if (daemonId) {
|
|
7038
|
-
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
7039
|
-
status.connection = connection ?? {
|
|
7040
|
-
perspective: 'selected_coordinator',
|
|
7041
|
-
source: 'not_reported',
|
|
7042
|
-
state: 'unknown',
|
|
7043
|
-
transport: 'unknown',
|
|
7044
|
-
reported: false,
|
|
7045
|
-
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
7046
|
-
};
|
|
7047
|
-
} else {
|
|
7048
|
-
status.connection = {
|
|
7049
|
-
perspective: 'selected_coordinator',
|
|
7050
|
-
source: 'not_reported',
|
|
7051
|
-
state: 'unknown',
|
|
7052
|
-
transport: 'unknown',
|
|
7053
|
-
reported: false,
|
|
7054
|
-
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
7055
|
-
};
|
|
7056
|
-
}
|
|
7057
|
-
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
7058
|
-
meshId,
|
|
7059
|
-
node,
|
|
7060
|
-
nodeId,
|
|
7061
|
-
liveSessionRecords: liveMeshSessions,
|
|
7062
|
-
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
7063
|
-
});
|
|
7064
|
-
const workspace = readLiveMeshNodeWorkspace({
|
|
7065
|
-
meshId,
|
|
7066
|
-
nodeId,
|
|
7067
|
-
liveSessionRecords: matchedLiveSessionRecords,
|
|
7068
|
-
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
7069
|
-
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
7070
|
-
status.workspace = workspace || node.workspace;
|
|
7071
|
-
if (matchedLiveSessionRecords.length > 0) {
|
|
7072
|
-
const sessionIds = matchedLiveSessionRecords
|
|
7073
|
-
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
7074
|
-
.filter(Boolean);
|
|
7075
|
-
const providerTypes = matchedLiveSessionRecords
|
|
7076
|
-
.map((record: any) => readStringValue(record?.providerType))
|
|
7077
|
-
.filter(Boolean) as string[];
|
|
7078
|
-
status.activeSessions = sessionIds;
|
|
7079
|
-
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
7080
|
-
if (providerTypes.length > 0) {
|
|
7081
|
-
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
7082
|
-
}
|
|
7083
|
-
}
|
|
7084
|
-
if (workspace) {
|
|
7085
|
-
if (!fs.existsSync(workspace)) {
|
|
7086
|
-
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
7087
|
-
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
7088
|
-
let remoteProbeApplied = false;
|
|
7089
|
-
if (inlineTransitGit) {
|
|
7090
|
-
status.git = inlineTransitGit;
|
|
7091
|
-
status.health = inlineTransitGit.isGitRepo
|
|
7092
|
-
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
7093
|
-
: 'degraded';
|
|
7094
|
-
const connection = readObjectRecord(status.connection);
|
|
7095
|
-
const connectionState = readStringValue(connection.state);
|
|
7096
|
-
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
7097
|
-
if (!connectionReported || connectionState === 'unknown') {
|
|
7098
|
-
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
7099
|
-
}
|
|
7100
|
-
remoteProbeApplied = true;
|
|
7101
|
-
} else if (refreshRequested && !isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
7102
|
-
// Only an explicit refresh fans out a blocking
|
|
7103
|
-
// per-node git probe. On the default load a peer
|
|
7104
|
-
// with no held truth falls through to
|
|
7105
|
-
// gitProbePending below — the graph still renders.
|
|
7106
|
-
// Bounded retry (shared with the bootstrap hydrate
|
|
7107
|
-
// path), gated on the peer staying connected, so a
|
|
7108
|
-
// slow TURN-relayed peer is recovered rather than
|
|
7109
|
-
// dropped after a single timeout.
|
|
7110
|
-
const runNodeProbe = () => probeRemoteMeshGitStatusWithRetry({
|
|
7111
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
7112
|
-
daemonId,
|
|
7113
|
-
workspace,
|
|
7114
|
-
timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
|
|
7115
|
-
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
7116
|
-
getConnection: this.deps.getMeshPeerConnectionStatus,
|
|
7117
|
-
onConnection: connection => { status.connection = connection; },
|
|
7118
|
-
});
|
|
7119
|
-
// Same shared cache as the bootstrap hydrate path: within one
|
|
7120
|
-
// mesh_status call this dedups the bootstrap probe against this
|
|
7121
|
-
// per-node probe for the same peer, and across calls it reuses a
|
|
7122
|
-
// recent result so the dashboard auto-retry loop can't restart a
|
|
7123
|
-
// fresh refreshUpstream probe seconds apart.
|
|
7124
|
-
const remoteGit = await meshGitProbeCache.probe(daemonId, workspace, runNodeProbe);
|
|
7125
|
-
if (remoteGit) {
|
|
7126
|
-
status.git = remoteGit;
|
|
7127
|
-
status.health = remoteGit.isGitRepo
|
|
7128
|
-
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
7129
|
-
: 'degraded';
|
|
7130
|
-
const connection = readObjectRecord(status.connection);
|
|
7131
|
-
const connectionState = readStringValue(connection.state);
|
|
7132
|
-
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
7133
|
-
if (!connectionReported || connectionState === 'unknown') {
|
|
7134
|
-
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
7135
|
-
}
|
|
7136
|
-
const reporter = recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
7137
|
-
persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
|
|
7138
|
-
remoteProbeApplied = true;
|
|
7139
|
-
}
|
|
7140
|
-
}
|
|
7141
|
-
if (!remoteProbeApplied) {
|
|
7142
|
-
const connectionState = readStringValue((status.connection as any)?.state);
|
|
7143
|
-
const pendingPeerGitProbe = !inlineTransitGit
|
|
7144
|
-
&& !isSelfNode
|
|
7145
|
-
&& !!daemonId
|
|
7146
|
-
&& (
|
|
7147
|
-
readStringValue(status.machineStatus) === 'online'
|
|
7148
|
-
|| readStringValue(status.health) === 'online'
|
|
7149
|
-
|| connectionState === 'connecting'
|
|
7150
|
-
|| connectionState === 'connected'
|
|
7151
|
-
|| connectionState === 'unknown'
|
|
7152
|
-
);
|
|
7153
|
-
if (pendingPeerGitProbe) {
|
|
7154
|
-
status.gitProbePending = true;
|
|
7155
|
-
status.health = 'unknown';
|
|
7156
|
-
}
|
|
7157
|
-
if (applyCachedInlineMeshNodeStatus(
|
|
7158
|
-
status,
|
|
7159
|
-
node,
|
|
7160
|
-
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
7161
|
-
)) {
|
|
7162
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
7163
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
7164
|
-
nodeStatuses.push(status);
|
|
7165
|
-
continue;
|
|
7166
|
-
}
|
|
7167
|
-
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
7168
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
7169
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
7170
|
-
nodeStatuses.push(status);
|
|
7171
|
-
continue;
|
|
7172
|
-
}
|
|
7173
|
-
}
|
|
7174
|
-
} else {
|
|
7175
|
-
try {
|
|
7176
|
-
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
7177
|
-
status.git = gitStatus;
|
|
7178
|
-
const reporter = recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
7179
|
-
persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
|
|
7180
|
-
if (gitStatus.isGitRepo) {
|
|
7181
|
-
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
7182
|
-
} else {
|
|
7183
|
-
status.health = 'degraded';
|
|
7184
|
-
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
7185
|
-
}
|
|
7186
|
-
} catch {
|
|
7187
|
-
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
7188
|
-
status.health = 'degraded';
|
|
7189
|
-
}
|
|
7190
|
-
}
|
|
7191
|
-
}
|
|
7192
|
-
} else {
|
|
7193
|
-
applyCachedInlineMeshNodeStatus(status, node);
|
|
7194
|
-
}
|
|
7195
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
7196
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
7197
|
-
nodeStatuses.push(status);
|
|
7198
|
-
}
|
|
7199
|
-
|
|
7200
|
-
// (B3) Resolve the coordinator daemon scope for the peek.
|
|
7201
|
-
// mesh_status is a read-only status query — it must not consume
|
|
7202
|
-
// (drain) pending events as a side effect. Coordinators that see
|
|
7203
|
-
// pendingCoordinatorEvents in the response are expected to call
|
|
7204
|
-
// get_pending_mesh_events to explicitly drain them after processing.
|
|
7205
|
-
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
7206
|
-
? args.coordinatorDaemonId.trim()
|
|
7207
|
-
: (this.deps.statusInstanceId || undefined);
|
|
7208
|
-
const pendingCoordinatorEvents = getPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
|
|
7209
|
-
// R4: surface recent fail-loud routing drops so a coordinator/operator can see
|
|
7210
|
-
// that a worker completion was lost (envelope present, mesh unresolved) instead
|
|
7211
|
-
// of it vanishing silently. Diagnostic-only — never cached (see omit below).
|
|
7212
|
-
const unroutableDeliveries = getRecentUnroutableDeliveries();
|
|
7213
|
-
const previewFreshness = (() => {
|
|
7214
|
-
const localRepoRoot = nodeStatuses
|
|
7215
|
-
.map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
|
|
7216
|
-
.find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
|
|
7217
|
-
return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
|
|
7218
|
-
})();
|
|
7219
|
-
const asyncRefineJobs = buildMeshAsyncRefineJobs({
|
|
7220
|
-
meshId,
|
|
7221
|
-
ledgerEntries: asyncRefineLedgerEntries,
|
|
7222
|
-
pendingEvents: [...pendingCoordinatorEvents],
|
|
7223
|
-
});
|
|
7224
|
-
const historicalSessions = buildHistoricalMeshSessions({
|
|
7225
|
-
meshId,
|
|
7226
|
-
nodes: mesh.nodes || [],
|
|
7227
|
-
liveSessionRecords: liveMeshSessions,
|
|
7228
|
-
});
|
|
7229
|
-
const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
|
|
7230
|
-
// withStats opts in to per-mission operational rollups (durations /
|
|
7231
|
-
// retries) for the dashboard mission detail. The rollup scans a
|
|
7232
|
-
// bounded ledger tail per mission, but only over the bounded set
|
|
7233
|
-
// returned here (live + capped history), so the cost stays linear
|
|
7234
|
-
// in visible missions rather than the whole mesh history.
|
|
7235
|
-
const missions = getMeshStatusMissionSummaries(meshId, { verbose: verboseMissions, withStats: true });
|
|
7236
|
-
const statusResult = {
|
|
7237
|
-
success: true,
|
|
7238
|
-
meshId: mesh.id,
|
|
7239
|
-
meshName: mesh.name,
|
|
7240
|
-
repoIdentity: mesh.repoIdentity,
|
|
7241
|
-
defaultBranch: mesh.defaultBranch,
|
|
7242
|
-
refreshedAt,
|
|
7243
|
-
meshHost,
|
|
7244
|
-
sourceOfTruth: {
|
|
7245
|
-
membership: meshRecord?.source === 'inline_cache'
|
|
7246
|
-
? 'coordinator_inline_mesh_cache'
|
|
7247
|
-
: meshRecord?.source === 'local_config'
|
|
7248
|
-
? 'local_mesh_config'
|
|
7249
|
-
: 'inline_bootstrap_snapshot',
|
|
7250
|
-
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
7251
|
-
meshHost: {
|
|
7252
|
-
owner: 'mesh_host_daemon',
|
|
7253
|
-
localRole: meshHost.role,
|
|
7254
|
-
hostDaemonId: meshHost.hostDaemonId,
|
|
7255
|
-
hostNodeId: meshHost.hostNodeId,
|
|
7256
|
-
hostAddress: meshHost.hostAddress,
|
|
7257
|
-
},
|
|
7258
|
-
...(requireDirectPeerTruth ? {
|
|
7259
|
-
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
7260
|
-
directPeerTruth: {
|
|
7261
|
-
required: true,
|
|
7262
|
-
satisfied: directTruthSatisfied,
|
|
7263
|
-
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
7264
|
-
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
7265
|
-
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
7266
|
-
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
7267
|
-
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
7268
|
-
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
7269
|
-
},
|
|
7270
|
-
} : {}),
|
|
7271
|
-
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
|
|
7272
|
-
},
|
|
7273
|
-
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
7274
|
-
...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
|
|
7275
|
-
nodes: nodeStatuses,
|
|
7276
|
-
queue: { tasks: queue, summary: queueSummary },
|
|
7277
|
-
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
7278
|
-
...(missions.length > 0 ? { missions } : {}),
|
|
7279
|
-
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
7280
|
-
...(historicalSessions ? { historicalSessions } : {}),
|
|
7281
|
-
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
7282
|
-
...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
|
|
7283
|
-
activeRefineJobs: Array.from(this.runningRefineJobs.values())
|
|
7284
|
-
.filter(job => job.meshId === meshId)
|
|
7285
|
-
.map(job => ({
|
|
7286
|
-
jobId: job.jobId,
|
|
7287
|
-
nodeId: job.targetNodeId,
|
|
7288
|
-
workspace: job.workspace,
|
|
7289
|
-
startedAt: job.startedAt,
|
|
7290
|
-
status: job.status,
|
|
7291
|
-
targetCoordinatorDaemonId: job.targetCoordinatorDaemonId,
|
|
7292
|
-
})),
|
|
7293
|
-
};
|
|
7294
|
-
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, unroutableDeliveries: _unroutableDeliveries, ...cacheableStatusResult } = statusResult as any;
|
|
7295
|
-
// Verbose carries full mission goals; never store it in the shared
|
|
7296
|
-
// (compact) aggregate cache or a later compact poll would return the
|
|
7297
|
-
// heavy goals from cache. Return it without caching.
|
|
7298
|
-
const rememberedStatus = verboseMissions
|
|
7299
|
-
? cacheableStatusResult
|
|
7300
|
-
: this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
7301
|
-
const returnedStatus = {
|
|
7302
|
-
...rememberedStatus,
|
|
7303
|
-
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
7304
|
-
...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
|
|
7305
|
-
};
|
|
7306
|
-
logRepoMeshStatusDebug('return_live', {
|
|
7307
|
-
meshId,
|
|
7308
|
-
command: 'mesh_status',
|
|
7309
|
-
refreshRequested,
|
|
7310
|
-
refreshReason,
|
|
7311
|
-
meshSource: meshRecord.source,
|
|
7312
|
-
directTruth,
|
|
7313
|
-
summary: summarizeRepoMeshStatusDebug(returnedStatus),
|
|
7314
|
-
});
|
|
7315
|
-
return returnedStatus;
|
|
7316
|
-
} catch (e: any) {
|
|
7317
|
-
return { success: false, error: e.message };
|
|
7318
|
-
}
|
|
7319
|
-
}
|
|
7320
|
-
|
|
7321
|
-
case 'get_mesh_review_inbox': {
|
|
7322
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7323
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7324
|
-
try {
|
|
7325
|
-
const { deriveMeshReviewInboxItems } = await import('../mesh/mesh-review-inbox.js');
|
|
7326
|
-
const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
|
|
7327
|
-
const { getGitDiffSummary } = await import('../git/git-diff.js');
|
|
7328
|
-
const { existsSync } = await import('node:fs');
|
|
7329
|
-
|
|
7330
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7331
|
-
const mesh = meshRecord?.mesh;
|
|
7332
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7333
|
-
|
|
7334
|
-
// Ensure we have a fresh aggregate status so nodeStatuses carry
|
|
7335
|
-
// computed fields (connection.state, branchConvergence, isLocalWorktree)
|
|
7336
|
-
// that the raw mesh.nodes config objects don't have.
|
|
7337
|
-
// When the caller provides an inlineMesh, prefer its nodes directly
|
|
7338
|
-
// (they already carry the computed fields from the coordinator).
|
|
7339
|
-
const inlineNodes = args?.inlineMesh && Array.isArray((args.inlineMesh as any)?.nodes)
|
|
7340
|
-
? (args.inlineMesh as any).nodes as Record<string, unknown>[]
|
|
7341
|
-
: null;
|
|
7342
|
-
let cachedStatus = !inlineNodes ? this.getCachedAggregateMeshStatus(meshId, mesh, {}) : null;
|
|
7343
|
-
if (!cachedStatus && !inlineNodes) {
|
|
7344
|
-
const freshStatus = await this.execute('mesh_status', {
|
|
7345
|
-
meshId,
|
|
7346
|
-
inlineMesh: args?.inlineMesh,
|
|
7347
|
-
refresh: true,
|
|
7348
|
-
}, 'get_mesh_review_inbox');
|
|
7349
|
-
cachedStatus = (freshStatus?.success !== false) ? freshStatus : null;
|
|
7350
|
-
}
|
|
7351
|
-
const nodeStatuses: Record<string, unknown>[] = inlineNodes
|
|
7352
|
-
? inlineNodes
|
|
7353
|
-
: Array.isArray(cachedStatus?.nodes)
|
|
7354
|
-
? cachedStatus.nodes as Record<string, unknown>[]
|
|
7355
|
-
: Array.isArray(mesh.nodes)
|
|
7356
|
-
? mesh.nodes as Record<string, unknown>[]
|
|
7357
|
-
: [];
|
|
7358
|
-
|
|
7359
|
-
const ledgerEntries = readLedgerEntries(meshId, { tail: 300 });
|
|
7360
|
-
const derivation = deriveMeshReviewInboxItems({ nodes: nodeStatuses, ledgerEntries });
|
|
7361
|
-
|
|
7362
|
-
for (const item of derivation.items) {
|
|
7363
|
-
const workspace = item.workspace;
|
|
7364
|
-
if (!workspace || !existsSync(workspace)) continue;
|
|
7365
|
-
const baseRef = item.defaultBranch
|
|
7366
|
-
? `origin/${item.defaultBranch}`
|
|
7367
|
-
: 'origin/main';
|
|
7368
|
-
try {
|
|
7369
|
-
const diffResult = await getGitDiffSummary(workspace, { baseRef, maxFiles: 100 });
|
|
7370
|
-
if (diffResult.isGitRepo) {
|
|
7371
|
-
item.diffSummary = {
|
|
7372
|
-
baseRef,
|
|
7373
|
-
files: diffResult.files.map(f => ({
|
|
7374
|
-
path: f.path,
|
|
7375
|
-
status: f.status,
|
|
7376
|
-
insertions: f.insertions,
|
|
7377
|
-
deletions: f.deletions,
|
|
7378
|
-
binary: f.binary,
|
|
7379
|
-
oldPath: f.oldPath,
|
|
7380
|
-
})),
|
|
7381
|
-
totalFiles: diffResult.files.length,
|
|
7382
|
-
totalInsertions: diffResult.totalInsertions,
|
|
7383
|
-
totalDeletions: diffResult.totalDeletions,
|
|
7384
|
-
truncated: diffResult.truncated,
|
|
7385
|
-
...(diffResult.error ? { error: diffResult.error } : {}),
|
|
7386
|
-
};
|
|
7387
|
-
}
|
|
7388
|
-
} catch {
|
|
7389
|
-
item.diffSummary = null;
|
|
7390
|
-
}
|
|
7391
|
-
}
|
|
7392
|
-
|
|
7393
|
-
return {
|
|
7394
|
-
success: true,
|
|
7395
|
-
meshId,
|
|
7396
|
-
inbox: derivation.items,
|
|
7397
|
-
remoteNodesExcluded: derivation.remoteNodesExcluded,
|
|
7398
|
-
excludedRemoteNodeIds: derivation.excludedRemoteNodeIds,
|
|
7399
|
-
};
|
|
7400
|
-
} catch (e: any) {
|
|
7401
|
-
return { success: false, error: e.message };
|
|
7402
|
-
}
|
|
7403
|
-
}
|
|
7404
|
-
|
|
7405
|
-
default:
|
|
7406
|
-
break;
|
|
6547
|
+
// RF-ROUTER HIGH family: high-coupling commands (mesh coordinator-event
|
|
6548
|
+
// relay + interactive prompt, mesh coordinator launch, mesh aggregate
|
|
6549
|
+
// status + review inbox) are handled by the registry after the LOW and
|
|
6550
|
+
// MED families and before the (now empty) switch. HIGH handlers reach the
|
|
6551
|
+
// most router-owned state — the aggregate-status memory cache and the
|
|
6552
|
+
// running-refine-job table — so the context carries those plus bound
|
|
6553
|
+
// read/write helpers and the router's own `execute` (the
|
|
6554
|
+
// get_mesh_review_inbox mesh_status re-entry). A hit returns the same
|
|
6555
|
+
// CommandRouterResult the inlined case used to; a miss falls through to
|
|
6556
|
+
// CommandHandler delegation.
|
|
6557
|
+
const highFamilyHandler = highFamilyRegistry.get(cmd);
|
|
6558
|
+
if (highFamilyHandler) {
|
|
6559
|
+
return await highFamilyHandler(this.buildHighFamilyContext(), args);
|
|
7407
6560
|
}
|
|
7408
6561
|
|
|
7409
6562
|
return null; // Not handled at this level → delegate to CommandHandler
|