@adhdev/daemon-core 0.9.82-rc.364 → 0.9.82-rc.366
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/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/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +2 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +116 -0
- package/dist/commands/router.d.ts +291 -0
- package/dist/index.js +3824 -3565
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3811 -3553
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +8 -0
- package/dist/system/hash.d.ts +8 -0
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +30 -3
- 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/med-family/cli-agent.ts +218 -0
- package/src/commands/med-family/fast-forward.ts +198 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +35 -0
- package/src/commands/med-family/mesh-crud.ts +788 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +131 -0
- package/src/commands/med-family/types.ts +120 -0
- package/src/commands/mesh-coordinator.ts +2 -2
- package/src/commands/router.ts +328 -2847
- package/src/config/mesh-config.ts +3 -2
- package/src/mesh/mesh-active-work.ts +59 -81
- package/src/mesh/mesh-events-coordinator.ts +35 -1
- package/src/system/hash.ts +23 -0
package/src/commands/router.ts
CHANGED
|
@@ -13,6 +13,11 @@ import { DaemonCdpManager } from '../cdp/manager.js';
|
|
|
13
13
|
import { registerExtensionProviders } from '../cdp/setup.js';
|
|
14
14
|
import { DaemonCommandHandler } from './handler.js';
|
|
15
15
|
import { lowFamilyRegistry } from './low-family/index.js';
|
|
16
|
+
import { medFamilyRegistry } from './med-family/index.js';
|
|
17
|
+
import { launchIde } from './med-family/ide.js';
|
|
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';
|
|
16
21
|
import { DaemonCliManager } from './cli-manager.js';
|
|
17
22
|
import { supportsExplicitSessionResume } from './cli-manager.js';
|
|
18
23
|
import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
|
|
@@ -49,17 +54,12 @@ import { logCommand } from '../logging/command-log.js';
|
|
|
49
54
|
import type { CommandLogEntry } from '../logging/command-log.js';
|
|
50
55
|
import * as yaml from 'js-yaml';
|
|
51
56
|
import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
|
|
52
|
-
import { getSessionHostSurfaceKind
|
|
53
|
-
import {
|
|
54
|
-
import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
|
|
55
|
-
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
56
|
-
import { getRecentUnroutableDeliveries } from '../mesh/mesh-routing.js';
|
|
57
|
+
import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
|
|
58
|
+
import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
57
59
|
import { buildMeshWorkerRelayStamp } from '../mesh/mesh-events-utils.js';
|
|
58
|
-
import { buildMeshHostRequiredFailure,
|
|
60
|
+
import { buildMeshHostRequiredFailure, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
59
61
|
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
60
62
|
import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
|
|
61
|
-
import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
|
|
62
|
-
import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
|
|
63
63
|
import {
|
|
64
64
|
MESH_REFINE_CONFIG_LOCATIONS,
|
|
65
65
|
MESH_REFINE_CONFIG_SCHEMA,
|
|
@@ -85,11 +85,10 @@ import { homedir, hostname as osHostname } from 'os';
|
|
|
85
85
|
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
86
86
|
import * as fs from 'fs';
|
|
87
87
|
import { execFileSync } from 'node:child_process';
|
|
88
|
-
import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
|
|
89
88
|
import { workingDirBasename } from '../providers/working-dir.js';
|
|
90
89
|
import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
|
|
91
90
|
|
|
92
|
-
function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
91
|
+
export function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
93
92
|
const record = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
94
93
|
? policy as Record<string, unknown>
|
|
95
94
|
: {};
|
|
@@ -114,7 +113,7 @@ function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
|
114
113
|
* the field entirely (full backward compat). Routing is governed by required_tags;
|
|
115
114
|
* any legacy `role` field on the input is ignored.
|
|
116
115
|
*/
|
|
117
|
-
function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
|
|
116
|
+
export function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
|
|
118
117
|
if (!Array.isArray(value)) return [];
|
|
119
118
|
const byType = new Map<string, { providerType: string; maxParallel?: number }>();
|
|
120
119
|
for (const raw of value) {
|
|
@@ -130,13 +129,13 @@ function normalizeProviderRoles(value: unknown): Array<{ providerType: string; m
|
|
|
130
129
|
return [...byType.values()];
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
function readObjectRecord(value: unknown): Record<string, any> {
|
|
132
|
+
export function readObjectRecord(value: unknown): Record<string, any> {
|
|
134
133
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
135
134
|
? value as Record<string, any>
|
|
136
135
|
: {};
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
function readStringValue(...values: unknown[]): string | undefined {
|
|
138
|
+
export function readStringValue(...values: unknown[]): string | undefined {
|
|
140
139
|
for (const value of values) {
|
|
141
140
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
142
141
|
}
|
|
@@ -150,7 +149,7 @@ function readNumberValue(...values: unknown[]): number | undefined {
|
|
|
150
149
|
return undefined;
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
152
|
+
export function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
154
153
|
for (const value of values) {
|
|
155
154
|
if (typeof value === 'boolean') return value;
|
|
156
155
|
}
|
|
@@ -160,7 +159,7 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
160
159
|
// summarizeRepoMeshDebugGit was a hand-synced copy of the cloud git-shape
|
|
161
160
|
// summarizer; both now call shared summarizeGitShape (@adhdev/mesh-shared).
|
|
162
161
|
|
|
163
|
-
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
162
|
+
export function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
164
163
|
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
165
164
|
return {
|
|
166
165
|
success: status?.success,
|
|
@@ -192,7 +191,7 @@ function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
|
192
191
|
};
|
|
193
192
|
}
|
|
194
193
|
|
|
195
|
-
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
194
|
+
export function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
196
195
|
try {
|
|
197
196
|
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
198
197
|
} catch {
|
|
@@ -224,7 +223,7 @@ function normalizeMeshHostname(value: unknown): string | undefined {
|
|
|
224
223
|
return hostname.toLowerCase().replace(/\.$/, '');
|
|
225
224
|
}
|
|
226
225
|
|
|
227
|
-
function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
|
|
226
|
+
export function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
|
|
228
227
|
return readStringValue(
|
|
229
228
|
node.machineId,
|
|
230
229
|
node.machine_id,
|
|
@@ -254,7 +253,7 @@ function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined
|
|
|
254
253
|
);
|
|
255
254
|
}
|
|
256
255
|
|
|
257
|
-
function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
256
|
+
export function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
258
257
|
return readStringValue(
|
|
259
258
|
node.hostname,
|
|
260
259
|
node.host,
|
|
@@ -294,7 +293,7 @@ function compactMeshIdentityEvidence(value: string | undefined): string | undefi
|
|
|
294
293
|
return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
|
|
295
294
|
}
|
|
296
295
|
|
|
297
|
-
function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
296
|
+
export function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
298
297
|
localMachineId?: string;
|
|
299
298
|
localDaemonId?: string;
|
|
300
299
|
coordinatorHostname?: string;
|
|
@@ -354,7 +353,7 @@ function normalizeInlineMeshGitStatus(
|
|
|
354
353
|
return sharedNormalizeGitStatus(status, readObjectRecord(node), options) as Record<string, unknown> | undefined;
|
|
355
354
|
}
|
|
356
355
|
|
|
357
|
-
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
356
|
+
export function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
358
357
|
return sharedPickBestTransitGitStatus(readObjectRecord(node), { lastCheckedAt: Date.now() }) as Record<string, unknown> | undefined;
|
|
359
358
|
}
|
|
360
359
|
|
|
@@ -367,7 +366,7 @@ function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDi
|
|
|
367
366
|
});
|
|
368
367
|
}
|
|
369
368
|
|
|
370
|
-
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
369
|
+
export function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
371
370
|
const source = readStringValue(connection.source);
|
|
372
371
|
const transport = readStringValue(connection.transport);
|
|
373
372
|
return {
|
|
@@ -382,7 +381,7 @@ function buildLivePeerGitConnection(connection: Record<string, unknown>, timesta
|
|
|
382
381
|
};
|
|
383
382
|
}
|
|
384
383
|
|
|
385
|
-
function recordInlineMeshDirectGitTruth(
|
|
384
|
+
export function recordInlineMeshDirectGitTruth(
|
|
386
385
|
node: any,
|
|
387
386
|
git: Record<string, unknown>,
|
|
388
387
|
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
@@ -459,7 +458,7 @@ function stampNodeReporterPlatform(node: any, platform: string | null, arch: str
|
|
|
459
458
|
* Fire-and-forget (same pattern as the worktreeBootstrap writer) — a persistence
|
|
460
459
|
* failure must never block the status response.
|
|
461
460
|
*/
|
|
462
|
-
function persistNodeReporterPlatform(
|
|
461
|
+
export function persistNodeReporterPlatform(
|
|
463
462
|
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config',
|
|
464
463
|
mesh: any,
|
|
465
464
|
nodeId: string | undefined,
|
|
@@ -733,7 +732,7 @@ function isInlineMeshAutoFastForwardEligible(git: Record<string, unknown> | null
|
|
|
733
732
|
return dirty !== true && countGitWorktreeChanges(git) === 0;
|
|
734
733
|
}
|
|
735
734
|
|
|
736
|
-
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
735
|
+
export function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
737
736
|
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
738
737
|
const branch = readStringValue(git.branch);
|
|
739
738
|
if (!branch) return 'degraded';
|
|
@@ -872,7 +871,7 @@ function buildInlineMeshBranchConvergence(args: {
|
|
|
872
871
|
};
|
|
873
872
|
}
|
|
874
873
|
|
|
875
|
-
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
874
|
+
export function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
876
875
|
const git = readObjectRecord(status.git);
|
|
877
876
|
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
878
877
|
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
@@ -887,7 +886,7 @@ function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<s
|
|
|
887
886
|
}
|
|
888
887
|
}
|
|
889
888
|
|
|
890
|
-
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
889
|
+
export function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
891
890
|
const followUps = nodes
|
|
892
891
|
.filter(node => {
|
|
893
892
|
if (readObjectRecord(node.branchConvergence).needsConvergence !== true) return false;
|
|
@@ -1073,7 +1072,7 @@ function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknow
|
|
|
1073
1072
|
}
|
|
1074
1073
|
}
|
|
1075
1074
|
|
|
1076
|
-
function finalizeMeshNodeStatus(args: {
|
|
1075
|
+
export function finalizeMeshNodeStatus(args: {
|
|
1077
1076
|
status: Record<string, unknown>;
|
|
1078
1077
|
node: any;
|
|
1079
1078
|
daemonId?: string;
|
|
@@ -1129,8 +1128,8 @@ function readMeshTimeoutEnvMs(name: string, defaultMs: number): number {
|
|
|
1129
1128
|
// round-trip to slow (often TURN-relayed) peers, so such a node was permanently
|
|
1130
1129
|
// marked unavailable and blocked the whole mesh graph. Default raised to 25s
|
|
1131
1130
|
// (still under the P2P REQUEST_TIMEOUT of 30s) and made env-overridable.
|
|
1132
|
-
const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
|
|
1133
|
-
const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
|
|
1131
|
+
export const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
|
|
1132
|
+
export const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
|
|
1134
1133
|
// How long a successful per-peer git_status probe stays fresh enough to be
|
|
1135
1134
|
// reused instead of issuing another blocking `refreshUpstream:true` fan-out.
|
|
1136
1135
|
// A slow (TURN-relayed) peer's probe can take 9-23s, and the dashboard's
|
|
@@ -1156,7 +1155,7 @@ const MESH_DIRECT_PROBE_REUSE_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_REUSE
|
|
|
1156
1155
|
* Lives on the router instance so the gate spans separate mesh_status calls,
|
|
1157
1156
|
* which is exactly where the refresh storm happens.
|
|
1158
1157
|
*/
|
|
1159
|
-
class MeshGitProbeCache {
|
|
1158
|
+
export class MeshGitProbeCache {
|
|
1160
1159
|
private inflight = new Map<string, Promise<Record<string, unknown> | null>>();
|
|
1161
1160
|
private recent = new Map<string, { at: number; value: Record<string, unknown> }>();
|
|
1162
1161
|
|
|
@@ -1263,7 +1262,7 @@ function isMeshConnectionDefinitivelyDown(
|
|
|
1263
1262
|
* attempt; a non-`connected` state short-circuits the retry loop (the very first
|
|
1264
1263
|
* attempt always runs so a missing connection getter still gets one try).
|
|
1265
1264
|
*/
|
|
1266
|
-
async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
1265
|
+
export async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
1267
1266
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
1268
1267
|
daemonId: string;
|
|
1269
1268
|
workspace: string;
|
|
@@ -1311,7 +1310,7 @@ async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
|
1311
1310
|
return null;
|
|
1312
1311
|
}
|
|
1313
1312
|
|
|
1314
|
-
async function hydrateInlineMeshDirectTruth(args: {
|
|
1313
|
+
export async function hydrateInlineMeshDirectTruth(args: {
|
|
1315
1314
|
mesh: any;
|
|
1316
1315
|
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
1317
1316
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
@@ -1480,7 +1479,7 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1480
1479
|
};
|
|
1481
1480
|
}
|
|
1482
1481
|
|
|
1483
|
-
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
1482
|
+
export function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
1484
1483
|
const meta = readObjectRecord(record?.meta);
|
|
1485
1484
|
const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
|
|
1486
1485
|
const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
|
|
@@ -1528,7 +1527,7 @@ function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, work
|
|
|
1528
1527
|
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
1529
1528
|
}
|
|
1530
1529
|
|
|
1531
|
-
function readLiveMeshNodeWorkspace(args: {
|
|
1530
|
+
export function readLiveMeshNodeWorkspace(args: {
|
|
1532
1531
|
meshId: string;
|
|
1533
1532
|
nodeId: string;
|
|
1534
1533
|
liveSessionRecords: any[];
|
|
@@ -1555,7 +1554,7 @@ function readLiveMeshNodeWorkspace(args: {
|
|
|
1555
1554
|
return '';
|
|
1556
1555
|
}
|
|
1557
1556
|
|
|
1558
|
-
function collectLiveMeshSessionRecords(args: {
|
|
1557
|
+
export function collectLiveMeshSessionRecords(args: {
|
|
1559
1558
|
meshId: string;
|
|
1560
1559
|
node: any;
|
|
1561
1560
|
nodeId: string;
|
|
@@ -1586,7 +1585,7 @@ function collectLiveMeshSessionRecords(args: {
|
|
|
1586
1585
|
return matches;
|
|
1587
1586
|
}
|
|
1588
1587
|
|
|
1589
|
-
function buildHistoricalMeshSessions(args: {
|
|
1588
|
+
export function buildHistoricalMeshSessions(args: {
|
|
1590
1589
|
meshId: string;
|
|
1591
1590
|
nodes: any[];
|
|
1592
1591
|
liveSessionRecords: any[];
|
|
@@ -1632,7 +1631,7 @@ function buildHistoricalMeshSessions(args: {
|
|
|
1632
1631
|
};
|
|
1633
1632
|
}
|
|
1634
1633
|
|
|
1635
|
-
function applyCachedInlineMeshNodeStatus(
|
|
1634
|
+
export function applyCachedInlineMeshNodeStatus(
|
|
1636
1635
|
status: Record<string, unknown>,
|
|
1637
1636
|
node: any,
|
|
1638
1637
|
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
@@ -1666,7 +1665,7 @@ function applyCachedInlineMeshNodeStatus(
|
|
|
1666
1665
|
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
1667
1666
|
}
|
|
1668
1667
|
|
|
1669
|
-
async function resolveProviderTypeFromPriority(args: {
|
|
1668
|
+
export async function resolveProviderTypeFromPriority(args: {
|
|
1670
1669
|
nodeId: string;
|
|
1671
1670
|
providerPriority: string[];
|
|
1672
1671
|
providerLoader: ProviderLoader;
|
|
@@ -1696,7 +1695,7 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
1696
1695
|
|
|
1697
1696
|
return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
|
|
1698
1697
|
}
|
|
1699
|
-
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
1698
|
+
export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
1700
1699
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
1701
1700
|
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
1702
1701
|
|
|
@@ -1846,7 +1845,7 @@ type MeshRefineSubmoduleReachabilitySummary = {
|
|
|
1846
1845
|
|
|
1847
1846
|
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1848
1847
|
|
|
1849
|
-
type MeshRefineJobHandle = {
|
|
1848
|
+
export type MeshRefineJobHandle = {
|
|
1850
1849
|
success: true;
|
|
1851
1850
|
async: true;
|
|
1852
1851
|
status: MeshRefineAsyncJobStatus;
|
|
@@ -1979,6 +1978,51 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
|
|
|
1979
1978
|
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.`;
|
|
1980
1979
|
}
|
|
1981
1980
|
|
|
1981
|
+
/**
|
|
1982
|
+
* Async git exec helper used across the synchronous-refine stage pipeline. Bound
|
|
1983
|
+
* once in the orchestrator and threaded through RefineContext so every stage runs
|
|
1984
|
+
* git the same way (execFile + promisify, utf8). Returns the child's stdout/stderr.
|
|
1985
|
+
*/
|
|
1986
|
+
type RefineExecFileAsync = (file: string, args: string[], options: { cwd: string; encoding: 'utf8' }) => Promise<{ stdout: string; stderr: string }>;
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* Accumulated state shared by the synchronous-refine stages. The orchestrator
|
|
1990
|
+
* (executeMeshRefineNodeSynchronously) seeds this in the resolve_refs stage and
|
|
1991
|
+
* each later stage reads / extends it. `branchHead` and `patchEquivalence` are the
|
|
1992
|
+
* only fields a stage mutates after creation (auto-rebase updates both), so they
|
|
1993
|
+
* are carried on the mutable context rather than re-threaded through return types.
|
|
1994
|
+
*/
|
|
1995
|
+
interface RefineContext {
|
|
1996
|
+
meshId: string;
|
|
1997
|
+
nodeId: string;
|
|
1998
|
+
args: any;
|
|
1999
|
+
refineStages: Array<Record<string, unknown>>;
|
|
2000
|
+
execFileAsync: RefineExecFileAsync;
|
|
2001
|
+
mesh: any;
|
|
2002
|
+
node: any;
|
|
2003
|
+
sourceNode: any;
|
|
2004
|
+
repoRoot: string;
|
|
2005
|
+
branch: string;
|
|
2006
|
+
baseBranch: string;
|
|
2007
|
+
baseHead: string;
|
|
2008
|
+
branchHead: string;
|
|
2009
|
+
validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
|
|
2010
|
+
patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
|
|
2011
|
+
submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Stage outcome for the synchronous-refine pipeline. A stage either produces a
|
|
2016
|
+
* terminal CommandRouterResult (an early-exit gate failure, or a successful
|
|
2017
|
+
* already-merged short-circuit), in which case the orchestrator returns it
|
|
2018
|
+
* immediately, or it returns `continue` with the (possibly extended) context for
|
|
2019
|
+
* the next stage. This makes the orchestrator a flat sequence of stage calls
|
|
2020
|
+
* while preserving the original body's exact early-return control flow.
|
|
2021
|
+
*/
|
|
2022
|
+
type RefineStageOutcome =
|
|
2023
|
+
| { kind: 'terminal'; result: CommandRouterResult }
|
|
2024
|
+
| { kind: 'continue'; ctx: RefineContext };
|
|
2025
|
+
|
|
1982
2026
|
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1983
2027
|
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1984
2028
|
process.stderr.write(
|
|
@@ -3045,7 +3089,7 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
3045
3089
|
}
|
|
3046
3090
|
}
|
|
3047
3091
|
|
|
3048
|
-
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
3092
|
+
export function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
3049
3093
|
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
3050
3094
|
const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
|
|
3051
3095
|
displayCommand: command.displayCommand,
|
|
@@ -3282,18 +3326,18 @@ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, op
|
|
|
3282
3326
|
return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
|
|
3283
3327
|
}
|
|
3284
3328
|
|
|
3285
|
-
function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
3329
|
+
export function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
3286
3330
|
return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
|
|
3287
3331
|
}
|
|
3288
3332
|
|
|
3289
|
-
function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
3333
|
+
export function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
3290
3334
|
if (!text.trim()) return {};
|
|
3291
3335
|
if (format === 'claude_mcp_json') return JSON.parse(text);
|
|
3292
3336
|
const parsed = loadYamlModule().load(text);
|
|
3293
3337
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
3294
3338
|
}
|
|
3295
3339
|
|
|
3296
|
-
function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
3340
|
+
export function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
3297
3341
|
if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
|
|
3298
3342
|
return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
|
|
3299
3343
|
}
|
|
@@ -3303,7 +3347,7 @@ function resolveHermesUserHome(): string {
|
|
|
3303
3347
|
return explicitHome || pathJoin(homedir(), '.hermes');
|
|
3304
3348
|
}
|
|
3305
3349
|
|
|
3306
|
-
function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
|
|
3350
|
+
export function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
|
|
3307
3351
|
const sourceHome = resolveHermesUserHome();
|
|
3308
3352
|
const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
|
|
3309
3353
|
if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
|
|
@@ -3314,7 +3358,7 @@ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Re
|
|
|
3314
3358
|
return { config: baseConfig, sourceHome, sourceConfigPath };
|
|
3315
3359
|
}
|
|
3316
3360
|
|
|
3317
|
-
function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
|
|
3361
|
+
export function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
|
|
3318
3362
|
const {
|
|
3319
3363
|
model: _model,
|
|
3320
3364
|
provider: _provider,
|
|
@@ -3343,7 +3387,7 @@ function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string,
|
|
|
3343
3387
|
return sanitized;
|
|
3344
3388
|
}
|
|
3345
3389
|
|
|
3346
|
-
function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
|
|
3390
|
+
export function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
|
|
3347
3391
|
if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
|
|
3348
3392
|
for (const fileName of ['.env', 'auth.json']) {
|
|
3349
3393
|
const sourcePath = pathJoin(sourceHome, fileName);
|
|
@@ -3470,7 +3514,7 @@ function normalizeCommandArgsWithInteractionId(args: any): Record<string, unknow
|
|
|
3470
3514
|
* basename to be a literal `*.json`, and re-joins under the verified parent so
|
|
3471
3515
|
* the returned path can't point outside the tree. Used by get/write_spec_source.
|
|
3472
3516
|
*/
|
|
3473
|
-
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
3517
|
+
export function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
3474
3518
|
const raw = hostAddress.trim();
|
|
3475
3519
|
if (!raw) throw new Error('hostAddress required');
|
|
3476
3520
|
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
@@ -3480,7 +3524,7 @@ function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
|
3480
3524
|
return url.toString();
|
|
3481
3525
|
}
|
|
3482
3526
|
|
|
3483
|
-
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
3527
|
+
export function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
3484
3528
|
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
3485
3529
|
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
3486
3530
|
? args.memberNode as Record<string, any>
|
|
@@ -3862,6 +3906,62 @@ export class DaemonCommandRouter {
|
|
|
3862
3906
|
this.deps.onMeshStateChange?.(meshId);
|
|
3863
3907
|
}
|
|
3864
3908
|
|
|
3909
|
+
/**
|
|
3910
|
+
* Build the MedFamilyContext handed to RF-ROUTER MED family handlers. Binds the
|
|
3911
|
+
* router-private collaborators those handlers need (mesh resolution, owner
|
|
3912
|
+
* gating, inline-cache mutation, worktree / session cleanup, refine job
|
|
3913
|
+
* starters, IDE stop/launch) plus the inline-mesh and git-probe caches. The
|
|
3914
|
+
* `launchIde` field closes over the freshly-built context so restart_session /
|
|
3915
|
+
* restart_ide invoke the IDE launch directly instead of recursing through
|
|
3916
|
+
* executeDaemonCommand('launch_ide').
|
|
3917
|
+
*/
|
|
3918
|
+
private buildMedFamilyContext(): MedFamilyContext {
|
|
3919
|
+
const ctx: MedFamilyContext = {
|
|
3920
|
+
deps: this.deps,
|
|
3921
|
+
getMeshForCommand: this.getMeshForCommand.bind(this),
|
|
3922
|
+
getCachedInlineMesh: this.getCachedInlineMesh.bind(this),
|
|
3923
|
+
requireMeshHostMutationOwner: this.requireMeshHostMutationOwner.bind(this),
|
|
3924
|
+
invalidateAggregateMeshStatus: this.invalidateAggregateMeshStatus.bind(this),
|
|
3925
|
+
updateInlineMeshNode: this.updateInlineMeshNode.bind(this),
|
|
3926
|
+
removeInlineMeshNode: this.removeInlineMeshNode.bind(this),
|
|
3927
|
+
normalizeMeshSessionCleanupMode: this.normalizeMeshSessionCleanupMode.bind(this),
|
|
3928
|
+
cleanupMeshSessions: this.cleanupMeshSessions.bind(this),
|
|
3929
|
+
cleanupLocalWorktreeNode: this.cleanupLocalWorktreeNode.bind(this),
|
|
3930
|
+
startMeshRefineJob: this.startMeshRefineJob.bind(this),
|
|
3931
|
+
batchRefineMeshNodes: this.batchRefineMeshNodes.bind(this),
|
|
3932
|
+
startMeshRefineBatchJob: this.startMeshRefineBatchJob.bind(this),
|
|
3933
|
+
stopIde: this.stopIde.bind(this),
|
|
3934
|
+
launchIde: (args: any) => launchIde(ctx, args),
|
|
3935
|
+
inlineMeshCache: this.inlineMeshCache,
|
|
3936
|
+
meshGitProbeCache: this.meshGitProbeCache,
|
|
3937
|
+
};
|
|
3938
|
+
return ctx;
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
/**
|
|
3942
|
+
* Build the HighFamilyContext handed to RF-ROUTER HIGH family handlers. Binds
|
|
3943
|
+
* the router-private collaborators those handlers need (mesh resolution, the
|
|
3944
|
+
* aggregate-status memory cache + its bound read/write helpers, the
|
|
3945
|
+
* running-refine-job table, inline-mesh + git-probe caches, and the router's
|
|
3946
|
+
* own `execute` for the get_mesh_review_inbox mesh_status re-entry). HIGH
|
|
3947
|
+
* handlers reach more router-owned state than MED, but the binding shape is
|
|
3948
|
+
* the same: bound methods + direct field references, none reachable from
|
|
3949
|
+
* `deps`.
|
|
3950
|
+
*/
|
|
3951
|
+
private buildHighFamilyContext(): HighFamilyContext {
|
|
3952
|
+
return {
|
|
3953
|
+
deps: this.deps,
|
|
3954
|
+
getMeshForCommand: this.getMeshForCommand.bind(this),
|
|
3955
|
+
getCachedAggregateMeshStatus: this.getCachedAggregateMeshStatus.bind(this),
|
|
3956
|
+
rememberAggregateMeshStatus: this.rememberAggregateMeshStatus.bind(this),
|
|
3957
|
+
execute: this.execute.bind(this),
|
|
3958
|
+
aggregateMeshStatusCache: this.aggregateMeshStatusCache,
|
|
3959
|
+
runningRefineJobs: this.runningRefineJobs,
|
|
3960
|
+
inlineMeshCache: this.inlineMeshCache,
|
|
3961
|
+
meshGitProbeCache: this.meshGitProbeCache,
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3865
3965
|
|
|
3866
3966
|
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
3867
3967
|
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
@@ -4766,33 +4866,77 @@ export class DaemonCommandRouter {
|
|
|
4766
4866
|
}
|
|
4767
4867
|
}
|
|
4768
4868
|
|
|
4869
|
+
/**
|
|
4870
|
+
* Synchronous refinery for a single worktree node — the gate pipeline that
|
|
4871
|
+
* validates, preflights (patch-equivalence / submodule-reachability /
|
|
4872
|
+
* no-op), merges, aligns submodules, cleans up the worktree node and
|
|
4873
|
+
* (optionally) pushes. The body is a flat sequence of stage methods; each
|
|
4874
|
+
* stage either returns a terminal CommandRouterResult (gate failure or a
|
|
4875
|
+
* successful already-merged short-circuit) or `continue` with the extended
|
|
4876
|
+
* context. Behavior — stage order, every early-exit, and every result shape —
|
|
4877
|
+
* is identical to the previous single inlined body.
|
|
4878
|
+
*/
|
|
4769
4879
|
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
4770
4880
|
const refineStages: Array<Record<string, unknown>> = [];
|
|
4771
4881
|
try {
|
|
4882
|
+
const resolved = await this.refineResolveRefsStage(meshId, nodeId, args, refineStages);
|
|
4883
|
+
if (resolved.kind === 'terminal') return resolved.result;
|
|
4884
|
+
const ctx = resolved.ctx;
|
|
4885
|
+
|
|
4886
|
+
const validation = await this.refineValidationStage(ctx);
|
|
4887
|
+
if (validation.kind === 'terminal') return validation.result;
|
|
4888
|
+
|
|
4889
|
+
const patchEquivalence = await this.refinePatchEquivalenceStage(ctx);
|
|
4890
|
+
if (patchEquivalence.kind === 'terminal') return patchEquivalence.result;
|
|
4891
|
+
|
|
4892
|
+
const submoduleReachability = await this.refineSubmoduleReachabilityStage(ctx);
|
|
4893
|
+
if (submoduleReachability.kind === 'terminal') return submoduleReachability.result;
|
|
4894
|
+
|
|
4895
|
+
const effectiveDiff = await this.refineEffectiveDiffStage(ctx);
|
|
4896
|
+
if (effectiveDiff.kind === 'terminal') return effectiveDiff.result;
|
|
4897
|
+
|
|
4898
|
+
const merge = await this.refineMergeAndFinalizeStage(ctx);
|
|
4899
|
+
return (merge as { kind: 'terminal'; result: CommandRouterResult }).result;
|
|
4900
|
+
} catch (e: any) {
|
|
4901
|
+
return { success: false, error: e.message, refineStages };
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
/**
|
|
4906
|
+
* resolve_refs stage: resolve the mesh / worktree node / source node /
|
|
4907
|
+
* repoRoot, then the worktree branch, base branch, fetched base head and
|
|
4908
|
+
* branch head. Seeds the RefineContext consumed by every later stage.
|
|
4909
|
+
*/
|
|
4910
|
+
private async refineResolveRefsStage(
|
|
4911
|
+
meshId: string,
|
|
4912
|
+
nodeId: string,
|
|
4913
|
+
args: any,
|
|
4914
|
+
refineStages: Array<Record<string, unknown>>,
|
|
4915
|
+
): Promise<RefineStageOutcome> {
|
|
4772
4916
|
// preferInline: same as startMeshRefineJob — inline-cache-only clone nodes must resolve.
|
|
4773
4917
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4774
4918
|
const mesh = meshRecord?.mesh;
|
|
4775
4919
|
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
4776
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
4920
|
+
if (!node) return { kind: 'terminal', result: { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages } };
|
|
4777
4921
|
|
|
4778
4922
|
if (!node.isLocalWorktree || !node.workspace) {
|
|
4779
|
-
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
4923
|
+
return { kind: 'terminal', result: { success: false, error: `Refinery requires a local worktree node`, refineStages } };
|
|
4780
4924
|
}
|
|
4781
4925
|
|
|
4782
4926
|
const sourceNode = node.clonedFromNodeId
|
|
4783
4927
|
? mesh?.nodes.find((n: any) => meshNodeIdMatches(n, node.clonedFromNodeId))
|
|
4784
4928
|
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
4785
4929
|
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
4786
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
4930
|
+
if (!repoRoot) return { kind: 'terminal', result: { success: false, error: 'Source node repoRoot not found', refineStages } };
|
|
4787
4931
|
|
|
4788
4932
|
const { execFile } = await import('node:child_process');
|
|
4789
4933
|
const { promisify } = await import('node:util');
|
|
4790
|
-
const execFileAsync = promisify(execFile);
|
|
4934
|
+
const execFileAsync = promisify(execFile) as unknown as RefineExecFileAsync;
|
|
4791
4935
|
|
|
4792
4936
|
const resolveStarted = Date.now();
|
|
4793
4937
|
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
4794
4938
|
const branch = branchStdout.trim();
|
|
4795
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
4939
|
+
if (!branch) return { kind: 'terminal', result: { success: false, error: 'Could not determine branch of the worktree node', refineStages } };
|
|
4796
4940
|
|
|
4797
4941
|
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
4798
4942
|
const baseBranch = baseBranchStdout.trim();
|
|
@@ -4819,9 +4963,39 @@ export class DaemonCommandRouter {
|
|
|
4819
4963
|
|
|
4820
4964
|
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
4821
4965
|
const baseHead = baseHeadRaw;
|
|
4822
|
-
|
|
4966
|
+
const branchHead = branchHeadStdout.trim();
|
|
4823
4967
|
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead, ...(fetchWarning ? { fetchWarning } : {}) });
|
|
4824
4968
|
|
|
4969
|
+
return {
|
|
4970
|
+
kind: 'continue',
|
|
4971
|
+
ctx: {
|
|
4972
|
+
meshId,
|
|
4973
|
+
nodeId,
|
|
4974
|
+
args,
|
|
4975
|
+
refineStages,
|
|
4976
|
+
execFileAsync,
|
|
4977
|
+
mesh,
|
|
4978
|
+
node,
|
|
4979
|
+
sourceNode,
|
|
4980
|
+
repoRoot,
|
|
4981
|
+
branch,
|
|
4982
|
+
baseBranch,
|
|
4983
|
+
baseHead,
|
|
4984
|
+
branchHead,
|
|
4985
|
+
validationSummary: undefined as any,
|
|
4986
|
+
patchEquivalence: undefined as any,
|
|
4987
|
+
submoduleReachability: undefined as any,
|
|
4988
|
+
},
|
|
4989
|
+
};
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
/**
|
|
4993
|
+
* validation stage: run the refinery validation gate (typecheck / test /
|
|
4994
|
+
* lint / build per node config) and block on failure or when no allowlisted
|
|
4995
|
+
* command was available. On pass, stores the summary on the context.
|
|
4996
|
+
*/
|
|
4997
|
+
private async refineValidationStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
4998
|
+
const { mesh, node, branch, baseBranch, refineStages } = ctx;
|
|
4825
4999
|
const validationStarted = Date.now();
|
|
4826
5000
|
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
|
|
4827
5001
|
// M2-2: consume the node's persisted bootstrap state; persist re-runs.
|
|
@@ -4833,6 +5007,7 @@ export class DaemonCommandRouter {
|
|
|
4833
5007
|
.catch(() => { /* persistence is best-effort */ });
|
|
4834
5008
|
},
|
|
4835
5009
|
});
|
|
5010
|
+
ctx.validationSummary = validationSummary;
|
|
4836
5011
|
recordMeshRefineStage(
|
|
4837
5012
|
refineStages,
|
|
4838
5013
|
'validation',
|
|
@@ -4868,7 +5043,7 @@ export class DaemonCommandRouter {
|
|
|
4868
5043
|
tail ? `Output (tail):\n${tail}` : '',
|
|
4869
5044
|
].filter(Boolean).join('\n');
|
|
4870
5045
|
};
|
|
4871
|
-
return {
|
|
5046
|
+
return { kind: 'terminal', result: {
|
|
4872
5047
|
success: false,
|
|
4873
5048
|
code: validationSummary.failureCode || 'validation_failed',
|
|
4874
5049
|
convergenceStatus: 'blocked_review',
|
|
@@ -4885,10 +5060,10 @@ export class DaemonCommandRouter {
|
|
|
4885
5060
|
validation: 'failed',
|
|
4886
5061
|
status: 'blocked_review',
|
|
4887
5062
|
},
|
|
4888
|
-
};
|
|
5063
|
+
} };
|
|
4889
5064
|
}
|
|
4890
5065
|
if (validationSummary.status === 'skipped') {
|
|
4891
|
-
return {
|
|
5066
|
+
return { kind: 'terminal', result: {
|
|
4892
5067
|
success: false,
|
|
4893
5068
|
code: 'validation_unavailable',
|
|
4894
5069
|
convergenceStatus: 'blocked_review',
|
|
@@ -4905,9 +5080,22 @@ export class DaemonCommandRouter {
|
|
|
4905
5080
|
validation: 'unavailable',
|
|
4906
5081
|
status: 'blocked_review',
|
|
4907
5082
|
},
|
|
4908
|
-
};
|
|
5083
|
+
} };
|
|
4909
5084
|
}
|
|
4910
5085
|
|
|
5086
|
+
return { kind: 'continue', ctx };
|
|
5087
|
+
}
|
|
5088
|
+
|
|
5089
|
+
/**
|
|
5090
|
+
* patch_equivalence stage: preflight that the worktree branch's cumulative
|
|
5091
|
+
* patch is equivalent to base+branch. On a "behind base" branch, auto-rebase
|
|
5092
|
+
* once and re-check; on an empty merge-tree with real branch changes, treat as
|
|
5093
|
+
* already-merged-via-another-path and short-circuit to cleanup. Mutates the
|
|
5094
|
+
* context's branchHead (after rebase) and patchEquivalence (rebased gate).
|
|
5095
|
+
*/
|
|
5096
|
+
private async refinePatchEquivalenceStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5097
|
+
const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, validationSummary, refineStages, execFileAsync } = ctx;
|
|
5098
|
+
let branchHead = ctx.branchHead;
|
|
4911
5099
|
const patchEquivalenceStarted = Date.now();
|
|
4912
5100
|
let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
4913
5101
|
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
@@ -4950,7 +5138,7 @@ export class DaemonCommandRouter {
|
|
|
4950
5138
|
patchEquivalence = rebasedPatchEquivalence;
|
|
4951
5139
|
didAutoRebase = true;
|
|
4952
5140
|
} else {
|
|
4953
|
-
return {
|
|
5141
|
+
return { kind: 'terminal', result: {
|
|
4954
5142
|
success: false,
|
|
4955
5143
|
code: 'needs_rebase',
|
|
4956
5144
|
convergenceStatus: 'blocked_review',
|
|
@@ -4969,14 +5157,14 @@ export class DaemonCommandRouter {
|
|
|
4969
5157
|
patchEquivalence: 'failed',
|
|
4970
5158
|
status: 'blocked_review',
|
|
4971
5159
|
},
|
|
4972
|
-
};
|
|
5160
|
+
} };
|
|
4973
5161
|
}
|
|
4974
5162
|
} catch (rebaseErr: any) {
|
|
4975
5163
|
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
4976
5164
|
recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
|
|
4977
5165
|
error: rebaseErr?.message || String(rebaseErr),
|
|
4978
5166
|
});
|
|
4979
|
-
return {
|
|
5167
|
+
return { kind: 'terminal', result: {
|
|
4980
5168
|
success: false,
|
|
4981
5169
|
code: 'needs_rebase_with_conflicts',
|
|
4982
5170
|
convergenceStatus: 'blocked_review',
|
|
@@ -4995,7 +5183,7 @@ export class DaemonCommandRouter {
|
|
|
4995
5183
|
patchEquivalence: 'failed',
|
|
4996
5184
|
status: 'blocked_review',
|
|
4997
5185
|
},
|
|
4998
|
-
};
|
|
5186
|
+
} };
|
|
4999
5187
|
}
|
|
5000
5188
|
}
|
|
5001
5189
|
|
|
@@ -5011,7 +5199,7 @@ export class DaemonCommandRouter {
|
|
|
5011
5199
|
// is a degenerate worktree case, not an "already merged" scenario.
|
|
5012
5200
|
const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
|
|
5013
5201
|
if (!didAutoRebase && !alreadyMergedViaOtherPath) {
|
|
5014
|
-
return {
|
|
5202
|
+
return { kind: 'terminal', result: {
|
|
5015
5203
|
success: false,
|
|
5016
5204
|
code: 'patch_equivalence_failed',
|
|
5017
5205
|
convergenceStatus: 'blocked_review',
|
|
@@ -5030,7 +5218,7 @@ export class DaemonCommandRouter {
|
|
|
5030
5218
|
patchEquivalence: 'failed',
|
|
5031
5219
|
status: 'blocked_review',
|
|
5032
5220
|
},
|
|
5033
|
-
};
|
|
5221
|
+
} };
|
|
5034
5222
|
}
|
|
5035
5223
|
|
|
5036
5224
|
if (!didAutoRebase && alreadyMergedViaOtherPath) {
|
|
@@ -5059,7 +5247,7 @@ export class DaemonCommandRouter {
|
|
|
5059
5247
|
payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
5060
5248
|
});
|
|
5061
5249
|
} catch { /* ledger append is best-effort */ }
|
|
5062
|
-
return {
|
|
5250
|
+
return { kind: 'terminal', result: {
|
|
5063
5251
|
success: removeResult?.success !== false,
|
|
5064
5252
|
code: 'already_merged',
|
|
5065
5253
|
merged: false,
|
|
@@ -5081,10 +5269,23 @@ export class DaemonCommandRouter {
|
|
|
5081
5269
|
patchEquivalence: 'already_merged',
|
|
5082
5270
|
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
|
|
5083
5271
|
},
|
|
5084
|
-
};
|
|
5272
|
+
} };
|
|
5085
5273
|
}
|
|
5086
5274
|
}
|
|
5087
5275
|
|
|
5276
|
+
ctx.branchHead = branchHead;
|
|
5277
|
+
ctx.patchEquivalence = patchEquivalence;
|
|
5278
|
+
return { kind: 'continue', ctx };
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
/**
|
|
5282
|
+
* submodule_reachability stage: verify every submodule gitlink commit that
|
|
5283
|
+
* would land via the merge is reachable from its configured remote main
|
|
5284
|
+
* branch (optionally auto-publishing when policy allows). Blocks the merge
|
|
5285
|
+
* when any commit is unreachable. Stores the result on the context.
|
|
5286
|
+
*/
|
|
5287
|
+
private async refineSubmoduleReachabilityStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5288
|
+
const { mesh, node, repoRoot, branch, baseBranch, branchHead, validationSummary, patchEquivalence, refineStages } = ctx;
|
|
5088
5289
|
const submoduleReachabilityStarted = Date.now();
|
|
5089
5290
|
const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
|
|
5090
5291
|
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
@@ -5141,7 +5342,7 @@ export class DaemonCommandRouter {
|
|
|
5141
5342
|
});
|
|
5142
5343
|
if (submoduleReachability.status === 'failed') {
|
|
5143
5344
|
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
5144
|
-
return {
|
|
5345
|
+
return { kind: 'terminal', result: {
|
|
5145
5346
|
success: false,
|
|
5146
5347
|
code: 'submodule_reachability_failed',
|
|
5147
5348
|
convergenceStatus: 'blocked_review',
|
|
@@ -5189,9 +5390,21 @@ export class DaemonCommandRouter {
|
|
|
5189
5390
|
reason: 'submodule_publish_required',
|
|
5190
5391
|
nextStep,
|
|
5191
5392
|
},
|
|
5192
|
-
};
|
|
5393
|
+
} };
|
|
5193
5394
|
}
|
|
5194
5395
|
|
|
5396
|
+
ctx.submoduleReachability = submoduleReachability;
|
|
5397
|
+
return { kind: 'continue', ctx };
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
/**
|
|
5401
|
+
* effective_diff stage (no-op guard): block a silent no-op merge where the
|
|
5402
|
+
* branch produces no effective root-tree diff against base — typically a
|
|
5403
|
+
* submodule that has commits but whose root-level gitlink (pointer) bump was
|
|
5404
|
+
* never committed, so the merge would land nothing real on main.
|
|
5405
|
+
*/
|
|
5406
|
+
private async refineEffectiveDiffStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5407
|
+
const { repoRoot, baseHead, branchHead, branch, baseBranch, validationSummary, patchEquivalence, refineStages } = ctx;
|
|
5195
5408
|
// No-op guard: block a silent no-op merge where the root tree is identical to base.
|
|
5196
5409
|
// This catches the trap where a submodule has commits but the root branch never
|
|
5197
5410
|
// committed the gitlink (oss-pointer) bump — merging would report success while the
|
|
@@ -5213,7 +5426,7 @@ export class DaemonCommandRouter {
|
|
|
5213
5426
|
hintLines.length ? `Submodules with uncommitted pointer bumps:\n${hintLines.join('\n')}` : '',
|
|
5214
5427
|
`Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`,
|
|
5215
5428
|
].filter(Boolean).join('\n');
|
|
5216
|
-
return {
|
|
5429
|
+
return { kind: 'terminal', result: {
|
|
5217
5430
|
success: false,
|
|
5218
5431
|
code: 'no_effective_diff',
|
|
5219
5432
|
convergenceStatus: 'blocked_review',
|
|
@@ -5236,9 +5449,20 @@ export class DaemonCommandRouter {
|
|
|
5236
5449
|
reason: 'no_effective_diff',
|
|
5237
5450
|
...(effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}),
|
|
5238
5451
|
},
|
|
5239
|
-
};
|
|
5452
|
+
} };
|
|
5240
5453
|
}
|
|
5241
5454
|
|
|
5455
|
+
return { kind: 'continue', ctx };
|
|
5456
|
+
}
|
|
5457
|
+
|
|
5458
|
+
/**
|
|
5459
|
+
* merge + finalize stage: perform the --no-ff merge, align submodule
|
|
5460
|
+
* checkouts after merge, clean up (remove) the worktree node per policy,
|
|
5461
|
+
* append the refinery ledger entry, and (unless approval is required) push the
|
|
5462
|
+
* base branch. Always terminal — produces the final CommandRouterResult.
|
|
5463
|
+
*/
|
|
5464
|
+
private async refineMergeAndFinalizeStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5465
|
+
const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync } = ctx;
|
|
5242
5466
|
let mergeResult: Record<string, unknown> | undefined;
|
|
5243
5467
|
const mergeStarted = Date.now();
|
|
5244
5468
|
try {
|
|
@@ -5255,7 +5479,7 @@ export class DaemonCommandRouter {
|
|
|
5255
5479
|
stdout: truncateValidationOutput(e?.stdout),
|
|
5256
5480
|
stderr: truncateValidationOutput(e?.stderr),
|
|
5257
5481
|
});
|
|
5258
|
-
return {
|
|
5482
|
+
return { kind: 'terminal', result: {
|
|
5259
5483
|
success: false,
|
|
5260
5484
|
error: `Merge failed (conflicts?): ${e.message}`,
|
|
5261
5485
|
validationSummary,
|
|
@@ -5270,7 +5494,7 @@ export class DaemonCommandRouter {
|
|
|
5270
5494
|
patchEquivalence: 'passed',
|
|
5271
5495
|
status: 'not_mergeable',
|
|
5272
5496
|
},
|
|
5273
|
-
};
|
|
5497
|
+
} };
|
|
5274
5498
|
}
|
|
5275
5499
|
|
|
5276
5500
|
const submoduleAlignmentStarted = Date.now();
|
|
@@ -5290,7 +5514,7 @@ export class DaemonCommandRouter {
|
|
|
5290
5514
|
});
|
|
5291
5515
|
}
|
|
5292
5516
|
if (submoduleAlignment.status === 'failed') {
|
|
5293
|
-
return {
|
|
5517
|
+
return { kind: 'terminal', result: {
|
|
5294
5518
|
success: false,
|
|
5295
5519
|
code: 'post_merge_submodule_alignment_failed',
|
|
5296
5520
|
error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
|
|
@@ -5316,7 +5540,7 @@ export class DaemonCommandRouter {
|
|
|
5316
5540
|
status: 'post_merge_alignment_failed',
|
|
5317
5541
|
nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
|
|
5318
5542
|
},
|
|
5319
|
-
};
|
|
5543
|
+
} };
|
|
5320
5544
|
}
|
|
5321
5545
|
|
|
5322
5546
|
const cleanupStarted = Date.now();
|
|
@@ -5396,7 +5620,7 @@ export class DaemonCommandRouter {
|
|
|
5396
5620
|
};
|
|
5397
5621
|
|
|
5398
5622
|
if (removeResult?.success === false) {
|
|
5399
|
-
return {
|
|
5623
|
+
return { kind: 'terminal', result: {
|
|
5400
5624
|
success: false,
|
|
5401
5625
|
code: 'cleanup_failed',
|
|
5402
5626
|
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
@@ -5412,7 +5636,7 @@ export class DaemonCommandRouter {
|
|
|
5412
5636
|
refineStages,
|
|
5413
5637
|
...(ledgerError ? { ledgerError } : {}),
|
|
5414
5638
|
finalBranchConvergenceState,
|
|
5415
|
-
};
|
|
5639
|
+
} };
|
|
5416
5640
|
}
|
|
5417
5641
|
|
|
5418
5642
|
// Push logic: after a successful merge, either auto-push or surface push info
|
|
@@ -5439,7 +5663,7 @@ export class DaemonCommandRouter {
|
|
|
5439
5663
|
}
|
|
5440
5664
|
}
|
|
5441
5665
|
|
|
5442
|
-
return {
|
|
5666
|
+
return { kind: 'terminal', result: {
|
|
5443
5667
|
success: true,
|
|
5444
5668
|
merged: true,
|
|
5445
5669
|
branch,
|
|
@@ -5461,10 +5685,7 @@ export class DaemonCommandRouter {
|
|
|
5461
5685
|
pushCommand: `git push origin ${baseBranch}`,
|
|
5462
5686
|
pushNote: 'requireApprovalForPush is enabled — run the push command or obtain user approval before pushing.',
|
|
5463
5687
|
}),
|
|
5464
|
-
};
|
|
5465
|
-
} catch (e: any) {
|
|
5466
|
-
return { success: false, error: e.message, refineStages };
|
|
5467
|
-
}
|
|
5688
|
+
} };
|
|
5468
5689
|
}
|
|
5469
5690
|
|
|
5470
5691
|
/**
|
|
@@ -6183,2772 +6404,32 @@ export class DaemonCommandRouter {
|
|
|
6183
6404
|
}, args);
|
|
6184
6405
|
}
|
|
6185
6406
|
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
? args.coordinatorDaemonId.trim()
|
|
6199
|
-
: undefined;
|
|
6200
|
-
const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
|
|
6201
|
-
return { success: true, events };
|
|
6202
|
-
}
|
|
6203
|
-
|
|
6204
|
-
case 'interactive_prompt_response': {
|
|
6205
|
-
const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
|
|
6206
|
-
? args.targetSessionId.trim()
|
|
6207
|
-
: typeof args?.sessionId === 'string' && args.sessionId.trim()
|
|
6208
|
-
? args.sessionId.trim()
|
|
6209
|
-
: '';
|
|
6210
|
-
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
6211
|
-
const response = normalizeInteractivePromptResponse(args?.response ?? args);
|
|
6212
|
-
const instance = this.deps.instanceManager.getInstance(sessionId);
|
|
6213
|
-
if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
|
|
6214
|
-
this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
|
|
6215
|
-
return { success: true };
|
|
6216
|
-
}
|
|
6217
|
-
|
|
6218
|
-
case 'launch_cli': {
|
|
6219
|
-
// The coordinator routing anchor (meshCoordinatorDaemonId) is stamped
|
|
6220
|
-
// upstream by mesh_launch_session, which resolves
|
|
6221
|
-
// coordinatorNode.daemonId || ctx.localDaemonId || ctx.localMachineId and
|
|
6222
|
-
// fail-closes for a remote node when none resolve. We deliberately do NOT
|
|
6223
|
-
// self-stamp this daemon's own id when the field is missing: for a
|
|
6224
|
-
// P2P-relayed remote worker launch, stamping the worker's own id would make
|
|
6225
|
-
// the self-forward gate (mesh-events-coordinator: daemonIdsEquivalent) treat the
|
|
6226
|
-
// worker as its own coordinator, suppressing the spontaneous completion-event
|
|
6227
|
-
// forward and leaving the event in the pending inbox until a read_chat
|
|
6228
|
-
// reconcile drains it. If the anchor is genuinely absent here, leave it
|
|
6229
|
-
// absent rather than poison the routing.
|
|
6230
|
-
const launchResult = await this.deps.cliManager.handleCliCommand(cmd, args);
|
|
6231
|
-
// Bug C fix (part 1): when launching a mesh node worker session, surface
|
|
6232
|
-
// bootstrapPending:true if the node's worktree bootstrap is still running.
|
|
6233
|
-
// This is informational — the launch is NOT blocked here (blocking is done
|
|
6234
|
-
// upstream by getWorktreeBootstrapLaunchBlock in the MCP layer).
|
|
6235
|
-
const meshNodeId = readStringValue((args?.settings as any)?.meshNodeId);
|
|
6236
|
-
const meshId = readStringValue((args?.settings as any)?.meshNodeFor);
|
|
6237
|
-
if (meshNodeId && meshId && launchResult?.success !== false) {
|
|
6238
|
-
try {
|
|
6239
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
6240
|
-
const meshObj = getMesh(meshId) ?? this.getCachedInlineMesh(meshId);
|
|
6241
|
-
const nodeObj = Array.isArray(meshObj?.nodes)
|
|
6242
|
-
? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, meshNodeId))
|
|
6243
|
-
: undefined;
|
|
6244
|
-
const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
|
|
6245
|
-
if (bootstrapStatus === 'running') {
|
|
6246
|
-
return { success: true, ...launchResult, bootstrapPending: true };
|
|
6247
|
-
}
|
|
6248
|
-
} catch { /* best-effort — do not fail launch for bootstrap probe errors */ }
|
|
6249
|
-
}
|
|
6250
|
-
return launchResult;
|
|
6251
|
-
}
|
|
6252
|
-
case 'stop_cli':
|
|
6253
|
-
case 'set_cli_view_mode':
|
|
6254
|
-
case 'record_provider_pty': {
|
|
6255
|
-
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
6256
|
-
}
|
|
6257
|
-
case 'agent_command': {
|
|
6258
|
-
// Relay-safety stamp: a dispatch carrying meshContext.coordinatorDaemonId
|
|
6259
|
-
// (mesh_send_task / queue assignment over P2P) is the worker daemon's chance
|
|
6260
|
-
// to persist the coordinator routing anchor onto the target session BEFORE the
|
|
6261
|
-
// turn runs. Without meshCoordinatorDaemonId on the session, the core forwarder
|
|
6262
|
-
// (injectMeshSystemMessage) cannot resolve a remote coordinator target, so the
|
|
6263
|
-
// completion event sits in the pending queue until a read_chat reconcile drains
|
|
6264
|
-
// it. Stamping here makes a reused/relaunched remote session relay-safe at
|
|
6265
|
-
// dispatch time even when it was not launched via mesh_launch_session.
|
|
6266
|
-
{
|
|
6267
|
-
const dispatchSessionId = readStringValue(args?.targetSessionId, (args as any)?.sessionId, (args as any)?.instanceId);
|
|
6268
|
-
const dispatchMeshContext = args?.meshContext as Record<string, unknown> | undefined;
|
|
6269
|
-
if (dispatchSessionId && dispatchMeshContext) {
|
|
6270
|
-
try {
|
|
6271
|
-
const inst = this.deps.instanceManager.getInstance(dispatchSessionId);
|
|
6272
|
-
if (inst && typeof inst.updateSettings === 'function') {
|
|
6273
|
-
const stamp = buildMeshWorkerRelayStamp(
|
|
6274
|
-
inst.getState?.()?.settings as Record<string, unknown> | undefined,
|
|
6275
|
-
{
|
|
6276
|
-
meshId: dispatchMeshContext.meshId,
|
|
6277
|
-
nodeId: dispatchMeshContext.nodeId,
|
|
6278
|
-
coordinatorDaemonId: dispatchMeshContext.coordinatorDaemonId,
|
|
6279
|
-
// Session-level anchor: preserved across the P2P dispatch to a
|
|
6280
|
-
// remote worker so its completion echoes back to the right session.
|
|
6281
|
-
coordinatorSessionId: dispatchMeshContext.coordinatorSessionId,
|
|
6282
|
-
},
|
|
6283
|
-
);
|
|
6284
|
-
if (stamp) inst.updateSettings(stamp);
|
|
6285
|
-
}
|
|
6286
|
-
} catch { /* best-effort — dispatch still proceeds without the stamp */ }
|
|
6287
|
-
}
|
|
6288
|
-
}
|
|
6289
|
-
const agentResult = await this.deps.cliManager.handleCliCommand(cmd, args);
|
|
6290
|
-
// Bug C fix (part 2): when dispatching a task to a mesh node session, override
|
|
6291
|
-
// the dispatch acknowledgement risk reason to 'bootstrap_still_running' when
|
|
6292
|
-
// the target node's worktree bootstrap is still running. Informational only —
|
|
6293
|
-
// dispatch is NOT blocked.
|
|
6294
|
-
const meshCtx = args?.meshContext as Record<string, unknown> | undefined;
|
|
6295
|
-
const dispatchNodeId = readStringValue(meshCtx?.nodeId);
|
|
6296
|
-
const dispatchMeshId = readStringValue(meshCtx?.meshId);
|
|
6297
|
-
if (dispatchNodeId && dispatchMeshId && agentResult?.success !== false) {
|
|
6298
|
-
try {
|
|
6299
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
6300
|
-
const meshObj = getMesh(dispatchMeshId) ?? this.getCachedInlineMesh(dispatchMeshId);
|
|
6301
|
-
const nodeObj = Array.isArray(meshObj?.nodes)
|
|
6302
|
-
? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, dispatchNodeId))
|
|
6303
|
-
: undefined;
|
|
6304
|
-
const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
|
|
6305
|
-
if (bootstrapStatus === 'running') {
|
|
6306
|
-
return {
|
|
6307
|
-
success: true,
|
|
6308
|
-
...agentResult,
|
|
6309
|
-
dispatchAcknowledgementRisk: true,
|
|
6310
|
-
dispatchAcknowledgementRiskReason: 'bootstrap_still_running',
|
|
6311
|
-
nextAction: 'Wait for worktree_bootstrap_complete event before dispatching work to this node.',
|
|
6312
|
-
};
|
|
6313
|
-
}
|
|
6314
|
-
} catch { /* best-effort */ }
|
|
6315
|
-
}
|
|
6316
|
-
return agentResult;
|
|
6317
|
-
}
|
|
6318
|
-
|
|
6319
|
-
// ─── Logs ───
|
|
6320
|
-
case 'list_saved_sessions': {
|
|
6321
|
-
const providerType = typeof args?.providerType === 'string'
|
|
6322
|
-
? args.providerType.trim()
|
|
6323
|
-
: typeof args?.agentType === 'string'
|
|
6324
|
-
? args.agentType.trim()
|
|
6325
|
-
: '';
|
|
6326
|
-
const kind = args?.kind === 'acp' ? 'acp' : 'cli';
|
|
6327
|
-
if (!providerType) {
|
|
6328
|
-
return { success: false, error: 'providerType required' };
|
|
6329
|
-
}
|
|
6330
|
-
|
|
6331
|
-
const wantsAll = args?.all === true;
|
|
6332
|
-
const offset = wantsAll ? 0 : Math.max(0, Number(args?.offset) || 0);
|
|
6333
|
-
const limit = wantsAll ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.min(100, Number(args?.limit) || 30));
|
|
6334
|
-
const requestedWorkspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
6335
|
-
const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
|
|
6336
|
-
? args.providerSessionId.trim()
|
|
6337
|
-
: typeof args?.activeProviderSessionId === 'string'
|
|
6338
|
-
? args.activeProviderSessionId.trim()
|
|
6339
|
-
: '';
|
|
6340
|
-
const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
|
|
6341
|
-
const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
|
|
6342
|
-
canonicalHistory: providerMeta?.nativeHistory,
|
|
6343
|
-
offset,
|
|
6344
|
-
limit,
|
|
6345
|
-
historyBehavior: providerMeta?.historyBehavior,
|
|
6346
|
-
scripts: providerMeta?.scripts as any,
|
|
6347
|
-
});
|
|
6348
|
-
const state = loadState();
|
|
6349
|
-
const savedSessions = getSavedProviderSessions(state, { providerType, kind });
|
|
6350
|
-
const recentSessions = getRecentActivity(state, 200)
|
|
6351
|
-
.filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
|
|
6352
|
-
const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
|
|
6353
|
-
const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
|
|
6354
|
-
const canResumeById = supportsExplicitSessionResume(providerMeta?.resume);
|
|
6355
|
-
|
|
6356
|
-
return {
|
|
6357
|
-
success: true,
|
|
6358
|
-
sessions: historySessions.map(session => {
|
|
6359
|
-
const saved = savedSessionById.get(session.historySessionId);
|
|
6360
|
-
const recent = recentSessionById.get(session.historySessionId);
|
|
6361
|
-
const workspace = saved?.workspace
|
|
6362
|
-
|| recent?.workspace
|
|
6363
|
-
|| session.workspace
|
|
6364
|
-
|| (requestedWorkspace && requestedProviderSessionId === session.historySessionId ? requestedWorkspace : undefined);
|
|
6365
|
-
return {
|
|
6366
|
-
id: session.historySessionId,
|
|
6367
|
-
providerSessionId: session.historySessionId,
|
|
6368
|
-
providerType,
|
|
6369
|
-
providerName: saved?.providerName || recent?.providerName || providerType,
|
|
6370
|
-
kind: saved?.kind || recent?.kind || kind,
|
|
6371
|
-
title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
|
|
6372
|
-
workspace,
|
|
6373
|
-
summaryMetadata: saved?.summaryMetadata || recent?.summaryMetadata,
|
|
6374
|
-
preview: session.preview,
|
|
6375
|
-
messageCount: session.messageCount,
|
|
6376
|
-
firstMessageAt: session.firstMessageAt,
|
|
6377
|
-
lastMessageAt: session.lastMessageAt,
|
|
6378
|
-
canResume: !!workspace && canResumeById,
|
|
6379
|
-
historySource: session.source,
|
|
6380
|
-
sourcePath: session.sourcePath,
|
|
6381
|
-
sourceMtimeMs: session.sourceMtimeMs,
|
|
6382
|
-
};
|
|
6383
|
-
}),
|
|
6384
|
-
hasMore,
|
|
6385
|
-
source,
|
|
6386
|
-
};
|
|
6387
|
-
}
|
|
6388
|
-
|
|
6389
|
-
// ─── restart_session: IDE / CLI / ACP unified ───
|
|
6390
|
-
case 'restart_session': {
|
|
6391
|
-
const targetType = args?.cliType || args?.agentType || args?.ideType;
|
|
6392
|
-
if (!targetType) throw new Error('cliType or ideType required');
|
|
6393
|
-
|
|
6394
|
-
// Check if IDE (in cdpManagers or provider category is ide)
|
|
6395
|
-
const isIde = this.deps.cdpManagers.has(targetType) ||
|
|
6396
|
-
this.deps.providerLoader.getMeta(targetType)?.category === 'ide';
|
|
6397
|
-
|
|
6398
|
-
if (isIde) {
|
|
6399
|
-
// IDE restart: stop (with process kill) → launch
|
|
6400
|
-
await this.stopIde(targetType, true);
|
|
6401
|
-
const launchResult = await this.executeDaemonCommand('launch_ide', { ideType: targetType, enableCdp: true, workspace: args?.workspace });
|
|
6402
|
-
return { success: true, restarted: true, ideType: targetType, launch: launchResult };
|
|
6403
|
-
}
|
|
6404
|
-
|
|
6405
|
-
// CLI/ACP restart: delegate to CliManager
|
|
6406
|
-
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
6407
|
-
}
|
|
6408
|
-
|
|
6409
|
-
// ─── IDE stop ───
|
|
6410
|
-
case 'stop_ide': {
|
|
6411
|
-
const ideType = args?.ideType;
|
|
6412
|
-
if (!ideType) throw new Error('ideType required');
|
|
6413
|
-
const killProcess = args?.killProcess !== false; // default true
|
|
6414
|
-
await this.stopIde(ideType, killProcess);
|
|
6415
|
-
try {
|
|
6416
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
6417
|
-
this.deps.detectedIdes.value = results;
|
|
6418
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
6419
|
-
} catch { /* ignore detection refresh errors */ }
|
|
6420
|
-
return { success: true, ideType, stopped: true, processKilled: killProcess };
|
|
6421
|
-
}
|
|
6422
|
-
|
|
6423
|
-
// ─── IDE restart ───
|
|
6424
|
-
case 'restart_ide': {
|
|
6425
|
-
const ideType = args?.ideType;
|
|
6426
|
-
if (!ideType) throw new Error('ideType required');
|
|
6427
|
-
await this.stopIde(ideType, true); // always kill process on restart
|
|
6428
|
-
const launchResult = await this.executeDaemonCommand('launch_ide', { ideType, enableCdp: true, workspace: args?.workspace });
|
|
6429
|
-
return { success: true, ideType, restarted: true, launch: launchResult };
|
|
6430
|
-
}
|
|
6431
|
-
|
|
6432
|
-
// ─── IDE launch + CDP connect ───
|
|
6433
|
-
case 'launch_ide': {
|
|
6434
|
-
const ideKey = args?.ideId || args?.ideType;
|
|
6435
|
-
const resolvedWorkspace = resolveIdeLaunchWorkspace(
|
|
6436
|
-
{
|
|
6437
|
-
workspace: args?.workspace,
|
|
6438
|
-
workspaceId: args?.workspaceId,
|
|
6439
|
-
useDefaultWorkspace: args?.useDefaultWorkspace,
|
|
6440
|
-
},
|
|
6441
|
-
loadConfig(),
|
|
6442
|
-
);
|
|
6443
|
-
const launchArgs = {
|
|
6444
|
-
ideId: ideKey,
|
|
6445
|
-
workspace: resolvedWorkspace,
|
|
6446
|
-
newWindow: args?.newWindow,
|
|
6447
|
-
};
|
|
6448
|
-
LOG.info('LaunchIDE', `target=${ideKey || 'auto'}`);
|
|
6449
|
-
const result = await launchWithCdp(launchArgs);
|
|
6450
|
-
|
|
6451
|
-
if (result.success && result.port && result.ideId && !this.deps.cdpManagers.has(result.ideId)) {
|
|
6452
|
-
const logFn = this.deps.getCdpLogFn
|
|
6453
|
-
? this.deps.getCdpLogFn(result.ideId)
|
|
6454
|
-
: LOG.forComponent(`CDP:${result.ideId}`).asLogFn();
|
|
6455
|
-
const provider = this.deps.providerLoader.getMeta(result.ideId);
|
|
6456
|
-
const manager = new DaemonCdpManager(result.port, logFn, undefined, provider?.targetFilter);
|
|
6457
|
-
const connected = await manager.connect();
|
|
6458
|
-
if (connected) {
|
|
6459
|
-
// Register active extension providers for this IDE in CDP manager
|
|
6460
|
-
registerExtensionProviders(this.deps.providerLoader, manager, result.ideId);
|
|
6461
|
-
this.deps.cdpManagers.set(result.ideId, manager);
|
|
6462
|
-
LOG.info('CDP', `Connected: ${result.ideId} (port ${result.port})`);
|
|
6463
|
-
LOG.info('CDP', `${this.deps.cdpManagers.size} IDE(s) connected`);
|
|
6464
|
-
|
|
6465
|
-
// Notify consumer (e.g. setupIdeInstance)
|
|
6466
|
-
this.deps.onCdpManagerCreated?.(result.ideId, manager);
|
|
6467
|
-
}
|
|
6468
|
-
}
|
|
6469
|
-
this.deps.onIdeConnected?.();
|
|
6470
|
-
try {
|
|
6471
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
6472
|
-
this.deps.detectedIdes.value = results;
|
|
6473
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
6474
|
-
} catch { /* ignore detection refresh errors */ }
|
|
6475
|
-
if (result.success && resolvedWorkspace) {
|
|
6476
|
-
try {
|
|
6477
|
-
const next = appendRecentActivity(loadState(), {
|
|
6478
|
-
kind: 'ide',
|
|
6479
|
-
providerType: result.ideId || ideKey,
|
|
6480
|
-
providerName: result.ideId || ideKey,
|
|
6481
|
-
workspace: resolvedWorkspace,
|
|
6482
|
-
title: result.ideId || ideKey,
|
|
6483
|
-
});
|
|
6484
|
-
saveState(next);
|
|
6485
|
-
} catch { /* ignore activity persist errors */ }
|
|
6486
|
-
} else if (result.success && (result.ideId || ideKey)) {
|
|
6487
|
-
try {
|
|
6488
|
-
saveState(appendRecentActivity(loadState(), {
|
|
6489
|
-
kind: 'ide',
|
|
6490
|
-
providerType: result.ideId || ideKey,
|
|
6491
|
-
providerName: result.ideId || ideKey,
|
|
6492
|
-
title: result.ideId || ideKey,
|
|
6493
|
-
}));
|
|
6494
|
-
} catch { /* ignore activity persist errors */ }
|
|
6495
|
-
}
|
|
6496
|
-
return { ...result };
|
|
6497
|
-
}
|
|
6498
|
-
|
|
6499
|
-
// ─── Detect providers ───
|
|
6500
|
-
case 'detect_provider': {
|
|
6501
|
-
const providerType = typeof args?.providerType === 'string' ? args.providerType.trim() : '';
|
|
6502
|
-
if (!providerType) return { success: false, error: 'providerType is required' };
|
|
6503
|
-
const normalizedType = this.deps.providerLoader.resolveAlias(providerType);
|
|
6504
|
-
const provider = this.deps.providerLoader.getByAlias(providerType);
|
|
6505
|
-
if (!provider) return { success: false, error: `Provider not found: ${providerType}` };
|
|
6506
|
-
if (provider.category !== 'cli' && provider.category !== 'acp') {
|
|
6507
|
-
return { success: false, error: `Provider detection is only supported for CLI/ACP providers: ${providerType}` };
|
|
6508
|
-
}
|
|
6509
|
-
if (!this.deps.providerLoader.isMachineProviderEnabled(normalizedType)) {
|
|
6510
|
-
return { success: false, error: `Provider is disabled on this machine: ${providerType}` };
|
|
6511
|
-
}
|
|
6512
|
-
const detected = await detectCLI(normalizedType, this.deps.providerLoader, { includeVersion: false });
|
|
6513
|
-
this.deps.providerLoader.setCliDetectionResults([{
|
|
6514
|
-
id: normalizedType,
|
|
6515
|
-
installed: !!detected,
|
|
6516
|
-
path: detected?.path,
|
|
6517
|
-
}], false);
|
|
6518
|
-
this.deps.onStatusChange?.();
|
|
6519
|
-
return {
|
|
6520
|
-
success: true,
|
|
6521
|
-
providerType: normalizedType,
|
|
6522
|
-
detected: !!detected,
|
|
6523
|
-
path: detected?.path || null,
|
|
6524
|
-
};
|
|
6525
|
-
}
|
|
6526
|
-
|
|
6527
|
-
// ─── Detect IDEs ───
|
|
6528
|
-
case 'detect_ides': {
|
|
6529
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
6530
|
-
this.deps.detectedIdes.value = results;
|
|
6531
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
6532
|
-
return { success: true, detectedInfo: results };
|
|
6533
|
-
}
|
|
6534
|
-
|
|
6535
|
-
// ─── Mesh CRUD (local meshes.json) ───
|
|
6536
|
-
case 'list_meshes': {
|
|
6537
|
-
try {
|
|
6538
|
-
const { listMeshes } = await import('../config/mesh-config.js');
|
|
6539
|
-
return { success: true, meshes: listMeshes() };
|
|
6540
|
-
} catch (e: any) {
|
|
6541
|
-
return { success: false, error: e.message };
|
|
6542
|
-
}
|
|
6543
|
-
}
|
|
6544
|
-
|
|
6545
|
-
case 'get_mesh': {
|
|
6546
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6547
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6548
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6549
|
-
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
6550
|
-
|
|
6551
|
-
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
6552
|
-
// Only an explicit refresh fans out a blocking peer probe.
|
|
6553
|
-
// Default loads are satisfied from held standing-state git truth.
|
|
6554
|
-
const probeRemotePeers = args?.refresh === true || args?.forceRefresh === true;
|
|
6555
|
-
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
6556
|
-
mesh: meshRecord.mesh,
|
|
6557
|
-
meshSource: meshRecord.source,
|
|
6558
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6559
|
-
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
6560
|
-
statusInstanceId: this.deps.statusInstanceId,
|
|
6561
|
-
localMachineId: loadConfig().machineId || '',
|
|
6562
|
-
probeRemotePeers,
|
|
6563
|
-
probeCache: this.meshGitProbeCache,
|
|
6564
|
-
});
|
|
6565
|
-
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
6566
|
-
const sourceOfTruth = {
|
|
6567
|
-
membership: meshRecord.source === 'inline_cache'
|
|
6568
|
-
? 'coordinator_inline_mesh_cache'
|
|
6569
|
-
: meshRecord.source === 'local_config'
|
|
6570
|
-
? 'local_mesh_config'
|
|
6571
|
-
: 'inline_bootstrap_snapshot',
|
|
6572
|
-
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
6573
|
-
directPeerTruth: {
|
|
6574
|
-
required: requireDirectPeerTruth,
|
|
6575
|
-
satisfied: directTruthSatisfied,
|
|
6576
|
-
directEvidenceCount: directTruth.directEvidenceCount,
|
|
6577
|
-
localConfirmedCount: directTruth.localConfirmedCount,
|
|
6578
|
-
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
6579
|
-
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
6580
|
-
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
6581
|
-
},
|
|
6582
|
-
};
|
|
6583
|
-
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
6584
|
-
return {
|
|
6585
|
-
success: false,
|
|
6586
|
-
code: 'mesh_direct_peer_truth_unavailable',
|
|
6587
|
-
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
6588
|
-
sourceOfTruth,
|
|
6589
|
-
};
|
|
6590
|
-
}
|
|
6591
|
-
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
6592
|
-
}
|
|
6593
|
-
|
|
6594
|
-
case 'create_mesh': {
|
|
6595
|
-
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
6596
|
-
const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
|
|
6597
|
-
const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
|
|
6598
|
-
const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
|
|
6599
|
-
if (!name) return { success: false, error: 'name required' };
|
|
6600
|
-
try {
|
|
6601
|
-
const { createMesh } = await import('../config/mesh-config.js');
|
|
6602
|
-
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
6603
|
-
? args.meshHost
|
|
6604
|
-
: undefined;
|
|
6605
|
-
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
6606
|
-
return { success: true, mesh };
|
|
6607
|
-
} catch (e: any) {
|
|
6608
|
-
return { success: false, error: e.message };
|
|
6609
|
-
}
|
|
6610
|
-
}
|
|
6611
|
-
|
|
6612
|
-
case 'update_mesh': {
|
|
6613
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6614
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6615
|
-
try {
|
|
6616
|
-
const { updateMesh } = await import('../config/mesh-config.js');
|
|
6617
|
-
const patch: Record<string, unknown> = {};
|
|
6618
|
-
if (typeof args?.name === 'string') patch.name = args.name;
|
|
6619
|
-
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
6620
|
-
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
6621
|
-
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
6622
|
-
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
6623
|
-
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
6624
|
-
const mesh = updateMesh(meshId, patch as any);
|
|
6625
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6626
|
-
this.inlineMeshCache.set(meshId, mesh);
|
|
6627
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6628
|
-
return { success: true, mesh };
|
|
6629
|
-
} catch (e: any) {
|
|
6630
|
-
return { success: false, error: e.message };
|
|
6631
|
-
}
|
|
6632
|
-
}
|
|
6633
|
-
|
|
6634
|
-
case 'get_mesh_host_pairing': {
|
|
6635
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6636
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6637
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6638
|
-
const mesh = meshRecord?.mesh;
|
|
6639
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6640
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
6641
|
-
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
6642
|
-
return {
|
|
6643
|
-
success: true,
|
|
6644
|
-
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
6645
|
-
meshId,
|
|
6646
|
-
hostAddress: meshHost.hostAddress,
|
|
6647
|
-
meshHost,
|
|
6648
|
-
manualPairing: {
|
|
6649
|
-
status: pairingStatus,
|
|
6650
|
-
joinImplemented: true,
|
|
6651
|
-
protocol: 'standalone_command_direct_v1',
|
|
6652
|
-
description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
|
|
6653
|
-
},
|
|
6654
|
-
};
|
|
6655
|
-
}
|
|
6656
|
-
|
|
6657
|
-
case 'configure_mesh_host_pairing': {
|
|
6658
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6659
|
-
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
6660
|
-
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
6661
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6662
|
-
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
6663
|
-
try {
|
|
6664
|
-
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
6665
|
-
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
6666
|
-
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
6667
|
-
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
6668
|
-
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
6669
|
-
return {
|
|
6670
|
-
success: true,
|
|
6671
|
-
code: 'mesh_host_pairing_pending',
|
|
6672
|
-
meshId,
|
|
6673
|
-
hostAddress: configured.hostAddress,
|
|
6674
|
-
meshHost,
|
|
6675
|
-
manualPairing: {
|
|
6676
|
-
status: meshHost.pairing?.status || 'pairing',
|
|
6677
|
-
joinImplemented: true,
|
|
6678
|
-
protocol: 'standalone_command_direct_v1',
|
|
6679
|
-
description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
|
|
6680
|
-
},
|
|
6681
|
-
};
|
|
6682
|
-
} catch (e: any) {
|
|
6683
|
-
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
6684
|
-
}
|
|
6685
|
-
}
|
|
6686
|
-
|
|
6687
|
-
case 'create_mesh_host_pairing_token': {
|
|
6688
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6689
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6690
|
-
try {
|
|
6691
|
-
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
6692
|
-
const created = createMeshHostPairingToken(meshId, {
|
|
6693
|
-
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
6694
|
-
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
6695
|
-
});
|
|
6696
|
-
if (!created) return { success: false, error: 'Mesh not found' };
|
|
6697
|
-
this.inlineMeshCache.set(meshId, created.mesh);
|
|
6698
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6699
|
-
return {
|
|
6700
|
-
success: true,
|
|
6701
|
-
code: 'mesh_host_pairing_token_created',
|
|
6702
|
-
meshId,
|
|
6703
|
-
token: created.token,
|
|
6704
|
-
tokenId: created.tokenId,
|
|
6705
|
-
expiresAt: created.expiresAt,
|
|
6706
|
-
meshHost: resolveMeshHostStatus(created.mesh),
|
|
6707
|
-
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
6708
|
-
};
|
|
6709
|
-
} catch (e: any) {
|
|
6710
|
-
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
6711
|
-
}
|
|
6712
|
-
}
|
|
6713
|
-
|
|
6714
|
-
case 'apply_mesh_host_join': {
|
|
6715
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6716
|
-
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
6717
|
-
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
6718
|
-
? args.memberNode
|
|
6719
|
-
: null;
|
|
6720
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6721
|
-
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
6722
|
-
try {
|
|
6723
|
-
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
6724
|
-
const applied = applyMeshHostJoinRequest(meshId, {
|
|
6725
|
-
token,
|
|
6726
|
-
memberNode: memberNode as any,
|
|
6727
|
-
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
6728
|
-
});
|
|
6729
|
-
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
6730
|
-
if (!applied.accepted) {
|
|
6731
|
-
return {
|
|
6732
|
-
success: false,
|
|
6733
|
-
code: 'mesh_host_join_rejected',
|
|
6734
|
-
meshId,
|
|
6735
|
-
tokenId: applied.tokenId,
|
|
6736
|
-
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
6737
|
-
error: applied.reason,
|
|
6738
|
-
};
|
|
6739
|
-
}
|
|
6740
|
-
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
6741
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6742
|
-
try {
|
|
6743
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
6744
|
-
appendLedgerEntry(meshId, {
|
|
6745
|
-
kind: 'node_joined',
|
|
6746
|
-
nodeId: applied.node.id,
|
|
6747
|
-
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
6748
|
-
});
|
|
6749
|
-
} catch { /* ledger append is best-effort */ }
|
|
6750
|
-
return {
|
|
6751
|
-
success: true,
|
|
6752
|
-
code: 'mesh_host_join_accepted',
|
|
6753
|
-
meshId,
|
|
6754
|
-
node: applied.node,
|
|
6755
|
-
tokenId: applied.tokenId,
|
|
6756
|
-
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
6757
|
-
};
|
|
6758
|
-
} catch (e: any) {
|
|
6759
|
-
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
6760
|
-
}
|
|
6761
|
-
}
|
|
6762
|
-
|
|
6763
|
-
case 'join_mesh_host_pairing': {
|
|
6764
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6765
|
-
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
6766
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6767
|
-
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
6768
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6769
|
-
const mesh = meshRecord?.mesh;
|
|
6770
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6771
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
6772
|
-
if (meshHost.role !== 'member') {
|
|
6773
|
-
return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
|
|
6774
|
-
}
|
|
6775
|
-
try {
|
|
6776
|
-
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
6777
|
-
const tokenId = tokenIdForManualPairing(token);
|
|
6778
|
-
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
6779
|
-
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
6780
|
-
}
|
|
6781
|
-
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
6782
|
-
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
6783
|
-
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
6784
|
-
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
6785
|
-
? args.hostDaemonId.trim()
|
|
6786
|
-
: meshHost.hostDaemonId;
|
|
6787
|
-
let hostResult: any;
|
|
6788
|
-
let transport: string;
|
|
6789
|
-
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
6790
|
-
transport = 'mesh_command_dispatch';
|
|
6791
|
-
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
6792
|
-
meshId: hostMeshId,
|
|
6793
|
-
token,
|
|
6794
|
-
memberMeshId: meshId,
|
|
6795
|
-
memberNode,
|
|
6796
|
-
});
|
|
6797
|
-
} else if (meshHost.hostAddress) {
|
|
6798
|
-
transport = 'standalone_http_command';
|
|
6799
|
-
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
6800
|
-
const response = await fetch(commandUrl, {
|
|
6801
|
-
method: 'POST',
|
|
6802
|
-
headers: { 'Content-Type': 'application/json' },
|
|
6803
|
-
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
6804
|
-
});
|
|
6805
|
-
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
6806
|
-
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
6807
|
-
} else {
|
|
6808
|
-
return {
|
|
6809
|
-
success: false,
|
|
6810
|
-
code: 'mesh_host_join_transport_unavailable',
|
|
6811
|
-
meshId,
|
|
6812
|
-
meshHost,
|
|
6813
|
-
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
6814
|
-
};
|
|
6815
|
-
}
|
|
6816
|
-
if (!hostResult?.success) {
|
|
6817
|
-
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
6818
|
-
}
|
|
6819
|
-
const joined = meshRecord.inline
|
|
6820
|
-
? null
|
|
6821
|
-
: markMeshHostPairingJoined(meshId, {
|
|
6822
|
-
tokenId: hostResult.tokenId || tokenId,
|
|
6823
|
-
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
6824
|
-
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
6825
|
-
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
6826
|
-
});
|
|
6827
|
-
if (joined) {
|
|
6828
|
-
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
6829
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6830
|
-
}
|
|
6831
|
-
return {
|
|
6832
|
-
success: true,
|
|
6833
|
-
code: 'mesh_host_join_applied',
|
|
6834
|
-
meshId,
|
|
6835
|
-
hostMeshId,
|
|
6836
|
-
transport,
|
|
6837
|
-
node: hostResult.node,
|
|
6838
|
-
tokenId: hostResult.tokenId || tokenId,
|
|
6839
|
-
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
6840
|
-
hostResult,
|
|
6841
|
-
manualPairing: {
|
|
6842
|
-
status: 'paired',
|
|
6843
|
-
joinImplemented: true,
|
|
6844
|
-
protocol: 'standalone_command_direct_v1',
|
|
6845
|
-
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
6846
|
-
},
|
|
6847
|
-
};
|
|
6848
|
-
} catch (e: any) {
|
|
6849
|
-
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
6850
|
-
}
|
|
6851
|
-
}
|
|
6852
|
-
|
|
6853
|
-
case 'delete_mesh': {
|
|
6854
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6855
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6856
|
-
try {
|
|
6857
|
-
const { deleteMesh } = await import('../config/mesh-config.js');
|
|
6858
|
-
const deleted = deleteMesh(meshId);
|
|
6859
|
-
return { success: true, deleted };
|
|
6860
|
-
} catch (e: any) {
|
|
6861
|
-
return { success: false, error: e.message };
|
|
6862
|
-
}
|
|
6863
|
-
}
|
|
6864
|
-
|
|
6865
|
-
case 'get_mesh_queue': {
|
|
6866
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6867
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6868
|
-
try {
|
|
6869
|
-
const { getMeshQueueStats, getQueue, describeTaskDependencyState } = await import('../mesh/mesh-work-queue.js');
|
|
6870
|
-
const status = Array.isArray(args?.status)
|
|
6871
|
-
? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
|
|
6872
|
-
: undefined;
|
|
6873
|
-
const rawQueue = getQueue(meshId, { status: status as any });
|
|
6874
|
-
// M1: annotate dependency state at view time (waitingOn / dependenciesSatisfied).
|
|
6875
|
-
const statusById = new Map(getQueue(meshId).map(task => [task.id, task.status]));
|
|
6876
|
-
const queue = rawQueue.map(task =>
|
|
6877
|
-
Array.isArray(task.dependsOn) && task.dependsOn.length > 0
|
|
6878
|
-
? { ...task, ...describeTaskDependencyState(task, statusById) }
|
|
6879
|
-
: task);
|
|
6880
|
-
const summary = getMeshQueueStats(meshId);
|
|
6881
|
-
return {
|
|
6882
|
-
success: true,
|
|
6883
|
-
queue,
|
|
6884
|
-
summary,
|
|
6885
|
-
sourceOfTruth: {
|
|
6886
|
-
kind: 'mesh_work_queue_file',
|
|
6887
|
-
activeStatuses: ['pending', 'assigned'],
|
|
6888
|
-
historicalStatuses: ['completed', 'failed', 'cancelled'],
|
|
6889
|
-
notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
|
|
6890
|
-
},
|
|
6891
|
-
};
|
|
6892
|
-
} catch (e: any) {
|
|
6893
|
-
return { success: false, error: e.message };
|
|
6894
|
-
}
|
|
6895
|
-
}
|
|
6896
|
-
|
|
6897
|
-
case 'cancel_mesh_queue_task': {
|
|
6898
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6899
|
-
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
6900
|
-
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
6901
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
6902
|
-
if (ownerFailure) return ownerFailure;
|
|
6903
|
-
try {
|
|
6904
|
-
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
6905
|
-
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
6906
|
-
const task = cancelTask(meshId, taskId, { reason });
|
|
6907
|
-
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
6908
|
-
return { success: true, task };
|
|
6909
|
-
} catch (e: any) {
|
|
6910
|
-
return { success: false, error: e.message };
|
|
6911
|
-
}
|
|
6912
|
-
}
|
|
6913
|
-
|
|
6914
|
-
case 'requeue_mesh_queue_task': {
|
|
6915
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6916
|
-
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
6917
|
-
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
6918
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
6919
|
-
if (ownerFailure) return ownerFailure;
|
|
6920
|
-
try {
|
|
6921
|
-
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
6922
|
-
const task = requeueTask(meshId, taskId, {
|
|
6923
|
-
reason: typeof args?.reason === 'string' ? args.reason : undefined,
|
|
6924
|
-
targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
|
|
6925
|
-
targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
|
|
6926
|
-
clearTargetNode: args?.clearTargetNode === true,
|
|
6927
|
-
clearTargetSession: args?.clearTargetSession !== false,
|
|
6928
|
-
});
|
|
6929
|
-
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
6930
|
-
return { success: true, task };
|
|
6931
|
-
} catch (e: any) {
|
|
6932
|
-
return { success: false, error: e.message };
|
|
6933
|
-
}
|
|
6934
|
-
}
|
|
6935
|
-
|
|
6936
|
-
case 'add_mesh_node': {
|
|
6937
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6938
|
-
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
6939
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6940
|
-
if (!workspace) return { success: false, error: 'workspace required' };
|
|
6941
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
6942
|
-
if (ownerFailure) return ownerFailure;
|
|
6943
|
-
try {
|
|
6944
|
-
const { addNode } = await import('../config/mesh-config.js');
|
|
6945
|
-
const providerPriority = Array.isArray(args?.providerPriority)
|
|
6946
|
-
? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
|
|
6947
|
-
: [];
|
|
6948
|
-
const readOnly = args?.readOnly === true;
|
|
6949
|
-
const providerRoles = normalizeProviderRoles(args?.providerRoles);
|
|
6950
|
-
const policy = {
|
|
6951
|
-
...(readOnly ? { readOnly: true } : {}),
|
|
6952
|
-
...(providerPriority.length ? { providerPriority } : {}),
|
|
6953
|
-
...(providerRoles.length ? { providerRoles } : {}),
|
|
6954
|
-
};
|
|
6955
|
-
const role = normalizeMeshDaemonRole(args?.role);
|
|
6956
|
-
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
6957
|
-
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
6958
|
-
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
6959
|
-
const node = addNode(meshId, {
|
|
6960
|
-
workspace,
|
|
6961
|
-
...(repoRoot ? { repoRoot } : {}),
|
|
6962
|
-
...(daemonId ? { daemonId } : {}),
|
|
6963
|
-
...(machineId ? { machineId } : {}),
|
|
6964
|
-
...(policy ? { policy } : {}),
|
|
6965
|
-
...(role ? { role } : {}),
|
|
6966
|
-
});
|
|
6967
|
-
if (!node) return { success: false, error: 'Mesh not found' };
|
|
6968
|
-
// mesh_status hands back a coordinator-memory aggregate
|
|
6969
|
-
// snapshot keyed on (meshId, queueRevision). Adding a
|
|
6970
|
-
// node touches neither, so without an explicit cache
|
|
6971
|
-
// bust the dashboard graph keeps rendering the pre-add
|
|
6972
|
-
// node list (empty for a fresh mesh) even after the
|
|
6973
|
-
// user clicks Refresh.
|
|
6974
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6975
|
-
return { success: true, node };
|
|
6976
|
-
} catch (e: any) {
|
|
6977
|
-
return { success: false, error: e.message };
|
|
6978
|
-
}
|
|
6979
|
-
}
|
|
6980
|
-
|
|
6981
|
-
case 'update_mesh_node': {
|
|
6982
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6983
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
6984
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
6985
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
6986
|
-
if (ownerFailure) return ownerFailure;
|
|
6987
|
-
try {
|
|
6988
|
-
const { updateNode } = await import('../config/mesh-config.js');
|
|
6989
|
-
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
6990
|
-
? { ...(args.policy as Record<string, unknown>) }
|
|
6991
|
-
: {};
|
|
6992
|
-
if (Array.isArray(args?.providerPriority)) {
|
|
6993
|
-
const providerPriority = args.providerPriority
|
|
6994
|
-
.map((type: any) => typeof type === 'string' ? type.trim() : '')
|
|
6995
|
-
.filter(Boolean);
|
|
6996
|
-
delete (policy as any).provider_priority;
|
|
6997
|
-
if (providerPriority.length) {
|
|
6998
|
-
(policy as any).providerPriority = providerPriority;
|
|
6999
|
-
} else {
|
|
7000
|
-
delete (policy as any).providerPriority;
|
|
7001
|
-
}
|
|
7002
|
-
}
|
|
7003
|
-
// providerRoles: per-(node, provider) role label + maxParallel cap.
|
|
7004
|
-
// Passing an explicit (possibly empty) array clears/replaces the
|
|
7005
|
-
// declarations; omitting the arg leaves any value already on policy
|
|
7006
|
-
// untouched (a full policy object passed by the caller still carries it).
|
|
7007
|
-
if (Array.isArray(args?.providerRoles)) {
|
|
7008
|
-
const providerRoles = normalizeProviderRoles(args.providerRoles);
|
|
7009
|
-
if (providerRoles.length) {
|
|
7010
|
-
(policy as any).providerRoles = providerRoles;
|
|
7011
|
-
} else {
|
|
7012
|
-
delete (policy as any).providerRoles;
|
|
7013
|
-
}
|
|
7014
|
-
}
|
|
7015
|
-
const patch: Record<string, unknown> = { policy: policy as any };
|
|
7016
|
-
if (typeof args?.systemPrompt === 'string') {
|
|
7017
|
-
const trimmed = (args.systemPrompt as string).trim();
|
|
7018
|
-
patch.systemPrompt = trimmed || undefined;
|
|
7019
|
-
} else if (args?.systemPrompt === null) {
|
|
7020
|
-
patch.systemPrompt = undefined;
|
|
7021
|
-
}
|
|
7022
|
-
const node = updateNode(meshId, nodeId, patch as any);
|
|
7023
|
-
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
7024
|
-
// Provider priority / systemPrompt changes don't touch
|
|
7025
|
-
// the queue revision, so without a manual bust the
|
|
7026
|
-
// cached aggregate keeps surfacing pre-update values
|
|
7027
|
-
// (priority chip, coordinator prompt preview, etc.).
|
|
7028
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
7029
|
-
return { success: true, node };
|
|
7030
|
-
} catch (e: any) {
|
|
7031
|
-
return { success: false, error: e.message };
|
|
7032
|
-
}
|
|
7033
|
-
}
|
|
7034
|
-
|
|
7035
|
-
case 'cleanup_mesh_sessions': {
|
|
7036
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7037
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7038
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7039
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
7040
|
-
if (ownerFailure) return ownerFailure;
|
|
7041
|
-
try {
|
|
7042
|
-
// preferInline so inline-cache-only clone nodes resolve (matches owner check above).
|
|
7043
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7044
|
-
const mesh = meshRecord?.mesh;
|
|
7045
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7046
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7047
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
7048
|
-
const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
|
|
7049
|
-
const sessionIds = Array.isArray(args?.sessionIds)
|
|
7050
|
-
? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
|
|
7051
|
-
: undefined;
|
|
7052
|
-
const result = await this.cleanupMeshSessions({
|
|
7053
|
-
meshId,
|
|
7054
|
-
nodeId,
|
|
7055
|
-
node,
|
|
7056
|
-
mode,
|
|
7057
|
-
sessionIds,
|
|
7058
|
-
dryRun: args?.dryRun === true,
|
|
7059
|
-
source: 'mesh_cleanup_sessions',
|
|
7060
|
-
});
|
|
7061
|
-
return result;
|
|
7062
|
-
} catch (e: any) {
|
|
7063
|
-
return { success: false, error: e.message };
|
|
7064
|
-
}
|
|
7065
|
-
}
|
|
7066
|
-
|
|
7067
|
-
case 'mesh_init': {
|
|
7068
|
-
const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
|
|
7069
|
-
const mesh = args?.inlineMesh || {};
|
|
7070
|
-
try {
|
|
7071
|
-
const detected = await detectCLIs(this.deps.providerLoader, { includeVersion: true });
|
|
7072
|
-
return { ...runMeshInit(mesh, workspace, detected, {
|
|
7073
|
-
write: args?.write === true,
|
|
7074
|
-
overwrite: args?.overwrite === true,
|
|
7075
|
-
}) };
|
|
7076
|
-
} catch (e: any) {
|
|
7077
|
-
return { success: false, error: e?.message || String(e) };
|
|
7078
|
-
}
|
|
7079
|
-
}
|
|
7080
|
-
|
|
7081
|
-
case 'plan_mesh_refine_node': {
|
|
7082
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7083
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7084
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7085
|
-
// preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
|
|
7086
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7087
|
-
const mesh = meshRecord?.mesh;
|
|
7088
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7089
|
-
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
7090
|
-
return {
|
|
7091
|
-
success: true,
|
|
7092
|
-
dryRun: true,
|
|
7093
|
-
nodeId,
|
|
7094
|
-
workspace: node.workspace,
|
|
7095
|
-
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
7096
|
-
mergeWillRun: false,
|
|
7097
|
-
cleanupWillRun: false,
|
|
7098
|
-
};
|
|
7099
|
-
}
|
|
7100
|
-
|
|
7101
|
-
case 'fast_forward_mesh_node': {
|
|
7102
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7103
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7104
|
-
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
7105
|
-
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
7106
|
-
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
7107
|
-
: undefined;
|
|
7108
|
-
let nodeDaemonId: string | undefined;
|
|
7109
|
-
let allowAutoPublishSubmoduleMainCommits = false;
|
|
7110
|
-
if (meshId && nodeId) {
|
|
7111
|
-
// preferInline so fast-forward can resolve inline-cache-only clone worktree nodes.
|
|
7112
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7113
|
-
const mesh = meshRecord?.mesh;
|
|
7114
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7115
|
-
if (!workspace) {
|
|
7116
|
-
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
7117
|
-
}
|
|
7118
|
-
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
7119
|
-
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
7120
|
-
}
|
|
7121
|
-
allowAutoPublishSubmoduleMainCommits = mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true;
|
|
7122
|
-
nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
7123
|
-
}
|
|
7124
|
-
// If the target node belongs to a remote daemon, forward the command there.
|
|
7125
|
-
// _meshDirectDispatch prevents re-forwarding (and P2P self-dial) when the stored
|
|
7126
|
-
// daemonId uses a legacy format that doesn't match the receiving daemon's identity.
|
|
7127
|
-
const selfDaemonId = this.deps.statusInstanceId;
|
|
7128
|
-
// daemonIdsEquivalent: a legacy-form stored daemonId that resolves to THIS
|
|
7129
|
-
// machine's core must be treated as local (not remote) so it is not forwarded /
|
|
7130
|
-
// P2P self-dialed. Equivalent → local.
|
|
7131
|
-
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
7132
|
-
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
7133
|
-
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'fast_forward_mesh_node', {
|
|
7134
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7135
|
-
workspace,
|
|
7136
|
-
_meshDirectDispatch: true,
|
|
7137
|
-
});
|
|
7138
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7139
|
-
}
|
|
7140
|
-
const result = await (fastForwardMeshNode({
|
|
7141
|
-
meshId: meshId || undefined,
|
|
7142
|
-
nodeId: nodeId || undefined,
|
|
7143
|
-
workspace,
|
|
7144
|
-
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
7145
|
-
execute: args?.execute === true,
|
|
7146
|
-
dryRun: args?.dryRun === true,
|
|
7147
|
-
updateSubmodules: args?.updateSubmodules === true,
|
|
7148
|
-
submoduleIgnorePaths,
|
|
7149
|
-
mode: args?.mode === 'push' ? 'push' : 'merge',
|
|
7150
|
-
pushSubmodules: args?.pushSubmodules === true,
|
|
7151
|
-
allowAutoPublishSubmoduleMainCommits,
|
|
7152
|
-
}) as Promise<unknown>);
|
|
7153
|
-
return result as CommandRouterResult;
|
|
7154
|
-
}
|
|
7155
|
-
|
|
7156
|
-
case 'refine_mesh_node': {
|
|
7157
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7158
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7159
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7160
|
-
|
|
7161
|
-
// Remote forward: a worktree node lives on its OWN daemon's machine, so the
|
|
7162
|
-
// refine (cd into node.workspace, merge → push → cleanup) must run on THAT
|
|
7163
|
-
// daemon — not the coordinator, whose filesystem has no such path. The sibling
|
|
7164
|
-
// fast_forward_mesh_node / clone_mesh_node handlers already forward to the
|
|
7165
|
-
// node's daemon; refine_mesh_node was the gap (the coordinator would cd into a
|
|
7166
|
-
// non-existent local path and fail), so remote-machine worktrees could not be
|
|
7167
|
-
// converged at all. Forward both dry-run (plan reads the worktree git state)
|
|
7168
|
-
// and execute (async merge job) so the same machine that owns the worktree
|
|
7169
|
-
// resolves it.
|
|
7170
|
-
//
|
|
7171
|
-
// coordinatorDaemonId: refine is ASYNC — the completed/failed event is queued
|
|
7172
|
-
// on the executing daemon's pending-events queue scoped to a coordinator id and
|
|
7173
|
-
// recovered by the coordinator's reconcile loop (pullRemoteNodeQueues →
|
|
7174
|
-
// get_pending_mesh_events). Without stamping our own status id, the remote
|
|
7175
|
-
// daemon would fall back to ITS OWN statusInstanceId as the coordinator
|
|
7176
|
-
// (startMeshRefineJob), scoping the terminal event to the wrong inbox where the
|
|
7177
|
-
// real coordinator never pulls it. Stamp the canonical status id (which is in
|
|
7178
|
-
// the coordinator's self-identity set used to scope the remote drain) so the
|
|
7179
|
-
// event routes back here. Preserve any caller-supplied coordinatorDaemonId.
|
|
7180
|
-
//
|
|
7181
|
-
// _meshDirectDispatch prevents re-forwarding (and P2P self-dial) once the call
|
|
7182
|
-
// has landed on the owning daemon — that daemon then executes locally even if
|
|
7183
|
-
// the stored daemonId uses a legacy form that doesn't match its own identity.
|
|
7184
|
-
{
|
|
7185
|
-
const meshRecordForForward = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7186
|
-
const forwardNode = meshRecordForForward?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7187
|
-
const nodeDaemonId = typeof forwardNode?.daemonId === 'string' ? forwardNode.daemonId.trim() : undefined;
|
|
7188
|
-
const selfDaemonId = this.deps.statusInstanceId;
|
|
7189
|
-
// daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core
|
|
7190
|
-
// is local — execute locally instead of forwarding. Equivalent → local.
|
|
7191
|
-
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
7192
|
-
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
7193
|
-
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
7194
|
-
? args.coordinatorDaemonId.trim()
|
|
7195
|
-
: undefined;
|
|
7196
|
-
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'refine_mesh_node', {
|
|
7197
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7198
|
-
coordinatorDaemonId: callerCoordinatorDaemonId || selfDaemonId,
|
|
7199
|
-
_meshDirectDispatch: true,
|
|
7200
|
-
});
|
|
7201
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7202
|
-
}
|
|
7203
|
-
}
|
|
7204
|
-
|
|
7205
|
-
// Dry-run (plan-only) is the default and stays synchronous: it does no
|
|
7206
|
-
// validation/merge/push and returns the plan instantly. Only execute=true
|
|
7207
|
-
// (and not dry_run) goes through the async refine job that actually
|
|
7208
|
-
// validates → merges → pushes → cleans up. Mirrors the
|
|
7209
|
-
// batch_refine_mesh_nodes / fast_forward_mesh_node dry_run/execute contract.
|
|
7210
|
-
const isDryRun = args?.dryRun !== false && args?.execute !== true;
|
|
7211
|
-
if (isDryRun) {
|
|
7212
|
-
// preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
|
|
7213
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7214
|
-
const mesh = meshRecord?.mesh;
|
|
7215
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7216
|
-
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
7217
|
-
return {
|
|
7218
|
-
success: true,
|
|
7219
|
-
dryRun: true,
|
|
7220
|
-
nodeId,
|
|
7221
|
-
workspace: node.workspace,
|
|
7222
|
-
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
7223
|
-
mergeWillRun: false,
|
|
7224
|
-
cleanupWillRun: false,
|
|
7225
|
-
hint: 'Dry-run only — no merge/push/cleanup performed. Re-invoke with execute:true to converge this node.',
|
|
7226
|
-
};
|
|
7227
|
-
}
|
|
7228
|
-
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
7229
|
-
}
|
|
7230
|
-
|
|
7231
|
-
case 'batch_refine_mesh_nodes': {
|
|
7232
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7233
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7234
|
-
const requestedNodeIds = Array.isArray(args?.nodeIds)
|
|
7235
|
-
? (args.nodeIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
|
|
7236
|
-
: undefined;
|
|
7237
|
-
// Dry-run (plan-only) stays synchronous: it does no validation/merge and
|
|
7238
|
-
// returns instantly. Execute goes through the async batch job — immediate
|
|
7239
|
-
// {async:true, status:'accepted'} + background convergence + terminal event,
|
|
7240
|
-
// matching the single-node refine_mesh_node contract so long validation
|
|
7241
|
-
// suites can't time out the IPC and strand the coordinator.
|
|
7242
|
-
const isDryRun = args?.dryRun !== false && args?.execute !== true;
|
|
7243
|
-
if (isDryRun) return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
|
|
7244
|
-
return this.startMeshRefineBatchJob(meshId, requestedNodeIds, args);
|
|
7245
|
-
}
|
|
7246
|
-
|
|
7247
|
-
case 'remove_mesh_node': {
|
|
7248
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7249
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7250
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7251
|
-
try {
|
|
7252
|
-
// preferInline so removal can resolve inline-cache-only clone worktree nodes.
|
|
7253
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7254
|
-
const mesh = meshRecord?.mesh;
|
|
7255
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7256
|
-
|
|
7257
|
-
// Guard: refuse to remove the coordinator's OWN local base node
|
|
7258
|
-
// (same machine, NOT a worktree). Removing it breaks live mesh
|
|
7259
|
-
// membership — the coordinator can no longer be reached and has
|
|
7260
|
-
// to be restarted. Worktree clones are always safe to remove;
|
|
7261
|
-
// only the non-worktree node bound to this daemon is protected.
|
|
7262
|
-
// An explicit force:true overrides for intentional mesh teardown.
|
|
7263
|
-
if (node && !args?._meshDirectDispatch && node.isLocalWorktree !== true && args?.force !== true) {
|
|
7264
|
-
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : '';
|
|
7265
|
-
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>) || '';
|
|
7266
|
-
const selfDaemonId = this.deps.statusInstanceId || '';
|
|
7267
|
-
const selfMachineId = (() => { try { return loadConfig().machineId || ''; } catch { return ''; } })();
|
|
7268
|
-
const isCoordinatorBaseNode =
|
|
7269
|
-
(!!selfDaemonId && (nodeDaemonId === selfDaemonId || nodeMachineId === selfDaemonId))
|
|
7270
|
-
|| (!!selfMachineId && (nodeDaemonId === selfMachineId || nodeMachineId === selfMachineId));
|
|
7271
|
-
if (isCoordinatorBaseNode) {
|
|
7272
|
-
return {
|
|
7273
|
-
success: false,
|
|
7274
|
-
removed: false,
|
|
7275
|
-
code: 'mesh_remove_coordinator_base_node_protected',
|
|
7276
|
-
error: `Refusing to remove the coordinator's own base node '${typeof node.workspace === 'string' ? node.workspace : nodeId}'. `
|
|
7277
|
-
+ `It is the local non-worktree node bound to this coordinator daemon; removing it breaks live mesh membership and forces a restart.`,
|
|
7278
|
-
recoveryHint: 'Remove worktree clone nodes instead, or pass force:true only if you are intentionally tearing down this mesh and accept that the coordinator must be re-registered/restarted.',
|
|
7279
|
-
};
|
|
7280
|
-
}
|
|
7281
|
-
}
|
|
7282
|
-
|
|
7283
|
-
const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
|
|
7284
|
-
args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
|
|
7285
|
-
);
|
|
7286
|
-
// Explicit sessionIds (e.g. supplied by refine auto-cleanup) bypass the
|
|
7287
|
-
// workspace-only-match guard so a delegate session that lacks a
|
|
7288
|
-
// meta.meshNodeId binding can still be stopped/deleted.
|
|
7289
|
-
const explicitSessionIds = Array.isArray(args?.sessionIds)
|
|
7290
|
-
? (args.sessionIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
|
|
7291
|
-
: undefined;
|
|
7292
|
-
let sessionCleanup: Record<string, unknown> | undefined;
|
|
7293
|
-
if (node && sessionCleanupMode !== 'preserve') {
|
|
7294
|
-
sessionCleanup = await this.cleanupMeshSessions({
|
|
7295
|
-
meshId,
|
|
7296
|
-
nodeId,
|
|
7297
|
-
node,
|
|
7298
|
-
mode: sessionCleanupMode,
|
|
7299
|
-
...(explicitSessionIds && explicitSessionIds.length > 0 ? { sessionIds: explicitSessionIds } : {}),
|
|
7300
|
-
source: 'mesh_remove_node',
|
|
7301
|
-
});
|
|
7302
|
-
if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
|
|
7303
|
-
}
|
|
7304
|
-
|
|
7305
|
-
let worktreeCleanup: Record<string, unknown> | undefined;
|
|
7306
|
-
if (node?.isLocalWorktree) {
|
|
7307
|
-
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
7308
|
-
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
7309
|
-
// clean up locally, do not forward. Equivalent → local.
|
|
7310
|
-
const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
7311
|
-
&& !args?._meshDirectDispatch;
|
|
7312
|
-
if (isRemoteWorktree) {
|
|
7313
|
-
// Worktree lives on a different machine — ask that daemon to clean it up.
|
|
7314
|
-
// _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
|
|
7315
|
-
const forwarded = await this.deps.dispatchMeshCommand!(nodeDaemonId!, 'remove_mesh_node', {
|
|
7316
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7317
|
-
_meshDirectDispatch: true,
|
|
7318
|
-
});
|
|
7319
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7320
|
-
}
|
|
7321
|
-
const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId, force: args?.force === true });
|
|
7322
|
-
// De-gating: membership removal is NOT gated on the worktree
|
|
7323
|
-
// directory actually being deleted. cleanupLocalWorktreeNode now
|
|
7324
|
-
// returns success:true (with a residue flag) whenever the path is
|
|
7325
|
-
// proven managed and the only remaining problem is leftover
|
|
7326
|
-
// directory bytes (e.g. Windows EINVAL). A success:false here means
|
|
7327
|
-
// a genuinely-unsafe condition — missing metadata, a non-managed /
|
|
7328
|
-
// unexpected path, a branch mismatch, a dirty worktree, or an
|
|
7329
|
-
// unverified force fallback — and those still block removal.
|
|
7330
|
-
if (cleanupResult.success === false) {
|
|
7331
|
-
return {
|
|
7332
|
-
success: false,
|
|
7333
|
-
removed: false,
|
|
7334
|
-
code: cleanupResult.code,
|
|
7335
|
-
error: cleanupResult.error,
|
|
7336
|
-
recoveryHint: cleanupResult.recoveryHint,
|
|
7337
|
-
...(sessionCleanup ? { sessionCleanup } : {}),
|
|
7338
|
-
worktreeCleanup: cleanupResult,
|
|
7339
|
-
};
|
|
7340
|
-
}
|
|
7341
|
-
worktreeCleanup = cleanupResult;
|
|
7342
|
-
}
|
|
7343
|
-
|
|
7344
|
-
let removed = false;
|
|
7345
|
-
if (meshRecord?.inline) {
|
|
7346
|
-
removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
|
|
7347
|
-
// Inline meshes share the same aggregate snapshot cache as
|
|
7348
|
-
// local-config meshes; without this bust the removed node
|
|
7349
|
-
// keeps showing up in the dashboard graph until the cache
|
|
7350
|
-
// ages out on its own.
|
|
7351
|
-
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
7352
|
-
// Node was already absent from the inline mesh (e.g. removed by a
|
|
7353
|
-
// prior refine cleanup). Treat as removed so caller gets removed:true.
|
|
7354
|
-
if (!removed && !node) removed = true;
|
|
7355
|
-
} else {
|
|
7356
|
-
const { removeNode } = await import('../config/mesh-config.js');
|
|
7357
|
-
removed = removeNode(meshId, nodeId);
|
|
7358
|
-
// Node already absent from config (e.g. removed by a prior refine
|
|
7359
|
-
// cleanup after a successful Refinery merge). Treat as removed so
|
|
7360
|
-
// the response is accurate.
|
|
7361
|
-
if (!removed && !node) removed = true;
|
|
7362
|
-
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
7363
|
-
}
|
|
7364
|
-
|
|
7365
|
-
// Record in task ledger
|
|
7366
|
-
if (removed) {
|
|
7367
|
-
try {
|
|
7368
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
7369
|
-
appendLedgerEntry(meshId, {
|
|
7370
|
-
kind: 'node_removed',
|
|
7371
|
-
nodeId,
|
|
7372
|
-
payload: {
|
|
7373
|
-
worktree: !!node?.isLocalWorktree,
|
|
7374
|
-
sessionCleanupMode,
|
|
7375
|
-
workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
|
|
7376
|
-
daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
|
|
7377
|
-
worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
|
|
7378
|
-
worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
|
|
7379
|
-
forced: worktreeCleanup?.forced === true ? true : undefined,
|
|
7380
|
-
forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
|
|
7381
|
-
},
|
|
7382
|
-
});
|
|
7383
|
-
} catch { /* ledger append is best-effort */ }
|
|
7384
|
-
}
|
|
7385
|
-
|
|
7386
|
-
// Surface leftover-directory residue at the top level so callers
|
|
7387
|
-
// see the node was dropped from the mesh even though the worktree
|
|
7388
|
-
// directory could not be fully removed (best-effort, non-gating).
|
|
7389
|
-
const residueWarning = worktreeCleanup?.residue === true && typeof worktreeCleanup?.residueWarning === 'string'
|
|
7390
|
-
? worktreeCleanup.residueWarning
|
|
7391
|
-
: undefined;
|
|
7392
|
-
return {
|
|
7393
|
-
success: true,
|
|
7394
|
-
removed,
|
|
7395
|
-
...(residueWarning ? { residueWarning } : {}),
|
|
7396
|
-
...(sessionCleanup ? { sessionCleanup } : {}),
|
|
7397
|
-
...(worktreeCleanup ? { worktreeCleanup } : {}),
|
|
7398
|
-
};
|
|
7399
|
-
} catch (e: any) {
|
|
7400
|
-
return { success: false, error: e.message };
|
|
7401
|
-
}
|
|
7402
|
-
}
|
|
7403
|
-
|
|
7404
|
-
case 'clone_mesh_node': {
|
|
7405
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7406
|
-
const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
|
|
7407
|
-
const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
|
|
7408
|
-
const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
|
|
7409
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7410
|
-
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
7411
|
-
if (!branch) return { success: false, error: 'branch required' };
|
|
7412
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
7413
|
-
if (ownerFailure) return ownerFailure;
|
|
7414
|
-
|
|
7415
|
-
try {
|
|
7416
|
-
// Resolve with preferInline so the clone writes the new node into the
|
|
7417
|
-
// same representation that get_mesh reads back. The MCP coordinator
|
|
7418
|
-
// passes inlineMesh on every mesh command, so when it owns an inline
|
|
7419
|
-
// mesh the membership read path (get_mesh, preferInline: true) returns
|
|
7420
|
-
// the inline cache. Without preferInline here, clone could resolve to a
|
|
7421
|
-
// local-config mesh and write the node only to config — leaving the
|
|
7422
|
-
// inline cache (and therefore get_mesh / refreshMeshFromDaemon) without
|
|
7423
|
-
// the node, so the new worktree node is never visible in live mesh
|
|
7424
|
-
// membership even though worktree_bootstrap_complete fires.
|
|
7425
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7426
|
-
const mesh = meshRecord?.mesh;
|
|
7427
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7428
|
-
|
|
7429
|
-
const sourceNode = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, sourceNodeId));
|
|
7430
|
-
if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
|
|
7431
|
-
|
|
7432
|
-
// Forward to the source node's daemon if it's on a different machine.
|
|
7433
|
-
// _meshDirectDispatch prevents infinite re-forwarding when the stored daemonId
|
|
7434
|
-
// uses a legacy format that doesn't match the receiving daemon's statusInstanceId.
|
|
7435
|
-
const sourceDaemonId = typeof sourceNode.daemonId === 'string' ? sourceNode.daemonId.trim() : undefined;
|
|
7436
|
-
// daemonIdsEquivalent: an equivalent-form source daemonId is this machine —
|
|
7437
|
-
// clone locally, do not forward. Equivalent → local.
|
|
7438
|
-
if (sourceDaemonId && !daemonIdsEquivalent(sourceDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
7439
|
-
&& !args?._meshDirectDispatch) {
|
|
7440
|
-
const forwarded = await this.deps.dispatchMeshCommand(sourceDaemonId, 'clone_mesh_node', {
|
|
7441
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7442
|
-
_meshDirectDispatch: true,
|
|
7443
|
-
});
|
|
7444
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7445
|
-
}
|
|
7446
|
-
|
|
7447
|
-
const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
|
|
7448
|
-
const { createWorktree } = await import('../git/git-worktree.js');
|
|
7449
|
-
const result = await createWorktree({
|
|
7450
|
-
repoRoot,
|
|
7451
|
-
branch,
|
|
7452
|
-
baseBranch,
|
|
7453
|
-
meshName: mesh.name,
|
|
7454
|
-
});
|
|
7455
|
-
|
|
7456
|
-
let node: any;
|
|
7457
|
-
if (meshRecord.inline) {
|
|
7458
|
-
const { randomUUID } = await import('crypto');
|
|
7459
|
-
node = {
|
|
7460
|
-
id: `node_${randomUUID().replace(/-/g, '')}`,
|
|
7461
|
-
workspace: result.worktreePath,
|
|
7462
|
-
repoRoot: result.worktreePath,
|
|
7463
|
-
daemonId: sourceNode.daemonId,
|
|
7464
|
-
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
7465
|
-
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
7466
|
-
policy: { ...(sourceNode.policy || {}) },
|
|
7467
|
-
isLocalWorktree: true,
|
|
7468
|
-
worktreeBranch: result.branch,
|
|
7469
|
-
clonedFromNodeId: sourceNodeId,
|
|
7470
|
-
};
|
|
7471
|
-
this.updateInlineMeshNode(meshId, mesh, node);
|
|
7472
|
-
} else {
|
|
7473
|
-
const { addNode } = await import('../config/mesh-config.js');
|
|
7474
|
-
node = addNode(meshId, {
|
|
7475
|
-
workspace: result.worktreePath,
|
|
7476
|
-
repoRoot: result.worktreePath,
|
|
7477
|
-
daemonId: sourceNode.daemonId,
|
|
7478
|
-
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
7479
|
-
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
7480
|
-
isLocalWorktree: true,
|
|
7481
|
-
worktreeBranch: result.branch,
|
|
7482
|
-
clonedFromNodeId: sourceNodeId,
|
|
7483
|
-
policy: { ...(sourceNode.policy || {}) },
|
|
7484
|
-
});
|
|
7485
|
-
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
7486
|
-
// Also reconcile the freshly-registered node into any warmed inline
|
|
7487
|
-
// cache for this mesh. get_mesh (preferInline: true) reads the inline
|
|
7488
|
-
// cache first when one exists; if we only wrote to local config the
|
|
7489
|
-
// node would be invisible to membership reads. updateInlineMeshNode is
|
|
7490
|
-
// a no-op when no inline cache is present.
|
|
7491
|
-
const inlineForReconcile = this.getCachedInlineMesh(meshId);
|
|
7492
|
-
if (inlineForReconcile) this.updateInlineMeshNode(meshId, inlineForReconcile, node);
|
|
7493
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
7494
|
-
}
|
|
7495
|
-
|
|
7496
|
-
const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
7497
|
-
node.worktreeBootstrap = bootstrapState;
|
|
7498
|
-
if (meshRecord.inline) {
|
|
7499
|
-
this.updateInlineMeshNode(meshId, mesh, node);
|
|
7500
|
-
return;
|
|
7501
|
-
}
|
|
7502
|
-
try {
|
|
7503
|
-
const { updateNode } = await import('../config/mesh-config.js');
|
|
7504
|
-
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
7505
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
7506
|
-
} catch { /* bootstrap status persistence is best-effort */ }
|
|
7507
|
-
};
|
|
7508
|
-
|
|
7509
|
-
const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
7510
|
-
try {
|
|
7511
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
7512
|
-
appendLedgerEntry(meshId, {
|
|
7513
|
-
kind: 'node_cloned',
|
|
7514
|
-
nodeId: node.id,
|
|
7515
|
-
payload: {
|
|
7516
|
-
sourceNodeId,
|
|
7517
|
-
branch: result.branch,
|
|
7518
|
-
worktreePath: result.worktreePath,
|
|
7519
|
-
submodulesInitialized: initSubmodules,
|
|
7520
|
-
worktreeBootstrap: {
|
|
7521
|
-
status: bootstrapState.status,
|
|
7522
|
-
required: bootstrapState.required,
|
|
7523
|
-
configSource: bootstrapState.configSource,
|
|
7524
|
-
configSourceType: bootstrapState.configSourceType,
|
|
7525
|
-
lastCommand: bootstrapState.lastCommand,
|
|
7526
|
-
exitCode: bootstrapState.exitCode,
|
|
7527
|
-
},
|
|
7528
|
-
},
|
|
7529
|
-
});
|
|
7530
|
-
} catch { /* ledger append is best-effort */ }
|
|
7531
|
-
};
|
|
7532
|
-
|
|
7533
|
-
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
7534
|
-
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
|
|
7535
|
-
const runningBootstrapState: WorktreeBootstrapState = {
|
|
7536
|
-
status: 'running',
|
|
7537
|
-
required: loadedBootstrap.config?.required !== false,
|
|
7538
|
-
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
7539
|
-
configSourceType: loadedBootstrap.sourceType,
|
|
7540
|
-
startedAt: new Date().toISOString(),
|
|
7541
|
-
};
|
|
7542
|
-
await persistWorktreeSetupState(runningBootstrapState);
|
|
7543
|
-
|
|
7544
|
-
const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
|
|
7545
|
-
let submodulesInitialized = false;
|
|
7546
|
-
if (initSubmodules) {
|
|
7547
|
-
try {
|
|
7548
|
-
const { runGit } = await import('../git/git-executor.js');
|
|
7549
|
-
await runGit(
|
|
7550
|
-
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
7551
|
-
['submodule', 'update', '--init', '--recursive'],
|
|
7552
|
-
{ timeoutMs: 120000 },
|
|
7553
|
-
);
|
|
7554
|
-
submodulesInitialized = true;
|
|
7555
|
-
|
|
7556
|
-
// Sync oss submodule to source node HEAD (best-effort)
|
|
7557
|
-
const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
|
|
7558
|
-
if (sourceWorkspace) {
|
|
7559
|
-
try {
|
|
7560
|
-
const { runGit: rg } = await import('../git/git-executor.js');
|
|
7561
|
-
const sourceCtx = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
|
|
7562
|
-
const worktreeCtx = { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true };
|
|
7563
|
-
|
|
7564
|
-
// Read source node's oss submodule SHA
|
|
7565
|
-
const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', 'oss'], { timeoutMs: 10000 });
|
|
7566
|
-
const sourceStatusLine = (typeof sourceStatusOut === 'string' ? sourceStatusOut : (sourceStatusOut as any)?.stdout ?? '').trim();
|
|
7567
|
-
const sourceShaMatch = sourceStatusLine.match(/^[+\- ]?([0-9a-f]{40})/);
|
|
7568
|
-
const sourceSha = sourceShaMatch?.[1];
|
|
7569
|
-
|
|
7570
|
-
if (sourceSha) {
|
|
7571
|
-
// Read worktree's current oss HEAD
|
|
7572
|
-
const ossCtx = { workspace: `${result.worktreePath}/oss`, repoRoot: `${result.worktreePath}/oss`, isGitRepo: true };
|
|
7573
|
-
const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
|
|
7574
|
-
const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
|
|
7575
|
-
|
|
7576
|
-
if (worktreeOssSha !== sourceSha) {
|
|
7577
|
-
// Fetch target SHA from source node's oss directory
|
|
7578
|
-
await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
|
|
7579
|
-
await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
|
|
7580
|
-
await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
|
|
7581
|
-
await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
|
|
7582
|
-
console.log(`[mesh] Synced oss submodule to source HEAD ${sourceSha.slice(0, 8)} in worktree`);
|
|
7583
|
-
}
|
|
7584
|
-
}
|
|
7585
|
-
} catch (ossErr: any) {
|
|
7586
|
-
console.warn('[mesh] oss submodule sync to source HEAD failed (best-effort):', ossErr.message);
|
|
7587
|
-
}
|
|
7588
|
-
}
|
|
7589
|
-
} catch (subErr: any) {
|
|
7590
|
-
// Submodule init is best-effort; don't fail the clone
|
|
7591
|
-
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
7592
|
-
}
|
|
7593
|
-
}
|
|
7594
|
-
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
7595
|
-
await persistWorktreeSetupState(bootstrapState);
|
|
7596
|
-
await appendCloneLedger(submodulesInitialized, bootstrapState);
|
|
7597
|
-
return { submodulesInitialized, bootstrapState };
|
|
7598
|
-
};
|
|
7599
|
-
|
|
7600
|
-
const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
|
|
7601
|
-
const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
|
|
7602
|
-
? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
|
|
7603
|
-
: 8000;
|
|
7604
|
-
const setupPromise = finishWorktreeSetup();
|
|
7605
|
-
const setupResult = await Promise.race([
|
|
7606
|
-
setupPromise.then((value) => ({ completed: true as const, value })),
|
|
7607
|
-
new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
|
|
7608
|
-
]);
|
|
7609
|
-
|
|
7610
|
-
const emitBootstrapEvent = (eventStatus: 'bootstrap_complete' | 'bootstrap_failed', bootstrapState: WorktreeBootstrapState, startedAtMs: number, extraPayload?: Record<string, unknown>): void => {
|
|
7611
|
-
try {
|
|
7612
|
-
const durationMs = Date.now() - startedAtMs;
|
|
7613
|
-
const event = `worktree_${eventStatus}` as const;
|
|
7614
|
-
const metadataEvent = {
|
|
7615
|
-
source: 'clone_mesh_node_bootstrap',
|
|
7616
|
-
nodeId: node.id,
|
|
7617
|
-
status: eventStatus,
|
|
7618
|
-
worktreePath: result.worktreePath,
|
|
7619
|
-
durationMs,
|
|
7620
|
-
bootstrapStatus: bootstrapState.status,
|
|
7621
|
-
...(bootstrapState.error ? { error: bootstrapState.error } : {}),
|
|
7622
|
-
...(bootstrapState.exitCode !== undefined ? { exitCode: bootstrapState.exitCode } : {}),
|
|
7623
|
-
...(extraPayload || {}),
|
|
7624
|
-
};
|
|
7625
|
-
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
7626
|
-
const forwarded = handleMeshForwardEvent(
|
|
7627
|
-
{ instanceManager: this.deps.instanceManager } as any,
|
|
7628
|
-
{ event, meshId, nodeId: node.id, workspace: result.worktreePath, metadataEvent },
|
|
7629
|
-
);
|
|
7630
|
-
if (forwarded?.success === true) return;
|
|
7631
|
-
}
|
|
7632
|
-
queuePendingMeshCoordinatorEvent({
|
|
7633
|
-
event,
|
|
7634
|
-
meshId,
|
|
7635
|
-
nodeLabel: node.id,
|
|
7636
|
-
nodeId: node.id,
|
|
7637
|
-
workspace: result.worktreePath,
|
|
7638
|
-
metadataEvent,
|
|
7639
|
-
queuedAt: Date.now(),
|
|
7640
|
-
});
|
|
7641
|
-
} catch { /* event emission is best-effort */ }
|
|
7642
|
-
};
|
|
7643
|
-
|
|
7644
|
-
const bootstrapStartedMs = Date.now();
|
|
7645
|
-
|
|
7646
|
-
if (!setupResult.completed) {
|
|
7647
|
-
setupPromise
|
|
7648
|
-
.then(({ bootstrapState }) => {
|
|
7649
|
-
emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
|
|
7650
|
-
})
|
|
7651
|
-
.catch((error: any) => {
|
|
7652
|
-
const failedState: WorktreeBootstrapState = {
|
|
7653
|
-
...runningBootstrapState,
|
|
7654
|
-
status: 'failed',
|
|
7655
|
-
completedAt: new Date().toISOString(),
|
|
7656
|
-
error: error?.message || String(error),
|
|
7657
|
-
};
|
|
7658
|
-
void persistWorktreeSetupState(failedState);
|
|
7659
|
-
void appendCloneLedger(false, failedState);
|
|
7660
|
-
emitBootstrapEvent('bootstrap_failed', failedState, bootstrapStartedMs, { error: error?.message || String(error) });
|
|
7661
|
-
});
|
|
7662
|
-
return {
|
|
7663
|
-
success: true,
|
|
7664
|
-
async: true,
|
|
7665
|
-
status: 'accepted',
|
|
7666
|
-
node,
|
|
7667
|
-
worktreePath: result.worktreePath,
|
|
7668
|
-
branch: result.branch,
|
|
7669
|
-
worktreeBootstrap: runningBootstrapState,
|
|
7670
|
-
worktreeSetup: {
|
|
7671
|
-
status: 'running',
|
|
7672
|
-
setupWaitMs,
|
|
7673
|
-
message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
|
|
7674
|
-
},
|
|
7675
|
-
};
|
|
7676
|
-
}
|
|
7677
|
-
|
|
7678
|
-
const { submodulesInitialized, bootstrapState } = setupResult.value;
|
|
7679
|
-
emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
|
|
7680
|
-
return {
|
|
7681
|
-
success: true,
|
|
7682
|
-
node,
|
|
7683
|
-
worktreePath: result.worktreePath,
|
|
7684
|
-
branch: result.branch,
|
|
7685
|
-
submodulesInitialized,
|
|
7686
|
-
worktreeBootstrap: bootstrapState,
|
|
7687
|
-
};
|
|
7688
|
-
} catch (e: any) {
|
|
7689
|
-
return { success: false, error: e.message };
|
|
7690
|
-
}
|
|
7691
|
-
}
|
|
7692
|
-
case 'retry_mesh_node_bootstrap': {
|
|
7693
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7694
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7695
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7696
|
-
if (!nodeId) return { success: false, error: 'nodeId required' };
|
|
7697
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'bootstrap retry');
|
|
7698
|
-
if (ownerFailure) return ownerFailure;
|
|
7699
|
-
|
|
7700
|
-
try {
|
|
7701
|
-
// preferInline so bootstrap-retry can resolve inline-cache-only clone worktree nodes.
|
|
7702
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7703
|
-
const mesh = meshRecord?.mesh;
|
|
7704
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7705
|
-
|
|
7706
|
-
const node = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7707
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
7708
|
-
if (!node.isLocalWorktree) return { success: false, error: 'Node is not a local worktree node' };
|
|
7709
|
-
|
|
7710
|
-
// Bootstrap runs scripts in the worktree path — forward to the node's daemon if remote.
|
|
7711
|
-
// _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
|
|
7712
|
-
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
7713
|
-
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
7714
|
-
// bootstrap locally, do not forward. Equivalent → local.
|
|
7715
|
-
if (nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
7716
|
-
&& !args?._meshDirectDispatch) {
|
|
7717
|
-
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId, 'retry_mesh_node_bootstrap', {
|
|
7718
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7719
|
-
_meshDirectDispatch: true,
|
|
7720
|
-
});
|
|
7721
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7722
|
-
}
|
|
7723
|
-
|
|
7724
|
-
const currentBootstrap = node.worktreeBootstrap as WorktreeBootstrapState | undefined;
|
|
7725
|
-
if (currentBootstrap?.status === 'running') {
|
|
7726
|
-
return { success: false, error: 'Bootstrap is already running for this node' };
|
|
7727
|
-
}
|
|
7728
|
-
|
|
7729
|
-
const worktreePath: string = node.workspace || node.repoRoot;
|
|
7730
|
-
if (!worktreePath) return { success: false, error: 'Node has no workspace path' };
|
|
7731
|
-
|
|
7732
|
-
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, worktreePath);
|
|
7733
|
-
const runningState: WorktreeBootstrapState = {
|
|
7734
|
-
status: 'running',
|
|
7735
|
-
required: loadedBootstrap.config?.required !== false,
|
|
7736
|
-
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
7737
|
-
configSourceType: loadedBootstrap.sourceType,
|
|
7738
|
-
startedAt: new Date().toISOString(),
|
|
7739
|
-
};
|
|
7740
|
-
|
|
7741
|
-
const persistState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
7742
|
-
node.worktreeBootstrap = bootstrapState;
|
|
7743
|
-
if (meshRecord.inline) {
|
|
7744
|
-
this.updateInlineMeshNode(meshId, mesh, node);
|
|
7745
|
-
return;
|
|
7746
|
-
}
|
|
7747
|
-
try {
|
|
7748
|
-
const { updateNode } = await import('../config/mesh-config.js');
|
|
7749
|
-
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
7750
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
7751
|
-
} catch { /* best-effort */ }
|
|
7752
|
-
};
|
|
7753
|
-
|
|
7754
|
-
await persistState(runningState);
|
|
7755
|
-
const bootstrapState = await runMeshWorktreeBootstrap(mesh, worktreePath);
|
|
7756
|
-
await persistState(bootstrapState);
|
|
7757
|
-
|
|
7758
|
-
return { success: true, bootstrapState };
|
|
7759
|
-
} catch (e: any) {
|
|
7760
|
-
return { success: false, error: e.message };
|
|
7761
|
-
}
|
|
7762
|
-
}
|
|
7763
|
-
|
|
7764
|
-
case 'trigger_mesh_queue': {
|
|
7765
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7766
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7767
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
7768
|
-
if (ownerFailure) return ownerFailure;
|
|
7769
|
-
try {
|
|
7770
|
-
const { triggerMeshQueue, tryAssignQueueTask } = await import('../mesh/mesh-events.js');
|
|
7771
|
-
|
|
7772
|
-
// Bug A fix: when preferredNodeId is provided, attempt to claim a pending
|
|
7773
|
-
// task for the preferred node's idle session first, before the general
|
|
7774
|
-
// round-robin trigger picks a different node.
|
|
7775
|
-
const preferredNodeId = typeof args?.preferredNodeId === 'string' ? args.preferredNodeId.trim() : '';
|
|
7776
|
-
if (preferredNodeId) {
|
|
7777
|
-
const cliInstances = this.deps.instanceManager.getByCategory('cli');
|
|
7778
|
-
// Sort: preferred node's sessions first, others after
|
|
7779
|
-
const sorted = [...cliInstances].sort((a, b) => {
|
|
7780
|
-
const aSettings = a.getState().settings as Record<string, unknown> || {};
|
|
7781
|
-
const bSettings = b.getState().settings as Record<string, unknown> || {};
|
|
7782
|
-
const aNode = readStringValue(aSettings.meshNodeId, aSettings.nodeId);
|
|
7783
|
-
const bNode = readStringValue(bSettings.meshNodeId, bSettings.nodeId);
|
|
7784
|
-
return (aNode === preferredNodeId ? -1 : 0) - (bNode === preferredNodeId ? -1 : 0);
|
|
7785
|
-
});
|
|
7786
|
-
for (const inst of sorted) {
|
|
7787
|
-
const state = inst.getState();
|
|
7788
|
-
const settings = state.settings as Record<string, unknown> || {};
|
|
7789
|
-
const nodeId = readStringValue(settings.meshNodeId, settings.nodeId);
|
|
7790
|
-
if (!nodeId || nodeId !== preferredNodeId) continue;
|
|
7791
|
-
const meshNodeFor = readStringValue(settings.meshNodeFor);
|
|
7792
|
-
if (meshNodeFor !== meshId) continue;
|
|
7793
|
-
const status = (readStringValue(state.status) || '').toLowerCase();
|
|
7794
|
-
if (status !== 'idle') continue;
|
|
7795
|
-
const sessionId = typeof state.instanceId === 'string' ? state.instanceId : '';
|
|
7796
|
-
const providerType = readStringValue(state.type, settings.providerType) || '';
|
|
7797
|
-
if (sessionId && providerType) {
|
|
7798
|
-
tryAssignQueueTask(this.deps as any, meshId, nodeId, sessionId, providerType);
|
|
7799
|
-
break;
|
|
7800
|
-
}
|
|
7801
|
-
}
|
|
7802
|
-
}
|
|
7803
|
-
|
|
7804
|
-
const trigger = await triggerMeshQueue(this.deps as any, meshId);
|
|
7805
|
-
return { success: true, trigger };
|
|
7806
|
-
} catch (e: any) {
|
|
7807
|
-
return { success: false, error: e.message };
|
|
7808
|
-
}
|
|
7809
|
-
}
|
|
7810
|
-
|
|
7811
|
-
// ─── Mesh Coordinator Launch ───
|
|
7812
|
-
case 'launch_mesh_coordinator': {
|
|
7813
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7814
|
-
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
7815
|
-
// Optional per-launch system-prompt addition. Dashboard or API
|
|
7816
|
-
// callers (e.g. when spawning a mesh-node-specific coordinator)
|
|
7817
|
-
// can pass extra context that gets appended to the rendered
|
|
7818
|
-
// default prompt under the "## Additional Context" section.
|
|
7819
|
-
// Going through buildCoordinatorSystemPrompt's userInstruction
|
|
7820
|
-
// means user-level override files (~/.adhdev/coordinator-prompts)
|
|
7821
|
-
// and this per-launch addition compose cleanly: an override
|
|
7822
|
-
// wins outright, but if there's no override, the default
|
|
7823
|
-
// prompt + the optional append.md file + this extra context
|
|
7824
|
-
// all stack in declared order.
|
|
7825
|
-
const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
|
|
7826
|
-
? args.extraSystemPrompt.trim()
|
|
7827
|
-
: '';
|
|
7828
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7829
|
-
|
|
7830
|
-
try {
|
|
7831
|
-
const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
|
|
7832
|
-
const { buildMissionPromptSection } = await import('../mesh/mesh-missions.js');
|
|
7833
|
-
// M3-3: inject the active mission summary into the coordinator prompt.
|
|
7834
|
-
// Best-effort — a store failure must not block coordinator launch.
|
|
7835
|
-
const buildMissionSectionBestEffort = (id: string): string => {
|
|
7836
|
-
try { return buildMissionPromptSection(id); } catch { return ''; }
|
|
7837
|
-
};
|
|
7838
|
-
|
|
7839
|
-
// Support inline mesh data from cloud (bypasses local meshes.json lookup)
|
|
7840
|
-
let mesh: any;
|
|
7841
|
-
if (args?.inlineMesh && typeof args.inlineMesh === 'object') {
|
|
7842
|
-
mesh = args.inlineMesh;
|
|
7843
|
-
// Cache cloud mesh so the MCP server can retrieve it via get_mesh
|
|
7844
|
-
this.inlineMeshCache.set(meshId, mesh);
|
|
7845
|
-
} else {
|
|
7846
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
7847
|
-
mesh = getMesh(meshId);
|
|
7848
|
-
}
|
|
7849
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7850
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
7851
|
-
if (!meshHost.canOwnCoordinator) {
|
|
7852
|
-
return {
|
|
7853
|
-
success: false,
|
|
7854
|
-
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
7855
|
-
meshId,
|
|
7856
|
-
cliType,
|
|
7857
|
-
};
|
|
7858
|
-
}
|
|
7859
|
-
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
7860
|
-
|
|
7861
|
-
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
7862
|
-
? args.coordinatorNodeId.trim()
|
|
7863
|
-
: '';
|
|
7864
|
-
const preferredCoordinatorNodeId = requestedCoordinatorNodeId
|
|
7865
|
-
|| (typeof mesh.coordinator?.preferredNodeId === 'string' ? mesh.coordinator.preferredNodeId.trim() : '');
|
|
7866
|
-
const coordinatorNode = preferredCoordinatorNodeId
|
|
7867
|
-
? mesh.nodes.find((node: any) => node?.id === preferredCoordinatorNodeId || node?.nodeId === preferredCoordinatorNodeId)
|
|
7868
|
-
: mesh.nodes[0];
|
|
7869
|
-
if (!coordinatorNode) {
|
|
7870
|
-
return {
|
|
7871
|
-
success: false,
|
|
7872
|
-
code: 'mesh_coordinator_node_not_found',
|
|
7873
|
-
error: `Coordinator node ${preferredCoordinatorNodeId} was not found in mesh`,
|
|
7874
|
-
meshId,
|
|
7875
|
-
cliType,
|
|
7876
|
-
};
|
|
7877
|
-
}
|
|
7878
|
-
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
7879
|
-
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
7880
|
-
: [];
|
|
7881
|
-
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
7882
|
-
const workspace = readLiveMeshNodeWorkspace({
|
|
7883
|
-
meshId,
|
|
7884
|
-
nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || ''),
|
|
7885
|
-
liveSessionRecords: liveMeshSessions,
|
|
7886
|
-
allowCoordinatorSession: true,
|
|
7887
|
-
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
7888
|
-
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
7889
|
-
if (!cliType) {
|
|
7890
|
-
const resolved = await resolveProviderTypeFromPriority({
|
|
7891
|
-
nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || 'coordinator'),
|
|
7892
|
-
providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
|
|
7893
|
-
providerLoader: this.deps.providerLoader,
|
|
7894
|
-
onStatusChange: this.deps.onStatusChange,
|
|
7895
|
-
});
|
|
7896
|
-
if (!resolved.providerType) {
|
|
7897
|
-
return {
|
|
7898
|
-
success: false,
|
|
7899
|
-
code: 'mesh_coordinator_provider_priority_unusable',
|
|
7900
|
-
error: resolved.error || 'No usable provider found from node providerPriority',
|
|
7901
|
-
meshId,
|
|
7902
|
-
cliType,
|
|
7903
|
-
workspace,
|
|
7904
|
-
};
|
|
7905
|
-
}
|
|
7906
|
-
cliType = resolved.providerType;
|
|
7907
|
-
}
|
|
7908
|
-
const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
|
|
7909
|
-
const coordinatorSetup = resolveMeshCoordinatorSetup({
|
|
7910
|
-
provider: providerMeta,
|
|
7911
|
-
cliType,
|
|
7912
|
-
meshId,
|
|
7913
|
-
workspace,
|
|
7914
|
-
});
|
|
7915
|
-
|
|
7916
|
-
if (coordinatorSetup.kind === 'unsupported') {
|
|
7917
|
-
return {
|
|
7918
|
-
success: false,
|
|
7919
|
-
code: 'mesh_coordinator_unsupported',
|
|
7920
|
-
error: coordinatorSetup.reason,
|
|
7921
|
-
meshId,
|
|
7922
|
-
cliType,
|
|
7923
|
-
workspace,
|
|
7924
|
-
};
|
|
7925
|
-
}
|
|
7926
|
-
|
|
7927
|
-
if (coordinatorSetup.kind === 'manual') {
|
|
7928
|
-
return {
|
|
7929
|
-
success: false,
|
|
7930
|
-
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
7931
|
-
error: coordinatorSetup.instructions,
|
|
7932
|
-
meshId,
|
|
7933
|
-
cliType,
|
|
7934
|
-
workspace,
|
|
7935
|
-
meshCoordinatorSetup: coordinatorSetup,
|
|
7936
|
-
};
|
|
7937
|
-
}
|
|
7938
|
-
|
|
7939
|
-
// ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
|
|
7940
|
-
if (coordinatorSetup.kind === 'cli_command') {
|
|
7941
|
-
// Build coordinator prompt first — fail closed on errors.
|
|
7942
|
-
let cliCmdSystemPrompt = '';
|
|
7943
|
-
try {
|
|
7944
|
-
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
|
|
7945
|
-
} catch (error: any) {
|
|
7946
|
-
const message = error?.message || String(error);
|
|
7947
|
-
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
7948
|
-
return {
|
|
7949
|
-
success: false,
|
|
7950
|
-
code: 'mesh_coordinator_prompt_failed',
|
|
7951
|
-
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
7952
|
-
meshId, cliType, workspace,
|
|
7953
|
-
};
|
|
7954
|
-
}
|
|
7955
|
-
|
|
7956
|
-
// Run the provider's MCP registration command under a
|
|
7957
|
-
// PTY. Some providers (agy, future bubbletea CLIs)
|
|
7958
|
-
// refuse to run without /dev/tty, so pipe-only
|
|
7959
|
-
// execFileSync silently no-ops the registration and
|
|
7960
|
-
// the coordinator ends up without any mcp tools. With
|
|
7961
|
-
// a real PTY the registration goes through and the
|
|
7962
|
-
// exit code tells us whether it actually persisted.
|
|
7963
|
-
let mcpRegistrationOk = false;
|
|
7964
|
-
let mcpRegistrationFailure: {
|
|
7965
|
-
command: string;
|
|
7966
|
-
output: string;
|
|
7967
|
-
exitCode: number | null;
|
|
7968
|
-
signal: number | null;
|
|
7969
|
-
timedOut: boolean;
|
|
7970
|
-
} | null = null;
|
|
7971
|
-
try {
|
|
7972
|
-
const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
|
|
7973
|
-
const registrationPlan = buildMeshCoordinatorRegistrationPlan(
|
|
7974
|
-
cliType,
|
|
7975
|
-
coordinatorSetup.serverName,
|
|
7976
|
-
coordinatorSetup.command,
|
|
7977
|
-
);
|
|
7978
|
-
for (const step of registrationPlan) {
|
|
7979
|
-
const renderedCommand = [step.command, ...step.args].join(' ');
|
|
7980
|
-
LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
|
|
7981
|
-
const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
|
|
7982
|
-
if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
|
|
7983
|
-
if (step.required) mcpRegistrationOk = true;
|
|
7984
|
-
continue;
|
|
7985
|
-
}
|
|
7986
|
-
LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
|
|
7987
|
-
if (step.required) {
|
|
7988
|
-
mcpRegistrationFailure = {
|
|
7989
|
-
command: renderedCommand,
|
|
7990
|
-
output: ptyResult.output.slice(-2000),
|
|
7991
|
-
exitCode: ptyResult.exitCode,
|
|
7992
|
-
signal: ptyResult.signal,
|
|
7993
|
-
timedOut: ptyResult.timedOut,
|
|
7994
|
-
};
|
|
7995
|
-
break;
|
|
7996
|
-
}
|
|
7997
|
-
}
|
|
7998
|
-
} catch (error: any) {
|
|
7999
|
-
LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
|
|
8000
|
-
mcpRegistrationFailure = {
|
|
8001
|
-
command: coordinatorSetup.command,
|
|
8002
|
-
output: error?.message || String(error),
|
|
8003
|
-
exitCode: null,
|
|
8004
|
-
signal: null,
|
|
8005
|
-
timedOut: false,
|
|
8006
|
-
};
|
|
8007
|
-
}
|
|
8008
|
-
|
|
8009
|
-
if (!mcpRegistrationOk) {
|
|
8010
|
-
return {
|
|
8011
|
-
success: false,
|
|
8012
|
-
code: 'mesh_coordinator_mcp_registration_failed',
|
|
8013
|
-
error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
|
|
8014
|
-
meshId,
|
|
8015
|
-
cliType,
|
|
8016
|
-
workspace,
|
|
8017
|
-
registration: mcpRegistrationFailure,
|
|
8018
|
-
};
|
|
8019
|
-
}
|
|
8020
|
-
|
|
8021
|
-
// Codex gives repo-local .mcp.json precedence over its
|
|
8022
|
-
// global `codex mcp add` registration. Refresh an
|
|
8023
|
-
// existing ADHDev entry so a stale workspace command
|
|
8024
|
-
// cannot shadow the registration we just verified.
|
|
8025
|
-
if (cliType === 'codex-cli') {
|
|
8026
|
-
const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
|
|
8027
|
-
if (fs.existsSync(repoMcpConfigPath)) {
|
|
8028
|
-
try {
|
|
8029
|
-
const repoMcpConfig = parseMeshCoordinatorMcpConfig(
|
|
8030
|
-
fs.readFileSync(repoMcpConfigPath, 'utf-8'),
|
|
8031
|
-
'claude_mcp_json',
|
|
8032
|
-
);
|
|
8033
|
-
const existingServers = repoMcpConfig.mcpServers;
|
|
8034
|
-
if (
|
|
8035
|
-
existingServers
|
|
8036
|
-
&& typeof existingServers === 'object'
|
|
8037
|
-
&& !Array.isArray(existingServers)
|
|
8038
|
-
&& existingServers[coordinatorSetup.serverName]
|
|
8039
|
-
) {
|
|
8040
|
-
fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
|
|
8041
|
-
...repoMcpConfig,
|
|
8042
|
-
mcpServers: {
|
|
8043
|
-
...existingServers,
|
|
8044
|
-
[coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
|
|
8045
|
-
},
|
|
8046
|
-
}, 'claude_mcp_json'), 'utf-8');
|
|
8047
|
-
LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
|
|
8048
|
-
}
|
|
8049
|
-
} catch (error: any) {
|
|
8050
|
-
return {
|
|
8051
|
-
success: false,
|
|
8052
|
-
code: 'mesh_coordinator_config_write_failed',
|
|
8053
|
-
error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
|
|
8054
|
-
meshId,
|
|
8055
|
-
cliType,
|
|
8056
|
-
workspace,
|
|
8057
|
-
};
|
|
8058
|
-
}
|
|
8059
|
-
}
|
|
8060
|
-
}
|
|
8061
|
-
|
|
8062
|
-
// Inject system prompt declaratively from provider.v1.json.
|
|
8063
|
-
const cliCmdArgs: string[] = [];
|
|
8064
|
-
const cliCmdEnv: Record<string, string> = {};
|
|
8065
|
-
let cliCmdContextFilePath: string | undefined;
|
|
8066
|
-
if (cliCmdSystemPrompt) {
|
|
8067
|
-
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
8068
|
-
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
8069
|
-
cliCmdSystemPrompt,
|
|
8070
|
-
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
8071
|
-
{ cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
|
|
8072
|
-
);
|
|
8073
|
-
cliCmdContextFilePath = effect.contextFilePath;
|
|
8074
|
-
}
|
|
8075
|
-
|
|
8076
|
-
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
8077
|
-
cliType,
|
|
8078
|
-
dir: workspace,
|
|
8079
|
-
cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
|
|
8080
|
-
env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
|
|
8081
|
-
settings: { meshCoordinatorFor: meshId },
|
|
8082
|
-
});
|
|
8083
|
-
|
|
8084
|
-
// R48 inject-then-remove. Spawn was just kicked off above; agy and
|
|
8085
|
-
// gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
|
|
8086
|
-
// cache it for the rest of the session, so we can safely strip
|
|
8087
|
-
// the wrapper from disk shortly after launch. That keeps any
|
|
8088
|
-
// worker session launched into the same workspace later from
|
|
8089
|
-
// picking up our wrapper block.
|
|
8090
|
-
if (cliCmdLaunch?.success && cliCmdContextFilePath) {
|
|
8091
|
-
const stripPath = cliCmdContextFilePath;
|
|
8092
|
-
setTimeout(() => {
|
|
8093
|
-
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
8094
|
-
stripCoordinatorWrapperFile(stripPath);
|
|
8095
|
-
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
|
|
8096
|
-
}).catch(() => { /* best-effort */ });
|
|
8097
|
-
}, 5000);
|
|
8098
|
-
}
|
|
8099
|
-
|
|
8100
|
-
if (!cliCmdLaunch?.success) {
|
|
8101
|
-
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
8102
|
-
}
|
|
8103
|
-
|
|
8104
|
-
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
8105
|
-
const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
|
|
8106
|
-
if (cliCmdSessionId) {
|
|
8107
|
-
const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
8108
|
-
registerMeshCoordinator({
|
|
8109
|
-
meshId,
|
|
8110
|
-
sessionId: cliCmdSessionId,
|
|
8111
|
-
workspace,
|
|
8112
|
-
startedAt: Date.now(),
|
|
8113
|
-
cliType,
|
|
8114
|
-
systemPrompt: cliCmdSystemPrompt || undefined,
|
|
8115
|
-
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
8116
|
-
injection: cliCmdInjectionDecl ? {
|
|
8117
|
-
mode: cliCmdInjectionDecl.mode,
|
|
8118
|
-
target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
|
|
8119
|
-
: 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
|
|
8120
|
-
: 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
|
|
8121
|
-
: undefined,
|
|
8122
|
-
} : undefined,
|
|
8123
|
-
});
|
|
8124
|
-
}
|
|
8125
|
-
try {
|
|
8126
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
8127
|
-
appendLedgerEntry(meshId, {
|
|
8128
|
-
kind: 'coordinator_started',
|
|
8129
|
-
sessionId: cliCmdSessionId,
|
|
8130
|
-
providerType: cliType,
|
|
8131
|
-
payload: { workspace },
|
|
8132
|
-
});
|
|
8133
|
-
} catch { /* best-effort */ }
|
|
8134
|
-
|
|
8135
|
-
return {
|
|
8136
|
-
success: true,
|
|
8137
|
-
meshId,
|
|
8138
|
-
cliType,
|
|
8139
|
-
workspace,
|
|
8140
|
-
sessionId: cliCmdSessionId,
|
|
8141
|
-
mcpRegistered: mcpRegistrationOk,
|
|
8142
|
-
};
|
|
8143
|
-
}
|
|
8144
|
-
|
|
8145
|
-
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
8146
|
-
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
8147
|
-
return {
|
|
8148
|
-
success: false,
|
|
8149
|
-
code: 'mesh_coordinator_unsupported',
|
|
8150
|
-
error: `Unsupported auto-import MCP config format: ${String(coordinatorSetup.configFormat)}`,
|
|
8151
|
-
meshId,
|
|
8152
|
-
cliType,
|
|
8153
|
-
workspace,
|
|
8154
|
-
};
|
|
8155
|
-
}
|
|
8156
|
-
|
|
8157
|
-
// Build the coordinator prompt before mutating workspace config or launching.
|
|
8158
|
-
// Prompt generation failures are configuration/data-shape errors; fail closed so
|
|
8159
|
-
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
8160
|
-
let systemPrompt = '';
|
|
8161
|
-
try {
|
|
8162
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
|
|
8163
|
-
} catch (error: any) {
|
|
8164
|
-
const message = error?.message || String(error);
|
|
8165
|
-
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
8166
|
-
return {
|
|
8167
|
-
success: false,
|
|
8168
|
-
code: 'mesh_coordinator_prompt_failed',
|
|
8169
|
-
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
8170
|
-
meshId,
|
|
8171
|
-
cliType,
|
|
8172
|
-
workspace,
|
|
8173
|
-
};
|
|
8174
|
-
}
|
|
8175
|
-
|
|
8176
|
-
// 1. Write provider-declared MCP config for CLIs that auto-import it.
|
|
8177
|
-
const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
|
|
8178
|
-
const { dirname } = await import('path');
|
|
8179
|
-
const mcpConfigPath = coordinatorSetup.configPath;
|
|
8180
|
-
const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
|
|
8181
|
-
? createHermesManualMeshCoordinatorSetup(meshId, workspace)
|
|
8182
|
-
: null;
|
|
8183
|
-
let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
|
|
8184
|
-
if (hermesManualFallback) {
|
|
8185
|
-
try {
|
|
8186
|
-
hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
|
|
8187
|
-
} catch (error: any) {
|
|
8188
|
-
const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
|
|
8189
|
-
LOG.error('MeshCoordinator', message);
|
|
8190
|
-
return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
|
|
8191
|
-
}
|
|
8192
|
-
}
|
|
8193
|
-
const returnManualFallback = (message: string) => ({
|
|
8194
|
-
success: false,
|
|
8195
|
-
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
8196
|
-
error: message,
|
|
8197
|
-
meshId,
|
|
8198
|
-
cliType,
|
|
8199
|
-
workspace,
|
|
8200
|
-
meshCoordinatorSetup: hermesManualFallback,
|
|
8201
|
-
});
|
|
8202
|
-
|
|
8203
|
-
// Merge ADHDev mesh server into existing config.
|
|
8204
|
-
// Pass full mesh data as env var so the MCP server can bootstrap
|
|
8205
|
-
// without depending on meshes.json or a running daemon.
|
|
8206
|
-
const mcpServerEntry: Record<string, any> = {
|
|
8207
|
-
command: coordinatorSetup.mcpServer.command,
|
|
8208
|
-
args: coordinatorSetup.mcpServer.args,
|
|
8209
|
-
};
|
|
8210
|
-
if (args?.inlineMesh) {
|
|
8211
|
-
const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
|
|
8212
|
-
const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
|
|
8213
|
-
mcpServerEntry.env = {
|
|
8214
|
-
ADHDEV_INLINE_MESH: JSON.stringify(mesh),
|
|
8215
|
-
ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
|
|
8216
|
-
};
|
|
8217
|
-
}
|
|
8218
|
-
|
|
8219
|
-
try {
|
|
8220
|
-
mkdirSync(dirname(mcpConfigPath), { recursive: true });
|
|
8221
|
-
} catch (error: any) {
|
|
8222
|
-
const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
|
|
8223
|
-
LOG.error('MeshCoordinator', message);
|
|
8224
|
-
if (hermesManualFallback) return returnManualFallback(message);
|
|
8225
|
-
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
8226
|
-
}
|
|
8227
|
-
|
|
8228
|
-
// Backup existing MCP config if present.
|
|
8229
|
-
const hadExistingMcpConfig = existsSync(mcpConfigPath);
|
|
8230
|
-
let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
|
|
8231
|
-
if (hermesBaseConfig) {
|
|
8232
|
-
copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
|
|
8233
|
-
}
|
|
8234
|
-
if (hadExistingMcpConfig) {
|
|
8235
|
-
try {
|
|
8236
|
-
const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
8237
|
-
const existingCoordinatorConfig = hermesManualFallback
|
|
8238
|
-
? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
|
|
8239
|
-
: parsedExistingMcpConfig;
|
|
8240
|
-
existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
|
|
8241
|
-
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
8242
|
-
} catch (error: any) {
|
|
8243
|
-
LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
|
|
8244
|
-
return {
|
|
8245
|
-
success: false,
|
|
8246
|
-
code: 'mesh_coordinator_config_parse_failed',
|
|
8247
|
-
error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
|
|
8248
|
-
};
|
|
8249
|
-
}
|
|
8250
|
-
}
|
|
8251
|
-
|
|
8252
|
-
const mcpServersKey = getMcpServersKey(configFormat);
|
|
8253
|
-
const existingServers = existingMcpConfig[mcpServersKey];
|
|
8254
|
-
const mcpConfig = {
|
|
8255
|
-
...existingMcpConfig,
|
|
8256
|
-
[mcpServersKey]: {
|
|
8257
|
-
...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
|
|
8258
|
-
[coordinatorSetup.serverName]: mcpServerEntry,
|
|
8259
|
-
},
|
|
8260
|
-
};
|
|
8261
|
-
try {
|
|
8262
|
-
writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
|
|
8263
|
-
} catch (error: any) {
|
|
8264
|
-
const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
|
|
8265
|
-
LOG.error('MeshCoordinator', message);
|
|
8266
|
-
if (hermesManualFallback) return returnManualFallback(message);
|
|
8267
|
-
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
8268
|
-
}
|
|
8269
|
-
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
8270
|
-
|
|
8271
|
-
const cliArgs: string[] = [];
|
|
8272
|
-
const launchEnv: Record<string, string> = {};
|
|
8273
|
-
if (configFormat === 'hermes_config_yaml') {
|
|
8274
|
-
launchEnv.HERMES_HOME = dirname(mcpConfigPath);
|
|
8275
|
-
launchEnv.HERMES_IGNORE_USER_CONFIG = '';
|
|
8276
|
-
}
|
|
8277
|
-
let autoImportContextFilePath: string | undefined;
|
|
8278
|
-
if (systemPrompt) {
|
|
8279
|
-
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
8280
|
-
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
8281
|
-
systemPrompt,
|
|
8282
|
-
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
8283
|
-
{ cliArgs, launchEnv, workspace, cliType },
|
|
8284
|
-
);
|
|
8285
|
-
autoImportContextFilePath = effect.contextFilePath;
|
|
8286
|
-
}
|
|
8287
|
-
if (cliType === 'claude-cli') {
|
|
8288
|
-
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
8289
|
-
}
|
|
8290
|
-
|
|
8291
|
-
// 3. Launch CLI session via existing cliManager.
|
|
8292
|
-
// Provider-specific prompt injection remains fail-closed: Claude gets
|
|
8293
|
-
// explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
|
|
8294
|
-
const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
8295
|
-
cliType,
|
|
8296
|
-
dir: workspace,
|
|
8297
|
-
cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
|
|
8298
|
-
env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
|
|
8299
|
-
settings: {
|
|
8300
|
-
meshCoordinatorFor: meshId
|
|
8301
|
-
}
|
|
8302
|
-
});
|
|
8303
|
-
|
|
8304
|
-
// R48 inject-then-remove. See the cli_command branch for context;
|
|
8305
|
-
// same idea: strip the wrapper from disk ~5s after launch so the
|
|
8306
|
-
// user's AGENTS.md / GEMINI.md is untouched the moment any
|
|
8307
|
-
// worker session opens up in the same workspace.
|
|
8308
|
-
if (launchResult?.success && autoImportContextFilePath) {
|
|
8309
|
-
const stripPath = autoImportContextFilePath;
|
|
8310
|
-
setTimeout(() => {
|
|
8311
|
-
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
8312
|
-
stripCoordinatorWrapperFile(stripPath);
|
|
8313
|
-
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
|
|
8314
|
-
}).catch(() => { /* best-effort */ });
|
|
8315
|
-
}, 5000);
|
|
8316
|
-
}
|
|
8317
|
-
|
|
8318
|
-
if (!launchResult?.success) {
|
|
8319
|
-
return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
|
|
8320
|
-
}
|
|
8321
|
-
|
|
8322
|
-
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
|
|
8323
|
-
const launchSessionId = launchResult.sessionId || launchResult.id;
|
|
8324
|
-
if (launchSessionId) {
|
|
8325
|
-
const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
8326
|
-
registerMeshCoordinator({
|
|
8327
|
-
meshId,
|
|
8328
|
-
sessionId: launchSessionId,
|
|
8329
|
-
workspace,
|
|
8330
|
-
startedAt: Date.now(),
|
|
8331
|
-
cliType,
|
|
8332
|
-
systemPrompt: systemPrompt || undefined,
|
|
8333
|
-
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
8334
|
-
mcpConfigPath,
|
|
8335
|
-
injection: autoImportInjectionDecl ? {
|
|
8336
|
-
mode: autoImportInjectionDecl.mode,
|
|
8337
|
-
target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
|
|
8338
|
-
: 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
|
|
8339
|
-
: 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
|
|
8340
|
-
: undefined,
|
|
8341
|
-
} : undefined,
|
|
8342
|
-
});
|
|
8343
|
-
}
|
|
8344
|
-
|
|
8345
|
-
// Record coordinator launch in task ledger
|
|
8346
|
-
try {
|
|
8347
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
8348
|
-
appendLedgerEntry(meshId, {
|
|
8349
|
-
kind: 'coordinator_started',
|
|
8350
|
-
sessionId: launchSessionId,
|
|
8351
|
-
providerType: cliType,
|
|
8352
|
-
payload: { workspace },
|
|
8353
|
-
});
|
|
8354
|
-
} catch { /* ledger append is best-effort */ }
|
|
8355
|
-
|
|
8356
|
-
return {
|
|
8357
|
-
success: true,
|
|
8358
|
-
meshId,
|
|
8359
|
-
cliType,
|
|
8360
|
-
workspace,
|
|
8361
|
-
sessionId: launchSessionId,
|
|
8362
|
-
mcpConfigWritten: true,
|
|
8363
|
-
};
|
|
8364
|
-
} catch (e: any) {
|
|
8365
|
-
LOG.error('MeshCoordinator', `Failed: ${e.message}`);
|
|
8366
|
-
return { success: false, error: e.message };
|
|
8367
|
-
}
|
|
8368
|
-
}
|
|
8369
|
-
|
|
8370
|
-
case 'mesh_status': {
|
|
8371
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
8372
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
8373
|
-
try {
|
|
8374
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
8375
|
-
const mesh = meshRecord?.mesh;
|
|
8376
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
8377
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
8378
|
-
|
|
8379
|
-
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
8380
|
-
// Compact (default) elides each mission's full goal text from the
|
|
8381
|
-
// payload — coordinators polling node health don't need every
|
|
8382
|
-
// mission's multi-hundred-char goal repeated. verbose=true (or the
|
|
8383
|
-
// explicit compact=false) restores full goals. Verbose bypasses the
|
|
8384
|
-
// shared (compact) aggregate cache so a verbose call never poisons
|
|
8385
|
-
// the compact cache and vice versa.
|
|
8386
|
-
const verboseMissions = args?.verbose === true || args?.compact === false;
|
|
8387
|
-
// See (B3) below: scope the peek to this daemon when the
|
|
8388
|
-
// caller doesn't tell us, otherwise scoped events look
|
|
8389
|
-
// missing and we falsely return a stale cache.
|
|
8390
|
-
const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
8391
|
-
? args.coordinatorDaemonId.trim()
|
|
8392
|
-
: (this.deps.statusInstanceId || undefined);
|
|
8393
|
-
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
|
|
8394
|
-
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
8395
|
-
if (!refreshRequested && !verboseMissions && pendingCoordinatorEventCount === 0) {
|
|
8396
|
-
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
8397
|
-
if (cachedStatus) {
|
|
8398
|
-
logRepoMeshStatusDebug('return_cached', {
|
|
8399
|
-
meshId,
|
|
8400
|
-
command: 'mesh_status',
|
|
8401
|
-
refreshRequested,
|
|
8402
|
-
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
8403
|
-
});
|
|
8404
|
-
return cachedStatus;
|
|
8405
|
-
}
|
|
8406
|
-
}
|
|
8407
|
-
const refreshReason = refreshRequested
|
|
8408
|
-
? 'explicit_refresh'
|
|
8409
|
-
: pendingCoordinatorEventCount > 0
|
|
8410
|
-
? 'pending_coordinator_events'
|
|
8411
|
-
: hadAggregateCache
|
|
8412
|
-
? 'stale_pending_cache_refresh'
|
|
8413
|
-
: 'cold_cache_miss';
|
|
8414
|
-
|
|
8415
|
-
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
8416
|
-
const queue = getQueue(meshId);
|
|
8417
|
-
const queueSummary = getMeshQueueStats(meshId);
|
|
8418
|
-
|
|
8419
|
-
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
8420
|
-
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
8421
|
-
const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
|
|
8422
|
-
const ledgerSummary = getLedgerSummary(meshId);
|
|
8423
|
-
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
8424
|
-
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
8425
|
-
: [];
|
|
8426
|
-
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
8427
|
-
|
|
8428
|
-
const localMachineId = loadConfig().machineId || '';
|
|
8429
|
-
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
8430
|
-
// Shared probe gate for this mesh_status call: the bootstrap
|
|
8431
|
-
// hydrate below and the per-node render loop further down both
|
|
8432
|
-
// probe the same peers — route both through this cache so they
|
|
8433
|
-
// dedup within the call and reuse recent results across calls.
|
|
8434
|
-
const meshGitProbeCache = this.meshGitProbeCache;
|
|
8435
|
-
const directTruth = requireDirectPeerTruth
|
|
8436
|
-
? await hydrateInlineMeshDirectTruth({
|
|
8437
|
-
mesh,
|
|
8438
|
-
meshSource: meshRecord.source,
|
|
8439
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8440
|
-
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
8441
|
-
statusInstanceId: this.deps.statusInstanceId,
|
|
8442
|
-
localMachineId,
|
|
8443
|
-
// Standing-state model: only an explicit refresh fans
|
|
8444
|
-
// out a blocking peer git probe. Default loads return
|
|
8445
|
-
// held truth so one slow peer can't block the graph.
|
|
8446
|
-
probeRemotePeers: refreshRequested,
|
|
8447
|
-
probeCache: meshGitProbeCache,
|
|
8448
|
-
})
|
|
8449
|
-
: {
|
|
8450
|
-
directEvidenceCount: 0,
|
|
8451
|
-
localConfirmedCount: 0,
|
|
8452
|
-
peerAttemptedCount: 0,
|
|
8453
|
-
peerConfirmedCount: 0,
|
|
8454
|
-
standingEvidenceCount: 0,
|
|
8455
|
-
unavailableNodeIds: [] as string[],
|
|
8456
|
-
deadNodeIds: [] as string[],
|
|
8457
|
-
};
|
|
8458
|
-
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
8459
|
-
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
8460
|
-
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
8461
|
-
&& !refreshRequested
|
|
8462
|
-
&& directTruth.directEvidenceCount > 0
|
|
8463
|
-
&& directTruth.peerAttemptedCount === 0;
|
|
8464
|
-
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
8465
|
-
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
8466
|
-
: directTruth;
|
|
8467
|
-
const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
8468
|
-
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
8469
|
-
&& Array.isArray(mesh.nodes)
|
|
8470
|
-
&& mesh.nodes
|
|
8471
|
-
.filter((node: any) => unavailableDirectTruthNodeIds.has(normalizeMeshNodeId(node) ?? ''))
|
|
8472
|
-
.every((node: any) => node?.isLocalWorktree === true);
|
|
8473
|
-
// Default (non-refresh) loads never hard-fail: held
|
|
8474
|
-
// standing-state truth is returned and the graph renders
|
|
8475
|
-
// immediately. The hard mesh_direct_peer_truth_unavailable
|
|
8476
|
-
// failure is reserved for an explicit refresh that actually
|
|
8477
|
-
// attempted a peer probe and could not confirm any evidence.
|
|
8478
|
-
const directTruthSatisfied = !requireDirectPeerTruth
|
|
8479
|
-
|| !refreshRequested
|
|
8480
|
-
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
8481
|
-
if (requireDirectPeerTruth && refreshRequested && !directTruthSatisfied) {
|
|
8482
|
-
const failureResult = {
|
|
8483
|
-
success: false,
|
|
8484
|
-
code: 'mesh_direct_peer_truth_unavailable',
|
|
8485
|
-
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
8486
|
-
sourceOfTruth: {
|
|
8487
|
-
membership: meshRecord.source === 'inline_cache'
|
|
8488
|
-
? 'coordinator_inline_mesh_cache'
|
|
8489
|
-
: meshRecord.source === 'local_config'
|
|
8490
|
-
? 'local_mesh_config'
|
|
8491
|
-
: 'inline_bootstrap_snapshot',
|
|
8492
|
-
coordinatorOwnsLiveTruth: false,
|
|
8493
|
-
currentStatus: 'direct_peer_truth_unavailable',
|
|
8494
|
-
directPeerTruth: {
|
|
8495
|
-
required: true,
|
|
8496
|
-
satisfied: false,
|
|
8497
|
-
directEvidenceCount: directTruth.directEvidenceCount,
|
|
8498
|
-
localConfirmedCount: directTruth.localConfirmedCount,
|
|
8499
|
-
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
8500
|
-
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
8501
|
-
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
8502
|
-
},
|
|
8503
|
-
},
|
|
8504
|
-
};
|
|
8505
|
-
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
8506
|
-
meshId,
|
|
8507
|
-
command: 'mesh_status',
|
|
8508
|
-
refreshRequested,
|
|
8509
|
-
meshSource: meshRecord.source,
|
|
8510
|
-
directTruth,
|
|
8511
|
-
});
|
|
8512
|
-
return failureResult;
|
|
8513
|
-
}
|
|
8514
|
-
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
8515
|
-
const coordinatorHostname = osHostname();
|
|
8516
|
-
const selectedCoordinatorNodeId = readStringValue(
|
|
8517
|
-
mesh.coordinator?.preferredNodeId,
|
|
8518
|
-
normalizeMeshNodeId(mesh.nodes?.[0] as any),
|
|
8519
|
-
);
|
|
8520
|
-
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
8521
|
-
? selectedCoordinatorNodeId
|
|
8522
|
-
: undefined;
|
|
8523
|
-
const refreshedAt = new Date().toISOString();
|
|
8524
|
-
const nodeStatuses = [];
|
|
8525
|
-
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
8526
|
-
const nodeId = normalizeMeshNodeId(node) ?? '';
|
|
8527
|
-
const daemonId = readStringValue(node.daemonId);
|
|
8528
|
-
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
|
|
8529
|
-
const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
|
|
8530
|
-
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
8531
|
-
const configuredCoordinatorNode = Boolean(
|
|
8532
|
-
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
8533
|
-
);
|
|
8534
|
-
const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
|
|
8535
|
-
&& !daemonId
|
|
8536
|
-
&& !nodeMachineId
|
|
8537
|
-
&& !nodeHostname;
|
|
8538
|
-
const isSelfNode = Boolean(
|
|
8539
|
-
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
8540
|
-
) || Boolean(
|
|
8541
|
-
daemonId && (daemonIdsEquivalent(daemonId, localMachineId) || daemonIdsEquivalent(daemonId, this.deps.statusInstanceId)),
|
|
8542
|
-
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
8543
|
-
|| sparseConfiguredCoordinatorNode;
|
|
8544
|
-
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
8545
|
-
localMachineId,
|
|
8546
|
-
localDaemonId: this.deps.statusInstanceId,
|
|
8547
|
-
coordinatorHostname,
|
|
8548
|
-
isSelfNode,
|
|
8549
|
-
});
|
|
8550
|
-
const status: Record<string, unknown> = {
|
|
8551
|
-
nodeId,
|
|
8552
|
-
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
8553
|
-
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
8554
|
-
? 'explicit_metadata'
|
|
8555
|
-
: 'workspace_host_provider_context',
|
|
8556
|
-
workspace: node.workspace,
|
|
8557
|
-
repoRoot: node.repoRoot,
|
|
8558
|
-
isLocalWorktree: node.isLocalWorktree,
|
|
8559
|
-
worktreeBranch: node.worktreeBranch,
|
|
8560
|
-
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
8561
|
-
daemonId,
|
|
8562
|
-
machineId: nodeMachineId || node.machineId,
|
|
8563
|
-
machine: machineIdentity,
|
|
8564
|
-
machineStatus: node.machineStatus,
|
|
8565
|
-
health: 'unknown',
|
|
8566
|
-
providers: node.providers || [],
|
|
8567
|
-
providerPriority,
|
|
8568
|
-
activeSessions: [],
|
|
8569
|
-
activeSessionDetails: [],
|
|
8570
|
-
launchReady: false,
|
|
8571
|
-
};
|
|
8572
|
-
if (isSelfNode) {
|
|
8573
|
-
status.connection = {
|
|
8574
|
-
perspective: 'selected_coordinator',
|
|
8575
|
-
source: 'mesh_peer_status',
|
|
8576
|
-
state: 'self',
|
|
8577
|
-
transport: 'local',
|
|
8578
|
-
reported: true,
|
|
8579
|
-
reason: 'Selected coordinator daemon',
|
|
8580
|
-
lastStateChangeAt: refreshedAt,
|
|
8581
|
-
};
|
|
8582
|
-
} else if (daemonId) {
|
|
8583
|
-
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
8584
|
-
status.connection = connection ?? {
|
|
8585
|
-
perspective: 'selected_coordinator',
|
|
8586
|
-
source: 'not_reported',
|
|
8587
|
-
state: 'unknown',
|
|
8588
|
-
transport: 'unknown',
|
|
8589
|
-
reported: false,
|
|
8590
|
-
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
8591
|
-
};
|
|
8592
|
-
} else {
|
|
8593
|
-
status.connection = {
|
|
8594
|
-
perspective: 'selected_coordinator',
|
|
8595
|
-
source: 'not_reported',
|
|
8596
|
-
state: 'unknown',
|
|
8597
|
-
transport: 'unknown',
|
|
8598
|
-
reported: false,
|
|
8599
|
-
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
8600
|
-
};
|
|
8601
|
-
}
|
|
8602
|
-
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
8603
|
-
meshId,
|
|
8604
|
-
node,
|
|
8605
|
-
nodeId,
|
|
8606
|
-
liveSessionRecords: liveMeshSessions,
|
|
8607
|
-
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
8608
|
-
});
|
|
8609
|
-
const workspace = readLiveMeshNodeWorkspace({
|
|
8610
|
-
meshId,
|
|
8611
|
-
nodeId,
|
|
8612
|
-
liveSessionRecords: matchedLiveSessionRecords,
|
|
8613
|
-
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
8614
|
-
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
8615
|
-
status.workspace = workspace || node.workspace;
|
|
8616
|
-
if (matchedLiveSessionRecords.length > 0) {
|
|
8617
|
-
const sessionIds = matchedLiveSessionRecords
|
|
8618
|
-
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
8619
|
-
.filter(Boolean);
|
|
8620
|
-
const providerTypes = matchedLiveSessionRecords
|
|
8621
|
-
.map((record: any) => readStringValue(record?.providerType))
|
|
8622
|
-
.filter(Boolean) as string[];
|
|
8623
|
-
status.activeSessions = sessionIds;
|
|
8624
|
-
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
8625
|
-
if (providerTypes.length > 0) {
|
|
8626
|
-
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
8627
|
-
}
|
|
8628
|
-
}
|
|
8629
|
-
if (workspace) {
|
|
8630
|
-
if (!fs.existsSync(workspace)) {
|
|
8631
|
-
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
8632
|
-
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
8633
|
-
let remoteProbeApplied = false;
|
|
8634
|
-
if (inlineTransitGit) {
|
|
8635
|
-
status.git = inlineTransitGit;
|
|
8636
|
-
status.health = inlineTransitGit.isGitRepo
|
|
8637
|
-
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
8638
|
-
: 'degraded';
|
|
8639
|
-
const connection = readObjectRecord(status.connection);
|
|
8640
|
-
const connectionState = readStringValue(connection.state);
|
|
8641
|
-
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
8642
|
-
if (!connectionReported || connectionState === 'unknown') {
|
|
8643
|
-
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
8644
|
-
}
|
|
8645
|
-
remoteProbeApplied = true;
|
|
8646
|
-
} else if (refreshRequested && !isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
8647
|
-
// Only an explicit refresh fans out a blocking
|
|
8648
|
-
// per-node git probe. On the default load a peer
|
|
8649
|
-
// with no held truth falls through to
|
|
8650
|
-
// gitProbePending below — the graph still renders.
|
|
8651
|
-
// Bounded retry (shared with the bootstrap hydrate
|
|
8652
|
-
// path), gated on the peer staying connected, so a
|
|
8653
|
-
// slow TURN-relayed peer is recovered rather than
|
|
8654
|
-
// dropped after a single timeout.
|
|
8655
|
-
const runNodeProbe = () => probeRemoteMeshGitStatusWithRetry({
|
|
8656
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8657
|
-
daemonId,
|
|
8658
|
-
workspace,
|
|
8659
|
-
timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
|
|
8660
|
-
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
8661
|
-
getConnection: this.deps.getMeshPeerConnectionStatus,
|
|
8662
|
-
onConnection: connection => { status.connection = connection; },
|
|
8663
|
-
});
|
|
8664
|
-
// Same shared cache as the bootstrap hydrate path: within one
|
|
8665
|
-
// mesh_status call this dedups the bootstrap probe against this
|
|
8666
|
-
// per-node probe for the same peer, and across calls it reuses a
|
|
8667
|
-
// recent result so the dashboard auto-retry loop can't restart a
|
|
8668
|
-
// fresh refreshUpstream probe seconds apart.
|
|
8669
|
-
const remoteGit = await meshGitProbeCache.probe(daemonId, workspace, runNodeProbe);
|
|
8670
|
-
if (remoteGit) {
|
|
8671
|
-
status.git = remoteGit;
|
|
8672
|
-
status.health = remoteGit.isGitRepo
|
|
8673
|
-
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
8674
|
-
: 'degraded';
|
|
8675
|
-
const connection = readObjectRecord(status.connection);
|
|
8676
|
-
const connectionState = readStringValue(connection.state);
|
|
8677
|
-
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
8678
|
-
if (!connectionReported || connectionState === 'unknown') {
|
|
8679
|
-
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
8680
|
-
}
|
|
8681
|
-
const reporter = recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
8682
|
-
persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
|
|
8683
|
-
remoteProbeApplied = true;
|
|
8684
|
-
}
|
|
8685
|
-
}
|
|
8686
|
-
if (!remoteProbeApplied) {
|
|
8687
|
-
const connectionState = readStringValue((status.connection as any)?.state);
|
|
8688
|
-
const pendingPeerGitProbe = !inlineTransitGit
|
|
8689
|
-
&& !isSelfNode
|
|
8690
|
-
&& !!daemonId
|
|
8691
|
-
&& (
|
|
8692
|
-
readStringValue(status.machineStatus) === 'online'
|
|
8693
|
-
|| readStringValue(status.health) === 'online'
|
|
8694
|
-
|| connectionState === 'connecting'
|
|
8695
|
-
|| connectionState === 'connected'
|
|
8696
|
-
|| connectionState === 'unknown'
|
|
8697
|
-
);
|
|
8698
|
-
if (pendingPeerGitProbe) {
|
|
8699
|
-
status.gitProbePending = true;
|
|
8700
|
-
status.health = 'unknown';
|
|
8701
|
-
}
|
|
8702
|
-
if (applyCachedInlineMeshNodeStatus(
|
|
8703
|
-
status,
|
|
8704
|
-
node,
|
|
8705
|
-
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
8706
|
-
)) {
|
|
8707
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
8708
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
8709
|
-
nodeStatuses.push(status);
|
|
8710
|
-
continue;
|
|
8711
|
-
}
|
|
8712
|
-
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
8713
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
8714
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
8715
|
-
nodeStatuses.push(status);
|
|
8716
|
-
continue;
|
|
8717
|
-
}
|
|
8718
|
-
}
|
|
8719
|
-
} else {
|
|
8720
|
-
try {
|
|
8721
|
-
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
8722
|
-
status.git = gitStatus;
|
|
8723
|
-
const reporter = recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
8724
|
-
persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
|
|
8725
|
-
if (gitStatus.isGitRepo) {
|
|
8726
|
-
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
8727
|
-
} else {
|
|
8728
|
-
status.health = 'degraded';
|
|
8729
|
-
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
8730
|
-
}
|
|
8731
|
-
} catch {
|
|
8732
|
-
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
8733
|
-
status.health = 'degraded';
|
|
8734
|
-
}
|
|
8735
|
-
}
|
|
8736
|
-
}
|
|
8737
|
-
} else {
|
|
8738
|
-
applyCachedInlineMeshNodeStatus(status, node);
|
|
8739
|
-
}
|
|
8740
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
8741
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
8742
|
-
nodeStatuses.push(status);
|
|
8743
|
-
}
|
|
8744
|
-
|
|
8745
|
-
// (B3) Resolve the coordinator daemon scope for the peek.
|
|
8746
|
-
// mesh_status is a read-only status query — it must not consume
|
|
8747
|
-
// (drain) pending events as a side effect. Coordinators that see
|
|
8748
|
-
// pendingCoordinatorEvents in the response are expected to call
|
|
8749
|
-
// get_pending_mesh_events to explicitly drain them after processing.
|
|
8750
|
-
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
8751
|
-
? args.coordinatorDaemonId.trim()
|
|
8752
|
-
: (this.deps.statusInstanceId || undefined);
|
|
8753
|
-
const pendingCoordinatorEvents = getPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
|
|
8754
|
-
// R4: surface recent fail-loud routing drops so a coordinator/operator can see
|
|
8755
|
-
// that a worker completion was lost (envelope present, mesh unresolved) instead
|
|
8756
|
-
// of it vanishing silently. Diagnostic-only — never cached (see omit below).
|
|
8757
|
-
const unroutableDeliveries = getRecentUnroutableDeliveries();
|
|
8758
|
-
const previewFreshness = (() => {
|
|
8759
|
-
const localRepoRoot = nodeStatuses
|
|
8760
|
-
.map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
|
|
8761
|
-
.find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
|
|
8762
|
-
return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
|
|
8763
|
-
})();
|
|
8764
|
-
const asyncRefineJobs = buildMeshAsyncRefineJobs({
|
|
8765
|
-
meshId,
|
|
8766
|
-
ledgerEntries: asyncRefineLedgerEntries,
|
|
8767
|
-
pendingEvents: [...pendingCoordinatorEvents],
|
|
8768
|
-
});
|
|
8769
|
-
const historicalSessions = buildHistoricalMeshSessions({
|
|
8770
|
-
meshId,
|
|
8771
|
-
nodes: mesh.nodes || [],
|
|
8772
|
-
liveSessionRecords: liveMeshSessions,
|
|
8773
|
-
});
|
|
8774
|
-
const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
|
|
8775
|
-
// withStats opts in to per-mission operational rollups (durations /
|
|
8776
|
-
// retries) for the dashboard mission detail. The rollup scans a
|
|
8777
|
-
// bounded ledger tail per mission, but only over the bounded set
|
|
8778
|
-
// returned here (live + capped history), so the cost stays linear
|
|
8779
|
-
// in visible missions rather than the whole mesh history.
|
|
8780
|
-
const missions = getMeshStatusMissionSummaries(meshId, { verbose: verboseMissions, withStats: true });
|
|
8781
|
-
const statusResult = {
|
|
8782
|
-
success: true,
|
|
8783
|
-
meshId: mesh.id,
|
|
8784
|
-
meshName: mesh.name,
|
|
8785
|
-
repoIdentity: mesh.repoIdentity,
|
|
8786
|
-
defaultBranch: mesh.defaultBranch,
|
|
8787
|
-
refreshedAt,
|
|
8788
|
-
meshHost,
|
|
8789
|
-
sourceOfTruth: {
|
|
8790
|
-
membership: meshRecord?.source === 'inline_cache'
|
|
8791
|
-
? 'coordinator_inline_mesh_cache'
|
|
8792
|
-
: meshRecord?.source === 'local_config'
|
|
8793
|
-
? 'local_mesh_config'
|
|
8794
|
-
: 'inline_bootstrap_snapshot',
|
|
8795
|
-
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
8796
|
-
meshHost: {
|
|
8797
|
-
owner: 'mesh_host_daemon',
|
|
8798
|
-
localRole: meshHost.role,
|
|
8799
|
-
hostDaemonId: meshHost.hostDaemonId,
|
|
8800
|
-
hostNodeId: meshHost.hostNodeId,
|
|
8801
|
-
hostAddress: meshHost.hostAddress,
|
|
8802
|
-
},
|
|
8803
|
-
...(requireDirectPeerTruth ? {
|
|
8804
|
-
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
8805
|
-
directPeerTruth: {
|
|
8806
|
-
required: true,
|
|
8807
|
-
satisfied: directTruthSatisfied,
|
|
8808
|
-
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
8809
|
-
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
8810
|
-
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
8811
|
-
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
8812
|
-
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
8813
|
-
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
8814
|
-
},
|
|
8815
|
-
} : {}),
|
|
8816
|
-
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
|
|
8817
|
-
},
|
|
8818
|
-
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
8819
|
-
...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
|
|
8820
|
-
nodes: nodeStatuses,
|
|
8821
|
-
queue: { tasks: queue, summary: queueSummary },
|
|
8822
|
-
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
8823
|
-
...(missions.length > 0 ? { missions } : {}),
|
|
8824
|
-
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
8825
|
-
...(historicalSessions ? { historicalSessions } : {}),
|
|
8826
|
-
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
8827
|
-
...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
|
|
8828
|
-
activeRefineJobs: Array.from(this.runningRefineJobs.values())
|
|
8829
|
-
.filter(job => job.meshId === meshId)
|
|
8830
|
-
.map(job => ({
|
|
8831
|
-
jobId: job.jobId,
|
|
8832
|
-
nodeId: job.targetNodeId,
|
|
8833
|
-
workspace: job.workspace,
|
|
8834
|
-
startedAt: job.startedAt,
|
|
8835
|
-
status: job.status,
|
|
8836
|
-
targetCoordinatorDaemonId: job.targetCoordinatorDaemonId,
|
|
8837
|
-
})),
|
|
8838
|
-
};
|
|
8839
|
-
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, unroutableDeliveries: _unroutableDeliveries, ...cacheableStatusResult } = statusResult as any;
|
|
8840
|
-
// Verbose carries full mission goals; never store it in the shared
|
|
8841
|
-
// (compact) aggregate cache or a later compact poll would return the
|
|
8842
|
-
// heavy goals from cache. Return it without caching.
|
|
8843
|
-
const rememberedStatus = verboseMissions
|
|
8844
|
-
? cacheableStatusResult
|
|
8845
|
-
: this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
8846
|
-
const returnedStatus = {
|
|
8847
|
-
...rememberedStatus,
|
|
8848
|
-
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
8849
|
-
...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
|
|
8850
|
-
};
|
|
8851
|
-
logRepoMeshStatusDebug('return_live', {
|
|
8852
|
-
meshId,
|
|
8853
|
-
command: 'mesh_status',
|
|
8854
|
-
refreshRequested,
|
|
8855
|
-
refreshReason,
|
|
8856
|
-
meshSource: meshRecord.source,
|
|
8857
|
-
directTruth,
|
|
8858
|
-
summary: summarizeRepoMeshStatusDebug(returnedStatus),
|
|
8859
|
-
});
|
|
8860
|
-
return returnedStatus;
|
|
8861
|
-
} catch (e: any) {
|
|
8862
|
-
return { success: false, error: e.message };
|
|
8863
|
-
}
|
|
8864
|
-
}
|
|
8865
|
-
|
|
8866
|
-
case 'get_mesh_review_inbox': {
|
|
8867
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
8868
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
8869
|
-
try {
|
|
8870
|
-
const { deriveMeshReviewInboxItems } = await import('../mesh/mesh-review-inbox.js');
|
|
8871
|
-
const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
|
|
8872
|
-
const { getGitDiffSummary } = await import('../git/git-diff.js');
|
|
8873
|
-
const { existsSync } = await import('node:fs');
|
|
8874
|
-
|
|
8875
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
8876
|
-
const mesh = meshRecord?.mesh;
|
|
8877
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
8878
|
-
|
|
8879
|
-
// Ensure we have a fresh aggregate status so nodeStatuses carry
|
|
8880
|
-
// computed fields (connection.state, branchConvergence, isLocalWorktree)
|
|
8881
|
-
// that the raw mesh.nodes config objects don't have.
|
|
8882
|
-
// When the caller provides an inlineMesh, prefer its nodes directly
|
|
8883
|
-
// (they already carry the computed fields from the coordinator).
|
|
8884
|
-
const inlineNodes = args?.inlineMesh && Array.isArray((args.inlineMesh as any)?.nodes)
|
|
8885
|
-
? (args.inlineMesh as any).nodes as Record<string, unknown>[]
|
|
8886
|
-
: null;
|
|
8887
|
-
let cachedStatus = !inlineNodes ? this.getCachedAggregateMeshStatus(meshId, mesh, {}) : null;
|
|
8888
|
-
if (!cachedStatus && !inlineNodes) {
|
|
8889
|
-
const freshStatus = await this.execute('mesh_status', {
|
|
8890
|
-
meshId,
|
|
8891
|
-
inlineMesh: args?.inlineMesh,
|
|
8892
|
-
refresh: true,
|
|
8893
|
-
}, 'get_mesh_review_inbox');
|
|
8894
|
-
cachedStatus = (freshStatus?.success !== false) ? freshStatus : null;
|
|
8895
|
-
}
|
|
8896
|
-
const nodeStatuses: Record<string, unknown>[] = inlineNodes
|
|
8897
|
-
? inlineNodes
|
|
8898
|
-
: Array.isArray(cachedStatus?.nodes)
|
|
8899
|
-
? cachedStatus.nodes as Record<string, unknown>[]
|
|
8900
|
-
: Array.isArray(mesh.nodes)
|
|
8901
|
-
? mesh.nodes as Record<string, unknown>[]
|
|
8902
|
-
: [];
|
|
8903
|
-
|
|
8904
|
-
const ledgerEntries = readLedgerEntries(meshId, { tail: 300 });
|
|
8905
|
-
const derivation = deriveMeshReviewInboxItems({ nodes: nodeStatuses, ledgerEntries });
|
|
8906
|
-
|
|
8907
|
-
for (const item of derivation.items) {
|
|
8908
|
-
const workspace = item.workspace;
|
|
8909
|
-
if (!workspace || !existsSync(workspace)) continue;
|
|
8910
|
-
const baseRef = item.defaultBranch
|
|
8911
|
-
? `origin/${item.defaultBranch}`
|
|
8912
|
-
: 'origin/main';
|
|
8913
|
-
try {
|
|
8914
|
-
const diffResult = await getGitDiffSummary(workspace, { baseRef, maxFiles: 100 });
|
|
8915
|
-
if (diffResult.isGitRepo) {
|
|
8916
|
-
item.diffSummary = {
|
|
8917
|
-
baseRef,
|
|
8918
|
-
files: diffResult.files.map(f => ({
|
|
8919
|
-
path: f.path,
|
|
8920
|
-
status: f.status,
|
|
8921
|
-
insertions: f.insertions,
|
|
8922
|
-
deletions: f.deletions,
|
|
8923
|
-
binary: f.binary,
|
|
8924
|
-
oldPath: f.oldPath,
|
|
8925
|
-
})),
|
|
8926
|
-
totalFiles: diffResult.files.length,
|
|
8927
|
-
totalInsertions: diffResult.totalInsertions,
|
|
8928
|
-
totalDeletions: diffResult.totalDeletions,
|
|
8929
|
-
truncated: diffResult.truncated,
|
|
8930
|
-
...(diffResult.error ? { error: diffResult.error } : {}),
|
|
8931
|
-
};
|
|
8932
|
-
}
|
|
8933
|
-
} catch {
|
|
8934
|
-
item.diffSummary = null;
|
|
8935
|
-
}
|
|
8936
|
-
}
|
|
8937
|
-
|
|
8938
|
-
return {
|
|
8939
|
-
success: true,
|
|
8940
|
-
meshId,
|
|
8941
|
-
inbox: derivation.items,
|
|
8942
|
-
remoteNodesExcluded: derivation.remoteNodesExcluded,
|
|
8943
|
-
excludedRemoteNodeIds: derivation.excludedRemoteNodeIds,
|
|
8944
|
-
};
|
|
8945
|
-
} catch (e: any) {
|
|
8946
|
-
return { success: false, error: e.message };
|
|
8947
|
-
}
|
|
8948
|
-
}
|
|
6407
|
+
// RF-ROUTER MED family: medium-coupling commands (CLI/ACP agent, IDE
|
|
6408
|
+
// lifecycle, mesh CRUD, mesh queue, mesh host pairing, fast-forward /
|
|
6409
|
+
// refine convergence) are handled by the registry after the LOW family and
|
|
6410
|
+
// before the switch. Unlike LOW handlers, MED handlers need router-private
|
|
6411
|
+
// collaborators, so the context carries bound methods + the inline-mesh /
|
|
6412
|
+
// git-probe caches + the launchIde helper (which breaks the original
|
|
6413
|
+
// launch_ide ↔ restart_* self-recursion). A hit returns the same
|
|
6414
|
+
// CommandRouterResult the inlined case used to; a miss falls through.
|
|
6415
|
+
const medFamilyHandler = medFamilyRegistry.get(cmd);
|
|
6416
|
+
if (medFamilyHandler) {
|
|
6417
|
+
return await medFamilyHandler(this.buildMedFamilyContext(), args);
|
|
6418
|
+
}
|
|
8949
6419
|
|
|
8950
|
-
|
|
8951
|
-
|
|
6420
|
+
// RF-ROUTER HIGH family: high-coupling commands (mesh coordinator-event
|
|
6421
|
+
// relay + interactive prompt, mesh coordinator launch, mesh aggregate
|
|
6422
|
+
// status + review inbox) are handled by the registry after the LOW and
|
|
6423
|
+
// MED families and before the (now empty) switch. HIGH handlers reach the
|
|
6424
|
+
// most router-owned state — the aggregate-status memory cache and the
|
|
6425
|
+
// running-refine-job table — so the context carries those plus bound
|
|
6426
|
+
// read/write helpers and the router's own `execute` (the
|
|
6427
|
+
// get_mesh_review_inbox mesh_status re-entry). A hit returns the same
|
|
6428
|
+
// CommandRouterResult the inlined case used to; a miss falls through to
|
|
6429
|
+
// CommandHandler delegation.
|
|
6430
|
+
const highFamilyHandler = highFamilyRegistry.get(cmd);
|
|
6431
|
+
if (highFamilyHandler) {
|
|
6432
|
+
return await highFamilyHandler(this.buildHighFamilyContext(), args);
|
|
8952
6433
|
}
|
|
8953
6434
|
|
|
8954
6435
|
return null; // Not handled at this level → delegate to CommandHandler
|