@adhdev/daemon-core 0.9.82-rc.13 → 0.9.82-rc.130
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/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +4 -1
- package/dist/cli-adapters/provider-cli-adapter.d.ts +25 -1
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +4 -0
- package/dist/config/mesh-config.d.ts +68 -1
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +15 -5
- package/dist/index.js +7461 -1380
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7417 -1364
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +73 -0
- package/dist/mesh/mesh-events.d.ts +54 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-refine-status.d.ts +27 -0
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +6 -1
- package/dist/repo-mesh-types.d.ts +62 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/chat/subscription-updates.ts +5 -1
- package/src/cli-adapter-types.ts +2 -1
- package/src/cli-adapters/provider-cli-adapter.ts +473 -18
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +32 -10
- package/src/commands/chat-commands.ts +960 -40
- package/src/commands/cli-manager.ts +138 -2
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3238 -423
- package/src/config/chat-history.ts +37 -9
- package/src/config/mesh-config.ts +249 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +17 -5
- package/src/index.ts +41 -4
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +176 -0
- package/src/mesh/coordinator-prompt.ts +31 -8
- package/src/mesh/mesh-active-work.ts +295 -0
- package/src/mesh/mesh-events.ts +595 -48
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +138 -1
- package/src/mesh/mesh-refine-status.ts +145 -0
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +234 -0
- package/src/providers/approval-utils.ts +12 -5
- package/src/providers/chat-message-normalization.ts +7 -12
- package/src/providers/cli-provider-instance.ts +289 -36
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +67 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
package/src/commands/router.ts
CHANGED
|
@@ -38,14 +38,36 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
|
|
|
38
38
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
|
-
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
|
|
41
|
+
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
42
|
+
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
43
|
+
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
44
|
+
import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
|
|
45
|
+
import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
|
|
46
|
+
import {
|
|
47
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
48
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
49
|
+
loadMeshRefineConfig,
|
|
50
|
+
resolveMeshRefineValidationPlan,
|
|
51
|
+
suggestMeshRefineConfig,
|
|
52
|
+
validateMeshRefineConfig,
|
|
53
|
+
type MeshRefineValidationCommandPlan,
|
|
54
|
+
} from '../mesh/refine-config.js';
|
|
55
|
+
import {
|
|
56
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
57
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
58
|
+
loadMeshWorktreeBootstrapConfig,
|
|
59
|
+
runMeshWorktreeBootstrap,
|
|
60
|
+
type WorktreeBootstrapState,
|
|
61
|
+
} from '../mesh/worktree-bootstrap-config.js';
|
|
42
62
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
43
63
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
44
64
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
65
|
+
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
45
66
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
46
|
-
import { homedir } from 'os';
|
|
47
|
-
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
67
|
+
import { homedir, hostname as osHostname } from 'os';
|
|
68
|
+
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
48
69
|
import * as fs from 'fs';
|
|
70
|
+
import { execFileSync } from 'node:child_process';
|
|
49
71
|
|
|
50
72
|
type ReleaseChannel = 'stable' | 'preview';
|
|
51
73
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -114,14 +136,95 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
114
136
|
return undefined;
|
|
115
137
|
}
|
|
116
138
|
|
|
117
|
-
function
|
|
139
|
+
function summarizeRepoMeshDebugGit(git: unknown): Record<string, unknown> | null {
|
|
140
|
+
const record = readObjectRecord(git);
|
|
141
|
+
if (!Object.keys(record).length) return null;
|
|
142
|
+
const submodules = Array.isArray(record.submodules)
|
|
143
|
+
? record.submodules.map((entry: any) => ({
|
|
144
|
+
path: readStringValue(entry?.path) ?? null,
|
|
145
|
+
commit: readStringValue(entry?.commit)?.slice(0, 12) ?? null,
|
|
146
|
+
dirty: readBooleanValue(entry?.dirty) ?? false,
|
|
147
|
+
outOfSync: readBooleanValue(entry?.outOfSync, entry?.out_of_sync) ?? false,
|
|
148
|
+
}))
|
|
149
|
+
: [];
|
|
150
|
+
return {
|
|
151
|
+
isGitRepo: readBooleanValue(record.isGitRepo),
|
|
152
|
+
workspace: readStringValue(record.workspace) ?? null,
|
|
153
|
+
repoRoot: readStringValue(record.repoRoot, record.repo_root) ?? null,
|
|
154
|
+
branch: readStringValue(record.branch) ?? null,
|
|
155
|
+
upstream: readStringValue(record.upstream) ?? null,
|
|
156
|
+
upstreamStatus: readStringValue(record.upstreamStatus, record.upstream_status) ?? null,
|
|
157
|
+
headCommit: readStringValue(record.headCommit, record.head_commit)?.slice(0, 12) ?? null,
|
|
158
|
+
ahead: readNumberValue(record.ahead) ?? null,
|
|
159
|
+
behind: readNumberValue(record.behind) ?? null,
|
|
160
|
+
dirtyCounts: {
|
|
161
|
+
staged: readNumberValue(record.staged) ?? 0,
|
|
162
|
+
modified: readNumberValue(record.modified) ?? 0,
|
|
163
|
+
untracked: readNumberValue(record.untracked) ?? 0,
|
|
164
|
+
deleted: readNumberValue(record.deleted) ?? 0,
|
|
165
|
+
renamed: readNumberValue(record.renamed) ?? 0,
|
|
166
|
+
},
|
|
167
|
+
lastCheckedAt: readNumberValue(record.lastCheckedAt, record.last_checked_at) ?? null,
|
|
168
|
+
submoduleCount: submodules.length,
|
|
169
|
+
submodules,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
174
|
+
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
175
|
+
return {
|
|
176
|
+
success: status?.success,
|
|
177
|
+
meshId: readStringValue(status?.meshId, status?.mesh_id) ?? null,
|
|
178
|
+
refreshedAt: readStringValue(status?.refreshedAt, status?.refreshed_at) ?? null,
|
|
179
|
+
sourceOfTruth: status?.sourceOfTruth ?? null,
|
|
180
|
+
branchConvergenceSummary: status?.branchConvergenceSummary ?? status?.branch_convergence_summary ?? null,
|
|
181
|
+
nodeCount: nodes.length,
|
|
182
|
+
nodes: nodes.map((node: any) => ({
|
|
183
|
+
nodeId: readStringValue(node?.nodeId, node?.id) ?? null,
|
|
184
|
+
daemonId: readStringValue(node?.daemonId, node?.daemon_id) ?? null,
|
|
185
|
+
workspace: readStringValue(node?.workspace, node?.git?.workspace) ?? null,
|
|
186
|
+
health: readStringValue(node?.health) ?? null,
|
|
187
|
+
machineStatus: readStringValue(node?.machineStatus, node?.machine_status) ?? null,
|
|
188
|
+
connection: node?.connection && typeof node.connection === 'object' ? {
|
|
189
|
+
state: readStringValue(node.connection.state) ?? null,
|
|
190
|
+
transport: readStringValue(node.connection.transport) ?? null,
|
|
191
|
+
source: readStringValue(node.connection.source) ?? null,
|
|
192
|
+
reported: readBooleanValue(node.connection.reported) ?? null,
|
|
193
|
+
} : null,
|
|
194
|
+
gitProbePending: node?.gitProbePending === true,
|
|
195
|
+
launchReady: node?.launchReady === true,
|
|
196
|
+
git: summarizeRepoMeshDebugGit(node?.git),
|
|
197
|
+
branchConvergence: node?.branchConvergence ?? node?.branch_convergence ?? null,
|
|
198
|
+
})),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
203
|
+
try {
|
|
204
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
205
|
+
} catch {
|
|
206
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${event}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
|
|
211
|
+
const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : '';
|
|
212
|
+
const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : '';
|
|
213
|
+
if (!normalizedPath) return undefined;
|
|
214
|
+
if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath;
|
|
215
|
+
if (!normalizedRoot) return undefined;
|
|
216
|
+
return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
|
|
118
220
|
if (!Array.isArray(value)) return undefined;
|
|
119
221
|
const submodules = value
|
|
120
222
|
.map(entry => {
|
|
121
223
|
const submodule = readObjectRecord(entry);
|
|
122
224
|
const path = readStringValue(submodule.path);
|
|
123
225
|
const commit = readStringValue(submodule.commit);
|
|
124
|
-
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
226
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
227
|
+
?? joinRepoPath(parentRepoRoot, path);
|
|
125
228
|
if (!path || !commit || !repoPath) return null;
|
|
126
229
|
return {
|
|
127
230
|
path,
|
|
@@ -137,60 +240,146 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
|
137
240
|
return submodules.length > 0 ? submodules : undefined;
|
|
138
241
|
}
|
|
139
242
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
243
|
+
function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
|
|
244
|
+
const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
|
|
245
|
+
if (explicit) return explicit;
|
|
246
|
+
const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
|
|
247
|
+
const workspaceName = workspace ? pathBasename(workspace) : undefined;
|
|
248
|
+
const host = readStringValue(node.machineName, node.machine_name, node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
|
|
249
|
+
const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
|
|
250
|
+
const parts = [workspaceName, host, provider].filter(Boolean);
|
|
251
|
+
if (parts.length > 0) return parts.join(' · ');
|
|
252
|
+
return nodeId || 'unidentified mesh node';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function normalizeMeshHostname(value: unknown): string | undefined {
|
|
256
|
+
const hostname = readStringValue(value);
|
|
257
|
+
if (!hostname) return undefined;
|
|
258
|
+
return hostname.toLowerCase().replace(/\.$/, '');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
|
|
262
|
+
return readStringValue(
|
|
263
|
+
node.machineId,
|
|
264
|
+
node.machine_id,
|
|
265
|
+
readObjectRecord(node.machine)?.id,
|
|
266
|
+
readObjectRecord(node.machine)?.machineId,
|
|
267
|
+
readObjectRecord(node.lastProbe)?.machineId,
|
|
268
|
+
readObjectRecord(node.last_probe)?.machine_id,
|
|
269
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.id,
|
|
270
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.machineId,
|
|
271
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.id,
|
|
272
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.machine_id,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined {
|
|
277
|
+
return readStringValue(
|
|
278
|
+
node.daemonId,
|
|
279
|
+
node.daemon_id,
|
|
280
|
+
readObjectRecord(node.machine)?.daemonId,
|
|
281
|
+
readObjectRecord(node.machine)?.daemon_id,
|
|
282
|
+
readObjectRecord(node.lastProbe)?.daemonId,
|
|
283
|
+
readObjectRecord(node.last_probe)?.daemon_id,
|
|
284
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemonId,
|
|
285
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemon_id,
|
|
286
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemonId,
|
|
287
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemon_id,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
292
|
+
return readStringValue(
|
|
293
|
+
node.hostname,
|
|
294
|
+
node.host,
|
|
295
|
+
node.machineHostname,
|
|
296
|
+
node.machine_hostname,
|
|
297
|
+
readObjectRecord(node.machine)?.hostname,
|
|
298
|
+
readObjectRecord(node.machine)?.host,
|
|
299
|
+
readObjectRecord(node.lastProbe)?.hostname,
|
|
300
|
+
readObjectRecord(node.last_probe)?.hostname,
|
|
301
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.hostname,
|
|
302
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.hostname,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function readMeshNodeDisplayMachineName(node: Record<string, unknown>): string | undefined {
|
|
307
|
+
return readStringValue(
|
|
308
|
+
node.machineName,
|
|
309
|
+
node.machine_name,
|
|
310
|
+
node.machineLabel,
|
|
311
|
+
node.machine_label,
|
|
312
|
+
node.machineNickname,
|
|
313
|
+
node.machine_nickname,
|
|
314
|
+
node.alias,
|
|
315
|
+
readObjectRecord(node.machine)?.name,
|
|
316
|
+
readObjectRecord(node.machine)?.displayName,
|
|
317
|
+
readObjectRecord(node.machine)?.display_name,
|
|
318
|
+
readObjectRecord(node.lastProbe)?.machineName,
|
|
319
|
+
readObjectRecord(node.last_probe)?.machine_name,
|
|
320
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.name,
|
|
321
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.name,
|
|
322
|
+
readMeshNodeHostname(node),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function compactMeshIdentityEvidence(value: string | undefined): string | undefined {
|
|
327
|
+
if (!value) return undefined;
|
|
328
|
+
return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
332
|
+
localMachineId?: string;
|
|
333
|
+
localDaemonId?: string;
|
|
334
|
+
coordinatorHostname?: string;
|
|
335
|
+
isSelfNode?: boolean;
|
|
336
|
+
}): Record<string, unknown> {
|
|
337
|
+
const machineId = readMeshNodeMachineId(node);
|
|
338
|
+
const daemonId = readMeshNodeDaemonId(node);
|
|
339
|
+
const hostname = readMeshNodeHostname(node);
|
|
340
|
+
const machineName = readMeshNodeDisplayMachineName(node);
|
|
341
|
+
const coordinatorHostname = readStringValue(opts.coordinatorHostname);
|
|
342
|
+
const machineIdMatches = Boolean(opts.localMachineId && machineId && opts.localMachineId === machineId);
|
|
343
|
+
const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && opts.localDaemonId === daemonId);
|
|
344
|
+
const hostnameMatches = Boolean(
|
|
345
|
+
normalizeMeshHostname(hostname)
|
|
346
|
+
&& normalizeMeshHostname(coordinatorHostname)
|
|
347
|
+
&& normalizeMeshHostname(hostname) === normalizeMeshHostname(coordinatorHostname),
|
|
348
|
+
);
|
|
349
|
+
const sameMachine = opts.isSelfNode === true || machineIdMatches || daemonIdMatches || hostnameMatches;
|
|
350
|
+
const evidence: string[] = [];
|
|
351
|
+
for (const [label, value] of [['machineName', machineName], ['hostname', hostname], ['machineId', machineId], ['daemonId', daemonId]] as const) {
|
|
352
|
+
const compact = compactMeshIdentityEvidence(value);
|
|
353
|
+
if (compact) evidence.push(`${label}:${compact}`);
|
|
174
354
|
}
|
|
355
|
+
const locality = sameMachine ? 'same_machine' : (evidence.length > 0 ? 'remote_known' : 'remote_or_unknown');
|
|
356
|
+
const localityReason = sameMachine
|
|
357
|
+
? (machineIdMatches ? 'matched coordinator machine id'
|
|
358
|
+
: daemonIdMatches ? 'matched coordinator daemon id'
|
|
359
|
+
: hostnameMatches ? 'matched coordinator hostname'
|
|
360
|
+
: 'selected coordinator node')
|
|
361
|
+
: evidence.length > 0
|
|
362
|
+
? `known remote/other machine identity; no local coordinator match (${evidence.join(', ')})`
|
|
363
|
+
: 'no useful machine identity evidence available';
|
|
364
|
+
return {
|
|
365
|
+
daemonId,
|
|
366
|
+
machineId,
|
|
367
|
+
hostname,
|
|
368
|
+
machineName,
|
|
369
|
+
displayName: machineName || hostname || daemonId || machineId,
|
|
370
|
+
coordinatorHostname,
|
|
371
|
+
sameMachine,
|
|
372
|
+
locality,
|
|
373
|
+
localityReason,
|
|
374
|
+
identityEvidence: evidence,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
175
377
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const probeGit = readObjectRecord(rawProbe.git);
|
|
182
|
-
const probeGitResult = readObjectRecord(probeGit.result);
|
|
183
|
-
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
184
|
-
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
185
|
-
const status = Object.keys(directStatus).length
|
|
186
|
-
? directStatus
|
|
187
|
-
: Object.keys(nestedStatus).length
|
|
188
|
-
? nestedStatus
|
|
189
|
-
: Object.keys(probeDirectStatus).length
|
|
190
|
-
? probeDirectStatus
|
|
191
|
-
: Object.keys(probeNestedStatus).length
|
|
192
|
-
? probeNestedStatus
|
|
193
|
-
: {};
|
|
378
|
+
function normalizeInlineMeshGitStatus(
|
|
379
|
+
status: Record<string, unknown>,
|
|
380
|
+
node: any,
|
|
381
|
+
options?: { lastCheckedAt?: number },
|
|
382
|
+
): Record<string, unknown> | undefined {
|
|
194
383
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
384
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
385
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -198,15 +387,20 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
198
387
|
: [];
|
|
199
388
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
389
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
-
const
|
|
390
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
391
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
202
392
|
return {
|
|
203
393
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
-
repoRoot:
|
|
394
|
+
repoRoot: repoRoot ?? null,
|
|
205
395
|
isGitRepo,
|
|
206
396
|
branch: readStringValue(status.branch) ?? null,
|
|
207
397
|
headCommit: readStringValue(status.headCommit) ?? null,
|
|
208
398
|
headMessage: readStringValue(status.headMessage) ?? null,
|
|
209
399
|
upstream: readStringValue(status.upstream) ?? null,
|
|
400
|
+
upstreamStatus: readStringValue(status.upstreamStatus, status.upstream_status)
|
|
401
|
+
?? (readStringValue(status.upstream) ? 'unchecked' : 'no_upstream'),
|
|
402
|
+
upstreamFetchedAt: readNumberValue(status.upstreamFetchedAt, status.upstream_fetched_at),
|
|
403
|
+
upstreamFetchError: readStringValue(status.upstreamFetchError, status.upstream_fetch_error),
|
|
210
404
|
ahead: readNumberValue(status.ahead) ?? 0,
|
|
211
405
|
behind: readNumberValue(status.behind) ?? 0,
|
|
212
406
|
staged: readNumberValue(status.staged) ?? 0,
|
|
@@ -217,14 +411,247 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
217
411
|
hasConflicts,
|
|
218
412
|
conflictFiles,
|
|
219
413
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
-
lastCheckedAt: Date.now(),
|
|
414
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
221
415
|
...(submodules ? { submodules } : {}),
|
|
222
416
|
};
|
|
223
417
|
}
|
|
224
418
|
|
|
419
|
+
function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
|
|
420
|
+
if (!git) return Number.NEGATIVE_INFINITY;
|
|
421
|
+
let score = 0;
|
|
422
|
+
if (readBooleanValue(git.isGitRepo) === true) score += 50;
|
|
423
|
+
if (readBooleanValue(git.isGitRepo) === false) score -= 10;
|
|
424
|
+
if (readStringValue(git.branch)) score += 20;
|
|
425
|
+
if (readStringValue(git.headCommit)) score += 20;
|
|
426
|
+
if (readStringValue(git.upstream)) score += 10;
|
|
427
|
+
if (readStringValue(git.upstreamStatus)) score += 5;
|
|
428
|
+
if (readNumberValue(git.ahead) !== undefined) score += 2;
|
|
429
|
+
if (readNumberValue(git.behind) !== undefined) score += 2;
|
|
430
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
|
|
431
|
+
if (readStringValue(git.error)) score -= 20;
|
|
432
|
+
return score;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
436
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
437
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
438
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
439
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
440
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
441
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
442
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
443
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
444
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
445
|
+
const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
|
|
446
|
+
let best: { git: Record<string, unknown>; score: number } | null = null;
|
|
447
|
+
for (const status of candidates) {
|
|
448
|
+
const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
449
|
+
if (!normalized) continue;
|
|
450
|
+
const score = scoreInlineMeshGitStatus(normalized);
|
|
451
|
+
if (!best || score > best.score) best = { git: normalized, score };
|
|
452
|
+
}
|
|
453
|
+
return best?.git;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
|
|
457
|
+
if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
|
|
458
|
+
return snapshot.nodes.some((node: any) => {
|
|
459
|
+
if (node?.gitProbePending !== true) return false;
|
|
460
|
+
const git = readObjectRecord(node?.git);
|
|
461
|
+
return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
466
|
+
const source = readStringValue(connection.source);
|
|
467
|
+
const transport = readStringValue(connection.transport);
|
|
468
|
+
return {
|
|
469
|
+
...connection,
|
|
470
|
+
perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
|
|
471
|
+
source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
|
|
472
|
+
state: 'connected',
|
|
473
|
+
transport: transport && transport !== 'unknown' ? transport : 'direct',
|
|
474
|
+
reported: true,
|
|
475
|
+
reason: 'Live peer git snapshot reported by the selected coordinator.',
|
|
476
|
+
lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function recordInlineMeshDirectGitTruth(
|
|
481
|
+
node: any,
|
|
482
|
+
git: Record<string, unknown>,
|
|
483
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
484
|
+
): void {
|
|
485
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
486
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
487
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
488
|
+
const nextGit: Record<string, unknown> = {
|
|
489
|
+
...git,
|
|
490
|
+
lastCheckedAt: checkedAt,
|
|
491
|
+
};
|
|
492
|
+
node.lastGit = {
|
|
493
|
+
source,
|
|
494
|
+
checkedAt,
|
|
495
|
+
status: nextGit,
|
|
496
|
+
};
|
|
497
|
+
node.last_git = node.lastGit;
|
|
498
|
+
node.machineStatus = 'online';
|
|
499
|
+
node.updatedAt = updatedAt;
|
|
500
|
+
node.lastSeenAt = updatedAt;
|
|
501
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
502
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
506
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
507
|
+
if (liveGit) return liveGit;
|
|
508
|
+
|
|
509
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
510
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
511
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
512
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
516
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
517
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
518
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
519
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
520
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
521
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
522
|
+
const branch = readStringValue(cachedGit.branch);
|
|
523
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
524
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
528
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
529
|
+
const {
|
|
530
|
+
cachedStatus,
|
|
531
|
+
lastGit: _lastGit,
|
|
532
|
+
last_git: _lastGitLegacy,
|
|
533
|
+
lastProbe: _lastProbe,
|
|
534
|
+
last_probe: _lastProbeLegacy,
|
|
535
|
+
error: _error,
|
|
536
|
+
health: _health,
|
|
537
|
+
machineStatus: _machineStatus,
|
|
538
|
+
lastSeenAt: _lastSeenAt,
|
|
539
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
540
|
+
updatedAt: _updatedAt,
|
|
541
|
+
updated_at: _updatedAtLegacy,
|
|
542
|
+
activeSession: _activeSession,
|
|
543
|
+
active_session: _activeSessionLegacy,
|
|
544
|
+
activeSessionId: _activeSessionId,
|
|
545
|
+
active_session_id: _activeSessionIdLegacy,
|
|
546
|
+
sessionId: _sessionId,
|
|
547
|
+
session_id: _sessionIdLegacy,
|
|
548
|
+
providerType: _providerType,
|
|
549
|
+
provider_type: _providerTypeLegacy,
|
|
550
|
+
...rest
|
|
551
|
+
} = node as Record<string, unknown>;
|
|
552
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
553
|
+
return { ...rest, cachedStatus };
|
|
554
|
+
}
|
|
555
|
+
return rest;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
559
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
560
|
+
return 'cachedStatus' in node
|
|
561
|
+
|| 'lastGit' in node
|
|
562
|
+
|| 'last_git' in node
|
|
563
|
+
|| 'lastProbe' in node
|
|
564
|
+
|| 'last_probe' in node
|
|
565
|
+
|| 'error' in node
|
|
566
|
+
|| 'health' in node
|
|
567
|
+
|| 'machineStatus' in node
|
|
568
|
+
|| 'lastSeenAt' in node
|
|
569
|
+
|| 'last_seen_at' in node
|
|
570
|
+
|| 'updatedAt' in node
|
|
571
|
+
|| 'updated_at' in node
|
|
572
|
+
|| 'activeSession' in node
|
|
573
|
+
|| 'active_session' in node
|
|
574
|
+
|| 'activeSessionId' in node
|
|
575
|
+
|| 'active_session_id' in node
|
|
576
|
+
|| 'sessionId' in node
|
|
577
|
+
|| 'session_id' in node
|
|
578
|
+
|| 'providerType' in node
|
|
579
|
+
|| 'provider_type' in node;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
583
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
584
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
585
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function readInlineMeshNodeId(node: any): string {
|
|
589
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
593
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
594
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
595
|
+
let changed = false;
|
|
596
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
597
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
598
|
+
changed = true;
|
|
599
|
+
return stripInlineMeshTransientNodeState(node);
|
|
600
|
+
});
|
|
601
|
+
if (!changed) return inlineMesh;
|
|
602
|
+
return {
|
|
603
|
+
...inlineMesh,
|
|
604
|
+
nodes,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
609
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
610
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
611
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
612
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
613
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
614
|
+
|
|
615
|
+
const cachedUpdatedAt = Date.parse(readStringValue(cached.updatedAt, cached.updated_at) || '');
|
|
616
|
+
const incomingUpdatedAt = Date.parse(readStringValue(incoming.updatedAt, incoming.updated_at) || '');
|
|
617
|
+
const preserveCachedMembership = Number.isFinite(cachedUpdatedAt)
|
|
618
|
+
&& (!Number.isFinite(incomingUpdatedAt) || cachedUpdatedAt > incomingUpdatedAt);
|
|
619
|
+
|
|
620
|
+
const cachedById = new Map<string, any>();
|
|
621
|
+
for (const node of cachedNodes) {
|
|
622
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
623
|
+
if (nodeId) cachedById.set(nodeId, node);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const nodes = incomingNodes.map((incomingNode: any) => {
|
|
627
|
+
const nodeId = readInlineMeshNodeId(incomingNode);
|
|
628
|
+
const cachedNode = nodeId ? cachedById.get(nodeId) : undefined;
|
|
629
|
+
if (!cachedNode && preserveCachedMembership) return null;
|
|
630
|
+
if (!cachedNode) return incomingNode;
|
|
631
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
632
|
+
return { ...cachedNode, ...incomingNode };
|
|
633
|
+
}
|
|
634
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
635
|
+
}).filter(Boolean);
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
...cached,
|
|
639
|
+
...incoming,
|
|
640
|
+
nodes,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
225
644
|
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
226
|
-
|
|
227
|
-
|
|
645
|
+
return countGitWorktreeChanges(git) > 0;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function countGitWorktreeChanges(git: Record<string, unknown> | null | undefined): number {
|
|
649
|
+
if (!git) return 0;
|
|
650
|
+
return Number(git.staged || 0)
|
|
651
|
+
+ Number(git.modified || 0)
|
|
652
|
+
+ Number(git.untracked || 0)
|
|
653
|
+
+ Number(git.deleted || 0)
|
|
654
|
+
+ Number(git.renamed || 0);
|
|
228
655
|
}
|
|
229
656
|
|
|
230
657
|
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
@@ -249,6 +676,167 @@ function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undef
|
|
|
249
676
|
return 'online';
|
|
250
677
|
}
|
|
251
678
|
|
|
679
|
+
function readMeshNodeLabel(status: Record<string, unknown>, node: any): string {
|
|
680
|
+
return readStringValue(status.nodeId, node?.id, node?.nodeId) ?? 'unknown';
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function buildInlineMeshBranchConvergence(args: {
|
|
684
|
+
mesh: any;
|
|
685
|
+
node: any;
|
|
686
|
+
status: Record<string, unknown>;
|
|
687
|
+
}): Record<string, unknown> {
|
|
688
|
+
const git = readObjectRecord(args.status.git);
|
|
689
|
+
const nodeLabel = readMeshNodeLabel(args.status, args.node);
|
|
690
|
+
const defaultBranch = readStringValue(args.mesh?.defaultBranch) ?? 'main';
|
|
691
|
+
const branch = readStringValue(git.branch, args.node?.worktreeBranch) ?? null;
|
|
692
|
+
const upstream = readStringValue(git.upstream) ?? null;
|
|
693
|
+
const upstreamStatus = readStringValue(git.upstreamStatus, git.upstream_status)
|
|
694
|
+
?? (upstream ? 'unchecked' : 'no_upstream');
|
|
695
|
+
const ahead = readNumberValue(git.ahead) ?? 0;
|
|
696
|
+
const behind = readNumberValue(git.behind) ?? 0;
|
|
697
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
698
|
+
const hasConflicts = readBooleanValue(git.hasConflicts)
|
|
699
|
+
?? (Array.isArray(git.conflictFiles) && git.conflictFiles.length > 0);
|
|
700
|
+
const base = {
|
|
701
|
+
defaultBranch,
|
|
702
|
+
branch,
|
|
703
|
+
upstream,
|
|
704
|
+
upstreamStatus,
|
|
705
|
+
ahead,
|
|
706
|
+
behind,
|
|
707
|
+
isWorktree: args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true,
|
|
708
|
+
isDefaultBranch: branch === defaultBranch,
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
if (readBooleanValue(git.isGitRepo) !== true) {
|
|
712
|
+
return {
|
|
713
|
+
...base,
|
|
714
|
+
status: 'blocked_review',
|
|
715
|
+
needsConvergence: true,
|
|
716
|
+
reason: 'git_status_unavailable',
|
|
717
|
+
nextStep: `Resolve git status for node '${nodeLabel}' before marking the task complete.`,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!branch) {
|
|
722
|
+
return {
|
|
723
|
+
...base,
|
|
724
|
+
status: 'blocked_review',
|
|
725
|
+
needsConvergence: true,
|
|
726
|
+
reason: 'branch_unknown',
|
|
727
|
+
nextStep: `Inspect node '${nodeLabel}' git branch before deciding whether it is merged to ${defaultBranch}.`,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (hasConflicts || uncommittedChanges > 0) {
|
|
732
|
+
return {
|
|
733
|
+
...base,
|
|
734
|
+
status: 'not_mergeable',
|
|
735
|
+
needsConvergence: true,
|
|
736
|
+
reason: hasConflicts ? 'conflicts_present' : 'dirty_workspace',
|
|
737
|
+
nextStep: `Commit, checkpoint, or resolve node '${nodeLabel}' before any main convergence step.`,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (branch === defaultBranch) {
|
|
742
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
743
|
+
return {
|
|
744
|
+
...base,
|
|
745
|
+
status: 'blocked_review',
|
|
746
|
+
needsConvergence: true,
|
|
747
|
+
reason: 'default_branch_upstream_unverified',
|
|
748
|
+
nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${nodeLabel}'.`,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (ahead > 0 || behind > 0) {
|
|
752
|
+
return {
|
|
753
|
+
...base,
|
|
754
|
+
status: 'blocked_review',
|
|
755
|
+
needsConvergence: true,
|
|
756
|
+
reason: 'default_branch_not_even_with_upstream',
|
|
757
|
+
nextStep: `Bring ${defaultBranch} even with its upstream before declaring convergence complete.`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
...base,
|
|
762
|
+
status: 'merged_to_main',
|
|
763
|
+
needsConvergence: false,
|
|
764
|
+
reason: 'clean_default_branch',
|
|
765
|
+
nextStep: null,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true) {
|
|
770
|
+
return {
|
|
771
|
+
...base,
|
|
772
|
+
status: 'cleanup_candidate',
|
|
773
|
+
needsConvergence: true,
|
|
774
|
+
reason: 'clean_non_default_worktree_branch',
|
|
775
|
+
nextStep: `Run mesh_refine_node(node_id: "${nodeLabel}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
780
|
+
return {
|
|
781
|
+
...base,
|
|
782
|
+
status: 'blocked_review',
|
|
783
|
+
needsConvergence: true,
|
|
784
|
+
reason: 'feature_branch_upstream_unverified',
|
|
785
|
+
nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (!upstream || ahead > 0 || behind > 0) {
|
|
790
|
+
return {
|
|
791
|
+
...base,
|
|
792
|
+
status: 'blocked_review',
|
|
793
|
+
needsConvergence: true,
|
|
794
|
+
reason: !upstream ? 'feature_branch_missing_upstream' : 'feature_branch_not_even_with_upstream',
|
|
795
|
+
nextStep: `Push or reconcile branch '${branch}', then merge it into ${defaultBranch} or mark it not_mergeable with a reason.`,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
...base,
|
|
801
|
+
status: 'pushed_feature_branch_needs_merge',
|
|
802
|
+
needsConvergence: true,
|
|
803
|
+
reason: 'clean_non_default_branch',
|
|
804
|
+
nextStep: `Review and merge branch '${branch}' into ${defaultBranch}; do not report the task as fully complete while it remains off main.`,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
809
|
+
const git = readObjectRecord(status.git);
|
|
810
|
+
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
811
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
812
|
+
status.isDirty = uncommittedChanges > 0;
|
|
813
|
+
status.uncommittedChanges = uncommittedChanges;
|
|
814
|
+
status.branchConvergence = buildInlineMeshBranchConvergence({ mesh, node, status });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
818
|
+
const followUps = nodes
|
|
819
|
+
.filter(node => readObjectRecord(node.branchConvergence).needsConvergence === true)
|
|
820
|
+
.map(node => {
|
|
821
|
+
const convergence = readObjectRecord(node.branchConvergence);
|
|
822
|
+
return {
|
|
823
|
+
nodeId: node.nodeId,
|
|
824
|
+
workspace: node.workspace,
|
|
825
|
+
branch: convergence.branch,
|
|
826
|
+
status: convergence.status,
|
|
827
|
+
reason: convergence.reason,
|
|
828
|
+
nextStep: convergence.nextStep,
|
|
829
|
+
};
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
needsFollowUp: followUps.length > 0,
|
|
834
|
+
unresolvedCount: followUps.length,
|
|
835
|
+
requiredFinalStates: ['merged_to_main', 'pushed_feature_branch_needs_merge', 'blocked_review', 'cleanup_candidate', 'not_mergeable'],
|
|
836
|
+
followUps,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
252
840
|
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
253
841
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
254
842
|
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
@@ -313,6 +901,169 @@ function toIsoTimestamp(value: unknown): string | null {
|
|
|
313
901
|
return stringValue || null;
|
|
314
902
|
}
|
|
315
903
|
|
|
904
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
905
|
+
const connection = readObjectRecord(status.connection);
|
|
906
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
907
|
+
const git = readObjectRecord(status.git);
|
|
908
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
909
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
910
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
911
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function finalizeMeshNodeStatus(args: {
|
|
916
|
+
status: Record<string, unknown>;
|
|
917
|
+
node: any;
|
|
918
|
+
daemonId?: string;
|
|
919
|
+
isSelfNode: boolean;
|
|
920
|
+
}): void {
|
|
921
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
922
|
+
if (!readStringValue(status.machineStatus)) {
|
|
923
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
924
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
925
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
926
|
+
}
|
|
927
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
928
|
+
const bootstrap = readObjectRecord(node?.worktreeBootstrap);
|
|
929
|
+
if (node?.isLocalWorktree && readStringValue(bootstrap.status)) {
|
|
930
|
+
status.worktreeBootstrap = bootstrap;
|
|
931
|
+
if (bootstrap.status === 'failed' && bootstrap.required !== false) {
|
|
932
|
+
status.launchReady = false;
|
|
933
|
+
status.launchBlockedReason = 'worktree_bootstrap_failed';
|
|
934
|
+
status.launchBlockedMessage = readStringValue(bootstrap.error)
|
|
935
|
+
|| 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (bootstrap.status === 'running' && bootstrap.required !== false) {
|
|
939
|
+
status.launchReady = false;
|
|
940
|
+
status.launchBlockedReason = 'worktree_bootstrap_running';
|
|
941
|
+
status.launchBlockedMessage = 'Required worktree bootstrap is still running; wait for it to finish before launching an agent into this node.';
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
946
|
+
status.launchReady = !!daemonId && (
|
|
947
|
+
readStringValue(status.machineStatus) === 'online'
|
|
948
|
+
|| connectionState === 'connected'
|
|
949
|
+
|| isSelfNode
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
954
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
955
|
+
daemonId: string;
|
|
956
|
+
workspace: string;
|
|
957
|
+
timeoutMs: number;
|
|
958
|
+
}): Promise<Record<string, unknown> | null> {
|
|
959
|
+
if (!args.dispatchMeshCommand) return null;
|
|
960
|
+
const remoteResult = await Promise.race([
|
|
961
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
|
|
962
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
963
|
+
]) as any;
|
|
964
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
965
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
966
|
+
? remoteGit as Record<string, unknown>
|
|
967
|
+
: null;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
971
|
+
mesh: any;
|
|
972
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
973
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
974
|
+
statusInstanceId?: string;
|
|
975
|
+
localMachineId?: string;
|
|
976
|
+
}): Promise<{
|
|
977
|
+
directEvidenceCount: number;
|
|
978
|
+
localConfirmedCount: number;
|
|
979
|
+
peerAttemptedCount: number;
|
|
980
|
+
peerConfirmedCount: number;
|
|
981
|
+
unavailableNodeIds: string[];
|
|
982
|
+
}> {
|
|
983
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
984
|
+
if (!nodes.length) {
|
|
985
|
+
return {
|
|
986
|
+
directEvidenceCount: 0,
|
|
987
|
+
localConfirmedCount: 0,
|
|
988
|
+
peerAttemptedCount: 0,
|
|
989
|
+
peerConfirmedCount: 0,
|
|
990
|
+
unavailableNodeIds: [],
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
995
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
996
|
+
nodes[0]?.id,
|
|
997
|
+
nodes[0]?.nodeId,
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
let localConfirmedCount = 0;
|
|
1001
|
+
let peerAttemptedCount = 0;
|
|
1002
|
+
let peerConfirmedCount = 0;
|
|
1003
|
+
const unavailableNodeIds: string[] = [];
|
|
1004
|
+
|
|
1005
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
1006
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
1007
|
+
const workspace = readStringValue(node?.workspace);
|
|
1008
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
1009
|
+
const isSelfNode = Boolean(
|
|
1010
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
1011
|
+
) || Boolean(
|
|
1012
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
1013
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
1014
|
+
|
|
1015
|
+
if (!workspace) {
|
|
1016
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (fs.existsSync(workspace)) {
|
|
1021
|
+
try {
|
|
1022
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
1023
|
+
if (localGit?.isGitRepo) {
|
|
1024
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
1025
|
+
localConfirmedCount += 1;
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
} catch {
|
|
1029
|
+
// Fall through to remote classification.
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
1034
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
peerAttemptedCount += 1;
|
|
1039
|
+
try {
|
|
1040
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
1041
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1042
|
+
daemonId,
|
|
1043
|
+
workspace,
|
|
1044
|
+
timeoutMs: 8_000,
|
|
1045
|
+
});
|
|
1046
|
+
if (remoteGit) {
|
|
1047
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1048
|
+
peerConfirmedCount += 1;
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
} catch {
|
|
1052
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
unavailableNodeIds.push(nodeId);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return {
|
|
1059
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
1060
|
+
localConfirmedCount,
|
|
1061
|
+
peerAttemptedCount,
|
|
1062
|
+
peerConfirmedCount,
|
|
1063
|
+
unavailableNodeIds,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
316
1067
|
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
317
1068
|
return {
|
|
318
1069
|
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
@@ -328,11 +1079,140 @@ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
|
328
1079
|
};
|
|
329
1080
|
}
|
|
330
1081
|
|
|
331
|
-
function
|
|
1082
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string, nodeWorkspace = '', nodeIsMissingLocalWorktree = false): boolean {
|
|
1083
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1084
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
1085
|
+
if (nodeIsMissingLocalWorktree) return false;
|
|
1086
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
1087
|
+
if (nodeWorkspace && recordWorkspace && recordWorkspace !== nodeWorkspace) return false;
|
|
1088
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1089
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
1093
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
1094
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
1095
|
+
|
|
1096
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1097
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
1098
|
+
|
|
1099
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
1103
|
+
meshId: string;
|
|
1104
|
+
nodeId: string;
|
|
1105
|
+
liveSessionRecords: any[];
|
|
1106
|
+
allowCoordinatorSession?: boolean;
|
|
1107
|
+
}): string {
|
|
1108
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
1109
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
1110
|
+
&& readStringValue(record?.workspace)
|
|
1111
|
+
));
|
|
1112
|
+
if (directNodeWorkspace) {
|
|
1113
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (args.allowCoordinatorSession) {
|
|
1117
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
1118
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
1119
|
+
&& readStringValue(record?.workspace)
|
|
1120
|
+
));
|
|
1121
|
+
if (coordinatorWorkspace) {
|
|
1122
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return '';
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function collectLiveMeshSessionRecords(args: {
|
|
1130
|
+
meshId: string;
|
|
1131
|
+
node: any;
|
|
1132
|
+
nodeId: string;
|
|
1133
|
+
liveSessionRecords: any[];
|
|
1134
|
+
allowCoordinatorSession?: boolean;
|
|
1135
|
+
}): any[] {
|
|
1136
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
1137
|
+
const nodeIsMissingLocalWorktree = args.node?.isLocalWorktree === true
|
|
1138
|
+
&& !!nodeWorkspace
|
|
1139
|
+
&& !fs.existsSync(nodeWorkspace);
|
|
1140
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
1141
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1142
|
+
if (recordNodeId && recordNodeId !== args.nodeId) return false;
|
|
1143
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId, nodeWorkspace || '', nodeIsMissingLocalWorktree)) return true;
|
|
1144
|
+
if (nodeIsMissingLocalWorktree) return false;
|
|
1145
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
if (args.allowCoordinatorSession) {
|
|
1149
|
+
for (const record of args.liveSessionRecords) {
|
|
1150
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
1151
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
1152
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
1153
|
+
matches.push(record);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return matches;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function buildHistoricalMeshSessions(args: {
|
|
1161
|
+
meshId: string;
|
|
1162
|
+
nodes: any[];
|
|
1163
|
+
liveSessionRecords: any[];
|
|
1164
|
+
}): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
|
|
1165
|
+
const liveNodeIds = new Set<string>();
|
|
1166
|
+
const liveWorkspaces = new Set<string>();
|
|
1167
|
+
const missingLocalWorktreeNodeIds = new Set<string>();
|
|
1168
|
+
for (const node of args.nodes || []) {
|
|
1169
|
+
const nodeId = readStringValue(node?.id, node?.nodeId);
|
|
1170
|
+
const workspace = readStringValue(node?.workspace);
|
|
1171
|
+
if (nodeId) liveNodeIds.add(nodeId);
|
|
1172
|
+
if (workspace) liveWorkspaces.add(workspace);
|
|
1173
|
+
if (nodeId && node?.isLocalWorktree === true && workspace && !fs.existsSync(workspace)) {
|
|
1174
|
+
missingLocalWorktreeNodeIds.add(nodeId);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const sessions: Record<string, unknown>[] = [];
|
|
1179
|
+
for (const record of args.liveSessionRecords || []) {
|
|
1180
|
+
const meta = readObjectRecord(record?.meta);
|
|
1181
|
+
const recordMeshId = readStringValue(meta.meshNodeFor, meta.meshCoordinatorFor);
|
|
1182
|
+
if (recordMeshId !== args.meshId) continue;
|
|
1183
|
+
const recordNodeId = readStringValue(meta.meshNodeId);
|
|
1184
|
+
const workspace = readStringValue(record?.workspace);
|
|
1185
|
+
const removedNode = !!recordNodeId && (!liveNodeIds.has(recordNodeId) || missingLocalWorktreeNodeIds.has(recordNodeId));
|
|
1186
|
+
const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
|
|
1187
|
+
if (!removedNode && !orphanedWorkspace) continue;
|
|
1188
|
+
sessions.push({
|
|
1189
|
+
...summarizeMeshSessionRecord(record),
|
|
1190
|
+
classification: removedNode ? 'removedNode' : 'orphanedSession',
|
|
1191
|
+
historical: true,
|
|
1192
|
+
meshNodeId: recordNodeId || null,
|
|
1193
|
+
reason: removedNode
|
|
1194
|
+
? 'Session is tagged to a mesh node that is no longer in live membership.'
|
|
1195
|
+
: 'Session workspace is no longer attached to a live mesh node.',
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
if (sessions.length === 0) return undefined;
|
|
1199
|
+
return {
|
|
1200
|
+
count: sessions.length,
|
|
1201
|
+
sessions: sessions.slice(0, 5),
|
|
1202
|
+
instruction: 'These sessions are separated from normal node activeSessions because their mesh node/workspace is no longer live. Use mesh_cleanup_sessions only if cleanup is intended.',
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function applyCachedInlineMeshNodeStatus(
|
|
1207
|
+
status: Record<string, unknown>,
|
|
1208
|
+
node: any,
|
|
1209
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
1210
|
+
): boolean {
|
|
332
1211
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const
|
|
1212
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
1213
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
1214
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
1215
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
336
1216
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
337
1217
|
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
338
1218
|
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
@@ -389,29 +1269,140 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
389
1269
|
}
|
|
390
1270
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
391
1271
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
392
|
-
type MeshRefineValidationCommand =
|
|
393
|
-
command: string;
|
|
394
|
-
args: string[];
|
|
395
|
-
displayCommand: string;
|
|
396
|
-
category: string;
|
|
397
|
-
source: string;
|
|
398
|
-
};
|
|
1272
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
399
1273
|
|
|
400
1274
|
type MeshRefineValidationSummary = {
|
|
401
1275
|
status: MeshRefineValidationStatus;
|
|
402
1276
|
required: true;
|
|
403
1277
|
commandsRun: Array<Record<string, unknown>>;
|
|
1278
|
+
bootstrapCommandsRun: Array<Record<string, unknown>>;
|
|
404
1279
|
rejectedCommands: Array<Record<string, unknown>>;
|
|
405
1280
|
skippedReason?: string;
|
|
1281
|
+
failureKind?: string;
|
|
1282
|
+
failureCode?: string;
|
|
406
1283
|
timeoutMs: number;
|
|
407
1284
|
outputLimitBytes: number;
|
|
1285
|
+
configSource?: string;
|
|
1286
|
+
configSourceType?: string;
|
|
1287
|
+
suggestions?: unknown[];
|
|
1288
|
+
suggestedConfig?: unknown;
|
|
408
1289
|
};
|
|
409
1290
|
|
|
1291
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
1292
|
+
|
|
1293
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
1294
|
+
status: MeshRefineStageStatus;
|
|
1295
|
+
equivalent: boolean;
|
|
1296
|
+
baseHead: string;
|
|
1297
|
+
branchHead: string;
|
|
1298
|
+
mergeBase?: string;
|
|
1299
|
+
mergedTree?: string;
|
|
1300
|
+
expectedPatchId?: string;
|
|
1301
|
+
actualPatchId?: string;
|
|
1302
|
+
durationMs: number;
|
|
1303
|
+
error?: string;
|
|
1304
|
+
stdout?: string;
|
|
1305
|
+
stderr?: string;
|
|
1306
|
+
actionableHint?: MeshRefineSubmoduleConflictHint;
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
type MeshRefineSubmoduleConflictHint = {
|
|
1310
|
+
kind: 'submodule_conflict';
|
|
1311
|
+
message: string;
|
|
1312
|
+
conflicts: Array<{
|
|
1313
|
+
path: string;
|
|
1314
|
+
baseCommit?: string;
|
|
1315
|
+
branchCommit?: string;
|
|
1316
|
+
}>;
|
|
1317
|
+
nextSteps: string[];
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
type MeshRefineSubmoduleAlignmentSummary = {
|
|
1321
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
1322
|
+
changedGitlinkPaths: string[];
|
|
1323
|
+
outOfSyncPaths: string[];
|
|
1324
|
+
updatedPaths: string[];
|
|
1325
|
+
verifiedPaths: string[];
|
|
1326
|
+
durationMs: number;
|
|
1327
|
+
reason?: string;
|
|
1328
|
+
command?: string;
|
|
1329
|
+
error?: string;
|
|
1330
|
+
stdout?: string;
|
|
1331
|
+
stderr?: string;
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
type MeshRefineSubmoduleReachabilityEntry = {
|
|
1335
|
+
path: string;
|
|
1336
|
+
commit: string;
|
|
1337
|
+
reachable: boolean;
|
|
1338
|
+
publishRequired?: boolean;
|
|
1339
|
+
autoPublishAllowed?: boolean;
|
|
1340
|
+
autoPublishAttempted?: boolean;
|
|
1341
|
+
autoPublishSucceeded?: boolean;
|
|
1342
|
+
autoPublishVerified?: boolean;
|
|
1343
|
+
autoPublishRefspec?: string;
|
|
1344
|
+
autoPublishSkippedReason?: string;
|
|
1345
|
+
importedFromWorktree?: boolean;
|
|
1346
|
+
checkedLocal?: boolean;
|
|
1347
|
+
localReachable?: boolean;
|
|
1348
|
+
remote?: string;
|
|
1349
|
+
remoteUrl?: string;
|
|
1350
|
+
remoteReachable?: boolean;
|
|
1351
|
+
remoteMainBranch?: string;
|
|
1352
|
+
remoteMainReachable?: boolean;
|
|
1353
|
+
fetchedFromOrigin?: boolean;
|
|
1354
|
+
error?: string;
|
|
1355
|
+
publishStdout?: string;
|
|
1356
|
+
publishStderr?: string;
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
type MeshRefineSubmoduleReachabilitySummary = {
|
|
1360
|
+
status: MeshRefineStageStatus;
|
|
1361
|
+
checked: number;
|
|
1362
|
+
unreachable: MeshRefineSubmoduleReachabilityEntry[];
|
|
1363
|
+
entries: MeshRefineSubmoduleReachabilityEntry[];
|
|
1364
|
+
durationMs: number;
|
|
1365
|
+
autoPublishAllowed?: boolean;
|
|
1366
|
+
autoPublishPolicySource?: string;
|
|
1367
|
+
error?: string;
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1371
|
+
|
|
1372
|
+
type MeshRefineJobHandle = {
|
|
1373
|
+
success: true;
|
|
1374
|
+
async: true;
|
|
1375
|
+
status: MeshRefineAsyncJobStatus;
|
|
1376
|
+
jobId: string;
|
|
1377
|
+
interactionId: string;
|
|
1378
|
+
meshId: string;
|
|
1379
|
+
nodeId: string;
|
|
1380
|
+
targetNodeId: string;
|
|
1381
|
+
targetDaemonId?: string;
|
|
1382
|
+
workspace?: string;
|
|
1383
|
+
startedAt: string;
|
|
1384
|
+
completedAt?: string;
|
|
1385
|
+
duplicate?: boolean;
|
|
1386
|
+
retryOfJobId?: string;
|
|
1387
|
+
eventDelivery: {
|
|
1388
|
+
pendingEvents: true;
|
|
1389
|
+
ledger: true;
|
|
1390
|
+
};
|
|
1391
|
+
evidence: {
|
|
1392
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
1393
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
1394
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
1399
|
+
|
|
410
1400
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
411
1401
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
412
1402
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
413
1403
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
414
1404
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
1405
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
415
1406
|
|
|
416
1407
|
function truncateValidationOutput(value: unknown): string {
|
|
417
1408
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -419,171 +1410,484 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
419
1410
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
420
1411
|
}
|
|
421
1412
|
|
|
422
|
-
function
|
|
1413
|
+
function recordMeshRefineStage(
|
|
1414
|
+
stages: Array<Record<string, unknown>>,
|
|
1415
|
+
stage: string,
|
|
1416
|
+
status: MeshRefineStageStatus,
|
|
1417
|
+
startedAt: number,
|
|
1418
|
+
details?: Record<string, unknown>,
|
|
1419
|
+
): void {
|
|
1420
|
+
stages.push({
|
|
1421
|
+
stage,
|
|
1422
|
+
status,
|
|
1423
|
+
durationMs: Date.now() - startedAt,
|
|
1424
|
+
...(details || {}),
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
|
|
1429
|
+
const refs = entries
|
|
1430
|
+
.map(entry => `${entry.path}@${entry.commit}`)
|
|
1431
|
+
.join(', ');
|
|
1432
|
+
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.`;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1436
|
+
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1437
|
+
return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
|
|
1438
|
+
}
|
|
1439
|
+
const loaded = loadMeshRefineConfig(mesh, workspace);
|
|
1440
|
+
if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1441
|
+
return { enabled: true, source: loaded.path || loaded.source };
|
|
1442
|
+
}
|
|
1443
|
+
return { enabled: false };
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
1447
|
+
const { execFileSync } = await import('node:child_process');
|
|
1448
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
1449
|
+
cwd,
|
|
1450
|
+
encoding: 'utf8',
|
|
1451
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1452
|
+
});
|
|
1453
|
+
if (!diff.trim()) return '';
|
|
1454
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
1455
|
+
cwd,
|
|
1456
|
+
input: diff,
|
|
1457
|
+
encoding: 'utf8',
|
|
1458
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1459
|
+
}).trim();
|
|
1460
|
+
return patchId.split(/\s+/)[0] || '';
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
1464
|
+
repoRoot: string,
|
|
1465
|
+
baseHead: string,
|
|
1466
|
+
branchHead: string,
|
|
1467
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
1468
|
+
const startedAt = Date.now();
|
|
423
1469
|
try {
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
:
|
|
429
|
-
|
|
430
|
-
|
|
1470
|
+
const { execFileSync } = await import('node:child_process');
|
|
1471
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
1472
|
+
cwd: repoRoot,
|
|
1473
|
+
encoding: 'utf8',
|
|
1474
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1475
|
+
});
|
|
1476
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
1477
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
1478
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
1479
|
+
if (!mergeBase || !mergedTree) {
|
|
1480
|
+
return {
|
|
1481
|
+
status: 'failed',
|
|
1482
|
+
equivalent: false,
|
|
1483
|
+
baseHead,
|
|
1484
|
+
branchHead,
|
|
1485
|
+
mergeBase: mergeBase || undefined,
|
|
1486
|
+
mergedTree: mergedTree || undefined,
|
|
1487
|
+
durationMs: Date.now() - startedAt,
|
|
1488
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
1489
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
1493
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
1494
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
1495
|
+
return {
|
|
1496
|
+
status: equivalent ? 'passed' : 'failed',
|
|
1497
|
+
equivalent,
|
|
1498
|
+
baseHead,
|
|
1499
|
+
branchHead,
|
|
1500
|
+
mergeBase,
|
|
1501
|
+
mergedTree,
|
|
1502
|
+
expectedPatchId,
|
|
1503
|
+
actualPatchId,
|
|
1504
|
+
durationMs: Date.now() - startedAt,
|
|
1505
|
+
};
|
|
1506
|
+
} catch (e: any) {
|
|
1507
|
+
return {
|
|
1508
|
+
status: 'failed',
|
|
1509
|
+
equivalent: false,
|
|
1510
|
+
baseHead,
|
|
1511
|
+
branchHead,
|
|
1512
|
+
durationMs: Date.now() - startedAt,
|
|
1513
|
+
error: e?.message || String(e),
|
|
1514
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1515
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1516
|
+
actionableHint: buildPatchEquivalenceSubmoduleConflictHint(
|
|
1517
|
+
repoRoot,
|
|
1518
|
+
baseHead,
|
|
1519
|
+
branchHead,
|
|
1520
|
+
`${e?.message || ''}\n${e?.stdout || ''}\n${e?.stderr || ''}`,
|
|
1521
|
+
),
|
|
1522
|
+
};
|
|
431
1523
|
}
|
|
432
1524
|
}
|
|
433
1525
|
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (/
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
1526
|
+
function buildPatchEquivalenceSubmoduleConflictHint(
|
|
1527
|
+
repoRoot: string,
|
|
1528
|
+
baseHead: string,
|
|
1529
|
+
branchHead: string,
|
|
1530
|
+
output: string,
|
|
1531
|
+
): MeshRefineSubmoduleConflictHint | undefined {
|
|
1532
|
+
if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return undefined;
|
|
1533
|
+
const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead)
|
|
1534
|
+
.map(path => ({
|
|
1535
|
+
path,
|
|
1536
|
+
baseCommit: readTreeObject(repoRoot, baseHead, path),
|
|
1537
|
+
branchCommit: readTreeObject(repoRoot, branchHead, path),
|
|
1538
|
+
}));
|
|
1539
|
+
if (conflicts.length === 0) return undefined;
|
|
1540
|
+
return {
|
|
1541
|
+
kind: 'submodule_conflict',
|
|
1542
|
+
message: 'Refinery could not synthesize a safe merge tree because the branch and base point the same submodule path at different commits.',
|
|
1543
|
+
conflicts,
|
|
1544
|
+
nextSteps: [
|
|
1545
|
+
'Inspect the listed submodule path in both base and branch: baseCommit is the commit currently recorded by the base workspace, branchCommit is the commit recorded by the worktree branch.',
|
|
1546
|
+
'Resolve the submodule first by checking out or creating the intended submodule commit, then commit the chosen gitlink in the root branch.',
|
|
1547
|
+
'Ensure the chosen submodule commit is reachable from the configured submodule remote main branch, then rerun mesh_refine_node.',
|
|
1548
|
+
],
|
|
1549
|
+
};
|
|
445
1550
|
}
|
|
446
1551
|
|
|
447
|
-
function
|
|
448
|
-
|
|
1552
|
+
function readChangedGitlinkPaths(repoRoot: string, fromRef: string, toRef: string): string[] {
|
|
1553
|
+
try {
|
|
1554
|
+
const output = execFileSync('git', ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
|
|
1555
|
+
cwd: repoRoot,
|
|
1556
|
+
encoding: 'utf8',
|
|
1557
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1558
|
+
});
|
|
1559
|
+
const paths = new Set<string>();
|
|
1560
|
+
for (const line of output.split('\n')) {
|
|
1561
|
+
if (!line.trim()) continue;
|
|
1562
|
+
const metaAndPath = line.split('\t');
|
|
1563
|
+
const meta = metaAndPath[0] || '';
|
|
1564
|
+
const path = metaAndPath[metaAndPath.length - 1]?.trim();
|
|
1565
|
+
if (!path) continue;
|
|
1566
|
+
const parts = meta.split(/\s+/);
|
|
1567
|
+
if (parts[0]?.includes('160000') || parts[1]?.includes('160000')) {
|
|
1568
|
+
paths.add(path);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return [...paths].sort();
|
|
1572
|
+
} catch {
|
|
1573
|
+
return [];
|
|
1574
|
+
}
|
|
449
1575
|
}
|
|
450
1576
|
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return
|
|
1577
|
+
function readTreeObject(repoRoot: string, ref: string, path: string): string | undefined {
|
|
1578
|
+
try {
|
|
1579
|
+
const output = execFileSync('git', ['ls-tree', ref, '--', path], {
|
|
1580
|
+
cwd: repoRoot,
|
|
1581
|
+
encoding: 'utf8',
|
|
1582
|
+
maxBuffer: 1024 * 1024,
|
|
1583
|
+
}).trim();
|
|
1584
|
+
const match = output.match(/\bcommit\s+([0-9a-f]{40})\b/i);
|
|
1585
|
+
return match?.[1];
|
|
1586
|
+
} catch {
|
|
1587
|
+
return undefined;
|
|
460
1588
|
}
|
|
1589
|
+
}
|
|
461
1590
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1591
|
+
async function alignRefinerySubmodulesAfterMerge(
|
|
1592
|
+
repoRoot: string,
|
|
1593
|
+
previousBaseHead: string,
|
|
1594
|
+
currentHead: string,
|
|
1595
|
+
options: { submoduleIgnorePaths?: string[] } = {},
|
|
1596
|
+
): Promise<MeshRefineSubmoduleAlignmentSummary> {
|
|
1597
|
+
const startedAt = Date.now();
|
|
1598
|
+
const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead)
|
|
1599
|
+
.filter(path => !(options.submoduleIgnorePaths || []).includes(path));
|
|
1600
|
+
const preStatus = await getGitRepoStatus(repoRoot, {
|
|
1601
|
+
includeSubmodules: true,
|
|
1602
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1603
|
+
timeoutMs: 15_000,
|
|
1604
|
+
});
|
|
1605
|
+
const outOfSyncPaths = (preStatus.submodules || [])
|
|
1606
|
+
.filter(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error)
|
|
1607
|
+
.map(submodule => submodule.path);
|
|
1608
|
+
const updatePaths = [...new Set([...changedGitlinkPaths, ...outOfSyncPaths])].sort();
|
|
466
1609
|
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
scriptName = second;
|
|
478
|
-
args = [scriptName];
|
|
479
|
-
} else {
|
|
480
|
-
return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
|
|
1610
|
+
if (updatePaths.length === 0) {
|
|
1611
|
+
return {
|
|
1612
|
+
status: 'skipped',
|
|
1613
|
+
changedGitlinkPaths,
|
|
1614
|
+
outOfSyncPaths,
|
|
1615
|
+
updatedPaths: [],
|
|
1616
|
+
verifiedPaths: [],
|
|
1617
|
+
durationMs: Date.now() - startedAt,
|
|
1618
|
+
reason: 'no_changed_or_out_of_sync_submodules',
|
|
1619
|
+
};
|
|
481
1620
|
}
|
|
482
1621
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1622
|
+
const commandArgs = ['submodule', 'update', '--init', '--recursive', '--', ...updatePaths];
|
|
1623
|
+
try {
|
|
1624
|
+
const { execFile } = await import('node:child_process');
|
|
1625
|
+
const { promisify } = await import('node:util');
|
|
1626
|
+
const execFileAsync = promisify(execFile);
|
|
1627
|
+
const result = await execFileAsync('git', commandArgs, {
|
|
1628
|
+
cwd: repoRoot,
|
|
1629
|
+
encoding: 'utf8',
|
|
1630
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1631
|
+
timeout: 60_000,
|
|
1632
|
+
});
|
|
1633
|
+
const postStatus = await getGitRepoStatus(repoRoot, {
|
|
1634
|
+
includeSubmodules: true,
|
|
1635
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1636
|
+
timeoutMs: 15_000,
|
|
1637
|
+
});
|
|
1638
|
+
const remaining = (postStatus.submodules || [])
|
|
1639
|
+
.filter(submodule => updatePaths.includes(submodule.path) && (submodule.dirty || submodule.outOfSync || !!submodule.error));
|
|
1640
|
+
return {
|
|
1641
|
+
status: remaining.length === 0 ? 'passed' : 'failed',
|
|
1642
|
+
changedGitlinkPaths,
|
|
1643
|
+
outOfSyncPaths,
|
|
1644
|
+
updatedPaths: updatePaths,
|
|
1645
|
+
verifiedPaths: updatePaths.filter(path => !remaining.some(submodule => submodule.path === path)),
|
|
1646
|
+
durationMs: Date.now() - startedAt,
|
|
1647
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1648
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
1649
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
1650
|
+
...(remaining.length > 0 ? { error: `Submodule checkout remained out of sync after update: ${remaining.map(entry => entry.path).join(', ')}` } : {}),
|
|
1651
|
+
};
|
|
1652
|
+
} catch (e: any) {
|
|
1653
|
+
return {
|
|
1654
|
+
status: 'failed',
|
|
1655
|
+
changedGitlinkPaths,
|
|
1656
|
+
outOfSyncPaths,
|
|
1657
|
+
updatedPaths: updatePaths,
|
|
1658
|
+
verifiedPaths: [],
|
|
1659
|
+
durationMs: Date.now() - startedAt,
|
|
1660
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1661
|
+
error: e?.message || String(e),
|
|
1662
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1663
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1664
|
+
};
|
|
488
1665
|
}
|
|
489
|
-
|
|
490
|
-
return {
|
|
491
|
-
command: {
|
|
492
|
-
command,
|
|
493
|
-
args,
|
|
494
|
-
displayCommand: [command, ...args].join(' '),
|
|
495
|
-
category,
|
|
496
|
-
source,
|
|
497
|
-
},
|
|
498
|
-
};
|
|
499
1666
|
}
|
|
500
1667
|
|
|
501
|
-
function
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1668
|
+
async function runMeshRefineSubmoduleReachabilityGate(
|
|
1669
|
+
repoRoot: string,
|
|
1670
|
+
mergedTree: string,
|
|
1671
|
+
options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
|
|
1672
|
+
): Promise<MeshRefineSubmoduleReachabilitySummary> {
|
|
1673
|
+
const startedAt = Date.now();
|
|
1674
|
+
const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
|
|
1675
|
+
try {
|
|
1676
|
+
const { execFile } = await import('node:child_process');
|
|
1677
|
+
const { promisify } = await import('node:util');
|
|
1678
|
+
const execFileAsync = promisify(execFile);
|
|
1679
|
+
const runGit = async (cwd: string, args: string[]): Promise<string> => {
|
|
1680
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
1681
|
+
cwd,
|
|
1682
|
+
encoding: 'utf8',
|
|
1683
|
+
timeout: 30_000,
|
|
1684
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1685
|
+
windowsHide: true,
|
|
514
1686
|
});
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
1687
|
+
return String(stdout || '');
|
|
1688
|
+
};
|
|
1689
|
+
const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
|
|
1690
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1691
|
+
await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
|
|
1692
|
+
};
|
|
1693
|
+
const publishCommitToRemoteMain = async (submodulePath: string, commit: string, branch = 'main'): Promise<{ stdout: string; stderr: string; refspec: string }> => {
|
|
1694
|
+
const refspec = `${commit}:refs/heads/${branch}`;
|
|
1695
|
+
const { stdout, stderr } = await execFileAsync('git', ['push', 'origin', refspec], {
|
|
1696
|
+
cwd: submodulePath,
|
|
1697
|
+
encoding: 'utf8',
|
|
1698
|
+
timeout: 30_000,
|
|
1699
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1700
|
+
windowsHide: true,
|
|
1701
|
+
});
|
|
1702
|
+
return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
|
|
1703
|
+
};
|
|
1704
|
+
const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
|
|
1705
|
+
if (!fs.existsSync(worktreeSubmodulePath)) return false;
|
|
1706
|
+
try {
|
|
1707
|
+
await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1708
|
+
} catch {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
|
|
1712
|
+
await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1713
|
+
return true;
|
|
1714
|
+
};
|
|
522
1715
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
1716
|
+
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1717
|
+
const gitlinks = treeOutput
|
|
1718
|
+
.split('\0')
|
|
1719
|
+
.filter(Boolean)
|
|
1720
|
+
.map(record => {
|
|
1721
|
+
const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
1722
|
+
return match ? { commit: match[1], path: match[2] } : null;
|
|
1723
|
+
})
|
|
1724
|
+
.filter((entry): entry is { commit: string; path: string } => !!entry);
|
|
1725
|
+
|
|
1726
|
+
for (const gitlink of gitlinks) {
|
|
1727
|
+
const submodulePath = pathResolve(repoRoot, gitlink.path);
|
|
1728
|
+
const entry: MeshRefineSubmoduleReachabilityEntry = {
|
|
1729
|
+
path: gitlink.path,
|
|
1730
|
+
commit: gitlink.commit,
|
|
1731
|
+
reachable: false,
|
|
1732
|
+
};
|
|
1733
|
+
try {
|
|
1734
|
+
if (!fs.existsSync(submodulePath)) {
|
|
1735
|
+
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1736
|
+
entry.publishRequired = true;
|
|
1737
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1738
|
+
entry.autoPublishAllowed = true;
|
|
1739
|
+
entry.autoPublishAttempted = false;
|
|
1740
|
+
entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
|
|
1741
|
+
}
|
|
1742
|
+
entries.push(entry);
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
540
1745
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1746
|
+
entry.checkedLocal = true;
|
|
1747
|
+
try {
|
|
1748
|
+
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1749
|
+
entry.localReachable = true;
|
|
1750
|
+
} catch {
|
|
1751
|
+
entry.localReachable = false;
|
|
1752
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
|
|
1753
|
+
try {
|
|
1754
|
+
const imported = await importCommitFromWorktreeSubmodule(
|
|
1755
|
+
submodulePath,
|
|
1756
|
+
pathResolve(options.worktreeRoot, gitlink.path),
|
|
1757
|
+
gitlink.commit,
|
|
1758
|
+
);
|
|
1759
|
+
if (imported) {
|
|
1760
|
+
entry.localReachable = true;
|
|
1761
|
+
entry.importedFromWorktree = true;
|
|
1762
|
+
}
|
|
1763
|
+
} catch (importError: any) {
|
|
1764
|
+
entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
// Probe the submodule remote before allowing cleanup/completion.
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
try {
|
|
1771
|
+
entry.remote = 'origin';
|
|
1772
|
+
let remoteUrl = '';
|
|
1773
|
+
try {
|
|
1774
|
+
remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
|
|
1775
|
+
if (!remoteUrl) throw new Error('origin remote has no URL');
|
|
1776
|
+
entry.remoteUrl = remoteUrl;
|
|
1777
|
+
} catch {
|
|
1778
|
+
entry.error = 'Submodule remote reachability check failed: no configured origin remote';
|
|
1779
|
+
entry.publishRequired = true;
|
|
1780
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1781
|
+
entry.autoPublishAllowed = true;
|
|
1782
|
+
entry.autoPublishAttempted = false;
|
|
1783
|
+
entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
|
|
1784
|
+
}
|
|
1785
|
+
entries.push(entry);
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
entry.remoteMainBranch = 'main';
|
|
1789
|
+
try {
|
|
1790
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1791
|
+
entry.fetchedFromOrigin = true;
|
|
1792
|
+
entry.remoteReachable = true;
|
|
1793
|
+
entry.remoteMainReachable = true;
|
|
1794
|
+
entry.reachable = true;
|
|
1795
|
+
} catch (e: any) {
|
|
1796
|
+
entry.remoteReachable = false;
|
|
1797
|
+
entry.remoteMainReachable = false;
|
|
1798
|
+
entry.publishRequired = true;
|
|
1799
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1800
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1801
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
|
|
1802
|
+
entry.autoPublishAllowed = true;
|
|
1803
|
+
entry.autoPublishAttempted = true;
|
|
1804
|
+
try {
|
|
1805
|
+
const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, 'main');
|
|
1806
|
+
entry.autoPublishRefspec = publish.refspec;
|
|
1807
|
+
entry.publishStdout = truncateValidationOutput(publish.stdout);
|
|
1808
|
+
entry.publishStderr = truncateValidationOutput(publish.stderr);
|
|
1809
|
+
entry.autoPublishSucceeded = true;
|
|
1810
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1811
|
+
entry.fetchedFromOrigin = true;
|
|
1812
|
+
entry.remoteReachable = true;
|
|
1813
|
+
entry.remoteMainReachable = true;
|
|
1814
|
+
entry.autoPublishVerified = true;
|
|
1815
|
+
entry.publishRequired = false;
|
|
1816
|
+
entry.reachable = true;
|
|
1817
|
+
entry.error = undefined;
|
|
1818
|
+
} catch (publishError: any) {
|
|
1819
|
+
entry.autoPublishSucceeded = false;
|
|
1820
|
+
entry.autoPublishVerified = false;
|
|
1821
|
+
const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
|
|
1822
|
+
entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
|
|
1823
|
+
}
|
|
1824
|
+
} else if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1825
|
+
entry.autoPublishAllowed = true;
|
|
1826
|
+
entry.autoPublishAttempted = false;
|
|
1827
|
+
entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
|
|
1828
|
+
|| 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
} catch (e: any) {
|
|
1832
|
+
entry.remoteReachable = false;
|
|
1833
|
+
entry.remoteMainReachable = false;
|
|
1834
|
+
entry.publishRequired = true;
|
|
1835
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1836
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1837
|
+
}
|
|
1838
|
+
} catch (e: any) {
|
|
1839
|
+
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
1840
|
+
entry.publishRequired = true;
|
|
1841
|
+
}
|
|
1842
|
+
entries.push(entry);
|
|
574
1843
|
}
|
|
1844
|
+
|
|
1845
|
+
const unreachable = entries.filter(entry => !entry.reachable);
|
|
1846
|
+
return {
|
|
1847
|
+
status: unreachable.length ? 'failed' : 'passed',
|
|
1848
|
+
checked: entries.length,
|
|
1849
|
+
unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
|
|
1850
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
|
|
1851
|
+
durationMs: Date.now() - startedAt,
|
|
1852
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1853
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1854
|
+
};
|
|
1855
|
+
} catch (e: any) {
|
|
1856
|
+
const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
|
|
1857
|
+
return {
|
|
1858
|
+
status: 'failed',
|
|
1859
|
+
checked: entries.length,
|
|
1860
|
+
unreachable,
|
|
1861
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
|
|
1862
|
+
durationMs: Date.now() - startedAt,
|
|
1863
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1864
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1865
|
+
error: truncateValidationOutput(e?.message || String(e)),
|
|
1866
|
+
};
|
|
575
1867
|
}
|
|
1868
|
+
}
|
|
576
1869
|
|
|
1870
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1871
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1872
|
+
const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
|
|
1873
|
+
displayCommand: command.displayCommand,
|
|
1874
|
+
category: command.category,
|
|
1875
|
+
source: command.source,
|
|
1876
|
+
cwd: command.cwd,
|
|
1877
|
+
timeoutMs: command.timeoutMs,
|
|
1878
|
+
});
|
|
577
1879
|
return {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1880
|
+
source: plan.source,
|
|
1881
|
+
sourceType: plan.sourceType,
|
|
1882
|
+
bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
|
|
1883
|
+
commands: plan.commands.map(mapCommand),
|
|
1884
|
+
unavailableReason: plan.unavailableReason,
|
|
1885
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1886
|
+
suggestions: plan.suggestions,
|
|
1887
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1888
|
+
note: plan.sourceType === 'unavailable'
|
|
1889
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1890
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
587
1891
|
};
|
|
588
1892
|
}
|
|
589
1893
|
|
|
@@ -591,60 +1895,119 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
591
1895
|
const { execFile } = await import('node:child_process');
|
|
592
1896
|
const { promisify } = await import('node:util');
|
|
593
1897
|
const execFileAsync = promisify(execFile);
|
|
594
|
-
const selection =
|
|
1898
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
595
1899
|
const summary: MeshRefineValidationSummary = {
|
|
596
1900
|
status: 'skipped',
|
|
597
1901
|
required: true,
|
|
598
1902
|
commandsRun: [],
|
|
1903
|
+
bootstrapCommandsRun: [],
|
|
599
1904
|
rejectedCommands: selection.rejectedCommands,
|
|
600
1905
|
skippedReason: undefined,
|
|
601
1906
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
602
1907
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1908
|
+
configSource: selection.source,
|
|
1909
|
+
configSourceType: selection.sourceType,
|
|
1910
|
+
suggestions: selection.suggestions,
|
|
1911
|
+
suggestedConfig: selection.suggestedConfig,
|
|
603
1912
|
};
|
|
604
1913
|
|
|
605
1914
|
if (!selection.commands.length) {
|
|
606
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1915
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
607
1916
|
return summary;
|
|
608
1917
|
}
|
|
609
1918
|
|
|
610
|
-
|
|
1919
|
+
const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
|
|
1920
|
+
command: candidate.command,
|
|
1921
|
+
args: candidate.args,
|
|
1922
|
+
displayCommand: candidate.displayCommand,
|
|
1923
|
+
category: candidate.category,
|
|
1924
|
+
source: candidate.source,
|
|
1925
|
+
cwd,
|
|
1926
|
+
passed,
|
|
1927
|
+
durationMs: Date.now() - startedAt,
|
|
1928
|
+
stdout: truncateValidationOutput(result?.stdout),
|
|
1929
|
+
stderr: truncateValidationOutput(result?.stderr || result?.message),
|
|
1930
|
+
...extras,
|
|
1931
|
+
});
|
|
1932
|
+
const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
|
|
1933
|
+
const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
|
|
1934
|
+
return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
|
|
1935
|
+
&& candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
|
|
1936
|
+
};
|
|
1937
|
+
const dependenciesLikelyMissing = (cwd: string): boolean => {
|
|
1938
|
+
if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
|
|
1939
|
+
if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
|
|
1940
|
+
return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
|
|
1941
|
+
.some(lock => fs.existsSync(pathJoin(cwd, lock)));
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1944
|
+
for (const candidate of selection.bootstrapCommands) {
|
|
611
1945
|
const startedAt = Date.now();
|
|
1946
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1947
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
612
1948
|
try {
|
|
613
1949
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
614
|
-
cwd
|
|
1950
|
+
cwd,
|
|
615
1951
|
encoding: 'utf8',
|
|
616
|
-
timeout
|
|
617
|
-
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
618
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
619
|
-
});
|
|
620
|
-
summary.commandsRun.push({
|
|
621
|
-
command: candidate.command,
|
|
622
|
-
args: candidate.args,
|
|
623
|
-
displayCommand: candidate.displayCommand,
|
|
624
|
-
category: candidate.category,
|
|
625
|
-
source: candidate.source,
|
|
626
|
-
passed: true,
|
|
627
|
-
exitCode: 0,
|
|
628
|
-
durationMs: Date.now() - startedAt,
|
|
629
|
-
stdout: truncateValidationOutput(result.stdout),
|
|
630
|
-
stderr: truncateValidationOutput(result.stderr),
|
|
1952
|
+
timeout,
|
|
1953
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1954
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
631
1955
|
});
|
|
1956
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
632
1957
|
} catch (error: any) {
|
|
633
|
-
summary.
|
|
634
|
-
command: candidate.command,
|
|
635
|
-
args: candidate.args,
|
|
636
|
-
displayCommand: candidate.displayCommand,
|
|
637
|
-
category: candidate.category,
|
|
638
|
-
source: candidate.source,
|
|
639
|
-
passed: false,
|
|
1958
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
640
1959
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
641
1960
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
642
1961
|
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1962
|
+
failureKind: 'dependency_bootstrap_failed',
|
|
1963
|
+
}));
|
|
1964
|
+
summary.status = 'failed';
|
|
1965
|
+
summary.failureKind = 'dependency_bootstrap_failed';
|
|
1966
|
+
summary.failureCode = 'dependency_bootstrap_failed';
|
|
1967
|
+
return summary;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
for (const candidate of selection.commands) {
|
|
1972
|
+
const startedAt = Date.now();
|
|
1973
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1974
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1975
|
+
if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
|
|
1976
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
|
|
1977
|
+
stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
|
|
1978
|
+
}, false, {
|
|
1979
|
+
exitCode: null,
|
|
1980
|
+
skipped: true,
|
|
1981
|
+
failureKind: 'missing_dependencies',
|
|
1982
|
+
}));
|
|
1983
|
+
summary.status = 'failed';
|
|
1984
|
+
summary.failureKind = 'missing_dependencies';
|
|
1985
|
+
summary.failureCode = 'missing_dependencies';
|
|
1986
|
+
return summary;
|
|
1987
|
+
}
|
|
1988
|
+
try {
|
|
1989
|
+
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
1990
|
+
cwd,
|
|
1991
|
+
encoding: 'utf8',
|
|
1992
|
+
timeout,
|
|
1993
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1994
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
646
1995
|
});
|
|
1996
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1997
|
+
} catch (error: any) {
|
|
1998
|
+
const stderr = truncateValidationOutput(error?.stderr || error?.message);
|
|
1999
|
+
const missingDependencyFailure = /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
|
|
2000
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
2001
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
2002
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
2003
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
2004
|
+
...(missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
|
|
2005
|
+
}));
|
|
647
2006
|
summary.status = 'failed';
|
|
2007
|
+
if (missingDependencyFailure) {
|
|
2008
|
+
summary.failureKind = 'missing_dependencies';
|
|
2009
|
+
summary.failureCode = 'missing_dependencies';
|
|
2010
|
+
}
|
|
648
2011
|
return summary;
|
|
649
2012
|
}
|
|
650
2013
|
}
|
|
@@ -776,6 +2139,8 @@ export interface CommandRouterDeps {
|
|
|
776
2139
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
777
2140
|
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
778
2141
|
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
2142
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
2143
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
779
2144
|
}
|
|
780
2145
|
|
|
781
2146
|
export interface CommandRouterResult {
|
|
@@ -887,42 +2252,266 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
887
2252
|
};
|
|
888
2253
|
}
|
|
889
2254
|
|
|
2255
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
2256
|
+
const raw = hostAddress.trim();
|
|
2257
|
+
if (!raw) throw new Error('hostAddress required');
|
|
2258
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
2259
|
+
url.pathname = '/api/v1/command';
|
|
2260
|
+
url.search = '';
|
|
2261
|
+
url.hash = '';
|
|
2262
|
+
return url.toString();
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
2266
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
2267
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
2268
|
+
? args.memberNode as Record<string, any>
|
|
2269
|
+
: null;
|
|
2270
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
2271
|
+
? (requestedNodeId
|
|
2272
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
2273
|
+
: mesh.nodes[0])
|
|
2274
|
+
: null;
|
|
2275
|
+
const source = explicit || configured;
|
|
2276
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
2277
|
+
? source.workspace.trim()
|
|
2278
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
2279
|
+
? args.workspace.trim()
|
|
2280
|
+
: process.cwd();
|
|
2281
|
+
if (!workspace) return null;
|
|
2282
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
2283
|
+
? source.id.trim()
|
|
2284
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
2285
|
+
? source.nodeId.trim()
|
|
2286
|
+
: undefined;
|
|
2287
|
+
return {
|
|
2288
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
2289
|
+
workspace,
|
|
2290
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
2291
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
2292
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
2293
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
2294
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
2295
|
+
role: 'member',
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
|
|
890
2299
|
export class DaemonCommandRouter {
|
|
891
2300
|
private deps: CommandRouterDeps;
|
|
892
2301
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
893
2302
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
894
2303
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
895
2304
|
private inlineMeshCache = new Map<string, any>();
|
|
2305
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
2306
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
|
|
2307
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
2308
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
2309
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
2310
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
896
2311
|
|
|
897
2312
|
constructor(deps: CommandRouterDeps) {
|
|
898
2313
|
this.deps = deps;
|
|
899
2314
|
}
|
|
900
2315
|
|
|
2316
|
+
private cloneJsonValue<T>(value: T): T {
|
|
2317
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
2318
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
2322
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
2323
|
+
const inlineNodesById = new Map<string, any>();
|
|
2324
|
+
for (const node of mesh.nodes) {
|
|
2325
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
2326
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
2327
|
+
}
|
|
2328
|
+
if (!inlineNodesById.size) return snapshot;
|
|
2329
|
+
|
|
2330
|
+
let changed = false;
|
|
2331
|
+
const unavailableNodeIds = new Set<string>();
|
|
2332
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
2333
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
2334
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
2335
|
+
const nodeId = readStringValue(entry);
|
|
2336
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
2340
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
2341
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
2342
|
+
if (!inlineNode) return statusNode;
|
|
2343
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
2344
|
+
if (!liveGit) return statusNode;
|
|
2345
|
+
const nextStatus = { ...statusNode };
|
|
2346
|
+
nextStatus.git = liveGit;
|
|
2347
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
2348
|
+
applyInlineMeshBranchConvergence(mesh, inlineNode, nextStatus);
|
|
2349
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
2350
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
2351
|
+
const connectionState = readStringValue(connection.state);
|
|
2352
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
2353
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
2354
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
2355
|
+
}
|
|
2356
|
+
delete nextStatus.gitProbePending;
|
|
2357
|
+
const error = readStringValue(nextStatus.error);
|
|
2358
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
2359
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
2360
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
2361
|
+
changed = true;
|
|
2362
|
+
return nextStatus;
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2365
|
+
const aggregateDirectTruthSatisfied = sourceOfTruth.coordinatorOwnsLiveTruth === true
|
|
2366
|
+
|| directPeerTruth.satisfied === true;
|
|
2367
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied)) return snapshot;
|
|
2368
|
+
const nextSourceOfTruth = {
|
|
2369
|
+
...sourceOfTruth,
|
|
2370
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
2371
|
+
directPeerTruth: {
|
|
2372
|
+
...directPeerTruth,
|
|
2373
|
+
satisfied: options?.requireDirectPeerTruth === true
|
|
2374
|
+
? aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0
|
|
2375
|
+
: directPeerTruth.satisfied,
|
|
2376
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
2377
|
+
},
|
|
2378
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
2379
|
+
coordinatorOwnsLiveTruth: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0,
|
|
2380
|
+
currentStatus: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
2381
|
+
} : {}),
|
|
2382
|
+
} : {}),
|
|
2383
|
+
};
|
|
2384
|
+
return {
|
|
2385
|
+
...snapshot,
|
|
2386
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied ? {
|
|
2387
|
+
success: false,
|
|
2388
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
2389
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
2390
|
+
} : {}),
|
|
2391
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
2392
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodes),
|
|
2393
|
+
nodes,
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
2398
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
2399
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
2400
|
+
if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
|
|
2401
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
2402
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
2403
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
2404
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
2405
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
2406
|
+
? snapshot.sourceOfTruth
|
|
2407
|
+
: {};
|
|
2408
|
+
snapshot.sourceOfTruth = {
|
|
2409
|
+
...sourceOfTruth,
|
|
2410
|
+
aggregateSnapshot: {
|
|
2411
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
2412
|
+
? sourceOfTruth.aggregateSnapshot
|
|
2413
|
+
: {}),
|
|
2414
|
+
owner: 'coordinator_daemon_memory',
|
|
2415
|
+
cached: true,
|
|
2416
|
+
source: 'memory',
|
|
2417
|
+
refreshReason: 'memory_cache_hit',
|
|
2418
|
+
ageMs,
|
|
2419
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
2420
|
+
returnedAt: new Date().toISOString(),
|
|
2421
|
+
},
|
|
2422
|
+
};
|
|
2423
|
+
return snapshot;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
2427
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
2428
|
+
const builtAt = Date.now();
|
|
2429
|
+
const next = this.cloneJsonValue(snapshot);
|
|
2430
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
2431
|
+
? next.sourceOfTruth
|
|
2432
|
+
: {};
|
|
2433
|
+
next.sourceOfTruth = {
|
|
2434
|
+
...sourceOfTruth,
|
|
2435
|
+
aggregateSnapshot: {
|
|
2436
|
+
owner: 'coordinator_daemon_memory',
|
|
2437
|
+
cached: false,
|
|
2438
|
+
source: 'live_refresh',
|
|
2439
|
+
refreshReason,
|
|
2440
|
+
ageMs: 0,
|
|
2441
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
2442
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
2443
|
+
},
|
|
2444
|
+
};
|
|
2445
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
|
|
2446
|
+
return next;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
901
2449
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
902
2450
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
903
|
-
this.
|
|
904
|
-
return inlineMesh as any;
|
|
2451
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
905
2452
|
}
|
|
906
2453
|
return this.inlineMeshCache.get(meshId);
|
|
907
2454
|
}
|
|
908
2455
|
|
|
2456
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
2457
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
2458
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
2459
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
2460
|
+
if (cached) {
|
|
2461
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
2462
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
2463
|
+
return merged;
|
|
2464
|
+
}
|
|
2465
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
2466
|
+
return sanitizedInlineMesh as any;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
909
2469
|
private async getMeshForCommand(
|
|
910
2470
|
meshId: string,
|
|
911
2471
|
inlineMesh?: unknown,
|
|
912
2472
|
options?: { preferInline?: boolean },
|
|
913
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
2473
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
914
2474
|
const preferInline = options?.preferInline === true;
|
|
915
2475
|
if (preferInline) {
|
|
916
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
917
|
-
if (cached)
|
|
2476
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
2477
|
+
if (cached) {
|
|
2478
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
2479
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
2480
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
2481
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
2482
|
+
}
|
|
2483
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
2484
|
+
}
|
|
2485
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
2486
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
2487
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
2488
|
+
}
|
|
918
2489
|
}
|
|
919
2490
|
try {
|
|
920
2491
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
921
2492
|
const mesh = getMesh(meshId);
|
|
922
|
-
if (mesh) return { mesh, inline: false };
|
|
2493
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
923
2494
|
} catch { /* fall through to inline cache */ }
|
|
924
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
925
|
-
|
|
2495
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
2496
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
2497
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
2498
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
2502
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
|
|
2506
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
2507
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
2508
|
+
const mesh = meshRecord?.mesh;
|
|
2509
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2510
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
2511
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
2512
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
2513
|
+
}
|
|
2514
|
+
return null;
|
|
926
2515
|
}
|
|
927
2516
|
|
|
928
2517
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -932,6 +2521,7 @@ export class DaemonCommandRouter {
|
|
|
932
2521
|
else mesh.nodes.push(node);
|
|
933
2522
|
mesh.updatedAt = new Date().toISOString();
|
|
934
2523
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2524
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
935
2525
|
}
|
|
936
2526
|
|
|
937
2527
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -941,6 +2531,7 @@ export class DaemonCommandRouter {
|
|
|
941
2531
|
mesh.nodes.splice(idx, 1);
|
|
942
2532
|
mesh.updatedAt = new Date().toISOString();
|
|
943
2533
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2534
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
944
2535
|
return true;
|
|
945
2536
|
}
|
|
946
2537
|
|
|
@@ -1219,6 +2810,7 @@ export class DaemonCommandRouter {
|
|
|
1219
2810
|
const deletedSessionIds: string[] = [];
|
|
1220
2811
|
const skippedSessionIds: string[] = [];
|
|
1221
2812
|
const skippedLiveSessionIds: string[] = [];
|
|
2813
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1222
2814
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1223
2815
|
const recordsRemainSessionIds: string[] = [];
|
|
1224
2816
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1253,6 +2845,12 @@ export class DaemonCommandRouter {
|
|
|
1253
2845
|
const completed = this.isCompletedHostedSession(record);
|
|
1254
2846
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1255
2847
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
2848
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
2849
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
2850
|
+
skippedSessionIds.push(sessionId);
|
|
2851
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
1256
2854
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1257
2855
|
skippedSessionIds.push(sessionId);
|
|
1258
2856
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1322,6 +2920,7 @@ export class DaemonCommandRouter {
|
|
|
1322
2920
|
deletedSessionIds,
|
|
1323
2921
|
skippedSessionIds,
|
|
1324
2922
|
skippedLiveSessionIds,
|
|
2923
|
+
skippedCoordinatorSessionIds,
|
|
1325
2924
|
...(deleteUnsupported ? {
|
|
1326
2925
|
deleteUnsupported: true,
|
|
1327
2926
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1462,6 +3061,567 @@ export class DaemonCommandRouter {
|
|
|
1462
3061
|
}
|
|
1463
3062
|
}
|
|
1464
3063
|
|
|
3064
|
+
|
|
3065
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
3066
|
+
return `${meshId}:${nodeId}`;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
private buildRefineJobHandle(args: {
|
|
3070
|
+
meshId: string;
|
|
3071
|
+
nodeId: string;
|
|
3072
|
+
node?: any;
|
|
3073
|
+
status?: MeshRefineAsyncJobStatus;
|
|
3074
|
+
startedAt?: string;
|
|
3075
|
+
completedAt?: string;
|
|
3076
|
+
jobId?: string;
|
|
3077
|
+
interactionId?: string;
|
|
3078
|
+
retryOfJobId?: string;
|
|
3079
|
+
}): MeshRefineJobHandle {
|
|
3080
|
+
return {
|
|
3081
|
+
success: true,
|
|
3082
|
+
async: true,
|
|
3083
|
+
status: args.status || 'accepted',
|
|
3084
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
3085
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
3086
|
+
meshId: args.meshId,
|
|
3087
|
+
nodeId: args.nodeId,
|
|
3088
|
+
targetNodeId: args.nodeId,
|
|
3089
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
3090
|
+
workspace: readStringValue(args.node?.workspace),
|
|
3091
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
3092
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
3093
|
+
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
3094
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
3095
|
+
evidence: {
|
|
3096
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
3097
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
3098
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
3099
|
+
},
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
3104
|
+
const metadataEvent = {
|
|
3105
|
+
source: 'refine_mesh_node_async_job',
|
|
3106
|
+
jobId: handle.jobId,
|
|
3107
|
+
interactionId: handle.interactionId,
|
|
3108
|
+
meshId: handle.meshId,
|
|
3109
|
+
nodeId: handle.targetNodeId,
|
|
3110
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3111
|
+
workspace: handle.workspace,
|
|
3112
|
+
status: handle.status,
|
|
3113
|
+
startedAt: handle.startedAt,
|
|
3114
|
+
completedAt: handle.completedAt,
|
|
3115
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3116
|
+
...(result ? { result } : {}),
|
|
3117
|
+
};
|
|
3118
|
+
const eventPayload = {
|
|
3119
|
+
event,
|
|
3120
|
+
meshId: handle.meshId,
|
|
3121
|
+
nodeLabel: handle.targetNodeId,
|
|
3122
|
+
nodeId: handle.targetNodeId,
|
|
3123
|
+
workspace: handle.workspace,
|
|
3124
|
+
metadataEvent,
|
|
3125
|
+
queuedAt: Date.now(),
|
|
3126
|
+
};
|
|
3127
|
+
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
3128
|
+
const forwarded = handleMeshForwardEvent(
|
|
3129
|
+
{ instanceManager: this.deps.instanceManager } as any,
|
|
3130
|
+
{
|
|
3131
|
+
event,
|
|
3132
|
+
meshId: handle.meshId,
|
|
3133
|
+
nodeId: handle.targetNodeId,
|
|
3134
|
+
workspace: handle.workspace,
|
|
3135
|
+
jobId: handle.jobId,
|
|
3136
|
+
interactionId: handle.interactionId,
|
|
3137
|
+
status: handle.status,
|
|
3138
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3139
|
+
startedAt: handle.startedAt,
|
|
3140
|
+
completedAt: handle.completedAt,
|
|
3141
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3142
|
+
...(result ? { result } : {}),
|
|
3143
|
+
},
|
|
3144
|
+
);
|
|
3145
|
+
if (forwarded?.success === true) return;
|
|
3146
|
+
LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
|
|
3147
|
+
}
|
|
3148
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
3152
|
+
try {
|
|
3153
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3154
|
+
appendLedgerEntry(handle.meshId, {
|
|
3155
|
+
kind,
|
|
3156
|
+
nodeId: handle.targetNodeId,
|
|
3157
|
+
payload: {
|
|
3158
|
+
source: 'refine_mesh_node_async_job',
|
|
3159
|
+
refineJob: {
|
|
3160
|
+
jobId: handle.jobId,
|
|
3161
|
+
interactionId: handle.interactionId,
|
|
3162
|
+
status: handle.status,
|
|
3163
|
+
meshId: handle.meshId,
|
|
3164
|
+
nodeId: handle.targetNodeId,
|
|
3165
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3166
|
+
workspace: handle.workspace,
|
|
3167
|
+
startedAt: handle.startedAt,
|
|
3168
|
+
completedAt: handle.completedAt,
|
|
3169
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3170
|
+
},
|
|
3171
|
+
async: true,
|
|
3172
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3173
|
+
...(result ? {
|
|
3174
|
+
success: result.success === true,
|
|
3175
|
+
result,
|
|
3176
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
3177
|
+
} : {}),
|
|
3178
|
+
},
|
|
3179
|
+
});
|
|
3180
|
+
} catch (e: any) {
|
|
3181
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
3186
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
3187
|
+
try {
|
|
3188
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3189
|
+
const mesh = meshRecord?.mesh;
|
|
3190
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3191
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
3192
|
+
|
|
3193
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
3194
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
const sourceNode = node.clonedFromNodeId
|
|
3198
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
3199
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
3200
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
3201
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
3202
|
+
|
|
3203
|
+
const { execFile } = await import('node:child_process');
|
|
3204
|
+
const { promisify } = await import('node:util');
|
|
3205
|
+
const execFileAsync = promisify(execFile);
|
|
3206
|
+
|
|
3207
|
+
const resolveStarted = Date.now();
|
|
3208
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
3209
|
+
const branch = branchStdout.trim();
|
|
3210
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
3211
|
+
|
|
3212
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3213
|
+
const baseBranch = baseBranchStdout.trim();
|
|
3214
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3215
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
3216
|
+
const baseHead = baseHeadStdout.trim();
|
|
3217
|
+
const branchHead = branchHeadStdout.trim();
|
|
3218
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
3219
|
+
|
|
3220
|
+
const validationStarted = Date.now();
|
|
3221
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
3222
|
+
recordMeshRefineStage(
|
|
3223
|
+
refineStages,
|
|
3224
|
+
'validation',
|
|
3225
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
3226
|
+
validationStarted,
|
|
3227
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
3228
|
+
);
|
|
3229
|
+
if (validationSummary.status === 'failed') {
|
|
3230
|
+
return {
|
|
3231
|
+
success: false,
|
|
3232
|
+
code: validationSummary.failureCode || 'validation_failed',
|
|
3233
|
+
convergenceStatus: 'blocked_review',
|
|
3234
|
+
error: validationSummary.failureCode === 'missing_dependencies'
|
|
3235
|
+
? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
|
|
3236
|
+
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
3237
|
+
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
3238
|
+
: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
3239
|
+
branch,
|
|
3240
|
+
into: baseBranch,
|
|
3241
|
+
validationSummary,
|
|
3242
|
+
refineStages,
|
|
3243
|
+
finalBranchConvergenceState: {
|
|
3244
|
+
branch,
|
|
3245
|
+
baseBranch,
|
|
3246
|
+
merged: false,
|
|
3247
|
+
removed: false,
|
|
3248
|
+
validation: 'failed',
|
|
3249
|
+
status: 'blocked_review',
|
|
3250
|
+
},
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
if (validationSummary.status === 'skipped') {
|
|
3254
|
+
return {
|
|
3255
|
+
success: false,
|
|
3256
|
+
code: 'validation_unavailable',
|
|
3257
|
+
convergenceStatus: 'blocked_review',
|
|
3258
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
3259
|
+
branch,
|
|
3260
|
+
into: baseBranch,
|
|
3261
|
+
validationSummary,
|
|
3262
|
+
refineStages,
|
|
3263
|
+
finalBranchConvergenceState: {
|
|
3264
|
+
branch,
|
|
3265
|
+
baseBranch,
|
|
3266
|
+
merged: false,
|
|
3267
|
+
removed: false,
|
|
3268
|
+
validation: 'unavailable',
|
|
3269
|
+
status: 'blocked_review',
|
|
3270
|
+
},
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
const patchEquivalenceStarted = Date.now();
|
|
3275
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3276
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
3277
|
+
equivalent: patchEquivalence.equivalent,
|
|
3278
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
3279
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
3280
|
+
error: patchEquivalence.error,
|
|
3281
|
+
actionableHint: patchEquivalence.actionableHint,
|
|
3282
|
+
});
|
|
3283
|
+
if (!patchEquivalence.equivalent) {
|
|
3284
|
+
return {
|
|
3285
|
+
success: false,
|
|
3286
|
+
code: 'patch_equivalence_failed',
|
|
3287
|
+
convergenceStatus: 'blocked_review',
|
|
3288
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
3289
|
+
branch,
|
|
3290
|
+
into: baseBranch,
|
|
3291
|
+
validationSummary,
|
|
3292
|
+
patchEquivalence,
|
|
3293
|
+
refineStages,
|
|
3294
|
+
finalBranchConvergenceState: {
|
|
3295
|
+
branch,
|
|
3296
|
+
baseBranch,
|
|
3297
|
+
merged: false,
|
|
3298
|
+
removed: false,
|
|
3299
|
+
validation: 'passed',
|
|
3300
|
+
patchEquivalence: 'failed',
|
|
3301
|
+
status: 'blocked_review',
|
|
3302
|
+
},
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
const submoduleReachabilityStarted = Date.now();
|
|
3307
|
+
const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
|
|
3308
|
+
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
3309
|
+
allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
|
|
3310
|
+
autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
|
|
3311
|
+
worktreeRoot: node.workspace,
|
|
3312
|
+
});
|
|
3313
|
+
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
3314
|
+
checked: submoduleReachability.checked,
|
|
3315
|
+
autoPublishAllowed: submoduleReachability.autoPublishAllowed,
|
|
3316
|
+
autoPublishPolicySource: submoduleReachability.autoPublishPolicySource,
|
|
3317
|
+
autoPublished: submoduleReachability.entries
|
|
3318
|
+
.filter(entry => entry.autoPublishAttempted)
|
|
3319
|
+
.map(entry => ({
|
|
3320
|
+
path: entry.path,
|
|
3321
|
+
commit: entry.commit,
|
|
3322
|
+
remote: entry.remote,
|
|
3323
|
+
remoteUrl: entry.remoteUrl,
|
|
3324
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3325
|
+
refspec: entry.autoPublishRefspec,
|
|
3326
|
+
succeeded: entry.autoPublishSucceeded,
|
|
3327
|
+
verified: entry.autoPublishVerified,
|
|
3328
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3329
|
+
error: entry.error,
|
|
3330
|
+
})),
|
|
3331
|
+
autoPublishSkipped: submoduleReachability.entries
|
|
3332
|
+
.filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
|
|
3333
|
+
.map(entry => ({
|
|
3334
|
+
path: entry.path,
|
|
3335
|
+
commit: entry.commit,
|
|
3336
|
+
remote: entry.remote,
|
|
3337
|
+
remoteUrl: entry.remoteUrl,
|
|
3338
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3339
|
+
reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
|
|
3340
|
+
})),
|
|
3341
|
+
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
3342
|
+
path: entry.path,
|
|
3343
|
+
commit: entry.commit,
|
|
3344
|
+
publishRequired: entry.publishRequired === true,
|
|
3345
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
3346
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
3347
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
3348
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
3349
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3350
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
3351
|
+
remote: entry.remote,
|
|
3352
|
+
remoteUrl: entry.remoteUrl,
|
|
3353
|
+
remoteReachable: entry.remoteReachable,
|
|
3354
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3355
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3356
|
+
error: entry.error,
|
|
3357
|
+
})),
|
|
3358
|
+
error: submoduleReachability.error,
|
|
3359
|
+
});
|
|
3360
|
+
if (submoduleReachability.status === 'failed') {
|
|
3361
|
+
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
3362
|
+
return {
|
|
3363
|
+
success: false,
|
|
3364
|
+
code: 'submodule_reachability_failed',
|
|
3365
|
+
convergenceStatus: 'blocked_review',
|
|
3366
|
+
publishRequired: true,
|
|
3367
|
+
blockedReason: 'submodule_publish_required',
|
|
3368
|
+
error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
|
|
3369
|
+
nextStep,
|
|
3370
|
+
nextSteps: [
|
|
3371
|
+
'Ask the user for explicit approval before pushing or publishing any submodule commit.',
|
|
3372
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
|
|
3373
|
+
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
3374
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
|
|
3375
|
+
],
|
|
3376
|
+
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
3377
|
+
path: entry.path,
|
|
3378
|
+
commit: entry.commit,
|
|
3379
|
+
remote: entry.remote,
|
|
3380
|
+
remoteUrl: entry.remoteUrl,
|
|
3381
|
+
remoteReachable: entry.remoteReachable,
|
|
3382
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3383
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3384
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
3385
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
3386
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
3387
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
3388
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3389
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
3390
|
+
error: entry.error,
|
|
3391
|
+
})),
|
|
3392
|
+
branch,
|
|
3393
|
+
into: baseBranch,
|
|
3394
|
+
validationSummary,
|
|
3395
|
+
patchEquivalence,
|
|
3396
|
+
submoduleReachability,
|
|
3397
|
+
refineStages,
|
|
3398
|
+
finalBranchConvergenceState: {
|
|
3399
|
+
branch,
|
|
3400
|
+
baseBranch,
|
|
3401
|
+
merged: false,
|
|
3402
|
+
removed: false,
|
|
3403
|
+
validation: 'passed',
|
|
3404
|
+
patchEquivalence: 'passed',
|
|
3405
|
+
submoduleReachability: 'failed',
|
|
3406
|
+
status: 'blocked_review',
|
|
3407
|
+
reason: 'submodule_publish_required',
|
|
3408
|
+
nextStep,
|
|
3409
|
+
},
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
3414
|
+
const mergeStarted = Date.now();
|
|
3415
|
+
try {
|
|
3416
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3417
|
+
mergeResult = {
|
|
3418
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
3419
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
3420
|
+
durationMs: Date.now() - mergeStarted,
|
|
3421
|
+
};
|
|
3422
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
3423
|
+
} catch (e: any) {
|
|
3424
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
3425
|
+
error: e?.message || String(e),
|
|
3426
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
3427
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
3428
|
+
});
|
|
3429
|
+
return {
|
|
3430
|
+
success: false,
|
|
3431
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
3432
|
+
validationSummary,
|
|
3433
|
+
patchEquivalence,
|
|
3434
|
+
refineStages,
|
|
3435
|
+
finalBranchConvergenceState: {
|
|
3436
|
+
branch,
|
|
3437
|
+
baseBranch,
|
|
3438
|
+
merged: false,
|
|
3439
|
+
removed: false,
|
|
3440
|
+
validation: 'passed',
|
|
3441
|
+
patchEquivalence: 'passed',
|
|
3442
|
+
status: 'not_mergeable',
|
|
3443
|
+
},
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
const submoduleAlignmentStarted = Date.now();
|
|
3448
|
+
const submoduleAlignment = await alignRefinerySubmodulesAfterMerge(repoRoot, baseHead, 'HEAD', {
|
|
3449
|
+
submoduleIgnorePaths: Array.isArray(sourceNode?.policy?.submoduleIgnorePaths)
|
|
3450
|
+
? sourceNode.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
3451
|
+
: undefined,
|
|
3452
|
+
});
|
|
3453
|
+
if (submoduleAlignment.status !== 'skipped') {
|
|
3454
|
+
recordMeshRefineStage(refineStages, 'submodule_alignment', submoduleAlignment.status, submoduleAlignmentStarted, {
|
|
3455
|
+
changedGitlinkPaths: submoduleAlignment.changedGitlinkPaths,
|
|
3456
|
+
outOfSyncPaths: submoduleAlignment.outOfSyncPaths,
|
|
3457
|
+
updatedPaths: submoduleAlignment.updatedPaths,
|
|
3458
|
+
verifiedPaths: submoduleAlignment.verifiedPaths,
|
|
3459
|
+
command: submoduleAlignment.command,
|
|
3460
|
+
error: submoduleAlignment.error,
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
if (submoduleAlignment.status === 'failed') {
|
|
3464
|
+
return {
|
|
3465
|
+
success: false,
|
|
3466
|
+
code: 'post_merge_submodule_alignment_failed',
|
|
3467
|
+
error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
|
|
3468
|
+
merged: true,
|
|
3469
|
+
branch,
|
|
3470
|
+
into: baseBranch,
|
|
3471
|
+
validationSummary,
|
|
3472
|
+
patchEquivalence,
|
|
3473
|
+
submoduleReachability,
|
|
3474
|
+
submoduleAlignment,
|
|
3475
|
+
mergeResult,
|
|
3476
|
+
refineStages,
|
|
3477
|
+
finalBranchConvergenceState: {
|
|
3478
|
+
branch: baseBranch,
|
|
3479
|
+
mergedBranch: branch,
|
|
3480
|
+
baseBranch,
|
|
3481
|
+
merged: true,
|
|
3482
|
+
removed: false,
|
|
3483
|
+
validation: 'passed',
|
|
3484
|
+
patchEquivalence: 'passed',
|
|
3485
|
+
submoduleReachability: 'passed',
|
|
3486
|
+
submoduleAlignment: 'failed',
|
|
3487
|
+
status: 'post_merge_alignment_failed',
|
|
3488
|
+
nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
|
|
3489
|
+
},
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
const cleanupStarted = Date.now();
|
|
3494
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
3495
|
+
meshId,
|
|
3496
|
+
nodeId,
|
|
3497
|
+
sessionCleanupMode: 'preserve',
|
|
3498
|
+
inlineMesh: args?.inlineMesh,
|
|
3499
|
+
});
|
|
3500
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
3501
|
+
removed: removeResult?.removed,
|
|
3502
|
+
code: removeResult?.code,
|
|
3503
|
+
error: removeResult?.error,
|
|
3504
|
+
});
|
|
3505
|
+
|
|
3506
|
+
let ledgerError: string | undefined;
|
|
3507
|
+
const ledgerStarted = Date.now();
|
|
3508
|
+
try {
|
|
3509
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3510
|
+
appendLedgerEntry(meshId, {
|
|
3511
|
+
kind: 'node_removed',
|
|
3512
|
+
nodeId,
|
|
3513
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment },
|
|
3514
|
+
});
|
|
3515
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
3516
|
+
} catch (e: any) {
|
|
3517
|
+
ledgerError = e?.message || String(e);
|
|
3518
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
const finalBranchConvergenceState = {
|
|
3522
|
+
branch: baseBranch,
|
|
3523
|
+
mergedBranch: branch,
|
|
3524
|
+
baseBranch,
|
|
3525
|
+
merged: true,
|
|
3526
|
+
removed: removeResult?.success !== false,
|
|
3527
|
+
validation: 'passed',
|
|
3528
|
+
patchEquivalence: 'passed',
|
|
3529
|
+
submoduleAlignment: submoduleAlignment.status,
|
|
3530
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
3531
|
+
};
|
|
3532
|
+
|
|
3533
|
+
if (removeResult?.success === false) {
|
|
3534
|
+
return {
|
|
3535
|
+
success: false,
|
|
3536
|
+
code: 'cleanup_failed',
|
|
3537
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
3538
|
+
merged: true,
|
|
3539
|
+
branch,
|
|
3540
|
+
into: baseBranch,
|
|
3541
|
+
removeResult,
|
|
3542
|
+
validationSummary,
|
|
3543
|
+
patchEquivalence,
|
|
3544
|
+
submoduleReachability,
|
|
3545
|
+
submoduleAlignment,
|
|
3546
|
+
mergeResult,
|
|
3547
|
+
refineStages,
|
|
3548
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3549
|
+
finalBranchConvergenceState,
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
return {
|
|
3554
|
+
success: true,
|
|
3555
|
+
merged: true,
|
|
3556
|
+
branch,
|
|
3557
|
+
into: baseBranch,
|
|
3558
|
+
removeResult,
|
|
3559
|
+
validationSummary,
|
|
3560
|
+
patchEquivalence,
|
|
3561
|
+
submoduleReachability,
|
|
3562
|
+
submoduleAlignment,
|
|
3563
|
+
mergeResult,
|
|
3564
|
+
refineStages,
|
|
3565
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3566
|
+
finalBranchConvergenceState,
|
|
3567
|
+
};
|
|
3568
|
+
} catch (e: any) {
|
|
3569
|
+
return { success: false, error: e.message, refineStages };
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
3574
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
3575
|
+
let result: Record<string, unknown>;
|
|
3576
|
+
try {
|
|
3577
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
3578
|
+
} catch (e: any) {
|
|
3579
|
+
result = { success: false, error: e?.message || String(e) };
|
|
3580
|
+
}
|
|
3581
|
+
const completedAt = new Date().toISOString();
|
|
3582
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
3583
|
+
meshId: handle.meshId,
|
|
3584
|
+
nodeId: handle.targetNodeId,
|
|
3585
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
3586
|
+
startedAt: handle.startedAt,
|
|
3587
|
+
completedAt,
|
|
3588
|
+
jobId: handle.jobId,
|
|
3589
|
+
interactionId: handle.interactionId,
|
|
3590
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3591
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
3592
|
+
});
|
|
3593
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
3594
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
3595
|
+
this.runningRefineJobs.delete(key);
|
|
3596
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
3597
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
3598
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
3602
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
3603
|
+
const running = this.runningRefineJobs.get(key);
|
|
3604
|
+
if (running) return { ...running, duplicate: true };
|
|
3605
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
3606
|
+
|
|
3607
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3608
|
+
const mesh = meshRecord?.mesh;
|
|
3609
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3610
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
3611
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
3612
|
+
|
|
3613
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
|
|
3614
|
+
this.runningRefineJobs.set(key, handle);
|
|
3615
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
3616
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
3617
|
+
|
|
3618
|
+
setImmediate(() => {
|
|
3619
|
+
void this.finishMeshRefineJob(handle, args);
|
|
3620
|
+
});
|
|
3621
|
+
|
|
3622
|
+
return handle;
|
|
3623
|
+
}
|
|
3624
|
+
|
|
1465
3625
|
// ─── Daemon-level command core ───────────────────
|
|
1466
3626
|
|
|
1467
3627
|
/**
|
|
@@ -1476,7 +3636,8 @@ export class DaemonCommandRouter {
|
|
|
1476
3636
|
}
|
|
1477
3637
|
|
|
1478
3638
|
case 'get_pending_mesh_events': {
|
|
1479
|
-
const
|
|
3639
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3640
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1480
3641
|
return { success: true, events };
|
|
1481
3642
|
}
|
|
1482
3643
|
|
|
@@ -2082,8 +4243,43 @@ export class DaemonCommandRouter {
|
|
|
2082
4243
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2083
4244
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2084
4245
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2085
|
-
if (meshRecord?.mesh) return { success:
|
|
2086
|
-
|
|
4246
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
4247
|
+
|
|
4248
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
4249
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
4250
|
+
mesh: meshRecord.mesh,
|
|
4251
|
+
meshSource: meshRecord.source,
|
|
4252
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4253
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
4254
|
+
localMachineId: loadConfig().machineId || '',
|
|
4255
|
+
});
|
|
4256
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
4257
|
+
const sourceOfTruth = {
|
|
4258
|
+
membership: meshRecord.source === 'inline_cache'
|
|
4259
|
+
? 'coordinator_inline_mesh_cache'
|
|
4260
|
+
: meshRecord.source === 'local_config'
|
|
4261
|
+
? 'local_mesh_config'
|
|
4262
|
+
: 'inline_bootstrap_snapshot',
|
|
4263
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
4264
|
+
directPeerTruth: {
|
|
4265
|
+
required: requireDirectPeerTruth,
|
|
4266
|
+
satisfied: directTruthSatisfied,
|
|
4267
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4268
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4269
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4270
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4271
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4272
|
+
},
|
|
4273
|
+
};
|
|
4274
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4275
|
+
return {
|
|
4276
|
+
success: false,
|
|
4277
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
4278
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
4279
|
+
sourceOfTruth,
|
|
4280
|
+
};
|
|
4281
|
+
}
|
|
4282
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
2087
4283
|
}
|
|
2088
4284
|
|
|
2089
4285
|
case 'create_mesh': {
|
|
@@ -2094,7 +4290,10 @@ export class DaemonCommandRouter {
|
|
|
2094
4290
|
if (!name) return { success: false, error: 'name required' };
|
|
2095
4291
|
try {
|
|
2096
4292
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
2097
|
-
const
|
|
4293
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
4294
|
+
? args.meshHost
|
|
4295
|
+
: undefined;
|
|
4296
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
2098
4297
|
return { success: true, mesh };
|
|
2099
4298
|
} catch (e: any) {
|
|
2100
4299
|
return { success: false, error: e.message };
|
|
@@ -2111,16 +4310,237 @@ export class DaemonCommandRouter {
|
|
|
2111
4310
|
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
2112
4311
|
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
2113
4312
|
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
4313
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
2114
4314
|
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
2115
4315
|
const mesh = updateMesh(meshId, patch as any);
|
|
2116
4316
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2117
4317
|
this.inlineMeshCache.set(meshId, mesh);
|
|
4318
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2118
4319
|
return { success: true, mesh };
|
|
2119
4320
|
} catch (e: any) {
|
|
2120
4321
|
return { success: false, error: e.message };
|
|
2121
4322
|
}
|
|
2122
4323
|
}
|
|
2123
4324
|
|
|
4325
|
+
case 'get_mesh_host_pairing': {
|
|
4326
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4327
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4328
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4329
|
+
const mesh = meshRecord?.mesh;
|
|
4330
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4331
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4332
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
4333
|
+
return {
|
|
4334
|
+
success: true,
|
|
4335
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
4336
|
+
meshId,
|
|
4337
|
+
hostAddress: meshHost.hostAddress,
|
|
4338
|
+
meshHost,
|
|
4339
|
+
manualPairing: {
|
|
4340
|
+
status: pairingStatus,
|
|
4341
|
+
joinImplemented: true,
|
|
4342
|
+
protocol: 'standalone_command_direct_v1',
|
|
4343
|
+
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.',
|
|
4344
|
+
},
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
case 'configure_mesh_host_pairing': {
|
|
4349
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4350
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
4351
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4352
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4353
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
4354
|
+
try {
|
|
4355
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
4356
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
4357
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
4358
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
4359
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
4360
|
+
return {
|
|
4361
|
+
success: true,
|
|
4362
|
+
code: 'mesh_host_pairing_pending',
|
|
4363
|
+
meshId,
|
|
4364
|
+
hostAddress: configured.hostAddress,
|
|
4365
|
+
meshHost,
|
|
4366
|
+
manualPairing: {
|
|
4367
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
4368
|
+
joinImplemented: true,
|
|
4369
|
+
protocol: 'standalone_command_direct_v1',
|
|
4370
|
+
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.',
|
|
4371
|
+
},
|
|
4372
|
+
};
|
|
4373
|
+
} catch (e: any) {
|
|
4374
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
case 'create_mesh_host_pairing_token': {
|
|
4379
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4380
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4381
|
+
try {
|
|
4382
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
4383
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
4384
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
4385
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
4386
|
+
});
|
|
4387
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
4388
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
4389
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4390
|
+
return {
|
|
4391
|
+
success: true,
|
|
4392
|
+
code: 'mesh_host_pairing_token_created',
|
|
4393
|
+
meshId,
|
|
4394
|
+
token: created.token,
|
|
4395
|
+
tokenId: created.tokenId,
|
|
4396
|
+
expiresAt: created.expiresAt,
|
|
4397
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
4398
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
4399
|
+
};
|
|
4400
|
+
} catch (e: any) {
|
|
4401
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
case 'apply_mesh_host_join': {
|
|
4406
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4407
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4408
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
4409
|
+
? args.memberNode
|
|
4410
|
+
: null;
|
|
4411
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4412
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
4413
|
+
try {
|
|
4414
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
4415
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
4416
|
+
token,
|
|
4417
|
+
memberNode: memberNode as any,
|
|
4418
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
4419
|
+
});
|
|
4420
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
4421
|
+
if (!applied.accepted) {
|
|
4422
|
+
return {
|
|
4423
|
+
success: false,
|
|
4424
|
+
code: 'mesh_host_join_rejected',
|
|
4425
|
+
meshId,
|
|
4426
|
+
tokenId: applied.tokenId,
|
|
4427
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
4428
|
+
error: applied.reason,
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
4432
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4433
|
+
try {
|
|
4434
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
4435
|
+
appendLedgerEntry(meshId, {
|
|
4436
|
+
kind: 'node_joined',
|
|
4437
|
+
nodeId: applied.node.id,
|
|
4438
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
4439
|
+
});
|
|
4440
|
+
} catch { /* ledger append is best-effort */ }
|
|
4441
|
+
return {
|
|
4442
|
+
success: true,
|
|
4443
|
+
code: 'mesh_host_join_accepted',
|
|
4444
|
+
meshId,
|
|
4445
|
+
node: applied.node,
|
|
4446
|
+
tokenId: applied.tokenId,
|
|
4447
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
4448
|
+
};
|
|
4449
|
+
} catch (e: any) {
|
|
4450
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
case 'join_mesh_host_pairing': {
|
|
4455
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4456
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4457
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4458
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
4459
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4460
|
+
const mesh = meshRecord?.mesh;
|
|
4461
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4462
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4463
|
+
if (meshHost.role !== 'member') {
|
|
4464
|
+
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.' };
|
|
4465
|
+
}
|
|
4466
|
+
try {
|
|
4467
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
4468
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
4469
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
4470
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
4471
|
+
}
|
|
4472
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
4473
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
4474
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
4475
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
4476
|
+
? args.hostDaemonId.trim()
|
|
4477
|
+
: meshHost.hostDaemonId;
|
|
4478
|
+
let hostResult: any;
|
|
4479
|
+
let transport: string;
|
|
4480
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
4481
|
+
transport = 'mesh_command_dispatch';
|
|
4482
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
4483
|
+
meshId: hostMeshId,
|
|
4484
|
+
token,
|
|
4485
|
+
memberMeshId: meshId,
|
|
4486
|
+
memberNode,
|
|
4487
|
+
});
|
|
4488
|
+
} else if (meshHost.hostAddress) {
|
|
4489
|
+
transport = 'standalone_http_command';
|
|
4490
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
4491
|
+
const response = await fetch(commandUrl, {
|
|
4492
|
+
method: 'POST',
|
|
4493
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4494
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
4495
|
+
});
|
|
4496
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
4497
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
4498
|
+
} else {
|
|
4499
|
+
return {
|
|
4500
|
+
success: false,
|
|
4501
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
4502
|
+
meshId,
|
|
4503
|
+
meshHost,
|
|
4504
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
4505
|
+
};
|
|
4506
|
+
}
|
|
4507
|
+
if (!hostResult?.success) {
|
|
4508
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
4509
|
+
}
|
|
4510
|
+
const joined = meshRecord.inline
|
|
4511
|
+
? null
|
|
4512
|
+
: markMeshHostPairingJoined(meshId, {
|
|
4513
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
4514
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
4515
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
4516
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
4517
|
+
});
|
|
4518
|
+
if (joined) {
|
|
4519
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
4520
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4521
|
+
}
|
|
4522
|
+
return {
|
|
4523
|
+
success: true,
|
|
4524
|
+
code: 'mesh_host_join_applied',
|
|
4525
|
+
meshId,
|
|
4526
|
+
hostMeshId,
|
|
4527
|
+
transport,
|
|
4528
|
+
node: hostResult.node,
|
|
4529
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
4530
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
4531
|
+
hostResult,
|
|
4532
|
+
manualPairing: {
|
|
4533
|
+
status: 'paired',
|
|
4534
|
+
joinImplemented: true,
|
|
4535
|
+
protocol: 'standalone_command_direct_v1',
|
|
4536
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
4537
|
+
},
|
|
4538
|
+
};
|
|
4539
|
+
} catch (e: any) {
|
|
4540
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
|
|
2124
4544
|
case 'delete_mesh': {
|
|
2125
4545
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2126
4546
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -2214,6 +4634,8 @@ export class DaemonCommandRouter {
|
|
|
2214
4634
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2215
4635
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2216
4636
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4637
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
4638
|
+
if (ownerFailure) return ownerFailure;
|
|
2217
4639
|
try {
|
|
2218
4640
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2219
4641
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -2229,6 +4651,8 @@ export class DaemonCommandRouter {
|
|
|
2229
4651
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2230
4652
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2231
4653
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4654
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
4655
|
+
if (ownerFailure) return ownerFailure;
|
|
2232
4656
|
try {
|
|
2233
4657
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2234
4658
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -2250,6 +4674,8 @@ export class DaemonCommandRouter {
|
|
|
2250
4674
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2251
4675
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2252
4676
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
4677
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
4678
|
+
if (ownerFailure) return ownerFailure;
|
|
2253
4679
|
try {
|
|
2254
4680
|
const { addNode } = await import('../config/mesh-config.js');
|
|
2255
4681
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -2260,7 +4686,18 @@ export class DaemonCommandRouter {
|
|
|
2260
4686
|
...(readOnly ? { readOnly: true } : {}),
|
|
2261
4687
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
2262
4688
|
};
|
|
2263
|
-
const
|
|
4689
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
4690
|
+
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
4691
|
+
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
4692
|
+
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
4693
|
+
const node = addNode(meshId, {
|
|
4694
|
+
workspace,
|
|
4695
|
+
...(repoRoot ? { repoRoot } : {}),
|
|
4696
|
+
...(daemonId ? { daemonId } : {}),
|
|
4697
|
+
...(machineId ? { machineId } : {}),
|
|
4698
|
+
...(policy ? { policy } : {}),
|
|
4699
|
+
...(role ? { role } : {}),
|
|
4700
|
+
});
|
|
2264
4701
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
2265
4702
|
return { success: true, node };
|
|
2266
4703
|
} catch (e: any) {
|
|
@@ -2272,6 +4709,8 @@ export class DaemonCommandRouter {
|
|
|
2272
4709
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2273
4710
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2274
4711
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4712
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
4713
|
+
if (ownerFailure) return ownerFailure;
|
|
2275
4714
|
try {
|
|
2276
4715
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2277
4716
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2300,6 +4739,8 @@ export class DaemonCommandRouter {
|
|
|
2300
4739
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2301
4740
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2302
4741
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4742
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
4743
|
+
if (ownerFailure) return ownerFailure;
|
|
2303
4744
|
try {
|
|
2304
4745
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2305
4746
|
const mesh = meshRecord?.mesh;
|
|
@@ -2325,131 +4766,97 @@ export class DaemonCommandRouter {
|
|
|
2325
4766
|
}
|
|
2326
4767
|
}
|
|
2327
4768
|
|
|
2328
|
-
case '
|
|
4769
|
+
case 'get_mesh_refine_config_schema': {
|
|
4770
|
+
return {
|
|
4771
|
+
success: true,
|
|
4772
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
4773
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
4774
|
+
worktreeBootstrap: {
|
|
4775
|
+
schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
4776
|
+
locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
4777
|
+
sourceOfTruth: 'repo worktree bootstrap config',
|
|
4778
|
+
runBehavior: 'When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node.',
|
|
4779
|
+
},
|
|
4780
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
4781
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
case 'validate_mesh_refine_config': {
|
|
4786
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4787
|
+
const mesh = args?.inlineMesh || {};
|
|
4788
|
+
const loaded = args?.config !== undefined
|
|
4789
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
4790
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
4791
|
+
const validation = loaded.config
|
|
4792
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
4793
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
4794
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
4795
|
+
}
|
|
4796
|
+
|
|
4797
|
+
case 'suggest_mesh_refine_config': {
|
|
4798
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4799
|
+
const mesh = args?.inlineMesh || {};
|
|
4800
|
+
return {
|
|
4801
|
+
success: true,
|
|
4802
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
4803
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
4804
|
+
};
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
case 'plan_mesh_refine_node': {
|
|
2329
4808
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2330
4809
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2331
4810
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2332
|
-
|
|
4811
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
4812
|
+
const mesh = meshRecord?.mesh;
|
|
4813
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
4814
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
4815
|
+
return {
|
|
4816
|
+
success: true,
|
|
4817
|
+
dryRun: true,
|
|
4818
|
+
nodeId,
|
|
4819
|
+
workspace: node.workspace,
|
|
4820
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
4821
|
+
mergeWillRun: false,
|
|
4822
|
+
cleanupWillRun: false,
|
|
4823
|
+
};
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
case 'fast_forward_mesh_node': {
|
|
4827
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4828
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4829
|
+
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
4830
|
+
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
4831
|
+
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
4832
|
+
: undefined;
|
|
4833
|
+
if (!workspace && meshId && nodeId) {
|
|
2333
4834
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2334
4835
|
const mesh = meshRecord?.mesh;
|
|
2335
4836
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
return { success: false, error: `Refinery requires a local worktree node` };
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
const sourceNode = node.clonedFromNodeId
|
|
2343
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2344
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2345
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2346
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2347
|
-
|
|
2348
|
-
const { execFile } = await import('node:child_process');
|
|
2349
|
-
const { promisify } = await import('node:util');
|
|
2350
|
-
const execFileAsync = promisify(execFile);
|
|
2351
|
-
|
|
2352
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2353
|
-
const branch = branchStdout.trim();
|
|
2354
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2355
|
-
|
|
2356
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2357
|
-
const baseBranch = baseBranchStdout.trim();
|
|
2358
|
-
|
|
2359
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2360
|
-
if (validationSummary.status === 'failed') {
|
|
2361
|
-
return {
|
|
2362
|
-
success: false,
|
|
2363
|
-
code: 'validation_failed',
|
|
2364
|
-
convergenceStatus: 'blocked_review',
|
|
2365
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2366
|
-
branch,
|
|
2367
|
-
into: baseBranch,
|
|
2368
|
-
validationSummary,
|
|
2369
|
-
finalBranchConvergenceState: {
|
|
2370
|
-
branch,
|
|
2371
|
-
baseBranch,
|
|
2372
|
-
merged: false,
|
|
2373
|
-
removed: false,
|
|
2374
|
-
validation: 'failed',
|
|
2375
|
-
status: 'blocked_review',
|
|
2376
|
-
},
|
|
2377
|
-
};
|
|
2378
|
-
}
|
|
2379
|
-
if (validationSummary.status === 'skipped') {
|
|
2380
|
-
return {
|
|
2381
|
-
success: false,
|
|
2382
|
-
code: 'validation_unavailable',
|
|
2383
|
-
convergenceStatus: 'blocked_review',
|
|
2384
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2385
|
-
branch,
|
|
2386
|
-
into: baseBranch,
|
|
2387
|
-
validationSummary,
|
|
2388
|
-
finalBranchConvergenceState: {
|
|
2389
|
-
branch,
|
|
2390
|
-
baseBranch,
|
|
2391
|
-
merged: false,
|
|
2392
|
-
removed: false,
|
|
2393
|
-
validation: 'unavailable',
|
|
2394
|
-
status: 'blocked_review',
|
|
2395
|
-
},
|
|
2396
|
-
};
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
try {
|
|
2400
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2401
|
-
} catch (e: any) {
|
|
2402
|
-
return {
|
|
2403
|
-
success: false,
|
|
2404
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2405
|
-
validationSummary,
|
|
2406
|
-
finalBranchConvergenceState: {
|
|
2407
|
-
branch,
|
|
2408
|
-
baseBranch,
|
|
2409
|
-
merged: false,
|
|
2410
|
-
removed: false,
|
|
2411
|
-
validation: 'passed',
|
|
2412
|
-
status: 'not_mergeable',
|
|
2413
|
-
},
|
|
2414
|
-
};
|
|
4837
|
+
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
4838
|
+
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
4839
|
+
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
2415
4840
|
}
|
|
2416
|
-
|
|
2417
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
2418
|
-
meshId,
|
|
2419
|
-
nodeId,
|
|
2420
|
-
sessionCleanupMode: 'kill',
|
|
2421
|
-
inlineMesh: args?.inlineMesh,
|
|
2422
|
-
});
|
|
2423
|
-
|
|
2424
|
-
try {
|
|
2425
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2426
|
-
appendLedgerEntry(meshId, {
|
|
2427
|
-
kind: 'node_removed',
|
|
2428
|
-
nodeId,
|
|
2429
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
2430
|
-
});
|
|
2431
|
-
} catch {}
|
|
2432
|
-
|
|
2433
|
-
return {
|
|
2434
|
-
success: true,
|
|
2435
|
-
merged: true,
|
|
2436
|
-
branch,
|
|
2437
|
-
into: baseBranch,
|
|
2438
|
-
removeResult,
|
|
2439
|
-
validationSummary,
|
|
2440
|
-
finalBranchConvergenceState: {
|
|
2441
|
-
branch: baseBranch,
|
|
2442
|
-
mergedBranch: branch,
|
|
2443
|
-
baseBranch,
|
|
2444
|
-
merged: true,
|
|
2445
|
-
removed: removeResult?.success !== false,
|
|
2446
|
-
validation: 'passed',
|
|
2447
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2448
|
-
},
|
|
2449
|
-
};
|
|
2450
|
-
} catch (e: any) {
|
|
2451
|
-
return { success: false, error: e.message };
|
|
2452
4841
|
}
|
|
4842
|
+
const result = await (fastForwardMeshNode({
|
|
4843
|
+
meshId: meshId || undefined,
|
|
4844
|
+
nodeId: nodeId || undefined,
|
|
4845
|
+
workspace,
|
|
4846
|
+
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
4847
|
+
execute: args?.execute === true,
|
|
4848
|
+
dryRun: args?.dryRun === true,
|
|
4849
|
+
updateSubmodules: args?.updateSubmodules === true,
|
|
4850
|
+
submoduleIgnorePaths,
|
|
4851
|
+
}) as Promise<unknown>);
|
|
4852
|
+
return result as CommandRouterResult;
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
case 'refine_mesh_node': {
|
|
4856
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4857
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4858
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4859
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
2453
4860
|
}
|
|
2454
4861
|
|
|
2455
4862
|
case 'remove_mesh_node': {
|
|
@@ -2493,6 +4900,7 @@ export class DaemonCommandRouter {
|
|
|
2493
4900
|
} else {
|
|
2494
4901
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2495
4902
|
removed = removeNode(meshId, nodeId);
|
|
4903
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2496
4904
|
}
|
|
2497
4905
|
|
|
2498
4906
|
// Record in task ledger
|
|
@@ -2530,6 +4938,8 @@ export class DaemonCommandRouter {
|
|
|
2530
4938
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2531
4939
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2532
4940
|
if (!branch) return { success: false, error: 'branch required' };
|
|
4941
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
4942
|
+
if (ownerFailure) return ownerFailure;
|
|
2533
4943
|
|
|
2534
4944
|
try {
|
|
2535
4945
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2578,39 +4988,124 @@ export class DaemonCommandRouter {
|
|
|
2578
4988
|
policy: { ...(sourceNode.policy || {}) },
|
|
2579
4989
|
});
|
|
2580
4990
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
4991
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2581
4992
|
}
|
|
2582
4993
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
4994
|
+
const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
4995
|
+
node.worktreeBootstrap = bootstrapState;
|
|
4996
|
+
if (meshRecord.inline) {
|
|
4997
|
+
this.updateInlineMeshNode(meshId, mesh, node);
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
2586
5000
|
try {
|
|
2587
|
-
const {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
5001
|
+
const { updateNode } = await import('../config/mesh-config.js');
|
|
5002
|
+
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
5003
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
5004
|
+
} catch { /* bootstrap status persistence is best-effort */ }
|
|
5005
|
+
};
|
|
5006
|
+
|
|
5007
|
+
const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
5008
|
+
try {
|
|
5009
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
5010
|
+
appendLedgerEntry(meshId, {
|
|
5011
|
+
kind: 'node_cloned',
|
|
5012
|
+
nodeId: node.id,
|
|
5013
|
+
payload: {
|
|
5014
|
+
sourceNodeId,
|
|
5015
|
+
branch: result.branch,
|
|
5016
|
+
worktreePath: result.worktreePath,
|
|
5017
|
+
submodulesInitialized: initSubmodules,
|
|
5018
|
+
worktreeBootstrap: {
|
|
5019
|
+
status: bootstrapState.status,
|
|
5020
|
+
required: bootstrapState.required,
|
|
5021
|
+
configSource: bootstrapState.configSource,
|
|
5022
|
+
configSourceType: bootstrapState.configSourceType,
|
|
5023
|
+
lastCommand: bootstrapState.lastCommand,
|
|
5024
|
+
exitCode: bootstrapState.exitCode,
|
|
5025
|
+
},
|
|
5026
|
+
},
|
|
5027
|
+
});
|
|
5028
|
+
} catch { /* ledger append is best-effort */ }
|
|
5029
|
+
};
|
|
5030
|
+
|
|
5031
|
+
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
5032
|
+
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
|
|
5033
|
+
const runningBootstrapState: WorktreeBootstrapState = {
|
|
5034
|
+
status: 'running',
|
|
5035
|
+
required: loadedBootstrap.config?.required !== false,
|
|
5036
|
+
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
5037
|
+
configSourceType: loadedBootstrap.sourceType,
|
|
5038
|
+
startedAt: new Date().toISOString(),
|
|
5039
|
+
};
|
|
5040
|
+
await persistWorktreeSetupState(runningBootstrapState);
|
|
5041
|
+
|
|
5042
|
+
const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
|
|
5043
|
+
let submodulesInitialized = false;
|
|
5044
|
+
if (initSubmodules) {
|
|
5045
|
+
try {
|
|
5046
|
+
const { runGit } = await import('../git/git-executor.js');
|
|
5047
|
+
await runGit(
|
|
5048
|
+
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
5049
|
+
['submodule', 'update', '--init', '--recursive'],
|
|
5050
|
+
{ timeoutMs: 120000 },
|
|
5051
|
+
);
|
|
5052
|
+
submodulesInitialized = true;
|
|
5053
|
+
} catch (subErr: any) {
|
|
5054
|
+
// Submodule init is best-effort; don't fail the clone
|
|
5055
|
+
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
5056
|
+
}
|
|
2596
5057
|
}
|
|
2597
|
-
|
|
5058
|
+
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
5059
|
+
await persistWorktreeSetupState(bootstrapState);
|
|
5060
|
+
await appendCloneLedger(submodulesInitialized, bootstrapState);
|
|
5061
|
+
return { submodulesInitialized, bootstrapState };
|
|
5062
|
+
};
|
|
2598
5063
|
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
5064
|
+
const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
|
|
5065
|
+
const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
|
|
5066
|
+
? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
|
|
5067
|
+
: 8000;
|
|
5068
|
+
const setupPromise = finishWorktreeSetup();
|
|
5069
|
+
const setupResult = await Promise.race([
|
|
5070
|
+
setupPromise.then((value) => ({ completed: true as const, value })),
|
|
5071
|
+
new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
|
|
5072
|
+
]);
|
|
5073
|
+
|
|
5074
|
+
if (!setupResult.completed) {
|
|
5075
|
+
setupPromise.catch((error: any) => {
|
|
5076
|
+
const failedState: WorktreeBootstrapState = {
|
|
5077
|
+
...runningBootstrapState,
|
|
5078
|
+
status: 'failed',
|
|
5079
|
+
completedAt: new Date().toISOString(),
|
|
5080
|
+
error: error?.message || String(error),
|
|
5081
|
+
};
|
|
5082
|
+
void persistWorktreeSetupState(failedState);
|
|
5083
|
+
void appendCloneLedger(false, failedState);
|
|
2606
5084
|
});
|
|
2607
|
-
|
|
5085
|
+
return {
|
|
5086
|
+
success: true,
|
|
5087
|
+
async: true,
|
|
5088
|
+
status: 'accepted',
|
|
5089
|
+
node,
|
|
5090
|
+
worktreePath: result.worktreePath,
|
|
5091
|
+
branch: result.branch,
|
|
5092
|
+
worktreeBootstrap: runningBootstrapState,
|
|
5093
|
+
worktreeSetup: {
|
|
5094
|
+
status: 'running',
|
|
5095
|
+
setupWaitMs,
|
|
5096
|
+
message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
|
|
5097
|
+
},
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
2608
5100
|
|
|
5101
|
+
const { submodulesInitialized, bootstrapState } = setupResult.value;
|
|
2609
5102
|
return {
|
|
2610
5103
|
success: true,
|
|
2611
5104
|
node,
|
|
2612
5105
|
worktreePath: result.worktreePath,
|
|
2613
5106
|
branch: result.branch,
|
|
5107
|
+
submodulesInitialized,
|
|
5108
|
+
worktreeBootstrap: bootstrapState,
|
|
2614
5109
|
};
|
|
2615
5110
|
} catch (e: any) {
|
|
2616
5111
|
return { success: false, error: e.message };
|
|
@@ -2619,6 +5114,8 @@ export class DaemonCommandRouter {
|
|
|
2619
5114
|
case 'trigger_mesh_queue': {
|
|
2620
5115
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2621
5116
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
5117
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
5118
|
+
if (ownerFailure) return ownerFailure;
|
|
2622
5119
|
try {
|
|
2623
5120
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2624
5121
|
if (meshId) {
|
|
@@ -2650,6 +5147,15 @@ export class DaemonCommandRouter {
|
|
|
2650
5147
|
mesh = getMesh(meshId);
|
|
2651
5148
|
}
|
|
2652
5149
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
5150
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
5151
|
+
if (!meshHost.canOwnCoordinator) {
|
|
5152
|
+
return {
|
|
5153
|
+
success: false,
|
|
5154
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
5155
|
+
meshId,
|
|
5156
|
+
cliType,
|
|
5157
|
+
};
|
|
5158
|
+
}
|
|
2653
5159
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2654
5160
|
|
|
2655
5161
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2669,7 +5175,16 @@ export class DaemonCommandRouter {
|
|
|
2669
5175
|
cliType,
|
|
2670
5176
|
};
|
|
2671
5177
|
}
|
|
2672
|
-
const
|
|
5178
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
5179
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
5180
|
+
: [];
|
|
5181
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
5182
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
5183
|
+
meshId,
|
|
5184
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
5185
|
+
liveSessionRecords: liveMeshSessions,
|
|
5186
|
+
allowCoordinatorSession: true,
|
|
5187
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2673
5188
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2674
5189
|
if (!cliType) {
|
|
2675
5190
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -3014,6 +5529,30 @@ export class DaemonCommandRouter {
|
|
|
3014
5529
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3015
5530
|
const mesh = meshRecord?.mesh;
|
|
3016
5531
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
5532
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
5533
|
+
|
|
5534
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
5535
|
+
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId).length;
|
|
5536
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
5537
|
+
if (!refreshRequested && pendingCoordinatorEventCount === 0) {
|
|
5538
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
5539
|
+
if (cachedStatus) {
|
|
5540
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
5541
|
+
meshId,
|
|
5542
|
+
command: 'mesh_status',
|
|
5543
|
+
refreshRequested,
|
|
5544
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
5545
|
+
});
|
|
5546
|
+
return cachedStatus;
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
const refreshReason = refreshRequested
|
|
5550
|
+
? 'explicit_refresh'
|
|
5551
|
+
: pendingCoordinatorEventCount > 0
|
|
5552
|
+
? 'pending_coordinator_events'
|
|
5553
|
+
: hadAggregateCache
|
|
5554
|
+
? 'stale_pending_cache_refresh'
|
|
5555
|
+
: 'cold_cache_miss';
|
|
3017
5556
|
|
|
3018
5557
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
3019
5558
|
const queue = getQueue(meshId);
|
|
@@ -3021,6 +5560,7 @@ export class DaemonCommandRouter {
|
|
|
3021
5560
|
|
|
3022
5561
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
3023
5562
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
5563
|
+
const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
|
|
3024
5564
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
3025
5565
|
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
3026
5566
|
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
@@ -3028,29 +5568,123 @@ export class DaemonCommandRouter {
|
|
|
3028
5568
|
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
3029
5569
|
|
|
3030
5570
|
const localMachineId = loadConfig().machineId || '';
|
|
5571
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
5572
|
+
const directTruth = requireDirectPeerTruth
|
|
5573
|
+
? await hydrateInlineMeshDirectTruth({
|
|
5574
|
+
mesh,
|
|
5575
|
+
meshSource: meshRecord.source,
|
|
5576
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5577
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
5578
|
+
localMachineId,
|
|
5579
|
+
})
|
|
5580
|
+
: {
|
|
5581
|
+
directEvidenceCount: 0,
|
|
5582
|
+
localConfirmedCount: 0,
|
|
5583
|
+
peerAttemptedCount: 0,
|
|
5584
|
+
peerConfirmedCount: 0,
|
|
5585
|
+
unavailableNodeIds: [] as string[],
|
|
5586
|
+
};
|
|
5587
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
5588
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
5589
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
5590
|
+
&& !refreshRequested
|
|
5591
|
+
&& directTruth.directEvidenceCount > 0
|
|
5592
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
5593
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
5594
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
5595
|
+
: directTruth;
|
|
5596
|
+
const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
5597
|
+
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
5598
|
+
&& Array.isArray(mesh.nodes)
|
|
5599
|
+
&& mesh.nodes
|
|
5600
|
+
.filter((node: any) => unavailableDirectTruthNodeIds.has(String(node.id || node.nodeId || '')))
|
|
5601
|
+
.every((node: any) => node?.isLocalWorktree === true);
|
|
5602
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
5603
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
5604
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
5605
|
+
const failureResult = {
|
|
5606
|
+
success: false,
|
|
5607
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
5608
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
5609
|
+
sourceOfTruth: {
|
|
5610
|
+
membership: meshRecord.source === 'inline_cache'
|
|
5611
|
+
? 'coordinator_inline_mesh_cache'
|
|
5612
|
+
: meshRecord.source === 'local_config'
|
|
5613
|
+
? 'local_mesh_config'
|
|
5614
|
+
: 'inline_bootstrap_snapshot',
|
|
5615
|
+
coordinatorOwnsLiveTruth: false,
|
|
5616
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
5617
|
+
directPeerTruth: {
|
|
5618
|
+
required: true,
|
|
5619
|
+
satisfied: false,
|
|
5620
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
5621
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
5622
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
5623
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
5624
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
5625
|
+
},
|
|
5626
|
+
},
|
|
5627
|
+
};
|
|
5628
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
5629
|
+
meshId,
|
|
5630
|
+
command: 'mesh_status',
|
|
5631
|
+
refreshRequested,
|
|
5632
|
+
meshSource: meshRecord.source,
|
|
5633
|
+
directTruth,
|
|
5634
|
+
});
|
|
5635
|
+
return failureResult;
|
|
5636
|
+
}
|
|
5637
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
5638
|
+
const coordinatorHostname = osHostname();
|
|
5639
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
5640
|
+
mesh.coordinator?.preferredNodeId,
|
|
5641
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
5642
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
5643
|
+
);
|
|
3031
5644
|
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
3032
|
-
?
|
|
5645
|
+
? selectedCoordinatorNodeId
|
|
3033
5646
|
: undefined;
|
|
3034
5647
|
const refreshedAt = new Date().toISOString();
|
|
3035
5648
|
const nodeStatuses = [];
|
|
3036
5649
|
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
3037
5650
|
const nodeId = String(node.id || node.nodeId || '');
|
|
3038
5651
|
const daemonId = readStringValue(node.daemonId);
|
|
5652
|
+
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
|
|
5653
|
+
const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
|
|
3039
5654
|
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
5655
|
+
const configuredCoordinatorNode = Boolean(
|
|
5656
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
5657
|
+
);
|
|
5658
|
+
const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
|
|
5659
|
+
&& !daemonId
|
|
5660
|
+
&& !nodeMachineId
|
|
5661
|
+
&& !nodeHostname;
|
|
3040
5662
|
const isSelfNode = Boolean(
|
|
3041
5663
|
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
3042
5664
|
) || Boolean(
|
|
3043
5665
|
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
3044
|
-
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
5666
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
5667
|
+
|| sparseConfiguredCoordinatorNode;
|
|
5668
|
+
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
5669
|
+
localMachineId,
|
|
5670
|
+
localDaemonId: this.deps.statusInstanceId,
|
|
5671
|
+
coordinatorHostname,
|
|
5672
|
+
isSelfNode,
|
|
5673
|
+
});
|
|
3045
5674
|
const status: Record<string, unknown> = {
|
|
3046
5675
|
nodeId,
|
|
3047
|
-
machineLabel: node
|
|
5676
|
+
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
5677
|
+
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
5678
|
+
? 'explicit_metadata'
|
|
5679
|
+
: 'workspace_host_provider_context',
|
|
3048
5680
|
workspace: node.workspace,
|
|
3049
5681
|
repoRoot: node.repoRoot,
|
|
3050
5682
|
isLocalWorktree: node.isLocalWorktree,
|
|
3051
5683
|
worktreeBranch: node.worktreeBranch,
|
|
5684
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
3052
5685
|
daemonId,
|
|
3053
|
-
machineId: node.machineId,
|
|
5686
|
+
machineId: nodeMachineId || node.machineId,
|
|
5687
|
+
machine: machineIdentity,
|
|
3054
5688
|
machineStatus: node.machineStatus,
|
|
3055
5689
|
health: 'unknown',
|
|
3056
5690
|
providers: node.providers || [],
|
|
@@ -3089,8 +5723,20 @@ export class DaemonCommandRouter {
|
|
|
3089
5723
|
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
3090
5724
|
};
|
|
3091
5725
|
}
|
|
3092
|
-
const matchedLiveSessionRecords =
|
|
3093
|
-
|
|
5726
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
5727
|
+
meshId,
|
|
5728
|
+
node,
|
|
5729
|
+
nodeId,
|
|
5730
|
+
liveSessionRecords: liveMeshSessions,
|
|
5731
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
5732
|
+
});
|
|
5733
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
5734
|
+
meshId,
|
|
5735
|
+
nodeId,
|
|
5736
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
5737
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
5738
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
5739
|
+
status.workspace = workspace || node.workspace;
|
|
3094
5740
|
if (matchedLiveSessionRecords.length > 0) {
|
|
3095
5741
|
const sessionIds = matchedLiveSessionRecords
|
|
3096
5742
|
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
@@ -3104,44 +5750,213 @@ export class DaemonCommandRouter {
|
|
|
3104
5750
|
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
3105
5751
|
}
|
|
3106
5752
|
}
|
|
3107
|
-
if (
|
|
3108
|
-
if (!fs.existsSync(
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
if (
|
|
5753
|
+
if (workspace) {
|
|
5754
|
+
if (!fs.existsSync(workspace)) {
|
|
5755
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
5756
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
5757
|
+
let remoteProbeApplied = false;
|
|
5758
|
+
if (inlineTransitGit) {
|
|
5759
|
+
status.git = inlineTransitGit;
|
|
5760
|
+
status.health = inlineTransitGit.isGitRepo
|
|
5761
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
5762
|
+
: 'degraded';
|
|
5763
|
+
const connection = readObjectRecord(status.connection);
|
|
5764
|
+
const connectionState = readStringValue(connection.state);
|
|
5765
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5766
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5767
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5768
|
+
}
|
|
5769
|
+
remoteProbeApplied = true;
|
|
5770
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
5771
|
+
try {
|
|
5772
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
5773
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5774
|
+
daemonId,
|
|
5775
|
+
workspace,
|
|
5776
|
+
timeoutMs: 8000,
|
|
5777
|
+
});
|
|
5778
|
+
if (remoteGit) {
|
|
5779
|
+
status.git = remoteGit;
|
|
5780
|
+
status.health = remoteGit.isGitRepo
|
|
5781
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
5782
|
+
: 'degraded';
|
|
5783
|
+
const connection = readObjectRecord(status.connection);
|
|
5784
|
+
const connectionState = readStringValue(connection.state);
|
|
5785
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5786
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5787
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5788
|
+
}
|
|
5789
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
5790
|
+
remoteProbeApplied = true;
|
|
5791
|
+
}
|
|
5792
|
+
} catch {
|
|
5793
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
5794
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
5795
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
5796
|
+
status.connection = refreshedConnection;
|
|
5797
|
+
try {
|
|
5798
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
5799
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5800
|
+
daemonId,
|
|
5801
|
+
workspace,
|
|
5802
|
+
timeoutMs: 12000,
|
|
5803
|
+
});
|
|
5804
|
+
if (remoteGit) {
|
|
5805
|
+
status.git = remoteGit;
|
|
5806
|
+
status.health = remoteGit.isGitRepo
|
|
5807
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
5808
|
+
: 'degraded';
|
|
5809
|
+
const connection = readObjectRecord(status.connection);
|
|
5810
|
+
const connectionState = readStringValue(connection.state);
|
|
5811
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5812
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5813
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5814
|
+
}
|
|
5815
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
5816
|
+
remoteProbeApplied = true;
|
|
5817
|
+
}
|
|
5818
|
+
} catch {
|
|
5819
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
if (!remoteProbeApplied) {
|
|
5825
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
5826
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
5827
|
+
&& !isSelfNode
|
|
5828
|
+
&& !!daemonId
|
|
5829
|
+
&& (
|
|
5830
|
+
readStringValue(status.machineStatus) === 'online'
|
|
5831
|
+
|| readStringValue(status.health) === 'online'
|
|
5832
|
+
|| connectionState === 'connecting'
|
|
5833
|
+
|| connectionState === 'connected'
|
|
5834
|
+
|| connectionState === 'unknown'
|
|
5835
|
+
);
|
|
5836
|
+
if (pendingPeerGitProbe) {
|
|
5837
|
+
status.gitProbePending = true;
|
|
5838
|
+
status.health = 'unknown';
|
|
5839
|
+
}
|
|
5840
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
5841
|
+
status,
|
|
5842
|
+
node,
|
|
5843
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
5844
|
+
)) {
|
|
5845
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5846
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
5847
|
+
nodeStatuses.push(status);
|
|
5848
|
+
continue;
|
|
5849
|
+
}
|
|
5850
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
5851
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5852
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
5853
|
+
nodeStatuses.push(status);
|
|
5854
|
+
continue;
|
|
5855
|
+
}
|
|
3121
5856
|
}
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
|
|
5857
|
+
} else {
|
|
5858
|
+
try {
|
|
5859
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
5860
|
+
status.git = gitStatus;
|
|
5861
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
5862
|
+
if (gitStatus.isGitRepo) {
|
|
5863
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
5864
|
+
} else {
|
|
5865
|
+
status.health = 'degraded';
|
|
5866
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
5867
|
+
}
|
|
5868
|
+
} catch {
|
|
5869
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
5870
|
+
status.health = 'degraded';
|
|
5871
|
+
}
|
|
3125
5872
|
}
|
|
3126
5873
|
}
|
|
3127
5874
|
} else {
|
|
3128
5875
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
3129
5876
|
}
|
|
3130
|
-
|
|
5877
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5878
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
3131
5879
|
nodeStatuses.push(status);
|
|
3132
5880
|
}
|
|
3133
5881
|
|
|
3134
|
-
|
|
5882
|
+
const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId);
|
|
5883
|
+
const previewFreshness = (() => {
|
|
5884
|
+
const localRepoRoot = nodeStatuses
|
|
5885
|
+
.map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
|
|
5886
|
+
.find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
|
|
5887
|
+
return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
|
|
5888
|
+
})();
|
|
5889
|
+
const asyncRefineJobs = buildMeshAsyncRefineJobs({
|
|
5890
|
+
meshId,
|
|
5891
|
+
ledgerEntries: asyncRefineLedgerEntries,
|
|
5892
|
+
pendingEvents: pendingCoordinatorEvents,
|
|
5893
|
+
});
|
|
5894
|
+
const historicalSessions = buildHistoricalMeshSessions({
|
|
5895
|
+
meshId,
|
|
5896
|
+
nodes: mesh.nodes || [],
|
|
5897
|
+
liveSessionRecords: liveMeshSessions,
|
|
5898
|
+
});
|
|
5899
|
+
const statusResult = {
|
|
3135
5900
|
success: true,
|
|
3136
5901
|
meshId: mesh.id,
|
|
3137
5902
|
meshName: mesh.name,
|
|
3138
5903
|
repoIdentity: mesh.repoIdentity,
|
|
3139
5904
|
defaultBranch: mesh.defaultBranch,
|
|
3140
|
-
refreshedAt
|
|
5905
|
+
refreshedAt,
|
|
5906
|
+
meshHost,
|
|
5907
|
+
sourceOfTruth: {
|
|
5908
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
5909
|
+
? 'coordinator_inline_mesh_cache'
|
|
5910
|
+
: meshRecord?.source === 'local_config'
|
|
5911
|
+
? 'local_mesh_config'
|
|
5912
|
+
: 'inline_bootstrap_snapshot',
|
|
5913
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
5914
|
+
meshHost: {
|
|
5915
|
+
owner: 'mesh_host_daemon',
|
|
5916
|
+
localRole: meshHost.role,
|
|
5917
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
5918
|
+
hostNodeId: meshHost.hostNodeId,
|
|
5919
|
+
hostAddress: meshHost.hostAddress,
|
|
5920
|
+
},
|
|
5921
|
+
...(requireDirectPeerTruth ? {
|
|
5922
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
5923
|
+
directPeerTruth: {
|
|
5924
|
+
required: true,
|
|
5925
|
+
satisfied: directTruthSatisfied,
|
|
5926
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
5927
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
5928
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
5929
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
5930
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
5931
|
+
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
5932
|
+
},
|
|
5933
|
+
} : {}),
|
|
5934
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
|
|
5935
|
+
},
|
|
5936
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
5937
|
+
...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
|
|
3141
5938
|
nodes: nodeStatuses,
|
|
3142
5939
|
queue: { tasks: queue, summary: queueSummary },
|
|
3143
5940
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
5941
|
+
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
5942
|
+
...(historicalSessions ? { historicalSessions } : {}),
|
|
5943
|
+
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
3144
5944
|
};
|
|
5945
|
+
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
|
|
5946
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
5947
|
+
const returnedStatus = pendingCoordinatorEvents.length > 0
|
|
5948
|
+
? { ...rememberedStatus, pendingCoordinatorEvents }
|
|
5949
|
+
: rememberedStatus;
|
|
5950
|
+
logRepoMeshStatusDebug('return_live', {
|
|
5951
|
+
meshId,
|
|
5952
|
+
command: 'mesh_status',
|
|
5953
|
+
refreshRequested,
|
|
5954
|
+
refreshReason,
|
|
5955
|
+
meshSource: meshRecord.source,
|
|
5956
|
+
directTruth,
|
|
5957
|
+
summary: summarizeRepoMeshStatusDebug(returnedStatus),
|
|
5958
|
+
});
|
|
5959
|
+
return returnedStatus;
|
|
3145
5960
|
} catch (e: any) {
|
|
3146
5961
|
return { success: false, error: e.message };
|
|
3147
5962
|
}
|
|
@@ -3199,7 +6014,7 @@ export class DaemonCommandRouter {
|
|
|
3199
6014
|
|
|
3200
6015
|
// 3. Kill OS process if requested
|
|
3201
6016
|
if (killProcess) {
|
|
3202
|
-
const running = isIdeRunning(ideType);
|
|
6017
|
+
const running = await isIdeRunning(ideType);
|
|
3203
6018
|
if (running) {
|
|
3204
6019
|
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
|
|
3205
6020
|
const killed = await killIdeProcess(ideType);
|